1. 賬戶與錢包
注意 本教程是基于 測試私網,但稍作修改就可以運用在測試公網上。
您將學到
您將學到如何創(chuàng)建錢包、管理錢包及其keys并通過eosc使用錢包和區(qū)塊鏈交互。
本教程的目標群體
本教程目標群體是希望學習錢包和賬戶管理的人。我們將盡可能地介紹eosc以及EOS錢包和賬戶是如何交互的。有一定基礎的用戶可查看參考命令。
前提條件
- 在您的系統(tǒng)上構建并運行
eosc和eos-walletd。 - 命令行操作的基本知識。
注意: 當使用docker安裝時,命令可能需要稍作改動。
1.1 創(chuàng)建并管理錢包
打開終端,進入EOS目錄
這會是我們更方便地操作eosc,它是一個與eosd 和 eos-walletd交互的命令行工具。
$ cd /path_to_eos/build/programs/eosc
首先您要用eosc的wallet create創(chuàng)造一個錢包
$ 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保護起來。下面的例子創(chuàng)建了另一個錢包并且展示了如何用 -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"
現在確認一下錢包已經用您指定的名字創(chuàng)建出來了。
$ eosc wallet list
Wallets:
[
"default *",
"periwinkle *"
]
每個錢包后面的星號 (*) 很重要,他們表示錢包已解鎖。方便起見,我們用create wallet創(chuàng)建出來的錢包默認是解鎖的。
用wallet lock鎖住第二個錢包
$ eosc wallet lock -n periwinkle
Locked: 'periwinkle'
再次運行wallet list,您就可以看到第二個星號不見了,表示該錢包已上鎖。
$ eosc wallet list
Wallets:
[
"default *",
"periwinkle"
]
解鎖一個有名字的錢包需要用wallet unlock命令并用-n參數指定錢包名,然后輸入錢包的 master密碼(您可以粘貼密碼)。下面我們復制第二個錢包的master密碼,執(zhí)行此命令并粘貼密碼后回車。然后您需要確認操作。
$ 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,錢包并不在打開狀態(tài),運行以下命令:
$ eosc wallet open
$ eosc wallet list
Wallets:
[
"default"
]
好多了。
注意: 如果您希望打開一個有名字的錢包,您可以$ eosc wallet open -n periwinkle,學會了嗎? ;)
從上面的信息中您可以看到錢包是默認鎖住的,把它解鎖才能進行下面的操作。
執(zhí)行wallet unlock命令并在要求輸入密碼時粘貼上default 錢包的master密碼。
$ eosc wallet unlock
Unlocked: 'default'
然后檢查錢包是否已解鎖。
$ eosc wallet list
Wallets:
[
"default *"
]
錢包名后面有星號,已解鎖,非常好。
您已經學會如何創(chuàng)建多個錢包及如何用eosc操作他們了。但空錢包沒什么意義,現在讓我們導入keys。
1.2 生成并導入EOS Keys
生成EOS key對有好幾種方法,本教程主要講eosc中create key命令的方法。
生成兩個密鑰對
$ eosc create key
Private key:###
Public key: ###
$ eosc create key
Private key:###
Public key: ###
現在您有兩個EOS 密鑰對了。此時,他們只是最初始的密鑰對,并沒有authority。
如果您一直根據上面來操作,您的default錢包應該是打開且解鎖的。
下面,我們執(zhí)行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..."
]
]
錢包鎖起來的時候,這些密鑰也會被保護起來。要從一個被鎖住的錢包中拿到密鑰需要有錢包創(chuàng)建時的master密碼。因為錢包文件本身是加密的,備份密鑰對并不是一定要做的,但最好還是在一個安全的地方備份您的錢包文件。
1.3 備份錢包
現在您的錢包里已經有密鑰對了,您最好養(yǎng)成備份但習慣,以防各種各樣的原因造成錢包丟失。比如使用u盤。沒有密碼,錢包是強熵加密的,想拿到里面的密鑰是非常難的 (基本不可能的)。
您可以在data-dir文件夾下找到您的錢包文件。如果您在啟動eos時用--data-dir參數指定過,您可以在/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
進入文件夾后您將看到兩個文件:default.wallet 和 periwinkle.wallet。把他們保存起來(熟能生巧?。?。
1.4 創(chuàng)建賬戶
如果您用的是測試公網,您需要有一個創(chuàng)世allocation或者從水龍頭賬戶申請一個賬戶。下面操作時請進行適當改動 (提示:應當用您自己的賬戶替換 inita 賬戶)
首先,我們看看 create account 命令及其必需參數:
$ eosc create account inita ${desired_account_name} ${public_key_1} ${public_key_2}
create account命令必需參數的解讀
-
inita是執(zhí)行新建操作的賬戶名。 -
desired_account_name是您希望新建的賬戶名。 -
public_key_1和public_key_2是公鑰,第一個是用于獲取您賬戶owner authority的, 第二個是用戶獲取active authority的。
您之前生成了兩個密鑰對,您可以翻看控制臺前面的記錄或者執(zhí)行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"...
太好了!您現在已經在區(qū)塊鏈上已經有一個賬戶了。
您做的很棒,您創(chuàng)建了一個錢包,學習了一些錢包是如何工作、生成密鑰及如何把密鑰導入錢包的知識。
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() 在一個合約的生命周期開始時被調用一次。可用它來設置環(huán)境來讓合約正確運行。
Apply( uint64_t code, uint64_t action_name) 被用作一個message的槽子。每次有message發(fā)給合約時,此函數即開始調用。它的兩個參數含義如下:
- 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被轉發(fā)給了轉入賬戶和轉出賬戶。這是非常有用的功能,因為它把那些“被通知的賬戶”引入鏈上并發(fā)揮功能。
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下創(chuàng)建的表)是空的,他就會被移除,這是因為只要有錢轉到不存在的賬戶里,表就會被新建出來。
移除不需要的表是一種節(jié)約資源的做法,是一種寫智能合約的最佳實踐。
注意: 當把上面的樣例代碼和倉庫里的實際代碼比較時,請注意為了賬戶可以更簡單的重命名,我們使用了TOKEN_NAME作為一種#define。上面的代碼中,我們用賬戶名替代了TOKEN_NAME以使得代碼更清晰。
ABI文件: currency.abi
Abi (即Application Binary Interface) 發(fā)送的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對象
根據合約中頭文件的信息,可以創(chuàng)建大多數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??雌饋砗拖旅娴腁BI文件類似:
"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可以通過命令行這樣觸發(fā):
$ eosc push message currency transfer ‘{“from”:“currency”,“to”:“tester”,“quantity”:50}’ -S currency -S tester -p currency@active
3. “Hello World”智能合約
為方便起見,我們創(chuàng)造了一個叫eoscpp的工具來引導產生新的智能合約。您需要先安裝eosio/eos并把${CMAKE_INSTALL_PREFIX}/bin放入您的環(huán)境變量,它才能正常工作。
$ eoscpp -n hello
$ cd hello
$ ls
上面在'./hello'文件夾創(chuàng)建了一個新的空工程,里面有三個文件:
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"
這個合約實現了兩個入口, init 和 apply。它所做的只是記錄提交的messages而并不作檢查。只要區(qū)塊生產者同意,任何人在任何時間都可以提交任何message。但沒有所需的簽名,合約將因消耗帶寬被收費。
您可以將合約像這樣編譯成文本版本的WASM (.wast) :
$ eoscpp -o hello.wast hello.cpp
部署您的合約
現在您已經編譯了您的應用,我們可以部署了。這需要您先:
- 啟動 eosd 并打開錢包插件
- 新建錢包,導入至少一個賬戶的密鑰
- 解鎖錢包
如果您的錢包里有${account}的密鑰且已經解鎖,您就可以用下面的命令把合約上傳到區(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 進程的輸出您將看到:
...] initt generated block #188249 @ 2017-09-13T22:00:24 with 0 trxs 0 pending
Init World!
Init World!
Init World!
您可以看到"Init World!"被執(zhí)行了三次,這其實并不是個bug。區(qū)塊鏈處理transactions的流程是:
1: eosd收到一個新transaction (正在驗證的transaction)
- 創(chuàng)建一個新的臨時會話
- 嘗試應用此transaction
- 成功并打印出"Init World!"
- 失敗則回滾所做的變化 (也有可能打印"Init World!"后失敗)
2 : eosd開始產出區(qū)塊
- 撤銷所有pending狀態(tài)
- pushes all transactions as it builds the block
- 第二次打印"Init World!"
- 完成區(qū)塊
- 撤銷所有創(chuàng)造區(qū)塊時的臨時變化
3rd : eosd如同從網絡上獲得區(qū)塊一樣將區(qū)塊追加到鏈上。
- 第三次打印 "Init World!"
此時,您的合約就可以開始接受messages了。因為默認message處理器接受所有messages,我們可以發(fā)送任何我們想發(fā)的東西。我們試一下發(fā)一個空的message:
$ eosc push message ${account} hello '"abcd"' --scope ${account}
此命令將"hello"message及16進制字符串"abcd"所代表的二進制文件傳出。注意,后面我們將展示如何定義ABI來用一個好看易讀的JSON對象替換16進制字符串。以上,我們只是想證明“hello”類型的message是如何發(fā)送到賬戶的。
結果是:
{
"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被第三次應用并成為產出的區(qū)塊之前被執(zhí)行和撤銷了兩次。
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還描述了如何將數據庫狀態(tài)和JSON的互相轉換。一旦您通過ABI描述了您的合約,開發(fā)者和用戶就能夠用JSON和您的合約無縫交互了。
我們正在開發(fā)使用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發(fā)生時,它的payload是transfer類型的。 transfer類型是在structs的列表中定義的,其中有個對象,name屬性是transfer。
...
"structs": [{
"name": "transfer",
"base": "",
"fields": {
"from": "account_name",
"to": "account_name",
"quantity": "uint64"
}
},{
...
這部分包括from, to 和 quantity等字段。這些字段都有對應的類型:account_name和uint64。account_name在types 列表中被定義為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": []
}
]
}
}
如果您繼續(xù)觀察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!
然后我們執(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來讀取 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語言版本同樣的結果。
獲取發(fā)送者的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
絕大多數合約開發(fā)中有非常多的前置條件,比如轉賬的金額要大于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板。玩家們有兩種角色host和challenger。Host 永遠是先手。每個玩家只能同時玩兩局比賽,一局是第一個玩家是host另一局是第二個玩家是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代表空各自。而且我們使用一維數組來保存游戲數據。因此:
| (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: 創(chuàng)建一個新游戲
- restart: 重啟一個現有的游戲, host或challenger都可以這么做
- close: 關閉一個現有的游戲,釋放存儲游戲的數據,只有host可以這么做
- move: 走一步
合約賬戶
在下面的教程中,我們將把合約添加到一個叫tic.tac.toe的賬戶中。為防止tic.tac.toe的賬戶名被占用,您可以用其他的賬戶名,只需要在代碼里面用您的賬戶名替換掉tic.tac.toe 。如果您沒有賬戶,請先創(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
請先解鎖錢包并導入私鑰,否則上面的命令將失敗。
開始!
我們將創(chuàng)建三個文件:
- 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)字節(jié)長度的數據將被當成表的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合約僅響應發(fā)給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的處理器,我們需要
- 確保message有host的簽名
- 確保同一個玩家并不在玩這盤游戲
- 確保該游戲不存在
- 把新建的游戲存入數據庫
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 處理器,我們需要:
- 確保message有host或challenger的簽名
- 確保該游戲存在
- 確保重啟的action是host或challenger做出的
- 重啟游戲
- 將更新過的游戲存入數據庫
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 處理器,我們需要:
- 確保message有host的簽名
- 確保該游戲存在
- 將該游戲從數據庫移除
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處理器,我們需要:
- 確保message有host或challenger的簽名
- 確保該游戲存在
- 確保該游戲并未結束
- 確保move的action是host或challenger做出的
- 確保輪到了正確的玩家行動
- 驗證這一步是有效的
- 用這一步升級游戲板
- 將move_turn分給另一個玩家
- 判斷贏家
- 把更新過的數據存入數據庫
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的完整代碼
創(chuàng)建 ABI
有了Abi (即 Application Binary Interface),合約才能理解您所發(fā)的二進制信息。打開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中,我們創(chuàng)造了一個叫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確認后,合約就在您的區(qū)塊鏈上生效了。您現在就可以玩了。
新建
$ 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
查看游戲狀態(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
}