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

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

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();

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

你可能感兴趣的:(EOS开发系列(三)编写一个智能合约)