2

上一章我们细致地学习了

  • 索引和迭代器的关系;
  • 如何生成和使用索引以及迭代器
  • 介绍了multi_index的相关操作

相信大家对multi_index已经有了比较全面的理论理解以及掌握了一些基础的操作。这一章将会教大家如何完整地构建一个智能合约,并在合约中直观地操作multi_index。

摘要

这一章主要以实操为主,会有较大篇幅的代码,希望大家最好可以照着文章自己操作一遍。

这一章将会以一个简单的智能合约例子,简单了解一个完整的EOS智能合约长什么样。希望大家通过这一章的学习,不仅可以有能力构建一个简单的智能合约,并且对multi_index在EOS智能合约中的重要性,会有更加深刻的认识。

头文件:*.hpp

C++的源代码文件分为两类:头文件(Header file)和源文件(Source code file)。

  • 头文件用于存放对类型定义、函数声明、全局变量声明等实体的声明,作为对外接口;
  • 源程序文件存放类型的实现、函数体、全局变量定义;

我们先来看头文件里的代码:

#include <eosiolib/eosio.hpp>
#include <eosiolib/print.hpp>
#include <string>
using namespace eosio;
using std::string;

最前面按惯例都是import,接着往下看:

class app : public contract {
public:
    using contract::contract;

    app(account_name self)
            : contract(self) {}

    // @abi action
    void hello(const account_name account);

    // @abi action
    void create(const account_name account,
                const string&      username,
                uint32_t           age,
                const string&      bio);

    // @abi action
    void get(const account_name account);

    // @abi action
    void update(const account_name account,
                const string&      username,
                uint32_t           age,
                const string&      bio);

    // @abi action
    void remove(const account_name account);

    // @abi action
    void byage(uint32_t age);

    // @abi action
    void agerange(uint32_t young, uint32_t old);

这里定义了源文件里的方法接口,接下来就到了最核心的multi_index的相关定义:

private:
    // @abi table profile i64
    struct profile {
        account_name    account;
        string          username;
        uint32_t        age;
        string          bio;

        account_name primary_key() const { return account; }
        uint64_t     by_age() const { return age; }

        EOSLIB_SERIALIZE(profile, (account)(username)(age)(bio))
    };

    typedef eosio::multi_index< N(profile), profile,
            // N(name of interface)
            indexed_by< N(age),
                        const_mem_fun<profile, uint64_t, &profile::by_age>
            >
    > profile_table;

};

这里定义了multi_index表的结构 (struct profile),主键以及按年龄的索引定义。(上一章详细讲过)

最后再加上EOSIO_ABI的声明:

EOSIO_ABI(app, (hello)(create)(get)(update)(remove)(byage)(agerange))

这里只需要简单地把所有方法串联在一起就可以了。

上述可以看到hpp头文件里的内容很简单,只包含了最简单的变量和接口的声明。而与之配套的*.cpp文件就要复杂一些,里面对这些接口都做了具体的实现。

源文件

首先肯定是引用头文件:

#include <app.hpp>
void app::hello(account_name account) {
    print("Hello ", name{account});
}

1. 添加数据

void app::create(const account_name account,
                     const string&      username,
                     uint32_t           age,
                     const string&      bio) {
    require_auth(account);

    profile_table profiles(_self, _self);

    auto itr = profiles.find(account);

    eosio_assert(itr == profiles.end(), "Account already exists");

    profiles.emplace(account, [&](auto& p) {
        p.account  = account;
        p.username = username;
        p.age      = age;
        p.bio      = bio;
    });
}

require_auth语句和以太坊中的require(msg.sender == xxx)类似,都对调用者的身份做了限制。

profile_table是一种类型,可以理解成表示multi_index表,后面的profiles(_self, _self)才是真正构建一个multi_index表的实例。profiles里的两个参数依次就是我们前面提到过的codescope,分别表示表的拥有账户以及代码层次结构的范围标识符(已经忘记的小伙伴可以翻看上一章内容)。

当profiles表实例化完成之后,紧接着就是插入数据。关于插入数据的操作上一章我们有过详细的介绍,这里就不再赘述了。主要注意防止主键重复的操作。

2. 根据主键获取相关信息

void app::get(const account_name account) {
    profile_table profiles(_self, _self);

    auto itr = profiles.find(account);

    eosio_assert(itr != profiles.end(), "Account does not exist");

    print("Account: ", name{itr->account}, " , ");
    print("Username: ", itr->username.c_str(), " , ");
    print("Age: ", itr->age , " , ");
    print("Bio: ", itr->bio.c_str());
}

这里也很简单,先把multi_index表实例化,之后要求查询的结果不能为空 (即itr != profiles.end()),如果不为空的话,就返回主键对应的其他字段的信息。

这些操作都是通过我们上一章介绍过的迭代器来完成。

3. 根据主键更新信息

void app::update(const account_name account,
                     const string&      username,
                     uint32_t           age,
                     const string&      bio) {
    require_auth(account);

    profile_table profiles(_self, _self);

    auto itr = profiles.find(account);

    eosio_assert(itr != profiles.end(), "Account does not exist");

    profiles.modify(itr, account, [&](auto& p) {
        p.username = username;
        p.age      = age;
        p.bio      = bio;
    });
}

和之前的操作类似,确保主键不为空的情况下,更新该主键对应的其他字段的信息。

4. 根据主键删除数据

void app::remove(const account_name account) {
    require_auth(account);

    profile_table profiles(_self, _self);

    auto itr = profiles.find(account);

    eosio_assert(itr != profiles.end(), "Account does not exist");

    profiles.erase(itr);
    print(name{account} , " deleted!");
}

5. 通过自定义的自定义索引实现查询

前面四个介绍的都是主键相关的增删改查的操作,别忘了我们在上一章中还曾经定义过自定义索引by_age(),即以年龄为条件进行筛选。具体实现如下:

void app::byage(uint32_t age) {
    print("Checking age: ", age, "\n");
    profile_table profiles(_self, _self);

    // get an interface to the 'profiles' containter
    // that looks up a profile by its age
    auto age_index = profiles.get_index<N(age)>();

    auto itr = age_index.lower_bound(age);

    for(; itr != age_index.end() && itr->age == age; ++itr) {
        print(itr->username.c_str(), " is ", itr->age, " years old\n");
    }
}

这里我们使用了在头文件里定义过的名为age的index,重新得到了一张以age排序的multi_index。

这里的lower_bound是EOS封装的API,返回age_index中,当前索引≥age的迭代器;之后遍历该迭代器,就可以获得所有age某个特定值的所有数据。

lower_bound相对应的,就是upper_bound方法,用法和lower_bound类似。如下就实现了同时指定age上下限的查询:

void app::agerange(uint32_t young, uint32_t old) {
    profile_table profiles(_self, _self);

    auto age_index = profiles.get_index<N(age)>();

    auto begin = age_index.lower_bound(young);
    auto end   = age_index.upper_bound(old);

    for_each(begin, end, [&](auto& p) {
        print(p.username.c_str(), " is ", p.age, " years old\n");
    });
}

合约部署

把前文中所有的hpp和cpp的代码片段拼接成完整的hpp和cpp文件进行编译:

#使用 -o 生成wast文件和wasm文件
eosiocpp -o ./app.wast ./app.cpp
#使用 -g 生成abi文件
eosiocpp -g ./app.abi ./app.cpp

生成wast和abi文件的详细内容我们之前章节介绍过了,这里也不赘述了。这时我们的当前文件夹下会出现app.wastapp.abi文件。

部署合约:

cleos set contract eosio ./ ./app.wast app.abi -p eosio@active

(该命令前文也详细介绍过每个参数的含义,详情参考第五篇)

下图为成功部署合约的画面:

eos-app-contract

合约调用

1. 查看表内容

cleos get table eosio eosio profile

通过上述指令查看表中的内容,参数eosio eosio profile分别表示前文提到过的code、scope和表名。

结果如下图:

eos-app-contract

因为在头文件里声明过,可以看到该表已存在,但是内容为空。

2. 插入数据

执行如下命令往表中插入数据,然后再次查询:

// 插入
cleos push action eosio create '["eosio","hammer","25","programmer"]' -p eosio@active
// 再次查询
cleos get table eosio eosio profile

eos-app-contract

这时就可以看到表中保存了我们刚插入的一条数据。

还记得我们曾经创建过一个叫做testeosio的账户么?我们再使用那个账户往表中插入一条数据(记得先unlock钱包哦):

// 切换账号插入数据
cleos push action eosio create '["testeosio","maggie","23","waitress"]' -p testeosio@active
// 查询
cleos get table eosio eosio profile

这时我们可以看到:

eos-app-contract

数据确实添加进入表中了。

传入数据的第一个参数必须是调用该方法的账户名,还记得代码中的require_auth么??

3. 查询数据

使用get方法,传入主键:

cleos push action eosio get '["testeosio"]' -p testeosio@active
cleos push action eosio get '["testeosio"]' -p eosio@active

这里我们分别使用不同账户进行查询,因为没有权限限制,所以任何人都可以查询任意信息。得到的结果如下:

eos-app-contract

4. 根据自定义索引age筛选数据

根据主键的更新和删除方法按照上面的调用方法类推即可,这里就不再赘述了。让我们来试试之前定义的自定义索引的相关方法,byage和之前方法类似,我们就一步到位,直接调用agerange方法了。

// 传入参数的第一个为年龄下限,第二个为年龄上限
cleos push action eosio agerange '["22","27"]' -p eosio@active

此时得到:

eos-app-contract

注意到这里只返回了一个值,这时我们切换到nodeos的终端界面发现返回了完整的结果:

eos-app-contract

如果大家自己调用byage方法,会发现nodeos终端也只会显示一个结果(即使两个都符合条件),因为它只会返回符合条件的第一个结果。

总结

在铺垫了那么多理论和碎片的操作知识之后,我们终于第一次以完整的合约的形式,实现了对multi_index的操作,例如如何以主键以及自定义索引实现增删改查。希望大家可以感受到理解了multi_index,才能更加准确地理解智能合约的数据存储以及运行原理。

下一章我们将介绍大家最感兴趣的token合约的实现以及使用。


王铁塔
33 声望29 粉丝