How to write Gear smart contract for Rust beginners

Posted by MasumX on Sat, 26 Feb 2022 14:14:15 +0100

For the pre knowledge of Gear contract, you can first understand this article: Gear contract disclosure.

This article will mainly explain how to use Rust to create a simple decentralized application on the Gear blockchain network.

We take a voting application as an example to study the infrastructure of Gear smart contract, and learn how to use the Actor model architecture of the program, process messages, and interact with states.

This article aims to demonstrate how easy and convenient it is to create applications on the Gear platform.

Let's start with Rust

Rust is a multi paradigm programming language that focuses on security and performance. Its construction takes into account speed and efficiency, and provides zero cost abstraction and functional features. For many developers, it solves common problems with other underlying languages, such as c and c + +.

About why Gear uses Rust, please see this article: Why does Gear use Rust?

In addition, rust has a significant advantage: the trust code can be compiled into wasm.

So let's start installing Rust on your computer.

First, open your favorite terminal and run the installer:

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

Let's install the toolchain to compile the code:

rustup toolchain add nightly
rustup target add wasm32-unknown-unknown --toolchain nightly
cargo install --git https://github.com/gear-tech/gear wasm-proc

Everything is ready. It's time to start our first program!

Create a Rust program

Let's create a voting application project with the help of the cargo command:

cargo new voting-app --lib

Look at the project structure:

voting-app/
├── Cargo.toml
└── src
     └── lib.rs

Cargo.toml is the project list in Rust, which contains the metadata required to compile the project and some necessary dependent libraries:

[package]
name = "voting-app"
version = "0.1.0"
authors = ["Gear Technologies"]
edition = "2018"
license = "GPL-3.0"

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

[dependencies]
gstd = { git = "https://github.com/gear-tech/gear", features = ["debug"] }
scale-info = { version = "1.0.0", default-features = false, features = ["derive"] }
codec = { package = "parity-scale-codec", version = "2.0.0", default-features = false, features = ["derive"] }
primitive-types = { version = "0.10.1", default-features = false, features = ["scale-info"]}


[profile.release]
lto = true
opt-level = 's'

Open Src / lib RS file, at the beginning of the file, import the Gear library file we need. Let's look at the basic structure of this program ^ [1] ^:

#![feature(const_btree_new)]
#![no_std]

// External packages (crates) and internal modules import
use codec::{Decode, Encode};
use gstd::{debug, msg, prelude::*};
use scale_info::TypeInfo;
// Init function that is executed once upon contract initialization
// Here is empty
#[no_mangle]
pub unsafe extern "C" fn init() {}

// Handle function that processes the incoming message
#[no_mangle]
pub unsafe extern "C" fn handle() {}

It is the smallest structure necessary for our application to work. The init() function is executed once during context initialization, and the Handle function is responsible for handling all incoming messages from the program.

Next, we will add a Voting structure that will contain the main code for the handler state.

#[derive(Clone)]
pub struct State {
    votes_received: BTreeMap<String, i32>,
}

impl State {
    // Create a state
    pub const fn new() -> Self {
        Self {
            votes_received: BTreeMap::new(),
        }
    }

    // Add new candidate
    pub fn add_candidate(&mut self, candidate: String) {
        self.votes_received.insert(candidate, 0);
    }

    // Vote for the candidate by name. If candidate no exist add it
    pub fn vote_for_candidate(&mut self, name: String) {
        let counter = self.votes_received.entry(name).or_insert(0);
        *counter += 1;
    }
}

// The state itself (i.e. the variable state will be accessed through)
static mut STATE: State = State::new();

We also need to define the metadata structure used to implement the input and output communication interfaces. The method described in this paper is binary mapping of interaction between different programming languages. For example, because the program is compiled into WASM format, it can only understand byte language. In order to simplify the operation, we define the data structure in advance for further encoding and decoding. To do this, we use a special macro gstd::metadata!

gstd::metadata! {
   title: "Voting App",
   handle:
       input: Action,
   state:
       input: StateAction,
       output: StateReply,
}

Now let's start processing the incoming message. Whenever our contract receives an incoming message, we will process it accordingly. Let's describe the handle() function:

#[derive(Debug, TypeInfo, Encode)]
pub enum StateReply {
   All(BTreeMap<String, i32>),
   VotesFor(i32),
}

#[derive(Debug, TypeInfo, Decode)]
pub enum StateAction {
   All,
   VotesFor(String),
}

// Handle function that processes the incoming message
#[no_mangle]
pub unsafe extern "C" fn handle() {
    let action: Action = msg::load().unwrap();

    debug!("Received action: {:?}", action);

    match action {
        Action::AddCandidate(name) => {
            STATE.add_candidate(name.clone());

            msg::reply((), 0, 0);

            debug!("Added new candidate: {:?}", name);
        }

        Action::VoteForCandidate(name) => {
            STATE.vote_for_candidate(name.clone());

            msg::reply((), 0, 0);

            debug!("Voted for: {:?}", name);
        }
    }
}

Now we can communicate with our program. Join and vote for candidates. The rest is to let our program show the names of all candidates or one person. To do this, we will use meta_ State() function, which will immediately return the state without any overhead.

// The function that returns a part of memory with a state
#[no_mangle]
pub unsafe extern "C" fn meta_state() -> *mut [i32; 2] {
   let query: StateAction = msg::load().expect("failed to decode input argument");

   let encoded = match query {
       StateAction::All => StateReply::All(STATE.votes_received.clone()).encode(),

       StateAction::VotesFor(name) => {
           let votes_for_candidate = STATE
               .votes_received
               .get(&name)
               .expect("Can't find any candidate");

           StateReply::VotesFor(votes_for_candidate.clone()).encode()
       }
   };

   let result = gstd::macros::util::to_wasm_ptr(&encoded[..]);
   core::mem::forget(encoded);

   result
}

Source file: https://github.com/gear-tech/VotingApp

Build Gear program

Our smart contract is ready! Now it needs to be compiled and uploaded to Gear blockchain. Let's start!

In the voting application folder, we compiled our smart contract:

RUSTFLAGS="-C link-args=--import-memory" cargo +nightly build --release --target=wasm32-unknown-unknown

wasm-proc --path ./target/wasm32-unknown-unknown/release/voting_app.wasm

After our application is compiled successfully, the final target file is in target / wasm32 unknown unknown / release / voting app opt. Wasm # and # target / wasm32 unknown unknown / release / voting app meta. Wasm (meta.wasm is a binary interface for interacting with javascript programs)

Other tools needed

📦 Install Polkadot JS extension

Download and install Polkadot JS browser extension: * * https://polkadot.js.org/extension/**

👛 Create account

Use Polkadot JS extension to create a new account. Don't forget to keep mnemonics and passwords in a safe place.

✉️ Upload program

  • Jump to * * Integrated development environment for polkadot ecosystem*
  • Use the Connect button to Connect to your account and allow the website to access your Polkadot JS plugin.
  • Use the get test account button to recharge your test account. This button can be pressed several times.
  • Upload the program (. opt.wasm) and metadata (. meta.wasm), give the program a meaningful name, and set the gas upper limit to 100000000. Use Polkadot JS plug-in signs the transaction.
  • Find the program on the recently uploaded program page and copy its address.

📒 Add new candidates / vote for candidates

  • Find your program in the "all programs" section and open the message sending form.
  • Add a new candidate or vote for an existing candidate
  • Set the gas limit to 300000000 and click Send request.

📒 Read status

  • Jump to read status in program page
  • Enter the candidate's name to get its number of votes, or leave it blank to view all current candidates.

In the next article, we will learn how to use the gtest library to write test cases for Gear smart contracts.

reference material

[1] Basic structure of the program: Gear program state | Gear documentation portal

About GearFans

Gear is the computing component of Boca ecology, and GearFans is the gear enthusiast community.

Topics: Blockchain