20181208_EOS智能合约-faucet

作者: mingmingz | 来源:发表于2018-12-08 16:43 被阅读12次

    eosio 水龙头(eosfaucet)合约

    ps:本教程算是进阶教程,需要有一定的eosio合约开发基础.请先参阅官方文档 https://developers.eos.io/ 或 我翻译的过时的教程进行基础学习
    完整代码可于github查看
    cdt版本:1.4.1

    我们将要编写一个自己的水龙头合约,通过用户请求,我们将会给用户发送EOS,并且请求会有限制,每分钟只能请求一次.

    通过本文你可以学到合约中的数据持久化(Data Persistence),内联函数(inline_action)的使用, assetsymbol对象的构建与使用,通过其他合约调用eosio.tokentransfer.

    第一步 创建文件夹

    进入自己存放合约的文件夹:

    cd /Users/zhong/coding/CLion/contracts
    

    创建一个文件夹以存放我们将要编写的合约:

    mkdir eosfaucet
    cd eosfaucet
    
    #pwd
    /Users/zhong/coding/CLion/contracts/eosfaucet
    

    第二步 创建并打开一个cpp

    touch eosfaucet.cpp
    

    然后用你喜欢的编辑器打开它(或打开该文件夹)

    第三步 编写合约类以及include EOSIO

    #include <eosiolib/eosio.hpp>
    #include <eosiolib/asset.hpp>
    
    using namespace eosio;
    
    CONTRACT eosfaucet : public contract {
    public:
        
        eosfaucet(name receiver, name code, datastream<const char *> ds)
            : contract(receiver, code, ds) {}
            
    private:
    
    };
    

    当使用C++时,第一个需要创建public method应该是constructor,constructor代表初始化该合约.

    EOSIO合约继承了 contract 类,使用合约的 code 以及 receiver 来初始化父合约.这里最重要的一个参数是code,它是一个区块链上的account,该合约将部署到该account上.

    第四步 创建表的数据结构

    private:的后一行开始加入以下代码:

    TABLE limit {
        name name;
        uint32_t time = 0;
    
        uint64_t primary_key() const { return name.value; }
    };
    

    我们的表很简单,就两个字段,一个name记录是哪个用户,一个time记录时间,使用name.value作为主键,因为它的类型是uint64_t,所以索引起来会很快.

    第五步 配置 Multi-Index Table

    在刚刚定义好的table后加入以下代码:

    typedef eosio::multi_index<"limit"_n, limit> limit_table;
    

    通过这行代码我们能定义好一个名字为limit_table 的 multi-index table.

    eosio::multi_index<> 中的第一个参数"limit"_n是表的名字,_n是一个宏,表的名称不能有下划线而且好像有字符长度限制,可以自己把表名写长一些看看报什么错.

    eosio::multi_index<> 中的第二个参数limit是我们这张表存储的row的类型

    limit_table是我们这张表的定义

    目前来说我们eosfaucet.cpp中的代码是这样的:

    #include <eosiolib/eosio.hpp>
    #include <eosiolib/asset.hpp>
    
    using namespace eosio;
    
    CONTRACT eosfaucet : public contract {
    public:
    
        eosfaucet(name receiver, name code, datastream<const char *> ds)
            : contract(receiver, code, ds) {}
    
    private:
        TABLE limit {
            name name;
            uint32_t time = 0;
    
            uint64_t primary_key() const { return name.value; }
        };
        typedef eosio::multi_index<"limit"_n, limit> limit_table;
    
    };
    

    第六步 初始化 multi-index table

    在刚刚定义的limit_table后进行一个对象声明:

    ...
    limit_table _limit_table;
    

    然后回到constructor进行初始化:

      eosfaucet(name receiver, name code, datastream<const char *> ds)
            : contract(receiver, code, ds), _limit_table(_code, _code.value) {}
    

    到这里为止,eos合约中的multi-index table的存储范围还需要解释说明一下,它的构造函数(multi_index( name code, uint64_t scope ))需要传入两个参数,codescope:

    code是指该表属于哪个account

    scope是指代码层次结构中的范围标识符

    那回到我们的构造函数后面_limit_table(_code, _code.value),我们构造limit_table时决定了该表属于_code,当前的_codeeosfaucet, _code.valueeosfaucet的uint64_t值.

    为什么需要知道这两个参数的含义?因为在查找表内容的时候需要指定他们,比如通过cleos命令:

    cleos get table [OPTIONS] account scope table
    

    命令中的account就是code,scope就是scope, table 是先前声明好的,例如我们例子中的"limit"_n,那么如果我们想通过cleos查找该表,就使用:

    cleos get table eosfaucet eosfaucet limit
    

    第七步 添加数据到表中

    在构造函数后声明一个action:

    ACTION get(name user) {
    
    }
    

    该action接受一个参数:user ,我们将通过该参数来查找limit表,以确认是否可以给该用户发钱.

    ACTION get(name user) {
        auto iterator = _limit_table.find(user.value);
    
        if (iterator != _limit_table.end()) {
            //用户在表里
    
        } else {
            //用户没在表里
    
    
        }
    }
    

    在表中查找该用户,获取迭代器_limit_table.find(user.value), 如果iterator不等于_limit_table.end()则说明该用户之前通过该函数取过钱,_limit_table.end()代表比该table最后一行记录还大一条的迭代器,也就是说该行是没存记录的,也就是没有通过该值找到迭代器.

    我们先处理else的情况:

    //用户没在表里
    _limit_table.emplace(get_self(), [&](auto & row) {
        row.name = user;
        row.time = now() + 60;
    
        //给用户发送EOS
        
    });
    

    如果是else,说明用户没领过钱,我们使用emplace添加到表中.

    emplace的第一个参数是payer,它是指这条记录的RAM花费将由谁来支付,我们使用get_self(),代表由合约本身的account来支付.

    记录的数据需要将用户记录上,并将时间记录上,因为我们不允许用户太频繁的从该合约取钱,限制为一分钟一次.

    now()取出来的值是以秒为单位的uint64_t,我们将其加60则是指多一分钟.

    到这位置我们先编译合约来跑一跑吧!

    第八步 使用DISPATCH

    在文件末尾加上:

    EOSIO_DISPATCH( eosfaucet, (get) )
    

    第一个参数是当前合约的名字,第二个参数是action列表

    我们的合约现在是这样的:

    #include <eosiolib/eosio.hpp>
    #include <eosiolib/asset.hpp>
    
    using namespace eosio;
    
    CONTRACT eosfaucet : public contract {
    public:
    
        eosfaucet(name receiver, name code, datastream<const char *> ds)
                : contract(receiver, code, ds), _limit_table(_code, _code.value) {}
    
        ACTION get(name user) {
            auto iterator = _limit_table.find(user.value);
    
            if (iterator != _limit_table.end()) {
                //用户在表里
    
            } else {
                //用户没在表里
                _limit_table.emplace(get_self(), [&](auto & row) {
                    row.name = user;
                    row.time = now() + 60;
    
                    //给用户发送EOS
    //                sendEOS(user);
                });
            }
        }
    
    private:
        TABLE limit {
            name name;
            uint32_t time = 0;
    
            uint64_t primary_key() const { return name.value; }
        };
    
        typedef eosio::multi_index<"limit"_n, limit> limit_table;
        limit_table _limit_table;
    
    };
    
    EOSIO_DISPATCH( eosfaucet, (get) )
    

    第九步 创建合约account,编译,部署

    • 创建account
    cleos create account eosio eosfaucet EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV
    

    response:

    executed transaction: 2fa15186334b6a8792bd5df708c68daa5dbbedd1cf42fe750229a1b442a352a4  200 bytes  391 us
    #         eosio <= eosio::newaccount            {"creator":"eosio","name":"eosfaucet","owner":{"threshold":1,"keys":[{"key":"EOS6MRyAjQq8ud7hVNYcfnV...
    warning: transaction executed locally, but may not be confirmed by the network yet    ] 
    

    我使用的是自己的测试网络,我通过eosio这一account来创建新的account,创建的account name是eosfaucet,将会持有该account的publicKey为EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV.

    • 编译合约
    #进入eosfaucet.cpp的文件夹
    cd /Users/zhong/coding/CLion/contracts/eosfaucet
    #编译
    eosio-cpp -o eosfaucet.wasm eosfaucet.cpp --abigen
    

    我们使用eosio-cpp来编译eosfaucet.cpp文件,-o eosfaucet.wasm声明输出的内容写到eosfaucet.wasm中,

    --abigen同时告诉eosio-cpp我们还需要生成abi文件.如果一步一步跟着来应该不会报错,没报错就可以部署合约了.

    • 部署合约
    cleos set contract eosfaucet /Users/zhong/coding/CLion/contracts/eosfaucet eosfaucet.wasm eosfaucet.abi
    

    如果提示你的钱包未解锁请先解锁

    response:

    Reading WASM from /Users/zhong/coding/CLion/contracts/eosfaucet/eosfaucet.wasm...
    Publishing contract...
    executed transaction: 8e6a7e923a1c9ffcb0037b974b2a5803a69f5f62896546bcdf0d18873bf19a5b  2768 bytes  652 us
    #         eosio <= eosio::setcode               {"account":"eosfaucet","vmtype":0,"vmversion":0,"code":"0061736d0100000001590f60027f7e00600000600001...
    #         eosio <= eosio::setabi                {"account":"eosfaucet","abi":"0e656f73696f3a3a6162692f312e3100020367657400010475736572046e616d65056c...
    warning: transaction executed locally, but may not be confirmed by the network yet    ] 
    

    第十步 调用 action

    cleos push action eosfaucet get '["alice"]' -p alice@active
    

    我们将要调用eosfaucet的action,调用名称为get的action,我们传的参数是["alice"],使用的用户权限是alice@active

    response:

    eosfaucet <= eosfaucet::get               {"user":"alice"}
    

    能看到这行说明我们调用已经成功了

    第十一步 让合约能transfer EOS

    现在我们实现以下在get函数中还没完成的sendEOS函数.

    我们先在合约声明上定义symbol对象:

    #define EOS_SYMBOL symbol("EOS",4)
    

    我们定义了一个EOS_SYMBOLsymbol("EOS",4),"EOS"是该symbol的名称,4是该symbol的小数精度,例如我们定义好的symbol就会是1.0000 EOS.

    然后在private:后添加以下代码:

    private:
    
        void sendEOS(name user){
            asset money = asset(10, EOS_SYMBOL);
            action(
                    permission_level{get_self(), "active"_n},
                    "eosio.token"_n,
                    "transfer"_n,
                    std::make_tuple(get_self(), user, money, std::string("memo"))
            ).send();
        }
    

    我们先使用定义好的EOS_SYMBOL构建一个asset对象,asset(10, EOS_SYMBOL)中的10代表该asset的大小,将其除以精度的位数就能得到asset的大小,例如我们构造的money的结果是0.0010 EOS.

    然后定义action:

     permission_level{get_self(), "active"_n},
                    "eosio.token"_n,
                    "transfer"_n,
                    std::make_tuple(get_self(), user, money, std::string("memo"))
    

    permission_level{get_self(), "active"_n} 使用自己的account,权限是active

    "eosio.token"_n 调用的合约是eosio.token

    "transfer"_n 调用的函数是transfer
    std::make_tuple(get_self(), user, money, std::string("get eos from faucet")) transfer需要的四个参数

    回到get函数中将注释打开:

    ...
        else {
                //用户没在表里
                _limit_table.emplace(get_self(), [&](auto & row) {
                    row.name = user;
                    row.time = now() + 60;
    
                    //给用户发送EOS
                    sendEOS(user);
                });
            }
    

    第十二步 再次编译合约,部署及调用get函数

    eosio-cpp -o eosfaucet.wasm eosfaucet.cpp --abigen
    
    cleos set contract eosfaucet /Users/zhong/coding/CLion/contracts/eosfaucet eosfaucet.wasm eosfaucet.abi
    

    重新部署好eosfaucet合约后我们还需要一步,就是先给这个合约一些EOS,不然它也没法给其他人转钱.给它转钱的命令我就不写了,你应该是会的.

    由于我们使用过alice来调用get,表里存在她的记录,所以我们需要换一个账号来调用.

    cleos push action eosfaucet get '["bob"]' -p bob@active
    

    response:

    Error 3090003: Provided keys, permissions, and delays do not satisfy declared authorizations
    Ensure that you have the related private keys inside your wallet and your wallet is unlocked.
    Error Details:
    transaction declares authority '{"actor":"eosfaucet","permission":"active"}', but does not have signatures for it under a provided delay of 0 ms, provided permissions [{"actor":"eosfaucet","permission":"eosio.code"}], provided keys [], and a delay max limit of 3888000000 ms
    

    你会看到如上所示的错误,这是因为在eos中,一个合约不能直接调用其他合约的action,它缺少permission.如果想要调用其他合约的action,我们需要通过以下命令给它权限:

    cleos set account permission eosfaucet active '{"threshold": 1,"keys": [{"key": "EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV","weight": 1}], "accounts": [{"permission":{"actor":"eosfaucet","permission":"eosio.code"},"weight":1}]}' -p eosfaucet@owner
    

    这行命令的意思是我们通过-p eosfaucet@ownereosfaucet active一定的权限,什么权限呢,{"permission":{"actor":"eosfaucet","permission":"eosio.code"},"weight":1}让eosfaucet拥有eosio.code的权限.有了这个权限,才能在合约内调用其他合约的内联函数.

    再次调用:

    cleos push action eosfaucet get '["bob"]' -p bob@active
    

    response:

    zhong:eosfaucet zhong$ cleos push action eosfaucet get '["bob"]' -p bob@active
    executed transaction: 199157afa21e65bf5702b235db673c44eb14690ce8338f1e2db70661acd34b95  104 bytes  526 us
    #     eosfaucet <= eosfaucet::get               {"user":"bob"}
    #   eosio.token <= eosio.token::transfer        {"from":"eosfaucet","to":"bob","quantity":"0.0010 EOS","memo":""}
    #     eosfaucet <= eosio.token::transfer        {"from":"eosfaucet","to":"bob","quantity":"0.0010 EOS","memo":""}
    #           bob <= eosio.token::transfer        {"from":"eosfaucet","to":"bob","quantity":"0.0010 EOS","memo":""}
    

    现在我们就能从eosfaucet中获取到eos了

    第十三步 完善get action

     ACTION get(name user) {
         auto iterator = _limit_table.find(user.value);
    
         if (iterator != _limit_table.end()) {
             //用户在表里
             auto find = _limit_table.get(user.value);
    
             //判断等待时间
             eosio_assert(find.time < now(), "you can not get EOS yet");
    
             _limit_table.modify(iterator, get_self(), [&](auto & row) {
                 row.time = now() + 60;
                 sendEOS(user);
             });
         }
    

    回到if判断中,先通过_limit_table.get(user.value)获取row对象,即我们定义好的limit Table.判断该row的time是否小于当前时间,如果小于,才能给他发EOS.

    由于又一次的领取,我们需要修改表中的数据,modify需要三个参数,第一个参数是将要修改的iterator,第二个参数是RAM的payer,第三个参数是修改数据的回调函数.

    由于我们的primary_key不用变,只需要将它的领取时间再添加一分钟即可.

    第十四步 再次编译合约,部署及调用get函数

    eosio-cpp -o eosfaucet.wasm eosfaucet.cpp --abigen
    
    cleos set contract eosfaucet /Users/zhong/coding/CLion/contracts/eosfaucet eosfaucet.wasm eosfaucet.abi
    

    连续调用get函数:

    cleos push action eosfaucet get '["bob"]' -p bob@active
    

    第一次会获取成功,但第二次会得到Error(assert的详细提醒需要在打开nodeos的时候添加--verbose-http-errors指令):

    Error 3050003: eosio_assert_message assertion failure
    Error Details:
    assertion failure with message: you can not get EOS yet
    

    到此,我们的水龙头合约就简单的完成了

    相关文章

      网友评论

        本文标题:20181208_EOS智能合约-faucet

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