...

Ubuntu 20.04 Installation

This installs several dependencies then downloads and extracts both wasi-sdk and clsdk. wasi-sdk provides clang and other tools and provides the C and C++ runtime libraries built for WASM. clsdk provides libraries and tools for working with eosio.

For convenience, consider adding the environment variables below to ~/.bashrc or whatever is appropriate for the shell you use.

sudo apt-get update
sudo apt-get install -yq    \
    binaryen                \
    build-essential         \
    cmake                   \
    gdb                     \
    git                     \
    libboost-all-dev        \
    libcurl4-openssl-dev    \
    libgmp-dev              \
    libssl-dev              \
    libusb-1.0-0-dev        \
    pkg-config              \
    wget

export WASI_SDK_PREFIX=~/work/wasi-sdk-12.0
export CLSDK_PREFIX=~/work/clsdk

export PATH=$CLSDK_PREFIX/bin:$PATH

cd ~/work
wget https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-12/wasi-sdk-12.0-linux.tar.gz
tar xf wasi-sdk-12.0-linux.tar.gz

cd ~/work
wget https://github.com/eoscommunity/Eden/releases/download/sdk-v0.2.0-alpha/clsdk-ubuntu-20-04.tar.gz
tar xf clsdk-ubuntu-20-04.tar.gz

Basic Contract

Here is a basic contract definition. Place example.cpp and CMakeLists.txt in an empty folder.

example.cpp

#include <eosio/asset.hpp>
#include <eosio/eosio.hpp>

// The contract class must be in a namespace
namespace example
{
   // The contract
   struct example_contract : public eosio::contract
   {
      // Use the base class constructors
      using eosio::contract::contract;

      // Action: user buys a dog
      void buydog(eosio::name user, eosio::name dog, const eosio::asset& price)
      {
         // TODO: buy a dog
      }
   };

   // First part of the dispatcher
   EOSIO_ACTIONS(example_contract,  //
                 "example"_n,       //
                 action(buydog, user, dog, price))
}  // namespace example

// Final part of the dispatcher
EOSIO_ACTION_DISPATCHER(example::actions)

// ABI generation
EOSIO_ABIGEN(actions(example::actions))

CMakeLists.txt

# All cmake projects need these
cmake_minimum_required(VERSION 3.16)
project(example)

# clsdk requires C++20
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# Libraries for building contracts and tests
find_package(clsdk REQUIRED)

# Build example.wasm contract
add_executable(example example.cpp)
target_link_libraries(example eosio-contract-simple-malloc)

# Generate example.abi
# This is a 2-step process:
#   * Build example.abi.wasm. This must link to eosio-contract-abigen.
#   * Run the wasm to generate the abi
add_executable(example-abigen example.cpp)
target_link_libraries(example-abigen eosio-contract-abigen)
add_custom_command(TARGET example-abigen POST_BUILD
    COMMAND cltester example-abigen.wasm >example.abi
)

# These symlinks help vscode
execute_process(COMMAND ln -sf ${clsdk_DIR} ${CMAKE_CURRENT_BINARY_DIR}/clsdk)
execute_process(COMMAND ln -sf ${WASI_SDK_PREFIX} ${CMAKE_CURRENT_BINARY_DIR}/wasi-sdk)

# Generate compile_commands.json to aid vscode and other editors
set(CMAKE_EXPORT_COMPILE_COMMANDS on)

Building

This will create example.wasm and example.abi:

mkdir build
cd build
cmake `clsdk-cmake-args` ..
make -j $(nproc)

Trying the contract

clsdk comes with nodeos, cleos, and keosd. The following will execute the contract:

# Start keosd on an empty directory
killall keosd
rm -rf testing-wallet testing-wallet-password
mkdir testing-wallet
keosd --wallet-dir `pwd`/testing-wallet --unlock-timeout 99999999 >keosd.log 2>&1 &

# Create a default wallet. This saves the password in testing-wallet-password
cleos wallet create -f testing-wallet-password

# Add the default development key
cleos wallet import --private-key 5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3

# Start up a fresh chain
killall nodeos
rm -rf data config
nodeos -d data --config-dir config --plugin eosio::chain_api_plugin --plugin eosio::producer_api_plugin -e -p eosio >nodeos.log 2>&1 &

# Install the contract
cleos create account eosio example EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV
cleos set abi example example.abi
cleos set code example example.wasm

# Try out the contract (does nothing)
cleos push action example buydog '["eosio", "fido", "100.0000 EOS"]' -p eosio

vscode support

The following files configure vscode:

Code completion and symbol lookup does not work until the project is built (above).

Tables

Here is a contract that uses a table. Place table.cpp and CMakeLists.txt in an empty folder.

table.cpp

#include <eosio/asset.hpp>
#include <eosio/eosio.hpp>

namespace example
{
   // A purchased animal
   struct animal
   {
      eosio::name name;             // Name of animal
      eosio::name type;             // Type of animal
      eosio::name owner;            // Who owns the animal
      eosio::asset purchase_price;  // How much the owner paid

      uint64_t primary_key() const { return name.value; }
   };

   // This does 2 things:
   // * Controls which fields are stored in the table
   // * Lets the ABI generator know the field names
   EOSIO_REFLECT(animal, name, type, owner, purchase_price)

   // Table definition
   typedef eosio::multi_index<"animal"_n, animal> animal_table;

   struct example_contract : public eosio::contract
   {
      using eosio::contract::contract;

      // Action: user buys a dog
      void buydog(eosio::name user, eosio::name dog, const eosio::asset& price)
      {
         require_auth(user);
         animal_table table{get_self(), get_self().value};
         table.emplace(user, [&](auto& record) {
            record.name = dog;
            record.type = "dog"_n;
            record.owner = user;
            record.purchase_price = price;
         });
      }
   };

   EOSIO_ACTIONS(example_contract,  //
                 "example"_n,       //
                 action(buydog, user, dog, price))
}  // namespace example

EOSIO_ACTION_DISPATCHER(example::actions)

EOSIO_ABIGEN(
    // Include the contract actions in the ABI
    actions(example::actions),

    // Include the table in the ABI
    table("animal"_n, example::animal))

Additional files

Building

This will create table.wasm and table.abi:

mkdir build
cd build
cmake `clsdk-cmake-args` ..
make -j $(nproc)

Trying the contract

# Create some users
cleos create account eosio alice EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV
cleos create account eosio bob EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV

# Install the contract
cleos create account eosio table EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV
cleos set abi table table.abi
cleos set code table table.wasm

# Try out the contract
cleos push action table buydog '["alice", "fido", "100.0000 EOS"]' -p alice
cleos push action table buydog '["alice", "rex", "120.0000 EOS"]' -p alice
cleos push action table buydog '["bob", "lambo", "70.0000 EOS"]' -p bob

# See the purchased animals
cleos get table table table animal

Notifications

This contract adds the following capabilities to the previous examples:

  • Receives notifications from eosio.token and tracks user balances
  • Deducts from the user balance whenever the user buys a dog

This example does not cover:

  • Removing empty balance records
  • Returning excess funds to users
  • Protecting against dust attacks on the balance table
  • Treating incoming funds from system accounts as special (e.g. unstaking, selling rex, selling ram)

Place notify.cpp and CMakeLists.txt in an empty folder.

notify.cpp

#include <eosio/asset.hpp>
#include <eosio/eosio.hpp>

namespace example
{
   // Keep track of deposited funds
   struct balance
   {
      eosio::name owner;
      eosio::asset balance;

      uint64_t primary_key() const { return owner.value; }
   };
   EOSIO_REFLECT(balance, owner, balance)
   typedef eosio::multi_index<"balance"_n, balance> balance_table;

   // A purchased animal
   struct animal
   {
      eosio::name name;
      eosio::name type;
      eosio::name owner;
      eosio::asset purchase_price;

      uint64_t primary_key() const { return name.value; }
   };
   EOSIO_REFLECT(animal, name, type, owner, purchase_price)
   typedef eosio::multi_index<"animal"_n, animal> animal_table;

   struct example_contract : public eosio::contract
   {
      using eosio::contract::contract;

      // eosio.token transfer notification
      void notify_transfer(eosio::name from,
                           eosio::name to,
                           const eosio::asset& quantity,
                           std::string memo)
      {
         // Only track incoming transfers
         if (from == get_self())
            return;

         // The dispatcher has already checked the token contract.
         // We need to check the token type.
         eosio::check(quantity.symbol == eosio::symbol{"EOS", 4},
                      "This contract does not deal with this token");

         // Record the change
         add_balance(from, quantity);
      }

      // Action: user buys a dog
      void buydog(eosio::name user, eosio::name dog, const eosio::asset& price)
      {
         require_auth(user);
         eosio::check(price.symbol == eosio::symbol{"EOS", 4},
                      "This contract does not deal with this token");
         eosio::check(price.amount >= 50'0000, "Dogs cost more than that");
         sub_balance(user, price);
         animal_table table{get_self(), get_self().value};
         table.emplace(user, [&](auto& record) {
            record.name = dog;
            record.type = "dog"_n;
            record.owner = user;
            record.purchase_price = price;
         });
      }

      // This is not an action; it's a function internal to the contract
      void add_balance(eosio::name owner, const eosio::asset& quantity)
      {
         balance_table table(get_self(), get_self().value);
         auto record = table.find(owner.value);
         if (record == table.end())
            table.emplace(get_self(), [&](auto& a) {
               a.owner = owner;
               a.balance = quantity;
            });
         else
            table.modify(record, eosio::same_payer, [&](auto& a) { a.balance += quantity; });
      }

      // This is not an action; it's a function internal to the contract
      void sub_balance(eosio::name owner, const eosio::asset& quantity)
      {
         balance_table table(get_self(), get_self().value);
         const auto& record = table.get(owner.value, "user does not have a balance");
         eosio::check(record.balance.amount >= quantity.amount, "not enough funds deposited");
         table.modify(record, owner, [&](auto& a) { a.balance -= quantity; });
      }
   };

   EOSIO_ACTIONS(example_contract,
                 "example"_n,
                 notify("eosio.token"_n, transfer),  // Hook up notification
                 action(buydog, user, dog, price))
}  // namespace example

EOSIO_ACTION_DISPATCHER(example::actions)

EOSIO_ABIGEN(actions(example::actions),
             table("balance"_n, example::balance),
             table("animal"_n, example::animal))

Additional files

Building

This will create notify.wasm and notify.abi:

mkdir build
cd build
cmake `clsdk-cmake-args` ..
make -j $(nproc)

Trying the contract

# Create some users
cleos create account eosio alice EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV
cleos create account eosio bob EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV

# Set up eosio.token
# Note: the build system created a symlink to clsdk for easy access to the token contract
cleos create account eosio eosio.token EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV
cleos set abi eosio.token clsdk/contracts/token.abi
cleos set code eosio.token clsdk/contracts/token.wasm

cleos push action eosio.token create '["eosio", "1000000000.0000 EOS"]' -p eosio.token
cleos push action eosio.token issue '["eosio", "1000000000.0000 EOS", ""]' -p eosio
cleos push action eosio.token open '["alice", "4,EOS", "alice"]' -p alice
cleos push action eosio.token open '["bob", "4,EOS", "bob"]' -p bob
cleos push action eosio.token transfer '["eosio", "alice", "10000.0000 EOS", "have some"]' -p eosio
cleos push action eosio.token transfer '["eosio", "bob", "10000.0000 EOS", "have some"]' -p eosio

# Install the contract
cleos create account eosio notify EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV
cleos set abi notify notify.abi
cleos set code notify notify.wasm

# Try out the contract
cleos push action eosio.token transfer '["alice", "notify", "300.0000 EOS", "for purchases"]' -p alice
cleos push action eosio.token transfer '["bob", "notify", "300.0000 EOS", "for purchases"]' -p bob

cleos push action notify buydog '["alice", "fido", "100.0000 EOS"]' -p alice
cleos push action notify buydog '["alice", "rex", "120.0000 EOS"]' -p alice
cleos push action notify buydog '["bob", "lambo", "70.0000 EOS"]' -p bob

# See the remaining balances and the purchased animals
cleos get table notify notify balance
cleos get table notify notify animal

Debugging

This contract is identical to the one in Notifications, except it extends CMakeLists.txt to build notify-debug.wasm and it has an additional config file (launch.json).

CMakeLists.txt

# All cmake projects need these
cmake_minimum_required(VERSION 3.16)
project(notify)

# clsdk requires C++20
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# Libraries for building contracts and tests
find_package(clsdk REQUIRED)

# Build notify.wasm contract
add_executable(notify notify.cpp)
target_link_libraries(notify eosio-contract-simple-malloc)

# Build notify-debug.wasm
# This is like notify.wasm, but includes debugging information.
add_executable(notify-debug notify.cpp)
target_link_libraries(notify-debug eosio-contract-simple-malloc-debug)

# Generate notify.abi
# This is a 2-step process:
#   * Build notify.abi.wasm. This must link to eosio-contract-abigen.
#   * Run the wasm to generate the abi
add_executable(notify-abigen notify.cpp)
target_link_libraries(notify-abigen eosio-contract-abigen)
add_custom_command(TARGET notify-abigen POST_BUILD
    COMMAND cltester notify-abigen.wasm >notify.abi
)

# These symlinks help vscode
execute_process(COMMAND ln -sf ${clsdk_DIR} ${CMAKE_CURRENT_BINARY_DIR}/clsdk)
execute_process(COMMAND ln -sf ${WASI_SDK_PREFIX} ${CMAKE_CURRENT_BINARY_DIR}/wasi-sdk)

# Generate compile_commands.json to aid vscode and other editors
set(CMAKE_EXPORT_COMPILE_COMMANDS on)

Additional files

Building

This will create notify.wasm, notify-debug.wasm, and notify.abi:

mkdir build
cd build
cmake `clsdk-cmake-args` ..
make -j $(nproc)

Nodeos debug_plugin

debug_plugin (included in the nodeos binary that comes with clsdk) adds these new capabilities to nodeos:

  • Wasm substitution (--subst contract.wasm:debug.wasm). This instructs nodeos to execute debug.wasm whenever it would otherwise execute contract.wasm. nodeos identifies wasms by hash, so this affects all accounts which have the same wasm installed.
  • Relaxed wasm limits. Debugging wasms are usually much larger than normal contract wasms. debug_plugin removes eosio wasm limits to allow the larger wasms to execute. They are also slower, so it also removes execution time limits.
  • Debug info support. It transforms wasm debug info into native debug info. This enables gdb to debug executing contracts.

Only substituted wasms get the relaxed limits and debug info support.

Caution: debug_plugin intentionally breaks consensus rules to function; nodes using it may fork away from production chains.

Caution: stopping nodeos from inside the debugger will corrupt its database.

Debugging using vscode

You should have the following project tree:

<project root>/   <==== open this directory in vscode
   .vscode/
      c_cpp_properties.json
      launch.json
      settings.json
   CMakeLists.txt
   notify.cpp
   build/         (Created by build step)
      clsdk -> ....
      notify-debug.wasm
      notify.abi
      notify.wasm
      wasi-sdk -> ....

launch.json sets the following nodeos options. Adjust them to your needs:

-d data --config-dir config
--plugin eosio::chain_api_plugin
--plugin eosio::producer_api_plugin
--plugin eosio::debug_plugin
--subst clsdk/contracts/token.wasm:clsdk/contracts/token-debug.wasm
--subst notify.wasm:notify-debug.wasm
-e -p eosio

Open notify.cpp and set some break points. You may also add break points to build/clsdk/contracts/token/src/token.cpp.

Start the debugger. nodeos will start running. To see its log, switch to the "cppdbg: nodeos" terminal.

From another terminal, use these commands to install and exercise the contracts. The debugger should hit the breakpoints you set in the contracts and pause.

cd build

# Create some users
cleos create account eosio alice EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV
cleos create account eosio bob EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV

# Set up eosio.token
# Note: the build system created a symlink to clsdk for easy access to the token contract
cleos create account eosio eosio.token EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV
cleos set abi eosio.token clsdk/contracts/token.abi
cleos set code eosio.token clsdk/contracts/token.wasm

cleos push action eosio.token create '["eosio", "1000000000.0000 EOS"]' -p eosio.token
cleos push action eosio.token issue '["eosio", "1000000000.0000 EOS", ""]' -p eosio
cleos push action eosio.token open '["alice", "4,EOS", "alice"]' -p alice
cleos push action eosio.token open '["bob", "4,EOS", "bob"]' -p bob
cleos push action eosio.token transfer '["eosio", "alice", "10000.0000 EOS", "have some"]' -p eosio
cleos push action eosio.token transfer '["eosio", "bob", "10000.0000 EOS", "have some"]' -p eosio

# Install the notify contract
cleos create account eosio notify EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV
cleos set abi notify notify.abi
cleos set code notify notify.wasm

# Try out the notify contract
cleos push action eosio.token transfer '["alice", "notify", "300.0000 EOS", "for purchases"]' -p alice
cleos push action eosio.token transfer '["bob", "notify", "300.0000 EOS", "for purchases"]' -p bob

cleos push action notify buydog '["alice", "fido", "100.0000 EOS"]' -p alice
cleos push action notify buydog '["alice", "rex", "120.0000 EOS"]' -p alice
cleos push action notify buydog '["bob", "lambo", "70.0000 EOS"]' -p bob

# See the remaining balances and the purchased animals
cleos get table notify notify balance
cleos get table notify notify animal

Debugging functionality

The following are available:

  • breakpoints
  • step in
  • step out
  • step over
  • continue
  • call stack

The following are not available

  • examining variables
  • examining memory

Corrupted database recovery

The debugger can cause nodeos to corrupt its database. There are 2 options to recover from the corruption:

  • Wipe the database and start over: from the build directory, run rm -rf data
  • Force a replay. This can trigger breakpoints (helpful for reproductions). From the build directory, run rm -rf data/state data/blocks/reversible. Alternatively, add --hard-replay-blockchain to the nodeos options in launch.json.

You can start nodeos again in the debugger after doing one of the above.

Debugging using gdb command line

You should have the following project tree:

<project root>/
   CMakeLists.txt
   notify.cpp
   build/         (Created by build step)
      clsdk -> ....
      notify-debug.wasm
      notify.abi
      notify.wasm
      wasi-sdk -> ....

To start a debug session on the command line:

cd build
gdb -q --args                                                           \
   ./clsdk/bin/nodeos                                                   \
   -d data --config-dir config                                          \
   --plugin eosio::chain_api_plugin                                     \
   --plugin eosio::producer_api_plugin                                  \
   --plugin eosio::debug_plugin                                         \
   --subst clsdk/contracts/token.wasm:clsdk/contracts/token-debug.wasm  \
   --subst notify.wasm:notify-debug.wasm                                \
   -e -p eosio

Ignore No debugging symbols found in ...; it will load debugging symbols for the wasm files instead.

The following gdb commands set options gdb needs to function, set some breakpoints, and start nodeos.

handle SIG34 noprint
set breakpoint pending on
set substitute-path clsdk-wasi-sdk: wasi-sdk
set substitute-path clsdk: clsdk
b example_contract::notify_transfer
b example_contract::buydog
run

From another terminal, use these commands to install and exercise the contracts. The debugger should hit the breakpoints you set in the contracts and pause.

cd build

# Create some users
cleos create account eosio alice EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV
cleos create account eosio bob EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV

# Set up eosio.token
# Note: the build system created a symlink to clsdk for easy access to the token contract
cleos create account eosio eosio.token EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV
cleos set abi eosio.token clsdk/contracts/token.abi
cleos set code eosio.token clsdk/contracts/token.wasm

cleos push action eosio.token create '["eosio", "1000000000.0000 EOS"]' -p eosio.token
cleos push action eosio.token issue '["eosio", "1000000000.0000 EOS", ""]' -p eosio
cleos push action eosio.token open '["alice", "4,EOS", "alice"]' -p alice
cleos push action eosio.token open '["bob", "4,EOS", "bob"]' -p bob
cleos push action eosio.token transfer '["eosio", "alice", "10000.0000 EOS", "have some"]' -p eosio
cleos push action eosio.token transfer '["eosio", "bob", "10000.0000 EOS", "have some"]' -p eosio

# Install the notify contract
cleos create account eosio notify EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV
cleos set abi notify notify.abi
cleos set code notify notify.wasm

# Try out the notify contract; these should trigger breakpoints
cleos push action eosio.token transfer '["alice", "notify", "300.0000 EOS", "for purchases"]' -p alice
cleos push action eosio.token transfer '["bob", "notify", "300.0000 EOS", "for purchases"]' -p bob

cleos push action notify buydog '["alice", "fido", "100.0000 EOS"]' -p alice
cleos push action notify buydog '["alice", "rex", "120.0000 EOS"]' -p alice
cleos push action notify buydog '["bob", "lambo", "70.0000 EOS"]' -p bob

# See the remaining balances and the purchased animals
cleos get table notify notify balance
cleos get table notify notify animal

Debugging functionality

The following functionality is supported:

  • breakpoints (b)
  • step in (s)
  • step out (fin)
  • step over (n)
  • continue (c)
  • call stack (bt)

The following are not available:

  • examining variables
  • examining memory

Corrupted database recovery

The debugger can cause nodeos to corrupt its database. There are 2 options to recover from the corruption:

  • Wipe the database and start over: from the build directory, run rm -rf data
  • Force a replay. This can trigger breakpoints (helpful for reproductions). From the build directory, run rm -rf data/state data/blocks/reversible. Alternatively, add --hard-replay-blockchain to the nodeos options.

You can start nodeos again in the debugger after doing one of the above.

cltester: Getting Started

Contract modifications

To simplify testing, the contract's class definition and table definitions should be in a header file.

This example is based on the Debug Example, but has these additions:

  • The contract source is now split into testable.hpp and testable.cpp
  • CMakeLists.txt has a new rule to build tests.wasm from tests.cpp (below)
  • launch.json now launches the test cases in cltester instead of starting nodeos

The files:

Simple test case

tests.cpp:

// cltester definitions
#include <eosio/tester.hpp>

// contract definitions
#include "testable.hpp"

// Catch2 unit testing framework. https://github.com/catchorg/Catch2
#define CATCH_CONFIG_MAIN
#include <catch2/catch.hpp>

using namespace eosio;

TEST_CASE("No tokens")
{
   // This starts a blockchain. This is similar to running nodeos, but forces
   // creation of a new blockchain and offers more control.
   test_chain chain;

   // Install the testable contract. Some notes:
   // * create_code_account is like create_account (used below), except it adds
   //   eosio.code to the active authority.
   // * cltester doesn't need the ABI to operate, so we don't need to set it.
   chain.create_code_account("example"_n);
   chain.set_code("example"_n, "testable.wasm");

   // Create a user account
   chain.create_account("alice"_n);

   // Alice tries to buy a dog, but has no tokens
   // This verifies the appropriate error is produced
   expect(chain.as("alice"_n).trace<example::actions::buydog>(  //
              "alice"_n, "fido"_n, s2a("0.0000 EOS")),
          "Dogs cost more than that");
}

Running the test

This builds the contract and the test:

mkdir build
cd build
cmake `clsdk-cmake-args` ..
make -j $(nproc)

Use one of these to run the test:

cltester tests.wasm        # minimal logging
cltester -v tests.wasm     # show blockchain logging. This also
                           # shows any contract prints in green.

cltester: Debugging using vscode

You should have the following project tree:

<project root>/   <==== open this directory in vscode
   .vscode/
      c_cpp_properties.json
      launch.json
      settings.json
   CMakeLists.txt
   testable.hpp
   testable.cpp
   tests.cpp
   build/         (Created by build step)
      clsdk -> ....
      testable-debug.wasm
      testable.abi
      testable.wasm
      tests.wasm
      wasi-sdk -> ....

launch.json is configured to run the tests using cltester instead of starting nodeos. It sets the following cltester options:

--subst testable.wasm testable-debug.wasm
-v
tests.wasm

Open testable.cpp and set some break points. You may also add break points to tests.

Start the debugger. cltester will start running. To see its log, switch to the "cppdbg: cltester" terminal. vscode should stop at one of your breakpoints.

cltester: Debugging using gdb command line

You should have the following project tree:

<project root>/
   CMakeLists.txt
   testable.hpp
   testable.cpp
   tests.cpp
   build/         (Created by build step)
      clsdk -> ....
      testable-debug.wasm
      testable.abi
      testable.wasm
      tests.wasm
      wasi-sdk -> ....

To start a debug session on the command line:

cd build
gdb -q --args                                \
   ./clsdk/bin/cltester                      \
   --subst testable.wasm testable-debug.wasm \
   -v                                        \
   tests.wasm

Ignore No debugging symbols found in ...; it will load debugging symbols for the wasm files instead.

The following gdb commands set options gdb needs to function, set some breakpoints, and start cltester.

handle SIG34 noprint
set breakpoint pending on
set substitute-path clsdk-wasi-sdk: wasi-sdk
set substitute-path clsdk: clsdk
b example_contract::notify_transfer
b example_contract::buydog
run

cltester: Token Support

Our test cases need to interact with the token contract in order to fully test our example. clsdk comes with a cltester-ready version of the token contract.

Example files:

Test cases

This demonstrates the following:

  • Interacting with the token contract in tests
  • Running multiple tests using multiple chains
  • Creating helper functions to reduce repetition in tests

tests.cpp:

#include <eosio/tester.hpp>
#include <token/token.hpp>  // comes bundled with clsdk
#include "testable.hpp"

#define CATCH_CONFIG_MAIN
#include <catch2/catch.hpp>

using namespace eosio;

// Set up the token contract
void setup_token(test_chain& t)
{
   t.create_code_account("eosio.token"_n);
   t.set_code("eosio.token"_n, CLSDK_CONTRACTS_DIR "token.wasm");

   // Create and issue tokens.
   t.as("eosio.token"_n).act<token::actions::create>("eosio"_n, s2a("1000000.0000 EOS"));
   t.as("eosio.token"_n).act<token::actions::create>("eosio"_n, s2a("1000000.0000 OTHER"));
   t.as("eosio"_n).act<token::actions::issue>("eosio"_n, s2a("1000000.0000 EOS"), "");
   t.as("eosio"_n).act<token::actions::issue>("eosio"_n, s2a("1000000.0000 OTHER"), "");
}

// Create and fund user accounts
void fund_users(test_chain& t)
{
   for (auto user : {"alice"_n, "bob"_n, "jane"_n, "joe"_n})
   {
      t.create_account(user);
      t.as("eosio"_n).act<token::actions::transfer>("eosio"_n, user, s2a("10000.0000 EOS"), "");
      t.as("eosio"_n).act<token::actions::transfer>("eosio"_n, user, s2a("10000.0000 OTHER"), "");
   }
}

// Set up the example contract
void setup_example(test_chain& t)
{
   t.create_code_account("example"_n);
   t.set_code("example"_n, "testable.wasm");
}

// Full setup for test chain
void setup(test_chain& t)
{
   setup_token(t);
   fund_users(t);
   setup_example(t);
}

TEST_CASE("Alice Attacks")
{
   // This is the first blockchain
   test_chain chain;
   setup(chain);

   // Alice tries to get a dog for free
   // This verifies the appropriate error is produced
   expect(chain.as("alice"_n).trace<example::actions::buydog>(  //
              "alice"_n, "fido"_n, s2a("0.0000 EOS")),
          "Dogs cost more than that");

   // Alice tries to buy a dog, but hasn't transferred any tokens to the contract
   expect(chain.as("alice"_n).trace<example::actions::buydog>(  //
              "alice"_n, "fido"_n, s2a("100.0000 EOS")),
          "user does not have a balance");

   // Alice tries to transfer an unsupported token to the contract
   expect(chain.as("alice"_n).trace<token::actions::transfer>(  //
              "alice"_n, "example"_n, s2a("100.0000 OTHER"), ""),
          "This contract does not deal with this token");

   // Alice transfers the correct token
   chain.as("alice"_n).act<token::actions::transfer>(  //
       "alice"_n, "example"_n, s2a("300.0000 EOS"), "");

   // Alice tries to get sneaky with the wrong token
   expect(chain.as("alice"_n).trace<example::actions::buydog>(  //
              "alice"_n, "fido"_n, s2a("100.0000 OTHER")),
          "This contract does not deal with this token");
}

TEST_CASE("No duplicate dog names")
{
   // This is a different blockchain than used from the previous test
   test_chain chain;
   setup(chain);

   // Alice goes first
   chain.as("alice"_n).act<token::actions::transfer>(  //
       "alice"_n, "example"_n, s2a("300.0000 EOS"), "");
   chain.as("alice"_n).act<example::actions::buydog>(  //
       "alice"_n, "fido"_n, s2a("100.0000 EOS"));
   chain.as("alice"_n).act<example::actions::buydog>(  //
       "alice"_n, "barf"_n, s2a("110.0000 EOS"));

   // Bob is next
   chain.as("bob"_n).act<token::actions::transfer>(  //
       "bob"_n, "example"_n, s2a("300.0000 EOS"), "");
   chain.as("bob"_n).act<example::actions::buydog>(  //
       "bob"_n, "wolf"_n, s2a("100.0000 EOS"));

   // Sorry, Bob
   expect(chain.as("bob"_n).trace<example::actions::buydog>(  //
              "bob"_n, "fido"_n, s2a("100.0000 EOS")),
          "could not insert object, most likely a uniqueness constraint was violated");
}

Running the test

This builds the contract, builds the tests, and runs the tests:

mkdir build
cd build
cmake `clsdk-cmake-args` ..
make -j $(nproc)
cltester -v tests.wasm

cltester: Reading Tables

Test cases can use the same database facilities (read-only) that contracts use. If contracts define all their tables in headers, then the test cases can get the table definitions from the headers.

Dumping table content

The tables in the example contract use a single scope. This makes it possible to iterate through all of the content in a table. The token contract uses a different scope for each user. This makes iteration infeasible. Instead, the test code has to explicitly specify each account to list the accounts' balances.

This extends the test cases in Token Support:

void dump_tokens(const std::vector<name> owners)
{
   printf("\nTokens\n=====\n");
   for (auto owner : owners)
   {
      token::accounts table("eosio.token"_n, owner.value);
      for (auto& account : table)
         printf("%-12s %s\n",
                owner.to_string().c_str(),
                account.balance.to_string().c_str());
   }
}

void dump_animals()
{
   printf("\nAnimals\n=====\n");
   example::animal_table table("example"_n, "example"_n.value);
   for (auto& animal : table)
      printf("%-12s %-12s %-12s %s\n",
             animal.name.to_string().c_str(),
             animal.type.to_string().c_str(),
             animal.owner.to_string().c_str(),
             animal.purchase_price.to_string().c_str());
}

TEST_CASE("Read Database")
{
   test_chain chain;
   setup(chain);

   chain.as("alice"_n).act<token::actions::transfer>(
       "alice"_n, "example"_n, s2a("300.0000 EOS"), "");
   chain.as("alice"_n).act<example::actions::buydog>(
       "alice"_n, "fido"_n, s2a("100.0000 EOS"));
   chain.as("alice"_n).act<example::actions::buydog>(
       "alice"_n, "barf"_n, s2a("110.0000 EOS"));
   chain.as("bob"_n).act<token::actions::transfer>(
       "bob"_n, "example"_n, s2a("300.0000 EOS"), "");
   chain.as("bob"_n).act<example::actions::buydog>(
       "bob"_n, "wolf"_n, s2a("100.0000 EOS"));

   dump_tokens({"eosio"_n, "alice"_n, "bob"_n, "example"_n});
   dump_animals();
}

JSON form

clsdk comes with json conversion functions. These can aid dumping tables.

template <typename Table>
void dump_table(name contract, uint64_t scope)
{
   Table table(contract, scope);
   for (auto& record : table)
      std::cout << format_json(record) << "\n";
}

TEST_CASE("Read Database 2")
{
   test_chain chain;
   setup(chain);

   chain.as("alice"_n).act<token::actions::transfer>(
       "alice"_n, "example"_n, s2a("300.0000 EOS"), "");
   chain.as("alice"_n).act<example::actions::buydog>(
       "alice"_n, "fido"_n, s2a("100.0000 EOS"));
   chain.as("alice"_n).act<example::actions::buydog>(
       "alice"_n, "barf"_n, s2a("110.0000 EOS"));
   chain.as("bob"_n).act<token::actions::transfer>(
       "bob"_n, "example"_n, s2a("300.0000 EOS"), "");
   chain.as("bob"_n).act<example::actions::buydog>(
       "bob"_n, "wolf"_n, s2a("100.0000 EOS"));

   printf("\nBalances\n=====\n");
   dump_table<example::balance_table>("example"_n, "example"_n.value);

   printf("\nAnimals\n=====\n");
   dump_table<example::animal_table>("example"_n, "example"_n.value);
}

Verifying table content

Test cases often need to verify table content is correct.

example::animal get_animal(name animal_name)
{
   example::animal_table table("example"_n, "example"_n.value);
   auto it = table.find(animal_name.value);
   if (it != table.end())
      return *it;
   else
      return {};  // return empty record if not found
}

TEST_CASE("Verify Animals")
{
   test_chain chain;
   setup(chain);

   chain.as("alice"_n).act<token::actions::transfer>(
       "alice"_n, "example"_n, s2a("300.0000 EOS"), "");
   chain.as("alice"_n).act<example::actions::buydog>(
       "alice"_n, "fido"_n, s2a("100.0000 EOS"));
   chain.as("alice"_n).act<example::actions::buydog>(
       "alice"_n, "barf"_n, s2a("110.0000 EOS"));
   chain.as("bob"_n).act<token::actions::transfer>(
       "bob"_n, "example"_n, s2a("300.0000 EOS"), "");
   chain.as("bob"_n).act<example::actions::buydog>(
       "bob"_n, "wolf"_n, s2a("100.0000 EOS"));

   auto fido = get_animal("fido"_n);
   CHECK(fido.name == "fido"_n);
   CHECK(fido.type == "dog"_n);
   CHECK(fido.owner == "alice"_n);
   CHECK(fido.purchase_price == s2a("100.0000 EOS"));
}

The CHECK macro verifies its condition is met. The -s command-line option shows the progress of these checks:

$ cltester tests.wasm -s

...
/.../tests.cpp:78: PASSED:
  CHECK( fido.name == "fido"_n )
with expansion:
  fido == fido

/.../tests.cpp:79: PASSED:
  CHECK( fido.type == "dog"_n )
with expansion:
  dog == dog
...

Table caching issue

The above examples open tables within helper functions to avoid an issue with multi_index. multi_index caches data and so gets confused if a contract modifies a table while the tester has that table currently open.

cltester: as/act/trace

The test_chain class supports this syntax for pushing single-action transactions:

chain.as("alice"_n).act<token::actions::transfer>(
       "alice"_n, "example"_n, s2a("300.0000 EOS"), "memo");

as

as(account) returns an object that represents an account's active authority. as also supports other authorities:

chain.as("alice"_n, "owner"_n).act<token::actions::transfer>(
       "alice"_n, "example"_n, s2a("300.0000 EOS"), "memo");

act

act<action wrapper>(action args) creates, signs, and executes a single-action transaction. It also verifies the transaction succeeded. If it fails, it aborts the test with an error message.

The contract headers use EOSIO_ACTIONS(...) to define the action wrappers, e.g. token::actions::transfer or example::actions::buydog. The wrappers record the default contract name (e.g. eosio.token), the name of the action (e.g. transfer), and the argument types. This allows strong type safety. It also bypasses any need for ABIs.

act signs with default_priv_key - a well-known key used for testing (5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3). This key pairs with default_pub_key (EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV). Both the create_account and create_code_account methods create accounts with default_pub_key.

trace

Like act, trace<action wrapper>(action args) creates, signs, and executes a single-action transaction. Unlike act, trace does not verify success. Instead, it returns the transaction's trace.

We could display the trace:

auto result = chain.as("alice"_n).trace<example::actions::buydog>(
    "alice"_n, "fido"_n, s2a("100.0000 OTHER"));
std::cout << format_json(result) << "\n";

This produces output like the following:

{
    "id": "F4EE6CACEF935889E35355568C492409C6F4535565B0B801EC31352DEFAA40F3",
    "status": "hard_fail",
    "cpu_usage_us": 0,
    "net_usage_words": 0,
    "elapsed": "62",
    "net_usage": "124",
    "scheduled": false,
    "action_traces": [
        {
            "action_ordinal": 1,
            "creator_action_ordinal": 0,
            "receipt": null,
            "receiver": "example",
            "act": {
                "account": "example",
                "name": "buydog",
                "authorization": [
                    {
                        "actor": "alice",
                        "permission": "active"
                    }
                ],
                "data": [...]
            },
            "context_free": false,
            "elapsed": "34",
            "console": "",
            "account_ram_deltas": [],
            "account_disk_deltas": [],
            "except": "eosio_assert_message assertion failure (3050003)\nassertion failure with message: This contract does not deal with this token\npending console output: \n",
            "error_code": "10000000000000000000",
            "return_value": []
        }
    ],
    "account_ram_delta": null,
    "except": "eosio_assert_message assertion failure (3050003)\nassertion failure with message: This contract does not deal with this token\npending console output: \n",
    "error_code": "10000000000000000000",
    "failed_dtrx_trace": []
}

expect

expect verifies that a transaction trace's except field contains within it an expected error message. If the the transaction succeeded, or the transaction failed but with a different message, then expect aborts the test with an error message. expect does a substring match.

expect(chain.as("alice"_n).trace<example::actions::buydog>(
            "alice"_n, "fido"_n, s2a("100.0000 OTHER")),
         "This contract does not deal with this token");

with_code

The action wrappers provide a default account name that the contract is normally installed on. e.g. the token wrappers assume eosio.token. with_code overrides this default.

This example sets up a fake EOS token to try to fool our example code.

// The hacker.token account runs the token contract
chain.create_code_account("hacker.token"_n);
chain.set_code("hacker.token"_n, CLSDK_CONTRACTS_DIR "token.wasm");
chain.as("hacker.token"_n)
      .with_code("hacker.token"_n)
      .act<token::actions::create>("hacker.token"_n, s2a("1000000.0000 EOS"));
chain.as("hacker.token"_n)
      .with_code("hacker.token"_n)
      .act<token::actions::issue>("hacker.token"_n, s2a("1000000.0000 EOS"), "");

// Give fake EOS to Alice
chain.as("hacker.token"_n)
      .with_code("hacker.token"_n)
      .act<token::actions::transfer>("hacker.token"_n, "alice"_n, s2a("10000.0000 EOS"), "");

// Alice transfers fake EOS to the example contract
chain.as("alice"_n)
      .with_code("hacker.token"_n)
      .act<token::actions::transfer>(
         "alice"_n, "example"_n, s2a("300.0000 EOS"), "");

// The contract didn't credit her account with the fake EOS
expect(chain.as("alice"_n).trace<example::actions::buydog>(
            "alice"_n, "fido"_n, s2a("100.0000 EOS")),
         "user does not have a balance");

as() variables

as() returns an object which can be stored in a variable to reduce repetition.

auto alice = chain.as("alice"_n);
alice.act<token::actions::transfer>(
    "alice"_n, "example"_n, s2a("300.0000 EOS"), "");
alice.act<example::actions::buydog>(
    "alice"_n, "fido"_n, s2a("100.0000 EOS"));
alice.act<example::actions::buydog>(
    "alice"_n, "barf"_n, s2a("110.0000 EOS"));

cltester: BIOS and Chain Configuration

cltester comes with multiple bios contracts which support different sets of protocol features.

NameRequired FeaturesAdditional Actions
biosPREACTIVATE_FEATURE
bios2PREACTIVATE_FEATURE, WTMSIG_BLOCK_SIGNATURESsetprods
bios3PREACTIVATE_FEATURE, WTMSIG_BLOCK_SIGNATURES, BLOCKCHAIN_PARAMETERS, ACTION_RETURN_VALUE, CONFIGURABLE_WASM_LIMITS2setprods, wasmcfg, enhanced setparams

cltester always activates PREACTIVATE_FEATURE; bios can be installed as soon as the chain is created. The other bioses need additional protocol features activated before they can be installed. The activate action in bios can activate these features. A helper method helps activate these in bulk.

Activating Protocol Features

bios contract headers include a helper function (activate) which activates multiple protocol features.

#include <bios/bios.hpp>

TEST_CASE("activate")
{
   test_chain chain;

   // Load bios
   chain.set_code("eosio"_n, CLSDK_CONTRACTS_DIR "bios.wasm");

   bios::activate(chain, {
      // Features available in 2.0
      eosio::feature::only_link_to_existing_permission,
      eosio::feature::forward_setcode,
      eosio::feature::wtmsig_block_signatures,
      eosio::feature::replace_deferred,
      eosio::feature::no_duplicate_deferred_id,
      eosio::feature::ram_restrictions,
      eosio::feature::webauthn_key,
      eosio::feature::disallow_empty_producer_schedule,
      eosio::feature::only_bill_first_authorizer,
      eosio::feature::restrict_action_to_self,
      eosio::feature::fix_linkauth_restriction,
      eosio::feature::get_sender,

      // Features added in 3.0
      eosio::feature::blockchain_parameters,
      eosio::feature::action_return_value,
      eosio::feature::get_code_hash,
      eosio::feature::configurable_wasm_limits2,
   });

   // Load bios3
   chain.set_code("eosio"_n, CLSDK_CONTRACTS_DIR "bios3.wasm");

   // Use the chain...
}

Setting Consensus Parameters (bios, bios2)

The setparams action sets consensus parameters which are available in nodeos 2.0 and earlier.

#include <bios/bios.hpp>

TEST_CASE("setparams")
{
   test_chain chain;

   // Load bios
   chain.set_code("eosio"_n, CLSDK_CONTRACTS_DIR "bios.wasm");

   // Allow deeper inline actions. Sets all other parameters to the default.
   chain.as("eosio"_n).act<bios::actions::setparams>(
      blockchain_parameters{
         .max_inline_action_depth = 10});

   // Use the chain...
}

eosio::blockchain_parameters has the following definition:

struct blockchain_parameters
{
   constexpr static int percent_1 = 100;  // 1 percent

   uint64_t max_block_net_usage = 1024 * 1024;
   uint32_t target_block_net_usage_pct = 10 * percent_1;
   uint32_t max_transaction_net_usage = max_block_net_usage / 2;
   uint32_t base_per_transaction_net_usage = 12;
   uint32_t net_usage_leeway = 500;
   uint32_t context_free_discount_net_usage_num = 20;
   uint32_t context_free_discount_net_usage_den = 100;
   uint32_t max_block_cpu_usage = 200'000;
   uint32_t target_block_cpu_usage_pct = 10 * percent_1;
   uint32_t max_transaction_cpu_usage = 3 * max_block_cpu_usage / 4;
   uint32_t min_transaction_cpu_usage = 100;
   uint32_t max_transaction_lifetime = 60 * 60;
   uint32_t deferred_trx_expiration_window = 10 * 60;
   uint32_t max_transaction_delay = 45 * 24 * 3600;
   uint32_t max_inline_action_size = 512 * 1024;
   uint16_t max_inline_action_depth = 4;
   uint16_t max_authority_depth = 6;
};

Setting Consensus Parameters (bios3)

The setparams action in bios3 adds an additional field: max_action_return_value_size.

#include <bios/bios.hpp>
#include <bios3/bios3.hpp>

TEST_CASE("setparams_bios3")
{
   test_chain chain;
   chain.set_code("eosio"_n, CLSDK_CONTRACTS_DIR "bios.wasm");
   bios::activate(chain, {
      eosio::feature::wtmsig_block_signatures,
      eosio::feature::blockchain_parameters,
      eosio::feature::action_return_value,
      eosio::feature::configurable_wasm_limits2,
   });
   chain.set_code("eosio"_n, CLSDK_CONTRACTS_DIR "bios3.wasm");

   // Allow larger return sizes. Sets all other parameters to the default.
   chain.as("eosio"_n).act<bios3::actions::setparams>(
      bios3::blockchain_parameters_v1{
         .max_action_return_value_size = 1024});

   // Use the chain...
}

Expanding WASM limits (bios3)

The wasmcfg action in bios3 expands the WASM limits.

#include <bios/bios.hpp>
#include <bios3/bios3.hpp>

TEST_CASE("wasmcfg")
{
   test_chain chain;
   chain.set_code("eosio"_n, CLSDK_CONTRACTS_DIR "bios.wasm");
   bios::activate(chain, {
      eosio::feature::wtmsig_block_signatures,
      eosio::feature::blockchain_parameters,
      eosio::feature::action_return_value,
      eosio::feature::configurable_wasm_limits2,
   });
   chain.set_code("eosio"_n, CLSDK_CONTRACTS_DIR "bios3.wasm");

   // low, default (same as low), or high
   chain.as("eosio"_n).act<bios3::actions::wasmcfg>("high"_n);

   // Use the chain...
}

cltester: Block Control

The test_chain class gives you control over block production, including control over time.

start_block

At any point, you can start producing a new block:

chain.start_block();

This finishes producing the current block (if one is being produced), then starts producing a new one. All transactions after this point go into the new block.

You can skip time:

chain.start_block(2000); // skip 2000ms-worth of blocks
  • 0 skips nothing; the new block is 500ms after the current block being produced (if any), or 500ms after the previous block.
  • 500 skips 1 block
  • 1000 skips 2 blocks

You can also skip to a specific time:

chain.start_block("2020-07-03T15:29:59.500");

or

eosio::time_point t = ...;
chain.start_block(t);

Note when skipping time: start_block creates an empty block immediately before the new one. This allows TAPoS to operate correctly after skipping large periods of time.

finish_block

At any point, you can stop producing the current block:

chain.finish_block();

After you call finish_block, the system is in a state where no blocks are being produced. The following cause the system to start producing a new block:

  • using start_block
  • pushing a transaction
  • using finish_block again. This causes the system to start a new block then finish it.

Getting the head block time

This gets the head block time as a time_point:

auto t = chain.get_head_block_info().timestamp.to_time_point();

Note: the head block is not the block that's currently being produced. Instead, it's the last block which was finished.

You can display the time:

std::cout << convert_to_json(t) << "\n";

You can also do arithmetic on time:

chain.start_block(
   chain.get_head_block_info().timestamp.to_time_point() +
   eosio::days(8) + eosio::hours(1));

Fixing duplicate transactions

It's easy to create a test which tries to push duplicate transactions. Call start_block before the duplicate transaction to solve this.

Fixing full blocks

It's easy to overfill a block, causing a test failure. Use start_block to solve this.

cltester: Starting Nodeos

cltester uses chainlib from nodeos, but cltester isn't nodeos, so is missing some functionality. e.g. it can't connect to nodes using p2p, and it can't serve json rpc requests. It can, however, spawn nodeos on a chain which cltester created. This can help with system-wide testing, e.g. testing nodejs and web apps.

TEST_CASE("start nodeos")
{
   // Prepare a chain
   test_chain chain;
   setup(chain);

   // cltester doesn't need ABIs, but most other tools do
   chain.set_abi("eosio.token"_n, CLSDK_CONTRACTS_DIR "token.abi");
   chain.set_abi("example"_n, "testable.abi");

   // Alice buys some dogs
   chain.as("alice"_n).act<token::actions::transfer>(
       "alice"_n, "example"_n, s2a("300.0000 EOS"), "");
   chain.as("alice"_n).act<example::actions::buydog>(
       "alice"_n, "fido"_n, s2a("100.0000 EOS"));
   chain.as("alice"_n).act<example::actions::buydog>(
       "alice"_n, "barf"_n, s2a("110.0000 EOS"));

   // Make the above irreversible. This causes the transactions to
   // go into the block log.
   chain.finish_block();
   chain.finish_block();

   // Copy blocks.log into a fresh directory for nodeos to use
   eosio::execute("rm -rf example_chain");
   eosio::execute("mkdir -p example_chain/blocks");
   eosio::execute("cp " + chain.get_path() + "/blocks/blocks.log example_chain/blocks");

   // Run nodeos
   eosio::execute(
       "nodeos -d example_chain "
       "--config-dir example_config "
       "--plugin eosio::chain_api_plugin "
       "--access-control-allow-origin \"*\" "
       "--access-control-allow-header \"*\" "
       "--http-validate-host 0 "
       "--http-server-address 0.0.0.0:8888 "
       "--contracts-console "
       "-e -p eosio");
}

After running the above, cleos should now function:

cleos push action example buydog '["alice", "spot", "90.0000 EOS"]' -p alice
cleos get table example example balance
cleos get table example example animal

cltester: Hostile Takeover

cltester can fork the state of an existing chain. This example loads an EOS snapshot, replaces the eosio and producer keys, and launches a nodeos instance which acts as 21 producers.

TEST_CASE("Takeover")
{
   std::cout << "Loading snapshot...\n";

   // This constructor loads a snapshot. The second argument is the max database size.
   test_chain chain{"/home/todd/work/snapshot-2021-11-21-22-eos-v4-0216739301.bin",
                    uint64_t(20) * 1024 * 1024 * 1024};

   // Replace production keys and eosio keys. These functions don't push any
   // transactions. Instead, they directly modify the chain state in a way which
   // violates consensus rules.
   std::cout << "Replacing keys...\n";
   chain.replace_producer_keys(test_chain::default_pub_key);
   chain.replace_account_keys("eosio"_n, "owner"_n, test_chain::default_pub_key);
   chain.replace_account_keys("eosio"_n, "active"_n, test_chain::default_pub_key);

   // We replaced the production keys, but the system contract can switch
   // them back. Let's fix that.
   for (auto prod :
        {"atticlabeosb"_n, "aus1genereos"_n, "big.one"_n,      "binancestake"_n, "bitfinexeos1"_n,
         "blockpooleos"_n, "eosasia11111"_n, "eoscannonchn"_n, "eoseouldotio"_n, "eosflytomars"_n,
         "eoshuobipool"_n, "eosinfstones"_n, "eosiosg11111"_n, "eoslaomaocom"_n, "eosnationftw"_n,
         "hashfineosio"_n, "newdex.bp"_n,    "okcapitalbp1"_n, "starteosiobp"_n, "whaleex.com"_n,
         "zbeosbp11111"_n})
   {
      std::cout << "    " << prod.to_string() << "\n";
      chain.replace_account_keys(prod, "owner"_n, test_chain::default_pub_key);
      chain.replace_account_keys(prod, "active"_n, test_chain::default_pub_key);
      chain.transact({
          action{{{"eosio"_n, "owner"_n}}, "eosio.null"_n, "free.trx"_n, std::tuple{}},
          action{{{prod, "owner"_n}},
                 "eosio"_n,
                 "regproducer"_n,
                 std::make_tuple(prod, test_chain::default_pub_key, std::string("url"),
                                 uint16_t(1234))},
      });
   }

   // Make a donation. This works because eosio.rex delegates to eosio,
   // and we replaced eosio's keys.
   chain.transact({
       action{{{"eosio"_n, "owner"_n}}, "eosio.null"_n, "free.trx"_n, std::tuple{}},
       action{{{"eosio.rex"_n, "owner"_n}},
              "eosio.token"_n,
              "transfer"_n,
              std::make_tuple("eosio.rex"_n, "genesis.eden"_n, s2a("50000000.0000 EOS"),
                              std::string("donate"))},
   });

   // Produce the block
   chain.finish_block();

   // shut down the chain so we can safely copy the database
   std::cout << "Shutdown...\n";
   chain.shutdown();

   // Copy everything into a fresh directory for nodeos to use
   std::cout << "Copy...\n";
   eosio::execute("rm -rf forked_chain");
   eosio::execute("cp -r " + chain.get_path() + " forked_chain");

   // Run nodeos. We must use the build which is packaged with clsdk since we're
   // loading the non-portable database.
   std::cout << "Start nodeos...\n";
   eosio::execute(
       "./clsdk/bin/nodeos "
       "-d forked_chain "
       "--config-dir example_config "
       "--plugin eosio::chain_api_plugin "
       "--access-control-allow-origin \"*\" "
       "--access-control-allow-header \"*\" "
       "--http-validate-host 0 "
       "--http-server-address 0.0.0.0:8888 "
       "--contracts-console "
       "-e "
       "-p atticlabeosb "
       "-p aus1genereos "
       "-p big.one "
       "-p binancestake "
       "-p bitfinexeos1 "
       "-p blockpooleos "
       "-p eosasia11111 "
       "-p eoscannonchn "
       "-p eoseouldotio "
       "-p eosflytomars "
       "-p eoshuobipool "
       "-p eosinfstones "
       "-p eosiosg11111 "
       "-p eoslaomaocom "
       "-p eosnationftw "
       "-p hashfineosio "
       "-p newdex.bp "
       "-p okcapitalbp1 "
       "-p starteosiobp "
       "-p whaleex.com "
       "-p zbeosbp11111 ");
}

Contract Pays

A system contract change, which is proposed here, can enable contract-pays on eosio chains. This document explains how the PR enables contract-pays and how contract developers may implement it.

The Approach

Early in eosio history, some on the Eosio Developers chat discussed doing the following:

  • Create an account, let's call it provider, with a new authority, let's call it payforit.
  • Create a contract on that account with an action, let's call it acceptcharge. This action scans the transaction and aborts if provider is unwilling to pay for it.
  • Use linkauth to enable provider@payforit to authorize provider::acceptcharge.
  • Delegate plenty of CPU and NET to provider.
  • Publish the private key of provider@payforit.

Usage:

  • Anyone could use provider@payforit's private key to sign a transaction.
  • The only thing provider@payforit should authorize (see below) is provider::acceptcharge.
  • If provider::acceptcharge is the first action, and that action doesn't abort the transaction, then provider will cover NET and CPU costs.

Attack Vectors:

  • provider@payforit can authorize updateauth, allowing anyone to replace the published private key with their own, denying access to others.
  • updateauth also would allow anyone to create a new subauthority under payforit, with keys of their choosing.
  • provider@payforit can authorize linkauth, allowing anyone to relink provider::acceptcharge to the new subauthority, or to active or owner.
  • provider@payforit can authorize unlinkauth, allowing anyone to disable payforit's access to provider::acceptcharge.
  • provider@payforit can also authorize deleteauth.
  • Since provider@payforit's authorization appears within the transaction, an attacker can set delay to non-0, consuming provider's RAM to store deferred transactions.

Attack Mitigation

A system contract update could block updateauth, linkauth, unlinkauth, and deleteauth. This covers most of the attack vectors.

To prevent RAM consumption attacks, provider must not pay for deferred transactions. The easiest way to prevent this is for provider to have no free RAM. Since provider::acceptcharge may need free RAM for tracking purposes, the contract should be on a different account than the resource provider account.

Example Code

// This version of eosio::get_action doesn't abort when index is out of range.
std::optional<eosio::action> better_get_action(uint32_t type, uint32_t index)
{
    auto size = eosio::internal_use_do_not_use::get_action(type, index, nullptr, 0);
    if (size < 0)
        return std::nullopt;
    std::vector<char> raw(size);
    auto size2 = eosio::internal_use_do_not_use::get_action(
        type, index, raw.data(), size);
    eosio::check(size2 == size, "get_action failed");
    return eosio::unpack<eosio::action>(raw.data(), size);
}

// Examine the transaction to see if we're ok accepting the CPU and NET charges
void the_contract::acceptcharge()
{
    // type 0: context-free action
    // type 1: normal action
    for (uint32_t type = 0; type < 2; ++type)
    {
        for (uint32_t index = 0;; ++index)
        {
            auto action = better_get_action(type, index);
            if (!action)
                break;
            // Simple rule: only allow actions on this contract
            eosio::check(action->account == get_self(),
                            "This transaction has something I won't pay for");
        }
    }
}

Auth Part 1

The native eosio account system is very flexible compared to prior chains, but it does have some limitations:

  • It uses a considerable amount of RAM (~3k minimum) per account, making accounts more costly than they could be.
  • It has a strong tie-in (delay) with the deprecated deferred transaction system.
  • Accounts are permanent; there is no way to destroy a native account and recover its RAM.
  • Any new capabilities require hard-forking changes and tend to worsen the following issue:
  • The permission system is difficult to explain to new users.

It is already possible to define new account systems using non-privileged contracts, but this is rarely done:

  • Contract-based authentication needs either server-pays or contract-pays to function. Server-pays requires deploying specialized infrastructure. Contract-pays doesn't exist yet on public eosio chains.
  • There is no existing standard that authenticators and web apps can rely on to authenticate users to contracts.
  • There is no existing standard that contracts can rely on to authenticate users to other contracts.
  • There is no existing tooling for building contracts which support contract-based authentication.
  • There is no existing standard that history services can rely on to interpret activity.

Contract-pays may be coming; see this proposal. This leaves the issue of standards for contract-auth, which this chapter starts to address, and tooling, which clsdk addresses.

run, run_auth, and verb

run is a proposed standard action that acts as an entry point for executing verbs using an extensible authentication system (run_auth). It has the following ABI:

{
    "name": "run",
    "fields": [
        {
            "name": "auth",
            "type": "run_auth"
        },
        {
            "name": "verbs",
            "type": "verb[]"
        }
    ]
},

run_auth is a variant containing various options for authenticating users. This standard proposes the following, with more to come in the future:

"variants": [
    {
        "name": "run_auth",
        "types": [
            "no_auth",          // No auth provided.
            "account_auth",     // Auth using either a native eosio account,
                                // or a contract-defined account which is tied to
                                // a native eosio account.
            "signature_auth"    // Auth using a contract-defined account which is
                                // tied to a public key.
        ]
    }
]

verb is a variant which is specific to the contract. verbs are similar to actions. Here's an example from the Eden contract:

"variants": [
    {
        "name": "verb",
        "types": [
            ...
            "inductprofil",
            "inductvideo",
            ...
            "electvote",
            "electvideo",
            ...
        ]
    },
]

clsdk provides a dispatcher which implements the run protocol, provides an ABI generator which produces the verb variant, and provides definitions of run_auth and its related types. clsdk's dispatcher executes all verbs within the context of the original run action. It avoids using inline actions since these complicate authentication and increase overhead.

Future Proposals

Additional proposals should cover:

  • A standard interface to contract-pays
  • A standard interface for contracts to forward authentication information to each other
  • A new notification system, since require_recipient does not play well with run

None of these proposals would require hard forks.

Not Covered

There is a topic which is application-specific: the creation and management of accounts. Hopefully we'll see several groups experiment with various approaches. Some potential ideas to explore:

  • Pay-to-key: these accounts could come and go as funds enter and exit them
  • Real-person accounts, including key recovery using human-to-human verification
  • Accounts with time-locked withdrawals

GraphQL

clsdk comes with the blocks-to-browser (btb) system. btb includes a library that supports running GraphQL queries within WASM. This chapter introduces GraphQL support separate from the rest of the btb system by running GraphQL within contracts. It doesn't use the new action-return-value system introduced with nodeos 2.1. Instead, it uses contract prints for compatibility with nodeos 2.0.

GraphQL: Getting Started

Contract and Test Modifications

This example is based on the cltester Token Example, but has these changes:

  • The contract has two new actions: graphql and graphqlschema, which are shown below
  • CMakeLists.txt adds the btb and btb-debug libraries as dependencies to example.wasm and example-debug.wasm
  • The test case sets up a chain and starts nodeos

The files:

Simple GraphQL Query

example-graphql.cpp:

#include "example.hpp"

// GraphQL Support
#include <btb/graphql.hpp>

// EOSIO_REFLECT2; augments methods with argument names
#include <eosio/reflection2.hpp>

// The root of GraphQL queries
struct Query
{
   // Identify the contract
   eosio::name contract;

   // Retrieve an animal, if it exists
   std::optional<example::animal> animal(eosio::name name) const
   {
      example::animal_table table{contract, contract.value};
      auto it = table.find(name.value);
      if (it != table.end())
         return *it;
      else
         return std::nullopt;
   }
};
EOSIO_REFLECT2(Query,                  //
               contract,               // query a field
               method(animal, "name")  // query a method; identifies the argument names
)

// Action: execute a GraphQL query and print the result
void example::example_contract::graphql(const std::string& query)
{
   Query root{get_self()};
   eosio::print(btb::gql_query(root, query, ""));
}

// Action: print the GraphQL schema
void example::example_contract::graphqlschema()
{
   eosio::print(btb::get_gql_schema<Query>());
}

const

std::optional<example::animal> animal(eosio::name name) const
{
   // ...
}

Since btb's GraphQL system doesn't support mutation, query methods must be marked const as above. It ignores non-const methods.

Starting the Example

This builds the example and starts nodeos:

mkdir build
cd build
cmake `clsdk-cmake-args` ..
make -j $(nproc)
cltester tests.wasm

Fetching the Schema

This uses cleos to fetch the schema. cleos's -v option shows print output.

$ cleos -v push action example graphqlschema '[]' -p alice

executed transaction: 79a16e17d6bde46a219a9e721ed17d610bbf7c2fa988f7db14e44b7e7fda97ae  96 bytes  185 us
#       example <= example::graphqlschema       ""
>> type animal {
>>     name: String!
>>     type: String!
>>     owner: String!
>>     purchase_price: String!
>> }
>> type Query {
>>     contract: String!
>>     animal(name: String!): animal
>> }

Querying the Contract Name

This queries the contract name:

$ cleos -v push action example graphql '["{contract}",""]' -p alice

executed transaction: 50fa29906ed5b40b3c51dc396f2262c292eda6970813909ff3e06ebf18f618f2  104 bytes  177 us
#       example <= example::graphql             {"query":"{contract}"}
>> {"data": {"contract":"example"}}

Querying an Animal

This queries a specific animal.

$ cleos -v push action example graphql '["{animal(name:\"fido\"){name,type,owner,purchase_price}}",""]' -p alice

executed transaction: 636a96b15cf7c1478992fc8f27716da7fc9c60105144a4c43ecf21035e840454  152 bytes  181 us
#       example <= example::graphql             {"query":"{animal(name:\"fido\"){name,type,owner,purchase_price}}"}
>> {"data": {"animal":{"name":"fido","type":"dog","owner":"alice","purchase_price":"100.0000 EOS"}}}

Here's the GraphQL query above:

{
  animal(name: "fido") {
    name
    type
    owner
    purchase_price
  }
}

Here's the formatted output:

{
    "data": {
        "animal": {
            "name": "fido",
            "type": "dog",
            "owner": "alice",
            "purchase_price": "100.0000 EOS"
        }
    }
}

GraphQL: GraphiQL UI

cleos isn't very handy for running GraphQL queries. This webapp, based on GraphiQL, provides a friendlier alternative.

The files:

Building and Running

This interface allows you to create queries using code completion then execute them.

index.tsx

index.tsx:

import { JsonRpc } from "eosjs/dist/eosjs-jsonrpc";
import { Api } from "eosjs/dist/eosjs-api";
import { JsSignatureProvider } from "eosjs/dist/eosjs-jssig";
import React from "react";
import { render } from "react-dom";
import GraphiQL from "graphiql";
import { buildSchema, GraphQLSchema } from "graphql";
import "./node_modules/graphiql/graphiql.min.css";
global.Buffer = require("buffer/").Buffer;

// nodeos RPC endpoint
const rpcUrl =
    window.location.protocol + "//" + window.location.hostname + ":8888";

// This contract runs the graphql queries
const contract = "example";

// user which authorizes queries
const user = "eosio";

// user's private key
const privateKey = "5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3";

// eosjs
let rpc = new JsonRpc(rpcUrl);
let signatureProvider = new JsSignatureProvider([privateKey]);
let api = new Api({ rpc, signatureProvider });

// show status during startup
let statusContent = "";
function log(msg: string) {
    statusContent += msg + "\n";
    render(<pre style={{ padding: 20 }}>{statusContent}</pre>, document.body);
}

// fetch schema using 'graphqlschema' action
let schema: GraphQLSchema | null = null;
async function fetchSchema() {
    const result = (await api.transact(
        {
            actions: [
                {
                    authorization: [{ actor: user, permission: "active" }],
                    account: contract,
                    name: "graphqlschema",
                    data: {},
                },
            ],
        },
        { useLastIrreversible: true, expireSeconds: 2 }
    )) as any;
    const sch = result.processed.action_traces[0].console;
    log("");
    log(sch);
    schema = buildSchema(sch);
}

// run query using 'graphql' action
async function fetcher({ query }: { query: string }) {
    const result = (await api.transact(
        {
            actions: [
                {
                    authorization: [{ actor: user, permission: "active" }],
                    account: contract,
                    name: "graphql",
                    data: { query },
                },
            ],
        },
        { useLastIrreversible: true, expireSeconds: 2 }
    )) as any;
    return JSON.parse(result.processed.action_traces[0].console);
}

// Populate the UI with this example query
const defaultQuery = `# GraphiQL is talking to a contract running in nodeos.
# Press the ▶ button above to run this query.

{
  contract
  animal(name: "fido") {
    name
    type
    owner
    purchase_price
  }
}`;

// Query UI
function Page() {
    return (
        <main style={{ height: "100%" }}>
            <GraphiQL
                {...{
                    fetcher,
                    defaultQuery,
                    schema: schema!,
                }}
            />
        </main>
    );
}

// Startup
(async () => {
    log(`Using RPC: ${rpc.endpoint}`);
    log("Fetching schema...");
    try {
        await fetchSchema();
        render(<Page></Page>, document.body);
    } catch (e) {
        console.log(e);
        log(e.message);
        log("See console for additional details");
    }
})();

GraphQL: Proxy Objects

Database objects don't normally provide a GraphQL-friendly interface. e.g. example::animal provides underscore_names, but GraphQL consumers usually expect mixedCaseNames. Proxy objects may provide a different interface; they may also add additional methods.

Example

This is a modification of example-graphql.cpp from Getting Started.

#include <btb/graphql.hpp>
#include <eosio/reflection2.hpp>
#include "example.hpp"

// GraphQL proxy for example::animal
struct Animal
{
   // The proxy holds a copy of the original database object instead
   // of holding a pointer or reference. This is necessary because
   // the database object gets destroyed when the table object goes
   // out of scope from within Query::animal(). A potential workaround
   // is to make the table object a member of the contract object.
   example::animal obj;

   // These methods have no arguments, so act like fields in GraphQL
   auto name() const { return obj.name; }
   auto type() const { return obj.type; }
   auto owner() const { return obj.owner; }
   auto purchasePrice() const { return obj.purchase_price; }

   // This method has an argument, so needs method(...) in the
   // EOSIO_REFLECT2 definition below.
   auto isA(eosio::name type) const { return type == obj.type; }
};
EOSIO_REFLECT2(Animal, name, type, owner, purchasePrice, method(isA, "type"))

struct Query
{
   eosio::name contract;

   // Returns a Proxy object instead of returning the original object
   std::optional<Animal> animal(eosio::name name) const
   {
      example::animal_table table{contract, contract.value};
      auto it = table.find(name.value);
      if (it != table.end())
         return Animal{*it};
      else
         return std::nullopt;
   }
};
EOSIO_REFLECT2(Query,                  //
               contract,               // query a field
               method(animal, "name")  // query a method; identifies the argument names
)

void example::example_contract::graphql(const std::string& query)
{
   Query root{get_self()};
   eosio::print(btb::gql_query(root, query, ""));
}

void example::example_contract::graphqlschema()
{
   eosio::print(btb::get_gql_schema<Query>());
}

Example Query

This query:

{
  animal(name: "fido") {
    name
    type
    owner
    purchasePrice
    isACat: isA(type: "cat")
    isADog: isA(type: "dog")
  }
}

Produces this result:

{
  "data": {
    "animal": {
      "name": "fido",
      "type": "dog",
      "owner": "alice",
      "purchasePrice": "100.0000 EOS",
      "isACat": false,
      "isADog": true
    }
  }
}

GraphQL: Linking Objects

Proxy objects can link together to form graphs.

Example

This is a modification of example-graphql.cpp from Getting Started.

#include <btb/graphql.hpp>
#include <eosio/reflection2.hpp>
#include "example.hpp"

struct User;

// GraphQL proxy for example::animal
struct Animal
{
   eosio::name contract;
   example::animal obj;

   auto name() const { return obj.name; }
   auto type() const { return obj.type; }
   auto purchasePrice() const { return obj.purchase_price; }

   // Link to a proxy which represents owner
   User owner() const;
};
EOSIO_REFLECT2(Animal, name, type, purchasePrice, owner)

// GraphQL proxy which represents a user. This proxy may exist even
// if there are no database records for that user.
struct User
{
   eosio::name contract;
   eosio::name name;

   // User's remaining balance, if any
   std::optional<eosio::asset> balance() const
   {
      example::balance_table table{contract, contract.value};
      auto it = table.find(name.value);
      if (it != table.end())
         return it->balance;
      else
         return std::nullopt;
   }

   // Link to proxy objects which represent animals owned by user
   std::vector<Animal> animals() const
   {
      std::vector<Animal> result;
      example::animal_table table{contract, contract.value};

      // This is an inefficent approach and will time out if there are
      // too many animals in the table. We could add a secondary index,
      // but that would consume RAM. The blocks-to-browser system
      // supports secondary indexes which don't consume on-chain RAM.
      for (auto& animal : table)
         if (animal.owner == name)
            result.push_back(Animal{contract, animal});

      return result;
   }
};
EOSIO_REFLECT2(User, name, balance, animals)

User Animal::owner() const
{
   return {contract, obj.owner};
}

struct Query
{
   eosio::name contract;

   User user(eosio::name name) const { return {contract, name}; }

   std::optional<Animal> animal(eosio::name name) const
   {
      example::animal_table table{contract, contract.value};
      auto it = table.find(name.value);
      if (it != table.end())
         return Animal{contract, *it};
      else
         return std::nullopt;
   }
};
EOSIO_REFLECT2(Query, contract, method(user, "name"), method(animal, "name"))

void example::example_contract::graphql(const std::string& query)
{
   Query root{get_self()};
   eosio::print(btb::gql_query(root, query, ""));
}

void example::example_contract::graphqlschema()
{
   eosio::print(btb::get_gql_schema<Query>());
}

Example Queries

Get information about Alice's and Joe's balances and animals. Joe has never interacted with the contract.

{
  alice: user(name: "alice") {
    name
    balance
    animals {
      name
      type
      purchasePrice
    }
  }
  joe: user(name: "joe") {
    name
    balance
    animals {
      name
      type
      purchasePrice
    }
  }
}

Result:

{
  "data": {
    "alice": {
      "name": "alice",
      "balance": "90.0000 EOS",
      "animals": [
        {
          "name": "barf",
          "type": "dog",
          "purchasePrice": "110.0000 EOS"
        },
        {
          "name": "fido",
          "type": "dog",
          "purchasePrice": "100.0000 EOS"
        }
      ]
    },
    "joe": {
      "name": "joe",
      "balance": null,
      "animals": []
    }
  }
}
{
  animal(name: "fido") {
    name
    type
    purchasePrice
    owner {
      name
      balance
    }
  }
}

Result:

{
  "data": {
    "animal": {
      "name": "fido",
      "type": "dog",
      "purchasePrice": "100.0000 EOS",
      "owner": {
        "name": "alice",
        "balance": "90.0000 EOS"
      }
    }
  }
}

All animals owned by the person who owns fido

{
  animal(name: "fido") {
    owner {
      name
      animals {
        name
      }
    }
  }
}

Result:

{
  "data": {
    "animal": {
      "owner": {
        "name": "alice",
        "animals": [
          {
            "name": "barf"
          },
          {
            "name": "fido"
          }
        ]
      }
    }
  }
}

GraphQL: Pagination

clsdk's GraphQL library supports most of the GraphQL Connection Model for paging through large data sets.

Example

This is a modification of example-graphql.cpp from Getting Started.

// Include support for the connection model (pagination)
#include <btb/graphql_connection.hpp>

#include <eosio/reflection2.hpp>
#include "example.hpp"

struct Animal
{
   example::animal obj;

   auto name() const { return obj.name; }
   auto type() const { return obj.type; }
   auto owner() const { return obj.owner; }
   auto purchasePrice() const { return obj.purchase_price; }
};
EOSIO_REFLECT2(Animal, name, type, owner, purchasePrice)

// Define the AnimalConnection and AnimalEdge GraphQL types
constexpr const char AnimalConnection_name[] = "AnimalConnection";
constexpr const char AnimalEdge_name[] = "AnimalEdge";
using AnimalConnection =
    btb::Connection<btb::ConnectionConfig<Animal, AnimalConnection_name, AnimalEdge_name>>;

struct Query
{
   eosio::name contract;

   // Searches for and pages through the animals in the database
   //
   // The gt, ge, lt, and le arguments support searching for animals with
   // names which are greater-than, greater-than-or-equal-to, less-than,
   // or less-than-or-equal-to the values provided. If more than 1 of these
   // are used, then the result is the intersection of these.
   //
   // If first is non-null, it limits the result to the first animals found
   // which meet the other criteria (gt, ge, lt, le, before, after).
   // If last is non-null, it limits the result to the last animals found.
   // Using first and last together is allowed, but is not recommended since
   // it has an unusual semantic, which matches the GraphQL spec.
   //
   // If before is non-null, then the result is limited to records before it.
   // If after is non-null, then the result is limited to records after it.
   // before and after are opaque cursor values.

   AnimalConnection animals(std::optional<eosio::name> gt,
                            std::optional<eosio::name> ge,
                            std::optional<eosio::name> lt,
                            std::optional<eosio::name> le,
                            std::optional<uint32_t> first,
                            std::optional<uint32_t> last,
                            std::optional<std::string> before,
                            std::optional<std::string> after) const
   {
      example::animal_table table{contract, contract.value};

      return btb::make_connection<AnimalConnection,  // The type of connection to use
                                  eosio::name        // The key type (animal name)
                                  >(
          gt, ge, lt, le, first, last, before, after,
          table,  // Either a table or a secondary index
          [](auto& obj) {
             // This is the key used for searching in the table or index
             // provided above
             return obj.name;
          },
          [&](auto& obj) {
             // Convert an object found in the table into a proxy
             return Animal{obj};
          },
          // Hook up the lower_bound and upper_bound functions. These
          // do the actual search.
          [](auto& table, auto key) { return table.lower_bound(key.value); },
          [](auto& table, auto key) { return table.upper_bound(key.value); });
   }
};
EOSIO_REFLECT2(Query,  //
               method(animals, "gt", "ge", "lt", "le", "first", "last", "before", "after"))

void example::example_contract::graphql(const std::string& query)
{
   Query root{get_self()};
   eosio::print(btb::gql_query(root, query, ""));
}

void example::example_contract::graphqlschema()
{
   eosio::print(btb::get_gql_schema<Query>());
}

Example Queries

Get all animals in the database

{
  animals {
    edges {
      node {
        name
      }
    }
  }
}

Get the first 5

{
  animals(first:5) {
    edges {
      node {
        name
      }
    }
  }
}

Get the last 5

{
  animals(last:5) {
    edges {
      node {
        name
      }
    }
  }
}

Get the first 5 starting with dog132

{
  animals(first: 5, ge: "dog132") {
    edges {
      node {
        name
      }
    }
  }
}

Pagination

{
  animals(first: 5) {
    pageInfo {
      hasPreviousPage
      hasNextPage
      startCursor
      endCursor
    }
    edges {
      node {
        name
      }
    }
  }
}

The result includes this in its output:

"pageInfo": {
  "hasPreviousPage": false,
  "hasNextPage": true,
  "startCursor": "0000000000B0AE39",
  "endCursor": "000000009010184D"
},

There are more results (hasNextPage is true) and we know where to resume (endCursor). To get the next 5:

{
  animals(first: 5, after: "000000009010184D") {
    pageInfo {
      hasPreviousPage
      hasNextPage
      startCursor
      endCursor
    }
    edges {
      node {
        name
      }
    }
  }
}