EOS 智能合约

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"

这里您可以看到我们创建了两个函数,initapply。他们所做的是记录所有提交的messages 到日志中且并不作检查。只要区块生产者同意,任何人在任何时间都可以提交任何message。如果缺少所需的签名,合约将会被收取消耗带宽的费用。

init

init仅在被初次部署的时候执行一次。它是用于初始化合约变量的,例如货币合约中提供token的数量。

apply

apply是message处理器,它监听所有输入的messages并根据函数中的规定进行反馈。apply函数需要两个输入参数,codeaction

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, toquantity等字段。这些字段都有对应的类型:account_nameuint64account_name 是一个用base32编码来表示uint64的内置类型。 要了解更多的可用内置类型,请点击这里.

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

上述types列表内,我们定义了一系列现有类型的别名。这里,我们把account_name定义为name的别名。

3. 清单

在开始EOS智能合约开发之前,我们需要搞清下面的内容:

构建最新的版本

请确认您环境中的是最新的版本,您才能获取到eoscppeosc这些对于您开发非常重要的工具。 如何获取最新构建版本可以在 环境 章节找到。

一旦您安装了最新版本的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导入到钱包。

了解合约

现在我们已经部署了合约,任何人都可以用eoscget 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接受一个有fromtoquantity字段的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"
    ],  
    ...
}

了解合约

现在我们已经部署了合约,任何人都可以用eoscget 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"]
      }
  ]
}

注释

  • 此合约接受createrestartclosemove的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走,用moveaction指定需要填入哪行哪列的格子来完成一次移动。
$ 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 (CC++)来完成。 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在验证、生产区块及区块应用的阶段都会被应用一次。

推荐阅读更多精彩内容