EOS 教程

1. 賬戶與錢包

注意 本教程是基于 測(cè)試私網(wǎng),但稍作修改就可以運(yùn)用在測(cè)試公網(wǎng)上帚桩。

您將學(xué)到

您將學(xué)到如何創(chuàng)建錢包亿驾、管理錢包及其keys并通過(guò)eosc使用錢包和區(qū)塊鏈交互。

本教程的目標(biāo)群體

本教程目標(biāo)群體是希望學(xué)習(xí)錢包和賬戶管理的人账嚎。我們將盡可能地介紹eosc以及EOS錢包和賬戶是如何交互的莫瞬。有一定基礎(chǔ)的用戶可查看參考命令儡蔓。

前提條件

  • 在您的系統(tǒng)上構(gòu)建并運(yùn)行eosceos-walletd
  • 命令行操作的基本知識(shí)疼邀。

注意: 當(dāng)使用docker安裝時(shí)喂江,命令可能需要稍作改動(dòng)。

1.1 創(chuàng)建并管理錢包

打開終端旁振,進(jìn)入EOS目錄

這會(huì)是我們更方便地操作eosc获询,它是一個(gè)與eosdeos-walletd交互的命令行工具。

$ cd /path_to_eos/build/programs/eosc

首先您要用eoscwallet create創(chuàng)造一個(gè)錢包

$ 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"

一個(gè)叫default的錢包現(xiàn)在已經(jīng)在eos-walletd里了拐袜,并且返回了一個(gè)該錢包的一個(gè)master password筐付。請(qǐng)將這個(gè)密碼安全地保存起來(lái)。這個(gè)密碼是用來(lái)解鎖(解密)您的錢包文件的阻肿。

該錢包文件叫做default.wallet瓦戚,被保存在了您的EOS目錄(您也可以在啟動(dòng)eos-walletd--data-dir制定特定目錄)下的data-dir文件夾里。

管理多個(gè)錢包和錢包名

eosc能夠管理多個(gè)錢包丛塌。每個(gè)錢包被各自的master password保護(hù)起來(lái)较解。下面的例子創(chuàng)建了另一個(gè)錢包并且展示了如何用 -n 參數(shù)給他命名

$ 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"

現(xiàn)在確認(rèn)一下錢包已經(jīng)用您指定的名字創(chuàng)建出來(lái)了。

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

每個(gè)錢包后面的星號(hào) (*) 很重要赴邻,他們表示錢包已解鎖印衔。方便起見(jiàn),我們用create wallet創(chuàng)建出來(lái)的錢包默認(rèn)是解鎖的姥敛。

wallet lock鎖住第二個(gè)錢包

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

再次運(yùn)行wallet list奸焙,您就可以看到第二個(gè)星號(hào)不見(jiàn)了,表示該錢包已上鎖彤敛。

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

解鎖一個(gè)有名字的錢包需要用wallet unlock命令并用-n參數(shù)指定錢包名与帆,然后輸入錢包的 master密碼(您可以粘貼密碼)。下面我們復(fù)制第二個(gè)錢包的master密碼墨榄,執(zhí)行此命令并粘貼密碼后回車玄糟。然后您需要確認(rèn)操作。

$ eosc wallet unlock -n periwinkle

eosc會(huì)告訴您錢包上鎖了

Unlocked: 'periwinkle'

注意: 您也可以用 --password 參數(shù)后跟master密碼袄秩,但是這會(huì)導(dǎo)致您的密碼在控制臺(tái)歷史當(dāng)中被明文地記錄下來(lái)阵翎。

現(xiàn)在查看一下錢包

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

好的,periwinkle錢包后面有星號(hào)之剧,表示它解鎖了郭卫。

注意: 使用'default'錢包不需要使用-n參數(shù)

現(xiàn)在重啟 eos-walletd,退回到您調(diào)用eosc的路徑下運(yùn)行以下命令

$ eosc wallet list
Wallets:
[]

有意思背稼,錢包去哪了呢贰军?

錢包需要被打開,因?yàn)槟P(guān)閉過(guò)eos-walletd雇庙,錢包并不在打開狀態(tài)谓形,運(yùn)行以下命令:

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

好多了。

注意: 如果您希望打開一個(gè)有名字的錢包疆前,您可以$ eosc wallet open -n periwinkle寒跳,學(xué)會(huì)了嗎? ;)

從上面的信息中您可以看到錢包是默認(rèn)鎖住的,把它解鎖才能進(jìn)行下面的操作竹椒。
執(zhí)行wallet unlock命令并在要求輸入密碼時(shí)粘貼上default 錢包的master密碼童太。

$ eosc wallet unlock
Unlocked: 'default'

然后檢查錢包是否已解鎖。

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

錢包名后面有星號(hào)胸完,已解鎖书释,非常好。

您已經(jīng)學(xué)會(huì)如何創(chuàng)建多個(gè)錢包及如何用eosc操作他們了赊窥。但空錢包沒(méi)什么意義爆惧,現(xiàn)在讓我們導(dǎo)入keys。

1.2 生成并導(dǎo)入EOS Keys

生成EOS key對(duì)有好幾種方法锨能,本教程主要講eosccreate key命令的方法伤为。

生成兩個(gè)密鑰對(duì)

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

現(xiàn)在您有兩個(gè)EOS 密鑰對(duì)了丰刊。此時(shí),他們只是最初始的密鑰對(duì),并沒(méi)有authority僚祷。

如果您一直根據(jù)上面來(lái)操作,您的default錢包應(yīng)該是打開且解鎖的佳遣。

下面奸鸯,我們執(zhí)行wallet import命令兩次,每次導(dǎo)入我們之前所生成的一個(gè)私鑰到您的 default錢包浸剩。

$ eosc wallet import ${private_key_1}

然后是第二個(gè)私鑰

$ eosc wallet import ${private_key_2}

如果順利钾军,每次wallet import命令都會(huì)返回您的私鑰對(duì)應(yīng)的公鑰,您的控制臺(tái)會(huì)是這樣的:

$ 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..."
  ]
]

錢包鎖起來(lái)的時(shí)候绢要,這些密鑰也會(huì)被保護(hù)起來(lái)巧颈。要從一個(gè)被鎖住的錢包中拿到密鑰需要有錢包創(chuàng)建時(shí)的master密碼。因?yàn)殄X包文件本身是加密的袖扛,備份密鑰對(duì)并不是一定要做的砸泛,但最好還是在一個(gè)安全的地方備份您的錢包文件。

1.3 備份錢包

現(xiàn)在您的錢包里已經(jīng)有密鑰對(duì)了蛆封,您最好養(yǎng)成備份但習(xí)慣唇礁,以防各種各樣的原因造成錢包丟失。比如使用u盤惨篱。沒(méi)有密碼盏筐,錢包是強(qiáng)熵加密的,想拿到里面的密鑰是非常難的 (基本不可能的)砸讳。

您可以在data-dir文件夾下找到您的錢包文件琢融。如果您在啟動(dòng)eos時(shí)用--data-dir參數(shù)指定過(guò)界牡,您可以在/path/to/eos/build/programs/eosd中找到(eos的具體路徑因系統(tǒng)不同而有不同)。

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

進(jìn)入文件夾后您將看到兩個(gè)文件:default.walletperiwinkle.wallet漾抬。把他們保存起來(lái)(熟能生巧K尥觥)。

1.4 創(chuàng)建賬戶

如果您用的是測(cè)試公網(wǎng)纳令,您需要有一個(gè)創(chuàng)世allocation或者從水龍頭賬戶申請(qǐng)一個(gè)賬戶挽荠。下面操作時(shí)請(qǐng)進(jìn)行適當(dāng)改動(dòng) (提示:應(yīng)當(dāng)用您自己的賬戶替換 inita 賬戶)

首先,我們看看 create account 命令及其必需參數(shù):

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

create account命令必需參數(shù)的解讀

  • inita 是執(zhí)行新建操作的賬戶名平绩。
  • desired_account_name 是您希望新建的賬戶名圈匆。
  • public_key_1public_key_2是公鑰,第一個(gè)是用于獲取您賬戶owner authority的, 第二個(gè)是用戶獲取active authority的捏雌。

您之前生成了兩個(gè)密鑰對(duì)跃赚,您可以翻看控制臺(tái)前面的記錄或者執(zhí)行wallet keys來(lái)查看。

$ 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密鑰等于對(duì)您賬戶的全面控制,而active密鑰等于對(duì)您賬戶資金的全面控制着裹。

用您之前所學(xué)的领猾,替換命令中的占位符然后回車:

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

您看到了一個(gè)提到"authorities"的報(bào)錯(cuò)了嗎?不用著急骇扇,我是故意讓您這么做的摔竿。您看到報(bào)錯(cuò)是因?yàn)槟鷽](méi)有加載@inita這個(gè)賬戶的密鑰。

inita 的密鑰存在 config.ini里少孝。但為方便起見(jiàn)继低,我將其復(fù)制了出來(lái)放在了下面。直接運(yùn)行下面的命令即可稍走。

$ eosc wallet import 5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3

將會(huì)返回

imported private key for: EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV

現(xiàn)在 @inita 賬戶的密鑰已經(jīng)加載袁翁,重新回到報(bào)錯(cuò)之前的create account 命令并回車。

順利的話 eosc 將返回一個(gè)含有transaction ID的JSON對(duì)象婿脸,類似于下面:

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

太好了粱胜!您現(xiàn)在已經(jīng)在區(qū)塊鏈上已經(jīng)有一個(gè)賬戶了。

您做的很棒狐树,您創(chuàng)建了一個(gè)錢包焙压,學(xué)習(xí)了一些錢包是如何工作、生成密鑰及如何把密鑰導(dǎo)入錢包的知識(shí)。

2. 貨幣合約概覽

目標(biāo)

下面的教程將幫助用戶了解github倉(cāng)庫(kù)中的樣例貨幣合約涯曲。

概覽

貨幣合約處理的是將貨幣從一個(gè)賬戶轉(zhuǎn)到另一個(gè)賬戶的工作野哭,而不同賬戶的余額保存在每個(gè)用戶的本地scope中。

Action

目前本合約只有一個(gè)action:
currency_transfer:將貨幣從一個(gè)賬戶轉(zhuǎn)到另一個(gè)賬戶幻件。

開始拨黔!

智能合約分為三個(gè)文件:

currency.hpp 合約中的聲明和數(shù)據(jù)結(jié)構(gòu)信息存在頭文件中
currency.cpp 合約的邏輯和實(shí)現(xiàn)
currency.abi 提供給用戶交互的接口定義

頭文件: currency.hpp

首先導(dǎo)入所需庫(kù)并定義您的命名空間

// 導(dǎo)入所需庫(kù)

#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
}

然后加入一個(gè)貨幣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的結(jié)構(gòu)如下所示:

struct transfer {
    account_name from;          //轉(zhuǎn)出賬戶
    account_name to;            //轉(zhuǎn)入賬戶
    currency_tokens quantity;   //轉(zhuǎn)賬金額
};

另外我們把余額信息存在表里傲武。表是如下定義的:

using accounts = eosio::table<N(defaultscope),N(currency),N(account),account,uint64_t>;
  • 第一個(gè)參數(shù)定義表的默認(rèn)scope蓉驹,比如當(dāng)有沒(méi)有指定scope的數(shù)據(jù)存入表中時(shí)城榛,它就會(huì)使用這個(gè)賬戶揪利。

  • 第二個(gè)參數(shù)定義表的所有者 (比如合約的名字)

  • 第三個(gè)參數(shù)定義表的名字

  • 第四個(gè)參數(shù)定義存儲(chǔ)數(shù)據(jù)結(jié)構(gòu)(將在后面定義)

  • 第五個(gè)參數(shù)定義表中key的類型

一旦表定義了,需要儲(chǔ)存的數(shù)據(jù)結(jié)構(gòu)(在我們的例子中是“賬戶”)也需要被定義狠持。這是在另一個(gè)struct中完成的:

struct account {
    //Constructor
    account( currency_tokens b = currency_tokens() ):balance(b){}

    //key是常量疟位,因?yàn)槊總€(gè)scope/currency/accounts只有一條記錄
    const uint64_t key = N(account);

    //賬戶的token數(shù)量
    currency_tokens balance;

    // 用于檢查賬戶是否為空的方法
    // 如果余額為0返回true
    bool is_empty()const { return balance.quantity == 0; }
};

這個(gè)結(jié)構(gòu)包含一個(gè)構(gòu)造器和一個(gè)用于判斷賬戶是否為空的標(biāo)準(zhǔn)函數(shù)。

需要注意的是喘垂,key的變量類型需要與之前在定義表時(shí) (第五個(gè)函數(shù))定義的類型一致甜刻。

為方便起見(jiàn),我們?cè)黾恿艘粋€(gè)存取器函數(shù)來(lái)獲取所有者的賬戶信息正勒,返回存在owner/TOKEN_NAME/account/account的信息得院。此函數(shù)存在頭文件中以提供第三方獲取用戶余額的能力。

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

注意: accounts:get函數(shù)返回賬戶所有者章贞。為應(yīng)對(duì)賬戶不存在的情況祥绞,它返回一個(gè)默認(rèn)結(jié)構(gòu)的賬戶。

源代碼文件: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"

所有的合約都有以上的骨架鸭限,每個(gè)合約都需要有以上的函數(shù):

Init() 在一個(gè)合約的生命周期開始時(shí)被調(diào)用一次蜕径。可用它來(lái)設(shè)置環(huán)境來(lái)讓合約正確運(yùn)行败京。

Apply( uint64_t code, uint64_t action_name) 被用作一個(gè)message的槽子兜喻。每次有message發(fā)給合約時(shí),此函數(shù)即開始調(diào)用赡麦。它的兩個(gè)參數(shù)含義如下:

  • code: 合約名稱
  • action_name: action名稱

在貨幣合約中朴皆,init() 函數(shù)如下所示:

void init() {
    account owned_account;

    //初始化貨幣賬戶,除非賬戶不存在
    if ( !accounts::get( owned_account, N(currency) )) {
        store_account( N(currency),
        account( currency_tokens(1000ll\*1000ll\*1000ll) ) );
    }
}

合約第一次運(yùn)行時(shí)泛粹,它會(huì)檢查currency賬戶是否有建立表且貨幣余額記錄在表中车荔。如果沒(méi)有建立表就會(huì)生成一個(gè)新表,余額為1000,000,000戚扳,這樣貨幣合約就成為了總量1000,000,000的貨幣單位的第一個(gè)所有者忧便。

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 >() );
    }
}

最好在上面的樣例代碼中實(shí)現(xiàn)一個(gè)message過(guò)濾器,使得合約只處理那些正確的messages并在過(guò)濾后調(diào)用message處理器。

注意 current_message() 會(huì)在message傳給特定處理器之前調(diào)用珠增,它是用來(lái)將合約收到的message轉(zhuǎn)為struct T的超歌。

Message處理器

實(shí)際上的貨幣轉(zhuǎn)賬是在這里操作的:

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 );
}

代碼非常直接,從轉(zhuǎn)出賬戶扣除轉(zhuǎn)賬金額并增加到轉(zhuǎn)入賬戶蒂教。

require_notice函數(shù)是一個(gè)inline action巍举,使得把收到的message轉(zhuǎn)到另一個(gè)賬戶成為可能。此例中message被轉(zhuǎn)發(fā)給了轉(zhuǎn)入賬戶和轉(zhuǎn)出賬戶凝垛。這是非常有用的功能懊悯,因?yàn)樗涯切氨煌ㄖ馁~戶”引入鏈上并發(fā)揮功能。
require_auth函數(shù)使得message被正確地簽名梦皮。在這個(gè)例子中炭分,轉(zhuǎn)出賬戶需要簽名,這個(gè)transaction才能被正確地處理剑肯。

注意我們正在使用頭文件里的get_account函數(shù)來(lái)獲得正確的賬戶對(duì)象捧毛。

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

最后通過(guò)store_account函數(shù)更新余額。

Store_account

這個(gè)函數(shù)是用來(lái)實(shí)際處理余額的儲(chǔ)存的:

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下創(chuàng)建的表)是空的呀忧,他就會(huì)被移除,這是因?yàn)橹灰绣X轉(zhuǎn)到不存在的賬戶里溃睹,表就會(huì)被新建出來(lái)而账。

移除不需要的表是一種節(jié)約資源的做法,是一種寫智能合約的最佳實(shí)踐因篇。

注意: 當(dāng)把上面的樣例代碼和倉(cāng)庫(kù)里的實(shí)際代碼比較時(shí)泞辐,請(qǐng)注意為了賬戶可以更簡(jiǎn)單的重命名,我們使用了TOKEN_NAME作為一種#define惜犀。上面的代碼中铛碑,我們用賬戶名替代了TOKEN_NAME以使得代碼更清晰。

ABI文件: currency.abi

Abi (即Application Binary Interface) 發(fā)送的message和二進(jìn)制版本的智能合約之間的接口虽界。我們先來(lái)看一個(gè)的通用版本汽烦,它包括如下對(duì)象:

  • struct: 合約中action/ table用到的數(shù)據(jù)結(jié)構(gòu)的列表

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

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

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

struct對(duì)象

根據(jù)合約中頭文件的信息,可以創(chuàng)建大多數(shù)ABI莉御。因此我們從數(shù)據(jù)結(jié)構(gòu)開始撇吞。頭文件中有兩個(gè)結(jié)構(gòu):

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; }
};

這些結(jié)構(gòu)就生成了如下ABI信息:

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

action對(duì)象

Action 對(duì)象也是類似的對(duì)應(yīng)。在這里我們?cè)谪泿藕霞s中有一個(gè)叫 “transfer” action礁叔‰咕保看起來(lái)和下面的ABI文件類似:

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

table對(duì)象

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

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

這張表就轉(zhuǎn)為下面的ABI對(duì)象:

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

這樣就組成了ABI文件琅关。

部署與運(yùn)行

現(xiàn)在三個(gè)文件 (currency.hpp, currency.cpp, currency.abi) 都可以通過(guò)命令行部署了:

$ eosc set contract currency currency.wast currency.abi

請(qǐng)確認(rèn)錢包已經(jīng)解鎖且含有 currency 的密鑰煮岁。部署后合約的action可以通過(guò)命令行這樣觸發(fā):

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

3. “Hello World”智能合約

為方便起見(jiàn),我們創(chuàng)造了一個(gè)叫eoscpp的工具來(lái)引導(dǎo)產(chǎn)生新的智能合約。您需要先安裝eosio/eos并把${CMAKE_INSTALL_PREFIX}/bin放入您的環(huán)境變量画机,它才能正常工作冶伞。

$ eoscpp -n hello
$ cd hello
$ ls

上面在'./hello'文件夾創(chuàng)建了一個(gè)新的空工程,里面有三個(gè)文件:

hello.abi hello.hpp hello.cpp

我們看一下最簡(jiǎn)單的合約:

$ 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"

這個(gè)合約實(shí)現(xiàn)了兩個(gè)入口步氏, initapply响禽。它所做的只是記錄提交的messages而并不作檢查。只要區(qū)塊生產(chǎn)者同意荚醒,任何人在任何時(shí)間都可以提交任何message芋类。但沒(méi)有所需的簽名,合約將因消耗帶寬被收費(fèi)界阁。

您可以將合約像這樣編譯成文本版本的WASM (.wast) :

$ eoscpp -o hello.wast hello.cpp

部署您的合約

現(xiàn)在您已經(jīng)編譯了您的應(yīng)用侯繁,我們可以部署了。這需要您先:

  1. 啟動(dòng) eosd 并打開錢包插件
  2. 新建錢包铺董,導(dǎo)入至少一個(gè)賬戶的密鑰
  3. 解鎖錢包

如果您的錢包里有${account}的密鑰且已經(jīng)解鎖巫击,您就可以用下面的命令把合約上傳到區(qū)塊鏈上

$ 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 進(jìn)程的輸出您將看到:

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

您可以看到"Init World!"被執(zhí)行了三次禀晓,這其實(shí)并不是個(gè)bug精续。區(qū)塊鏈處理transactions的流程是:

1: eosd收到一個(gè)新transaction (正在驗(yàn)證的transaction)

  • 創(chuàng)建一個(gè)新的臨時(shí)會(huì)話
  • 嘗試應(yīng)用此transaction
  • 成功并打印出"Init World!"
  • 失敗則回滾所做的變化 (也有可能打印"Init World!"后失敗)

2 : eosd開始產(chǎn)出區(qū)塊

  • 撤銷所有pending狀態(tài)
  • pushes all transactions as it builds the block
  • 第二次打印"Init World!"
  • 完成區(qū)塊
  • 撤銷所有創(chuàng)造區(qū)塊時(shí)的臨時(shí)變化

3rd : eosd如同從網(wǎng)絡(luò)上獲得區(qū)塊一樣將區(qū)塊追加到鏈上。

  • 第三次打印 "Init World!"

此時(shí)粹懒,您的合約就可以開始接受messages了重付。因?yàn)槟J(rèn)message處理器接受所有messages,我們可以發(fā)送任何我們想發(fā)的東西凫乖。我們?cè)囈幌掳l(fā)一個(gè)空的message:

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

此命令將"hello"message及16進(jìn)制字符串"abcd"所代表的二進(jìn)制文件傳出确垫。注意,后面我們將展示如何定義ABI來(lái)用一個(gè)好看易讀的JSON對(duì)象替換16進(jìn)制字符串帽芽。以上删掀,我們只是想證明“hello”類型的message是如何發(fā)送到賬戶的。

結(jié)果是:

{
  "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": []
      }
    ]
  }
}

如果您繼續(xù)查看eosd的輸出导街,您將在屏幕上看到:

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

再一次披泪,您的合約在transaction被第三次應(yīng)用并成為產(chǎn)出的區(qū)塊之前被執(zhí)行和撤銷了兩次。

Message名的限定

Message的類型實(shí)際上是base32編碼的64位整數(shù)搬瑰。所以Message名的前12個(gè)字符需限制在字母a-z, 1-5, 以及'.' 款票。第13個(gè)以后的字符限制在前16個(gè)字符('.' and a-p)。

ABI - Application Binary Interface

Application Binary Interface (ABI)是一個(gè)基于JSON的描述文件泽论,是關(guān)于轉(zhuǎn)換JSON和二進(jìn)制格式的用戶actions的艾少。ABI還描述了如何將數(shù)據(jù)庫(kù)狀態(tài)和JSON的互相轉(zhuǎn)換。一旦您通過(guò)ABI描述了您的合約翼悴,開發(fā)者和用戶就能夠用JSON和您的合約無(wú)縫交互了缚够。

我們正在開發(fā)使用C++源碼自動(dòng)生成ABI的工具,但目前為止您還是只能手動(dòng)生成。

這里是一個(gè)合約的骨架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"]
    }
  ]
}

您肯定注意到了這個(gè)ABI 定義了一個(gè)叫transfer的action谍椅,它的類型也是transfer陶冷。這就告訴EOS.IO當(dāng)${account}->transfer的message發(fā)生時(shí),它的payload是transfer類型的毯辅。 transfer類型是在structs的列表中定義的埂伦,其中有個(gè)對(duì)象,name屬性是transfer思恐。

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

這部分包括from, toquantity等字段沾谜。這些字段都有對(duì)應(yīng)的類型:account_nameuint64account_nametypes 列表中被定義為name的別名胀莹,而name是一個(gè)內(nèi)置類型基跑,用于用base32編碼uint64_t (比如賬戶名)。

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

在弄清骨架ABI后描焰,我們可以構(gòu)造一個(gè)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": []
      }
    ]
  }
}

如果您繼續(xù)觀察eosd的輸出媳否,您將看到:

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

處理轉(zhuǎn)賬Message的參數(shù)

根據(jù)ABI,transfer message應(yīng)該是如下格式的:

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

我們也知道account_name -> uint64表示這個(gè)message的二進(jìn)制表示如同:

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

EOS.IO的C API通過(guò)Message API提供獲取message的payload的能力:

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

讓我們修改hello.cpp來(lái)打印出消息內(nèi)容:

#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因?yàn)橹夭渴饘⒃俅握{(diào)用init()

Init World!
Init World!
Init World!

然后我們執(zhí)行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來(lái)讀取 Messages

目前我們使用是C API因?yàn)檫@是EOS.IO直接暴露給WASM虛擬機(jī)的最底層的API荆秦。幸運(yùn)的是篱竭,eoslib提供了一個(gè)更高級(jí)的API,移除了很多不必要的代碼步绸。

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

我們可以向下面一樣更新 hello.cpp 把它變得更簡(jiǎn)潔:

#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前后的類型檢查壓縮為一個(gè)單個(gè)的current-Message調(diào)用。

在編譯和上傳后瓤介,您將看到和C語(yǔ)言版本同樣的結(jié)果吕喘。

獲取發(fā)送者的Authority來(lái)進(jìn)行轉(zhuǎn)賬

合約最普遍的需求之一就是定義誰(shuí)可以進(jìn)行這樣的操作。比如在貨幣轉(zhuǎn)賬的例子里刑桑,我們就需要定義為from字段的賬戶核準(zhǔn)此message氯质。

EOS.IO軟件負(fù)責(zé)加強(qiáng)和驗(yàn)證簽名,您需要做的是獲取所需的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" );
       }
    }
    ...

建立和部署后闻察,我們可以再試一次轉(zhuǎ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:
...

這表示此操作嘗試請(qǐng)求應(yīng)用您的transaction梁肿,打印出了初始的"Hello World"蜓陌,然后當(dāng)eosio::require_auth沒(méi)能成功獲取initb賬戶的authorization后,操作終止了吩蔑。

我們可以通過(guò)讓eosc增加所需的permission來(lái)修復(fù)這個(gè)問(wèn)題:

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

--permission 命令定義了賬戶和permission等級(jí)钮热,此例中我們使用active authority,也就是默認(rèn)值烛芬。

這次轉(zhuǎn)賬應(yīng)該就成功了隧期,如同我們之前看到的一樣飒责。

Aborting a Message on Error

絕大多數(shù)合約開發(fā)中有非常多的前置條件,比如轉(zhuǎn)賬的金額要大于0仆潮。如果用戶嘗試進(jìn)行一個(gè)非法action宏蛉,合約必須終止且已做出的任何變動(dòng)都必須自動(dòng)回滾。

    ...
    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" );
       }
    }
    ...

我們編譯性置、部署并嘗試進(jìn)行一次金額為0的轉(zhuǎn)賬:

 $ 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

目標(biāo)

下面的教程將引導(dǎo)用戶構(gòu)建一個(gè)樣例的PvP的游戲合約拾并。我們用tic tac toe游戲來(lái)舉例。本教程的結(jié)果在 這里.

前提

在此游戲中鹏浅,我們用標(biāo)準(zhǔn)的3x3 tic tac toe板嗅义。玩家們有兩種角色hostchallenger。Host 永遠(yuǎn)是先手隐砸。每個(gè)玩家只能同時(shí)玩兩局比賽之碗,一局是第一個(gè)玩家是host另一局是第二個(gè)玩家是host。

游戲板

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

不同于傳統(tǒng)的tic tac toe游戲季希,我們不用ox 褪那,而用1 代表host的一步,2代表challenger的一步式塌,0代表空各自博敬。而且我們使用一維數(shù)組來(lái)保存游戲數(shù)據(jù)。因此:

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

假設(shè) x 珊搀,是host上面的游戲板可表示為[0, 2, 1, 0, 1, 0, 1, 2, 2]冶忱。

Action

用戶需要用下列actions來(lái)和合約交互:

  • create: 創(chuàng)建一個(gè)新游戲
  • restart: 重啟一個(gè)現(xiàn)有的游戲尾菇, host或challenger都可以這么做
  • close: 關(guān)閉一個(gè)現(xiàn)有的游戲境析,釋放存儲(chǔ)游戲的數(shù)據(jù),只有host可以這么做
  • move: 走一步

合約賬戶

在下面的教程中派诬,我們將把合約添加到一個(gè)叫tic.tac.toe的賬戶中劳淆。為防止tic.tac.toe的賬戶名被占用,您可以用其他的賬戶名默赂,只需要在代碼里面用您的賬戶名替換掉tic.tac.toe 沛鸵。如果您沒(méi)有賬戶,請(qǐng)先創(chuàng)建缆八。

$ 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

請(qǐng)先解鎖錢包并導(dǎo)入私鑰曲掰,否則上面的命令將失敗。

開始!

我們將創(chuàng)建三個(gè)文件:

  • tic_tac_toe.hpp => 定義合約結(jié)構(gòu)的頭文件
  • tic_tac_toe.cpp => 合約的主要邏輯
  • tic_tac_toe.abi => 用戶和合約交互的接口

定義結(jié)構(gòu)

讓我們先從定義合約結(jié)構(gòu)開始奈辰。打開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
}

游戲表

對(duì)于這個(gè)合約我們需要把游戲列表存在表中栏妖,我們來(lái)定義它:

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

NB: 如果您要把合約上傳到其他賬戶上,請(qǐng)用您的賬戶名替代tic.tac.toe奖恰。

第一個(gè)參數(shù)定義表的默認(rèn)scope吊趾,比如當(dāng)有沒(méi)有指定scope的數(shù)據(jù)存入表中時(shí)宛裕,它就會(huì)使用這個(gè)賬戶。

  • 第二個(gè)參數(shù)定義表的所有者 (比如合約的名字)

  • 第三個(gè)參數(shù)定義表的名字

  • 第四個(gè)參數(shù)定義存儲(chǔ)數(shù)據(jù)結(jié)構(gòu)(將在后面定義)

  • 第五個(gè)參數(shù)定義表中key的類型

游戲結(jié)構(gòu)

下面我們來(lái)定義游戲的結(jié)構(gòu)论泛。注意在代碼中揩尸,定義結(jié)構(gòu)需要在定義表之前。

...
namespace tic_tac_toe {
    struct PACKED(game) {
        // 默認(rèn) 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;
        // 輪到誰(shuí)走, = 可能是host或challenger的賬戶名
        account_name     turn; 
        // 贏家, = 空或平手或者是host或challenger的賬戶名
        account_name     winner = N(none); 
        // 游戲板列表的長(zhǎng)度岩榆,需放在游戲板列表的前面一個(gè)。有此字段abi序列化工具才能正確的可以打包寫入數(shù)據(jù)庫(kù)或從數(shù)據(jù)庫(kù)拆包數(shù)據(jù)
        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);
        }
    };
    ...
}

記住坟瓢,在前面表定義的時(shí)候朗恳,我們聲明表的key數(shù)據(jù)類型是uint64_t。因此载绿,在前面的游戲結(jié)構(gòu)中粥诫,結(jié)構(gòu)中前sizeof(uint64_t)字節(jié)長(zhǎng)度的數(shù)據(jù)將被當(dāng)成表的key。順便一提崭庸,account_name只是uint64_t的別名怀浆。

Action 結(jié)構(gòu)

Create

要新建游戲,我們需要 host 賬戶名和 challenger 賬戶名怕享。

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

要重啟游戲执赡,我們需要host 賬戶名和 challenger 賬戶名來(lái)找到該游戲。而且函筋,我們需要指定是誰(shuí)重啟了游戲沙合,這樣才能驗(yàn)證是否有有效的簽名。

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

要關(guān)閉游戲跌帐,我們需要host 賬戶名和 challenger 賬戶名來(lái)找到該游戲首懈。

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

要移動(dòng)一步,我們需要host 賬戶名和 challenger 賬戶名來(lái)找到該游戲谨敛。 而且究履,我們需要指定是誰(shuí)走的這一步以及這一步走在哪。

...
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合約僅響應(yīng)發(fā)給tic.tac.toe賬戶的message并且根據(jù)不同的action類型來(lái)給出不同響應(yīng)最仑。讓我們?cè)赼pply函數(shù)中加入message過(guò)濾器。

  ...
  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>());
            }
        }
  }
  ...

注意我們?cè)诎裮essage傳入特定處理器之前使用了current_message<T>()炊甲,它是將收到的message 轉(zhuǎn)為struct T的泥彤。

NB: 如果您正部署到另一個(gè)賬戶,請(qǐng)用您的賬戶名替換tic.tac.toe卿啡。

為了簡(jiǎn)潔起見(jiàn)吟吝,我們把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 處理器

對(duì)于create message的處理器,我們需要

  1. 確保message有host的簽名
  2. 確保同一個(gè)玩家并不在玩這盤游戲
  3. 確保該游戲不存在
  4. 把新建的游戲存入數(shù)據(jù)庫(kù)
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 處理器

對(duì)于 restart message 處理器牵囤,我們需要:

  1. 確保message有host或challenger的簽名
  2. 確保該游戲存在
  3. 確保重啟的action是host或challenger做出的
  4. 重啟游戲
  5. 將更新過(guò)的游戲存入數(shù)據(jù)庫(kù)
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 處理器

對(duì)于close message 處理器爸黄,我們需要:

  1. 確保message有host的簽名
  2. 確保該游戲存在
  3. 將該游戲從數(shù)據(jù)庫(kù)移除
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處理器

對(duì)于move message處理器滞伟,我們需要:

  1. 確保message有host或challenger的簽名
  2. 確保該游戲存在
  3. 確保該游戲并未結(jié)束
  4. 確保move的action是host或challenger做出的
  5. 確保輪到了正確的玩家行動(dòng)
  6. 驗(yàn)證這一步是有效的
  7. 用這一步升級(jí)游戲板
  8. 將move_turn分給另一個(gè)玩家
  9. 判斷贏家
  10. 把更新過(guò)的數(shù)據(jù)存入數(shù)據(jù)庫(kù)
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);
    }
    ...
}

驗(yàn)證操作

驗(yàn)證游戲的操作意思是每一步都需要落在游戲板上的一個(gè)空格子里:

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;
        }
    ...
}

判斷贏家

第一個(gè)把自己的三個(gè)標(biāo)記在橫向,縱向或?qū)蔷€連線的玩家獲勝炕贵。

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的完整代碼

創(chuàng)建 ABI

有了Abi (即 Application Binary Interface)梆奈,合約才能理解您所發(fā)的二進(jìn)制信息。打開tic_tac_toe.abi并定義如下框架代碼:

{
  "structs": [{
      "name": "...",
      "base": "...",
      "fields": { ... }
  }, ...],
  "actions": [{
      "action_name": "...",
      "type": "..."
  }, ...],
  "tables": [{
      "table_name": "...",
      "type": "...",
      "key_names" : [...],
      "key_types" : [...]
  }, ...]
  • struct: 合約中action/ table所用到的數(shù)據(jù)結(jié)構(gòu)列表
  • actions: 合約中可用的actions
  • tables: 合約中可用的表

表 ABI

在tic_tac_toe.hpp中称开,我們創(chuàng)造了一個(gè)叫g(shù)ame的single index i64的表亩钟。它保存了game 結(jié)構(gòu)并使用challenger作為key(數(shù)據(jù)類型是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

對(duì)actions來(lái)說(shuō)清酥,我們?cè)?code>actions里定義actions,在structs定義actions的數(shù)據(jù)結(jié)構(gòu)蕴侣。

{
    ...
    "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"
    }
  ]
    ...
}

部署焰轻!

現(xiàn)在所有文件(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密鑰已導(dǎo)入。如果您要把該合約上傳到其他賬戶狞膘,請(qǐng)用您的賬戶名替換tic.tac.toe并且確保您的錢包里有改賬戶的密鑰揩懒。

開玩!

部署并且 transaction確認(rèn)后挽封,合約就在您的區(qū)塊鏈上生效了已球。您現(xiàn)在就可以玩了。

新建

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

移動(dòng)

$ 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 

關(guān)閉

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

查看游戲狀態(tài)

$ 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
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末辅愿,一起剝皮案震驚了整個(gè)濱河市智亮,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌渠缕,老刑警劉巖鸽素,帶你破解...
    沈念sama閱讀 221,198評(píng)論 6 514
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異亦鳞,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)棒坏,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,334評(píng)論 3 398
  • 文/潘曉璐 我一進(jìn)店門燕差,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人坝冕,你說(shuō)我怎么就攤上這事徒探。” “怎么了喂窟?”我有些...
    開封第一講書人閱讀 167,643評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵测暗,是天一觀的道長(zhǎng)央串。 經(jīng)常有香客問(wèn)我,道長(zhǎng)碗啄,這世上最難降的妖魔是什么质和? 我笑而不...
    開封第一講書人閱讀 59,495評(píng)論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮稚字,結(jié)果婚禮上饲宿,老公的妹妹穿的比我還像新娘贩毕。我一直安慰自己良瞧,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,502評(píng)論 6 397
  • 文/花漫 我一把揭開白布靡菇。 她就那樣靜靜地躺著昌讲,像睡著了一般国夜。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上短绸,一...
    開封第一講書人閱讀 52,156評(píng)論 1 308
  • 那天支竹,我揣著相機(jī)與錄音,去河邊找鬼鸠按。 笑死礼搁,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的目尖。 我是一名探鬼主播馒吴,決...
    沈念sama閱讀 40,743評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼瑟曲!你這毒婦竟也來(lái)了饮戳?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,659評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤洞拨,失蹤者是張志新(化名)和其女友劉穎扯罐,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體烦衣,經(jīng)...
    沈念sama閱讀 46,200評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡歹河,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,282評(píng)論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了花吟。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片秸歧。...
    茶點(diǎn)故事閱讀 40,424評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖衅澈,靈堂內(nèi)的尸體忽然破棺而出键菱,到底是詐尸還是另有隱情,我是刑警寧澤今布,帶...
    沈念sama閱讀 36,107評(píng)論 5 349
  • 正文 年R本政府宣布经备,位于F島的核電站拭抬,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏侵蒙。R本人自食惡果不足惜造虎,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,789評(píng)論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望蘑志。 院中可真熱鬧累奈,春花似錦、人聲如沸急但。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,264評(píng)論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)波桩。三九已至戒努,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間镐躲,已是汗流浹背储玫。 一陣腳步聲響...
    開封第一講書人閱讀 33,390評(píng)論 1 271
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留萤皂,地道東北人撒穷。 一個(gè)月前我還...
    沈念sama閱讀 48,798評(píng)論 3 376
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像裆熙,于是被迫代替她去往敵國(guó)和親端礼。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,435評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容

  • 1 EOS智能合約的介紹1.1 所需背景知識(shí)1.2 EOS智能合約基礎(chǔ)知識(shí)1.3 技術(shù)局限性 2 智能合約文件2....
    cenkai88閱讀 30,511評(píng)論 5 28
  • 草案:2017 年 6 月 26 日 (@dayzh (https://steemit.com/@dayzh)) ...
    區(qū)塊鏈生存指南閱讀 2,033評(píng)論 0 4
  • 今天偶然間在58看到了剽悍一只貓的文章,里面有提到簡(jiǎn)書僚稿,突然想起很久以前就想下載這個(gè)軟件凡桥,但是腦子里被別的東西擠的...
    珊小珊1992閱讀 401評(píng)論 3 1
  • 1 我現(xiàn)在養(yǎng)成了與女兒每天交談的習(xí)慣缅刽。 今天女兒給我講了一個(gè)發(fā)生在他的班里的小事情。 她說(shuō)唤崭,她的一個(gè)同學(xué)中午吃飯的...
    唯川閱讀 548評(píng)論 4 6
  • 突然 上方的日期變成十一月了 不習(xí)慣的同時(shí) 竟也感慨 我撐過(guò)了九月 也這樣不知不覺(jué)地錯(cuò)過(guò)了十月 上完高數(shù)課 去了一...
    林有樸蔌閱讀 232評(píng)論 0 0