Smart Contract: Build a Fullstack DAPP  on NEAR using Rust, Near-API-Js,  React & Chakra-UI

Smart Contract: Build a Fullstack DAPP on NEAR using Rust, Near-API-Js, React & Chakra-UI

·

12 min read

Introduction

As we may already know, the web is evolving, with the existence of blockchain, decentralized applications (dApp), decentralized autonomous organization (DAO), and web3. The NEAR blockchain is one of several blockchain networks available.

The NEAR blockchain is a layer one, sharded, proof-of-stake blockchain that prioritizes usability and performance for both developers and users. Smart contracts written in AssemblyScript, JavaScript, or Rust can interact with the NEAR blockchain. More information is available in the official documentation.

Building a NEAR Quote dApp

In this article, we will create a Quote dApp by writing the NEAR smart contract in Rust with the goal of interacting with and changing the state of a smart contract on the NEAR blockchain.

This project will be divided into two parts:

  1. A smart contract - is a contract that stores and retrieves data on the blockchain.

  2. The frontend - The interface through which you interact with the smart contract.

You will have a basic understanding of how to build a decentralized Quote application on the NEAR Blockchain by the end of this article.

Prerequisites

  1. Node.js ≥ 12 installed on your machine.

  2. Basic understanding of Rust

  3. Basic knowledge of React

  4. A NEAR testnet account

  5. Comfortable using terminal

Smart Contract Tech Stack

The following will be used as a tech stack to build out a functional smart contract:

  • rustup toolchain

  • near-cli

Setting up the project

For Windows, add the Windows Subsystem for Linux by following the instructions here (WSL 2).

Use the following command to install Rust on Linux or MacOS.

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

Run the following command after successfully installing rustup to configure your current shell:

source $HOME/.cargo/env

Following that, we must add the wasm target to the toolchain, which Rust uses to produce WebAssembly as an output. In your terminal, enter the following command:

rustup target add wasm32-unknown-unknown

Getting started with NEAR

The smart contract lives on the NEAR network and can be accessed via exposed methods.

To build on NEAR with Rust, you must understand the following programming concepts:

  • Modules

  • Collections

  • Data Types

  • Structures

Create the Project Structure

Use the code block below in the terminal to generate the project structure:

npx create-near-app near-dapp --contract rust --frontend react

To get you started, the above command will create the entire folder structure, which will include both the frontend and the contract.

Launch your preferred IDE and navigate to the project folder. Go to the /contract/src/lib.rs directory. That is the smart contract entry file. There is an example smart contract there. We'll clean that up and create the Quote dApp's entire smart contract structure.

Before we get into the nitty-gritty of creating the smart contract's state and functions, we should review some programming concepts mentioned earlier to better understand the flow. The following are examples:

Modules

Modules are used in NEAR to reuse third-party libraries and to help organize your code. The NEAR SDK, which offers a lot for your contract, is the main module used in NEAR.

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

The code block above demonstrates how to use the NEAR SDK. It contains everything required to create a working smart contract. For example, BorshDeserialize, BorshSerialize, near_bindgen, env, and so on.

Collections

Depending on the use case, collections are used to manage large data structures. The NEAR SDK collections store data in persistent storage. To gain access to an element, simply deserialize it. It also has pagination. More on Collections can be found here.

use near_sdk::collections::UnorderedMap;

#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize, PanicOnDefault)]
pub struct Contract {
  pub history: UnorderedMap<AccountId, String>,  
}

Data Types

NEAR smart contracts can also use primitive data types. Several examples can be found in the code block below:

u8, u16, u32, u64, u128, i8, i16, i32, i64, i128, String, &str, Vec<T>, ...

Structures

The Struct in a NEAR smart contract represents the state of the contract, upon which the structure is implemented and functions are created to either mutate or read data from the contract. The keyword pub signifies the named struct is public.

#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize)]
pub struct Contract {
  pub account_id: String, 
  pub total_amount: U128,
}

#[near_bindgen] 
impl Contract {
// functions here
}

near bindgen is an important macro in the preceding code block that is used on a struct and the implementation of functions to generate the necessary code to be a valid NEAR contract and expose the intended functions to be able to be called externally.

Create a NEAR Account

Moving forward, we must create two accounts for our project. The first is a top-level account, and the second is a sub-account. Here's an example of what we're talking about:

  1. ogenienis.testnet - A top level account

  2. quote.ogenienis.testnet - A sub-account

The first account will be used to create the second.

Create an account at NEAR Testnet Wallet. If you already have one, you can skip this step.

To access your NEAR account, copy and paste the snippet below into your terminal. Make sure you have the near-cli installed:

yarn install --global near-cli
near login

You should be redirected to your browser to authenticate your terminal access.

Create a sub-account

Create a sub-account using the command below; this account will be used to tip an author.

near create-account quote.ogenienis.testnet --masterAccount ogenienis.testnet --initialBalance 10

sub-account

Creating the Contract's State

In this section, we will create the contract's state.

The first approach is to use the NEAR SDK to create named structs.

use near_sdk::collections::UnorderedMap;
use near_sdk::json_types::U128;
use near_sdk::Promise;
use near_sdk::{
    borsh::{self, BorshDeserialize, BorshSerialize},
    env, near_bindgen,
    serde::{Deserialize, Serialize},
    AccountId, PanicOnDefault,
};

pub type QuoteId = String; // custom type

#[derive(BorshSerialize)] 
pub enum StorageKey {
    QuoteId,
}

#[derive(Serialize, Deserialize)]
#[serde(crate = "near_sdk::serde")]
pub struct JsonQuoteData {
    pub id: QuoteId,
    pub text: String,
    pub author: AccountId,
    pub tip: String,
    pub tip_count: u64,
    pub created_at: u64,
}

#[derive(BorshDeserialize, BorshSerialize)]
pub struct QuoteData {
    pub id: QuoteId,
    pub text: String,
    pub author: AccountId,
    pub tip: String,
    pub tip_count: u64,
    pub created_at: u64,
}

#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize, PanicOnDefault)]
pub struct Quote {
    owner_id: AccountId,
    quotes: UnorderedMap<QuoteId, QuoteData>,
}

The code block above illustrates the structure of the contract as well as the modules required for the contract to function properly.

The following modules were imported from the NEAR SDK:

  • #[derive(BorshSerialize, BorshDeserialize)] - Used to Serialize and Deserialize a data structure

  • #[derive(Serialize, Deserialize)] - A data structure can be deserialized and serialized from any data format supported by Serde.

  • #[serde(crate = "near_sdk::serde")] - Serde is a framework for serializing and deserializing data structures in Rust efficiently and also supports JSON.

  • #[derive(PanicOnDefault)] - PanicOnDefault is used to make the contract panic when the contract is not initialized.

The Structure used in the contract :

  • QuoteId - This is a named custom type that is set to String.

  • StorageKey - This an enum used to hold QuoteId.

  • JsonQuoteData - This is a name struct used to return data as JSON from certain function calls.

  • QuoteData - This is a name struct that holds the data for each quote created in the contract.

  • Quote - This is a name struct that holds the state of the contract. The owner_id holds who deployed the contract quotes holds all the quotes entered into the contract.

Read and Mutate State Functions

In this section, we will write functions that will interact with our contract's state. We need to create two types of functions:

  • call function - These are functions that can mutate/change the state of our contract

  • view function - These are functions that can read the state of our contract

All functions are to be placed within the implementation block:

impl Quote {
...
}

Initialize Contract State

The function new is used to initialize the contract's state.

#[near_bindgen]
impl Quote {
    #[init]
    pub fn new(owner_id: AccountId) -> Self {
        Self {
            owner_id,
            quotes: UnorderedMap::new(StorageKey::QuoteId.try_to_vec().unwrap()),
        }
    }
}

The #[init] initialization function can only be called once. The smart contract's owner will initialize the smart contract's state on the NEAR blockchain for the first time.

Create Quote Function

create quote will be the next function added. This function is in charge of adding new quotes to the smart contract. Using the following code:

  pub fn create_quote(&mut self, author: AccountId, text: String) -> JsonQuoteData {
        let quote_id = format!("QUO-{}", (&env::block_timestamp()));

        self.quotes.insert(
            &quote_id,
            &QuoteData {
                id: quote_id.clone(),
                text: text.clone(),
                author: author.clone(),
                tip: "1000000000000000000000000".to_string(),
                tip_count: 0,
                created_at: env::block_timestamp(),
            },
        );

        JsonQuoteData {
            id: quote_id,
            text,
            author,
            tip: "1000000000000000000000000".to_string(),
            tip_count: 0,
            created_at: env::block_timestamp(),
        }
    }

The code block above shows how to create a new quote. After a new quote is created, the data is returned in JSON format.

Fetch Quotes

The function fetch_quotes is used to retrieve all quotes from the smart contract. The function accepts two arguments, from_index takes in a String of "1" and limit takes in a type of u64. This is mainly for pagination if the data present isn't fixed.

  pub fn fetch_quotes(
        &self,
        from_index: Option<U128>,
        limit: Option<u64>,
    ) -> Vec<JsonQuoteData> {
        let start_index: u128 = from_index.map(From::from).unwrap_or_default();
        assert!(
            (self.quotes.len() as u128) > start_index,
            "Error: Out of bounds, start your search with a smaller from_index."
        );

        let limit = limit.map(|v| v as usize).unwrap_or(usize::MAX);
        assert_ne!(limit, 0, "Provide limit higher than 0.");

        self.quotes
            .iter()
            .skip(start_index as usize)
            .take(limit)
            .map(|(id, quote)| JsonQuoteData {
                id: id.to_string(),
                text: quote.text,
                author: quote.author,
                tip: quote.tip,
                tip_count: quote.tip_count,
                created_at: quote.created_at,
            })
            .collect()
    }

Total Quote

The function total_quotes returns the total number of quotes available in the contract.

  pub fn total_quotes(&self) -> U128 {
        U128(self.quotes.len() as u128)
    }

Tip Author

When the Tip function is called, the value 1NEAR is transferred to the author of the quote. We pass the quote id as an argument, and it retrieves the quote. The function also calls the tip_count function, which increases the tip_count value by one each time the tip_author function is called by people who tipped the author. Because tokens are transferred, this is also a #[payable] function.

 #[payable]
    pub fn tip_author(&mut self, id: QuoteId) -> String {
        match self.quotes.get(&id) {
            Some(ref mut quote) => {
                let tip = quote.tip.parse().unwrap();

                assert_eq!(
                    env::attached_deposit(),
                    tip,
                    "attached deposit should be equal to the price of the product"
                );

                let owner = &quote.author.as_str();

                Promise::new(owner.parse().unwrap()).transfer(tip);


                self.quotes.insert(
                    &quote.id,
                    &QuoteData {
                        id: quote.id.clone(),
                        text: quote.text.clone(),
                        author: quote.author.clone(),
                        tip: quote.tip.clone(),
                        tip_count: quote.tip_count,
                        created_at: quote.created_at,
                    },
                );
                self.tip_count(id.clone());

                format!("Tip sent")
            }
            _ => {
                env::panic_str("product not found");
            }

        }
    }

    pub fn tip_count(&mut self, id: QuoteId) {
        let mut get_quote = self.quotes.get(&id).expect("Not Found");
        get_quote.tip_count += 1;

        self.quotes.insert(
            &get_quote.id,
            &QuoteData {
                id: get_quote.id.clone(),
                text: get_quote.text.clone(),
                author: get_quote.author.clone(),
                tip: get_quote.tip.clone(),
                tip_count: get_quote.tip_count,
                created_at: get_quote.created_at,
            },
        );

    }

Delete Quote

To delete a quote, the function delete quote accepts an argument id and uses the assert eq! method to determine whether the user calling the function is the author of the quote. This function can only be called by the author of the quote.

  pub fn delete_quote(&mut self, id: QuoteId) -> String {
        assert_eq!(
            env::predecessor_account_id(),
            self.owner_id,
            "Only owner can delete this quote",
        );

        let quote = format!("{} was deleted successfully", id.clone());
        self.quotes.remove(&id);
        quote
    }

Build, Compile and Deploy Smart Contract

Now that we've implemented the contract functions, run the following command in your terminal to build and compile the smart contract:

yarn deploy

After running the command, you should see the following output in the terminal:

Deployed Contract

If you see the above output in your terminal, it means that the deployment was successful and the contract was deployed to dev-1674296775418-58495141920439. This applies only to this contract; yours will be different. Inspect your project folders neardev/dev-account to ensure you have the correct contract name. It should be there. The compiled contract can be found in out/main.wasm.

You can view the deployed contract's transaction here.

Making Contract Calls

As previously stated, there are two ways to interact with a smart contract in NEAR:

  • call function

  • view function

Syntax

near <CONTRACT-CALL-TYPE> <CONTRACT-NAME> <FUNCTION-NAME> <PAYLOAD> --accountId=<ACCOUNT-ID>
  • <CONTRACT-CALL-TYPE>: This will be either a call or view type.

  • <CONTRACT-NAME>: The name of your deployed contract.

  • <FUNCTION-NAME>: The name in the smart contract.

  • <PAYLOAD>: The data you pass in as an argument to the function you are calling. This is optional when <CONTRACT-CALL-TYPE> is view.

  • <ACCOUNT-ID>: The name of your NEAR Account.

Usage

In this section, we will interact with the smart contract that we successfully deployed to the NEAR Blockchain.

To initialize the state of our smart contract, we will call the new function.

Replace dev-1674296775418-58495141920439 with yours in neardev/dev-account and ogenienis.testnet with your account Id.

New

The function new is used to initialize the smart contract's state. This function accepts an argument owner id, which is the smart contract's owner.

near call dev-1674296775418-58495141920439 new '{"owner_id": "ogenienis.testnet"}' --accountId=ogenienis.testnet

The transaction can be found on the blockchain explorer.

Initialize the contract

Create Quote

Use the following code to create a quote:

 near call dev-1674296775418-58495141920439 create_quote '{"author": "ogenienis.testnet", "text": "Fortune favours the bold"}' --accountId=ogenienis.testnet

Create Quote

Fetch Quotes

Use the code block below to view the quotes we've stored in the contract's state:

near view dev-1674296775418-58495141920439 fetch_quotes '{"from_index": "0", "limit": 20}' --accountId=ogenienis.testnet

Fetch quotes

Total Quotes

The total number of quotes within the contract is returned by this function.

near view dev-1674296775418-58495141920439 total_quotes  --accountId=ogenienis.testnet

Total Quotes

Tip Author

The sub-account we previously set up needs to be used to tip the author of a quote. Also, 1NEAR is attached to the function call, which is 10^24 Yocto. Yocto is the smallest denomination of the NEAR Token.

near call dev-1674296775418-58495141920439 tip_author '{"id": "QUO-1674297436602231522"}' --depositYocto=1000000000000000000000000 --accountId=quote.ogenienis.testnet

Tip Author

By using the fetch_quote function, you can see the total number of tips for the quote you just tipped.

Tip count

Delete Quote

The quote is selected and removed from the state of the contract using the delete quote function, which accepts the argument id as a payload.

near call dev-1674296775418-58495141920439 delete_quote '{"id": "QUO-1674252113803434847"}' --accountId=ogenienis.testnet

The result of the removed quote is shown in the image below.

Deleted Quote

Call fetch quotes and check to see if the deleted quote is still present to be certain.

With that, we successfully deployed a functional smart contract to the NEAR Testnet blockchain. 🚀 🚀

In the next part of this tutorial, we will create a user interface and connect the frontend to interact with the deployed smart contract on the NEAR blockchain.

Build a Fullstack DAPP on NEAR using Rust, Near-API-Js, React & Chakra-UI: Frontend

Stay tuned!

Conclusion

In this article, we explored how to build a smart contract on the NEAR blockchain using Rust. We created the state of the contract and the functions to interact with the contract on the NEAR blockchain. Explore the contract's source code on my GitHub Repository.