EOSIO Rust

DISCLAIMER: This project is in early development and we looking for feedback on all APIs and features. All APIs and features should be considered unstable and insecure until version 1.0 is released. This code is not yet suitable for production environments where user funds are at risk. Thank you.

This project intends to enable developers to write full-stack EOSIO applications using the Rust programming language. We believe Rust is an excellent choice for EOSIO smart contract development with its focus on safety, speed, and WebAssembly. Furthermore, projects like wasm-bindgen and stdweb make it possible to write full-stack Rust web applications, limiting the need for Javascript and enabling code reuse between browsers, servers, and smart contracts.

The primary goals of this project are to provide Rust crates that:

  • Enable developers to write secure EOSIO smart contracts.
  • Streamline the development of full-stack EOSIO web applications.
  • Simplify managing and updating EOSIO table schemas.
  • Allow developers to publish reusable smart contract code.

Getting Help

If you find a bug or think of an improvement please open an issue and let us know!

Otherwise if you are stuck on something or run into problems, here are some resources that could help:

Install

To write EOS smart contracts in Rust we will need to install Rust and setup an EOS local node.

Install Rust

EOSIO Rust works with stable Rust 1.31 and above.

Install Rust with rustup per the official instructions:

curl https://sh.rustup.rs -sSf | sh

We will also need the wasm32-unknown-unknown target, which can be installed with rustup:

rustup target add wasm32-unknown-unknown

Install EOS

To test and deploy smart contracts you will want to have a local EOS node running. The easiest way to setup a node is with Docker. See the official Docker quickstart guide for instructions.

We recommend using docker-compose to manage nodeos and keosd containers. You can download the official docker-compose-latest.yml file and start the containers using these commands:

wget https://raw.githubusercontent.com/EOSIO/eos/master/Docker/docker-compose-latest.yml
docker volume create --name=nodeos-data-volume
docker volume create --name=keosd-data-volume
docker-compose -f docker-compose-latest.yml up

Note #1! If you are using cleos within a Docker container, you need to mount your project directory as a volume so that cleos can deploy your files. If you're using Docker Compose, add your project directory to the volumes section of the keosd container like so (abbreviated):

services:
    keosd:
        volumes:
            - ./:mnt/dev/project:ro

Note #2! If you are expecting to see console output from nodeos then be sure to add --contracts-console to the end of the nodeosd command like so (abbreviated):

services:
    nodeosd:
        command: /opt/eosio/bin/nodeosd.sh ... --contracts-console

Optional Dependencies

wasm-gc

wasm-gc is a command-line tool that removes unused code in WASM files. It can be installed with Cargo:

cargo install wasm-gc

Binaryen

Binaryen comes with a command-line tool called wasm-opt that optimizes WASM file sizes. Binaryen can be installed with most system package managers.

WebAssembly Binary Toolkit (WABT)

WABT comes with a command-line tool wasm2wat that can be used to create textual representations of WASM files, which can be useful for debugging. WABT can be installed with most system package managers.

Quick Start

In this quick-start tutorial we will create a simple EOSIO smart contract in Rust that accepts an account name and prints a greeting message.

Create a new Rust library:

cargo new hello --lib

Edit Cargo.toml:

[package]
name = "hello"
version = "0.1.0"
edition = "2018"
publish = false

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

[dependencies]
eosio = { path = "../../crates/eosio" }
eosio_cdt = { path = "../../crates/eosio_cdt" }

Edit src/lib.rs:

#[eosio::action]
fn hi(name: eosio::AccountName) {
    eosio_cdt::print!("Hello, ", name, "!");
}

eosio::abi!(hi);

Compile with the following command:

RUSTFLAGS="-C link-args=-zstack-size=48000" \
cargo build --release -target=wasm32-unknown-unknown

The smart contract should now be built at target/wasm32-unknown-unknown/release/hello.wasm

Deploying

Create a new file called abi.json (in future versions this will be automatically generated):

{
    "version": "eosio::abi/1.0",
    "structs": [
        {
            "name": "hi",
            "base": "",
            "fields": [
                {
                    "name": "name",
                    "type": "name"
                }
            ]
        }
    ],
    "actions": [
        {
            "name": "hi",
            "type": "hi"
        }
    ]
}

Assuming you have cleos setup and have created the hello account:

cleos set abi hello abi.json
cleos set code hello target/wasm32-unknown-unknown/release/hello.wasm

Say Hello

Finally, say hello:

cleos push action hello hi '["world"]' -p 'hello@active'

If all went well you should see Hello, world in the console. Otherwise, if the transaction was sent successfully but you don't see any output, you may need to use the --contract-console option with nodeos.

Examples

Examples can be found in the examples directory.

Hello, World!

This example shows how to create a smart contract in Rust that prints a greeting, without using the eosio crate. This is used to demonstrate the code that is being abstracted away, and to compare the abstraction overhead.

Usage

cleos push action hello hi '["world"]' -p 'hello@active'

Cargo.toml

[package]
name = "hello_bare"
version = "0.1.0"
edition = "2018"
publish = false

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

Source

// Declare the EOSIO externs to read action data and print to the console.
#[cfg(target_arch = "wasm32")]
extern "C" {
    pub fn read_action_data(msg: *mut CVoid, len: u32) -> u32;
    pub fn prints_l(cstr: *const u8, len: u32);
    pub fn printn(name: u64);
}

#[repr(u8)]
pub enum CVoid {
    // Two dummy variants so the #[repr] attribute can be used.
    Variant1,
    Variant2,
}

// EOSIO smart contracts are expected to have an `apply` function.
#[cfg(target_arch = "wasm32")]
#[no_mangle]
pub extern "C" fn apply(_receiver: u64, _code: u64, _action: u64) {
    // First print "Hi, " to the console.
    {
        let msg = "Hi, ";
        let ptr = msg.as_ptr();
        let len = msg.len() as u32;
        unsafe { prints_l(ptr, len) };
    }

    // Read the action data, which is one EOSIO name (a u64, or 8 bytes).
    let name = {
        let mut bytes = [0u8; 8];
        let ptr: *mut CVoid = &mut bytes[..] as *mut _ as *mut CVoid;
        unsafe { read_action_data(ptr, 8) };
        u64::from_le_bytes(bytes)
    };

    // Finally, print the name to the console.
    unsafe { printn(name) };
}

ABI

{
  "version": "eosio::abi/1.0",
  "structs": [
    {
      "name": "hi",
      "base": "",
      "fields": [
        {
          "name": "user",
          "type": "name"
        }
      ]
    }
  ],
  "actions": [
    {
      "name": "hi",
      "type": "hi"
    }
  ]
}

Hello, World!

This example shows how to create a smart contract in Rust that prints a greeting.

Usage

cleos push action hello hi '["world"]' -p 'hello@active'

Cargo.toml

[package]
name = "hello"
version = "0.1.0"
edition = "2018"
publish = false

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

[dependencies]
eosio = { path = "../../crates/eosio" }
eosio_cdt = { path = "../../crates/eosio_cdt" }

Source

#[eosio::action]
fn hi(name: eosio::AccountName) {
    eosio_cdt::print!("Hello, ", name, "!");
}

eosio::abi!(hi);

ABI

{
  "version": "eosio::abi/1.0",
  "structs": [
    {
      "name": "hi",
      "base": "",
      "fields": [
        {
          "name": "user",
          "type": "name"
        }
      ]
    }
  ],
  "actions": [
    {
      "name": "hi",
      "type": "hi"
    }
  ]
}

Tic-Tac-Toe

This example shows how to create a smart contract that reads and writes tables.

Usage

cleos push action tictactoe create '["alice","bob"]' -p 'alice@active'
cleos push action tictactoe makemove '["alice","bob",1,0,1]' -p 'alice@active'
cleos push action tictactoe restart '["alice","bob",1]' -p 'alice@active'
cleos push action tictactoe close '["alice","bob"]' -p 'alice@active'

Cargo.toml

[package]
name = "hello"
version = "0.1.0"
edition = "2018"
publish = false

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

[dependencies]
eosio = { path = "../../crates/eosio" }
eosio_cdt = { path = "../../crates/eosio_cdt" }

Source

#[eosio::action]
fn hi(name: eosio::AccountName) {
    eosio_cdt::print!("Hello, ", name, "!");
}

eosio::abi!(hi);

ABI

{
  "version": "eosio::abi/1.0",
  "structs": [
    {
      "name": "hi",
      "base": "",
      "fields": [
        {
          "name": "user",
          "type": "name"
        }
      ]
    }
  ],
  "actions": [
    {
      "name": "hi",
      "type": "hi"
    }
  ]
}

Address Book

[package]
name = "addressbook"
version = "0.1.0"
edition = "2018"
publish = false

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

[dependencies]
eosio = { path = "../../crates/eosio" }
eosio_cdt = { path = "../../crates/eosio_cdt" }
use eosio::*;
use eosio_cdt::*;

eosio::abi!(add, update, erase, like, likezip);

#[eosio::table("address")]
struct Address {
    #[eosio(primary_key)]
    account: AccountName,
    first_name: String,
    last_name: String,
    street: String,
    city: String,
    state: String,
    #[eosio(secondary_key)]
    zip: u32,
    liked: u64,
}

#[eosio::action]
fn add(
    account: AccountName,
    first_name: String,
    last_name: String,
    street: String,
    city: String,
    state: String,
    zip: u32,
) {
    require_auth(account);

    let code = current_receiver();
    let table = Address::table(code, code);

    let address = table.find(account);
    assert!(address.is_none(), "Address for account already exists");

    let address = Address {
        account,
        first_name,
        last_name,
        street,
        city,
        state,
        zip,
        liked: 0,
    };
    table.emplace(account, address).expect("write");
}

#[eosio::action]
fn update(
    account: AccountName,
    first_name: String,
    last_name: String,
    street: String,
    city: String,
    state: String,
    zip: u32,
) {
    require_auth(account);

    let code = current_receiver();
    let table = Address::table(code, code);
    let cursor = table.find(account).expect("Address for account not found");

    let mut address = cursor.get().expect("read");
    address.first_name = first_name;
    address.last_name = last_name;
    address.street = street;
    address.city = city;
    address.state = state;
    address.zip = zip;

    cursor.modify(Payer::Same, address).expect("write");
}

#[eosio::action]
fn erase(account: AccountName) {
    require_auth(account);

    let code = current_receiver();
    let addresses = Address::table(code, code);
    let cursor = addresses
        .find(account)
        .expect("Address for account not found");

    cursor.erase().expect("read");
}

#[eosio::action]
fn like(account: AccountName) {
    let code = current_receiver();
    let addresses = Address::table(code, code);
    let cursor = addresses
        .find(account)
        .expect("Address for account not found");

    let mut address = cursor.get().expect("read");
    address.liked += 1;
    cursor
        .modify(Payer::New(address.account), address)
        .expect("write");
}

#[eosio::action]
fn likezip(zip: u32) {
    let code = current_receiver();
    let table = Address::by_zip(code, code);
    for cursor in table.lower_bound(zip).into_iter() {
        let mut addr = cursor.get().expect("read");
        if addr.zip != zip {
            break;
        }
        addr.liked += 1;
        cursor.modify(Payer::Same, addr).expect("write");
    }
}

User Guide

In this guide we will go through the basics of developing EOSIO smart contracts with Rust. For other guides on EOSIO software please see block.one's EOSIO developer portal.

Declaring Actions

Tables

Using Secondary Keys

Deriving Common Traits

Compiling Smart Contracts

Deploying Smart Contracts

Optimizing WASM files