EOS开发系列(三)编写一个智能合约
在上一章,我们讨论了如何部署一个智能合约到区块链上,并演示了如何进行新合约里面的交易。今天我们从头开始,演示一下如何编写一个简单的智能合约,同时在过程中尽可能讲清楚智能合约的组成。
准备环境
之前我们在运行eosd和eosc时都是编译好的环境下进行的。今天发现根据文档上的描述运营eoscpp -n hello时会出现以下错误:
cp: /usr/local/share/skeleton/.: No such file or directory
最后发现需要在build目录下运行命令:sudo make install
运行完成后,eos的工具会被安装到系统目录下,命令就可以使用了。
Hello World
使用命令:
eoscpp -n hello
在此之前可以先创建一个目录,我是在~目录下创建了一个eos_contract的目录,进入该目录后执行上述命令,会得到一个hello的目录,进入该目录后可以看到三个文件:
hello.abi hello.hpp hello.cpp
这算是一个模板了,该模板是可以直接编译。
我们先看一下hello.cpp的内容
#include
/**
* 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() {
eos::print( "Init World!\n" );
}
/// The apply method implements the dispatch of events to this contract
void apply( uint64_t code, uint64_t action ) {
eos::print( "Hello World: ", eos::Name(code), "->", eos::Name(action), "\n" );
}
} // extern "C"
这里面定义了两个方法:init()和apply(uint64_t code, uint64 action)。根据文档的介绍我们可以了解到,EOS对合约的处理是基于消息和状态机的,那这两个方法就不难理解了,init()就是做初始化用的,而apply是处理所有消息的入口。理论上这里可以实现任何业务逻辑。
将上面的合约编译并部署到区块链上:
$ eoscpp -o hello.wast hello.cpp
编译非常简单,编译出来的文件后缀为wast是wasm的文本形式
$ eosc set contract ${account} hello.wast hello.abi #这里的account我使用的是inita
eosc set contract inita hello.wast hello.abi
Reading WAST...
Assembling WASM...
Publishing contract...
{
"transaction_id": "08888ade6813f75630ff8cc635f6f854dba50795a490024b37d01ce76533ab07",
"processed": {
"refBlockNum": 13925,
"refBlockPrefix": 1540220911,
"expiration": "2017-10-02T03:05:42",
"scope": [
"eos",
"inita"
],
"signatures": [
"1f463e298f4679c5752a9c90c044600e9442e2788dd63cb0acde5fcac5a13d7718732448241a04b31c915a7eb1fd32e8c5eabab5b7be90e098f232da14137679d5"
],
"messages": [{
"code": "eos",
"type": "setcode",
"authorization": [{
"account": "inita",
"permission": "active"
}
],
"data": "000000008040934b0000f1010061736d0100000001110460017f0060017e0060000060027e7e00021b0203656e76067072696e746e000103656e76067072696e7473000003030202030404017000000503010001071903066d656d6f7279020004696e69740002056170706c7900030a20020600411010010b17004120100120001000413010012001100041c00010010b0b3f050041040b04504000000041100b0d496e697420576f726c64210a000041200b0e48656c6c6f20576f726c643a20000041300b032d3e000041c0000b020a000029046e616d6504067072696e746e0100067072696e7473010004696e697400056170706c790201300131010b4163636f756e744e616d65044e616d6502087472616e7366657200030466726f6d0b4163636f756e744e616d6502746f0b4163636f756e744e616d6506616d6f756e740655496e743634076163636f756e740002076163636f756e74044e616d650762616c616e63650655496e74363401000000b298e982a4087472616e736665720100000080bafac6080369363401076163636f756e7400076163636f756e74"
}
],
"output": [{
"notify": [],
"deferred_transactions": []
}
]
}
}
执行完成后你会得到以上的返回,同时如果你观察eosd的窗口,你会得到下面的输出:
Init World!
Init World!
Init World!
主要,这里你会发行init方法被执行了3遍,根据文档,我们可以发现EOS在deploy一个智能合约时会执行三遍,都发生了什么呢:
1、eosd 收到了一个新的交易
-创建一个临时的session
-尝试应用该交易
-成功执行并打印“Init World!”
-或者失败撤销所有改动
2、eosd开始生产一个区块
-撤销所有未决状态
-将所有的交易放到这个构建的块内
-第二次打印“Init World!”
-结束创建块
-撤销所有在创建区块期间做的临时变更
3、eosd像从网络接收到块一样将产生的块放到链上
-第三次打印“Init World!”
这样之后,这个hello合约就可以接收消息了,我们向合约发送一个消息:
$ eosc push message inita hello '"abcd"' --scope inita
{
"transaction_id": "9ee98e38d6f5554b3d47b0dfa6d444dcdb058c6ed793d44f0187cc56f725a955",
"processed": {
"refBlockNum": 14079,
"refBlockPrefix": 380957384,
"expiration": "2017-10-02T03:13:24",
"scope": [
"inita"
],
"signatures": [],
"messages": [{
"code": "inita",
"type": "hello",
"authorization": [],
"data": "abcd"
}
],
"output": [{
"notify": [],
"deferred_transactions": []
}
]
}
}
此时如果你观察eosd的输出你会发现输出了
Hello World: inita->hello
Hello World: inita->hello
Hello World: inita->hello
再一次输出了三次,说明我们的合约被执行了三次并且撤销了两次。
关于消息名称的限制
消息名称是保存在一个64位的整形内。这意味着它们前12个字符被限制在(a-z,1-5,和'.')之间。如果有第十三个字符,那他们就转换为前16个字符被限制在('.'和a-p)之间.
ABI - 应用二进制接口
还记得我们使用eoscpp -n hello 产生的文件吗?其中有一个叫hello.abi,我们可以看一下该文件的内容:
{
"types": [{
"newTypeName": "AccountName",
"type": "Name"
}
],
"structs": [{
"name": "transfer",
"base": "",
"fields": {
"from": "AccountName",
"to": "AccountName",
"amount": "UInt64"
}
},{
"name": "account",
"base": "",
"fields": {
"account": "Name",
"balance": "UInt64"
}
}
],
"actions": [{
"action": "transfer",
"type": "transfer"
}
],
"tables": [{
"table": "account",
"type": "account",
"indextype": "i64",
"keynames" : ["account"],
"keytypes" : ["Name"]
}
]
}
这是hello的abi,它内部定义了交易类型transfer,该abi后面会详细讲解,而且最终eos会提供工具来根据cpp代码来产生abi,因此暂时不做过多讲解。
根据该transfer交易类型,我们可以发送以下消息:
eosc push message inita transfer '{"from":"currency","to":"inita","amount":50}' --scope initc
你将得到类似下面的信息:
{
"transaction_id": "fca3f878f3c1b7911a7d48b47f43c542bd797bab8b260d209232a73213c2a60a",
"processed": {
"refBlockNum": 14392,
"refBlockPrefix": 4086597457,
"expiration": "2017-10-02T03:29:03",
"scope": [
"initc"
],
"signatures": [],
"messages": [{
"code": "inita",
"type": "transfer",
"authorization": [],
"data": {
"from": "currency",
"to": "inita",
"amount": 50
},
"hex_data": "00000079b822651d000000008040934b3200000000000000"
}
],
"output": [{
"notify": [],
"deferred_transactions": []
}
]
}
}
在eosd上还会有输出:
Hello World: inita->transfer
Hello World: inita->transfer
Hello World: inita->transfer
处理Transfer消息的参数
根据ABI定义
"fields": {
"from": "AccountName",
"to": "AccountName",
"amount": "UInt64"
}
我们同时知道AccountName -> Name -> UInt64,所以在代码里可以定义相同的数据结构
structtransfer {
uint64_tfrom;
uint64_tto;
uint64_tamount;
};
修改hello.cpp代码如下:
#include <hello.hpp>
extern"C"{
voidinit() {
eos::print("Init World!\n");
}
structtransfer {
uint64_tfrom;
uint64_tto;
uint64_tamount;
};
voidapply(uint64_tcode,uint64_taction ) {
eos::print("Hello World: ",eos::Name(code),"->",eos::Name(action),"\n");
if( action ==N(transfer) ) {
transfer message;
static_assert(sizeof(message) == 3*sizeof(uint64_t),"unexpected padding");
autoread =readMessage( &message,sizeof(message) );
assert( read ==sizeof(message),"message too short");
eos::print("Transfer ", message.amount," from ",eos::Name(message.from)," to ",eos::Name(message.to),"\n");
}
}
}// extern "C"
在apply方法中增加了对消息的处理,重新进行编译部署再次发送transfer消息,在eosd中会有如下输出:
Hello World: inita->transfer
Transfer 50 from currency to inita
Hello World: inita->transfer
Transfer 50 from currency to inita
Hello World: inita->transfer
Transfer 50 from currency to inita
使用c++的api读消息
上面的cpp代码实际上使用的是c api,可以改为如下代码,效果是一致的:
#include <hello.hpp>
extern"C"{
voidinit() {
eos::print("Init World!\n");
}
structtransfer {
eos::Namefrom;
eos::Nameto;
uint64_tamount;
};
voidapply(uint64_tcode,uint64_taction ) {
eos::print("Hello World: ",eos::Name(code),"->",eos::Name(action),"\n");
if( action ==N(transfer) ) {
auto message = eos::currentMessage<transfer>();
eos::print("Transfer ", message.amount," from ", message.from," to ", message.to,"\n");
}
}
}// extern "C"
增加认证和异常
eos::requireAuth( message.from );//增加发送端的认证
assert( message.amount > 0,"Must transfer an amount greater than 0");//增加对对金额的校验。
这两个就不在演示了。
参考资料:https://eosio.github.io/eos/md_contracts_eoslib_tutorial.html