📒 EOS - Пример биржевого контракта и преимущества C++ (@dan)
На этой неделе я сосредоточил своё внимание на API, который разработчики смарт-контрактов будут использовать для написания контрактов. Чтобы помочь упростить структуру этого API, я самостоятельно реализовал пример типового контракта. На этот раз пример немного сложнее, чем просто валютный контракт, но он представляет собой целый обменник между встроенной валютой EOS и гипотетическим контрактом CURRENCY.
Преимущества API на C ++
Разработчики, использующие в качестве основы EOS, будут писать свои смарт-контракты на C++, потом написанный на нем программный код будет компилироваться в Web Assembly, а затем публиковаться в блокчейн. Другими словами, мы можем использовать систему типов и шаблонов C ++ для обеспечения безопасности наших контрактов.
Одним из основополагающих моментов обеспечения безопасности является анализ размерности, главная идея которого состоит в четком отслеживании использующихся единиц измерения. При разработке биржи вы имеете дело с несколькими единицами измерения: EOS, CURRENCY и EOS / CURRENCY.
Простая реализация данного момента предполагала бы нечто подобное:
struct account {
uint64_t eos_balance;
uint64_t currency_balance;
};
Проблема использования данного простого подхода заключается в том, что случайно может быть записан следующий код:
void buy( Bid order ) {
...
buyer_account.currency _balance -= order.quantity;
...
}
На первый взгляд ошибка неочевидна, но при более тщательной проверке можно заметить, что ордера на покупку (Bids) используют eos_balance
, а не currency_balance
. В этом случае рынок устанавливает цену CURRENCY в EOS.
Также может возникнуть следующая ошибка:
auto receive_tokens = order.quantity * order.price;
Эта конкретная строка кода может быть валидна, если ордер представляет собой предложение на покупку (Bid), но в случае, если это предложение на продажу (Ask), цена должна быть инвертирована. Как видите, без надлежащего анализа разрядности вы не можете быть уверены, что складываете яблоки с яблоками, а не яблоки с апельсинами.
К счастью, C ++ позволяет нам использовать шаблоны и перегрузку операторов для определения беззатратной проверки используемых единиц измерения во время выполнения.
template<typename NumberType, uint64_t CurrencyType = N(eos) >
struct token {
token(){}
explicit token( NumberType v ):quantity(v){};
NumberType quantity = 0;
token& operator-=( const token& a ) {
assert( quantity >= a.quantity,
"integer underflow subtracting token balance" );
quantity -= a.quantity;
return *this;
}
token& operator+=( const token& a ) {
assert( quantity + a.quantity >= a.quantity,
"integer overflow adding token balance" );
quantity += a.quantity;
return *this;
}
inline friend token operator+( const token& a, const token& b ) {
token result = a;
result += b;
return result;
}
inline friend token operator-( const token& a, const token& b ) {
token result = a;
result -= b;
return result;
}
explicit operator bool()const { return quantity != 0; }
};
Благодаря использованию такого определения переменных теперь в аккаунте есть чёткое разграничение типов:
struct Account {
eos::Tokens eos_balance;
currency::Tokens currency_balance;
};
struct Bid {
eos::Tokens quantity;
};
При использовании описанного выше кода будет генерироваться ошибка компиляции, потому что -= operator
не определён для eos::Tokens
и currency::Tokens
.
void buy( Bid order ) {
...
buyer_account.currency _balance -= order.quantity;
...
}
Применяя этот метод, я смог использовать компилятор для определения и исправления многих несоответствий типов единиц измерения в моей реализации примера биржевого контракта. Самое приятное заключается в том, что результирующий код веб асемблера, сгенерированный компилятором C++, идентичен тому, что было бы сгенерировано, если бы я просто использовал тип uint64_t
для всех своих балансов.
Еще один момент, который вы можете отметить, заключается в том, что класс token
также автоматически проверяет исключения переполнения и переполнения снизу.
Упрощенный валютный контракт
В процессе написания биржевого контракта я сначала обновил валютный контракт. При этом я переделал код валютного контракта в файл заголовка currency.hpp
и исходник currency.cpp
, чтобы биржевой контракт мог получить доступ к типам, определенным валютным контрактом.
currency.hpp
#include <eoslib/eos.hpp>
#include <eoslib/token.hpp>
#include <eoslib/db.hpp>
/**
* Make it easy to change the account name the currency is deployed to.
*/
#ifndef TOKEN_NAME
#define TOKEN_NAME currency
#endif
namespace TOKEN_NAME {
typedef eos::token<uint64_t,N(currency)> Tokens;
/**
* Transfer requires that the sender and receiver be the first two
* accounts notified and that the sender has provided authorization.
*/
struct Transfer {
AccountName from;
AccountName to;
Tokens quantity;
};
struct Account {
Tokens balance;
bool isEmpty()const { return balance.quantity == 0; }
};
/**
* Accounts information for owner is stored:
*
* owner/TOKEN_NAME/account/account -> Account
*
* This API is made available for 3rd parties wanting read access to
* the users balance. If the account doesn't exist a default constructed
* account will be returned.
*/
inline Account getAccount( AccountName owner ) {
Account account;
/// scope, code, table, key, value
Db::get( owner, N(currency), N(account), N(account), account );
return account;
}
} /// namespace TOKEN_NAME
currency.cpp
#include <currency/currency.hpp> /// defines transfer struct (abi)
namespace TOKEN_NAME {
/// When storing accounts, check for empty balance and remove account
void storeAccount( AccountName account, const Account& a ) {
if( a.isEmpty() ) {
printi(account);
/// scope table key
Db::remove( account, N(account), N(account) );
} else {
/// scope table key value
Db::store( account, N(account), N(account), a );
}
}
void apply_currency_transfer( const TOKEN_NAME::Transfer& transfer ) {
requireNotice( transfer.to, transfer.from );
requireAuth( transfer.from );
auto from = getAccount( transfer.from );
auto to = getAccount( transfer.to );
from.balance -= transfer.quantity; /// token subtraction has underflow assertion
to.balance += transfer.quantity; /// token addition has overflow assertion
storeAccount( transfer.from, from );
storeAccount( transfer.to, to );
}
} // namespace TOKEN_NAME
Ознакомление с биржевым контрактом
Биржевой контракт обрабатывает сообщения currency::Transfer
и eos::Transfer
всякий раз, когда биржа является отправителем или получателем. Он также реализует три собственных сообщения: купить, продать и отменить. Биржевой контракт определяет собственный открытый интерфейс, типы сообщений и таблицы баз данных в файле exchange.hpp
.
exchange.hpp
#include <currency/currency.hpp>
namespace exchange {
struct OrderID {
AccountName name = 0;
uint64_t number = 0;
};
typedef eos::price<eos::Tokens,currency::Tokens> Price;
struct Bid {
OrderID buyer;
Price price;
eos::Tokens quantity;
Time expiration;
};
struct Ask {
OrderID seller;
Price price;
currency::Tokens quantity;
Time expiration;
};
struct Account {
Account( AccountName o = AccountName() ):owner(o){}
AccountName owner;
eos::Tokens eos_balance;
currency::Tokens currency_balance;
uint32_t open_orders = 0;
bool isEmpty()const { return ! ( bool(eos_balance) | bool(currency_balance) | open_orders); }
};
Account getAccount( AccountName owner ) {
Account account(owner);
Db::get( N(exchange), N(exchange), N(account), owner, account );
return account;
}
TABLE2(Bids,exchange,exchange,bids,Bid,BidsById,OrderID,BidsByPrice,Price);
TABLE2(Asks,exchange,exchange,bids,Ask,AsksById,OrderID,AsksByPrice,Price);
struct BuyOrder : public Bid { uint8_t fill_or_kill = false; };
struct SellOrder : public Ask { uint8_t fill_or_kill = false; };
}
Текст исходного программного кода биржевого контракта слишком объёмен для этого поста, но вы можете увидеть его на github. Чтобы вы получили представление о том, как он будет реализован, я представлю вам основной обработчик сообщений для SellOrder
:
void apply_exchange_sell( SellOrder order ) {
Ask& ask = order;
requireAuth( ask.seller.name );
assert( ask.quantity > currency::Tokens(0), "invalid quantity" );
assert( ask.expiration > now(), "order expired" );
static Ask existing_ask;
assert( AsksById::get( ask.seller, existing_ask ), "order with this id already exists" );
auto seller_account = getAccount( ask.seller.name );
seller_account.currency_balance -= ask.quantity;
static Bid highest_bid;
if( !BidsByPrice::back( highest_bid ) ) {
assert( !order.fill_or_kill, "order not completely filled" );
Asks::store( ask );
save( seller_account );
return;
}
auto buyer_account = getAccount( highest_bid.buyer.name );
while( highest_bid.price >= ask.price ) {
match( highest_bid, buyer_account, ask, seller_account );
if( highest_bid.quantity == eos::Tokens(0) ) {
save( seller_account );
save( buyer_account );
Bids::remove( highest_bid );
if( !BidsByPrice::back( highest_bid ) ) {
break;
}
buyer_account = getAccount( highest_bid.buyer.name );
} else {
break; // buyer's bid should be filled
}
}
save( seller_account );
if( ask.quantity ) {
assert( !order.fill_or_kill, "order not completely filled" );
Asks::store( ask );
}
}
Как вы можете видеть, представленный выше программный код является относительно лаконичным, читабельным, безопасным и, что самое главное, быстрым.
Является ли C ++ безопасным языком?
Те, кто участвует в языковых войнах, могут быть знакомы с проблемами, возникающими у программистов C и C ++ при управлении памятью. К счастью, большинство этих проблем исчезают при создании смарт-контрактов, потому что ваша программа “перезагружается” и возобновляет работу “с чистого листа” в начале обработки каждого сообщения. Кроме того, необходимость реализовать динамическое распределение памяти возникает крайне редко. Биржевой контракт не вызывает ни функции new
и delete
, ни malloc
и free
. Фреймворк WebAssembly автоматически отклонит любую транзакцию, которая неправильно обращается к памяти.
Это означает, что большинство проблем C ++ исчезают при его использовании для обработчиков сообщений с коротким жизненным циклом, а взамен у нас остается множество преимуществ.
Заключение
Программное обеспечение EOS.IO прекрасно развивается, и я как никогда счастлив оттого, насколько проще писать смарт-контракты, используя этот API.
Свежие новости в Телеграм: t.me/EOS_RU
Оригинал поста: ЗДЕСЬ