20181208_EOS智能合约-faucet

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容

  • 学习区块链,最刺激的莫过于发币,第一篇文章里介绍了如何搭建EOS开发环境,第二篇文章我们已经介绍了如何部署调用合约...
    P叔阅读 4,686评论 5 6
  • 8月8日 爱出者爱返,福往者福来。这几天感受到老家的朋友对我的爱,感触很多,出来二十多年了,以前每次回家都没几个人...
    成功的种子阅读 122评论 0 0
  • 后天才是中秋,但是大部分人已经开始放假了。当然,这个时候,我肯定是在上课了。。 因为对中秋其实真的没什么感觉。从0...
    ipirate阅读 167评论 2 1
  • 这一年,新添一个宝贝,但是自己的生活陷入了一片烦乱和忙碌,没有任何起色。随着朗朗进入小学状态不太好,我分分钟处于崩...
    蔷薇如雪阅读 189评论 0 1