Introduction

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:

Installation

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

Install Rust

Install Rust with rustup per the official instructions:

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

This project requires nightly Rust and the wasm32-unknown-unknown target to be available, which can be installed with rustup:

rustup install nightly
rustup target add wasm32-unknown-unknown --toolchain nightly
rustup default nightly

Install EOS

An EOS node is required to deploy and test smart contracts. The easiest way to setup a node is to use 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

Create the project:

cargo +nightly new hello --lib
cd hello

File Cargo.toml:

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

[dependencies]
eosio = "0.2"

[profile.release]
lto = true

File .cargo/config:

[target.wasm32-unknown-unknown]
rustflags = [
  "-C", "link-args=-z stack-size=48000"
]

File src/lib.rs:


# #![allow(unused_variables)]
#fn main() {
use eosio::*;

#[eosio_action]
fn hi(name: AccountName) {
    eosio_print!("Hello, ", name);
}

eosio_abi!(hi);
#}

File hello.abi.json:

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

Compile and minify the smart contract (requires optional dependencies):

cargo build --release --target=wasm32-unknown-unknown
wasm-gc target/wasm32-unknown-unknown/release/hello.wasm hello_gc.wasm
wasm-opt hello_gc.wasm --output hello_gc_opt.wasm -Oz

Deploying the smart contract will depend on how you have your EOSIO node setup. Assuming you followed the docker-compose instructions above, run these commands in a terminal:

alias cleos='docker-compose exec keosd cleos --url http://nodeosd:8888 --wallet-url http://127.0.0.1:8900'

# Create a wallet and the 'hello' account
PUBKEY=EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV
PRIVKEY=5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3
cleos wallet create --to-console
cleos wallet import --private-key $PRIVKEY
cleos create account eosio hello $PUBKEY $PUBKEY

# Deploy the ABI and WASM files
cleos set abi hello /mnt/dev/project/hello.abi.json
cleos set code hello /mnt/dev/project/hello_gc_opt.wasm

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

If all went well you should see Hello, world in the console.

Getting Started

In this section we will walk through writing the hello C++ example in Rust. In this example you will learn how to setup and optimize a basic smart contract, accept an input, and print to the console.

Creating the project

First, let's create a new project with Cargo and change directories:

cargo +nightly new hello --lib
cd hello

You should now have a directory that looks like this:

src/
  lib.rs
Cargo.toml

Configuring Cargo

The Cargo.toml file is used by Rust to manage dependencies and other configuration options. If you open this file now it should look similar to this:

[package]
name = "hello"
version = "0.1.0"
authors = []
edition = "2018"

[dependencies]

Let's change this to add eosio as a dependency, and change the crate type so that Rust generates a .wasm file:

[package]
name = "hello"
version = "0.1.0"
authors = []
edition = "2018"

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

[dependencies]
eosio = "0.2"

Generating and Optimizing a WASM File

At this point we can compile our project to produce a .wasm file:

cargo build --release --target=wasm32-unknown-unknown

You should now see a generated file at target/wasm32-unknown-unknown/release/hello.wasm:

$ ls -lh target/wasm32-unknown-unknown/release | grep wasm
-rwxr-xr-x 2 sagan sagan 1.9M Oct 25 16:34 hello.wasm

This file will be huge at almost 2MB! But we can significantly reduce file size by enabling link-time optimization. Add this to the bottom of our Cargo.toml file:

[profile.release]
lto = true

Now if we rebuild the project we should see a much smaller .wasm file:

$ cargo build --release --target=wasm32-unknown-unknown
$ ls -lh target/wasm32-unknown-unknown/release | grep wasm
-rwxr-xr-x 2 sagan sagan  52K Oct 25 16:48 hello_world.wasm

That's better, but 52KB is still heavy for an empty smart contract. Luckily we can use wasm-gc and wasm-opt to reduce the file size even further:

wasm-gc target/wasm32-unknown-unknown/release/hello.wasm hello_gc.wasm
wasm-opt hello_gc.wasm --output hello_gc_opt.wasm -Oz
$ ls -lh | grep wasm
-rw-r--r-- 1 sagan sagan  109 Oct 25 16:57 hello_gc_opt.wasm
-rw-r--r-- 1 sagan sagan  116 Oct 25 16:56 hello_gc.wasm

By using wasm-gc and wasm-opt we are able to get the file size down to just over 100 bytes! But this is before we've added any code. Realistically you can expect simple contracts to be under 15KB.

Writing the Smart Contract

Now that we know how to prepare the .wasm file, let's start coding. Open up src/lib.rs and replace its contents with this:


# #![allow(unused_variables)]
#fn main() {
use eosio::*;                       // Include everything from the eosio crate

#[eosio_action]                     // Mark this function as an action
fn hi(name: AccountName) {
    eosio_print!("Hello, ", name);  // Print to the console
}

eosio_abi!(hi);                     // Create the 'apply' function
#}

See the API documentation for more details on what this code is doing.

Let's recompile our project and minify the the WASM file again:

cargo build --release --target=wasm32-unknown-unknown
wasm-gc target/wasm32-unknown-unknown/release/hello.wasm hello_gc.wasm
wasm-opt hello_gc.wasm --output hello_gc_opt.wasm -Oz

Creating the ABI File

In the future ABI files will be automatically generated, but for now they must be typed out manually. Copy this code into a file called hello.abi.json:

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

Deploying, First Attempt

At this point we have our WASM and ABI files, so we're ready to deploy our smart contract.

For this you will need to have access to an EOS node. Please see the Installing EOS section for instructions. The code below will assume that you've started nodeos and keosd containers using Docker Compose.

First create an alias for cleos:

alias cleos='docker-compose exec keosd cleos --url http://nodeosd:8888 --wallet-url http://127.0.0.1:8900'

Then create a wallet, import a private key, and create the hello account:

PUBKEY=EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV
PRIVKEY=5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3
cleos wallet create --to-console
cleos wallet import --private-key $PRIVKEY
cleos create account eosio hello $PUBKEY $PUBKEY

Deploy the ABI:

cleos set abi hello /mnt/dev/project/hello.abi.json

Deploy the WASM:

cleos set code hello /mnt/dev/project/hello_gc_opt.wasm

...but this will fail with an error!

$ cleos set code hello /mnt/dev/project/hello_gc_opt.wasm
Reading WASM from /mnt/dev/project/hello_gc_opt.wasm...
Setting Code...
Error 3070002: Runtime Error Processing WASM

In the nodeos console log you will see this error message:

error 2018-10-26T03:46:12.176 thread-0  http_plugin.cpp:580           handle_exception     ] FC Exception encountered while processing chain.push_transaction
debug 2018-10-26T03:46:12.176 thread-0  http_plugin.cpp:581           handle_exception     ] Exception Details: 3070002 wasm_execution_error: Runtime Error Processing WASM
Smart contract data segments must lie in first 64KiB
     {"k":64}
     thread-0  wasm_eosio_validation.cpp:30 validate
pending console output:
     {"console":""}
     thread-0  apply_context.cpp:72 exec_one

This is happening because Rust by default reserves 1MB for the stack, but EOS expects data to be within the first 64KB.

Deploying, Second Attempt

We can fix this by telling the Rust compiler to reserve less than 64KB for the stack. Create a new file at .cargo/config with these contents:

[target.wasm32-unknown-unknown]
rustflags = [
  "-C", "link-args=-z stack-size=48000"
]

48KB seems to be a reasonable number, but feel free to experiment.

Now let's try to rebuild and redeploy our contract:

cargo build --release --target=wasm32-unknown-unknown
wasm-gc target/wasm32-unknown-unknown/release/hello.wasm hello_gc.wasm
wasm-opt hello_gc.wasm --output hello_gc_opt.wasm -Oz
cleos set code hello /mnt/dev/project/hello_gc_opt.wasm

Finally, say hello:

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

Success!

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. The equivalent C++ code has been provided where possible.

Directory Description
addressbook An example of how to interact with tables using secondary indexes.
eosio_token The standard eosio.token contract ported to Rust.
hello The most basic contract using the eosio crate.
hello_bare A bare bones version of the hello contract without any dependencies.
tictactoe An example of how to interact with EOSIO tables.

Hello, World!

Tic-Tac-Toe

Address Book

URL Shortener

Roadmap

See the 1.0 milestone for a full list of fixes and features planned for 1.0.

Listed below are features that are planned for the 1.0 release. The goal is to have a 1.0 release candidate with all these features by Q1 2019, but this may change depending on community feedback and support.

Unit Testing

Tracking this feature in issue #4

A proper test suite is crucial for developers to build secure and correct smart contracts.

EOS already supports unit tests for smart contracts (see eosio.contracts for an example), so to support this in Rust we will likely need to:

  1. Generate more FFI bindings for EOS libraries.
  2. Create a new eosio_test crate that will be a test harness, similar to how wasm-bindgen uses wasm-bindgen-test to support testing in headless browsers.

ABI Generation

Tracking this feature in issue #5

Hand-written ABI files are unnecessary and expose developers to risk if they aren't kept updated.

Since we already have #[eosio::action] and #[eosio::table] attributes, it should be fairly straightforward to implement this feature by detecting these attributes and generating a JSON file.

ABI to Rust

Tracking this feature in issue #6

It would be nice to have a CLI command that would generate Rust code from on-chain ABIs. This would make it significantly easier to interact with external contracts through inline actions.

Implementing this feature would require fetching the ABI JSON from an EOS node and creating a Rust file containing the generated tables and actions.

Schema Migrations

Tracking this feature in issue #7

Making changes to EOS table fields is currently not a pleasant experience. It can be a fragile error-prone process that involves duplicating code to work with multiple versions of structs. We believe that a better solution can be found by taking inspiration from projects like Diesel and Django migrations.

Implementing this feature will require significant effort and discovery. This may be a 1.0+ feature.

RPC API

Tracking this feature in issue #8

All EOS apps need a way to talk to EOS nodes, to fetch table rows and to send transactions. In order for full-stack Rust-based EOS applications to come to fruition, there needs to be a solid RPC API. In Javascript there is eosjs, and something similar should exist for Rust.

Implementing this will be tricky since we need to support browser and server environments.

This could get even more complicated if we decide to optionally support futures. For the initial release futures will probably be mandatory.

There are a lot of things to consider so this may be a 1.0+ feature.

eosio-rust CLI

Tracking this feature in issue #9

We already have several features that need CLIs. Consolidating all our CLIs under one CLI will make things simpler for developers and allow us to add new commands later on.

Commands should be implemented to:

  • Create a new eosio-rust project, e.g. eosio-rust new
  • Generate an ABI file, e.g. eosio-rust to-abi
  • Generate Rust from an ABI, e.g. eosio-rust from-abi
  • Manage table schemas, e.g. eosio-rust schema
  • Run unit tests, e.g. eosio-rust test

wasm-bindgen and stdweb Support

Tracking this feature in issue #10

A big selling point of Rust is its first-class support for WebAssembly and the possibility of writing full-stack web applications in one highly performant language. It would be great if we could use the same structs and functions from our smart contracts in our frontend code as well.

Implementing this may require rethinking some things, specifically traits that are implemented on primitive types like SecondaryTableKey seem to be causing some issues.

serde Support

Tracking this feature in issue #11

Serde is the defacto standard when it comes to serializing and deserializing data. It will be necessary for table structs to support Serde's Serialize/Deserialize traits in order to implement the RPC API later on.

Implementing this will require writing custom serializers/deserializers for EOS types, for example:

  • Booleans are 0 or 1
  • Large numbers can sometimes be integers, sometimes be strings

API Documentation

Contributing

Git Commit Messages

  • Use the present tense ("Add feature" not "Added feature")
  • Use the imperative mood ("Move cursor to..." not "Moves cursor to...")
  • Limit the first line to 72 characters or less
  • Reference issues and pull requests liberally after the first line
  • When only changing documentation, include [ci skip] in the commit title
  • Consider starting the commit message with an applicable emoji:
    • 🎨 :art: when improving the format/structure of the code
    • 🐎 :racehorse: when improving performance
    • 🚱 :non-potable_water: when plugging memory leaks
    • 📝 :memo: when writing docs
    • 🐧 :penguin: when fixing something on Linux
    • 🍎 :apple: when fixing something on macOS
    • 🏁 :checkered_flag: when fixing something on Windows
    • 🐛 :bug: when fixing a bug
    • 🔥 :fire: when removing code or files
    • 💚 :green_heart: when fixing the CI build
    • :white_check_mark: when adding tests
    • 🔒 :lock: when dealing with security
    • ⬆️ :arrow_up: when upgrading dependencies
    • ⬇️ :arrow_down: when downgrading dependencies
    • 👕 :shirt: when removing linter warnings
    • 🎉:tada: when tagging a release
    • 🐳 :whale: when dealing with Docker

Based on https://github.com/atom/atom/blob/master/CONTRIBUTING.md#git-commit-messages

Examples

Appears under "Features" header, pencil subheader:

feat(pencil): add 'graphiteWidth' option

Appears under "Bug Fixes" header, graphite subheader, with a link to issue #28:

fix(graphite): stop graphite breaking when width < 0.1

Closes #28

Appears under "Performance Improvements" header, and under "Breaking Changes" with the breaking change explanation:

perf(pencil): remove graphiteWidth option

BREAKING CHANGE: The graphiteWidth option has been removed. The default graphite width of 10mm is always used for performance reason.

The following commit and commit 667ecc1 do not appear in the changelog if they are under the same release. If not, the revert commit appears under the "Reverts" header.

revert: feat(pencil): add 'graphiteWidth' option

This reverts commit 667ecc1654a317a13331b17617d973392f415f02.

Commit Message Format

A commit message consists of a header, body and footer. The header has a type, scope and subject:

<type>(<scope>): <subject>
<BLANK LINE>
<body>
<BLANK LINE>
<footer>

The header is mandatory and the scope of the header is optional.

Any line of the commit message cannot be longer 100 characters! This allows the message to be easier to read on GitHub as well as in various git tools.

Revert

If the commit reverts a previous commit, it should begin with revert:, followed by the header of the reverted commit. In the body it should say: This reverts commit <hash>., where the hash is the SHA of the commit being reverted.

Type

If the prefix is feat, fix or perf, it will always appear in the changelog.

Other prefixes are up to your discretion. Suggested prefixes are docs, chore, style, refactor, and test for non-changelog related tasks.

Scope

The scope could be anything specifying place of the commit change. For example $location, $browser, $compile, $rootScope, ngHref, ngClick, ngView, etc...

Subject

The subject contains succinct description of the change:

  • use the imperative, present tense: "change" not "changed" nor "changes"
  • don't capitalize first letter
  • no dot (.) at the end

Body

Just as in the subject, use the imperative, present tense: "change" not "changed" nor "changes". The body should include the motivation for the change and contrast this with previous behavior.

Footer

The footer should contain any information about Breaking Changes and is also the place to reference GitHub issues that this commit Closes.

Breaking Changes should start with the word BREAKING CHANGE: with a space or two newlines. The rest of the commit message is then used for this.

A detailed explanation can be found in this document.

Based on https://github.com/angular/angular.js/blob/master/CONTRIBUTING.md#commit