...
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 executedebug.wasm
whenever it would otherwise executecontract.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, runrm -rf data
- Force a replay. This can trigger breakpoints (helpful for reproductions). From the
build
directory, runrm -rf data/state data/blocks/reversible
. Alternatively, add--hard-replay-blockchain
to the nodeos options inlaunch.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, runrm -rf data
- Force a replay. This can trigger breakpoints (helpful for reproductions). From the
build
directory, runrm -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
andtestable.cpp
- CMakeLists.txt has a new rule to build
tests.wasm
fromtests.cpp
(below) launch.json
now launches the test cases in cltester instead of starting nodeos
The files:
- testable.hpp
- testable.cpp
- CMakeLists.txt
- .vscode/c_cpp_properties.json
- .vscode/settings.json
- .vscode/launch.json
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:
- testable.hpp
- testable.cpp
- tests.cpp
- CMakeLists.txt
- .vscode/c_cpp_properties.json
- .vscode/settings.json
- .vscode/launch.json
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.
Name | Required Features | Additional Actions |
---|---|---|
bios | PREACTIVATE_FEATURE | |
bios2 | PREACTIVATE_FEATURE , WTMSIG_BLOCK_SIGNATURES | setprods |
bios3 | PREACTIVATE_FEATURE , WTMSIG_BLOCK_SIGNATURES , BLOCKCHAIN_PARAMETERS , ACTION_RETURN_VALUE , CONFIGURABLE_WASM_LIMITS2 | setprods , 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 itpayforit
. - Create a contract on that account with an action, let's call it
acceptcharge
. This action scans the transaction and aborts ifprovider
is unwilling to pay for it. - Use
linkauth
to enableprovider@payforit
to authorizeprovider::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) isprovider::acceptcharge
. - If
provider::acceptcharge
is the first action, and that action doesn't abort the transaction, thenprovider
will cover NET and CPU costs.
Attack Vectors:
provider@payforit
can authorizeupdateauth
, 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 underpayforit
, with keys of their choosing.provider@payforit
can authorizelinkauth
, allowing anyone to relinkprovider::acceptcharge
to the new subauthority, or toactive
orowner
.provider@payforit
can authorizeunlinkauth
, allowing anyone to disablepayforit
's access toprovider::acceptcharge
.provider@payforit
can also authorizedeleteauth
.- Since
provider@payforit
's authorization appears within the transaction, an attacker can setdelay
to non-0, consumingprovider
'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
andgraphqlschema
, which are shown below CMakeLists.txt
adds thebtb
andbtb-debug
libraries as dependencies toexample.wasm
andexample-debug.wasm
- The test case sets up a chain and starts nodeos
The files:
- example.hpp
- example.cpp
- example-graphql.cpp
- tests.cpp
- CMakeLists.txt
- .vscode/c_cpp_properties.json
- .vscode/settings.json
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
- Follow the instructions in GraphQL: Getting Started to start nodeos
- Run
yarn && yarn build && yarn start
- Open http://localhost:3000/
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
Owner-to-animal links
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-to-owner links
{
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"
}
}
}
}
Circular links
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
}
}
}
}