Need help with pwasm-tutorial?
Click the “chat” button below for chat support from the developer who created it, or find similar developers for support.

About the developer

openethereum
204 Stars 25 Forks GNU General Public License v3.0 160 Commits 8 Opened issues

Description

A step-by-step tutorial on how to write contracts in Wasm for Kovan

Services available

!
?

Need anything else?

Contributors list

Tutorial Prerequisites

There is a list of all tools and dependencies required for this tutorial.

Rust

rustup is the easiest way to install Rust toolchains. Rust nightly toolchain is required since our contracts require some unstable features:

rustup install nightly-2018-11-12

Also, we need to install

wasm32-unknown-unknown
to compile contracts to Wasm:
bash
rustup target add wasm32-unknown-unknown

Parity wasm-build

wasm-build takes the raw

.wasm
file produced by Rust compiler and packs it to the form of valid contract.
cargo install pwasm-utils-cli --bin wasm-build

Parity

Follow the parity setup guide. You'll need Parity version 1.9.5 or later.

Web3.js

We'll be using

Web3.js
to connect to the Parity node. Change dir to the root
pwasm-tutorial
and run yarn or npm to install
Web3.js
:
yarn install

Tutorial source code

We provide a full source code for each step in this tutorial under

step-*
directories.

General structure

Source code: https://github.com/paritytech/pwasm-tutorial/tree/master/step-0

// Contract doesn't use Rust's standard library
#![no_std]

// pwasm-ethereum implements bindings to the runtime extern crate pwasm_ethereum;

/// Will be described in the next step #[no_mangle] pub fn deploy() { }

/// The call function is the main function of the deployed contract #[no_mangle] pub fn call() { // Send a result pointer to the runtime pwasm_ethereum::ret(&b"result"[..]); }

pwasm-ethereum

pwasm-ethereum is a collection of bindings to interact with ethereum-like network.

Building

To make sure that everything is set up go to the

step-0
directory and run
./build.sh

As a result the

pwasm_tutorial_contract.wasm
should be created in the
target
directory.

Take a look on the contents of the

./build.sh
executable:
#!/bin/bash

cargo build --release --target wasm32-unknown-unknown wasm-build --target=wasm32-unknown-unknown ./target pwasm_tutorial_contract

First, we run

cargo build --release --target wasm32-unknown-unknown
which yields a "raw" Wasm binary and put it into the "target" directory:
target/wasm32-unknown-unknown/release/pwasm_tutorial_contract.wasm
. Then we run the
wasm-build
tool. It takes
pwasm_tutorial_contract.wasm
raw file generated by
cargo build
, trims, optimises it and produces the so-called contract "constructor". It packs the actual contract code into that constructor. So on deploy, executor will put the raw contract code into the blockchain as a result of the successful transaction.

For your convenience, every step in our tutorial features a

build.sh
shell script, which incorporates the proper
wasm-build
call (unfortunately, cargo's build pipeline is not yet extensible enough to feature such steps automatically). Alternatively, one can trivially call the same wasm packing manually after every build.

The constructor

Source code: https://github.com/paritytech/pwasm-tutorial/tree/master/step-1

When deploying a contract we often want to set its initial storage values (e.g.

totalSupply
if it's a token contact). To address this problem we are exporting another function "deploy" which executes only once on contract deployment.
// This contract will return the address from which it was deployed
#![no_std]

extern crate pwasm_ethereum; extern crate parity_hash;

use parity_hash::H256;

// The "deploy" will be executed only once on deployment but will not be stored on the blockchain #[no_mangle] pub fn deploy() { // Lets set the sender address to the contract storage at address "0" pwasm_ethereum::write(&H256::zero().into(), &H256::from(pwasm_ethereum::sender()).into()); // Note we shouldn't write any result into the call descriptor in deploy. }

// The following code will be stored on the blockchain. #[no_mangle] pub fn call() { // Will read the address of the deployer which we wrote to the storage on the deploy stage let owner = pwasm_ethereum::read(&H256::zero().into()); // Send a result pointer to the runtime pwasm_ethereum::ret(owner.as_ref()); }

Contract ABI declaration

Source code: https://github.com/paritytech/pwasm-tutorial/tree/master/step-2

Let's implement a simple ERC-20 token contract.

// ...

pub mod token { use pwasm_ethereum; use pwasm_abi::types::*;

// eth_abi is a procedural macros https://doc.rust-lang.org/book/first-edition/procedural-macros.html
use pwasm_abi_derive::eth_abi;

lazy_static! {
    static ref TOTAL_SUPPLY_KEY: H256 =
        H256::from([2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]);
}

#[eth_abi(TokenEndpoint)]
pub trait TokenInterface {
/// The constructor
    fn constructor(&mut self, _total_supply: U256);
    /// Total amount of tokens
    fn totalSupply(&mut self) -> U256;
}

pub struct TokenContract;

impl TokenInterface for TokenContract {
    fn constructor(&mut self, total_supply: U256) {
        // Set up the total supply for the token
        pwasm_ethereum::write(&TOTAL_SUPPLY_KEY, &total_supply.into());
    }

    fn totalSupply(&mut self) -> U256 {
        pwasm_ethereum::read(&TOTAL_SUPPLY_KEY).into()
    }
}

} // Declares the dispatch and dispatch_ctor methods use pwasm_abi::eth::EndpointInterface;

#[no_mangle] pub fn call() { let mut endpoint = token::TokenEndpoint::new(token::TokenContract{}); // Read http://solidity.readthedocs.io/en/develop/abi-spec.html#formal-specification-of-the-encoding for details pwasm_ethereum::ret(&endpoint.dispatch(&pwasm_ethereum::input())); }

#[no_mangle] pub fn deploy() { let mut endpoint = token::TokenEndpoint::new(token::TokenContract{}); // endpoint.dispatch_ctor(&pwasm_ethereum::input()); }

token::TokenInterface
is an interface definition of the contract.
pwasm_abi_derive::eth_abi
is a procedural macros uses a trait
token::TokenInterface
to generate decoder (
TokenEndpoint
) for payload in Solidity ABI format.
TokenEndpoint
implements an
EndpointInterface
trait:
/// Endpoint interface for contracts
pub trait EndpointInterface {
    /// Dispatch payload for regular method
    fn dispatch(&mut self, payload: &[u8]) -> Vec;

/// Dispatch constructor payload
fn dispatch_ctor(&mut self, payload: &[u8]);

}

The

dispatch
expects
payload
and returns a result in the format defined in Solidity ABI spec. It maps payload to the corresponding method of the
token::TokenInterface
implementation. The
dispatch_ctor
maps payload only to the
TokenInterface::constructor
and returns no result.

A complete implementation of ERC20 can be found here https://github.com/paritytech/pwasm-token-example.

pwasm-std

pwasm-std is a lightweight standard library. It implements common data structures, conversion utils and provides bindings to the runtime.

Make calls to other contracts

Source code: https://github.com/paritytech/pwasm-tutorial/tree/master/step-3

In order to make calls to our

TokenInterface
we need to generate the payload
TokenEndpoint::dispatch()
expects. So
pwasm_abi_derive::eth_abi
can generate an implementation of
TokenInterface
which will prepare payload for each method.
#[eth_abi(TokenEndpoint, TokenClient)]
pub trait TokenInterface {
    /// The constructor
    fn constructor(&mut self, _total_supply: U256);
    /// Total amount of tokens
    #[constant] // #[constant] hint affect the resulting JSON abi. It sets "constant": true prore
    fn totalSupply(&mut self) -> U256;
}

We've added a second argument

TokenClient
to the
eth_abi
macro as a second argument (it is optional) -- this way we ask to generate a client implementation for
TokenInterface
trait and name it as
TokenClient
.

As mentioned above, a first argument to the

eth_abi
macro requests the name for to be generated Endpoint implementation, which turns Ethereum ABI-encoded payloads into calls to the corresponding
TokenContract
methods with deserialized params.

Client (

TokenClient
), created via the second argument, is doing the opposite to endpoint, providing an implementation which generates Ethereum ABI-compatible calls (consumable by
TokenEndpoint
) for every
TokenInterface
call.

Let's suppose we've deployed a token contract on

0x7BA4324585CB5597adC283024819254345CD7C62
address. That's how we can make calls to it.
extern pwasm_ethereum;
extern pwasm_std;

use token::TokenClient; use pwasm_std::hash::Address;

let token = TokenClient::new(Address::from("0x7BA4324585CB5597adC283024819254345CD7C62")); let tokenSupply = token.totalSupply();

token.totalSupply()
will execute
pwasm_ethereum::call(Address::from("0x7BA4324585CB5597adC283024819254345CD7C62"), payload)
with
address
and
payload
generated according to
totalSupply()
signature. Optionally it's possible to set a
value
(in Wei) to transfer with the call and set a
gas
limit.
let token = TokenClient::new(Address::from("0x7BA4324585CB5597adC283024819254345CD7C62"))
    .value(10000000.into()) // send a value with the call
    .gas(21000); // set a gas limit
let tokenSupply = token.totalSupply();

If you move to

step-3
directory and run
cargo build --release --target wasm32-unknown-unknown
you will find a
TokenInterface.json
in the
target/json
generated from
TokenInterface
trait with the following content:
[
  {
    "type": "function",
    "name": "totalSupply",
    "inputs": [],
    "outputs": [
      {
        "name": "returnValue",
        "type": "uint256"
      }
    ],
    "constant": true
  },
  {
    "type": "constructor",
    "inputs": [
      {
        "name": "_total_supply",
        "type": "uint256"
      }
    ]
  }
]

JSON above is an ABI definition which can be used along with Web.js to run transactions and calls to contract:

var Web3 = require("web3");
var fs = require("fs");
var web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545"));
var abi = JSON.parse(fs.readFileSync("./target/TokenInterface.json"));
var TokenContract = new web3.eth.Contract(abi, "0x7BA4324585CB5597adC283024819254345CD7C62", { from: web3.eth.defaultAccount });
var totalSupply = TokenContract.methods.totalSupply().call().then(console.log);

Events

Source code: https://github.com/paritytech/pwasm-tutorial/tree/master/step-4

Events allow the convenient usage of the EVM logging facilities, which in turn can be used to “call” JavaScript callbacks in the user interface of a dapp, which listen for these events.

Let's implement the

transfer
method for our ERC-20 contract.
step-4
directory contains the complete implementation.
pub mod token {

use pwasm_ethereum;
use pwasm_std::types::*;

#[eth_abi(TokenEndpoint, TokenClient)]
pub trait TokenInterface {
    /// The constructor
    fn constructor(&mut self, _total_supply: U256);
    /// Total amount of tokens
    #[constant]
    fn totalSupply(&mut self) -> U256;
    /// What is the balance of a particular account?
    #[constant]
    fn balanceOf(&mut self, _owner: Address) -> U256;
    /// Transfer the balance from owner's account to another account
    fn transfer(&mut self, _to: Address, _amount: U256) -> bool;
    /// Event declaration
    #[event]
    fn Transfer(&mut self, indexed_from: Address, indexed_to: Address, _value: U256);
}

pub struct TokenContract;

impl TokenInterface for TokenContract {
    fn constructor(&mut self, total_supply: U256) {
        // ...
    }

    fn totalSupply(&mut self) -> U256 {
        // ...
    }

    fn balanceOf(&mut self, owner: Address) -> U256 {
        read_balance_of(&owner)
    }

    fn transfer(&mut self, to: Address, amount: U256) -> bool {
        let sender = pwasm_ethereum::sender();
        let senderBalance = read_balance_of(&sender);
        let recipientBalance = read_balance_of(&to);
        if amount == 0.into() || senderBalance < amount || to == sender {
            false
        } else {
            let new_sender_balance = senderBalance - amount;
            let new_recipient_balance = recipientBalance + amount;
            pwasm_ethereum::write(&balance_key(&sender), &new_sender_balance.into());
            pwasm_ethereum::write(&balance_key(&to), &new_recipient_balance.into());
            self.Transfer(sender, to, amount);
            true
        }
    }
}

// Reads balance by address
fn read_balance_of(owner: &Address) -> U256 {
    pwasm_ethereum::read(&balance_key(owner)).into()
}

// Generates a balance key for some address.
// Used to map balances with their owners.
fn balance_key(address: &Address) -> H256 {
    let mut key = H256::from(*address);
    key.as_bytes_mut()[0] = 1; // just a naive "namespace";
    key
}

}

Events are declared as part of a contract trait definition. Arguments which start with the "indexed_" prefix are considered as "topics", other arguments are data associated with an event.

#[eth_abi(TokenEndpoint, TokenClient)]
pub trait TokenInterface {
    fn transfer(&mut self, _to: Address, _amount: U256) -> bool;
    #[event]
    fn Transfer(&mut self, indexed_from: Address, indexed_to: Address, _value: U256);
}

fn transfer(&mut self, to: Address, amount: U256) -> bool { let sender = pwasm_ethereum::sender(); let senderBalance = read_balance_of(&sender); let recipientBalance = read_balance_of(&to); if amount == 0.into() || senderBalance < amount || to == sender { false } else { let new_sender_balance = senderBalance - amount; let new_recipient_balance = recipientBalance + amount; pwasm_ethereum::write(&balance_key(&sender), &new_sender_balance.into()); pwasm_ethereum::write(&balance_key(&to), &new_recipient_balance.into()); self.Transfer(sender, to, amount); true } }

Topics are useful to filter events produced by contract. In following example we use Web3.js to subscribe to the

Transfer
events of deployed
TokenContract
. ```javascript var Web3 = require("web3"); var web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545")); var abi = JSON.parse(fs.readFileSync("./target/TokenInterface.json")); var TokenContract = new web3.eth.Contract(abi, "0x7BA4324585CB5597adC283024819254345CD7C62", { from: web3.eth.defaultAccount });

// Subscribe to the Transfer event TokenContract.events.Transfer({ from: "0x7BA4324585CB5597adC283024819254345CD7C62" // Filter transactions by sender }, function (err, event) { console.log(event); }); ```

Run node and deploy contract

Now it's time to deploy our Wasm contract on the blockchain. We can either test in own local development chain or publish it on the public Kovan network.

Option 1: Setup and run development node

Parity 1.9.5 includes support for running Wasm contracts. See instructions on how to setup a Wasm-enabled dev node.

Option 2: Run Kovan node

Kovan network supports Wasm contracts. This will run Parity node on Kovan:

bash
parity --chain kovan
When it syncs up follow https://github.com/kovan-testnet/faucet to set up an account with some Kovan ETH to be able to pay gas for transactions.

Deploy

Let Parity run in a separate terminal window.

Now cd to

step-5
and build the contract:
bash
./build.sh
It should produce 2 files we need: - a compiled Wasm binary
./target/pwasm_tutorial_contract.wasm
- an ABI file:
./target/json/TokenInterface.json

At this point we can use Web.js to connect to the Parity node and deploy Wasm

pwasm_tutorial_contract.wasm
. Run the following code in
node
console:
var Web3 = require("web3");
var fs = require("fs");
// Connect to our local node
var web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545"));
// NOTE: if you run Kovan node there should be an address you've got in the "Option 2: Run Kovan node" step
web3.eth.defaultAccount = "0x004ec07d2329997267ec62b4166639513386f32e";
// read JSON ABI
var abi = JSON.parse(fs.readFileSync("./target/json/TokenInterface.json"));
// convert Wasm binary to hex format
var codeHex = '0x' + fs.readFileSync("./target/pwasm_tutorial_contract.wasm").toString('hex');

var TokenContract = new web3.eth.Contract(abi, { data: codeHex, from: web3.eth.defaultAccount });

var TokenDeployTransaction = TokenContract.deploy({data: codeHex, arguments: [10000000]});

// Will create TokenContract with totalSupply = 10000000 and print a result web3.eth.personal.unlockAccount(web3.eth.defaultAccount, "user").then(() => TokenDeployTransaction.estimateGas()).then(gas => TokenDeployTransaction.send({gasLimit: gas, from: web3.eth.defaultAccount})).then(contract => { console.log("Address of new contract: " + contract.options.address); TokenContract = contract; }).catch(err => console.log(err));

Now we're able transfer some tokens:

javascript
web3.eth.personal.unlockAccount(web3.eth.defaultAccount, "user").then(() => TokenContract.methods.transfer("0x7BA4324585CB5597adC283024819254345CD7C62", 200).send()).then(console.log).catch(console.log);

And check balances: ```javascript // Check balance of recipient. Should print 200 TokenContract.methods.balanceOf("0x7BA4324585CB5597adC283024819254345CD7C62").call().then(console.log).catch(console.log);

// Check balance of sender (owner of the contract). Should print 10000000 - 200 = 9999800 TokenContract.methods.balanceOf(web3.eth.defaultAccount).call().then(console.log).catch(console.log); ```

Testing

pwasm-test makes it easy to test a contract's logic. It allows to emulate the blockchain state and mock any pwasm-ethereum call.

By default our contracts built with

#![no_std]
, but
rust test
needs the Rust stdlib for threading and I/O. Thus, in order to run tests we've added a following feature gate in Cargo.toml:
[features]
std = ["pwasm-std/std", "pwasm-ethereum/std"]

Now you can

cd step-5
and
cargo test --features std
should pass.

Take a look https://github.com/paritytech/pwasm-tutorial/blob/master/step-5/src/lib.rs#L116-L161 to see an example how to test a

transfer
method of our token contract.
#[cfg(test)]
#[allow(non_snake_case)]
mod tests {
    extern crate pwasm_test;
    extern crate std;
    use super::*;
    use self::pwasm_test::{ext_reset, ext_get};
    use parity_hash::Address;
    use token::TokenInterface;

#[test]
fn should_succeed_transfering_1000_from_owner_to_another_address() {
    let mut contract = token::TokenContract{};
    let owner_address = Address::from("0xea674fdde714fd979de3edf0f56aa9716b898ec8");
    let sam_address = Address::from("0xdb6fd484cfa46eeeb73c71edee823e4812f9e2e1");
    // Here we're creating an External context using ExternalBuilder and set the `sender` to the `owner_address`
    // so `pwasm_ethereum::sender()` in TokenContract::constructor() will return that `owner_address`
    ext_reset(|e| e.sender(owner_address.clone()));
    let total_supply = 10000.into();
    contract.constructor(total_supply);
    assert_eq!(contract.balanceOf(owner_address), total_supply);
    assert_eq!(contract.transfer(sam_address, 1000.into()), true);
    assert_eq!(contract.balanceOf(owner_address), 9000.into());
    assert_eq!(contract.balanceOf(sam_address), 1000.into());
    // 1 log entry should be created
    assert_eq!(ext_get().logs().len(), 1);
}

}

Here you can find more examples on how to: - mock calls to other contracts - read event logs created by contract - init contract with storage.

More testing examples: https://github.com/paritytech/pwasm-token-example/blob/master/contract/src/lib.rs#L194

In order to test the interaction between contracts, we're able to mock callee contract client. See comprehensive here: https://github.com/paritytech/pwasm-repo-contract/blob/master/contract/src/lib.rs#L453

We use cookies. If you continue to browse the site, you agree to the use of cookies. For more information on our use of cookies please see our Privacy Policy.