NEAR Documentation
  • Concepts
  • Develop
  • Tools
  • Tutorials
  • Community
  • Tokens
  • Stake
  • Integrate
  • GitHub

›Rust

General

  • Create a Transaction
  • Ledger Connection
  • Figment Tutorials

Front-End

  • API Examples
  • API Workshop

Smart Contracts

    AssemblyScript

    • AssemblyScript Workshop
    • Issue a Token
    • Cross Contract Calls

    Rust

    • Intro to Rust

Videos

  • Accounts & keys
  • Application Reviews
  • Contract Reviews
  • Misc
Edit

An Introduction to Rust Smart Contracts

Writing smart contracts is a paradigm shift. There are only a few new concepts (state, transfer, account/balance information…) used, but they go a long way toward building full-fledged applications on the blockchain. This way of thinking has its own learning curve. Currently, the preferred programming language for writing smart contracts on NEAR is Rust. On top of learning smart contracts, developers unfamiliar with the Rust programming language may have an additional barrier to entry. This page is meant to provide an easy onboarding to Rust and smart contract development.

The example shown here will be a simple smart contract that serves as a counter, incrementing, decrementing, and returning the counter value. There is no previous Rust development required for this example. Folks familiar with the language may choose to jump straight into the examples located at near.dev.

For those who won't wish to dive into the deep end, consider this page a safe "wading pool" with no diving signs posted.

3-Step Rust Installation

1. Install Rustup

run curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

(Taken from official installation guide)

2. Configure your current shell

run source $HOME/.cargo/env

(alternatively you can simply relaunch your terminal window)

3. Add wasm target to your toolchain

run rustup target add wasm32-unknown-unknown

Why unknown-unknown?

Resources

The online book from the official Rust site is a great resource to start with. It's recommended to read Chapter 1 before continuing, especially the cargo section.

Getting started

Some smart contract examples from NEAR have a primary file that holds the code: /src/lib.rs. This is the conventional filename for a Rust library. Libraries will work great for compiling into WebAssembly and deployed the blockchain.

The example shown here will use a lib.rs file with smart contract logic using a struct, the struct's functions, and unit tests. This will all be in one file for this simple example. As developers build more complex smart contracts, it's a good idea to organize code the Rust way.

We'll break down the code in pieces below. If you wish to preview the complete code, please expand the bullets below.

Full example files

Full Cargo.toml file

[package]
name = "rust-counter-tutorial"
version = "0.1.0"
authors = ["Near Inc <hello@near.org>"]
edition = "2018"

[lib]
crate-type = ["cdylib", "rlib"]

[dependencies]
near-sdk = "2.0.0"

[profile.release]
codegen-units = 1
# Tell `rustc` to optimize for small code size.
opt-level = "z"
lto = true
debug = false
panic = "abort"
# Opt into extra safety checks on arithmetic operations https://stackoverflow.com/a/64136471/249801
overflow-checks = true

Full src/lib.rs file

//! This contract implements simple counter backed by storage on blockchain.
//!
//! The contract provides methods to [increment] / [decrement] counter and
//! [get it's current value][get_num] or [reset].
//!
//! [increment]: struct.Counter.html#method.increment
//! [decrement]: struct.Counter.html#method.decrement
//! [get_num]: struct.Counter.html#method.get_num
//! [reset]: struct.Counter.html#method.reset

use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
use near_sdk::{env, near_bindgen};

#[global_allocator]
static ALLOC: near_sdk::wee_alloc::WeeAlloc = near_sdk::wee_alloc::WeeAlloc::INIT;

// add the following attributes to prepare your code for serialization and invocation on the blockchain
// More built-in Rust attributes here: https://doc.rust-lang.org/reference/attributes.html#built-in-attributes-index
#[near_bindgen]
#[derive(Default, BorshDeserialize, BorshSerialize)]
pub struct Counter {
    // See more data types at https://doc.rust-lang.org/book/ch03-02-data-types.html
    val: i8, // i8 is signed. unsigned integers are also available: u8, u16, u32, u64, u128
}

#[near_bindgen]
impl Counter {
    /// Returns 8-bit signed integer of the counter value.
    ///
    /// This must match the type from our struct's 'val' defined above.
    ///
    /// Note, the parameter is `&self` (without being mutable) meaning it doesn't modify state.
    /// In the frontend (/src/main.js) this is added to the "viewMethods" array
    /// using near-cli we can call this by:
    ///
    /// ```bash
    /// near view counter.YOU.testnet get_num
    /// ```
    pub fn get_num(&self) -> i8 {
        return self.val;
    }

    /// Increment the counter.
    ///
    /// Note, the parameter is "&mut self" as this function modifies state.
    /// In the frontend (/src/main.js) this is added to the "changeMethods" array
    /// using near-cli we can call this by:
    ///
    /// ```bash
    /// near call counter.YOU.testnet increment --accountId donation.YOU.testnet
    /// ```
    pub fn increment(&mut self) {
        // note: adding one like this is an easy way to accidentally overflow
        // real smart contracts will want to have safety checks
        self.val += 1;
        let log_message = format!("Increased number to {}", self.val);
        env::log(log_message.as_bytes());
        after_counter_change();
    }

    /// Decrement (subtract from) the counter.
    ///
    /// In (/src/main.js) this is also added to the "changeMethods" array
    /// using near-cli we can call this by:
    ///
    /// ```bash
    /// near call counter.YOU.testnet decrement --accountId donation.YOU.testnet
    /// ```
    pub fn decrement(&mut self) {
        // note: subtracting one like this is an easy way to accidentally overflow
        // real smart contracts will want to have safety checks
        self.val -= 1;
        let log_message = format!("Decreased number to {}", self.val);
        env::log(log_message.as_bytes());
        after_counter_change();
    }

    /// Reset to zero.
    pub fn reset(&mut self) {
        self.val = 0;
        // Another way to log is to cast a string into bytes, hence "b" below:
        env::log(b"Reset counter to zero");
    }
}

// unlike the struct's functions above, this function cannot use attributes #[derive(…)] or #[near_bindgen]
// any attempts will throw helpful warnings upon 'cargo build'
// while this function cannot be invoked directly on the blockchain, it can be called from an invoked function
fn after_counter_change() {
    // show helpful warning that i8 (8-bit signed integer) will overflow above 127 or below -128
    env::log("Make sure you don't overflow, my friend.".as_bytes());
}

/*
 * the rest of this file sets up unit tests
 * to run these, the command will be:
 * cargo test --package rust-counter-tutorial -- --nocapture
 * Note: 'rust-counter-tutorial' comes from cargo.toml's 'name' key
 */

// use the attribute below for unit tests
#[cfg(test)]
mod tests {
    use super::*;
    use near_sdk::MockedBlockchain;
    use near_sdk::{testing_env, VMContext};

    // part of writing unit tests is setting up a mock context
    // in this example, this is only needed for env::log in the contract
    // this is also a useful list to peek at when wondering what's available in env::*
    fn get_context(input: Vec<u8>, is_view: bool) -> VMContext {
        VMContext {
            current_account_id: "alice.testnet".to_string(),
            signer_account_id: "robert.testnet".to_string(),
            signer_account_pk: vec![0, 1, 2],
            predecessor_account_id: "jane.testnet".to_string(),
            input,
            block_index: 0,
            block_timestamp: 0,
            account_balance: 0,
            account_locked_balance: 0,
            storage_usage: 0,
            attached_deposit: 0,
            prepaid_gas: 10u64.pow(18),
            random_seed: vec![0, 1, 2],
            is_view,
            output_data_receivers: vec![],
            epoch_height: 19,
        }
    }

    // mark individual unit tests with #[test] for them to be registered and fired
    #[test]
    fn increment() {
        // set up the mock context into the testing environment
        let context = get_context(vec![], false);
        testing_env!(context);
        // instantiate a contract variable with the counter at zero
        let mut contract = Counter { val: 0 };
        contract.increment();
        println!("Value after increment: {}", contract.get_num());
        // confirm that we received 1 when calling get_num
        assert_eq!(1, contract.get_num());
    }

    #[test]
    fn decrement() {
        let context = get_context(vec![], false);
        testing_env!(context);
        let mut contract = Counter { val: 0 };
        contract.decrement();
        println!("Value after decrement: {}", contract.get_num());
        // confirm that we received -1 when calling get_num
        assert_eq!(-1, contract.get_num());
    }

    #[test]
    fn increment_and_reset() {
        let context = get_context(vec![], false);
        testing_env!(context);
        let mut contract = Counter { val: 0 };
        contract.increment();
        contract.reset();
        println!("Value after reset: {}", contract.get_num());
        // confirm that we received -1 when calling get_num
        assert_eq!(0, contract.get_num());
    }
}

This example project starts out very simple:

.
├── Cargo.toml
└── src
   └── lib.rs

Once we test, build, and get ready to deploy, a few more files and folders will be added here.

Versioning for this example

At the time of this writing, this example works with the following versions:

  • cargo: cargo 1.49.0 (d00d64df9 2020-12-05)
  • rustc: rustc 1.49.0 (e1884a8e3 2020-12-29)
  • near-cli: 1.5.3 (we'll explain near-cli later)

Breaking it down

Imports and initial code near the top

use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
use near_sdk::{env, near_bindgen};

#[global_allocator]
static ALLOC: near_sdk::wee_alloc::WeeAlloc = near_sdk::wee_alloc::WeeAlloc::INIT;

At the top of this file we have the standard imports. The packages that follow the use statement can be found as dependencies in Cargo.toml. All the imports involving serialization will be used to bundle our code/storage so that it's ready for the blockchain.

Note that we're taking env from near-sdk-rs. This will provide a similar concept to context as seen in other blockchains. (Example: the sender of a transaction, tokens sent, logging, etc…)

Last, the reference to wee_alloc is a way to optimize memory management. This section is rather boilerplate, but worth briefly mentioning.

Below are some snippets from the lib.rs file:

#[near_bindgen]
#[derive(Default, BorshDeserialize, BorshSerialize)]
pub struct Counter {
    val: i8, // i8 is signed. unsigned integers are also available: u8, u16, u32, u64, u128
}

#[near_bindgen]
impl Counter {    
  … 

When writing smart contracts, the pattern is to have a struct with an associated impl where we write the core logic into functions. It's actually common in Rust to have this pattern elsewhere.

"…most functions will end up being inside impl blocks…" - Rust docs

Our core logic: the struct

We declare our Counter and impl, defining the functions we'll be invoking on the blockchain.

Above the definitions we see attributes specific to NEAR:

#[near_bindgen]
#[derive(Default, BorshDeserialize, BorshSerialize)]

These essentially allow the compilation into WebAssembly to be compatible and optimized for the NEAR blockchain.

We use the context env to write logs, as mentioned earlier.

env can return very useful information including:
  • signer_account_id - the account id of that signed the original transaction that led to this execution
  • attached_deposit - if someone sent tokens along with the call
  • account balance - the balance attached to the given account
  • and more…

More info available here.

Unit tests

The unit tests begin at:

mod tests {
  …
}

and continue until the end of the lib.rs file. The code here is fairly boilerplate. The custom unit test code comes into play here:

let mut contract = Counter{ val: 0 };
contract.increment();
// confirm that we received 1 when calling get_num
println!("Value after increment: {}", contract.get_num());
assert_eq!(1, contract.get_num());

Notice the naming convention of variables here. There's a lot of snake_case and variables prefixed with an underscore. Upon building a project, Rust informs you if naming conventions are off. It's actually quite easy to write proper Rust code using the compiler's suggestions.

You may add as many tests as you need following the pattern in this file. Similar to unit tests in other languages and frameworks, just add the attribute:

#[test]

above the block of code to have it run in the suite of tests.


Finally: test, compile, and deploy 🚀

We're going to need two things to deploy this contract.

  1. a NEAR account, created with Wallet
  2. near-cli installed according to these instructions

Please use the links above if you haven't already, as we'll need those for deploying the smart contract.

Test the code

cargo test -- --nocapture

Compile the code

env 'RUSTFLAGS=-C link-arg=-s' cargo build --target wasm32-unknown-unknown --release

Windows users: please modify the above command as:

set RUSTFLAGS=-C link-arg=-s
cargo build --target wasm32-unknown-unknown --release

The above command is essentially setting special flags and optimizing the resulting .wasm file. At the end of the day it's simply a customized cargo build --release command that should look familiar from Chapter 1.

Login with near-cli

We're going to use near-cli to login to our account created earlier at the Wallet site. In your command prompt, navigate to the directory containing the Cargo.toml file. (It also contains the src directory.)

Note: The default network for near-cli is testnet. If you would like to change this to mainnet or betanet, please see near-cli network selection for instructions.

near login

A link will be output after this command is run. Copy/paste the link into a browser. (Or on OS X, hold Command and click the link if your Terminal application allows it.)

Follow the instructions in Wallet to authenticate your account, then head back to Terminal to complete the final step confirming the account name.

Notice that our project directory now has a few new items:

.
├── Cargo.lock  ⟵ created during build to lock dependencies
├── Cargo.toml
├── src
│  └── lib.rs
└── target      ⟵ created during build, holds the compiled wasm

Now that our keys from logging in have been saved to the home directory. In Linux and OS X this will be ~/.near-credentials. NEAR CLI looks for keys in that directory, allowing us to deploy the compiled contract to NEAR.

In the following steps, please replace YOUR_ACCOUNT_HERE with the name of the account created in the NEAR Wallet.

Deploying

near deploy --wasmFile target/wasm32-unknown-unknown/release/rust_counter_tutorial.wasm --accountId YOUR_ACCOUNT_HERE

Invoking

We'll use near-cli to invoke methods on our smart contract.

Increment:

near call YOUR_ACCOUNT_HERE increment --accountId YOUR_ACCOUNT_HERE

Note that in the above command, we use the account name twice. If we were to translate this into a sentence it would be:

Please call the contract deployed to NEAR account X. On that contract is a method called "increment" that takes no additional arguments. Oh, and we happen to be calling this contract using keys from account X, too.

Contract methods can be called from other NEAR accounts quite easily. Please see the examples page for more information.

Next, call the "decrement" method in the same fashion:

Decrement:

near call YOUR_ACCOUNT_HERE decrement --accountId YOUR_ACCOUNT_HERE

Check counter:

near view YOUR_ACCOUNT_HERE get_num --accountId YOUR_ACCOUNT_HERE

The smart contract is alive on the blockchain!

This example is as bare bones as it gets, but illustrates all the moving parts associated with writing a smart contract with Rust. Admittedly, it's a poor example when it comes to creating anything user-facing.

Now that we're familiar with the build process, a natural next step might be to check out create-near-app. This project includes another Rust smart contract but has an interface. With create-near-app many of the steps we performed on the command line are wrapped neatly into build scripts.

Read more about create-near-app or try it out now by running:

npx create-near-app --contract=rust new-awesome-app

Follow the instructions to set up a simple Rust smart contract with a React front-end. Happy coding!

Troubleshooting

If near commands return an error containing:

Cannot deserialize the contract state.

Please see this StackOverflow post.

Last updated on 2/24/2021 by Mike Purvis
← PreviousAccounts & keys →
  • 3-Step Rust Installation
    • 1. Install Rustup
    • 2. Configure your current shell
    • 3. Add wasm target to your toolchain
  • Resources
  • Getting started
  • Full example files
  • Versioning for this example
  • Breaking it down
    • Imports and initial code near the top
    • Our core logic: the struct
    • Unit tests
  • Finally: test, compile, and deploy 🚀
    • Test the code
    • Compile the code
    • Login with near-cli
    • Deploying
    • Invoking
  • Troubleshooting
  • Wallet
  • Explorer
  • Examples
  • Forum
  • Twitter
  • GitHub
  • Discord
  • Telegram
  • WeChat
  • YouTube

Developers

  • Overview
  • Technology
  • GitHub
  • Bounties
  • Grants

Community

  • Forum
  • Events
  • Contributor Program
  • Guild Program
  • Startup Accelerator
  • Bounties
  • Tokens

About

  • Team
  • Careers
  • Backers
  • Press Kit
  • Brand Guidelines
  • Privacy Policy

2020 NEAR Protocol|All rights reserved|hello@near.org|Privacy Policy