EOSDEV #3. Обработка входящих сообщений в смарт-контракте EOS
В прошлом примере мы разбирали процесс написания простого контракта Hello World. Теперь нам предстоит добавить в него немного логики.
Кто сталкивался со смарт-контрактами на языке Solidity в сети Ethereum, думаю обратили внимание, что помимо самого смарт-контракта, там часто фигурирует ABI (Application Binary Interface) интерфейс в формате JSON файла с расширением .abi
. Он необходим для того, чтобы объяснить программе-клиенту, каким образом можно взаимодействовать со смарт-контрактом, какие функции и их аргументы можно вызывать и т.п.. Компиляторы для Ethereum автоматически генерируют ABI интерфейс на основании кода Solidity, мы практически с ним не взаимодействуем. EOS компиляторы этого пока не умеют и интерфейс сейчас необходимо прописывать вручную. Разработчики сейчас работают над этим недочетом и в самое ближайшее время добавят автоматическую генерацию ABI файлов на основе исходного кода смарт-контракта (подробнее в тикете https://github.com/EOSIO/eos/pull/456). Обратите внимание, что синтаксис ABI файлов EOS отличается от синтаксиса ABI файлов контрактов сети Ethereum.
Итак, предлагаю добавить в существующий Hello World логику обработки входящего сообщения. Для этого мы модифицируем ABI интерфейс и логику самого приложения.
Самостоятельно редактируем ABI файл
Модифицируем наш hello.abi
файл заменив его содержание на следующее:
{
"types": [{
"newTypeName": "PlayerAccountName",
"type": "Name"
}
],
"structs": [{
"name": "transfer",
"base": "",
"fields": {
"from": "PlayerAccountName",
"to": "PlayerAccountName",
"amount": "UInt64"
}
},{
"name": "account",
"base": "",
"fields": {
"account": "Name",
"balance": "UInt64"
}
}
],
"actions": [{
"action": "transfer",
"type": "transfer"
}
],
"tables": [{
"table": "account",
"type": "account",
"indextype": "i64",
"keynames" : ["account"],
"keytypes" : ["Name"]
}
]
}
Обращу внимание на то, что приложение, создаваемое с помощью eoscpp -n appname
генерирует его из шаблона, находящегося в папке https://github.com/EOSIO/eos/tree/master/contracts/skeleton. На данный момент, файл .abi
, находящийся в этой папке не имеет ничего общего с логикой самого контракта, написанной на C++, который находится рядом. Думаю со временем шаблон базового приложения будет исправлен.
Зато сейчас этот .abi
файл имеет много общего с вышеописанным примером. Единственно, чем они отличаются, это название кастомного типа. В нем это AccountName
, у нас PlayerAccountName
. Я изменил его специально, чтобы сделать акцент на том месте, где можно кастомизировать свои типы.
Рассмотрим его содержанее детальнее:
"types": [{
"newTypeName": "PlayerAccountName",
"type": "Name"
}
],
Список типов содержит один единственный элемент. Ключи newTypeName
и type
являются частью синтаксиса ABI файлов в EOS. Первый ключ newTypeName
определяет название нового типа и в нашем примере имеет значение "PlayerAccountName"
. Второй ключ type имеет значение "Name"
. Name
здесь является встроенным типом в EOS. Можно рассматривать элемент массива types
как typedef
на уровне этого ABI файла:
typedef Name PlayerAccountName;
В файле https://github.com/EOSIO/eos/blob/master/contracts/eoslib/types.hpp мы найдем определение самого Name
. Он нужен для обертки типа uint64_t
, чтобы он передавался только в те методы, которые ожидают его в аргументах.
Ниже в ABI файле идет список structs
:
"structs": [{
"name": "transfer",
"base": "",
"fields": {
"from": "PlayerAccountName",
"to": "PlayerAccountName",
"amount": "UInt64"
}
},{
...
В нем перечислены используемые типы transfer
и account
. Структура полей transfer
содержит в себе два поля определенного в types
типа PlayerAccountName
и одно поле системного типа UInt64
, в то время как структура полей action
использует системный тип Name
для хранения значения аккаунта.
Элемент ‘actions’ определяет список действий и типов передаваемых с их помощью. В данном случае и действие и тип имеют одинаковое название transfer
.
"actions": [{
"action": "transfer",
"type": "transfer"
}
],
Читаем сообщения с помощью EOS C API
В вышеопределенном ABI файле сообщения имеют следующий вид:
"fields": {
"from": "PlayerAccountName",
"to": "PlayerAccountName",
"amount": "UInt64"
}
В C это тип данных будет представлен соответствующей структурой:
struct transfer {
uint64_t from;
uint64_t to;
uint64_t amount;
};
Поле amount
явно определяет тип свой в ABI файле. Остальные же два поля являются алиасами к Name
, который в свою очередь является алиасом-оберткой для того же uint64_t
. Поэтому в C все элементы имеют одинаковый тип uint64_t
.
EOS C API предоставляет нам следующие методы для работы с сообщениями Message API:
uint32_t message_size();
uint32_t read_message( void* msg, uint32_t msglen );
Воспользуемся ими и добавим логику чтения сообщения в функцию apply()
:
#include <hello.hpp>
extern "C" {
void init() {
eosio::print( "Init World!\n" );
}
struct transfer {
uint64_t from;
uint64_t to;
uint64_t amount;
};
void apply( uint64_t code, uint64_t action ) {
eosio::print( "Hello World: ", eosio::Name(code), "->", eosio::Name(action), "\n" );
if( action == N(transfer) ) {
transfer message;
static_assert( sizeof(message) == 3*sizeof(uint64_t), "unexpected padding" );
auto read = read_message( &message, sizeof(message) );
assert( read == sizeof(message), "message too short" );
eosio::print( "Transfer ", message.amount, " from ", eosio::Name(message.from), " to ", eosio::Name(message.to), "\n" );
}
}
} // extern "C"
Скомпилируем это и задеплоим
eoscpp -o hello.wast hello.cpp
eosc set contract inita hello.wast hello.abi
eosd
опять вызовет init 3 раза:
Init World!
Init World!
Init World!
Теперь мы можем вызвать метод transfer
с помощью утилиты eosc
:
eosc push message inita transfer '{"from":"currency","to":"inita","amount":50}' --scope inita
eosd
нам выведет следующие строки:
Используем EOS C++ API для чтения сообщений
EOS C++ API предоставляет нам более высокоуровневый подход для чтения сообщений
namespace eosio {
template<typename T>
T current_message();
}
Обновим hello.cpp
в соответствии с этим подходом
#include <hello.hpp>
extern "C" {
void init() {
eosio::print( "Init World!\n" );
}
struct transfer {
eosio::Name from;
eosio::Name to;
uint64_t amount;
};
void apply( uint64_t code, uint64_t action ) {
eosio::print( "Hello World: ", eosio::Name(code), "->", eosio::Name(action), "\n" );
if( action == N(transfer) ) {
auto message = eosio::current_message<transfer>();
eosio::print( "Transfer ", message.amount, " from ", message.from, " to ", message.to, "\n" );
}
}
} // extern "C"
Обратите внимание, что теперь, вместо типа uint64_t
для хранения названия аккаунтов мы используем вспомогательный тип eosio::Name
. После повторной компиляции и деплоя кода мы получим тот же самый результат работы смарт-контракта, что и в случае с C API.
Заключение
На канале разработчиков сейчас обсуждаются вопросы, вроде таких, как какой стандарт использовать для наименования методов - camelCase
или under_score
. Буквально сегодня наткнулся на то, что методы, используемые в C API переименованы:
Поэтому интерфейс взаимодействия смарт-контрактов наверняка еще не раз поменяется и пример, который мы разобрали возможно опять перестанет работать. В этом посте я лишь хотел показать то, что платформа EOS уже сейчас позволяет запускать приложения с чуть более сложной, чем Hello World логикой.
В следующем посте рассмотрим функционал консольного клиента eosc
.