Inkpad

Inkpad provides an enviroment for running ink! contract outside of substrate.

Quickstart

0. Install cargo-contract

cargo install cargo-contract

1. Install inkpad

cargo install inkpad

2. Run cargo contract new

cargo contract new flipper

3. Compile with debug info

cargo contract build

4. Test flipper

inkpad target/ink/flipper.contract info

Prerequisites

First you'll want to install the inkpad CLI, and wasm-pack -V should print the version that you just installed.

Next, you'll need to donwload cargo-contract for compiling your ink! contract to wasm binaries.

Finally, use the commands of the CLI or follow the Tutorials to start your journey.

ink! contract

Since inkpad is an execution of ink! contract, we need to have an ink! contract first of all.

0. Download cargo-contract

For generating an ink! contract, we need to download cargo-contract

cargo install cargo-contract --force

1. New ink! contract

Once we have cargo-contract installed in our machine, we can run

cargo new my-ink-contract

to generate a template ink contract.

2. Compile ink! contract

cd my-ink-contract
cargo contract build

patractlabs/cargo-contract

For compiling ink! contract with debug info, we can use patractlabs/cargo-contract along with inkpad.

0. Installation

cargo install --git https://github.com/patractlabs/cargo-contract.git --branch tag-v0.12.1 --force

1. Compile with debug info

 𝝺 cargo contract build -h
cargo-contract-build 0.10.0
Executes build of the smart-contract which produces a wasm binary that is ready for deploying.

It does so by invoking `cargo build` and then post processing the final binary.

USAGE:
    cargo contract build [FLAGS] [OPTIONS]

FLAGS:
    -d, --debug      Enable debug info in the wasm bundle
    -h, --help       Prints help information
        --quiet      No output printed to stdout
    -V, --version    Prints version information
        --verbose    Use verbose output

OPTIONS:
        --generate <all | code-only>       Which build artifacts to generate. [default: all]
        --manifest-path <manifest-path>    Path to the Cargo.toml of the contract to build
    -Z, --unstable-options <options>...    Use the original manifest (Cargo.toml), do not modify for build optimizations

See the --debug flag, run cargo build -d in the contract path will embed debug info in the wasm of our contract.

CLI

inkpad has an CLI implementation to help you debug contracts in command line.

 𝝺 inkpad
inkpad 0.2.0
Inkpad command tool

USAGE:
    inkpad [*.contract | name | code-hash] <SUBCOMMAND>

FLAGS:
    -h, --help       Prints help information
    -V, --version    Prints version information

ARGS:
    <*.contract | name | code-hash>    If empty, inkpad will load the last contract which has been executed

SUBCOMMANDS:
    call      Calls a call method
    deploy    Calls a deploy method
    help      Prints this message or the help of the given subcommand(s)
    info      Prints info of *.contract
    list      Lists all contracts

Arguments

Once we use *.contract as the argument of inkpad, inkpad will load and record the target contract in database(~/.inkpad) by default.

inkpad call

The inkpad call command call a method of the supplied contract.

 𝝺 inkpad call -h
inkpad-call
Calls a call method

USAGE:
    inkpad call [OPTIONS] <method>

FLAGS:
    -h, --help       Prints help information
    -V, --version    Prints version information

OPTIONS:
        --address <address>                        Contract callee
    -b, --balance <balance>                        contract balance
        --caller <caller>                          Contract caller
    -m, --minimum-balance <minimum-balance>        minimum balance
    -n, --now <now>                                current time
    -a, --args <string,>...                        Arguments
    -v, --value-transferred <value-transferred>    transferred value

ARGS:
    <method>    Calling method

The options of method call are destructed from Transaction

address

Callee address

balance

Contract balance

caller

Caller address

minimum-balance

minimum_balance in transaction

now

Transaction time

args

Transaction arguments, should be parity-scale-codec format

value

Transferred value

inkpad deploy

The inkpad deploy command deploy the supplied contract with constructor, like inkpad call

 𝝺 inkpad deploy -h
inkpad-deploy
Calls a deploy method

USAGE:
    inkpad deploy [OPTIONS] <method>

FLAGS:
    -h, --help       Prints help information
    -V, --version    Prints version information

OPTIONS:
        --address <address>                        Contract callee
    -b, --balance <balance>                        contract balance
        --caller <caller>                          Contract caller
    -m, --minimum-balance <minimum-balance>        minimum balance
    -n, --now <now>                                current time
    -a, --args <string,>...                        Arguments
    -v, --value-transferred <value-transferred>    transferred value

ARGS:
    <method>    Calling method

The options of method deploy are destructed from Transaction

address

Callee address

balance

Contract balance

caller

Caller address

minimum-balance

minimum_balance in transaction

now

Transaction time

args

Transaction arguments, should be parity-scale-codec format

value

Transferred value

inkpad info

Shows info of the current contract

 𝝺 inkpad info -h
inkpad-info 0.2.0
Prints info of *.contract

USAGE:
    inkpad info

FLAGS:
    -h, --help       Prints help information
    -V, --version    Prints version information

This will print the info of the last contract inkpad rememberred or we can execute this with contract argument.

inkpad list

Lists loaded contracts

 𝝺 inkpad list -h
inkpad-list 0.2.0
Lists all contracts

USAGE:
    inkpad list

FLAGS:
    -h, --help       Prints help information
    -V, --version    Prints version information

for example

 𝝺 inkpad list

	contract             code-hash
	---------------------------------------------------------------------------------------
	delegator            0x85970b066ab92b4495bba682917ded604019a6fce9b247c36b35e8c967287f4f
	accumulator          0x906811f0dca85909fd267df762c09de7a6af62cd1f1aa73d3778d58d308fd376
	adder                0xc60c90582f3e6767d9b8cca5b83f747b8b86afb8c425b2e23dfad5e4784409e1
	subber               0xcc9b22fda1a527754190c8833b919a26cf440bd9aca7f0e5810104f09a8e0b82

The code-hash displayed could be used in call-contracts

Tutorials

We have three tutorials that help you get started with inkpad:

debug ink! contract

The goal of this tutorial is to introduce you to use inkpad for debuging ink! contract.

Be sure to have read and followed the Prerequisites.

Getting Started

0. Compile ink! contract with debug info

For cargo-contract, see 3.2.

1. Introduce Trap in inkpad


#![allow(unused)]
fn main() {
/// A trap code describing the reason for a trap.
///
/// All trap instructions have an explicit trap code.
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum TrapCode {
    /// The current stack space was exhausted.
    StackOverflow,

    /// An out-of-bounds memory access.
    MemoryOutOfBounds,

    /// A wasm atomic operation was presented with a not-naturally-aligned linear-memory address.
    HeapMisaligned,

    /// An out-of-bounds access to a table.
    TableOutOfBounds,

    /// Indirect call to a null table entry.
    IndirectCallToNull,

    /// Signature mismatch on indirect call.
    BadSignature,

    /// An integer arithmetic operation caused an overflow.
    IntegerOverflow,

    /// An integer division by zero.
    IntegerDivisionByZero,

    /// Failed float-to-int conversion.
    BadConversionToInteger,

    /// Code that was supposed to have been unreachable was reached.
    UnreachableCodeReached,

    /// Execution has potentially run too long and may be interrupted.
    Interrupt,

    /// HostError
    HostError(Box<Error>),

    // Unknown Error
    Unknown,

    // Termination
    Termination,

    // Restoration
    Restoration,
}

}

inkpad supports Trap both from wasmi and wasmtime

2. Embed panic in contract


#![allow(unused)]
#![cfg_attr(not(feature = "std"), no_std)]

fn main() {
use ink_lang as ink;

#[ink::contract]
mod flipper_trap {
    #[ink(storage)]
    pub struct FlipperTrap {
        value: bool,
    }

    impl FlipperTrap {
        #[ink(constructor)]
        pub fn new(init_value: bool) -> Self {
            Self { value: init_value }
        }

        #[ink(constructor)]
        pub fn default() -> Self {
            Self::new(Default::default())
        }

        #[ink(message)]
        pub fn flip(&mut self) {
            panic!("trap here");
            self.value = !self.value;
        }

        /// Simply returns the current value of our `bool`.
        #[ink(message)]
        pub fn get(&self) -> bool {
            self.value
        }
    }
}

}

3. Catch the trap in contract


#![allow(unused)]
fn main() {
#[test]
fn test_flipper_trap() {
    // Assume we compile the contract in `2.` to `flipper_trap.contract`
    let mut rt = Runtime::contract(
        include_bytes!("flipper_trap.contract"),
        Some(Instance),
    )
    .expect("Create runtime failed");

    rt.deploy("default", vec![], None).expect("Deploy failed");
    assert_eq!(rt.call("get", vec![], None), Ok(Some(vec![0])));

    if let Some(inkpad_runtime::Error::CallContractFailed {
        error: inkpad_executor::Error::Trap(Trap { code, .. }),
    }) = rt.call("flip", vec![], None).err()
    {
        assert_eq!(code, TrapCode::UnreachableCodeReached);
    } else {
        panic!("Call flipper_trap with unexpected error");
    }
}

}

write tests for ink! contract

In this chapter, we'll introduce how to write tests of ink! contract with inkpad, as an ink! contract executor, it is really easy.

Getting Started

0. Template contract

First of all, we need an ink! contract, flipper here.

cargo contract new flipper

1. add build.rs to our contract

// flipper/build.rs
use std::process::Command;

fn main() {
    println!("cargo:rerun-if-changed=src/lib.rs");
    Command::new("cargo").args(&["contract", "build"]);
}

2. add inkpad as dev-dependencies

[dev-dependencies]
inkpad = "^0"

3. write tests with inkpad


#![allow(unused)]
fn main() {
// /tests/flipper.rs
use inkpad_executor::{Trap, TrapCode};
use inkpad_ri::Instance;
use inkpad_runtime::Runtime;
use parity_scale_codec::Encode;

#[test]
fn test_flipper() {
    let mut rt = Runtime::contract(
        include_bytes!("../contracts/flipper.contract"),
        Some(Instance),
    )
    .expect("Create runtime failed");

    rt.deploy("default", vec![], None).expect("Deploy failed");
    assert_eq!(rt.call("get", vec![], None), Ok(Some(vec![0])));

    rt.deploy("new", vec![true.encode()], None)
        .expect("Deploy failed");
    assert_eq!(rt.call("get", vec![], None), Ok(Some(vec![1])));

    rt.call("flip", vec![], None).expect("Call contract failed");
    assert_eq!(rt.call("get", vec![], None), Ok(Some(vec![0])));
}
}

4. run tests

# flipper/
cargo test

run ink! contract in browser

Inkpad supports no_std which means we can run ink! contract in browser with inkpad as well.

This chapter will introduce how to run ink! contract in browser!

Getting Started

0. prepare your ink! contract

Please follow the instructions of prerequisites/ink! contract to generate an ink! contract.

1. use @patract/inkpad-browser in your package

{
  dependencies: {
    "@patract/inkpad-browser": "^0.1.4"
  }
}

2. run ink! contract with inkpad

import { Runtime } from "@inkpad/browser";

// this flipper.contract is the output after `0.`
// under /target/ink/flipper.contract
import CONTRACT from "flipper.contract";

(async () => {
    const rt = new Runtime(contract.toString());
    // arguments of call or deploy should be 
    // parity-scale-codec encoded
    rt.deploy("default", []);
    rt.call("default", []);
});

The usage of @patract/inkpad-browser is as same as the rust exports, more functions please check docs.rs/inkpad.

write your implementation of inkpad

inkpad supports both wasmi and wasmtime as wasm executor, which means you can run ink! contract with inkpad anywhere.

When should I use inkpad with wsmtime feature?

wasmtime is the default and recommanded executor of inkpad, fast and full features.

When should I use inkpad with wasmi feature?

If you have requirements running inkpad in browser or some IoT devices, you don't want any std dependencies of rust, you need to use inkpad with wasmi feature.

Why customized implementation?

inkpad supports the seal functions provided by pallet-contracts of substrate by default, so if you want to use inkpad in some special devices, you need to re-implement the seal functions yourselves.

Getting Started

0. implement your storage

The Storage trait in inkpad/crate/support is the entry of inkpad storage, first of all, we need to construct a storage for our implementation.

For example, the storage implementation of inkpad-cli is like:


#![allow(unused)]
fn main() {
use inkpad_support::traits::{self, Cache, Frame};

/// A inkpad storage implementation using sled
#[derive(Clone)]
pub struct Storage {
    pub db: Db,
    cache: Tree,
    frame: Vec<Rc<RefCell<State<Memory>>>>,
    state: HostState,
}

impl traits::Storage for Storage {
    fn set(&mut self, key: Vec<u8>, value: Vec<u8>) -> Option<Vec<u8>> {
        self.cache.insert(key, value).ok()?.map(|v| v.to_vec())
    }

    fn remove(&mut self, key: &[u8]) -> Option<Vec<u8>> {
        self.cache.remove(key).ok()?.map(|v| v.to_vec())
    }

    fn get(&self, key: &[u8]) -> Option<Vec<u8>> {
        self.cache.get(key).ok()?.map(|v| v.to_vec())
    }
}

impl Cache<Memory> for Storage {
    fn frame(&self) -> &Vec<Rc<RefCell<State<Memory>>>> {
        &self.frame
    }

    fn frame_mut(&mut self) -> &mut Vec<Rc<RefCell<State<Memory>>>> {
        &mut self.frame
    }

    fn memory(&self) -> Option<Memory> {
        Some(self.frame.last()?.borrow().memory.clone())
    }

    /// Flush data
    fn flush(&mut self) -> Option<()> {
        for state in self.frame.iter() {
            let state = state.borrow().clone();
            self.state.insert(state.hash, state.state);
        }

        let mut data = if let Some(state) = self.db.get(PREVIOUS_STATE).ok()? {
            HostState::decode(&mut state.as_ref()).ok()?
        } else {
            BTreeMap::new()
        };

        data.append(&mut self.state.clone());
        self.db.insert(PREVIOUS_STATE, data.encode()).ok()?;
        self.db.flush().ok()?;
        Some(())
    }
}

impl Frame<Memory> for Storage {}
}

1. construct your seal calls

we need to construct seal calls for different platforms, for example, inkpad-cli use system interfaces, inkpad-browser use browser interfaces.

For example, the seal calls of inkpad-browser is like


#![allow(unused)]
fn main() {
use inkpad_sandbox::{RuntimeInterfaces, Sandbox};

/// Browser interface
pub struct Interface;

impl RuntimeInterfaces for Interface {
    /// Println
    fn seal_println(sandbox: &mut Sandbox, args: &[Value]) -> Ret {
        if args.len() != 2 {
            return Err(Error::WrongArugmentLength);
        }

        let data = sandbox.read_sandbox_memory(args[0].into(), args[1].into())?;
        if let Ok(utf8) = core::str::from_utf8(&data) {
            log(utf8);
        }

        Ok(None)
    }

    /// Generate random value
    fn seal_random(sandbox: &mut Sandbox, args: &[Value]) -> Ret {
        if args.len() != 4 {
            return Err(Error::WrongArugmentLength);
        }
        let subject_ptr = args[0].into();
        let subject_len = args[1].into();
        let output_ptr: u32 = args[2].into();
        let output_len: u32 = args[2].into();

        // random
        let mut dest: [u8; 1] = [0];
        err_check(getrandom(&mut dest));
        let mut subject_buf = sandbox
            .read_sandbox_memory(subject_ptr, subject_len)?
            .to_vec();
        subject_buf.push(dest[0]);

        let output = blake2b::blake2b(32, &[], &subject_buf);
        sandbox.write_sandbox_output(output_ptr, output_len, output.as_bytes())?;
        Ok(None)
    }

    /// sha2 256
    fn seal_hash_sha2_256(sandbox: &mut Sandbox, args: &[Value]) -> Ret {
        if args.len() != 3 {
            return Err(Error::WrongArugmentLength);
        }
        let input_ptr = args[0].into();
        let input_len = args[1].into();
        let output_ptr = args[2].into();

        // hash
        let mut dest: [u8; 32] = [0; 32];
        let mut hasher = Sha256::new();
        let input = sandbox.read_sandbox_memory(input_ptr, input_len)?;
        hasher.update(&input);
        dest.copy_from_slice(&hasher.finalize());
        sandbox.write_sandbox_memory(output_ptr, dest.as_ref())?;

        // result
        Ok(None)
    }

    /// keccak 256
    fn seal_hash_keccak_256(sandbox: &mut Sandbox, args: &[Value]) -> Ret {
        if args.len() != 3 {
            return Err(Error::WrongArugmentLength);
        }
        let input_ptr = args[0].into();
        let input_len = args[1].into();
        let output_ptr = args[2].into();

        // hash
        let mut dest: [u8; 32] = [0; 32];
        let mut keccak = Keccak::v256();
        let input = sandbox.read_sandbox_memory(input_ptr, input_len)?;
        keccak.update(&input);
        keccak.finalize(&mut dest);
        sandbox.write_sandbox_memory(output_ptr, dest.as_ref())?;

        // result
        Ok(None)
    }

    /// blake2 256
    fn seal_hash_blake2_256(sandbox: &mut Sandbox, args: &[Value]) -> Ret {
        if args.len() != 3 {
            return Err(Error::WrongArugmentLength);
        }
        let input_ptr = args[0].into();
        let input_len = args[1].into();
        let output_ptr = args[2].into();

        // hash
        let mut dest = [0; 32];
        let input = sandbox.read_sandbox_memory(input_ptr, input_len)?;
        dest.copy_from_slice(blake2b::blake2b(32, &[], &input).as_bytes());
        sandbox.write_sandbox_memory(output_ptr, dest.as_ref())?;

        // result
        Ok(None)
    }

    /// blake2 128
    fn seal_hash_blake2_128(sandbox: &mut Sandbox, args: &[Value]) -> Ret {
        if args.len() != 3 {
            return Err(Error::WrongArugmentLength);
        }
        let input_ptr = args[0].into();
        let input_len = args[1].into();
        let output_ptr = args[2].into();

        // hash
        let mut dest = [0; 16];
        let input = sandbox.read_sandbox_memory(input_ptr, input_len)?;
        dest.copy_from_slice(blake2b::blake2b(16, &[], &input).as_bytes());
        sandbox.write_sandbox_memory(output_ptr, dest.as_ref())?;

        // result
        Ok(None)
    }
}

}

2. Build Runtime with your implementations

/// This Instance has `RuntimeInterfaces` trait bundled
use my_runtime_interfaces::Instance;
use my_storage::Storage;

fn main() {
    let storage = Storage::new();
    let rt = Runtime::from_contract(&source, storage, Some(Instance));
}

Here we go, a new implementation of inkpad.