美文网首页区块链区块链学习
EOS开发系列(三)编写一个智能合约

EOS开发系列(三)编写一个智能合约

作者: 王巨 | 来源:发表于2017-10-02 14:46 被阅读1661次

    在上一章,我们讨论了如何部署一个智能合约到区块链上,并演示了如何进行新合约里面的交易。今天我们从头开始,演示一下如何编写一个简单的智能合约,同时在过程中尽可能讲清楚智能合约的组成。

    准备环境

    之前我们在运行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

    相关文章

      网友评论

      • 每皮1024:请问为什么可以向合约发送信息啊($ eosc push message inita hello '"abcd"' --scope inita),我看我的abi里面只action部分只支持transfer,我照着上面的代码根据我的实际情况修改会产生
        Failed with error: Parse Error (4)
        Unexpected char '97' in "abcd"
        我和你唯一的区别是我用inita又授权建立一个helloworld账户,然后把合约绑定在这个账户上而不是inita上
      • 李大狗腿:和楼上遇到的同样的问题……用的是最新的版本。应该用Dawn1.1吗?
        李大狗腿:我觉得是因为eosd的conf配置文件不对所导致的,所以想看一下您的配置文件。
        李大狗腿:如果方便的话想加微信请教——微信号:197626581
      • Simth:执行这个命令eosc set contract inita hello.wast hello.abi
        报错信息如下
        Reading WAST...
        Assembling WASM...
        Publishing contract...
        1837236ms main.cpp:1165 main ] Failed with error: Assert Exception (10)
        status_code == 200: Error code 401
        : {"code":401,"message":"UnAuthorized","details":"signatures do not satisfy declared authorizations (3030002)\nTransaction declares authority '{\"account\":\"inita\",\"permission\":\"active\"}', but does not have signatures for it.\n"}
        1024猿:你这个问题是钱包未解锁,你先解锁钱包再执行就可以了。
        每皮1024:不知道你们还有没有碰到这样的问题,我是先关闭eosd,然后$ ./eosd --skip-transaction-signatures --genesis-json path/to/genesis.json,接着记得重新开启解锁钱包就可以了。网上搜的,EOS还不知道具体原因,有知道的麻烦讲解一下,谢谢。
        王巨:@Simth 你用哪个版本编译的?是Dawn1.1还是Dawn2.0?

      本文标题:EOS开发系列(三)编写一个智能合约

      本文链接:https://www.haomeiwen.com/subject/djwzextx.html