Table of Contents

February 27, 2022 5 min read

My Tryst with Building a Notes App in Rust on NEAR: Part 2 of 2

A detailed overview of how to get started with Rust

We will create a simple Create, Read, Update, Delete notes backend in Rust that utilises the on-chain storage offered by NEAR. Let us start with pasting the following code in lib.rs :

use near_sdk::{near_bindgen, env };
use near_sdk::AccountId;
use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
use near_sdk::collections::{ UnorderedMap, TreeMap, Vector};
use near_sdk::Promise; 

// 1. Main Struct

// 2. Default Implementation

// 3. Core Logic

// 4. Tests

At the top of the contract, we need to import a few code modules with the use declaration. We will expand on these portions of the near_sdk below.Next, we set up the global allocator from the wee_alloc crate using the setup_alloc!() macro. Allocators are the way that programs in Rust obtain memory from the system at runtime.

wee_alloc is a memory allocator designed for WebAssembly. It generates less than a kilobyte of uncompressed WebAssembly code. This macro is shorthand for the boilerplate code:

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

Main struct

While writing our smart contract, we will follow a pattern using one structure and an implementation. This is the most used pattern for smart contracts on Rust.

#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize)]
pub struct NotePad {
    pub status_updates: UnorderedMap<AccountId, String>,
    pub notes : UnorderedMap<AccountId,Vec<String>>,
}

Our main structure here is NotePad which has two fields status_updates and notes. I have chosen 2 fields to demonstrate a simple unordered map as well as a nested data structure. Where we have a Vec inside an Unordered Map.

Here status_updates is of the type UnorderedMap which we had imported from near_sdk::collections.  The Unordered_map utilises the blockchain trie storage in a better way and thus is considered more efficient. There are other key-value storing data structures also available with near_sdk::collections.  You can refer to the documentation of this module.

Now you would be wondering what even is [near_bindgen] or [derive(BorshDeserialize, BorshSerialize)]. Well, they are what we call attributes. So, attributes are basically a tag that is used to tell us something about the behaviours of various elements like methods, structures, classes etc.

With the [near_bindgen] macro, give our struct a generated boilerplate to make it compatible with the NEAR blockchain.

Whereas [derive(BorshDeserialize, BorshSerialize)] assists the serialization and de-serialization of the data for sending or receiving on NEAR.

Default implementation

Every type in Rust has a Default implementation but here we want to provide our own default implementation for the struct. Add the following snippet below the comment

impl Default for NotePad {
    fn default() -> Self {
        // Check incase the contract is not initialized
        env::panic(b"The contract is not initialized.")
    }
}

Core logic

Now we are going to add methods to struct. These methods are core logic for our smart contract.

#[near_bindgen]
impl Produce {
    /// Init attribute used for instantiation.
    #[init]
    pub fn new() -> Self {
        // Useful snippet,making sure state isn't already initialized
        assert!(env::state_read::<Self>().is_none(), "Already initialized");
        
        Self {
            status_updates: UnorderedMap::new(b"s".to_vec()),
            notes: UnorderedMap::new(b"w".to_vec()),
        }
    }
  
}

When creating methods, we must have an implementation block defined by the impl keyword followed by the name of the struct to be implemented. The pub keyword makes the methods publicly available, meaning they can be called by anyone with access to the protocol and a means of signing the transaction.

Self refers to the current type, which is KeyValue. Lastly, we are returning Self with a new unordered_map. While creating a new unordered_map, we must pass the ID as Vec<u8> type

so we are converting b"s" which is a byte string, to Vec<u8>, using the to_vec() function. The prefix b is used to specify that we want a byte array of the string.

In new(b"w".to_vec(), for every initialization we have to use a different letter after b.

The following methods are core logic for our smart contract.

Set_Status

 pub fn set_status(&mut self, status: String) {
        // Generic implementation 
        self.status_updates.insert(&env::predecessor_account_id(), &status);
        // Checks if the notes contains the vector associated with a given account ID
        if self.notes.get(&env::predecessor_account_id()).is_none() {
            // If there is no such entry, we initialize a new Vector and insert the status and then push the vector into the map
            let mut vec = Vec:: new();
            vec.push(status);
            self.notes.insert(&env::predecessor_account_id(), &vec);
        }
        else {
            // If the account id exists in the map, we just retrieve the vector, insert the note and insert it back again. 
            let mut vec1 = self.notes.get(&env::predecessor_account_id()).unwrap();
            vec1.push(status);
            self.notes.insert(&env::predecessor_account_id(), &vec1);
        }

    }
    

The method set_status is used to add a string note to our structs.

The method set_status is used to add a string note to our structs.

It takes two arguments  &mut self, status. We are using &mut self to borrow self mutably. You can learn more about borrowing here.

&env::predecessor_account_id() - This gets the account ID which will become the key for our unordered_map.

The status_updates can only store one note per user. And hence every time set_status function is called, the previous note would be overwritten.

Toh tackle this shortcoming we have notes. It stores a vector of strings associated with each account ID.

Delete_Status and Delete_Notes

	// Following are the generic map applications
    pub fn delete_status(&mut self) {
        self.status_updates.remove(&env::predecessor_account_id());
    }
     // Deletes notes associated to the account id through which the function is called
    pub fn delete_note(&mut self) {
        self.notes.remove(&env::predecessor_account_id());
    }

    

The above methods delete the notes associated with the particular account ID.

Get_Notes

    // Retrives the vector with the given account ID
    pub fn get_notes(&self, account_id: AccountId) -> Vec<std::string::String> {
        match self.notes.get(&account_id) {
            Some(x) => x,
            None => vec![],
        }
        // self.notes.get(&account_id).unwrap() # Converts it from optional to vector
    }
  
    // Retreives all the notes from the map
    pub fn get_updates(&self)-> Vec<Vec<std::string::String>> {
        
        let _keys = self.notes.keys_as_vector();
        let _values = self.notes.values_as_vector();
        let v1_iter = _values.iter();
        // println!("{_values:?}");
        let mut ans = Vec:: new();
        for i in v1_iter {
            ans.push(i);
        }
        ans
    }

Get_notes retrieves the notes associated with a given account ID whereas get_updates retrieves all the stored notes on the blockchain database.

Compiling the Contract

Now that we have written and tested the Rust smart contract, we will compile it into WebAssembly for deployment on NEAR.

Run the file ./build.sh

cargo build --target wasm32-unknown-unknown --release
cp target/wasm32-unknown-unknown/release/test101.wasm res

This would have generated an optimised WebAssembly file which we can deploy on NEAR.

Deploying the Contract

First you have to login into your account using near-cli.

near login

This will redirect you to the NEAR wallet requesting full access to your account. From here, select which account you would like an access key.After you click allow, you will be asked to confirm this authorization by entering the account name.

After the successful login, we will deploy the smart contract on Near testnet. Suppose the name of my testnet account is intro.testnet. Now I will create a sub account under my main account with the name notes.intro.testnet.

This sub account will be completely independent of the main account and will have separate credentials. We will initialise this account by transferring a small amount of say 20 Near from the main account.

To do so

near create-account notes.intro.testnet --masterAccount intro.testnet --initialBalance 20

near deploy --wasmFile target/wasm32-unknown-unknown/release/test101.wasm --accountId notes.intro.testnet --initFunction 'new' --initArgs '{}'

The deploy function here deploys the wasm file we generated as a result of the build command on the account notes.intro.testnet.

Interacting with the contract

Now that we have deployed our contract, we can interact with it using the NEAR CLI.

To add notes.

near call CONTRACT_ID set_status '{"status" : "Trying out writing a smart contract" }' --accountId ACCOUNT_ID

To get notes of a particular account

near call CONTRACT_ID get_notes '{ "account_id": "ACCOUNT_ID" }' --accountId ACCOUNT_ID

To get all notes

near call CONTRACT_ID get_updates --accountId ACCOUNT_ID

Lastly, we will delete the key:

near call CONTRACT_ID delete_notes --accountId ACCOUNT_ID

Conclusion

With this tutorial, we have covered the absolute basics of the NEAR smart contract programming with Rust. The delved intro the structures of the Rust smart contracts, the macros and collections offered by NEAR SDK, compiling and deploying WebAssembly and lastly interacting with our deployed Rust contracts on NEAR using the NEAR CLI. I hope this would help you with writing your first smart contract. Happy coding :)

Great! Next, complete checkout for full access to Crypto Capable.
Welcome back! You've successfully signed in.
You've successfully subscribed to Crypto Capable.
Success! Your account is fully activated, you now have access to all content.
Success! Your billing info has been updated.
Your billing was not updated.