Deploy a Contract
In this chapter, we'll learn how to build, deploy, and call a minimal smart contract on our local node.
Preparing Ground
Let's start with creating a fresh local network with an account to which we'll deploy a contract. You might want to re-read how to run a node to understand what's going on here:
$ cargo run --profile dev-release -p neard -- init
$ cargo run --profile dev-release -p neard -- run
$ NEAR_ENV=local near create-account alice.test.near --masterAccount test.near
As a sanity check, querying the state of alice.test.near
account should work:
$ NEAR_ENV=local near state alice.test.near
Loaded master account test.near key from /home/matklad/.near/validator_key.json with public key = ed25519:7tU4NtFozPWLotcfhbT9KfBbR3TJHPfKJeCri8Me6jU7
Account alice.test.near
{
amount: '100000000000000000000000000',
block_hash: 'EEMiLrk4ZiRzjNJXGdhWPJfKXey667YBnSRoJZicFGy9',
block_height: 24,
code_hash: '11111111111111111111111111111111',
locked: '0',
storage_paid_at: 0,
storage_usage: 182,
formattedAmount: '100'
}
Minimal Contract
NEAR contracts are WebAssembly blobs of bytes. To
create a contract, a contract developer typically uses an SDK for some
high-level programming language, such as JavaScript, which takes care of
producing the right .wasm
.
In this guide, we are interested in how things work under the hood, so we'll do everything manually, and implement a contract in Rust without any help from SDKs.
As we are looking for something simple, let's create a contract with a single
"method", hello
, which returns a "hello world"
string. To "define a method",
a wasm module should export a function. To "return a value", the contract needs
to interact with the environment to say "hey, this is the value I am returning".
Such "interactions" are carried through host functions, which are quite a bit
like syscalls in traditional operating systems.
The set of host functions that the contract can import is defined in
imports.rs
.
In this particular case, we need the value_return
function:
value_return<[value_len: u64, value_ptr: u64] -> []>
This means that the value_return
function takes a pointer to a slice of bytes,
the length of the slice, and returns nothing. If the contract calls this function,
the slice would be considered a result of the function.
To recap, we want to produce a .wasm
file with roughly the following content:
(module
(import "env" "value_return" (func $value_return (param i64 i64)))
(func (export "hello") ... ))
Cargo Boilerplate
Armed with this knowledge, we can write Rust code to produce the required WASM. Before we start doing that, some amount of setup code is required.
Let's start with creating a new crate:
$ cargo new hello-near --lib
To compile to wasm, we also need to add a relevant rustup toolchain:
$ rustup toolchain add wasm32-unknown-unknown
Then, we need to tell Cargo that the final artifact we want to get is a WebAssembly module.
This requires the following cryptic spell in Cargo.toml:
# hello-near/Cargo.toml
[lib]
crate-type = ["cdylib"]
Here, we ask Cargo to build a "C dynamic library". When compiling for wasm,
that'll give us a .wasm
module. This part is a bit confusing, sorry about
that :(
Next, as we are aiming for minimalism here, we need to disable optional bits
of the Rust runtime. Namely, we want to make our crate no_std
(this means
that we are not going to use the Rust standard library), set panic=abort
as our panic strategy and define a panic handler to abort execution.
# hello-near/Cargo.toml
[package]
name = "hello-near"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[profile.release]
panic = "abort"
#![allow(unused)] fn main() { // hello-near/src/lib.rs #![no_std] #[panic_handler] fn panic_handler(_info: &core::panic::PanicInfo) -> ! { core::arch::wasm32::unreachable() } }
At this point, we should be able to compile our code to wasm, and it should be fairly small. Let's do that:
$ cargo b -r --target wasm32-unknown-unknown
Compiling hello-near v0.1.0 (~/hello-near)
Finished release [optimized] target(s) in 0.24s
$ ls target/wasm32-unknown-unknown/release/hello_near.wasm
.rwxr-xr-x 106 matklad 15 Nov 15:34 target/wasm32-unknown-unknown/release/hello_near.wasm
106 bytes is pretty small! Let's see what's inside. For that, we'll use
the wasm-tools
suite of CLI utilities.
$ cargo install wasm-tools
λ wasm-tools print target/wasm32-unknown-unknown/release/hello_near.wasm
(module
(memory (;0;) 16)
(global $__stack_pointer (;0;) (mut i32) i32.const 1048576)
(global (;1;) i32 i32.const 1048576)
(global (;2;) i32 i32.const 1048576)
(export "memory" (memory 0))
(export "__data_end" (global 1))
(export "__heap_base" (global 2))
)
Rust Contract
Finally, let's implement an actual contract. We'll need an extern "C"
block to
declare the value_return
import, and a #[unsafe(no_mangle)] extern "C"
function to
declare the hello
export:
#![allow(unused)] fn main() { // hello-near/src/lib.rs #![no_std] extern "C" { fn value_return(len: u64, ptr: u64); } #[unsafe(no_mangle)] pub extern "C" fn hello() { let msg = "hello world"; unsafe { value_return(msg.len() as u64, msg.as_ptr() as u64) } } #[panic_handler] fn panic_handler(_info: &core::panic::PanicInfo) -> ! { core::arch::wasm32::unreachable() } }
After building the contract, the output wasm shows us that it's roughly what we want:
$ cargo b -r --target wasm32-unknown-unknown
Compiling hello-near v0.1.0 (/home/matklad/hello-near)
Finished release [optimized] target(s) in 0.05s
$ wasm-tools print target/wasm32-unknown-unknown/release/hello_near.wasm
(module
(type (;0;) (func (param i64 i64)))
(type (;1;) (func))
(import "env" "value_return" (; <- Here's our import. ;)
(func $value_return (;0;) (type 0)))
(func $hello (;1;) (type 1)
i64.const 11
i32.const 1048576
i64.extend_i32_u
call $value_return
)
(memory (;0;) 17)
(global $__stack_pointer (;0;) (mut i32) i32.const 1048576)
(global (;1;) i32 i32.const 1048587)
(global (;2;) i32 i32.const 1048592)
(export "memory" (memory 0))
(export "hello" (func $hello)) (; <- And export! ;)
(export "__data_end" (global 1))
(export "__heap_base" (global 2))
(data $.rodata (;0;) (i32.const 1048576) "hello world")
)
Deploying the Contract
Now that we have the WASM, let's deploy it!
$ NEAR_ENV=local near deploy alice.test.near \
./target/wasm32-unknown-unknown/release/hello_near.wasm
Loaded master account test.near key from /home/matklad/.near/validator_key.json with public key = ed25519:ChLD1qYic3G9qKyzgFG3PifrJs49CDYeERGsG58yaSoL
Starting deployment. Account id: alice.test.near, node: http://127.0.0.1:3030, helper: http://localhost:3000, file: ./target/wasm32-unknown-unknown/release/hello_near.wasm
Transaction Id GDbTLUGeVaddhcdrQScVauYvgGXxSssEPGUSUVAhMWw8
To see the transaction in the transaction explorer, please open this url in your browser
http://localhost:9001/transactions/GDbTLUGeVaddhcdrQScVauYvgGXxSssEPGUSUVAhMWw8
Done deploying to alice.test.near
And, finally, let's call our contract:
$ NEAR_ENV=local $near call alice.test.near hello --accountId alice.test.near
Scheduling a call: alice.test.near.hello()
Loaded master account test.near key from /home/matklad/.near/validator_key.json with public key = ed25519:ChLD1qYic3G9qKyzgFG3PifrJs49CDYeERGsG58yaSoL
Doing account.functionCall()
Transaction Id 9WMwmTf6pnFMtj1KBqjJtkKvdFXS4kt3DHnYRnbFpJ9e
To see the transaction in the transaction explorer, please open this url in your browser
http://localhost:9001/transactions/9WMwmTf6pnFMtj1KBqjJtkKvdFXS4kt3DHnYRnbFpJ9e
'hello world'
Note that we pass alice.test.near
twice: the first time to specify which contract
we are calling, the second time to determine who calls the contract. That is,
the second account is the one that spends tokens. In the following example bob
spends NEAR to call the contact deployed to the alice
account:
$ NEAR_ENV=local $near call alice.test.near hello --accountId bob.test.near
Scheduling a call: alice.test.near.hello()
Loaded master account test.near key from /home/matklad/.near/validator_key.json with public key = ed25519:ChLD1qYic3G9qKyzgFG3PifrJs49CDYeERGsG58yaSoL
Doing account.functionCall()
Transaction Id 4vQKtP6zmcR4Xaebw8NLF6L5YS96gt5mCxc5BUqUcC41
To see the transaction in the transaction explorer, please open this url in your browser
http://localhost:9001/transactions/4vQKtP6zmcR4Xaebw8NLF6L5YS96gt5mCxc5BUqUcC41
'hello world'