簡介
智能合約是現在區塊鏈的一大特色,而不同的鏈使用的智能合約的虛擬機各不相同,編碼語言也有很大差異。而今天我們開始學習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),這裏都使用的是當前合約的名稱。
- 查詢記錄
查詢有多種方式,也就是多索引表提供了多種查詢的方式,默認的,使用find
和get
方法是直接使用主鍵進行查詢,下次我們會介紹使用第二、第三等索引來進行查詢。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,這樣你不用手動指定合約文件了。
總結
至此,我想大家應該對合約的編寫有了一個大致的了解了,至少你可以參照着寫個簡單的合約出來了,這其中還有很多技巧和高級用法,我會在後續的文章中繼續和大家分享。
本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※網頁設計公司推薦不同的風格,搶佔消費者視覺第一線
※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益
※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面
※南投搬家公司費用需注意的眉眉角角,別等搬了再說!
※教你寫出一流的銷售文案?
※聚甘新