原文地址:https://steemit.com/eos/@dan/eos-example-exchange-contract-and-benefits-of-c
这个星期我都忙着写API,智能合约开发者可以使用这些API来开发合约。为了更好地设计这些api,我自己写了一个示例合约。这次的示例比一个货币合约稍微复杂一些,但它是一个本地eos货币与一个示例货币合约之间的完整交易。
c++API的好处
开发者要使用c++在eos上开发智能合约,这些合约将由Web Assembly编译,然后发布到区块链上。这意味着我们可以利用c++的类型和模版(template)系统以保证我们合约的安全性。
最基础的一种安全就是维度分析(dimensional analysis)。开发交易所(exchange)功能的时候,你要跟好几种不同的单位打交道:EOS,CURRENCY,以及EOS/CURRENCY.
一个简单的实现大概是这样的:
struct account {
uint64_t eos_balance;
uint64_t currency_balance;
};
上面这段代码的问题在于,你有可能会这样写:
void buy( Bid order ) {
...
buyer_account.currency _balance -= order.quantity;
...
}
初看上去,问题似乎并不明显,但是仔细一看,你就发现应该是用eos_balance而不是currency_balance。因为在这个例子中,市场使用EOS来给CURRENCY定价。另外一种错误就是:
auto receive_tokens = order.quantity * order.price;
如果命令是Bid的话,那么这行代码就是有效的,但是如果命令是Ask的话,就需要把price反转。正如你所看到的,如果不使用适当的维度分析的话,就没办法确定程序的正确性。
好在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;
};
有了这个定义,下面的代码就会产生一个编译错误,因为eos::Tokens
和currency::Tokens
没有定义-=
操作符。
void buy( Bid order ) {
...
buyer_account.currency _balance -= order.quantity;
...
}
使用这项技术,我就可以在我的示例交易所合约的实现中使用编译器来识别以及修复单位的不匹配。这么做的最大的好处是,如果我的balance都用uint64_t的话,最终c++编译器生成的web跟应当生成的是一样的。
你可能注意到,token类也会自动检查越界的exception。
简化货币合约
在开发交易所合约的过程中,我首先更新了货币合约。我对货币合约进行了重构,分成了一个头文件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消息。它还会实现三种它自己的消息:buy,sell 和cancel。交易所合约在exchange.hpp中定义它的公共接口,头文件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; };
}
交易所合约的完整源代码有点长,就不贴上来了,你可以在这里查看.现在我只演示一下对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++程序员都自己管理内存,而这比较麻烦。幸运的是,开发智能合约的时候这些问题都不存在,因为你的程序在每条消息开始的时候都用slate重新开始(restart)。而且,我们很少需要实现动态内存分配。交易所合约并不会调用new,delete,也不调用malloc,free。WebAssembly框架会自动地拒绝任何可能产生内存错误的交易。
这意味着当使用c++处理短期的消息处理方法的时候,c++的多数毛病都不存在,而只有c++的好处。
结论
EOS.IO软件进展十分顺利,使用这些API来开发智能合约充满了乐趣,我对此十分高兴。
💪强,记号
优秀的开发团队做出的东西,一定有创新
这是翻译的 @dan的文章?你跟他一个团队?
翻译的,原文地址已加上
👍
翻译文章请注明作者和原文链接
忘了,已加
翻译的很好
感觉eos能坚持下去的话,会成功取代以太