EOS 教程
1. 账户与钱包
注意 本教程是基于 测试私网,但稍作修改就可以运用在测试公网上。
您将学到
您将学到如何创建钱包、管理钱包及其keys并通过eosc
使用钱包和区块链交互。
本教程的目标群体
本教程目标群体是希望学习钱包和账户管理的人。我们将尽可能地介绍eosc
以及EOS钱包和账户是如何交互的。有一定基础的用户可查看参考命令。
前提条件
- 在您的系统上构建并运行
eosc
和eos-walletd
。 - 命令行操作的基本知识。
注意: 当使用docker安装时,命令可能需要稍作改动。
1.1 创建并管理钱包
打开终端,进入EOS目录
这会是我们更方便地操作eosc
,它是一个与eosd
和 eos-walletd
交互的命令行工具。
$ cd /path_to_eos/build/programs/eosc
首先您要用eosc
的wallet create
创造一个钱包
$ eosc wallet create
Creating wallet: default
Save password to use in the future to unlock this wallet.
Without password imported keys will not be retrievable.
"A MASTER PASSWORD"
一个叫default的钱包现在已经在eos-walletd
里了,并且返回了一个该钱包的一个master password。请将这个密码安全地保存起来。这个密码是用来解锁(解密)您的钱包文件的。
该钱包文件叫做default.wallet
,被保存在了您的EOS目录(您也可以在启动eos-walletd
用--data-dir
制定特定目录)下的data-dir
文件夹里。
管理多个钱包和钱包名
eosc
能够管理多个钱包。每个钱包被各自的master password保护起来。下面的例子创建了另一个钱包并且展示了如何用 -n
参数给他命名
$ eosc wallet create -n periwinkle
Creating wallet: periwinkle
Save password to use in the future to unlock this wallet.
Without password imported keys will not be retrievable.
"A MASTER PASSWORD"
现在确认一下钱包已经用您指定的名字创建出来了。
$ eosc wallet list
Wallets:
[
"default *",
"periwinkle *"
]
每个钱包后面的星号 (*) 很重要,他们表示钱包已解锁。方便起见,我们用create wallet
创建出来的钱包默认是解锁的。
用wallet lock
锁住第二个钱包
$ eosc wallet lock -n periwinkle
Locked: 'periwinkle'
再次运行wallet list
,您就可以看到第二个星号不见了,表示该钱包已上锁。
$ eosc wallet list
Wallets:
[
"default *",
"periwinkle"
]
解锁一个有名字的钱包需要用wallet unlock
命令并用-n
参数指定钱包名,然后输入钱包的 master密码(您可以粘贴密码)。下面我们复制第二个钱包的master密码,执行此命令并粘贴密码后回车。然后您需要确认操作。
$ eosc wallet unlock -n periwinkle
eosc会告诉您钱包上锁了
Unlocked: 'periwinkle'
注意: 您也可以用 --password
参数后跟master密码,但是这会导致您的密码在控制台历史当中被明文地记录下来。
现在查看一下钱包
$ eosc wallet list
Wallets:
[
"default *",
"periwinkle *"
]
好的,periwinkle钱包后面有星号,表示它解锁了。
注意: 使用'default'钱包不需要使用-n
参数
现在重启 eos-walletd
,退回到您调用eosc
的路径下运行以下命令
$ eosc wallet list
Wallets:
[]
有意思,钱包去哪了呢?
钱包需要被打开,因为您关闭过eos-walletd
,钱包并不在打开状态,运行以下命令:
$ eosc wallet open
$ eosc wallet list
Wallets:
[
"default"
]
好多了。
注意: 如果您希望打开一个有名字的钱包,您可以$ eosc wallet open -n periwinkle
,学会了吗? ;)
从上面的信息中您可以看到钱包是默认锁住的,把它解锁才能进行下面的操作。
执行wallet unlock
命令并在要求输入密码时粘贴上default 钱包的master密码。
$ eosc wallet unlock
Unlocked: 'default'
然后检查钱包是否已解锁。
$ eosc wallet list
Wallets:
[
"default *"
]
钱包名后面有星号,已解锁,非常好。
您已经学会如何创建多个钱包及如何用eosc
操作他们了。但空钱包没什么意义,现在让我们导入keys。
1.2 生成并导入EOS Keys
生成EOS key对有好几种方法,本教程主要讲eosc
中create key
命令的方法。
生成两个密钥对
$ eosc create key
Private key:###
Public key: ###
$ eosc create key
Private key:###
Public key: ###
现在您有两个EOS 密钥对了。此时,他们只是最初始的密钥对,并没有authority。
如果您一直根据上面来操作,您的default钱包应该是打开且解锁的。
下面,我们执行wallet import
命令两次,每次导入我们之前所生成的一个私钥到您的 default
钱包。
$ eosc wallet import ${private_key_1}
然后是第二个私钥
$ eosc wallet import ${private_key_2}
如果顺利,每次wallet import
命令都会返回您的私钥对应的公钥,您的控制台会是这样的:
$ eosc wallet import ${private_key_1}
imported private key for: ${public_key_1}
$ eosc wallet import ${private_key_2}
imported private key for: ${public_key_2}
我们用wallet keys
看看加载了哪些密钥
$ eosc wallet keys
[[
"EOS6....",
"5KQwr..."
],
[
"EOS3....",
"5Ks0e..."
]
]
钱包锁起来的时候,这些密钥也会被保护起来。要从一个被锁住的钱包中拿到密钥需要有钱包创建时的master密码。因为钱包文件本身是加密的,备份密钥对并不是一定要做的,但最好还是在一个安全的地方备份您的钱包文件。
1.3 备份钱包
现在您的钱包里已经有密钥对了,您最好养成备份但习惯,以防各种各样的原因造成钱包丢失。比如使用u盘。没有密码,钱包是强熵加密的,想拿到里面的密钥是非常难的 (基本不可能的)。
您可以在data-dir
文件夹下找到您的钱包文件。如果您在启动eos时用--data-dir
参数指定过,您可以在/path/to/eos/build/programs/eosd
中找到(eos的具体路径因系统不同而有不同)。
$ cd /path_to_eos/build/programs/eosd && ls
blockchain blocks config.ini default.wallet periwinkle.wallet
进入文件夹后您将看到两个文件:default.wallet
和 periwinkle.wallet
。把他们保存起来(熟能生巧!)。
1.4 创建账户
如果您用的是测试公网,您需要有一个创世allocation或者从水龙头账户申请一个账户。下面操作时请进行适当改动 (提示:应当用您自己的账户替换 inita 账户)
首先,我们看看 create account
命令及其必需参数:
$ eosc create account inita ${desired_account_name} ${public_key_1} ${public_key_2}
create account
命令必需参数的解读
-
inita
是执行新建操作的账户名。 -
desired_account_name
是您希望新建的账户名。 -
public_key_1
和public_key_2
是公钥,第一个是用于获取您账户owner authority的, 第二个是用户获取active authority的。
您之前生成了两个密钥对,您可以翻看控制台前面的记录或者执行wallet keys
来查看。
$ eosc wallet keys
[[
"EOS6....",
"5KQwr..."
],
[
"EOS3....",
"5Ks0e..."
]
]
提醒一下,公钥是以EOS...
开头。在您给密钥分配authority前,上面的密钥都是初始的。which one you decide to user for active and owner are inconsequential until you have created your account.
注意, 您的owner密钥等于对您账户的全面控制,而active密钥等于对您账户资金的全面控制。
用您之前所学的,替换命令中的占位符然后回车:
$ eosc create account inita ${desired_account_name} ${public_key_1} ${public_key_2}
您看到了一个提到"authorities"的报错了吗?不用着急,我是故意让您这么做的。您看到报错是因为您没有加载@inita这个账户的密钥。
inita 的密钥存在 config.ini
里。但为方便起见,我将其复制了出来放在了下面。直接运行下面的命令即可。
$ eosc wallet import 5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3
将会返回
imported private key for: EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV
现在 @inita 账户的密钥已经加载,重新回到报错之前的create account
命令并回车。
顺利的话 eosc
将返回一个含有transaction ID的JSON对象,类似于下面:
{
"transaction_id": "6acd2ece68c4b86c1fa209c3989235063384020781f2c67bbb80bc8d540ca120",
"processed": {
"refBlockNum": "25217",
"refBlockPrefix": "2095475630",
"expiration": "2017-07-25T17:54:55",
"scope": [
"eos"...
太好了!您现在已经在区块链上已经有一个账户了。
您做的很棒,您创建了一个钱包,学习了一些钱包是如何工作、生成密钥及如何把密钥导入钱包的知识。
2. 货币合约概览
目标
下面的教程将帮助用户了解github仓库中的样例货币合约。
概览
货币合约处理的是将货币从一个账户转到另一个账户的工作,而不同账户的余额保存在每个用户的本地scope中。
Action
目前本合约只有一个action:
currency_transfer:将货币从一个账户转到另一个账户。
开始!
智能合约分为三个文件:
currency.hpp | 合约中的声明和数据结构信息存在头文件中 |
---|---|
currency.cpp | 合约的逻辑和实现 |
currency.abi | 提供给用户交互的接口定义 |
头文件: currency.hpp
首先导入所需库并定义您的命名空间
// 导入所需库
#include <eoslib/eos.hpp> // Generic eos library, i.e. print, type, math, etc
#include <eoslib/token.hpp> // Token usage
#include <eoslib/db.hpp> // Database access
namespace currency {
// Your code here
}
然后加入一个货币token。 It’s in fact a uin64_t wrapper which checks for proper types and under/overflows for standard-compatible token messages
typedef eosio::token<uint64_t,N(currency)> currency_tokens;
我们action的结构如下所示:
struct transfer {
account_name from; //转出账户
account_name to; //转入账户
currency_tokens quantity; //转账金额
};
另外我们把余额信息存在表里。表是如下定义的:
using accounts = eosio::table<N(defaultscope),N(currency),N(account),account,uint64_t>;
-
第一个参数定义表的默认scope,比如当有没有指定scope的数据存入表中时,它就会使用这个账户。
-
第二个参数定义表的所有者 (比如合约的名字)
-
第三个参数定义表的名字
-
第四个参数定义存储数据结构(将在后面定义)
-
第五个参数定义表中key的类型
一旦表定义了,需要储存的数据结构(在我们的例子中是“账户”)也需要被定义。这是在另一个struct中完成的:
struct account {
//Constructor
account( currency_tokens b = currency_tokens() ):balance(b){}
//key是常量,因为每个scope/currency/accounts只有一条记录
const uint64_t key = N(account);
//账户的token数量
currency_tokens balance;
// 用于检查账户是否为空的方法
// 如果余额为0返回true
bool is_empty()const { return balance.quantity == 0; }
};
这个结构包含一个构造器和一个用于判断账户是否为空的标准函数。
需要注意的是,key的变量类型需要与之前在定义表时 (第五个函数)定义的类型一致。
为方便起见,我们增加了一个存取器函数来获取所有者的账户信息,返回存在owner/TOKEN_NAME/account/account的信息。此函数存在头文件中以提供第三方获取用户余额的能力。
inline account get_account( account_name owner ) {
account owned_account;
accounts::get( owned_account, owner );
return owned_account;
}
注意: accounts:get函数返回账户所有者。为应对账户不存在的情况,它返回一个默认结构的账户。
源代码文件:currency.cpp
#include <currency/currency.hpp>
// The init() and apply() methods must have C calling convention
extern "C" {
// Only called once
void init() {
}
// The apply method implements the dispatch of events to this contract
void apply( uint64_t code, uint64_t action_name ) {
// Put your message handler here
}
} // extern "C"
所有的合约都有以上的骨架,每个合约都需要有以上的函数:
Init() 在一个合约的生命周期开始时被调用一次。可用它来设置环境来让合约正确运行。
Apply( uint64_t code, uint64_t action_name) 被用作一个message的槽子。每次有message发给合约时,此函数即开始调用。它的两个参数含义如下:
- code: 合约名称
- action_name: action名称
在货币合约中,init() 函数如下所示:
void init() {
account owned_account;
//初始化货币账户,除非账户不存在
if ( !accounts::get( owned_account, N(currency) )) {
store_account( N(currency),
account( currency_tokens(1000ll\*1000ll\*1000ll) ) );
}
}
合约第一次运行时,它会检查currency账户是否有建立表且货币余额记录在表中。如果没有建立表就会生成一个新表,余额为1000,000,000,这样货币合约就成为了总量1000,000,000的货币单位的第一个所有者。
message槽如下所示:
void apply( uint64_t code, uint64_t action ) {
if( code == N(currency) ) {
if( action == N(transfer) )
account::apply_currency_transfer( current_message<account::transfer >() );
}
}
最好在上面的样例代码中实现一个message过滤器,使得合约只处理那些正确的messages并在过滤后调用message处理器。
注意 current_message() 会在message传给特定处理器之前调用,它是用来将合约收到的message转为struct T的。
Message处理器
实际上的货币转账是在这里操作的:
void apply_currency_transfer( const account::transfer& transfer_msg )
{
require_notice( transfer_msg.to, transfer_msg.from );
require_auth( transfer_msg.from );
auto from = get_account( transfer_msg.from );
auto to = get_account( transfer_msg.to );
from.balance -= transfer_msg.quantity;
to.balance += transfer_msg.quantity;
store_account( transfer_msg.from, from );
store_account( transfer_msg.to, to );
}
代码非常直接,从转出账户扣除转账金额并增加到转入账户。
require_notice函数是一个inline action,使得把收到的message转到另一个账户成为可能。此例中message被转发给了转入账户和转出账户。这是非常有用的功能,因为它把那些“被通知的账户”引入链上并发挥功能。
require_auth函数使得message被正确地签名。在这个例子中,转出账户需要签名,这个transaction才能被正确地处理。
注意我们正在使用头文件里的get_account函数来获得正确的账户对象。
Since we are using tokens, automatic over and underflow assertions are being backed into the actual subtraction and addition operations.
最后通过store_account函数更新余额。
Store_account
这个函数是用来实际处理余额的储存的:
void store_account( account_name current_account, const account& value ) {
if( a.is_empty() ) {
accounts::remove( value, current_account);
} else {
accounts::store( value, current_account);
}
}
有趣的是,如果账户(也就是在current_account的scope下创建的表)是空的,他就会被移除,这是因为只要有钱转到不存在的账户里,表就会被新建出来。
移除不需要的表是一种节约资源的做法,是一种写智能合约的最佳实践。
注意: 当把上面的样例代码和仓库里的实际代码比较时,请注意为了账户可以更简单的重命名,我们使用了TOKEN_NAME作为一种#define。上面的代码中,我们用账户名替代了TOKEN_NAME以使得代码更清晰。
ABI文件: currency.abi
Abi (即Application Binary Interface) 发送的message和二进制版本的智能合约之间的接口。我们先来看一个的通用版本,它包括如下对象:
-
struct: 合约中action/ table用到的数据结构的列表
-
actions: 合约中可用的actions的列表
-
tables: 合约中可用的tables的列表
{
"structs": \[{
"name": "...",
"base": "...",
"fields": { ... }
}, ...\],
"actions": \[{
"action_name": "...",
"type": "..."
}, ...\],
"tables": \[{
"table_name": "...",
"type": "...",
"key_names" : \[...\],
"key_types" : \[...\]
}, ...\]
}
struct对象
根据合约中头文件的信息,可以创建大多数ABI。因此我们从数据结构开始。头文件中有两个结构:
struct transfer {
account_name from;
account_name to;
currency_tokens quantity;
};
struct account {
account( currency_tokens b = currency_tokens() ):balance(b){}
const uint64_t key = N(account);
currency_tokens balance;
bool is_empty()const { return balance.quantity == 0; }
};
这些结构就生成了如下ABI信息:
"structs": \[{
"name": "transfer",
"base": "",
"fields": {
"from": "account_name",
"to": "account_name",
"quantity": "uint64"
}
},{
"name": "account",
"base": "",
"fields": {
"key": "name",
"balance": "uint64"
}
}\]
action对象
Action 对象也是类似的对应。在这里我们在货币合约中有一个叫 “transfer” action。看起来和下面的ABI文件类似:
"actions": \[{
"action_name": "transfer",
"type": "transfer"
}\]
table对象
头文件中, a single index called “account” table定义如下:
eosio::table<N(defaultscope),N(currency),N(account),account,uint64_t>;
这张表就转为下面的ABI对象:
"tables": \[{
"table_name": "account",
"type": "account",
"index_type": "i64",
"key_names" : \["key"\],
"key_types" : \["name"\]
}\]
这样就组成了ABI文件。
部署与运行
现在三个文件 (currency.hpp, currency.cpp, currency.abi) 都可以通过命令行部署了:
$ eosc set contract currency currency.wast currency.abi
请确认钱包已经解锁且含有 currency 的密钥。部署后合约的action可以通过命令行这样触发:
$ eosc push message currency transfer ‘{“from”:“currency”,“to”:“tester”,“quantity”:50}’ -S currency -S tester -p currency@active
3. “Hello World”智能合约
为方便起见,我们创造了一个叫eoscpp
的工具来引导产生新的智能合约。您需要先安装eosio/eos并把${CMAKE_INSTALL_PREFIX}/bin放入您的环境变量,它才能正常工作。
$ eoscpp -n hello
$ cd hello
$ ls
上面在'./hello'文件夹创建了一个新的空工程,里面有三个文件:
hello.abi hello.hpp hello.cpp
我们看一下最简单的合约:
$ cat hello.cpp
#include <hello.hpp>
/**
* The init() and apply() methods must have C calling convention so that the blockchain can lookup and
* call these methods.
*/
extern "C" {
/**
* This method is called once when the contract is published or updated.
*/
void init() {
eosio::print( "Init World!\n" );
}
/// The apply method implements the dispatch of events to this contract
void apply( uint64_t code, uint64_t action ) {
eosio::print( "Hello World: ", eosio::name(code), "->", eosio::name(action), "\n" );
}
} // extern "C"
这个合约实现了两个入口, init
和 apply
。它所做的只是记录提交的messages而并不作检查。只要区块生产者同意,任何人在任何时间都可以提交任何message。但没有所需的签名,合约将因消耗带宽被收费。
您可以将合约像这样编译成文本版本的WASM (.wast) :
$ eoscpp -o hello.wast hello.cpp
部署您的合约
现在您已经编译了您的应用,我们可以部署了。这需要您先:
- 启动 eosd 并打开钱包插件
- 新建钱包,导入至少一个账户的密钥
- 解锁钱包
如果您的钱包里有${account}
的密钥且已经解锁,您就可以用下面的命令把合约上传到区块链上
$ eosc set contract ${account} hello.wast hello.abi
Reading WAST...
Assembling WASM...
Publishing contract...
{
"transaction_id": "1abb46f1b69feb9a88dbff881ea421fd4f39914df769ae09f66bd684436443d5",
"processed": {
"ref_block_num": 144,
"ref_block_prefix": 2192682225,
"expiration": "2017-09-14T05:39:15",
"scope": [
"eos",
"${account}"
],
"signatures": [
"2064610856c773423d239a388d22cd30b7ba98f6a9fbabfa621e42cec5dd03c3b87afdcbd68a3a82df020b78126366227674dfbdd33de7d488f2d010ada914b438"
],
"messages": [{
"code": "eos",
"type": "setcode",
"authorization": [{
"account": "${account}",
"permission": "active"
}
],
"data": "0000000080c758410000f1010061736d0100000001110460017f0060017e0060000060027e7e00021b0203656e76067072696e746e000103656e76067072696e7473000003030202030404017000000503010001071903066d656d6f7279020004696e69740002056170706c7900030a20020600411010010b17004120100120001000413010012001100041c00010010b0b3f050041040b04504000000041100b0d496e697420576f726c64210a000041200b0e48656c6c6f20576f726c643a20000041300b032d3e000041c0000b020a000029046e616d6504067072696e746e0100067072696e7473010004696e697400056170706c790201300131010b4163636f756e744e616d65044e616d6502087472616e7366657200030466726f6d0b4163636f756e744e616d6502746f0b4163636f756e744e616d6506616d6f756e740655496e743634076163636f756e740002076163636f756e74044e616d650762616c616e63650655496e74363401000000b298e982a4087472616e736665720100000080bafac6080369363401076163636f756e7400076163636f756e74"
}
],
"output": [{
"notify": [],
"deferred_transactions": []
}
]
}
}
如果您查看eosd 进程的输出您将看到:
...] initt generated block #188249 @ 2017-09-13T22:00:24 with 0 trxs 0 pending
Init World!
Init World!
Init World!
您可以看到"Init World!"被执行了三次,这其实并不是个bug。区块链处理transactions的流程是:
1: eosd收到一个新transaction (正在验证的transaction)
- 创建一个新的临时会话
- 尝试应用此transaction
- 成功并打印出"Init World!"
- 失败则回滚所做的变化 (也有可能打印"Init World!"后失败)
2 : eosd开始产出区块
- 撤销所有pending状态
- pushes all transactions as it builds the block
- 第二次打印"Init World!"
- 完成区块
- 撤销所有创造区块时的临时变化
3rd : eosd如同从网络上获得区块一样将区块追加到链上。
- 第三次打印 "Init World!"
此时,您的合约就可以开始接受messages了。因为默认message处理器接受所有messages,我们可以发送任何我们想发的东西。我们试一下发一个空的message:
$ eosc push message ${account} hello '"abcd"' --scope ${account}
此命令将"hello"message及16进制字符串"abcd"所代表的二进制文件传出。注意,后面我们将展示如何定义ABI来用一个好看易读的JSON对象替换16进制字符串。以上,我们只是想证明“hello”类型的message是如何发送到账户的。
结果是:
{
"transaction_id": "69d66204ebeeee68c91efef6f8a7f229c22f47bcccd70459e0be833a303956bb",
"processed": {
"ref_block_num": 57477,
"ref_block_prefix": 1051897037,
"expiration": "2017-09-13T22:17:04",
"scope": [
"${account}"
],
"signatures": [],
"messages": [{
"code": "${account}",
"type": "hello",
"authorization": [],
"data": "abcd"
}
],
"output": [{
"notify": [],
"deferred_transactions": []
}
]
}
}
如果您继续查看eosd的输出,您将在屏幕上看到:
Hello World: ${account}->hello
Hello World: ${account}->hello
Hello World: ${account}->hello
再一次,您的合约在transaction被第三次应用并成为产出的区块之前被执行和撤销了两次。
Message名的限定
Message的类型实际上是base32编码的64位整数。所以Message名的前12个字符需限制在字母a-z, 1-5, 以及'.' 。第13个以后的字符限制在前16个字符('.' and a-p)。
ABI - Application Binary Interface
Application Binary Interface (ABI)是一个基于JSON的描述文件,是关于转换JSON和二进制格式的用户actions的。ABI还描述了如何将数据库状态和JSON的互相转换。一旦您通过ABI描述了您的合约,开发者和用户就能够用JSON和您的合约无缝交互了。
我们正在开发使用C++源码自动生成ABI的工具,但目前为止您还是只能手动生成。
这里是一个合约的骨架ABI的例子:
{
"types": [{
"new_type_name": "account_name",
"type": "name"
}
],
"structs": [{
"name": "transfer",
"base": "",
"fields": {
"from": "account_name",
"to": "account_name",
"quantity": "uint64"
}
},{
"name": "account",
"base": "",
"fields": {
"account": "name",
"balance": "uint64"
}
}
],
"actions": [{
"action": "transfer",
"type": "transfer"
}
],
"tables": [{
"table": "account",
"type": "account",
"index_type": "i64",
"key_names" : ["account"],
"key_types" : ["name"]
}
]
}
您肯定注意到了这个ABI 定义了一个叫transfer
的action,它的类型也是transfer
。这就告诉EOS.IO当${account}->transfer
的message发生时,它的payload是transfer
类型的。 transfer
类型是在structs
的列表中定义的,其中有个对象,name
属性是transfer
。
...
"structs": [{
"name": "transfer",
"base": "",
"fields": {
"from": "account_name",
"to": "account_name",
"quantity": "uint64"
}
},{
...
这部分包括from
, 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": []
}
]
}
}
如果您继续观察eosd的输出,您将看到:
Hello World: ${account}->transfer
Hello World: ${account}->transfer
Hello World: ${account}->transfer
处理转账Message的参数
根据ABI,transfer message应该是如下格式的:
"fields": {
"from": "account_name",
"to": "account_name",
"quantity": "uint64"
}
我们也知道account_name -> uint64表示这个message的二进制表示如同:
struct transfer {
uint64_t from;
uint64_t to;
uint64_t quantity;
};
EOS.IO的C API通过Message API提供获取message的payload的能力:
uint32_t message_size();
uint32_t read_message( void* msg, uint32_t msglen );
让我们修改hello.cpp来打印出消息内容:
#include <hello.hpp>
/**
* The init() and apply() methods must have C calling convention so that the blockchain can lookup and
* call these methods.
*/
extern "C" {
/**
* This method is called once when the contract is published or updated.
*/
void init() {
eosio::print( "Init World!\n" );
}
struct transfer {
uint64_t from;
uint64_t to;
uint64_t quantity;
};
/// The apply method implements the dispatch of events to this contract
void apply( uint64_t code, uint64_t action ) {
eosio::print( "Hello World: ", eosio::name(code), "->", eosio::name(action), "\n" );
if( action == N(transfer) ) {
transfer message;
static_assert( sizeof(message) == 3*sizeof(uint64_t), "unexpected padding" );
auto read = readMessage( &message, sizeof(message) );
assert( read == sizeof(message), "message too short" );
eosio::print( "Transfer ", message.quantity, " from ", eosio::name(message.from), " to ", eosio::name(message.to), "\n" );
}
}
} // extern "C"
这样我们就可以重编译并部署了:
eoscpp -o hello.wast hello.cpp
eosc set contract ${account} hello.wast hello.abi
eosd因为重部署将再次调用init()
Init World!
Init World!
Init World!
然后我们执行transfer:
$ eosc push message ${account} transfer '{"from":"currency","to":"inita","quantity":50}' --scope ${account}
{
"transaction_id": "a777539b7d5f752fb40e6f2d019b65b5401be8bf91c8036440661506875ba1c0",
"processed": {
"ref_block_num": 20,
"ref_block_prefix": 463381070,
"expiration": "2017-09-14T01:05:49",
"scope": [
"${account}"
],
"signatures": [],
"messages": [{
"code": "${account}",
"type": "transfer",
"authorization": [],
"data": {
"from": "currency",
"to": "inita",
"quantity": 50
},
"hex_data": "00000079b822651d000000008040934b3200000000000000"
}
],
"output": [{
"notify": [],
"deferred_transactions": []
}
]
}
}
后面我们将看到eosd有如下输出:
Hello World: ${account}->transfer
Transfer 50 from currency to inita
Hello World: ${account}->transfer
Transfer 50 from currency to inita
Hello World: ${account}->transfer
Transfer 50 from currency to inita
使用 C++ API来读取 Messages
目前我们使用是C API因为这是EOS.IO直接暴露给WASM虚拟机的最底层的API。幸运的是,eoslib提供了一个更高级的API,移除了很多不必要的代码。
/// eoslib/message.hpp
namespace eosio {
template<typename T>
T current_message();
}
我们可以向下面一样更新 hello.cpp 把它变得更简洁:
#include <hello.hpp>
/**
* The init() and apply() methods must have C calling convention so that the blockchain can lookup and
* call these methods.
*/
extern "C" {
/**
* This method is called once when the contract is published or updated.
*/
void init() {
eosio::print( "Init World!\n" );
}
struct transfer {
eosio::name from;
eosio::name to;
uint64_t quantity;
};
/// The apply method implements the dispatch of events to this contract
void apply( uint64_t code, uint64_t action ) {
eosio::print( "Hello World: ", eosio::name(code), "->", eosio::name(action), "\n" );
if( action == N(transfer) ) {
auto message = eosio::current_message<transfer>();
eosio::print( "Transfer ", message.quantity, " from ", message.from, " to ", message.to, "\n" );
}
}
} // extern "C"
您可以注意到我们更新了transfer
的struct,直接使用eosio::name
类型并将read_message
前后的类型检查压缩为一个单个的current-Message
调用。
在编译和上传后,您将看到和C语言版本同样的结果。
获取发送者的Authority来进行转账
合约最普遍的需求之一就是定义谁可以进行这样的操作。比如在货币转账的例子里,我们就需要定义为from
字段的账户核准此message。
EOS.IO软件负责加强和验证签名,您需要做的是获取所需的authority。
...
void apply( uint64_t code, uint64_t action ) {
eosio::print( "Hello World: ", eosio::name(code), "->", eosio::name(action), "\n" );
if( action == N(transfer) ) {
auto message = eosio::current_message<transfer>();
eosio::require_auth( message.from );
eosio::print( "Transfer ", message.quantity, " from ", message.from, " to ", message.to, "\n" );
}
}
...
建立和部署后,我们可以再试一次转账:
eosc push message ${account} transfer '{"from":"initb","to":"inita","quantity":50}' --scope ${account}
1881603ms thread-0 main.cpp:797 operator() ] Converting argument to binary...
1881630ms thread-0 main.cpp:851 main ] Failed with error: 10 assert_exception: Assert Exception
status_code == 200: Error
: 3030001 tx_missing_auth: missing required authority
Transaction is missing required authorization from initb
{"acct":"initb"}
thread-0 message_handling_contexts.cpp:19 require_authorization
...
如果您查看eosd
,您将看到:
Hello World: initc->transfer
1881629ms thread-0 chain_api_plugin.cpp:60 operator() ] Exception encountered while processing chain.push_transaction:
...
这表示此操作尝试请求应用您的transaction,打印出了初始的"Hello World",然后当eosio::require_auth
没能成功获取initb
账户的authorization后,操作终止了。
我们可以通过让eosc增加所需的permission来修复这个问题:
eosc push message ${account} transfer '{"from":"initb","to":"inita","quantity":50}' --scope ${account} --permission initb@active
--permission
命令定义了账户和permission等级,此例中我们使用active authority,也就是默认值。
这次转账应该就成功了,如同我们之前看到的一样。
Aborting a Message on Error
绝大多数合约开发中有非常多的前置条件,比如转账的金额要大于0。如果用户尝试进行一个非法action,合约必须终止且已做出的任何变动都必须自动回滚。
...
void apply( uint64_t code, uint64_t action ) {
eosio::print( "Hello World: ", eosio::name(code), "->", eosio::name(action), "\n" );
if( action == N(transfer) ) {
auto message = eosio::current_message<transfer>();
assert( message.quantity > 0, "Must transfer a quantity greater than 0" );
eosio::require_auth( message.from );
eosio::print( "Transfer ", message.quantity, " from ", message.from, " to ", message.to, "\n" );
}
}
...
我们编译、部署并尝试进行一次金额为0的转账:
$ eoscpp -o hello.wast hello.cpp
$ eosc set contract ${account} hello.wast hello.abi
$ eosc push message ${account} transfer '{"from":"initb","to":"inita","quantity":0}' --scope initc --permission initb@active
3071182ms thread-0 main.cpp:851 main ] Failed with error: 10 assert_exception: Assert Exception
status_code == 200: Error
: 10 assert_exception: Assert Exception
test: assertion failed: Must transfer a quantity greater than 0
4. Tic-Tac-Toe
目标
下面的教程将引导用户构建一个样例的PvP的游戏合约。我们用tic tac toe游戏来举例。本教程的结果在 这里.
前提
在此游戏中,我们用标准的3x3 tic tac toe板。玩家们有两种角色host和challenger。Host 永远是先手。每个玩家只能同时玩两局比赛,一局是第一个玩家是host另一局是第二个玩家是host。
游戏板
(0,0) | (1,0) | (2,0) | |
---|---|---|---|
(0,0) | - | o | x |
(0,1) | - | x | - |
(0,2) | x | o | o |
不同于传统的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: 创建一个新游戏
- restart: 重启一个现有的游戏, host或challenger都可以这么做
- close: 关闭一个现有的游戏,释放存储游戏的数据,只有host可以这么做
- move: 走一步
合约账户
在下面的教程中,我们将把合约添加到一个叫tic.tac.toe
的账户中。为防止tic.tac.toe
的账户名被占用,您可以用其他的账户名,只需要在代码里面用您的账户名替换掉tic.tac.toe
。如果您没有账户,请先创建。
$ eosc create account ${creator_name} ${contract_account_name} ${contract_pub_owner_key} ${contract_pub_active_key} --permission ${creator_name}@active
# e.g. $ eosc create account inita tic.tac.toe EOS4toFS3YXEQCkuuw1aqDLrtHim86Gz9u3hBdcBw5KNPZcursVHq EOS7d9A3uLe6As66jzN8j44TXJUqJSK3bFjjEEqR4oTvNAB3iM9SA --permission inita@active
请先解锁钱包并导入私钥,否则上面的命令将失败。
开始!
我们将创建三个文件:
- tic_tac_toe.hpp => 定义合约结构的头文件
- tic_tac_toe.cpp => 合约的主要逻辑
- tic_tac_toe.abi => 用户和合约交互的接口
定义结构
让我们先从定义合约结构开始。打开tic_tac_toe.hpp 并且从下面的模版代码开始
// Import necessary library
#include <eoslib/eos.hpp> // Generic eos library, i.e. print, type, math, etc
#include <eoslib/db.hpp> // Database access
using namespace eosio;
namespace tic_tac_toe {
// Your code here
}
游戏表
对于这个合约我们需要把游戏列表存在表中,我们来定义它:
...
namespace tic_tac_toe {
...
using Games = eosio::table<N(tic.tac.toe),N(tic.tac.toe),N(games),game,uint64_t>;
}
NB: 如果您要把合约上传到其他账户上,请用您的账户名替代tic.tac.toe
。
第一个参数定义表的默认scope,比如当有没有指定scope的数据存入表中时,它就会使用这个账户。
-
第二个参数定义表的所有者 (比如合约的名字)
-
第三个参数定义表的名字
-
第四个参数定义存储数据结构(将在后面定义)
-
第五个参数定义表中key的类型
游戏结构
下面我们来定义游戏的结构。注意在代码中,定义结构需要在定义表之前。
...
namespace tic_tac_toe {
struct PACKED(game) {
// 默认 constructor
game() {};
// Constructor
game(account_name challenger, account_name host):challenger(challenger), host(host), turn(host) {
// 初始化游戏板
initialize_board();
};
// challenger的账户名,也是表中的key
account_name challenger;
// host的账户名
account_name host;
// 轮到谁走, = 可能是host或challenger的账户名
account_name turn;
// 赢家, = 空或平手或者是host或challenger的账户名
account_name winner = N(none);
// 游戏板列表的长度,需放在游戏板列表的前面一个。有此字段abi序列化工具才能正确的可以打包写入数据库或从数据库拆包数据
uint8_t board_len = 9;
// 游戏板列表
uint8_t board[9];
// 用空格初始化游戏板
void initialize_board() {
for (uint8_t i = 0; i < board_len ; i++) {
board[i] = 0;
}
}
// 重置游戏
void reset_game() {
initialize_board();
turn = host;
winner = N(none);
}
};
...
}
记住,在前面表定义的时候,我们声明表的key数据类型是uint64_t
。因此,在前面的游戏结构中,结构中前sizeof(uint64_t)
字节长度的数据将被当成表的key。顺便一提,account_name
只是uint64_t
的别名。
Action 结构
Create
要新建游戏,我们需要 host 账户名和 challenger 账户名。
...
namespace tic_tac_toe {
...
struct create {
account_name challenger;
account_name host;
};
...
}
Restart
要重启游戏,我们需要host 账户名和 challenger 账户名来找到该游戏。而且,我们需要指定是谁重启了游戏,这样才能验证是否有有效的签名。
...
namespace tic_tac_toe {
...
struct restart {
account_name challenger;
account_name host;
account_name by;
};
...
}
Close
要关闭游戏,我们需要host 账户名和 challenger 账户名来找到该游戏。
...
namespace tic_tac_toe {
...
struct close {
account_name challenger;
account_name host;
};
...
}
Move
要移动一步,我们需要host 账户名和 challenger 账户名来找到该游戏。 而且,我们需要指定是谁走的这一步以及这一步走在哪。
...
namespace tic_tac_toe {
...
struct movement {
uint32_t row;
uint32_t column;
};
struct Move {
account_name challenger;
account_name host;
account_name by; // the account who wants to make the move
movement m;
};
...
}
您可以在 这里 找到atic_tac_toe.hpp 的最终代码。
主程序
打开tic_tac_toe.cpp并配置骨架代码
#include <tic_tac_toe.hpp>
using namespace eosio;
/**
* The init() and apply() methods must have C calling convention so that the blockchain can lookup and
* call these methods.
*/
extern "C" {
// Only called once
void init() {
}
/// The apply method implements the dispatch of events to this contract
void apply( uint64_t code, uint64_t action_name ) {
// Put your message handler here
}
} // extern "C"
Message 处理器
我们希望tic_tac_toe合约仅响应发给tic.tac.toe
账户的message并且根据不同的action类型来给出不同响应。让我们在apply函数中加入message过滤器。
...
void apply( uint64_t code, uint64_t action_name ) {
if (code == N(tic.tac.toe)) {
if (action_name == N(create)) {
tic_tac_toe::apply_create(current_message<tic_tac_toe::create>());
} else if (action_name == N(restart)) {
tic_tac_toe::apply_restart(current_message<tic_tac_toe::restart>());
} else if (action_name == N(close)) {
tic_tac_toe::apply_close(current_message<tic_tac_toe::close>());
} else if (action_name == N(move)) {
tic_tac_toe::apply_move(current_message<tic_tac_toe::move>());
}
}
}
...
注意我们在把message传入特定处理器之前使用了current_message<T>()
,它是将收到的message 转为struct T
的。
NB: 如果您正部署到另一个账户,请用您的账户名替换tic.tac.toe
。
为了简洁起见,我们把message处理器包装在namespace tic_tac_toe
中:
namespace tic_tac_toe {
void apply_create(const create& c) {
// Put code for create action here
}
void apply_restart(const restart& r) {
// Put code for restart action here
}
void apply_close(const close& c) {
// Put code for close action here
}
void apply_move(const move& m) {
// Put code for move action here
}
...
}
create Message 处理器
对于create message的处理器,我们需要
- 确保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的完整代码
创建 ABI
有了Abi (即 Application Binary Interface),合约才能理解您所发的二进制信息。打开tic_tac_toe.abi并定义如下框架代码:
{
"structs": [{
"name": "...",
"base": "...",
"fields": { ... }
}, ...],
"actions": [{
"action_name": "...",
"type": "..."
}, ...],
"tables": [{
"table_name": "...",
"type": "...",
"key_names" : [...],
"key_types" : [...]
}, ...]
- struct: 合约中action/ table所用到的数据结构列表
- actions: 合约中可用的actions
- tables: 合约中可用的表
表 ABI
在tic_tac_toe.hpp中,我们创造了一个叫game的single index i64的表。它保存了game
结构并使用challenger
作为key(数据类型是account_name
)。因此,abi文件是:
{
...
"structs": [{
"name": "game",
"base": "",
"fields": {
"challenger": "account_name",
"host": "account_name",
"turn": "account_name",
"winner": "account_name",
"board": "uint8[]"
}
}],
"tables": [{
"table_name": "games",
"type": "game",
"index_type": "i64",
"key_names" : ["challenger"],
"key_types" : ["account_name"]
}
]
...
}
Actions ABI
对actions来说,我们在actions
里定义actions,在structs
定义actions的数据结构。
{
...
"structs": [{
"name": "create",
"base": "",
"fields": {
"challenger": "account_name",
"host": "account_name"
}
},{
"name": "restart",
"base": "",
"fields": {
"challenger": "account_name",
"host": "account_name",
"by": "account_name"
}
},{
"name": "close",
"base": "",
"fields": {
"challenger": "account_name",
"host": "account_name"
}
},{
"name": "movement",
"base": "",
"fields": {
"row": "uint32",
"column": "uint32"
}
},{
"name": "move",
"base": "",
"fields": {
"challenger": "account_name",
"host": "account_name",
"by": "account_name",
"movement": "movement"
}
}],
"actions": [{
"action_name": "create",
"type": "create"
},{
"action_name": "restart",
"type": "restart"
},{
"action_name": "close",
"type": "close"
},{
"action_name": "move",
"type": "move"
}
]
...
}
部署!
现在所有文件(tic_tac_toe.hpp, tic_tac_toe.cpp, tic_tac_toe.abi)都完成了。可以部署了!
$ eosc set contract tic.tac.toe tic_tac_toe.wast tic_tac_toe.abi
注意您的钱包需要是解锁的,而tic.tac.toe
密钥已导入。如果您要把该合约上传到其他账户,请用您的账户名替换tic.tac.toe
并且确保您的钱包里有改账户的密钥。
开玩!
部署并且 transaction确认后,合约就在您的区块链上生效了。您现在就可以玩了。
新建
$ eosc push message tic.tac.toe create '{"challenger":"inita", "host":"initb"}' -S initb -S tic.tac.toe -p initb@active
移动
$ eosc push message tic.tac.toe move '{"challenger":"inita", "host":"initb", "by":"initb", "movement":{"row":0, "column":0} }' -S initb -S tic.tac.toe -p initb@active
$ eosc push message tic.tac.toe move '{"challenger":"inita", "host":"initb", "by":"inita", "movement":{"row":1, "column":1} }' -S initb -S tic.tac.toe -p inita@active
重启
$ eosc push message tic.tac.toe restart '{"challenger":"inita", "host":"initb", "by":"initb"}' -S initb -S tic.tac.toe -p initb@active
关闭
$ eosc push message tic.tac.toe close '{"challenger":"inita", "host":"initb"}' -S initb -S tic.tac.toe -p initb@active
查看游戏状态
$ eosc get table initb tic.tac.toe games
{
"rows": [{
"challenger": "inita",
"host": "initb",
"turn": "inita",
"winner": "none",
"board": [
1,
0,
0,
0,
2,
0,
0,
0,
0
]
}
],
"more": false
}