美文网首页区块链研习社区块链学习
EOS开发系列(四)exchange合约样例解读

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

作者: 王巨 | 来源:发表于2017-10-22 11:17 被阅读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写出来的代码好看多了,之前只能使用引用的,现在可以用代码段了。

    相关文章

      网友评论

        本文标题:EOS开发系列(四)exchange合约样例解读

        本文链接:https://www.haomeiwen.com/subject/galguxtx.html