EOS 教程

作者: cenkai88 | 来源:发表于2018-01-10 17:13 被阅读2189次

    1. 账户与钱包

    注意 本教程是基于 测试私网,但稍作修改就可以运用在测试公网上。

    您将学到

    您将学到如何创建钱包、管理钱包及其keys并通过eosc使用钱包和区块链交互。

    本教程的目标群体

    本教程目标群体是希望学习钱包和账户管理的人。我们将尽可能地介绍eosc以及EOS钱包和账户是如何交互的。有一定基础的用户可查看参考命令

    前提条件

    • 在您的系统上构建并运行eosceos-walletd
    • 命令行操作的基本知识。

    注意: 当使用docker安装时,命令可能需要稍作改动。

    1.1 创建并管理钱包

    打开终端,进入EOS目录

    这会是我们更方便地操作eosc,它是一个与eosdeos-walletd交互的命令行工具。

    $ cd /path_to_eos/build/programs/eosc
    

    首先您要用eoscwallet create创造一个钱包

    $ eosc wallet create
    Creating wallet: default
    Save password to use in the future to unlock this wallet.
    Without password imported keys will not be retrievable.
    "A MASTER PASSWORD"
    

    一个叫default的钱包现在已经在eos-walletd里了,并且返回了一个该钱包的一个master password。请将这个密码安全地保存起来。这个密码是用来解锁(解密)您的钱包文件的。

    该钱包文件叫做default.wallet,被保存在了您的EOS目录(您也可以在启动eos-walletd--data-dir制定特定目录)下的data-dir文件夹里。

    管理多个钱包和钱包名

    eosc能够管理多个钱包。每个钱包被各自的master password保护起来。下面的例子创建了另一个钱包并且展示了如何用 -n 参数给他命名

    $ eosc wallet create -n periwinkle
    Creating wallet: periwinkle
    Save password to use in the future to unlock this wallet.
    Without password imported keys will not be retrievable.
    "A MASTER PASSWORD"
    

    现在确认一下钱包已经用您指定的名字创建出来了。

    $ eosc wallet list
    Wallets:
    [
      "default *",
      "periwinkle *"
    ]
    

    每个钱包后面的星号 (*) 很重要,他们表示钱包已解锁。方便起见,我们用create wallet创建出来的钱包默认是解锁的。

    wallet lock锁住第二个钱包

    $ eosc wallet lock -n periwinkle
    Locked: 'periwinkle'
    

    再次运行wallet list,您就可以看到第二个星号不见了,表示该钱包已上锁。

    $ eosc wallet list
    Wallets:
    [
      "default *",
      "periwinkle"
    ]
    

    解锁一个有名字的钱包需要用wallet unlock命令并用-n参数指定钱包名,然后输入钱包的 master密码(您可以粘贴密码)。下面我们复制第二个钱包的master密码,执行此命令并粘贴密码后回车。然后您需要确认操作。

    $ eosc wallet unlock -n periwinkle
    

    eosc会告诉您钱包上锁了

    Unlocked: 'periwinkle'
    

    注意: 您也可以用 --password 参数后跟master密码,但是这会导致您的密码在控制台历史当中被明文地记录下来。

    现在查看一下钱包

    $ eosc wallet list
    Wallets:
    [
      "default *",
      "periwinkle *"
    ]
    

    好的,periwinkle钱包后面有星号,表示它解锁了。

    注意: 使用'default'钱包不需要使用-n参数

    现在重启 eos-walletd,退回到您调用eosc的路径下运行以下命令

    $ eosc wallet list
    Wallets:
    []
    

    有意思,钱包去哪了呢?

    钱包需要被打开,因为您关闭过eos-walletd,钱包并不在打开状态,运行以下命令:

    $ eosc wallet open
    $ eosc wallet list
    Wallets:
    [
      "default"
    ]
    

    好多了。

    注意: 如果您希望打开一个有名字的钱包,您可以$ eosc wallet open -n periwinkle,学会了吗? ;)

    从上面的信息中您可以看到钱包是默认锁住的,把它解锁才能进行下面的操作。
    执行wallet unlock命令并在要求输入密码时粘贴上default 钱包的master密码。

    $ eosc wallet unlock
    Unlocked: 'default'
    

    然后检查钱包是否已解锁。

    $ eosc wallet list
    Wallets:
    [
      "default *"
    ]
    

    钱包名后面有星号,已解锁,非常好。

    您已经学会如何创建多个钱包及如何用eosc操作他们了。但空钱包没什么意义,现在让我们导入keys。

    1.2 生成并导入EOS Keys

    生成EOS key对有好几种方法,本教程主要讲eosccreate key命令的方法。

    生成两个密钥对

    $ eosc create key
    Private key:###
    Public key: ###
    $ eosc create key
    Private key:###
    Public key: ###
    

    现在您有两个EOS 密钥对了。此时,他们只是最初始的密钥对,并没有authority。

    如果您一直根据上面来操作,您的default钱包应该是打开且解锁的。

    下面,我们执行wallet import命令两次,每次导入我们之前所生成的一个私钥到您的 default钱包。

    $ eosc wallet import ${private_key_1}
    

    然后是第二个私钥

    $ eosc wallet import ${private_key_2}
    

    如果顺利,每次wallet import命令都会返回您的私钥对应的公钥,您的控制台会是这样的:

    $ eosc wallet import ${private_key_1}
    imported private key for: ${public_key_1}
    $ eosc wallet import ${private_key_2}
    imported private key for: ${public_key_2}
    

    我们用wallet keys看看加载了哪些密钥

    $ eosc wallet keys
    [[
        "EOS6....",
        "5KQwr..."
      ],
      [
        "EOS3....",
        "5Ks0e..."
      ]
    ]
    

    钱包锁起来的时候,这些密钥也会被保护起来。要从一个被锁住的钱包中拿到密钥需要有钱包创建时的master密码。因为钱包文件本身是加密的,备份密钥对并不是一定要做的,但最好还是在一个安全的地方备份您的钱包文件。

    1.3 备份钱包

    现在您的钱包里已经有密钥对了,您最好养成备份但习惯,以防各种各样的原因造成钱包丢失。比如使用u盘。没有密码,钱包是强熵加密的,想拿到里面的密钥是非常难的 (基本不可能的)。

    您可以在data-dir文件夹下找到您的钱包文件。如果您在启动eos时用--data-dir参数指定过,您可以在/path/to/eos/build/programs/eosd中找到(eos的具体路径因系统不同而有不同)。

    $ cd /path_to_eos/build/programs/eosd && ls
    blockchain   blocks   config.ini   default.wallet   periwinkle.wallet
    

    进入文件夹后您将看到两个文件:default.walletperiwinkle.wallet。把他们保存起来(熟能生巧!)。

    1.4 创建账户

    如果您用的是测试公网,您需要有一个创世allocation或者从水龙头账户申请一个账户。下面操作时请进行适当改动 (提示:应当用您自己的账户替换 inita 账户)

    首先,我们看看 create account 命令及其必需参数:

    $ eosc create account inita ${desired_account_name} ${public_key_1} ${public_key_2}
    

    create account命令必需参数的解读

    • inita 是执行新建操作的账户名
    • desired_account_name 是您希望新建的账户名。
    • public_key_1public_key_2是公钥,第一个是用于获取您账户owner authority的, 第二个是用户获取active authority的。

    您之前生成了两个密钥对,您可以翻看控制台前面的记录或者执行wallet keys来查看。

    $ eosc wallet keys
    [[
        "EOS6....",
        "5KQwr..."
      ],
      [
        "EOS3....",
        "5Ks0e..."
      ]
    ]
    

    提醒一下,公钥是以EOS...开头。在您给密钥分配authority前,上面的密钥都是初始的。which one you decide to user for active and owner are inconsequential until you have created your account.

    注意, 您的owner密钥等于对您账户的全面控制,而active密钥等于对您账户资金的全面控制。

    用您之前所学的,替换命令中的占位符然后回车:

    $ eosc create account inita ${desired_account_name} ${public_key_1} ${public_key_2}
    

    您看到了一个提到"authorities"的报错了吗?不用着急,我是故意让您这么做的。您看到报错是因为您没有加载@inita这个账户的密钥。

    inita 的密钥存在 config.ini里。但为方便起见,我将其复制了出来放在了下面。直接运行下面的命令即可。

    $ eosc wallet import 5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3
    

    将会返回

    imported private key for: EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV
    

    现在 @inita 账户的密钥已经加载,重新回到报错之前的create account 命令并回车。

    顺利的话 eosc 将返回一个含有transaction ID的JSON对象,类似于下面:

    {
      "transaction_id": "6acd2ece68c4b86c1fa209c3989235063384020781f2c67bbb80bc8d540ca120",
      "processed": {
        "refBlockNum": "25217",
        "refBlockPrefix": "2095475630",
        "expiration": "2017-07-25T17:54:55",
        "scope": [
          "eos"...
    
    

    太好了!您现在已经在区块链上已经有一个账户了。

    您做的很棒,您创建了一个钱包,学习了一些钱包是如何工作、生成密钥及如何把密钥导入钱包的知识。

    2. 货币合约概览

    目标

    下面的教程将帮助用户了解github仓库中的样例货币合约

    概览

    货币合约处理的是将货币从一个账户转到另一个账户的工作,而不同账户的余额保存在每个用户的本地scope中。

    Action

    目前本合约只有一个action:
    currency_transfer:将货币从一个账户转到另一个账户。

    开始!

    智能合约分为三个文件:

    currency.hpp 合约中的声明和数据结构信息存在头文件中
    currency.cpp 合约的逻辑和实现
    currency.abi 提供给用户交互的接口定义

    头文件: currency.hpp

    首先导入所需库并定义您的命名空间

    // 导入所需库
    
    #include <eoslib/eos.hpp>   // Generic eos library, i.e. print, type, math, etc
    #include <eoslib/token.hpp> // Token usage
    #include <eoslib/db.hpp>    // Database access
    
    namespace currency {
        // Your code here
    }
    
    

    然后加入一个货币token。 It’s in fact a uin64_t wrapper which checks for proper types and under/overflows for standard-compatible token messages

    typedef eosio::token<uint64_t,N(currency)> currency_tokens;
    

    我们action的结构如下所示:

    struct transfer {
        account_name from;          //转出账户
        account_name to;            //转入账户
        currency_tokens quantity;   //转账金额
    };
    
    

    另外我们把余额信息存在表里。表是如下定义的:

    using accounts = eosio::table<N(defaultscope),N(currency),N(account),account,uint64_t>;
    
    • 第一个参数定义表的默认scope,比如当有没有指定scope的数据存入表中时,它就会使用这个账户。

    • 第二个参数定义表的所有者 (比如合约的名字)

    • 第三个参数定义表的名字

    • 第四个参数定义存储数据结构(将在后面定义)

    • 第五个参数定义表中key的类型

    一旦表定义了,需要储存的数据结构(在我们的例子中是“账户”)也需要被定义。这是在另一个struct中完成的:

    struct account {
        //Constructor
        account( currency_tokens b = currency_tokens() ):balance(b){}
    
        //key是常量,因为每个scope/currency/accounts只有一条记录
        const uint64_t key = N(account);
    
        //账户的token数量
        currency_tokens balance;
    
        // 用于检查账户是否为空的方法
        // 如果余额为0返回true
        bool is_empty()const { return balance.quantity == 0; }
    };
    
    

    这个结构包含一个构造器和一个用于判断账户是否为空的标准函数。

    需要注意的是,key的变量类型需要与之前在定义表时 (第五个函数)定义的类型一致。

    为方便起见,我们增加了一个存取器函数来获取所有者的账户信息,返回存在owner/TOKEN_NAME/account/account的信息。此函数存在头文件中以提供第三方获取用户余额的能力。

    inline account get_account( account_name owner ) {
        account owned_account;
        accounts::get( owned_account, owner );
        return owned_account;
    }
    
    

    注意: accounts:get函数返回账户所有者。为应对账户不存在的情况,它返回一个默认结构的账户。

    源代码文件:currency.cpp

    #include <currency/currency.hpp>
    
    // The init() and apply() methods must have C calling convention
    
    extern "C" {
        // Only called once
        void init() {
        }
    
        // The apply method implements the dispatch of events to this contract
        void apply( uint64_t code, uint64_t action_name ) {
            // Put your message handler here
        }
    } // extern "C"
    
    

    所有的合约都有以上的骨架,每个合约都需要有以上的函数:

    Init() 在一个合约的生命周期开始时被调用一次。可用它来设置环境来让合约正确运行。

    Apply( uint64_t code, uint64_t action_name) 被用作一个message的槽子。每次有message发给合约时,此函数即开始调用。它的两个参数含义如下:

    • code: 合约名称
    • action_name: action名称

    在货币合约中,init() 函数如下所示:

    void init() {
        account owned_account;
    
        //初始化货币账户,除非账户不存在
        if ( !accounts::get( owned_account, N(currency) )) {
            store_account( N(currency),
            account( currency_tokens(1000ll\*1000ll\*1000ll) ) );
        }
    }
    
    

    合约第一次运行时,它会检查currency账户是否有建立表且货币余额记录在表中。如果没有建立表就会生成一个新表,余额为1000,000,000,这样货币合约就成为了总量1000,000,000的货币单位的第一个所有者。

    message槽如下所示:

    void apply( uint64_t code, uint64_t action ) {
        if( code == N(currency) ) {
            if( action == N(transfer) )
                account::apply_currency_transfer( current_message<account::transfer >() );
        }
    }
    

    最好在上面的样例代码中实现一个message过滤器,使得合约只处理那些正确的messages并在过滤后调用message处理器。

    注意 current_message() 会在message传给特定处理器之前调用,它是用来将合约收到的message转为struct T的。

    Message处理器

    实际上的货币转账是在这里操作的:

    void apply_currency_transfer( const account::transfer& transfer_msg )
    {
        require_notice( transfer_msg.to, transfer_msg.from );
        require_auth( transfer_msg.from );
    
        auto from = get_account( transfer_msg.from );
        auto to = get_account( transfer_msg.to );
    
        from.balance -= transfer_msg.quantity;
        to.balance += transfer_msg.quantity;
    
        store_account( transfer_msg.from, from );
        store_account( transfer_msg.to, to );
    }
    
    

    代码非常直接,从转出账户扣除转账金额并增加到转入账户。

    require_notice函数是一个inline action,使得把收到的message转到另一个账户成为可能。此例中message被转发给了转入账户和转出账户。这是非常有用的功能,因为它把那些“被通知的账户”引入链上并发挥功能。
    require_auth函数使得message被正确地签名。在这个例子中,转出账户需要签名,这个transaction才能被正确地处理。

    注意我们正在使用头文件里的get_account函数来获得正确的账户对象。

    Since we are using tokens, automatic over and underflow assertions are being backed into the actual subtraction and addition operations.

    最后通过store_account函数更新余额。

    Store_account

    这个函数是用来实际处理余额的储存的:

    void store_account( account_name current_account, const account& value ) {
        if( a.is_empty() ) {
            accounts::remove( value, current_account);
        } else {
            accounts::store( value, current_account);
        }
    }
    
    

    有趣的是,如果账户(也就是在current_account的scope下创建的表)是空的,他就会被移除,这是因为只要有钱转到不存在的账户里,表就会被新建出来。

    移除不需要的表是一种节约资源的做法,是一种写智能合约的最佳实践。

    注意: 当把上面的样例代码和仓库里的实际代码比较时,请注意为了账户可以更简单的重命名,我们使用了TOKEN_NAME作为一种#define。上面的代码中,我们用账户名替代了TOKEN_NAME以使得代码更清晰。

    ABI文件: currency.abi

    Abi (即Application Binary Interface) 发送的message和二进制版本的智能合约之间的接口。我们先来看一个的通用版本,它包括如下对象:

    • struct: 合约中action/ table用到的数据结构的列表

    • actions: 合约中可用的actions的列表

    • tables: 合约中可用的tables的列表

    {
        "structs": \[{
            "name": "...",
            "base": "...",  
            "fields": { ... }
        }, ...\],
        "actions": \[{
            "action_name": "...",
            "type": "..."
        }, ...\],
        "tables": \[{
            "table_name": "...",
            "type": "...",
            "key_names" : \[...\],
            "key_types" : \[...\]
        }, ...\]
    }
    
    

    struct对象

    根据合约中头文件的信息,可以创建大多数ABI。因此我们从数据结构开始。头文件中有两个结构:

    struct transfer {
        account_name from;
        account_name to;
        currency_tokens quantity;
    };
    
    struct account {
        account( currency_tokens b = currency_tokens() ):balance(b){}
        const uint64_t key = N(account);
        currency_tokens balance;
        bool is_empty()const { return balance.quantity == 0; }
    };
    

    这些结构就生成了如下ABI信息:

    "structs": \[{
        "name": "transfer",
        "base": "",
        "fields": {
            "from": "account_name",
            "to": "account_name",
            "quantity": "uint64"
        }
    },{
        "name": "account",
        "base": "",
        "fields": {
            "key": "name",
            "balance": "uint64"
        }
    }\]
    
    

    action对象

    Action 对象也是类似的对应。在这里我们在货币合约中有一个叫 “transfer” action。看起来和下面的ABI文件类似:

    "actions": \[{
        "action_name": "transfer",
        "type": "transfer"
    }\]
    
    

    table对象

    头文件中, a single index called “account” table定义如下:

    eosio::table<N(defaultscope),N(currency),N(account),account,uint64_t>;
    
    

    这张表就转为下面的ABI对象:

    "tables": \[{
        "table_name": "account",
        "type": "account",
        "index_type": "i64",
        "key_names" : \["key"\],
        "key_types" : \["name"\]
    }\]
    
    

    这样就组成了ABI文件。

    部署与运行

    现在三个文件 (currency.hpp, currency.cpp, currency.abi) 都可以通过命令行部署了:

    $ eosc set contract currency currency.wast currency.abi
    

    请确认钱包已经解锁且含有 currency 的密钥。部署后合约的action可以通过命令行这样触发:

    $ eosc push message currency transfer ‘{“from”:“currency”,“to”:“tester”,“quantity”:50}’ -S currency -S tester -p currency@active
    

    3. “Hello World”智能合约

    为方便起见,我们创造了一个叫eoscpp的工具来引导产生新的智能合约。您需要先安装eosio/eos并把${CMAKE_INSTALL_PREFIX}/bin放入您的环境变量,它才能正常工作。

    $ eoscpp -n hello
    $ cd hello
    $ ls
    

    上面在'./hello'文件夹创建了一个新的空工程,里面有三个文件:

    hello.abi hello.hpp hello.cpp
    

    我们看一下最简单的合约:

    $ cat hello.cpp
    
    #include <hello.hpp>
    
    /**
     *  The init() and apply() methods must have C calling convention so that the blockchain can lookup and
     *  call these methods.
     */
    extern "C" {
    
        /**
         *  This method is called once when the contract is published or updated.
         */
        void init()  {
           eosio::print( "Init World!\n" );
        }
    
        /// The apply method implements the dispatch of events to this contract
        void apply( uint64_t code, uint64_t action ) {
           eosio::print( "Hello World: ", eosio::name(code), "->", eosio::name(action), "\n" );
        }
    
    } // extern "C"
    

    这个合约实现了两个入口, initapply。它所做的只是记录提交的messages而并不作检查。只要区块生产者同意,任何人在任何时间都可以提交任何message。但没有所需的签名,合约将因消耗带宽被收费。

    您可以将合约像这样编译成文本版本的WASM (.wast) :

    $ eoscpp -o hello.wast hello.cpp
    

    部署您的合约

    现在您已经编译了您的应用,我们可以部署了。这需要您先:

    1. 启动 eosd 并打开钱包插件
    2. 新建钱包,导入至少一个账户的密钥
    3. 解锁钱包

    如果您的钱包里有${account}的密钥且已经解锁,您就可以用下面的命令把合约上传到区块链上

    $ eosc set contract ${account} hello.wast hello.abi
    Reading WAST...
    Assembling WASM...
    Publishing contract...
    {
      "transaction_id": "1abb46f1b69feb9a88dbff881ea421fd4f39914df769ae09f66bd684436443d5",
      "processed": {
        "ref_block_num": 144,
        "ref_block_prefix": 2192682225,
        "expiration": "2017-09-14T05:39:15",
        "scope": [
          "eos",
          "${account}"
        ],
        "signatures": [
          "2064610856c773423d239a388d22cd30b7ba98f6a9fbabfa621e42cec5dd03c3b87afdcbd68a3a82df020b78126366227674dfbdd33de7d488f2d010ada914b438"
        ],
        "messages": [{
            "code": "eos",
            "type": "setcode",
            "authorization": [{
                "account": "${account}",
                "permission": "active"
              }
            ],
            "data": "0000000080c758410000f1010061736d0100000001110460017f0060017e0060000060027e7e00021b0203656e76067072696e746e000103656e76067072696e7473000003030202030404017000000503010001071903066d656d6f7279020004696e69740002056170706c7900030a20020600411010010b17004120100120001000413010012001100041c00010010b0b3f050041040b04504000000041100b0d496e697420576f726c64210a000041200b0e48656c6c6f20576f726c643a20000041300b032d3e000041c0000b020a000029046e616d6504067072696e746e0100067072696e7473010004696e697400056170706c790201300131010b4163636f756e744e616d65044e616d6502087472616e7366657200030466726f6d0b4163636f756e744e616d6502746f0b4163636f756e744e616d6506616d6f756e740655496e743634076163636f756e740002076163636f756e74044e616d650762616c616e63650655496e74363401000000b298e982a4087472616e736665720100000080bafac6080369363401076163636f756e7400076163636f756e74"
          }
        ],
        "output": [{
            "notify": [],
            "deferred_transactions": []
          }
        ]
      }
    }
    

    如果您查看eosd 进程的输出您将看到:

    ...] initt generated block #188249 @ 2017-09-13T22:00:24 with 0 trxs  0 pending
    Init World!
    Init World!
    Init World!
    

    您可以看到"Init World!"被执行了三次,这其实并不是个bug。区块链处理transactions的流程是:

    1: eosd收到一个新transaction (正在验证的transaction)

    • 创建一个新的临时会话
    • 尝试应用此transaction
    • 成功并打印出"Init World!"
    • 失败则回滚所做的变化 (也有可能打印"Init World!"后失败)

    2 : eosd开始产出区块

    • 撤销所有pending状态
    • pushes all transactions as it builds the block
    • 第二次打印"Init World!"
    • 完成区块
    • 撤销所有创造区块时的临时变化

    3rd : eosd如同从网络上获得区块一样将区块追加到链上。

    • 第三次打印 "Init World!"

    此时,您的合约就可以开始接受messages了。因为默认message处理器接受所有messages,我们可以发送任何我们想发的东西。我们试一下发一个空的message:

    $ eosc push message ${account} hello '"abcd"' --scope ${account}
    

    此命令将"hello"message及16进制字符串"abcd"所代表的二进制文件传出。注意,后面我们将展示如何定义ABI来用一个好看易读的JSON对象替换16进制字符串。以上,我们只是想证明“hello”类型的message是如何发送到账户的。

    结果是:

    {
      "transaction_id": "69d66204ebeeee68c91efef6f8a7f229c22f47bcccd70459e0be833a303956bb",
      "processed": {
        "ref_block_num": 57477,
        "ref_block_prefix": 1051897037,
        "expiration": "2017-09-13T22:17:04",
        "scope": [
          "${account}"
        ],
        "signatures": [],
        "messages": [{
            "code": "${account}",
            "type": "hello",
            "authorization": [],
            "data": "abcd"
          }
        ],
        "output": [{
            "notify": [],
            "deferred_transactions": []
          }
        ]
      }
    }
    

    如果您继续查看eosd的输出,您将在屏幕上看到:

    Hello World: ${account}->hello
    Hello World: ${account}->hello
    Hello World: ${account}->hello
    

    再一次,您的合约在transaction被第三次应用并成为产出的区块之前被执行和撤销了两次。

    Message名的限定

    Message的类型实际上是base32编码的64位整数。所以Message名的前12个字符需限制在字母a-z, 1-5, 以及'.' 。第13个以后的字符限制在前16个字符('.' and a-p)。

    ABI - Application Binary Interface

    Application Binary Interface (ABI)是一个基于JSON的描述文件,是关于转换JSON和二进制格式的用户actions的。ABI还描述了如何将数据库状态和JSON的互相转换。一旦您通过ABI描述了您的合约,开发者和用户就能够用JSON和您的合约无缝交互了。

    我们正在开发使用C++源码自动生成ABI的工具,但目前为止您还是只能手动生成。

    这里是一个合约的骨架ABI的例子:

    {
      "types": [{
          "new_type_name": "account_name",
          "type": "name"
        }
      ],
      "structs": [{
          "name": "transfer",
          "base": "",
          "fields": {
            "from": "account_name",
            "to": "account_name",
            "quantity": "uint64"
          }
        },{
          "name": "account",
          "base": "",
          "fields": {
            "account": "name",
            "balance": "uint64"
          }
        }
      ],
      "actions": [{
          "action": "transfer",
          "type": "transfer"
        }
      ],
      "tables": [{
          "table": "account",
          "type": "account",
          "index_type": "i64",
          "key_names" : ["account"],
          "key_types" : ["name"]
        }
      ]
    }
    

    您肯定注意到了这个ABI 定义了一个叫transfer的action,它的类型也是transfer。这就告诉EOS.IO当${account}->transfer的message发生时,它的payload是transfer类型的。 transfer类型是在structs的列表中定义的,其中有个对象,name属性是transfer

    ...
      "structs": [{
          "name": "transfer",
          "base": "",
          "fields": {
            "from": "account_name",
            "to": "account_name",
            "quantity": "uint64"
          }
        },{
    ...
    

    这部分包括from, toquantity等字段。这些字段都有对应的类型:account_nameuint64account_nametypes 列表中被定义为name的别名,而name是一个内置类型,用于用base32编码uint64_t (比如账户名)。

    {
      "types": [{
          "new_type_name": "account_name",
          "type": "name"
        }
      ],
    ...
    

    在弄清骨架ABI后,我们可以构造一个transfer类型的message:

    eosc push message ${account} transfer '{"from":"currency","to":"inita","quantity":50}' --scope initc
    2570494ms thread-0   main.cpp:797                  operator()           ] Converting argument to binary...
    {
      "transaction_id": "b191eb8bff3002757839f204ffc310f1bfe5ba1872a64dda3fc42bfc2c8ed688",
      "processed": {
        "ref_block_num": 253,
        "ref_block_prefix": 3297765944,
        "expiration": "2017-09-14T00:44:28",
        "scope": [
          "initc"
        ],
        "signatures": [],
        "messages": [{
            "code": "initc",
            "type": "transfer",
            "authorization": [],
            "data": {
              "from": "currency",
              "to": "inita",
              "quantity": 50
            },
            "hex_data": "00000079b822651d000000008040934b3200000000000000"
          }
        ],
        "output": [{
            "notify": [],
            "deferred_transactions": []
          }
        ]
      }
    }
    

    如果您继续观察eosd的输出,您将看到:

    Hello World: ${account}->transfer
    Hello World: ${account}->transfer
    Hello World: ${account}->transfer
    
    

    处理转账Message的参数

    根据ABI,transfer message应该是如下格式的:

         "fields": {
             "from": "account_name",
             "to": "account_name",
             "quantity": "uint64"
         }
    

    我们也知道account_name -> uint64表示这个message的二进制表示如同:

    struct transfer {
        uint64_t from;
        uint64_t to;
        uint64_t quantity;
    };
    

    EOS.IO的C API通过Message API提供获取message的payload的能力:

    uint32_t message_size();
    uint32_t read_message( void* msg, uint32_t msglen );
    
    

    让我们修改hello.cpp来打印出消息内容:

    #include <hello.hpp>
    
    /**
     *  The init() and apply() methods must have C calling convention so that the blockchain can lookup and
     *  call these methods.
     */
    extern "C" {
    
        /**
         *  This method is called once when the contract is published or updated.
         */
        void init()  {
           eosio::print( "Init World!\n" );
        }
    
        struct transfer {
           uint64_t from;
           uint64_t to;
           uint64_t quantity;
        };
    
        /// The apply method implements the dispatch of events to this contract
        void apply( uint64_t code, uint64_t action ) {
           eosio::print( "Hello World: ", eosio::name(code), "->", eosio::name(action), "\n" );
           if( action == N(transfer) ) {
              transfer message;
              static_assert( sizeof(message) == 3*sizeof(uint64_t), "unexpected padding" );
              auto read = readMessage( &message, sizeof(message) );
              assert( read == sizeof(message), "message too short" );
              eosio::print( "Transfer ", message.quantity, " from ", eosio::name(message.from), " to ", eosio::name(message.to), "\n" );
           }
        }
    
    } // extern "C"
    

    这样我们就可以重编译并部署了:

    eoscpp -o hello.wast hello.cpp 
    eosc set contract ${account} hello.wast hello.abi
    

    eosd因为重部署将再次调用init()

    Init World!
    Init World!
    Init World!
    

    然后我们执行transfer:

    $ eosc push message ${account} transfer '{"from":"currency","to":"inita","quantity":50}' --scope ${account}
    {
      "transaction_id": "a777539b7d5f752fb40e6f2d019b65b5401be8bf91c8036440661506875ba1c0",
      "processed": {
        "ref_block_num": 20,
        "ref_block_prefix": 463381070,
        "expiration": "2017-09-14T01:05:49",
        "scope": [
          "${account}"
        ],
        "signatures": [],
        "messages": [{
            "code": "${account}",
            "type": "transfer",
            "authorization": [],
            "data": {
              "from": "currency",
              "to": "inita",
              "quantity": 50
            },
            "hex_data": "00000079b822651d000000008040934b3200000000000000"
          }
        ],
        "output": [{
            "notify": [],
            "deferred_transactions": []
          }
        ]
      }
    }
    

    后面我们将看到eosd有如下输出:

    Hello World: ${account}->transfer
    Transfer 50 from currency to inita
    Hello World: ${account}->transfer
    Transfer 50 from currency to inita
    Hello World: ${account}->transfer
    Transfer 50 from currency to inita
    

    使用 C++ API来读取 Messages

    目前我们使用是C API因为这是EOS.IO直接暴露给WASM虚拟机的最底层的API。幸运的是,eoslib提供了一个更高级的API,移除了很多不必要的代码。

    /// eoslib/message.hpp
    namespace eosio {
         template<typename T>
         T current_message();
    }
    

    我们可以向下面一样更新 hello.cpp 把它变得更简洁:

    #include <hello.hpp>
    
    /**
     *  The init() and apply() methods must have C calling convention so that the blockchain can lookup and
     *  call these methods.
     */
    extern "C" {
    
        /**
         *  This method is called once when the contract is published or updated.
         */
        void init()  {
           eosio::print( "Init World!\n" );
        }
    
        struct transfer {
           eosio::name from;
           eosio::name to;
           uint64_t quantity;
        };
    
        /// The apply method implements the dispatch of events to this contract
        void apply( uint64_t code, uint64_t action ) {
           eosio::print( "Hello World: ", eosio::name(code), "->", eosio::name(action), "\n" );
           if( action == N(transfer) ) {
              auto message = eosio::current_message<transfer>();
              eosio::print( "Transfer ", message.quantity, " from ", message.from, " to ", message.to, "\n" );
           }
        }
    
    } // extern "C"
    
    

    您可以注意到我们更新了transfer的struct,直接使用eosio::name 类型并将read_message前后的类型检查压缩为一个单个的current-Message调用。

    在编译和上传后,您将看到和C语言版本同样的结果。

    获取发送者的Authority来进行转账

    合约最普遍的需求之一就是定义谁可以进行这样的操作。比如在货币转账的例子里,我们就需要定义为from字段的账户核准此message。

    EOS.IO软件负责加强和验证签名,您需要做的是获取所需的authority。

        ...
        void apply( uint64_t code, uint64_t action ) {
           eosio::print( "Hello World: ", eosio::name(code), "->", eosio::name(action), "\n" );
           if( action == N(transfer) ) {
              auto message = eosio::current_message<transfer>();
              eosio::require_auth( message.from );
              eosio::print( "Transfer ", message.quantity, " from ", message.from, " to ", message.to, "\n" );
           }
        }
        ...
    

    建立和部署后,我们可以再试一次转账:

     eosc push message ${account} transfer '{"from":"initb","to":"inita","quantity":50}' --scope ${account}
     1881603ms thread-0   main.cpp:797                  operator()           ] Converting argument to binary...
     1881630ms thread-0   main.cpp:851                  main                 ] Failed with error: 10 assert_exception: Assert Exception
     status_code == 200: Error
     : 3030001 tx_missing_auth: missing required authority
     Transaction is missing required authorization from initb
         {"acct":"initb"}
             thread-0  message_handling_contexts.cpp:19 require_authorization
    ...
    

    如果您查看eosd ,您将看到:

    Hello World: initc->transfer
    1881629ms thread-0   chain_api_plugin.cpp:60       operator()           ] Exception encountered while processing chain.push_transaction:
    ...
    
    

    这表示此操作尝试请求应用您的transaction,打印出了初始的"Hello World",然后当eosio::require_auth没能成功获取initb账户的authorization后,操作终止了。

    我们可以通过让eosc增加所需的permission来修复这个问题:

     eosc push message ${account} transfer '{"from":"initb","to":"inita","quantity":50}' --scope ${account} --permission initb@active
    

    --permission 命令定义了账户和permission等级,此例中我们使用active authority,也就是默认值。

    这次转账应该就成功了,如同我们之前看到的一样。

    Aborting a Message on Error

    绝大多数合约开发中有非常多的前置条件,比如转账的金额要大于0。如果用户尝试进行一个非法action,合约必须终止且已做出的任何变动都必须自动回滚。

        ...
        void apply( uint64_t code, uint64_t action ) {
           eosio::print( "Hello World: ", eosio::name(code), "->", eosio::name(action), "\n" );
           if( action == N(transfer) ) {
              auto message = eosio::current_message<transfer>();
              assert( message.quantity > 0, "Must transfer a quantity greater than 0" );
              eosio::require_auth( message.from );
              eosio::print( "Transfer ", message.quantity, " from ", message.from, " to ", message.to, "\n" );
           }
        }
        ...
    

    我们编译、部署并尝试进行一次金额为0的转账:

     $ eoscpp -o hello.wast hello.cpp
     $ eosc set contract ${account} hello.wast hello.abi
     $ eosc push message ${account} transfer '{"from":"initb","to":"inita","quantity":0}' --scope initc --permission initb@active
     3071182ms thread-0   main.cpp:851                  main                 ] Failed with error: 10 assert_exception: Assert Exception
     status_code == 200: Error
     : 10 assert_exception: Assert Exception
     test: assertion failed: Must transfer a quantity greater than 0
    

    4. Tic-Tac-Toe

    目标

    下面的教程将引导用户构建一个样例的PvP的游戏合约。我们用tic tac toe游戏来举例。本教程的结果在 这里.

    前提

    在此游戏中,我们用标准的3x3 tic tac toe板。玩家们有两种角色hostchallenger。Host 永远是先手。每个玩家只能同时玩两局比赛,一局是第一个玩家是host另一局是第二个玩家是host。

    游戏板

    (0,0) (1,0) (2,0)
    (0,0) - o x
    (0,1) - x -
    (0,2) x o o

    不同于传统的tic tac toe游戏,我们不用ox ,而用1 代表host的一步,2代表challenger的一步,0代表空各自。而且我们使用一维数组来保存游戏数据。因此:

    (0,0) (1,0) (2,0)
    (0,0) - o x
    (0,1) - x -
    (0,2) x o o

    假设 x ,是host上面的游戏板可表示为[0, 2, 1, 0, 1, 0, 1, 2, 2]

    Action

    用户需要用下列actions来和合约交互:

    • create: 创建一个新游戏
    • restart: 重启一个现有的游戏, host或challenger都可以这么做
    • close: 关闭一个现有的游戏,释放存储游戏的数据,只有host可以这么做
    • move: 走一步

    合约账户

    在下面的教程中,我们将把合约添加到一个叫tic.tac.toe的账户中。为防止tic.tac.toe的账户名被占用,您可以用其他的账户名,只需要在代码里面用您的账户名替换掉tic.tac.toe 。如果您没有账户,请先创建。

    $ eosc create account ${creator_name} ${contract_account_name} ${contract_pub_owner_key} ${contract_pub_active_key} --permission ${creator_name}@active
    # e.g. $ eosc create account inita tic.tac.toe  EOS4toFS3YXEQCkuuw1aqDLrtHim86Gz9u3hBdcBw5KNPZcursVHq EOS7d9A3uLe6As66jzN8j44TXJUqJSK3bFjjEEqR4oTvNAB3iM9SA --permission inita@active
    

    请先解锁钱包并导入私钥,否则上面的命令将失败。

    开始!

    我们将创建三个文件:

    • tic_tac_toe.hpp => 定义合约结构的头文件
    • tic_tac_toe.cpp => 合约的主要逻辑
    • tic_tac_toe.abi => 用户和合约交互的接口

    定义结构

    让我们先从定义合约结构开始。打开tic_tac_toe.hpp 并且从下面的模版代码开始

    // Import necessary library
    #include <eoslib/eos.hpp> // Generic eos library, i.e. print, type, math, etc
    #include <eoslib/db.hpp> // Database access
    
    using namespace eosio;
    namespace tic_tac_toe {
        // Your code here
    }
    

    游戏表

    对于这个合约我们需要把游戏列表存在表中,我们来定义它:

    ...
    namespace tic_tac_toe {
        ...
        using Games = eosio::table<N(tic.tac.toe),N(tic.tac.toe),N(games),game,uint64_t>;
    }
    
    

    NB: 如果您要把合约上传到其他账户上,请用您的账户名替代tic.tac.toe

    第一个参数定义表的默认scope,比如当有没有指定scope的数据存入表中时,它就会使用这个账户。

    • 第二个参数定义表的所有者 (比如合约的名字)

    • 第三个参数定义表的名字

    • 第四个参数定义存储数据结构(将在后面定义)

    • 第五个参数定义表中key的类型

    游戏结构

    下面我们来定义游戏的结构。注意在代码中,定义结构需要在定义表之前。

    ...
    namespace tic_tac_toe {
        struct PACKED(game) {
            // 默认 constructor
            game() {};
            // Constructor
            game(account_name challenger, account_name host):challenger(challenger), host(host), turn(host) {
                // 初始化游戏板
                initialize_board();
            };
            // challenger的账户名,也是表中的key
            account_name     challenger;
            // host的账户名
            account_name     host;
            // 轮到谁走, = 可能是host或challenger的账户名
            account_name     turn; 
            // 赢家, = 空或平手或者是host或challenger的账户名
            account_name     winner = N(none); 
            // 游戏板列表的长度,需放在游戏板列表的前面一个。有此字段abi序列化工具才能正确的可以打包写入数据库或从数据库拆包数据
            uint8_t          board_len = 9;
            // 游戏板列表
            uint8_t          board[9]; 
    
            // 用空格初始化游戏板
            void initialize_board() {
                for (uint8_t i = 0; i < board_len ; i++) {
                board[i] = 0;
                }
            }
    
            // 重置游戏
            void reset_game() {
                initialize_board();
                turn = host;
                winner = N(none);
            }
        };
        ...
    }
    

    记住,在前面表定义的时候,我们声明表的key数据类型是uint64_t。因此,在前面的游戏结构中,结构中前sizeof(uint64_t)字节长度的数据将被当成表的key。顺便一提,account_name只是uint64_t的别名。

    Action 结构

    Create

    要新建游戏,我们需要 host 账户名和 challenger 账户名。

    ...
    namespace tic_tac_toe {
        ...
        struct create {
            account_name   challenger;
            account_name   host;
        };
        ...
    }
    
    Restart

    要重启游戏,我们需要host 账户名和 challenger 账户名来找到该游戏。而且,我们需要指定是谁重启了游戏,这样才能验证是否有有效的签名。

    ...
    namespace tic_tac_toe {
        ...
        struct restart {
            account_name   challenger;
            account_name   host;
            account_name   by;
        };
        ...
    }
    
    Close

    要关闭游戏,我们需要host 账户名和 challenger 账户名来找到该游戏。

    ...
    namespace tic_tac_toe {
        ...
        struct close {
            account_name   challenger;
            account_name   host;
        };
        ...
    }
    
    Move

    要移动一步,我们需要host 账户名和 challenger 账户名来找到该游戏。 而且,我们需要指定是谁走的这一步以及这一步走在哪。

    ...
    namespace tic_tac_toe {
        ...
        struct movement {
            uint32_t    row;
            uint32_t    column;
        };
    
        struct Move {
            account_name   challenger;
            account_name   host;
            account_name   by; // the account who wants to make the move
            movement       m;
        };
        ...
    }
    

    您可以在 这里 找到atic_tac_toe.hpp 的最终代码。

    主程序

    打开tic_tac_toe.cpp并配置骨架代码

    #include <tic_tac_toe.hpp>
    using namespace eosio;
    /**
    *  The init() and apply() methods must have C calling convention so that the blockchain can lookup and
    *  call these methods.
    */
    extern "C" {
    
      // Only called once
      void init()  {
      }
    
      /// The apply method implements the dispatch of events to this contract
      void apply( uint64_t code, uint64_t action_name ) {
          // Put your message handler here
      }
    
    } // extern "C"
    

    Message 处理器

    我们希望tic_tac_toe合约仅响应发给tic.tac.toe账户的message并且根据不同的action类型来给出不同响应。让我们在apply函数中加入message过滤器。

      ...
      void apply( uint64_t code, uint64_t action_name ) {
            if (code == N(tic.tac.toe)) {
                if (action_name == N(create)) {
                    tic_tac_toe::apply_create(current_message<tic_tac_toe::create>());
                } else if (action_name == N(restart)) {
                    tic_tac_toe::apply_restart(current_message<tic_tac_toe::restart>());
                } else if (action_name == N(close)) {
                    tic_tac_toe::apply_close(current_message<tic_tac_toe::close>());
                } else if (action_name == N(move)) {
                    tic_tac_toe::apply_move(current_message<tic_tac_toe::move>());
                }
            }
      }
      ...
    

    注意我们在把message传入特定处理器之前使用了current_message<T>(),它是将收到的message 转为struct T的。

    NB: 如果您正部署到另一个账户,请用您的账户名替换tic.tac.toe

    为了简洁起见,我们把message处理器包装在namespace tic_tac_toe中:

    namespace tic_tac_toe {
    
      void apply_create(const create& c) {
        // Put code for create action here
      }
    
      void apply_restart(const restart& r) {
        // Put code for restart action here
      }
    
      void apply_close(const close& c) {
        // Put code for close action here
      }
    
      void apply_move(const move& m) {
        // Put code for move action here
      }
      ...
    }
    
    

    create Message 处理器

    对于create message的处理器,我们需要

    1. 确保message有host的签名
    2. 确保同一个玩家并不在玩这盘游戏
    3. 确保该游戏不存在
    4. 把新建的游戏存入数据库
    namespace tic_tac_toe {
        ...
        void apply_create(const create& c) {
            require_auth(c.host);
            assert(c.challenger != c.host, "challenger shouldn't be the same as host");
    
            // Check if game already exists
            game existing_game;
            bool game_exists = Games::get(c.challenger, existing_game, c.host);
            assert(game_exists == false, "game already exists");
    
            game game_to_create(c.challenger, c.host);
            Games::store(game_to_create, c.host);
        }
        ...
    }
    
    

    Restart Message 处理器

    对于 restart message 处理器,我们需要:

    1. 确保message有host或challenger的签名
    2. 确保该游戏存在
    3. 确保重启的action是host或challenger做出的
    4. 重启游戏
    5. 将更新过的游戏存入数据库
    namespace tic_tac_toe {
        ...
        void apply_restart(const restart& r) {
            require_auth(r.by);
    
            // Check if game exists
            game game_to_restart;
            bool game_exists = Games::get(r.challenger, game_to_restart, r.host);
            assert(game_exists == true, "game doesn't exist!");
    
            // Check if this game belongs to the message sender
            assert(r.by == game_to_restart.host || r.by == game_to_restart.challenger, "this is not your game!");
    
            // Reset game
            game_to_restart.reset_game();
    
            Games::update(game_to_restart, game_to_restart.host);
        }
        ...
    }
    
    

    Close Message 处理器

    对于close message 处理器,我们需要:

    1. 确保message有host的签名
    2. 确保该游戏存在
    3. 将该游戏从数据库移除
    namespace tic_tac_toe {
        ...
        void apply_close(const close& c) {
            require_auth(c.host);
    
            // Check if game exists
            game game_to_close;
            bool game_exists = Games::get(c.challenger, game_to_close, c.host);
            assert(game_exists == true, "game doesn't exist!");
    
            Games::remove(game_to_close, game_to_close.host);
        }
        ...
    }
    
    

    Move Message处理器

    对于move message处理器,我们需要:

    1. 确保message有host或challenger的签名
    2. 确保该游戏存在
    3. 确保该游戏并未结束
    4. 确保move的action是host或challenger做出的
    5. 确保轮到了正确的玩家行动
    6. 验证这一步是有效的
    7. 用这一步升级游戏板
    8. 将move_turn分给另一个玩家
    9. 判断赢家
    10. 把更新过的数据存入数据库
    namespace tic_tac_toe {
        ...
        bool is_valid_movement(const movement& mvt, const game& game_for_movement) {
        // Put code here
        }
    
        account_name get_winner(const game& current_game) {
            // Put code here
        }
    
        void apply_move(const move& m) {
            require_auth(m.by);
    
            // Check if game exists
            game game_to_move;
            bool game_exists = Games::get(m.challenger, game_to_move, m.host);
            assert(game_exists == true, "game doesn't exist!");
    
            // Check if this game hasn't ended yet
            assert(game_to_move.winner == N(none), "the game has ended!");
            // Check if this game belongs to the message sender
            assert(m.by == game_to_move.host || m.by == game_to_move.challenger, "this is not your game!");
            // Check if this is the  message sender's turn
            assert(m.by == game_to_move.turn, "it's not your turn yet!");
    
            // Check if user makes a valid movement
            assert(is_valid_movement(m.mvt, game_to_move), "not a valid movement!");
    
            // Fill the cell, 1 for host, 2 for challenger
            bool is_movement_by_host = m.by == game_to_move.host;
            if (is_movement_by_host) {
            game_to_move.board[m.mvt.row * 3 + m.mvt.column] = 1;
            game_to_move.turn = game_to_move.challenger;
            } else {
            game_to_move.board[m.mvt.row * 3 + m.mvt.column] = 2;
            game_to_move.turn = game_to_move.host;
            }
            // Update winner
            game_to_move.winner = get_winner(game_to_move);
            Games::update(game_to_move, game_to_move.host);
        }
        ...
    }
    
    

    验证操作

    验证游戏的操作意思是每一步都需要落在游戏板上的一个空格子里:

    namespace tic_tac_toe {
        ...
            bool is_empty_cell(const uint8_t& cell) {
                return cell == 0;
            }
            bool is_valid_movement(const movement& mvt, const game& game_for_movement) {
                uint32_t movement_location = mvt.row * 3 + mvt.column;
                bool is_valid = movement_location < game_for_movement.board_len && is_empty_cell(game_for_movement.board[movement_location]);
                return is_valid;
            }
        ...
    }
    

    判断赢家

    第一个把自己的三个标记在横向,纵向或对角线连线的玩家获胜。

    namespace tic_tac_toe {
        ...
        account_name get_winner(const game& current_game) {
            if((current_game.board[0] == current_game.board[4] && current_game.board[4] == current_game.board[8]) ||
            (current_game.board[1] == current_game.board[4] && current_game.board[4] == current_game.board[7]) ||
            (current_game.board[2] == current_game.board[4] && current_game.board[4] == current_game.board[6]) ||
            (current_game.board[3] == current_game.board[4] && current_game.board[4] == current_game.board[5])) {
                //  - | - | x    x | - | -    - | - | -    - | x | -
                //  - | x | -    - | x | -    x | x | x    - | x | -
                //  x | - | -    - | - | x    - | - | -    - | x | -
                if (current_game.board[4] == 1) {
                    return current_game.host;
                } else if (current_game.board[4] == 2) {
                    return current_game.challenger;
                }
            } else if ((current_game.board[0] == current_game.board[1] && current_game.board[1] == current_game.board[2]) ||
                    (current_game.board[0] == current_game.board[3] && current_game.board[3] == current_game.board[6])) {
                //  x | x | x       x | - | -
                //  - | - | -       x | - | -
                //  - | - | -       x | - | -
                if (current_game.board[0] == 1) {
                    return current_game.host;
                } else if (current_game.board[0] == 2) {
                    return current_game.challenger;
                }
            } else if ((current_game.board[2] == current_game.board[5] && current_game.board[5] == current_game.board[8]) ||
                    (current_game.board[6] == current_game.board[7] && current_game.board[7] == current_game.board[8])) {
                //  - | - | -       - | - | x
                //  - | - | -       - | - | x
                //  x | x | x       - | - | x
                if (current_game.board[8] == 1) {
                    return current_game.host;
                } else if (current_game.board[8] == 2) {
                    return current_game.challenger;
                }
            } else {
                bool is_board_full = true;
                for (uint8_t i = 0; i < current_game.board_len; i++) {
                    if (is_empty_cell(current_game.board[i])) {
                        is_board_full = false;
                        break;
                    }
                }
                if (is_board_full) {
                    return N(draw);
                }
            }
            return N(none);
        }
        ...
    }
    

    您可以在 这里 找到tic_tac_toe.cpp的完整代码

    创建 ABI

    有了Abi (即 Application Binary Interface),合约才能理解您所发的二进制信息。打开tic_tac_toe.abi并定义如下框架代码:

    {
      "structs": [{
          "name": "...",
          "base": "...",
          "fields": { ... }
      }, ...],
      "actions": [{
          "action_name": "...",
          "type": "..."
      }, ...],
      "tables": [{
          "table_name": "...",
          "type": "...",
          "key_names" : [...],
          "key_types" : [...]
      }, ...]
    
    • struct: 合约中action/ table所用到的数据结构列表
    • actions: 合约中可用的actions
    • tables: 合约中可用的表

    表 ABI

    在tic_tac_toe.hpp中,我们创造了一个叫game的single index i64的表。它保存了game 结构并使用challenger作为key(数据类型是account_name)。因此,abi文件是:

    {
        ...
        "structs": [{
          "name": "game",
          "base": "",
          "fields": {
            "challenger": "account_name",
            "host": "account_name",
            "turn": "account_name",
            "winner": "account_name",
            "board": "uint8[]"
          }
        }],
        "tables": [{
                "table_name": "games",
                "type": "game",
                "index_type": "i64",
                "key_names" : ["challenger"],
                "key_types" : ["account_name"]
            }
        ]
        ...
    }
    

    Actions ABI

    对actions来说,我们在actions里定义actions,在structs定义actions的数据结构。

    {
        ...
        "structs": [{
          "name": "create",
          "base": "",
          "fields": {
            "challenger": "account_name",
            "host": "account_name"
          }
        },{
          "name": "restart",
          "base": "",
          "fields": {
            "challenger": "account_name",
            "host": "account_name",
            "by": "account_name"
          }
        },{
          "name": "close",
          "base": "",
          "fields": {
            "challenger": "account_name",
            "host": "account_name"
          }
        },{
          "name": "movement",
          "base": "",
          "fields": {
            "row": "uint32",
            "column": "uint32"
          }
        },{
          "name": "move",
          "base": "",
          "fields": {
            "challenger": "account_name",
            "host": "account_name",
            "by": "account_name",
            "movement": "movement"
          }
        }],
      "actions": [{
          "action_name": "create",
          "type": "create"
        },{
          "action_name": "restart",
          "type": "restart"
        },{
          "action_name": "close",
          "type": "close"
        },{
          "action_name": "move",
          "type": "move"
        }
      ]
        ...
    }
    

    部署!

    现在所有文件(tic_tac_toe.hpp, tic_tac_toe.cpp, tic_tac_toe.abi)都完成了。可以部署了!

    $ eosc set contract tic.tac.toe tic_tac_toe.wast tic_tac_toe.abi
    

    注意您的钱包需要是解锁的,而tic.tac.toe密钥已导入。如果您要把该合约上传到其他账户,请用您的账户名替换tic.tac.toe并且确保您的钱包里有改账户的密钥。

    开玩!

    部署并且 transaction确认后,合约就在您的区块链上生效了。您现在就可以玩了。

    新建

    $ eosc push message tic.tac.toe create '{"challenger":"inita", "host":"initb"}' -S initb -S tic.tac.toe -p initb@active 
    

    移动

    $ eosc push message tic.tac.toe move '{"challenger":"inita", "host":"initb", "by":"initb", "movement":{"row":0, "column":0} }' -S initb -S tic.tac.toe -p initb@active 
    $ eosc push message tic.tac.toe move '{"challenger":"inita", "host":"initb", "by":"inita", "movement":{"row":1, "column":1} }' -S initb -S tic.tac.toe -p inita@active 
    

    重启

    $ eosc push message tic.tac.toe restart '{"challenger":"inita", "host":"initb", "by":"initb"}' -S initb -S tic.tac.toe -p initb@active 
    

    关闭

    $ eosc push message tic.tac.toe close '{"challenger":"inita", "host":"initb"}' -S initb -S tic.tac.toe -p initb@active
    

    查看游戏状态

    $ eosc get table initb tic.tac.toe games
    {
      "rows": [{
          "challenger": "inita",
          "host": "initb",
          "turn": "inita",
          "winner": "none",
          "board": [
            1,
            0,
            0,
            0,
            2,
            0,
            0,
            0,
            0
          ]
        }
      ],
      "more": false
    }
    

    相关文章

      网友评论

        本文标题:EOS 教程

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