1. EOS智能合约的介绍
1.1. 所需背景知识
C / C++ 经验
基于EOS.IO的区块链使用Web Assembly(WASM)执行开发者提供的应用代码。WASM是一个已崭露头角的web标准,受到Google, Microsoft, Apple及其他大公司的广泛支持。目前为止,最成熟的用于构建应用及WASM代码编译的工具链是clang/llvm及其C/C++编译器。
其他由第三方开发中的工具链包括:Rust, Python, and Solidiity。尽管用其他语言更简单,但是他们的性能很可能制约你所构建的应用规模。我们希望C++ 将成为开发高性能及安全智能合约的最佳语言。
Linux / Mac OS 经验
EOS.IO软件仅官方支持如下环境
- Ubuntu 16.10 或更高
- MacOS Sierra 或更高
命令行知识
EOS.IO提供了一系列工具,需要基本的命令行知识来操作它们。
1.2. EOS智能合约基础知识
通信模型
EOS智能合约通过messages 及 共享内存数据库(比如只要一个合约被包含在transaction的读取域中with an async vibe,它就可以读取另一个合约的数据库)相互通信。异步通信导致的spam问题将由资源限制算法来解决。下面是两个在合约里可定义的通信模型:
Inline. Inline保证执行当前的transaction或unwind;无论成功或失败都不会有通知。Inline 操作的scopes和authorities和原来的transaction一样。
Deferred. Defer将稍后由区块生产者来安排;结果可能是传递通信结果或者只是超时。Deferred可以触及不同的scopes,可以携带发送它的合约的authority*此特性在STAT不可用
Message vs Transaction
一个message代表单个操作, 一个transaction是一个或多个messages的集合。合约和账户通过messages通信。Messages可以单个地发送,如果希望一次执行批处理也可以集合起来发送。
单message的Transaction.
{
"ref_block_num": "100",
"ref_block_prefix": "137469861",
"expiration": "2017-09-25T06:28:49",
"scope": ["initb","initc"],
"messages": [
{
"code": "eos",
"type": "transfer",
"authorization": [
{
"account": "initb",
"permission": "active"
}
],
"data": "000000000041934b000000008041934be803000000000000"
}
],
"signatures": [],
"authorizations": []
}
多messages的Transaction,这些messages将全部成功或全部失败。
{
"ref_block_num": "100",
"ref_block_prefix": "137469861",
"expiration": "2017-09-25T06:28:49",
"scope": [...],
"messages": [{
"code": "...",
"type": "...",
"authorization": [...],
"data": "..."
}, {
"code": "...",
"type": "...",
"authorization": [...],
"data": "..."
}, ...
],
"signatures": [],
"authorizations": []
}
Message名的限定
Message的类型实际上是base32编码的64位整数。所以Message名的前12个字符需限制在字母a-z, 1-5, 以及'.' 。第13个以后的字符限制在前16个字符('.' and a-p)。
Transaction确认
获得一个transaction哈希并不等于transaction完成,它只表示该节点无报错地接受了,而其他区块生产者很可能也会接受它。
但要确认该transaction,你需要在transaction历史中查看含有该transaction的区块数。
1.3. 技术限制
- 无浮点数. 合约不接受浮点小数计算因为这在CPU层级上是一个不确定的行为,会导致意想不到的分叉。
- Transaction需要在1 ms内执行. transaction的执行时间需要在*小于等于1ms否则transaction将会失败。
- 最大 30 tps. 目前根据测试公网设置,每个账户最多每秒可发布30个transactions.
2 智能合约文件
为简单起见我们创造了一个工具叫 eoscpp,它可以用来引导产生一个新合约。eoscpp将创造三个智能合约文件,他们是你起步开发的框架。
$ eoscpp -n ${contract}
以上将在'./${project}'文件夹下创建一个新项目,包含三个文件:
${contract}.abi ${contract}.hpp ${contract}.cpp
2.1. HPP
HPP是包含CPP文件所引用的变量、常量、函数的头文件。
2.2. CPP
CPP文件是包含合约功能的源文件。
如果您通过eoscpp工具产生CPP文件,产生的文件将和如下相似:
#include <${contract}.hpp>
/**
* init() 和 apply() 方法一定要有C调用约定 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" ); // Replace with actual code
}
/// 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。如果缺少所需的签名,合约将会被收取消耗带宽的费用。
init
init
仅在被初次部署的时候执行一次。它是用于初始化合约变量的,例如货币合约中提供token的数量。
apply
apply
是message处理器,它监听所有输入的messages并根据函数中的规定进行反馈。apply
函数需要两个输入参数,code
和 action
。
code filter
为了响应特定message,您可以如下构建您的apply
函数。您也可以忽略code filter来构建一个响应通用messages的函数。
if (code == N(${contract_name}) {
//响应特定message的处理器
}
在其中您可以定义对不同actions的响应。
action filter
为了相应特定action,您可以如下构建您的apply
函数。常和code filter一起使用。
if (action == N(${action_name}) {
//响应该action的处理器
}
2.3. WAST
想要部署到EOS.IO区块链上的任何程序都需要先编译成WASM格式。这是区块链接受的唯一格式。
一旦您完成了CPP文件的开发,您可以用eoscpp
工具将它编译成一个文本版本的WASM (.wast) 文件。
$ eoscpp -o ${contract}.wast ${contract}.cpp
2.4. ABI
Application Binary Interface (ABI)是一个基于JSON的描述文件,是关于转换JSON和二进制格式的用户actions的。ABI还描述了如何将数据库状态和JSON的互相转换。一旦您通过ABI描述了您的合约,开发者和用户就能够用JSON和您的合约无缝交互了。
ABI文件可通过eoscpp
工具从HPP文件生成:
$ eoscpp -g ${contract}.abi ${contract}.hpp
这里是一个合约的骨架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
是一个用base32编码来表示uint64的内置类型。 要了解更多的可用内置类型,请点击这里.
{
"types": [{
"new_type_name": "account_name",
"type": "name"
}
],
...
上述types
列表内,我们定义了一系列现有类型的别名。这里,我们把account_name
定义为name
的别名。
3. 清单
在开始EOS智能合约开发之前,我们需要搞清下面的内容:
构建最新的版本
请确认您环境中的是最新的版本,您才能获取到eoscpp
和 eosc
这些对于您开发非常重要的工具。 如何获取最新构建版本可以在 环境 章节找到。
一旦您安装了最新版本的eosio/eos代码,请确认您的环境变量中有${CMAKE_INSTALL_PREFIX}/bin,如果没有的话您可以用下面的命令安装。
cd build
make install
连接到EOS.IO区块链
您可以用以下命令连接到一个节点
$ eosc -H ${node_ip} -p ${port_num}
node_ip可以是私有的节点IP。如果您连接的是测试公网,您需要用节点的公共IP这里.
port_num 是8888或8889,具体取决于配置。
创建钱包获取账户
为了在区块链上部署合约,您需要在EOS.IO区块链上创建一个账户。每个合约都需要一个相关联的账户。
如果您已经有EOS Tokens,您应该在测试公网上已经有一个账户了。如果您需要新建一个测试账户,请参考以下信息:
4. 与智能合约交互的例子
在深入了解如何构建一个智能合约前,我们这里提供了一些智能合约的例子,供您参考以更快的理解EOS智能合约是如何工作的。
为了和这些样例合约交互,您需要先完成清单上面的步骤并且部署样例合约到EOS.IO区块链上。
4.1. 货币合约
部署样例合约
样例货币合约可在这里找到,如果您已经下载了EOSIO仓库的话,那您应当可以在本地磁盘上找到。
文件夹中包括.abi, .cpp 和 .hpp 文件,在您部署合约前您需要编译生成.wast文件。
$ eoscpp -o currency.wast currency.cpp
您成功生成.wast文件后,您可以使用set contract
命令来部署。
$ eosc set contract ${contract_account_name} ../contracts/currency.wast ../contracts/currency.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}"
],
...
}
请确认您的钱包是解锁状态,且您已经将对应${contract_account_name}的有效的key导入到钱包。
了解合约
现在我们已经部署了合约,任何人都可以用eosc
的get code
命令来获取合约的.abi文件并且了解此合约有哪些可用接口。
$ eosc get code currency -a currency.abi
code hash: 86968a9091ce32255777e2017fccaede8cea2d4978b30f25b41ee97b9d77bed0
saving abi to currency.abi
$ cat currency.abi
{
"types": [{
"newTypeName": "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",
"indextype": "i64",
"keynames": [
"account"
],
"keytype": [],
"type": "account"
}
]
}
备注
- 合约接受一个叫
transfer
的transaction,此transaction接受一个有from
、to
及quantity
字段的message。 - 同时有一个叫
account
的table用于存储数据。
既然我们有一个transfer action,而这个账户table可以用来查余额,我们可以用eosc
来和他们交互。
读取账户余额
要从一个表中读取数据,需使用get table
命令:eosc get table ${account} ${contract} ${table}
.
$ eosc get table ${account} currency account
{
"rows": [{
"key": "account",
"balance": 1000000000
}
],
"more": false
}
资金转账
任何人都可以在任何时间向任何合约发任何message但是合约有可能拒绝那些没有特定权限的messages。Messages实际上并不是从任何“人”发出的,它们是“伴随一个或多个账户及其特定等级的permission”发出的。
下面的命令将在货币合约中从account_a到account_b转账50个token。
$ eosc push message currency transfer '{"from":"${account_a}","to":"${account_b}","quantity":50}' --scope ${account_a},${account_b} --permission ${account_a}@active
我们指定了--scope
参数来给与货币合约对于那些能修改自身余额的用户的读写权限。在下个版本中,scope将会自动确定。
您将会收到如下的包含transaction_id
字段的JSON输出,作为本次transaction成功提交的确认信息。
1589302ms thread-0 currency.cpp:271 operator() ] Converting argument to binary...
1589304ms thread-0 currency.cpp:290 operator() ] Transaction result:
{
"transaction_id": "1c4911c0b277566dce4217edbbca0f688f7bdef761ed445ff31b31f286720057",
"processed": {
"refBlockNum": 1173,
"refBlockPrefix": 2184027244,
"expiration": "2017-08-24T18:28:07",
"scope": [...],
"signatures": [],
"messages": [...]
}
}
一旦您获得了这个成功结果,您就可以像刚才一样从账户table中查看账户的余额和状态了。
4.2. Tic-Tac-Toe
tic-tac-toe是一个两人玩的纸笔游戏,用X和O,两人分别轮流在3×3的格子里标记,先完成横向,纵向或对角线三个格子的标记的人获得胜利。
游戏规则
- 每对玩家可以有最多2轮游戏,第一轮是1号玩家是host,2号玩家是challenger,第二轮反过来。
- 游戏数据存储于“host”的游戏表格中,而"challenger"是key。
例子
Coordinate | 0 | 1 | 2 |
---|---|---|---|
0 | - | o | x |
1 | - | x | - |
2 | x | o | o |
用数字在板上表示:
- 0 代表空格子
- 1 代表被host占据的
- 2 代表被challenger占据的
因此,假设x是host,o是challenger,上面的比赛板可以在此局游戏的对象中如下表示:[0, 2, 1, 0, 1, 0, 1, 2, 2]。
部署样例合约
tic_tac_toe合约在这里,如果您已经下载了EOSIO仓库的话,那您应当可以在本地磁盘上找到。
文件夹中包括.abi, .cpp 和 .hpp 文件,在您部署合约前您需要编译生成.wast文件。
$ eoscpp -o tic_tac_toe.wast tic_tac_toe.cpp
您成功生成.wast文件后,您可以使用set contract命令来部署。对于此例来说,我们希望在tic.tac.toe
账户上部署。注意EOS.IO区块链只支持base32字符作为账户名,这也是为什么下划线被替换成了'.'。如果您要在除了tic.tac.toe
的其他账户部署此应用,您需要将.hpp,.cpp,和 .abi文件中的tic.tac.toe
替换为您自己的账户名。
$ eosc set contract tic.tac.toe tic_tac_toe.wast tic_tac_toe.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",
"tic.tac.toe"
],
...
}
了解合约
现在我们已经部署了合约,任何人都可以用eosc
的get code
命令来获取合约的.abi文件并且了解此合约有哪些可用接口。
$ eosc get code tic.tac.toe. -a tic_tac_toe.abi
code hash: c78d16396a5a63b1be47fd570633084cb5fe2eaa9980ca87ec25061d68299294
saving abi to tic_tac_toe.abi
$ cat tic_tac_toe.abi
{
"types": [{
"new_type_name": "account_name",
"type": "name"
}
],
"structs": [{
"name": "game",
"base": "",
"fields": {
"challenger": "account_name",
"host": "account_name",
"turn": "account_name",
"winner": "account_name",
"board": "uint8[]"
}
},{
"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"
}
],
"tables": [{
"table_name": "games",
"type": "game",
"index_type": "i64",
"key_names" : ["challenger"],
"key_types" : ["account_name"]
}
]
}
注释
- 此合约接受
create
,restart
,close
和move
的actions,每个actions接受具有不同字段的messages。 - 叫
games
的table用于保存数据
如何玩游戏:
- 使用
create
来创建游戏,设置您的账户为host其他人的为challenger。
$ eosc push message tic.tac.toe create '{"challenger":"${challenger_account_name}","host":"${your_account_name}"}' --permission ${your_account}@active
- 第一步host走,用
move
action指定需要填入哪行哪列的格子来完成一次移动。
$ eosc push message tic.tac.toe move '{"challenger":"${challenger_account_name}","host":"${your_account_name}","by":"${your_account_name}","''{"row":0,"column":1}"}' --permission ${your_account}@active
- 然后让challenger走,然后再是host走。不断重复直至决出赢家。
$ eosc push message tic.tac.toe move '{"challenger":"${challenger_account_name}","host":"${your_account_name}","by":"${your_account_name}","''{"row":1,"column":1}"}' --permission ${challenger_account}@active
- 用
restart
action重启游戏
$ eosc push message tic.tac.toe restart '{"challenger":"${challenger_account_name}","host":"${your_account_name}","by":"${your_account_name}"}' --permission ${your_account}@active
- 用
close
action来将此游戏从数据库清除,这样会在游戏结束后释放空间。
$ eosc push message tic.tac.toe close '{"challenger":"${challenger_account_name}","host":"${your_account_name}"}' --permission ${your_account}@active
5. 完成您的第一个EOS智能合约
Hello World
此章节中,我们将一步步地构建一个hello world合约。
开始前,您需要先完成清单上的所有步骤。
开始吧 首先,我们使用eoscpp
来生成智能合约的骨架。这将在hello文件夹里产生一个空白工程,里面有abi,hpp和cpp文件。
$ eoscpp -n hello
CPP文件含有一个当收到message后打印 Hello World: ${account}->${action}的样例代码。
void apply( uint64_t code, uint64_t action ) {
eosio::print( "Hello World: ", eosio::name(code), "->", eosio::name(action), "\n" );
}
我们从.cpp文件生成.wast文件。
$ eoscpp -o hello.wast hello.cpp
您获得.wast 和 .abi 文件后,就可以将合约部署到区块链上了。
假设您的钱包已经解锁了并且有${account}
的keys,您就可以上传用此命令把合约上传到区块链上:
$ 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被第三次应用并成为产出的区块之前被执行和撤销了两次。
如果我们查看ABI文件,您将会注意到这个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"
}
},{
...
在弄清骨架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
根据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 = read_message( &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" {
/**
* 此方法仅在合约发布或升级时调用一次
*/
void init() {
eosio::print( "Init World!\n" );
}
struct transfer {
eosio::name from;
eosio::name to;
uint64_t quantity;
};
/// apply 方法实现了合约事件的分发
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,也就是默认值。
这次转账应该就成功了,如同我们之前看到的一样。
发生错误时终止Message
绝大多数合约开发中有非常多的前置条件,比如转账的金额要大于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::currentMessage<transfer>();
assert( message.quantity > 0, "Must transfer a quantity greater than 0" );
eosio::requireAuth( 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
到此为止您已经完成了Hello World教程,您可以自己编写您的第一个智能合约了。
6. 部署和升级智能合约
如上面在教程里所提到的,将合约部署到区块链上可以通过set contract
命令简单的完成。并且如果您有权限的话,set contract
命令还可更新现有合约。
使用下面的命令来:
- 部署一个新合约
- 更新现存合约
$ eosc set contract ${account} ${contract}.wast ${contract}.abi
7. 命令小结
下载并构建最新的 EOS.IO 软件 $ build.sh ${architecture} ${build_mode}
开发智能合约
- 使用 eoscpp工具创建骨架
$ eoscpp -n ${contract}
- 在.cpp 和 .hpp文件中编辑您的智能合约
- 生成.abi文件
$ eoscpp -g ${contract}.abi ${contract}.hpp
- 生成.wast文件
$ eoscpp -o ${contract}.wast ${contract}.cpp
部署智能合约
- 连接到一个节点上
$ eosc -H ${node_ip} -p ${port_num}
- 创建钱包
$ eosc wallet create
- [创建账户] 如果您没有EOS keys的话
- 导入账户的key
$ eosc wallet import ${private_key}
- 解锁钱包
$ eosc wallet unlock ${wallet}
- 部署合约
$ eosc set contract ${account} ${contract}.wast ${contract}.abi
8. 调试智能合约
为调试智能合约,您需要安装本地的eosd节点。本地的eosd节点可以以单独的调试私网运行也可以作为调试公网(或官方的调试网络)的延伸来运行。当您在第一次创建智能合约的时候,最好先在测试私网中测试调试完毕您的智能合约,因为您可以完全掌握整个区块链。这使得您有无限的eos而且可以随时重置区块链的状态。当合约可以上生产环境时,可以通过将您的本地eosd和测试公网(或官方的调试网络)连接起来以完成公网的调试,这样您就可以在本地的eosd上看到测试网络的数据了。
因为概念是一致的,所以接下来的指南中将会介绍在测试私网中的调试。
如果您还没有安装您的本地eosd请根据安装指南安装。默认情况下,您的本地eosd将只在测试私网中运行,除非您修改config.ini 文件来将其与测试公网(或官方的调试网络)节点连接,就像该指南中提到的一样。
8.1. 方法
用于调试智能合约的主要方法是 Caveman调试法,我们使用打印的方法来监控一个变量并检查合约的流程。在智能合约中打印信息可以通过打印API (C 和 C++)来完成。 C++ API 是 C API的封装,因此大多数情况下我们用的是C++ API。
8.2. 打印
C API 支持打印如下的数据类型:
- prints - a null terminated char array (string)
- prints_l - any char array (string) with given size
- printi - 64-bit unsigned integer
- printi128 - 128-bit unsigned integer
- printd - double encoded as 64-bit unsigned integer
- printn - base32 string encoded as 64-bit unsigned integer
- printhex - hex given binary of data and its size
打印时,C++ API 通过重写print()方法封装了一些上面的C API使得用户不需要关心需要调用那个打印函数。C++ 打印 API支持
- a null terminated char array (string)
- integer (128-bit unsigned, 64-bit unsigned, 32-bit unsigned, signed, unsigned)
- base32 string encoded as 64-bit unsigned integer
- struct that has print() method
8.3. 例子
让我们写一个新的合约作为调试的例子
- debug.hpp
#include <eoslib/eos.hpp>
#include <eoslib/db.hpp>
namespace debug {
struct foo {
account_name from;
account_name to;
uint64_t amount;
void print() const {
eosio::print("Foo from ", eosio::name(from), " to ",eosio::name(to), " with amount ", amount, "\n");
}
};
}
- debug.cpp
#include <debug.hpp>
extern "C" {
void init() {
}
void apply( uint64_t code, uint64_t action ) {
if (code == N(debug)) {
eosio::print("Code is debug\n");
if (action == N(foo)) {
eosio::print("Action is foo\n");
debug::foo f = eosio::current_message<debug::foo>();
if (f.amount >= 100) {
eosio::print("Amount is larger or equal than 100\n");
} else {
eosio::print("Amount is smaller than 100\n");
eosio::print("Increase amount by 10\n");
f.amount += 10;
eosio::print(f);
}
}
}
}
} // extern "C"
- debug.hpp
{
"structs": [{
"name": "foo",
"base": "",
"fields": {
"from": "account_name",
"to": "account_name",
"amount": "uint64"
}
}
],
"actions": [{
"action_name": "foo",
"type": "foo"
}
]
}
让我们部署并发个message给它。假设您已经创建了debug
账户,钱包中也有对应的key。
$ eoscpp -o debug.wast debug.cpp
$ eosc set contract debug debug.wast debug.abi
$ eosc push message debug foo '{"from":"inita", "to":"initb", "amount":10}' --scope debug
当您查看本地eosd节点日志时,您将看到前一条message发送后的如下内容
Code is debug
Action is foo
Amount is smaller than 100
Increase amount by 10
Foo from inita to initb with amount 20
这样您就可以确认您的message经过了正确的控制流且数据被正确地更新了。您可能会看到上面的至少两次,这很正常因为每个transaction在验证、生产区块及区块应用的阶段都会被应用一次。