Zihao

Make small but daily progress

0%

EOS合约基础教程--智能合约简介

1 EOS智能合约和以太坊合约的区别

EOS的智能合约里面有一个action(动作)和transaction(交易)的概念。
在以太坊中,基本上只有transaction的概念,如果我只要执行一种操作,而且是只读操作,就不需要签名。如果需要划资金,有一些写的操作,那就需要用户用公钥对这个操作去签名,然后pos的一个transaction,这是以太坊的概念。

对于EOS,它多了一个action的概念,action其实它也是对一个智能合约中的某个函数的调用。transaction是由一个或者多个action组合而成的关系,就是在一个transaction里,可以包含多个action,这样你可以在一个transaction里签一次名,就可以调多个函数,做一组操作。

2 EOS 合约

2.1 基础知识

EOS智能合约通过messages 及 共享内存数据库(比如只要一个合约被包含在transaction的读取域中with an async vibe,它就可以读取另一个合约的数据库)相互通信。异步通信导致的spam问题将由资源限制算法来解决。下面是两个在合约里可定义的通信模型:

  • Inline 内联通信模型,采用请求其他操作的形式,需要作为调用操作的一部分执行。Inline保证执行当前的transaction或unwind;无论成功或失败都不会有通知。Inline 操作的scopes和authorities和原来的transaction一样。
  • Deferred 延迟通信模型,采用发送到对等交易的动作通知的形式, 根据生产者的判断,延迟的操作最多可以安排在稍后的时间运行由区块生产者来安排。无法保证将执行延期操作,结果可能是传递通信结果或者只是超时。延期交易具有发送合约的权限,交易可以取消延期交易。

2.2 目录结构

2.2.1 创建智能合约文件

eoscpp将创造三个智能合约文件,他们是你起步开发的框架。

1
eoscpp -n ${contract}

以上将在’./${project}’文件夹下创建一个新项目,包含三个文件:

1
${contract}.abi ${contract}.hpp ${contract}.cpp

2.2.2 HPP

HPP是包含CPP文件所引用的变量、常量、函数的头文件。

2.2.3 CPP

CPP文件是包含合约功能的源文件。

2.2.4 WAST

想要部署到EOS.IO区块链上的任何程序都需要先编译成WASM格式。这是区块链接受的唯一格式。
一旦您完成了CPP文件的开发,您可以用eoscpp工具将它编译成一个文本版本的WASM (.wast) 文件。

1
$ eoscpp -o ${contract}.wast ${contract}.cpp

2.2.5 ABI

Application Binary Interface (ABI)是一个基于JSON的描述文件,是关于转换JSON和二进制格式的用户actions的。ABI还描述了如何将数据库状态和JSON的互相转换。一旦您通过ABI描述了您的合约,开发者和用户就能够用JSON和您的合约无缝交互了。
ABI文件可通过eoscpp工具从HPP文件生成:

1
$ eoscpp -g ${contract}.abi ${contract}.hpp

这里是一个合约的骨架ABI的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
{
"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。

1
2
3
4
5
6
7
8
9
10
11
...
"structs": [{
"name": "transfer",
"base": "",
"fields": {
"from": "account_name",
"to": "account_name",
"quantity": "uint64"
}
},{
...

这部分包括fromto 和 quantity等字段。这些字段都有对应的类型:account_nameuint64account_name 是一个用base32编码来表示uint64的内置类型。

1
2
3
4
5
6
7
8
{
"types": [{
"new_type_name": "account_name",
"type": "name"
}
],
...

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

2.3 合约内容结构

虽然 EOS 合约是一个 C++ 类,但,一个 EOS 合约又不仅仅是一个普通的 C++ 类,它必须符合一定的条件才能成为合约:

2.3.1 类的构造方法必须接受且只能接受三个参数

1
contractName( name receiver, name code, datastream<const char*> ds );

参数说明

参数 类型 说明
receiver eosio::name 合约的名字,准确的说,是部署当前合约的账号
code eosio::name 调用合约 动作的账号,一般是当前合约所在的账号
ds datastream<const char*> 保存了那些使用 eosio::ignore 忽略的字段

注意: ds 参数目前没啥大的作用,但是,如果合约想要对未由调度程序处理的操作字段进行进一步反序列化(例如,要重新使用 ignore 忽略的字段),这会派上用场。

2.3.2 导出合约中的动作

任何一个合约,都必须定义一个 EOSIO_DISPATCH( hello, (hi)) 用于导出合约中的动作,这样,EOS 执行环境才知道该合约可以接受哪些动作

EOSIO_DISPATCH() 是一个宏定义,它的原型如下

1
#define EOSIO_DISPATCH( TYPE, MEMBERS ) ...

参数说明

参数 说明
TYPE 合约的名字,注意,是合约的名字,而不是合约所在的账号的名字
MEMBERS 由小括号扩起来的 0 个或多个动作名,比如 (hi) ,每个动作都是一个 C++ public 方法

2.3.3 范例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <eosiolib/eosio.hpp>

using namespace eosio;
using namespace std;

class hello
{
public:
hello( name receiver, name code, datastream<const char*> ds )
{
print("receiver:");
print(receiver);
print(" code:");
print(code);
print(" ds length:");
print(ds.remaining());

if( ds.remaining() > 0 ){
std::string data;
ds >> data;
print(" ds:");
print(std::string(data));
}
}

[[eosio::action]]
void hi(){
}
};

EOSIO_DISPATCH(hello,(hi))

然后使用下面的命令来编译

1
eosio-cpp -o hello.wasm hello.cpp --abigen

编译结果如下

1
2
3
Warning, empty ricardian clause file
Warning, empty ricardian clause file
Warning, action <hi> does not have a ricardian contract

上面这几个警告是可以忽略的,因为我们没有给合约和方法添加 李嘉图 (Ricardian) 说明文件

李嘉图 (Ricardian) 说明文件如何编写,我们会在以后的章节中讲解

接着使用下面的命令来部署合约

1
cleos set contract hello ../hello -p hello

运行结果如下

1
2
3
4
5
Reading WASM from ../hello/hello.wasm...
Skipping set abi because the new abi is the same as the existing abi
Publishing contract...
executed transaction: 683503d8936d27ffc6fdd1b604782757c8bedcae899c327b34475272ade24b00 2808 bytes 588 us
# eosio <= eosio::setcode {"account":"hello","vmtype":0,"vmversion":0,"code":"0061736d0100000001681260017f006000006000017f6002...

2.3.4 合约调用

2.3.4.1 使用当前合约账号来执行合约,且不传递任何参数

1
2
3
4
cleos push action hello hi '[]' -p hello
executed transaction: 41ad0f8d064a6980151d979666df4103fe0364033fdfee6945cd40b7f39df70b 96 bytes 212 us
# hello <= hello::hi ""
>> receiver:hello code:hello ds length:0

2.3.4.2 使用当前合约账号来执行合约,且一些参数

1
2
3
4
cleos push action hello hi '["hello","ni","hao"]' -p hello
executed transaction: 4723aa8ec187e47ead46580d415ebe5b17fd0849b234fd54e092e08f088c6534 96 bytes 220 us
# hello <= hello::hi ""
>> receiver:hello code:hello ds length:0

2.3.4.3 使用其它账号来执行合约,不传递任何参数

1
2
3
4
cleos push action hello hi '[]' -p hi
executed transaction: c0c15799a02387078e7e9c9f0ba09f5987cc7b949b6d44ca4be78a606e58afa2 96 bytes 226 us
# hello <= hello::hi ""
>> receiver:hello code:hello ds length:0

2.3.4.4 使用其它账号执行合约,传递一些参数

1
2
3
4
cleos push action hello hi '["hello","ni","hao"]' -p hi
executed transaction: 27a9ade415ecb2f49cb82e2fc44e2c36116d4b5a6fa5fc5c1dfed30a5eab698f 96 bytes 242 us
# hello <= hello::hi ""
>> receiver:hello code:hello ds length:0

从上面的执行结果中可以看出,

  • 如果直接执行合约中的方法,那么 code 和 receiver 都是一样的,都是当前合约所在的账号
  • datastream<const char*> 一般情况下都是空的,也就是说,没有任何数据

2.3.5 合约基础类 eosio::contract

如果我们每写一个合约就要自己动手写一个长长的构造函数,肯定是不开心的,作为程序员,能偷懒就偷懒才是我们的理想。

为此,我们可能希望把包含了三个参数的构造方法写在一个基础的合约里,比如,我们定义一个名为 contract 的基础合约

2.3.5.1 basic.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <eosiolib/eosio.hpp>

using namespace eosio;
using namespace std;

class basic
{
public:
basic( name receiver, name code, datastream<const char*> ds ):
_self(receiver),_code(code),_ds(ds) {}

name _self;
name _code;
datastream<const char*> _ds;
};

然后其它合约在扩展自这个合约,并使用 using 关键字来引用基础类的构造方法

2.3.5.2 hello.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <eosiolib/eosio.hpp>

#include "basic.cpp"

using namespace eosio;
using namespace std;

class hello:public basic {
public:
using basic::basic;

[[eosio::action]]
void hi(){
print("receiver:");
print(_self);
print(" code:");
print(_code);
print(" ds length:");
print(_ds.remaining());

if( _ds.remaining() > 0 ){
std::string data;
_ds >> data;
print(" ds:");
print(std::string(data));
}
}
};

EOSIO_DISPATCH(hello,(hi))

然后编译、部署、执行合约,会得到相同的结果

1
2
3
4
cleos push action hello hi '["hello","ni","hao"]' -p hi
executed transaction: 040514a09eb7fe268d5ce7d23a38080505ab73cdf72e3bdf47eff7228f2e1747 96 bytes 225 us
# hello <= hello::hi ""
>> receiver:hello code:hello ds length:0

EOS 官方也考虑到了这一点,创建了一个合约基础类 contract,并放在命名空间 eosio 下,头文件为 <eosiolib/contract.hpp>

该文件在 Github 上的地址为 https://github.com/EOSIO/eosio.cdt/blob/master/libraries/eosiolib/contract.hpp

同时,#include <eosiolib/eosio.hpp> 头文件也默认包含了该头文件,因此,只要包含了 #include <eosiolib/eosio.hpp> 就可以了

我们把上面的范例改改,改成官方的合约基础类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <eosiolib/eosio.hpp>

using namespace eosio;
using namespace std;

class hello:public eosio::contract {
public:
using eosio::contract::contract;

[[eosio::action]]
void hi(){
print("receiver:");
print(_self);
print(" code:");
print(_code);
print(" ds length:");
print(_ds.remaining());

if( _ds.remaining() > 0 ){
std::string data;
_ds >> data;
print(" ds:");
print(std::string(data));
}
}
};

EOSIO_DISPATCH(hello,(hi))

3 EOS 动作action

动作 ( action ) 是 EOS 合约基础组成单位。一个动作,在 C++ 合约类中,表示如下

  • 一个动作必须是 C++ 合约类的成员方法
  • 成为动作的成员方法,必须使用 [[eosio::action]] C++11 特性修饰,否则就是一个普通的类成员函数
  • 成为动作的成员方法,访问级别必须是公开的 public
  • 成为动作的成员方法,必须没有任何返回值,也不能返回任何值,也就是说,必须使用 void 作为返回值
  • 成为动作的成员方法,可以接受任意数量的参数
  • 成为动作的成员方法,必须在 EOSIO_DISPATCH 中导出

一个 EOS 链是由多个块 ( block ) 组成的:
image.png

每个块中包含多笔交易:
image.png

交易由action组成:
image.png

4 调试智能合约

现在user官方网站推荐的一个调试方法就是print,把信息打印出来。这个必须要我们搭建本地节点,因为如果没有本地节点,相当于你print打印在别人的节点上,你根本看不到这个打印信息是什么,所以说你必须要搭建一个本地节点。搭建本地节点后,你运行智能合约,就会看到print出来的输出结果。

4.1 debug.hpp

1
2
3
4
5
6
7
8
9
10
11
12
13
#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");
}
};
}

4.2 debug.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <debug.hpp>

extern "C" {

void apply( uint64_t receiver, 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::unpack_action_data<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"

4.3 编译运行

1
2
$ eosiocpp -o debug.wast debug.cpp
$ cleos set contract debug debug.wast debug.abi

实战

部署智能合约

1
$ cleos set contract eosio build/contracts/eosio.bios -p eosio
  • eosio是要部署的账号,就是你用哪个账号去部署智能合约;
  • build/contracts/eosio.bios表示的是路径;
  • eos.bios是生成一个智能合约的目录。

运行Token合约

欢迎关注我的其它发布渠道