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将创造三个智能合约文件,他们是你起步开发的框架。
eoscpp -n ${contract}
以上将在'./${project}'文件夹下创建一个新项目,包含三个文件:
${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) 文件。
$ eoscpp -o ${contract}.wast ${contract}.cpp
2.2.5 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的别名。
2.3 合约内容结构
虽然 EOS 合约是一个 C++ 类,但,一个 EOS 合约又不仅仅是一个普通的 C++ 类,它必须符合一定的条件才能成为合约:
2.3.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()
是一个宏定义,它的原型如下
#define EOSIO_DISPATCH( TYPE, MEMBERS ) ...
参数说明
参数 | 说明 |
---|---|
TYPE | 合约的名字,注意,是合约的名字,而不是合约所在的账号的名字 |
MEMBERS | 由小括号扩起来的 0 个或多个动作名,比如 (hi) ,每个动作都是一个 C++ public 方法 |
2.3.3 范例
#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))
然后使用下面的命令来编译
eosio-cpp -o hello.wasm hello.cpp --abigen
编译结果如下
Warning, empty ricardian clause file
Warning, empty ricardian clause file
Warning, action <hi> does not have a ricardian contract
上面这几个警告是可以忽略的,因为我们没有给合约和方法添加 李嘉图 (Ricardian) 说明文件
李嘉图 (Ricardian) 说明文件如何编写,我们会在以后的章节中讲解
接着使用下面的命令来部署合约
cleos set contract hello ../hello -p hello
运行结果如下
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 使用当前合约账号来执行合约,且不传递任何参数
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 使用当前合约账号来执行合约,且一些参数
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 使用其它账号来执行合约,不传递任何参数
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 使用其它账号执行合约,传递一些参数
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
#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
#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))
然后编译、部署、执行合约,会得到相同的结果
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>
就可以了
我们把上面的范例改改,改成官方的合约基础类
#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
#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
#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 编译运行
$ eosiocpp -o debug.wast debug.cpp
$ cleos set contract debug debug.wast debug.abi
实战
部署智能合约
$ cleos set contract eosio build/contracts/eosio.bios -p eosio
- eosio是要部署的账号,就是你用哪个账号去部署智能合约;
- build/contracts/eosio.bios表示的是路径;
- eos.bios是生成一个智能合约的目录。
运行Token合约
网友评论