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)行
eosc
和eos-walletd
。 - 命令行操作的基本知識(shí)疼邀。
注意: 當(dāng)使用docker安裝時(shí)喂江,命令可能需要稍作改動(dòng)。
1.1 創(chuàng)建并管理錢包
打開終端旁振,進(jìn)入EOS目錄
這會(huì)是我們更方便地操作eosc
获询,它是一個(gè)與eosd
和 eos-walletd
交互的命令行工具。
$ cd /path_to_eos/build/programs/eosc
首先您要用eosc
的wallet 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ì)有好幾種方法锨能,本教程主要講eosc
中create 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.wallet
和 periwinkle.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_1
和public_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è)入口步氏, init
和 apply
响禽。它所做的只是記錄提交的messages而并不作檢查。只要區(qū)塊生產(chǎn)者同意荚醒,任何人在任何時(shí)間都可以提交任何message芋类。但沒(méi)有所需的簽名,合約將因消耗帶寬被收費(fèi)界阁。
您可以將合約像這樣編譯成文本版本的WASM (.wast) :
$ eoscpp -o hello.wast hello.cpp
部署您的合約
現(xiàn)在您已經(jīng)編譯了您的應(yīng)用侯繁,我們可以部署了。這需要您先:
- 啟動(dòng) eosd 并打開錢包插件
- 新建錢包铺董,導(dǎo)入至少一個(gè)賬戶的密鑰
- 解鎖錢包
如果您的錢包里有${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
, to
和 quantity
等字段沾谜。這些字段都有對(duì)應(yīng)的類型:account_name
和uint64
。account_name
在types
列表中被定義為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板嗅义。玩家們有兩種角色host和challenger。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游戲季希,我們不用o
和 x
褪那,而用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的處理器,我們需要
- 確保message有host的簽名
- 確保同一個(gè)玩家并不在玩這盤游戲
- 確保該游戲不存在
- 把新建的游戲存入數(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 處理器牵囤,我們需要:
- 確保message有host或challenger的簽名
- 確保該游戲存在
- 確保重啟的action是host或challenger做出的
- 重啟游戲
- 將更新過(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 處理器爸黄,我們需要:
- 確保message有host的簽名
- 確保該游戲存在
- 將該游戲從數(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處理器滞伟,我們需要:
- 確保message有host或challenger的簽名
- 確保該游戲存在
- 確保該游戲并未結(jié)束
- 確保move的action是host或challenger做出的
- 確保輪到了正確的玩家行動(dòng)
- 驗(yàn)證這一步是有效的
- 用這一步升級(jí)游戲板
- 將move_turn分給另一個(gè)玩家
- 判斷贏家
- 把更新過(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
}