NEAR zero to hero: Building a Smart Contract in Rust

This is the first of NEAR Nigeria zero to hero series. The goal of this series is to transform a programmer with no experience in developing decentralised applications to a skilled NEAR developer. Learn more about this series here.

This tutorial is meant to provide easy on-boarding to Rust and smart contract development. You will set up a Smart Contract development environment from the ground up and compile, test, and run your first Rust smart contract on NEAR.

Introduction

Writing smart contracts can be a paradigm shift. A few new concepts including state, transfer, account and balance information are used, toward building full-fledged applications on the blockchain.

The example presented in this tutorial is a simple smart contract that serves as a Counter, incrementing, decrementing, and returning the counter value.

Prerequisites​

To complete this tutorial successfully, you will need:

  • Rust toolchain
  • A NEAR account
  • NEAR command-line interface (near-cli)

Setting up the requirements​

In this section, you will learn how to install and set up the basic tools to create smart contracts in Rust. Along with the Rust environment, you will create a NEAR account and install the near-cli.

Installing the Rust toolchain

The following instructions are taken from the official Rust installation guide. If you already have the Rust toolchain, you can skip these steps.

  1. Install Rustup
  • Open a terminal and run

curl — proto ‘=https’ — tlsv1.2 -sSf https://sh.rustup.rs | sh

  • Wait for the command to finish running

2. Configure your current shell

  • Run

source $HOME/.cargo/env

#note: alternatively you can simply relaunch your terminal window

3. Add wasm target to your toolchain

Run

rustup target add wasm32-unknown-unknown

Creating a NEAR account

The easiest way to create an account on NEAR is using the NEAR Wallet. NEAR has several development networks operating independently of each other with their own accountIDs. For this example, you will create a new testnet account.

#note: If you already have a NEAR testnet account, you can skip these steps.

  1. Reserve an Account ID

2. Secure your account

  • Choose your account recovery method. For simplicity, in this tutorial you can select “Email Account Recovery”.

3. E-mail / Phone Number Account Recovery

  • Enter the account activation code that you received.

4. Success!

  • You just created a testnet account and received 200 Ⓝ! Upon recovery method confirmation you should be directed to your account dashboard.

Installing the near-cli

#note: If you already have the command line interface, you can skip these steps.

Linux and mac-OS

  1. Install npm and node using a package manager such as nvm.
  2. Ensure you have installed Node version 12 or above.
  3. Install near-cli globally by running:

npm install -g near-cli

Windows

Note: For Windows users, we recommend using Windows Subsystem for Linux (WSL).

  1. Install WSL [click here]
  2. Install Node.js [click here]
  3. Change npm default directory [click here]
  • This is to avoid any permission issues with WSL
  1. Open WSL and install near-cli globally by running:

npm install -g near-cli

Creating the repository​

Now that you have all the tools in place, you can create a new project repository for the smart contract using cargo. To create the repository, navigate back to your projects directory, and run the following commands:

cargo new rust-counter-tutorial — lib && cd rust-counter-tutorial

The command first creates a new directory called rust-counter-tutorial and Cargo creates its files in a directory of the same name.

If you check the rust-counter-tutorial directory, you will see that Cargo has generated two files and one directory for you: a Cargo.toml file and a src directory with a lib.rs file inside.

Creating the files

This smart contract project starts out with a simple layout like this:

.

├── Cargo.toml

└── src

└── lib.rs

Smart contracts from NEAR usually 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 deploying to the blockchain.

#note: Once you test, build, and get ready to deploy, a few more files and folders will be added.

Editing Cargo.toml

  • Open Cargo.toml in your text editor of choice. This file is in the TOML (Tom’s Obvious, Minimal Language) format, which is Cargo’s configuration format, and is similar to a package.json file.
  • Next, replace the content with the following Cargo.toml file.

[package]
name = “counter_contract”
version = “1.0.0”
authors = [“Near Inc <
hello@near.org>”]
edition = “2018”

[lib]
crate-type = [“cdylib”, “rlib”]

[dependencies]
near-sdk = “4.0.0”
uint = { version = “0.9.3”, default-features = false }

[dev-dependencies]
near-sdk-sim = “3.2.0”

[profile.release]
codegen-units = 1
opt-level = “z”
lto = true
debug = false
panic = “abort”
overflow-checks = true

Editing lib.rs

  • Open the ./src/lib.rs file in your text editor, and paste the content of the following 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].
use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
use near_sdk::{log, near_bindgen};

#[near_bindgen]
#[derive(Default, BorshDeserialize, BorshSerialize)]
pub struct Counter {
val: i8,
}

#[near_bindgen]
impl Counter {
/// Public method: Returns the counter value.
pub fn get_num(&self) -> i8 {
return self.val;
}

/// Public method: Increment the counter.
pub fn increment(&mut self) {
self.val += 1;
log!(“Increased number to {}”, self.val);
}

/// Public method: Decrement the counter.
pub fn decrement(&mut self) {
self.val -= 1;
log!(“Decreased number to {}”, self.val);
}

/// Public method — Reset to zero.
pub fn reset(&mut self) {
self.val = 0;
log!(“Reset counter to zero”);
}
}

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

// use the attribute below for unit tests
#[cfg(test)]
mod tests {
use super::*;

#[test]
fn increment() {
// instantiate a contract variable with the counter at zero
let mut contract = Counter { val: 0 };
contract.increment();
assert_eq!(1, contract.get_num());
}

#[test]
fn decrement() {
let mut contract = Counter { val: 0 };
contract.decrement();
assert_eq!(-1, contract.get_num());
}

#[test]
fn increment_and_reset() {
let mut contract = Counter { val: 0 };
contract.increment();
contract.reset();
assert_eq!(0, contract.get_num());
}

#[test]
#[should_panic]
fn panics_on_overflow() {
let mut contract = Counter { val: 127 };
contract.increment();
}

#[test]
#[should_panic]
fn panics_on_underflow() {
let mut contract = Counter { val: -128 };
contract.decrement();
}
}

  • This example uses a lib.rs file with the smart contract logic using a struct, the struct’s functions, and unit tests.

Breaking it down

Before we continue, let’s review some parts of the smart contract’s source code in ./src/lib.rs. We’ll break down the code in pieces in the next section.

Imports and initial code

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

use near_sdk::{env, near_bindgen};

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

#note: The code takes env from near-sdk-rs. This will provide a blockchain context for the smart contract execution including; the sender of a transaction, tokens sent, logging, etc.

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 {

#note: When writing smart contracts, the pattern is to have a struct with an associated impl where you write the core logic into functions.

The core logic: the struct

We declare our Counter and impl, which define 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.

As mentioned earlier, env to write logs.

Unit tests

The unit tests begin at:

mod tests {

}

and continue until the end of the lib.rs file.

Writing a test

The 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());

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 executed in the test suite.

Test & compile

In this section, you will test the smart contract, compile it, and generate a wasm release binary.

Test the code

You can easily test the smart contract code using cargo. Navigate to the “src” directory and run:

cargo test — nocapture

You should get an output like:

running 3 tests

Value after decrement: -1

Value after increment: 1

Value after reset: 0

test tests::decrement … ok

test tests::increment … ok

test tests::increment_and_reset … ok

test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Compile the code

Assuming that all the tests passed ok, you can go ahead and compile the smart contract. Navigate to “src” directory and run:

cargo build -target wasm32-unknown-unknown -release

#note: The above build command is setting a target flag to create a WebAssembly .wasm file.

Notice that your project directory now has a few additional items:

.

├── Cargo.lock ⟵ created during build to lock dependencies

├── Cargo.toml

├── src

│ └── lib.rs

└── target ⟵ created during build, holds the compiled wasm

Deploy the smart contract 🚀

With the compiled .wasm file ready, you can go ahead and deploy the smart contract. To deploy it, you will use near-cli and your testnet NEAR account.

Login with near-cli

First, use near-cli to login to the account you created earlier. In your terminal, navigate to the directory containing the Cargo.toml file and the src directory like so:

.

├── Cargo.toml

└── src

└── lib.rs

└── target

Run:

near login

A link will be shown after you execute this command. Open the link into your web browser.

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

#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.

Now that your login keys have been saved to the home directory, you can use near-cli to deploy the compiled contract to NEAR.

Deploying the contract

Finally, use near-cli to deploy the smart contract to NEAR test network:

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

#note: replace YOUR_ACCOUNT_HERE with the name of the account you created in the NEAR Wallet. For example: my-username.testnet.

Congratulations! Your smart contract is alive on the blockchain!!!

Invoking the methods

After the deployment, you are ready to invoke methods on the smart contract.

Increment

Call the increment method using near-cli:

near call YOUR_ACCOUNT_HERE increment — accountId YOUR_ACCOUNT_HERE

You should see an output like:

Scheduling a call: YOUR_ACCOUNT.testnet.increment()

Receipt: 3r9VBxypkdMVJNzbmfUd1GYz4iHjdDM7VZX7DijZ9jg3 ← may vary

Log [YOUR_ACCOUNT.testnet]: Increased number to 1

Log [YOUR_ACCOUNT.testnet]: Make sure you don’t overflow, my friend.

Transaction Id 4Cc8BAj3NiMB2Z5XBPmozKJy2dGtpJSoSaA1m7hHxRGQ ← may vary

#note: that in the above command, you are using the account name twice. A simple translation into a sentence would be:

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

Decrement

Next, call the decrement method in the same way:

near call YOUR_ACCOUNT_HERE decrement — accountId YOUR_ACCOUNT_HERE

You should see a response on your terminal:

Scheduling a call: YOUR_ACCOUNT.testnet.decrement()

Receipt: 3HiRrL4fg9Q62VV9aVfAdRk8bQ5nYEYTni9482qjPJ71 ← may vary

Log [YOUR_ACCOUNT.testnet]: Decreased number to 0

Log [YOUR_ACCOUNT.testnet]: Make sure you don’t overflow, my friend.

Transaction Id 7jVgp677evM7srG697bVWErErzieBWExLvkSiBfvZ8YC ← may vary

Check counter value

To check the current counter value, call the get_num method:

near view YOUR_ACCOUNT_HERE get_num — accountId YOUR_ACCOUNT_HERE

The method should provide a response:

View call: YOUR_ACCOUNT.testnet.get_num()

0

Next steps

This example is as bare bones as it gets, but illustrates all the moving parts associated with writing a smart contract with Rust.

Now that you’re familiar with the build process, a natural next step is to learn how to build a full-stack NEAR decentralized application. In the next series you will learn how to build a full-stack NEAR decentralized application.

Versioning for this article

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

  • cargo: cargo 1.53.0 (4369396ce 2021–04–27)
  • rustc: rustc 1.53.0 (53cb7b09b 2021–06–17)
  • near-cli: 2.1.0

--

--

Near Protocol Nigeria (NPK Guild)

NPK Guild is a Near Protocol Community based in Nigeria, aimed at educating and incentivising young Nigerians onBlockchain technology and the NEAR ECOSYSTEM