1. 程式人生 > >EOS入門指南PART8——智慧合約入門實戰

EOS入門指南PART8——智慧合約入門實戰

上一章我們細緻地學習了

  • 索引和迭代器的關係;
  • 如何生成和使用索引以及迭代器
  • 介紹了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 [email protected]

(該命令前文也詳細介紹過每個引數的含義,詳情參考第五篇)

下圖為成功部署合約的畫面:

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 [email protected]
// 再次查詢
cleos get table eosio eosio profile

eos-app-contract

這時就可以看到表中儲存了我們剛插入的一條資料。

還記得我們曾經建立過一個叫做testeosio的賬戶麼?我們再使用那個賬戶往表中插入一條資料(記得先unlock錢包哦):

// 切換賬號插入資料
cleos push action eosio create '["testeosio","maggie","23","waitress"]' -p [email protected]
// 查詢
cleos get table eosio eosio profile

這時我們可以看到:

eos-app-contract

資料確實新增進入表中了。

傳入資料的第一個引數必須是呼叫該方法的賬戶名,還記得程式碼中的require_auth麼?��

3. 查詢資料

使用get方法,傳入主鍵:

cleos push action eosio get '["testeosio"]' -p [email protected]
cleos push action eosio get '["testeosio"]' -p [email protected]

這裡我們分別使用不同賬戶進行查詢,因為沒有許可權限制,所以任何人都可以查詢任意資訊。得到的結果如下:

eos-app-contract

4. 根據自定義索引age篩選資料

根據主鍵的更新和刪除方法按照上面的呼叫方法類推即可,這裡就不再贅述了。讓我們來試試之前定義的自定義索引的相關方法,byage和之前方法類似,我們就一步到位,直接呼叫agerange方法了。

// 傳入引數的第一個為年齡下限,第二個為年齡上限
cleos push action eosio agerange '["22","27"]' -p [email protected]

此時得到:

eos-app-contract

注意到這裡只返回了一個值,這時我們切換到nodeos的終端介面發現返回了完整的結果:

eos-app-contract

如果大家自己呼叫byage方法,會發現nodeos終端也只會顯示一個結果(即使兩個都符合條件),因為它只會返回符合條件的第一個結果。

總結

在鋪墊了那麼多理論和碎片的操作知識之後,我們終於第一次以完整的合約的形式,實現了對multi_index的操作,例如如何以主鍵以及自定義索引實現增刪改查。希望大家可以感受到理解了multi_index,才能更加準確地理解智慧合約的資料儲存以及執行原理。

下一章我們將介紹大家最感興趣的token合約的實現以及使用。