EOS基礎全家桶(十三)智慧合約基礎

fishopark發表於2020-06-18

簡介

智慧合約是現在區塊鏈的一大特色,而不同的鏈使用的智慧合約的虛擬機器各不相同,編碼語言也有很大差異。而今天我們開始學習EOS的智慧合約,我也是從EOS初期一直開發合約至今,期間踩過無數坑,也在Stack Overflow上提過問(最後自己解決了),在實際生產中也積累了很多經驗,所以我會連續幾周分多次分享合約開發的經驗,今天先來點基礎的。

一些C++的程式設計基礎

EOS就是使用C++開發的,這也為它帶來了諸多好處,而合約也沿用C++作為開發語言,雖然合約中無法直接使用Boost等框架(你可以自己引入,但這也意味著合約會很大,會佔用大量賬號的記憶體),但是我們還是可以使用很多C++的小型庫,並伴隨著eosio.cdt的發展,融入了更多實用的合約功能。

如果你之前沒有使用C系列的開發語言做過開發,比如:C語言、C++或者是C#,那麼你需要先學習下C語言的基本語法和資料結構,這裡我不做展開,在我們的系列文章的開篇就介紹了我推薦的Learn EOS - c/c++ 教程英文版,有一定英語基礎的朋友可以直接看這個,其他朋友也可以在網上找一些C++的入門教程看下。

如果你已經有了一定的C語言基礎,那麼寫合約的話,你會發現需要的基礎也並不多,依葫蘆畫瓢就能寫出各種基礎功能了,所以,你並不需要擔心太多語言上的門檻,畢竟合約只是一個特定環境下執行的程式,你能用到的東西並不會很多。

CDT選擇

EOS的早期版本進行合約開發還沒有CDT工具,那時的合約藉助的是原始碼中的工具eosiocpp,所以你看2018年的部落格,進行合約編譯都是用它,但你現在是見不到了。隨著官方CDT的迭代,在CDT的1.4版本開始被官方推薦使用,CDT後面也經歷了幾個大的版本更新,逐步改善合約編寫方式,更加趨於簡潔、直觀。

但是不同的CDT版本,也意味著編譯器的不同,所以合約開發也會有所區別,比如一些語法變了,一些庫名稱變了,增加了一些新的標註……

我們的教程側重還是介紹最新的語法,所以推薦使用1.6以上的版本。我也會盡量在後面的介紹中補充說明老的CDT的寫法,方便大家對照網上其他老部落格的合約。

來個HelloWorld

學習任何程式設計,我們都不能少了Mr.HelloWorld,先來給大家打個招呼吧。

#include <eosio/eosio.hpp>

using namespace eosio;

class [[eosio::contract]] hello : public contract
{
public:
    using contract::contract;

    [[eosio::action]] void hi(name user)
    {
        print("Hello, ", user);
    }
};

  

基本合約結構及型別

hello合約就是一個最簡單的合約了,而且還有一個可呼叫的action為hi。我們首先還是來介紹下一個合約的程式結構吧。

  • 程式頭

包含了引入的標頭檔案、庫檔案等,還有全域性的名稱空間的引入等。

#include <eosio/eosio.hpp>

using namespace eosio;

  

這裡eosio庫是我們的合約基礎庫,所有和eos相關的型別和方法,都在這個庫裡面,而這個庫裡面eosio.hpp是基礎,包含了contract等的定義,所以所有的合約都要引入。

【CDT老版本】早期cdt版本中庫名稱不是eosio,而是eosiolib

預設的,我們引入了eosio的名稱空間,因為eosio的所有內容都是在這個名稱空間下的,所以我們全域性引入,會方便我們後續的程式碼編寫。

  • 合約類定義

其實就是定義了一個class,繼承contract,並通過[[eosio::contract]]標註這個類是一個合約。使用using引入contract也是為了後續程式碼可以更簡潔。

class [[eosio::contract]] hello : public contract{
public:
    using contract::contract;
}

  

【CDT老版本】早期cdt版本中直接使用了CONTRACT來定義合約類,比如:CONTRACT hello: public contract {}

  • action定義

寫一個public的方法,引數儘量用簡單或者是eosio內建的型別定義,無返回值(合約呼叫無法返回任何結果,除非報錯),然後在用[[eosio::action]]標註這個方法是一個合約action就行。

注意:action的名稱要求符合name型別的規則,name規則請看下面的常用型別中的說明。

[[eosio::action]]
void hi( name user ) {
    print( "Hello, ", user);
}

  

因為合約無法除錯,所以只能通過print來列印資訊,或者直接通過斷言丟擲異常來進行除錯。

【CDT老版本】早期cdt版本中直接使用ACTION來定義方法,比如:ACTION hi( name user ){}

  • 常用型別
型別說明示例
name 名稱型別,賬號名、表名、action名都是該型別,只能使用26個小寫字母和1到5的數字,特殊可以使用小數點,總長不超過13。 name("hi") 或者 "hi"_n
asset 資產型別,Token都是使用該型別,包含了Token符號和小數位,是一個複合型別,字元形式為1.0000 EOS asset(10000, symbol("TADO", 4)就是1.0000 TADO)
uint64_t 無符號64位整型,主要資料型別,表主鍵、name實質都是改型別 uint64_t amount = 10000000;
  • 內建常用物件或方法

在合約中,contract基類提供了一些方便的內建物件。

首先是get_self()或者是_self,這個方法可以獲取到當前合約所在的賬號,比如你把hello合約部署到了helloworld111這個賬號,那麼get_self()就可以獲取到helloworld111。

然後是get_code()或者是_code,這個方法可以獲取到當前交易請求的action方法名,這個在進行內聯action呼叫時可以用於判斷入口action。

最後是get_datastream()或者_ds,這個方法獲取的是資料流,如果你使用的是複雜型別,或者是自定義型別,那麼你無法在方法的引數上直接獲取到反序列化的變數值,你必須自己通過資料流來解析。

常用的還有獲取當前時間current_time_point(),這個需要引入#include <eosio/transaction.hpp>

資料持久化

當然,合約裡面,我們總會有些功能需要把資料存下來,在鏈上持久化儲存。所以我們就需要定義合約表了。

合約的表存在相應的合約賬號中,可以劃分表範圍(scope),每個表都有一個主鍵,uint64_t型別的,還可以有多個其他索引,表的查詢都是基於索引的。

這裡先提一句,表資料所佔用的記憶體,預設是合約賬號的記憶體,也可以使用其他賬號的,但需要許可權,這個以後我們再介紹。

我們擴充套件一下hello合約。

#include <eosio/eosio.hpp>
#include <eosio/transaction.hpp>

using namespace eosio;

class [[eosio::contract]] hello : public contract
{
public:
    using contract::contract;

    hello(name receiver, name code, datastream<const char *> ds) : contract(receiver, code, ds), friend_table(get_self(), get_self().value)
    {
    }

    [[eosio::action]] void hi(name user)
    {
        print("Hello, ", user);

        uint32_t now = current_time_point().sec_since_epoch();

        auto friend_itr = friend_table.find(user.value);
        if (friend_itr == friend_table.end())
        {
            friend_table.emplace(get_self(), [&](auto &f) {
                f.friend_name = user;
                f.visit_time = now;
            });
        }
        else
        {
            friend_table.modify(friend_itr, get_self(), [&](auto &f) {
                f.visit_time = now;
            });
        }
    }

    [[eosio::action]] void nevermeet(name user)
    {
        print("Never see you again, ", user);

        auto friend_itr = friend_table.find(user.value);
        check(friend_itr != friend_table.end(), "I don't know who you are.");

        friend_table.erase(friend_itr);
    }

private:
    struct [[eosio::table]] my_friend
    {
        name friend_name;
        uint64_t visit_time;

        uint64_t primary_key() const { return friend_name.value; }
    };

    typedef eosio::multi_index<"friends"_n, my_friend> friends;

    friends friend_table;
};

  

可以看到,我們已經擴充了不少東西了,包括建構函式,表定義,多索引表配置,並完善了原先的hi方法,增加了nevermeet方法。

我們現在模擬的是這樣一個使用場景,我們遇到一個朋友的時候,就會和他打招呼(呼叫hi),如果這個朋友是一個新朋友,就會插入一條記錄到我們的朋友表中,如果是一個老朋友了,我們就會更新這個朋友的記錄中的訪問時間。當我們決定不再見這個朋友了,就是絕交了(呼叫nevermeet),我們就會把這個朋友的記錄刪除。

  • 表定義

首先我們需要宣告我們的朋友表。定義一個結構體,然後用[[eosio::table]]標註這個結構體是一個合約表。在結構體裡定義一個函式名primary_key,返回uint64_t型別,作為主鍵的定義。

private:
    struct [[eosio::table]] my_friend
    {
        name friend_name;
        uint64_t visit_time;

        uint64_t primary_key() const { return friend_name.value; }
    };

  

我們這裡宣告瞭一個my_friend的表,合約的表名不在這裡定義,所以結構體的名稱不必滿足name的規則。我們定義了兩個欄位,friend_name(朋友的名稱)和visit_time(拜訪時間),主鍵我們直接使用了friend_name,這個欄位是name型別的,而name型別的實質就是一個uint64_t的型別(所以name的規則那麼苛刻)。

【CDT老版本】早期cdt版本中直接使用TABLE來定義合約表,比如:TABLE my_friend{}

  • 多索引表配置

合約裡的表都是通過多索引來定義的,這是合約表的結構基礎。所以這裡才是定義表名和查詢索引的地方。

typedef eosio::multi_index<"friends"_n, my_friend> friends;

  

我們現在只介紹最簡單的單索引的定義,以後再介紹多索引的定義方式,這裡的"friends"_n就是定義表名,所以使用了name型別,之後my_friend是表的結構型別,typedef實質上就是宣告瞭一個型別別名,名字是friends的型別。

  • 建構函式

建構函式這裡並不是必須,但是為了我們能在全域性直接使用合約表,所以我們要在建構函式進行表物件的例項化。

public:
    hello(name receiver, name code, datastream<const char *> ds) : contract(receiver, code, ds), friend_table(get_self(), get_self().value)
    {
    }

private:
    friends friend_table;

  

這一段是標準合約建構函式,hello(name receiver, name code, datastream<const char *> ds) : contract(receiver, code, ds),合約型別例項化時會傳入receiver也就是我們的合約賬號(一般情況下),code就是我們的action名稱,ds就是資料流。

friend_table(get_self(), get_self().value)這一段就是對我們定義的friend_table變數的例項化,friend_table變數就是我們定義的多索引表的friends型別的例項。在合約裡我們就可以直接使用friend_table變數來進行表操作了。例項化時傳遞的兩個引數正是表所在合約的名稱和表範圍(scope),這裡都使用的是當前合約的名稱。

  • 查詢記錄

查詢有多種方式,也就是多索引表提供了多種查詢的方式,預設的,使用findget方法是直接使用主鍵進行查詢,下次我們會介紹使用第二、第三等索引來進行查詢。find返回的是指標,資料是否存在,需要通過判斷指標是否是指到了表末尾,如果等於表末尾,就說明資料不存在,否則,指標的值就是資料物件。get直接返回的就是資料物件,所以在呼叫get時,就必須傳遞資料不存在時的錯誤資訊。

auto friend_itr = friend_table.find(user.value);
if (friend_itr == friend_table.end())
{
    //資料不存在
}else
{
    //資料存在
}

  

我們在hi方法中先查詢了user是否存在。如果不存在,我們就新增資料,如果存在了,就修改資料中的visit_time欄位的值為當前時間。

  • 新增記錄

多索引的表物件新增記錄使用emplace方法,第一個引數就是記憶體使用的物件,第二個引數就是新增表物件時的委託方法。

uint32_t now = current_time_point().sec_since_epoch();

auto friend_itr = friend_table.find(user.value);
if (friend_itr == friend_table.end())
{
    friend_table.emplace(get_self(), [&](auto &f) {
        f.friend_name = user;
        f.visit_time = now;
    });
}
else
{
    //資料存在
}

  

這裡先定義了一個變數now來表示當前時間,正是使用的內建方法current_time_point(),這個還是用了它的sec_since_epoch()方法,是為了直接獲取秒單位的值。

我們查詢後發現這個user的資料不存在,所以就進行插入操作,記憶體直接使用的合約賬號的,所以使用get_self(),然後對錶資料物件進行賦值。

  • 修改記錄

多索引的表物件修改記錄使用modify方法,第一個引數是傳遞需要修改的資料指標,第二個引數是記憶體使用的物件,第二個引數就是表物件修改時的委託方法。

friend_table.modify(friend_itr, get_self(), [&](auto &f) {
    f.visit_time = now;
});

  

我們將查詢到的使用者物件的指標friend_itr傳入,然後記憶體還是使用合約賬號的,委託中,我們只修改visit_time的值(主鍵是不能修改的)。

  • 刪除記錄
  • 多索引的表物件刪除記錄使用erase方法,只有一個引數,就是要刪除的物件指標,有返回值,是刪除資料後的指標偏移,也就是下一條資料的指標。
auto friend_itr = friend_table.find(user.value);
check(friend_itr != friend_table.end(), "I don't know who you are.");

friend_table.erase(friend_itr);

  

我們的示例中,將查詢到的這條資料直接刪除,併為使用變數來接收下一條資料的指標,在連續刪除資料時,你會需要獲取下一條資料的指標,因為已刪除的資料的指標已經失效了。

編譯

編譯我們再之前也有過介紹,安裝了eosio.cdt後,我們就有了eosio-cpp命令,進入到合約資料夾中,直接執行以下命令就會在當前目錄生成wasm和abi檔案。

eosio-cpp -abigen hello.cpp -o hello.wasm

注意:替換命令中使用的hello.cpp為實際合約程式碼檔名,而hello.wasm為實際合約的wasm檔名。

當然,編譯不通過的時候,你就要看看錯誤是什麼了,這可能會考驗一下你的C++功底。

釋出

決定了要釋出的賬號後,記得要購買足夠的記憶體和抵押足夠的資源。合約的記憶體消耗我們可以大致這樣估算,看下編譯好了的合約wasm檔案有多大,然後乘以10,就是你釋出到鏈上大概所需的記憶體大小了。

釋出合約我們使用cleos set contract命令,其後跟合約賬號名和合約目錄,為了方便,我建議你把合約的目錄名保持和合約檔名一致。

cleos set contract helloworld111 ./hello -p helloworld111

這裡我們給出的程式碼是將hello目錄下的hello合約釋出到helloworld111。我這裡的資料夾是hello,裡面的abi和wasm也都是hello,這樣你不用手動指定合約檔案了。

總結

至此,我想大家應該對合約的編寫有了一個大致的瞭解了,至少你可以參照著寫個簡單的合約出來了,這其中還有很多技巧和高階用法,我會在後續的文章中繼續和大家分享。

相關文章