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'