Creating WASMs

The sources for a minimal WASM pair include:

  • A common header (my.hpp)
  • A server implementation (my-server.cpp)
  • A client implementation (my-client.cpp)

The header declares the set of available queries and responses. Here's an example header for a WASM pair which can query token balances for a range of accounts.

// my.hpp

#pragma once
#include <eosio/asset.hpp>
#include <eosio/block_select.hpp>

// Parameters for a query
struct get_my_tokens_request {
    eosio::block_select snapshot_block = {};
    eosio::name         code           = {};
    eosio::symbol_code  sym            = {};
    eosio::name         first_account  = {};
    eosio::name         last_account   = {};
    uint32_t            max_results    = {};
};

// Enables JSON <> binary conversion
STRUCT_REFLECT(get_my_tokens_request) {
    STRUCT_MEMBER(get_my_tokens_request, snapshot_block)
    STRUCT_MEMBER(get_my_tokens_request, code)
    STRUCT_MEMBER(get_my_tokens_request, sym)
    STRUCT_MEMBER(get_my_tokens_request, first_account)
    STRUCT_MEMBER(get_my_tokens_request, last_account)
    STRUCT_MEMBER(get_my_tokens_request, max_results)
}

// A single balance
struct token_balance {
    eosio::name           account = {};
    eosio::extended_asset amount  = {};
};

STRUCT_REFLECT(token_balance) {
    STRUCT_MEMBER(token_balance, account)
    STRUCT_MEMBER(token_balance, amount)
}

// Query response
struct get_my_tokens_response {
    std::vector<token_balance> balances = {};
    std::optional<eosio::name> more     = {};

    EOSLIB_SERIALIZE(get_my_tokens_response, (balances)(more))
};

STRUCT_REFLECT(get_my_tokens_response) {
    STRUCT_MEMBER(get_my_tokens_response, balances)
    STRUCT_MEMBER(get_my_tokens_response, more)
}

// The set of available queries. Each query has a name which identifies it.
using my_query_request = eosio::tagged_variant<
    eosio::serialize_tag_as_name,
    eosio::tagged_type<"get.my.toks"_n, get_my_tokens_request>>;

// The set of available responses. Each response has a name which identifies it.
// The name must match the request's name.
using my_query_response = eosio::tagged_variant<
    eosio::serialize_tag_as_name,
    eosio::tagged_type<"get.my.toks"_n, get_my_tokens_response>>;

Server WASM

The server WASM uses the database to find answers to incoming requests.

// my-server.cpp

#include "my.hpp"
#include <eosio/database.hpp>
#include <eosio/input_output.hpp>

// process this query
void process(get_my_tokens_request& req, const eosio::database_status& status) {

    // query the database for a range of contract rows,
    // ordered by (code, table, primary_key, scope)
    auto s = query_database(eosio::query_contract_row_range_code_table_pk_scope{
        // look at this point in time
        .snapshot_block = get_block_num(req.snapshot_block, status),

        // get records with keys >= first
        .first =
            {
                .code        = req.code,
                .table       = "accounts"_n,
                .primary_key = req.sym.raw(),
                .scope       = req.first_account.value,
            },

        // get records with keys <= last
        .last =
            {
                .code        = req.code,
                .table       = "accounts"_n,
                .primary_key = req.sym.raw(),
                .scope       = req.last_account.value,
            },

        // get this many results max. wasm-ql may return fewer results.
        .max_results = req.max_results,
    });

    get_my_tokens_response response;

    // loop through each record
    eosio::for_each_contract_row<eosio::asset>(s, [&](eosio::contract_row& r, eosio::asset* a) {
        // let requestor know how to continue the search
        response.more = eosio::name{r.scope + 1};

        // if the row is present and contains a valid asset, record the result
        if (r.present && a)
            response.balances.push_back({
                .account = eosio::name{r.scope},
                .amount = eosio::extended_asset{*a, req.code}
            });

        // continue the loop
        return true;
    });

    // send the result to the requestor
    eosio::set_output_data(pack(my_query_response{std::move(response)}));
}

// initialize this WASM
extern "C" __attribute__((eosio_wasm_entry)) void initialize() {}

// wasm-ql calls this for each incoming query
extern "C" void run_query() {
    // deserialize the request
    auto request = eosio::unpack<my_query_request>(eosio::get_input_data());

    // dispatch the request to the appropriate `process` overload
    std::visit([](auto& x) { process(x, eosio::get_database_status()); }, request.value);
}

Client WASM

The client WASM converts between JSON and the binary format the server WASM understands

// my-client.cpp

#include "my.hpp"
#include <eosio/input_output.hpp>
#include <eosio/parse_json.hpp>
#include <eosio/schema.hpp>

// initialize this WASM
extern "C" __attribute__((eosio_wasm_entry)) void initialize() {}

// produce JSON schema for request
extern "C" void describe_query_request() {
    eosio::set_output_data(eosio::make_json_schema<my_query_request>());
}

// produce JSON schema for response
extern "C" void describe_query_response() {
    eosio::set_output_data(eosio::make_json_schema<my_query_response>());
}

// convert request from JSON to binary
extern "C" void create_query_request() {
    eosio::set_output_data(pack(std::make_tuple(
        "local"_n,      // must be "local"
        "my"_n,         // name of server WASM
        eosio::parse_json<my_query_request>(eosio::get_input_data()))));
}

// convert response from binary to JSON
extern "C" void decode_query_response() {
    eosio::set_output_data(to_json(eosio::unpack<my_query_response>(eosio::get_input_data())));
}

Building

Use the CDT to build the WASMs:

# Location of History Tools repository
export HT_TOOLS_DIR=~/history-tools

# Location of the CDT
export CDT_DIR=/usr/local/eosio.cdt

$CDT_DIR/bin/eosio-cpp                                                      \
    -Os                                                                     \
    -I $HT_TOOLS_DIR/libraries/eosiolib/wasmql                              \
    -I $HT_TOOLS_DIR/external/abieos/external/date/include                  \
    $HT_TOOLS_DIR/libraries/eosiolib/wasmql/eosio/temp_placeholders.cpp     \
    -fquery-server                                                          \
    --eosio-imports=$HT_TOOLS_DIR/libraries/eosiolib/wasmql/server.imports  \
    my-server.cpp                                                           \
    -o my-server.wasm

$CDT_DIR/bin/eosio-cpp                                                      \
    -Os                                                                     \
    -I $HT_TOOLS_DIR/libraries/eosiolib/wasmql                              \
    -I $HT_TOOLS_DIR/external/abieos/external/date/include                  \
    $HT_TOOLS_DIR/libraries/eosiolib/wasmql/eosio/temp_placeholders.cpp     \
    -fquery-client                                                          \
    --eosio-imports=$HT_TOOLS_DIR/libraries/eosiolib/wasmql/client.imports  \
    my-client.cpp                                                           \
    -o my-client.wasm

Deploying

Place my-server.wasm in the directory that wasm-ql's --wql-wasm-dir option points to. Make my-client.wasm available to clients.

Example use

Modify the code in Using Client WASMs:

// Use this for browsers:
const myClientWasm = await HistoryTools.createClientWasm({
    mod: await WebAssembly.compileStreaming(fetch('.../path/to/my-client.wasm')),
    encoder, decoder
});

// Use this for nodejs
const myClientWasm = await HistoryTools.createClientWasm({
    mod: new WebAssembly.Module(fs.readFileSync('.../path/to/my-client.wasm')),
    encoder, decoder
});

// Use this for either:
const request = myClientWasm.createQueryRequest(JSON.stringify(
    ['get.my.toks', {
        snapshot_block: ['head', 0],
        code: 'eosio.token',
        sym: 'EOS',
        first_account: 'eosio',
        last_account: 'eosio.zzzzzz',
        max_results: 10,
    }]
));

const queryReply = await fetch('http(s)://server.name:port/wasmql/v1/query', {
    method: 'POST',
    body: HistoryTools.combineRequests([
        request,
    ]),
});
if (queryReply.status !== 200)
    throw new Error(queryReply.status + ': ' + queryReply.statusText + ': ' + await queryReply.text());

const responses = HistoryTools.splitResponses(new Uint8Array(await queryReply.arrayBuffer()));

prettyPrint('The tokens', myClientWasm.decodeQueryResponse(responses[0]));