NEAR Bindgen
near-bindgen
Rust library for writing NEAR smart contracts.
Features | Pre-requisites | Writing Rust Contract | Building Rust Contract | Running Rust Contract | Limitations and Future Work
Example
Wrap a struct in #[near_bindgen]
and it generates a smart contract compatible with the NEAR blockchain:
#[near_bindgen]
#[derive(Default, BorshDeserialize, BorshSerialize)]
pub struct StatusMessage {
records: HashMap<String, String>,
}
#[near_bindgen]
impl StatusMessage {
pub fn set_status(&mut self, message: String) {
let account_id = env::signer_account_id();
self.records.insert(account_id, message);
}
pub fn get_status(&self, account_id: String) -> Option<String> {
self.records.get(&account_id).cloned()
}
}
Features
Unit-testable. Writing unit tests is easy with
near-bindgen
:#[test] fn set_get_message() { // Use VMContext to setup gas, balance, storage usage, account id, etc. let context = VMContext { ... }; let config = Config::default(); testing_env!(context, config); let mut contract = StatusMessage::default(); contract.set_status(&mut env, "hello".to_string()); assert_eq!("hello".to_string(), contract.get_status("bob.near".to_string()).unwrap()); }
To run unit tests include
env_test
feature:cargo test --package status-message --features env_test
Asynchronous cross-contract calls. Asynchronous cross-contract calls allow parallel execution of multiple contracts in parallel with subsequent aggregation on another contract.
env
exposes the following methods:promise_create
-- schedules an execution of a function on some contract;promise_then
-- attaches the callback back to the current contract once the function is executed;promise_and
-- combinator, allows waiting on several promises simultaneously, before executing the callback;promise_return
-- treats the result of execution of the promise as the result of the current function.
Follow examples/cross-contract to see various usages of cross contract calls, including system-level actions done from inside the contract like balance transfer (examples of other system-level actions are: account creation, access key creation/deletion, contract deployment, etc).
Initialization methods. We can define an initialization method that can be used to initialize the state of the contract.
#[near_bindgen(init => new)] impl StatusMessage { pub fn new(user: String, status: String) -> Self { let mut res = Self::default(); res.records.insert(user, status); res } }
Pre-requisites
To develop Rust contracts you would need to:
- Install Rustup:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
- Install the
nightly
Rust compiler:
rustup install nightly
- Use the nightly compiler for your repo:
cd ./myproject
rustup override set nightly
Writing Rust Contract
You can follow the examples/status-message crate that shows a simple Rust contract.
The general workflow is the following:
Create a crate and configure the
Cargo.toml
similarly to how it is configured in examples/status-message/Cargo.toml;Crate needs to have one
pub
struct that will represent the smart contract itself:- The struct needs to implement
Default
trait which NEAR will use to create the initial state of the contract upon its first usage; - The struct also needs to implement
BorshSerialize
andBorshDeserialize
traits which NEAR will use to save/load contract's internal state;
Here is an example of a smart contract struct:
#[near_bindgen] #[derive(Default, BorshSerialize, BorshDeserialize)] pub struct MyContract { data: HashMap<u64, u64> }
- The struct needs to implement
Define methods that NEAR will expose as smart contract methods:
- You are free to define any methods for the struct but only public methods will be exposed as smart contract methods;
- Methods need to use either
&self
,&mut self
, orself
; - Decorate the
impl
section with#[near_bindgen]
macro. That is where all the M.A.G.I.C. (Macros-Auto-Generated Injected Code) is happening - If you need to use blockchain interface, e.g. to get the current account id then you can access it with
env::*
;
Here is an example of smart contract methods:
#[near_bindgen] impl MyContract { pub fn insert_data(&mut self, key: u64, value: u64) -> Option<u64> { self.data.insert(key) } pub fn get_data(&self, key: u64) -> Option<u64> { self.data.get(&key).cloned() } }
Building Rust Contract
We can build the contract using rustc:
RUSTFLAGS='-C link-arg=-s' cargo +nightly build --target wasm32-unknown-unknown --release
Running Rust Contract
If you skipped the previous steps you can use the already built contract from examples/status-message/res/status-message.wasm.
Start the local testnet
Let's start the local Near testnet to run the contract on it.
- Make sure you have Docker installed;
- Clone the nearprotocol/nearcore;
- Make sure you are in
master
branch, then run
It might take a minute to start if you machine have not downloaded the docker image yet.rm -rf testdir; ./scripts/start_unittest.py
Note, the locally running node will create testdir
directory where it will keep the node state and the configs, including
the secret key of the validator's account which we can use to create new accounts later.
Create the project and deploy the contract
Make sure you have the newest version of near-shell installed by running:
npm install -g near-shell
Create the near-shell project. This will allow having configuration like URL of the node in the config file instead of passing it with each near-shell command.
near new_project ./myproject; cd ./myproject
Modify the config to point to the local node: open
./src/config.js
in./myproject
and changenodeUrl
underdevelopment
to behttp://localhost:3030
. This is how it should look like:case 'development': return { networkId: 'default', nodeUrl: 'http://localhost:3030', ... }
Create account for your smart contract, e.g. we can use
status_message
as the account identifier:near create_account status_message --masterAccount=test.near --homeDir=../nearcore/testdir
Note,
homeDir
should point to the home directory of the node which contains the secret key which we will use to sign transactions.Deploy the contract code to the newly created account:
near deploy --accountId=status_message --homeDir=../nearcore/testdir --wasmFile=../examples/status-message/res/status_message.wasm
Call contract functions
Let's call the
set_status
function on the smart contract:near call status_message set_status "{\"message\": \"Hello\"}" --accountId=test.near --homeDir=../nearcore/testdir
Notice that we use account id
test.near
to call a smart contract deployed tostatus_message
account id. The smart contract will remember that accounttest.near
left the message"Hello"
, see the implementation in examples/status-message/src/lib.rs.Do another call to
get_status
function to check that the message was correctly recorded:near call status_message get_status "{\"account_id\": \"test.near\"}" --accountId=test.near --homeDir=../nearcore/testdir
Observe the output:
Result: Hello
Do another call to
get_status
but this time inquire about the account that have not left any messages:near call status_message get_status "{\"account_id\": \"some_other_account\"}" --accountId=test.near --homeDir=../nearcore/testdir
Observe the output:
Result: null
Cleaning up
- Stop the node using docker commands:
docker stop nearcore watchtower docker rm nearcore watchtower
- Remove the node project directory:
rm -rf myproject
- Remove the node data:
rm -rf testdir
Limitations and Future Work
The current implementation of wasm_bindgen
has the following limitations:
- The smart contract struct should be serializable with borsh which is true for most of the structs;
- The method arguments and the return type should be json-serializable, which is true for most of the types, with some exceptions. For instance,
a
HashMap<MyEnum, SomeValue>
whereMyEnum
is a non-trivial tagged-union with field-structs in variants will not serialize into json, you would need to convert it toVec<(MyEnum, SomeValue)>
first. Require arguments and the return type to be json-serializable for compatiblity with contracts written in other languages, like TypeScript; - Smart contract can use
std
but cannot use wasm-incompatible OS-level features, like threads, file system, network, etc. In the future we will support the file system too; - Smart contracts should be deterministic and time-independent, e.g. we cannot use
Instant::now
. In the future we will exposeInstant::now
;
We also have the following temporary inefficiencies:
- Current smart contracts do not utilize the trie and do not use state storage efficiently. It is okay for small collections,
but in the future we will provide an alternative
near::collections::{HashMap, HashSet, Vec}
that will be using storage in an efficient way; - The current smart contract size is around typically ~80-180Kb, which happens because we compile-in the
bincode
andserde-json
libraries. In the future, we will cherry-pick only the necessary components from these libraries. For now you can usewasm-opt
to slightly shrink the size:
See Binaryen for the installation instructions.wasm-opt -Oz --output ./pkg/optimized_contract.wasm ./pkg/contract.wasm