Skip to content
This repository has been archived by the owner on Feb 14, 2021. It is now read-only.

Latest commit

 

History

History
504 lines (405 loc) · 20.7 KB

README.md

File metadata and controls

504 lines (405 loc) · 20.7 KB

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:

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<u8>;

	/// 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.

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:

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:

./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:

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

And check balances:

// 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:

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