区块链研习社区块链学习

EOS开发系列(四)exchange合约样例解读

2017-10-22  本文已影响1327人  王巨

概述

今天我们来解读一下在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写出来的代码好看多了,之前只能使用引用的,现在可以用代码段了。

上一篇下一篇

猜你喜欢

热点阅读