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裡的兩個引數依次就是我們前面提到過的code
和scope
,分別表示表的擁有賬戶以及程式碼層次結構的範圍識別符號(已經忘記的小夥伴可以翻看上一章內容)。
當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.wast
和app.abi
檔案。
部署合約:
cleos set contract eosio ./ ./app.wast app.abi -p [email protected]
(該命令前文也詳細介紹過每個引數的含義,詳情參考第五篇)
下圖為成功部署合約的畫面:
合約呼叫
1. 查看錶內容
cleos get table eosio eosio profile
通過上述指令查看錶中的內容,引數eosio eosio profile
分別表示前文提到過的code、scope和表名。
結果如下圖:
因為在標頭檔案裡宣告過,可以看到該表已存在,但是內容為空。
2. 插入資料
執行如下命令往表中插入資料,然後再次查詢:
// 插入
cleos push action eosio create '["eosio","hammer","25","programmer"]' -p [email protected]
// 再次查詢
cleos get table eosio eosio profile
這時就可以看到表中儲存了我們剛插入的一條資料。
還記得我們曾經建立過一個叫做testeosio
的賬戶麼?我們再使用那個賬戶往表中插入一條資料(記得先unlock錢包哦):
// 切換賬號插入資料
cleos push action eosio create '["testeosio","maggie","23","waitress"]' -p [email protected]
// 查詢
cleos get table eosio eosio profile
這時我們可以看到:
資料確實新增進入表中了。
傳入資料的第一個引數必須是呼叫該方法的賬戶名,還記得程式碼中的require_auth
麼?��
3. 查詢資料
使用get方法,傳入主鍵:
cleos push action eosio get '["testeosio"]' -p [email protected]
cleos push action eosio get '["testeosio"]' -p [email protected]
這裡我們分別使用不同賬戶進行查詢,因為沒有許可權限制,所以任何人都可以查詢任意資訊。得到的結果如下:
4. 根據自定義索引age篩選資料
根據主鍵的更新和刪除方法按照上面的呼叫方法類推即可,這裡就不再贅述了。讓我們來試試之前定義的自定義索引的相關方法,byage
和之前方法類似,我們就一步到位,直接呼叫agerange
方法了。
// 傳入引數的第一個為年齡下限,第二個為年齡上限
cleos push action eosio agerange '["22","27"]' -p [email protected]
此時得到:
注意到這裡只返回了一個值,這時我們切換到nodeos的終端介面發現返回了完整的結果:
如果大家自己呼叫byage
方法,會發現nodeos終端也只會顯示一個結果(即使兩個都符合條件),因為它只會返回符合條件的第一個結果。
總結
在鋪墊了那麼多理論和碎片的操作知識之後,我們終於第一次以完整的合約的形式,實現了對multi_index的操作,例如如何以主鍵以及自定義索引實現增刪改查。希望大家可以感受到理解了multi_index,才能更加準確地理解智慧合約的資料儲存以及執行原理。
下一章我們將介紹大家最感興趣的token合約的實現以及使用。