概述
今天我们来解读一下在github上eos的exchange合约的样例代码,看它能实现哪些功能。在github上有该合约的代码路径:https://github.com/EOSIO/eos/tree/master/contracts/exchange
在该目录下有以下几个文件
CMakeLists.txt #该文件是编译用的我们不去管
exchange.abi #该文件定义的是exchange合约的abi
exchange.hpp #合约头文件
exchange.cpp #合约实现
ABI文件
我们先贴出来ABI文件的内容
{
"types": [{
"newTypeName": "AccountName",
"type": "Name"
}
],
"structs": [{
"name": "OrderID",
"fields": {
"name" : "AccountName",
"id" : "UInt64"
}
},{
"name" : "Bid",
"fields" : {
"buyer" : "OrderID",
"price" : "UInt128",
"quantity" : "UInt64",
"expiration" : "Time"
}
},{
"name" : "Ask",
"fields" : {
"seller" : "OrderID",
"price" : "UInt128",
"quantity" : "UInt64",
"expiration" : "Time"
}
},{
"name" : "Account",
"fields" : {
"owner" : "AccountName",
"eos_balance" : "UInt64",
"currency_balance" : "UInt64",
"open_orders" : "UInt32"
}
},{
"name" : "BuyOrder",
"base" : "Bid",
"fields" : {
"fill_or_kill" : "UInt8"
}
},{
"name" : "SellOrder",
"base" : "Ask",
"fields" : {
"fill_or_kill" : "UInt8"
}
}
],
"actions": [{
"action": "buy",
"type": "BuyOrder"
},{
"action": "sell",
"type": "SellOrder"
},{
"action": "cancelbuy",
"type": "OrderID"
},{
"action": "cancelsell",
"type": "OrderID"
}
],
"tables": [
{"table":"bids","type":"Bid"},
{"table":"asks","type":"Ask"},
{"table":"account","type":"Account"}
]
}
目前ABI文件在官网上没有详细的介绍,而且后续该文件会由工具自动从c++文件生成,可能后续也不会有太多介绍。好在该文件不是太复杂,我们可以尝试分析一下,可以看到ABI文件有四个部分
- types
- structs
- actions
- tables
types
我看了所有样例的ABI文件该字段貌似都是
"types": [{
"newTypeName": "AccountName",
"type": "Name"
}
]
应该是定义合约类型的,但是为什么全部一样,猜测此处后续还会完善。我们暂时先略过。
structs
该部分是需要好好研究的,因为它定义的合约里面的交易中的数据结构,可以看到在exchange的ABI中定义了以下六个交易数据结构:
- OrderID
- Bid
- Ask
- Account
- BuyOrder
- SellOrder
actions
该部分定义了合约的交易类型,可以看到合约定义了以下四种:交易类型
- buy
- sell
- cancelbuy
- cancelsell
tables
tables貌似是存储的数据结构,用于定义合约的数据存储,当前先略过。
exchange.hpp
先贴一下exchange.hpp的内容
/**
* @file
* @copyright defined in eos/LICENSE.txt
*/
#include <currency/currency.hpp>
namespace exchange {
using currency::CurrencyTokens;
using EosTokens = eos::Tokens;
struct OrderID {
AccountName name = 0;
uint64_t number = 0;
};
typedef eos::price<EosTokens,CurrencyTokens> Price;
struct PACKED( Bid ) {
OrderID buyer;
Price price;
eos::Tokens quantity;
Time expiration;
void print() {
eos::print( "{ quantity: ", quantity, ", price: ", price, " }" );
}
};
static_assert( sizeof(Bid) == 32+12, "unexpected padding" );
struct PACKED( Ask ) {
OrderID seller;
Price price;
CurrencyTokens quantity;
Time expiration;
void print() {
eos::print( "{ quantity: ", quantity, ", price: ", price, " }" );
}
};
static_assert( sizeof(Ask) == 32+12, "unexpected padding" );
struct PACKED( Account ) {
Account( AccountName o = AccountName() ):owner(o){}
AccountName owner;
EosTokens eos_balance;
CurrencyTokens currency_balance;
uint32_t open_orders = 0;
bool isEmpty()const { return ! ( bool(eos_balance) | bool(currency_balance) | open_orders); }
};
using Accounts = Table<N(exchange),N(exchange),N(account),Account,uint64_t>;
TABLE2(Bids,exchange,exchange,bids,Bid,BidsById,OrderID,BidsByPrice,Price);
TABLE2(Asks,exchange,exchange,asks,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; };
inline Account getAccount( AccountName owner ) {
Account account(owner);
Accounts::get( account );
return account;
}
}
我们可以看到首先该合约引用了,因为交易需要用到eos和发行的资产
using currency::CurrencyTokens;
using EosTokens = eos::Tokens;
然后就是在头文件中定义的数据结构
- OrderID
- Bid
- Ask
- Account
- BuyOrder
- SellOrder
以及tables。同时还定义了一个inline方法getAccount。
exchange.cpp
先贴代码
/**
* @file exchange.cpp
* @copyright defined in eos/LICENSE.txt
* @brief defines an example exchange contract
*
* This exchange contract assumes the existence of two currency contracts
* located at @currencya and @currencyb. These currency contracts have
* provided an API header defined in currency.hpp which the exchange
* contract will use to process messages related to deposits and withdraws.
*
* The exchange contract knows that the currency contracts requireNotice()
* of both the sender and receiver; therefore, the exchange contract can
* implement a message handler that will be called anytime funds are deposited
* to or withdrawn from the exchange.
*
* When tokens are sent to @exchange from another account the exchange will
* credit the user's balance of the proper currency.
*
* To withdraw from the exchange, the user simply reverses the "to" and "from"
* fields of the currency contract transfer message. The currency contract will
* require the "authority" of the exchange, but the exchange's init() function
* configured this permission to allow *anyone* to transfer from the exchange.
*
* To prevent people from stealing all the money from the exchange, the
* exchange's transfer handler requires both the authority of the receiver and
* asserts that the user has a sufficient balance on the exchange. Lacking
* both of these the exchange will kill the transfer.
*
* The exchange and one of the currency contracts are forced to execute in the same
* thread anytime there is a deposit or withdraw. The transaction containing
* the transfer are already required to include the exchange in the scope by
* the currency contract.
*
* creating, canceling, and filling orders do not require blocking either currency
* contract. Users can only deposit or withdraw to their own currency account.
*/
#include <exchange/exchange.hpp> /// defines transfer struct
#include <eoslib/print.hpp>
using namespace exchange;
using namespace eos;
namespace exchange {
inline void save( const Account& a ) {
if( a.isEmpty() ) {
print("remove");
Accounts::remove(a);
}
else {
print("store");
Accounts::store(a);
}
}
template<typename Lambda>
inline void modifyAccount( AccountName a, Lambda&& modify ) {
auto acnt = getAccount( a );
modify( acnt );
save( acnt );
}
/**
* This method is called after the "transfer" action of code
* "currencya" is called and "exchange" is listed in the notifiers.
*/
void apply_currency_transfer( const currency::Transfer& transfer ) {
if( transfer.to == N(exchange) ) {
modifyAccount( transfer.from, [&]( Account& account ){
account.currency_balance += transfer.quantity;
});
} else if( transfer.from == N(exchange) ) {
requireAuth( transfer.to ); /// require the receiver of funds (account owner) to authorize this transfer
modifyAccount( transfer.to, [&]( Account& account ){
account.currency_balance -= transfer.quantity;
});
} else {
assert( false, "notified on transfer that is not relevant to this exchange" );
}
}
/**
* This method is called after the "transfer" action of code
* "currencya" is called and "exchange" is listed in the notifiers.
*/
void apply_eos_transfer( const eos::Transfer& transfer ) {
if( transfer.to == N(exchange) ) {
modifyAccount( transfer.from, [&]( Account& account ){
account.eos_balance += transfer.quantity;
});
} else if( transfer.from == N(exchange) ) {
requireAuth( transfer.to ); /// require the receiver of funds (account owner) to authorize this transfer
modifyAccount( transfer.to, [&]( Account& account ){
account.eos_balance -= transfer.quantity;
});
} else {
assert( false, "notified on transfer that is not relevant to this exchange" );
}
}
void match( Bid& bid, Account& buyer, Ask& ask, Account& seller ) {
print( "match bid: ", bid, "\nmatch ask: ", ask, "\n");
eos::Tokens ask_eos = ask.quantity * ask.price;
EosTokens fill_amount_eos = min<eos::Tokens>( ask_eos, bid.quantity );
CurrencyTokens fill_amount_currency;
if( fill_amount_eos == ask_eos ) { /// complete fill of ask
fill_amount_currency = ask.quantity;
} else { /// complete fill of buy
fill_amount_currency = fill_amount_eos / ask.price;
}
print( "\n\nmatch bid: ", Name(bid.buyer.name), ":", bid.buyer.number,
"match ask: ", Name(ask.seller.name), ":", ask.seller.number, "\n\n" );
bid.quantity -= fill_amount_eos;
seller.eos_balance += fill_amount_eos;
ask.quantity -= fill_amount_currency;
buyer.currency_balance += fill_amount_currency;
}
/**
*
*
*/
void apply_exchange_buy( BuyOrder order ) {
Bid& bid = order;
requireAuth( bid.buyer.name );
assert( bid.quantity > eos::Tokens(0), "invalid quantity" );
assert( bid.expiration > now(), "order expired" );
print( Name(bid.buyer.name), " created bid for ", order.quantity, " currency at price: ", order.price, "\n" );
Bid existing_bid;
assert( !BidsById::get( bid.buyer, existing_bid ), "order with this id already exists" );
print( __FILE__, __LINE__, "\n" );
auto buyer_account = getAccount( bid.buyer.name );
buyer_account.eos_balance -= bid.quantity;
Ask lowest_ask;
if( !AsksByPrice::front( lowest_ask ) ) {
print( "\n No asks found, saving buyer account and storing bid\n" );
assert( !order.fill_or_kill, "order not completely filled" );
Bids::store( bid );
buyer_account.open_orders++;
save( buyer_account );
return;
}
print( "ask: ", lowest_ask, "\n" );
print( "bid: ", bid, "\n" );
auto seller_account = getAccount( lowest_ask.seller.name );
while( lowest_ask.price <= bid.price ) {
print( "lowest ask <= bid.price\n" );
match( bid, buyer_account, lowest_ask, seller_account );
if( lowest_ask.quantity == CurrencyTokens(0) ) {
seller_account.open_orders--;
save( seller_account );
save( buyer_account );
Asks::remove( lowest_ask );
if( !AsksByPrice::front( lowest_ask ) ) {
break;
}
seller_account = getAccount( lowest_ask.seller.name );
} else {
break; // buyer's bid should be filled
}
}
print( "lowest_ask >= bid.price or buyer's bid has been filled\n" );
if( bid.quantity && !order.fill_or_kill ) buyer_account.open_orders++;
save( buyer_account );
print( "saving buyer's account\n" );
if( bid.quantity ) {
print( bid.quantity, " eos left over" );
assert( !order.fill_or_kill, "order not completely filled" );
Bids::store( bid );
return;
}
print( "bid filled\n" );
}
void apply_exchange_sell( SellOrder order ) {
Ask& ask = order;
requireAuth( ask.seller.name );
assert( ask.quantity > CurrencyTokens(0), "invalid quantity" );
assert( ask.expiration > now(), "order expired" );
print( "\n\n", Name(ask.seller.name), " created sell for ", order.quantity,
" currency at price: ", order.price, "\n");
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;
Bid highest_bid;
if( !BidsByPrice::back( highest_bid ) ) {
assert( !order.fill_or_kill, "order not completely filled" );
print( "\n No bids found, saving seller account and storing ask\n" );
Asks::store( ask );
seller_account.open_orders++;
save( seller_account );
return;
}
print( "\n bids found, lets see what matches\n" );
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 == EosTokens(0) ) {
buyer_account.open_orders--;
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
}
}
if( ask.quantity && !order.fill_or_kill ) seller_account.open_orders++;
save( seller_account );
if( ask.quantity ) {
assert( !order.fill_or_kill, "order not completely filled" );
print( "saving ask\n" );
Asks::store( ask );
return;
}
print( "ask filled\n" );
}
void apply_exchange_cancel_buy( OrderID order ) {
requireAuth( order.name );
Bid bid_to_cancel;
assert( BidsById::get( order, bid_to_cancel ), "bid with this id does not exists" );
auto buyer_account = getAccount( order.name );
buyer_account.eos_balance += bid_to_cancel.quantity;
buyer_account.open_orders--;
Bids::remove( bid_to_cancel );
save( buyer_account );
print( "bid removed\n" );
}
void apply_exchange_cancel_sell( OrderID order ) {
requireAuth( order.name );
Ask ask_to_cancel;
assert( AsksById::get( order, ask_to_cancel ), "ask with this id does not exists" );
auto seller_account = getAccount( order.name );
seller_account.currency_balance += ask_to_cancel.quantity;
seller_account.open_orders--;
Asks::remove( ask_to_cancel );
save( seller_account );
print( "ask removed\n" );
}
} // namespace exchange
extern "C" {
void init() {
/*
setAuthority( "currencya", "transfer", "anyone" );
setAuthority( "currencyb", "transfer", "anyone" );
registerHandler( "apply", "currencya", "transfer" );
registerHandler( "apply", "currencyb", "transfer" );
*/
}
// void validate( uint64_t code, uint64_t action ) { }
// void precondition( uint64_t code, uint64_t action ) { }
/**
* The apply method implements the dispatch of events to this contract
*/
void apply( uint64_t code, uint64_t action ) {
if( code == N(exchange) ) {
switch( action ) {
case N(buy):
apply_exchange_buy( currentMessage<exchange::BuyOrder>() );
break;
case N(sell):
apply_exchange_sell( currentMessage<exchange::SellOrder>() );
break;
case N(cancelbuy):
apply_exchange_cancel_buy( currentMessage<exchange::OrderID>() );
break;
case N(cancelsell):
apply_exchange_cancel_sell( currentMessage<exchange::OrderID>() );
break;
default:
assert( false, "unknown action" );
}
}
else if( code == N(currency) ) {
if( action == N(transfer) )
apply_currency_transfer( currentMessage<currency::Transfer>() );
}
else if( code == N(eos) ) {
if( action == N(transfer) )
apply_eos_transfer( currentMessage<eos::Transfer>() );
}
else {
}
}
}
init和apply函数
可以看到init()函数除了注释没有具体代码,说明当前合约初始化时不需要初始化其他的数据。
重点看apply函数,该函数是真正的入口函数。可以看到该函数里面根据code区分当前是exchange还是currency还是eos,若是exchange就走交易合约,若是currency就走currency的转账合约,若是eos就走eos的转账合约。
我们重点看exchange。
exchange
可以看到exchange根据四个action分别走了四个分支
switch( action ) {
case N(buy):
apply_exchange_buy( currentMessage<exchange::BuyOrder>() );
break;
case N(sell):
apply_exchange_sell( currentMessage<exchange::SellOrder>() );
break;
case N(cancelbuy):
apply_exchange_cancel_buy( currentMessage<exchange::OrderID>() );
break;
case N(cancelsell):
apply_exchange_cancel_sell( currentMessage<exchange::OrderID>() );
break;
default:
assert( false, "unknown action" );
}
细节就不展开讲了,因为把代码讲明白可能需要的文字数量是代码的好几倍。
总结
从上面的分析我们其实可以看出来一个智能合约的基本结构,同时可以学习一下智能合约编写的基本思路。就是定义数据结构、基本操作以及相关存储最终实现相关逻辑。当然目前的合约只是一个样例,不能实用,达到真正实用的合约还需要很多的打磨。后面后持续跟进。
ps:使用markdown写出来的代码好看多了,之前只能使用引用的,现在可以用代码段了。
网友评论