簡(jiǎn)介
智能合約是現(xiàn)在區(qū)塊鏈的一大特色,而不同的鏈?zhǔn)褂玫闹悄芎霞s的虛擬機(jī)各不相同船响,編碼語言也有很大差異躬拢。而今天我們開始學(xué)習(xí)EOS的智能合約,我也是從EOS初期一直開發(fā)合約至今见间,期間踩過無數(shù)坑聊闯,也在Stack Overflow上提過問(最后自己解決了),在實(shí)際生產(chǎn)中也積累了很多經(jīng)驗(yàn)米诉,所以我會(huì)連續(xù)幾周分多次分享合約開發(fā)的經(jīng)驗(yàn)菱蔬,今天先來點(diǎn)基礎(chǔ)的。
一些C++的編程基礎(chǔ)
EOS就是使用C++開發(fā)的史侣,這也為它帶來了諸多好處拴泌,而合約也沿用C++作為開發(fā)語言,雖然合約中無法直接使用Boost等框架(你可以自己引入抵窒,但這也意味著合約會(huì)很大弛针,會(huì)占用大量賬號(hào)的內(nèi)存),但是我們還是可以使用很多C++的小型庫李皇,并伴隨著eosio.cdt的發(fā)展,融入了更多實(shí)用的合約功能宙枷。
如果你之前沒有使用C系列的開發(fā)語言做過開發(fā)掉房,比如:C語言、C++或者是C#慰丛,那么你需要先學(xué)習(xí)下C語言的基本語法和數(shù)據(jù)結(jié)構(gòu)卓囚,這里我不做展開,在我們的系列文章的開篇就介紹了我推薦的Learn EOS - c/c++ 教程英文版诅病,有一定英語基礎(chǔ)的朋友可以直接看這個(gè)哪亿,其他朋友也可以在網(wǎng)上找一些C++的入門教程看下。
如果你已經(jīng)有了一定的C語言基礎(chǔ)贤笆,那么寫合約的話蝇棉,你會(huì)發(fā)現(xiàn)需要的基礎(chǔ)也并不多,依葫蘆畫瓢就能寫出各種基礎(chǔ)功能了芥永,所以篡殷,你并不需要擔(dān)心太多語言上的門檻,畢竟合約只是一個(gè)特定環(huán)境下運(yùn)行的程序埋涧,你能用到的東西并不會(huì)很多板辽。
CDT選擇
EOS的早期版本進(jìn)行合約開發(fā)還沒有CDT工具奇瘦,那時(shí)的合約借助的是源碼中的工具eosiocpp,所以你看2018年的博客劲弦,進(jìn)行合約編譯都是用它耳标,但你現(xiàn)在是見不到了。隨著官方CDT的迭代邑跪,在CDT的1.4版本開始被官方推薦使用次坡,CDT后面也經(jīng)歷了幾個(gè)大的版本更新,逐步改善合約編寫方式呀袱,更加趨于簡(jiǎn)潔贸毕、直觀。
但是不同的CDT版本夜赵,也意味著編譯器的不同明棍,所以合約開發(fā)也會(huì)有所區(qū)別,比如一些語法變了寇僧,一些庫名稱變了摊腋,增加了一些新的標(biāo)注……
我們的教程側(cè)重還是介紹最新的語法,所以推薦使用1.6以上的版本嘁傀。我也會(huì)盡量在后面的介紹中補(bǔ)充說明老的CDT的寫法兴蒸,方便大家對(duì)照網(wǎng)上其他老博客的合約。
來個(gè)HelloWorld
學(xué)習(xí)任何編程细办,我們都不能少了Mr.HelloWorld橙凳,先來給大家打個(gè)招呼吧。
#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);
}
};
基本合約結(jié)構(gòu)及類型
hello合約就是一個(gè)最簡(jiǎn)單的合約了笑撞,而且還有一個(gè)可調(diào)用的action為hi岛啸。我們首先還是來介紹下一個(gè)合約的程序結(jié)構(gòu)吧。
- 程序頭
包含了引入的頭文件茴肥、庫文件等坚踩,還有全局的命名空間的引入等。
#include <eosio/eosio.hpp>
using namespace eosio;
這里eosio庫是我們的合約基礎(chǔ)庫瓤狐,所有和eos相關(guān)的類型和方法瞬铸,都在這個(gè)庫里面,而這個(gè)庫里面eosio.hpp是基礎(chǔ)础锐,包含了contract等的定義嗓节,所以所有的合約都要引入。
【CDT老版本】早期cdt版本中庫名稱不是eosio郁稍,而是eosiolib
默認(rèn)的赦政,我們引入了eosio的命名空間,因?yàn)閑osio的所有內(nèi)容都是在這個(gè)命名空間下的,所以我們?nèi)忠牖肿牛瑫?huì)方便我們后續(xù)的代碼編寫桐愉。
- 合約類定義
其實(shí)就是定義了一個(gè)class,繼承contract掰派,并通過[[eosio::contract]]
標(biāo)注這個(gè)類是一個(gè)合約从诲。使用using引入contract也是為了后續(xù)代碼可以更簡(jiǎn)潔。
class [[eosio::contract]] hello : public contract{
public:
using contract::contract;
}
【CDT老版本】早期cdt版本中直接使用了
CONTRACT
來定義合約類靡羡,比如:CONTRACT hello: public contract {}
- action定義
寫一個(gè)public的方法系洛,參數(shù)盡量用簡(jiǎn)單或者是eosio內(nèi)置的類型定義,無返回值(合約調(diào)用無法返回任何結(jié)果略步,除非報(bào)錯(cuò))描扯,然后在用[[eosio::action]]
標(biāo)注這個(gè)方法是一個(gè)合約action就行。
注意:action的名稱要求符合name類型的規(guī)則趟薄,name規(guī)則請(qǐng)看下面的常用類型中的說明绽诚。
[[eosio::action]]
void hi( name user ) {
print( "Hello, ", user);
}
因?yàn)楹霞s無法調(diào)試,所以只能通過print來打印信息杭煎,或者直接通過斷言拋出異常來進(jìn)行調(diào)試恩够。
【CDT老版本】早期cdt版本中直接使用
ACTION
來定義方法,比如:ACTION hi( name user ){}
- 常用類型
類型 | 說明 | 示例 |
---|---|---|
name | 名稱類型羡铲,賬號(hào)名蜂桶、表名、action名都是該類型也切,只能使用26個(gè)小寫字母和1到5的數(shù)字扑媚,特殊可以使用小數(shù)點(diǎn),總長(zhǎng)不超過13雷恃。 |
name("hi") 或者 "hi"_n
|
asset | 資產(chǎn)類型钦购,Token都是使用該類型,包含了Token符號(hào)和小數(shù)位褂萧,是一個(gè)復(fù)合類型,字符形式為1.0000 EOS
|
asset(10000, symbol("TADO", 4) 就是1.0000 TADO ) |
uint64_t | 無符號(hào)64位整型葵萎,主要數(shù)據(jù)類型导犹,表主鍵、name實(shí)質(zhì)都是改類型 | uint64_t amount = 10000000; |
- 內(nèi)置常用對(duì)象或方法
在合約中羡忘,contract基類提供了一些方便的內(nèi)置對(duì)象谎痢。
首先是get_self()
或者是_self
,這個(gè)方法可以獲取到當(dāng)前合約所在的賬號(hào)督勺,比如你把hello合約部署到了helloworld111這個(gè)賬號(hào)纤房,那么get_self()
就可以獲取到helloworld111睛竣。
然后是get_code()
或者是_code
罩抗,這個(gè)方法可以獲取到當(dāng)前交易請(qǐng)求的action方法名滨嘱,這個(gè)在進(jìn)行內(nèi)聯(lián)action調(diào)用時(shí)可以用于判斷入口action峰鄙。
最后是get_datastream()
或者_ds
,這個(gè)方法獲取的是數(shù)據(jù)流太雨,如果你使用的是復(fù)雜類型吟榴,或者是自定義類型,那么你無法在方法的參數(shù)上直接獲取到反序列化的變量值囊扳,你必須自己通過數(shù)據(jù)流來解析吩翻。
常用的還有獲取當(dāng)前時(shí)間current_time_point()
,這個(gè)需要引入#include <eosio/transaction.hpp>
锥咸。
數(shù)據(jù)持久化
當(dāng)然狭瞎,合約里面,我們總會(huì)有些功能需要把數(shù)據(jù)存下來搏予,在鏈上持久化存儲(chǔ)熊锭。所以我們就需要定義合約表了。
合約的表存在相應(yīng)的合約賬號(hào)中缔刹,可以劃分表范圍(scope)球涛,每個(gè)表都有一個(gè)主鍵,uint64_t類型的校镐,還可以有多個(gè)其他索引亿扁,表的查詢都是基于索引的。
這里先提一句鸟廓,表數(shù)據(jù)所占用的內(nèi)存从祝,默認(rèn)是合約賬號(hào)的內(nèi)存,也可以使用其他賬號(hào)的引谜,但需要權(quán)限牍陌,這個(gè)以后我們?cè)俳榻B。
我們擴(kuò)展一下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;
};
可以看到毒涧,我們已經(jīng)擴(kuò)充了不少東西了,包括構(gòu)造函數(shù)贝室,表定義契讲,多索引表配置,并完善了原先的hi方法滑频,增加了nevermeet方法捡偏。
我們現(xiàn)在模擬的是這樣一個(gè)使用場(chǎng)景,我們遇到一個(gè)朋友的時(shí)候峡迷,就會(huì)和他打招呼(調(diào)用hi)银伟,如果這個(gè)朋友是一個(gè)新朋友,就會(huì)插入一條記錄到我們的朋友表中,如果是一個(gè)老朋友了彤避,我們就會(huì)更新這個(gè)朋友的記錄中的訪問時(shí)間傅物。當(dāng)我們決定不再見這個(gè)朋友了,就是絕交了(調(diào)用nevermeet)忠藤,我們就會(huì)把這個(gè)朋友的記錄刪除挟伙。
- 表定義
首先我們需要聲明我們的朋友表。定義一個(gè)結(jié)構(gòu)體模孩,然后用[[eosio::table]]
標(biāo)注這個(gè)結(jié)構(gòu)體是一個(gè)合約表尖阔。在結(jié)構(gòu)體里定義一個(gè)函數(shù)名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; }
};
我們這里聲明了一個(gè)my_friend的表,合約的表名不在這里定義块茁,所以結(jié)構(gòu)體的名稱不必滿足name的規(guī)則齿坷。我們定義了兩個(gè)字段,friend_name(朋友的名稱)和visit_time(拜訪時(shí)間)数焊,主鍵我們直接使用了friend_name永淌,這個(gè)字段是name類型的,而name類型的實(shí)質(zhì)就是一個(gè)uint64_t的類型(所以name的規(guī)則那么苛刻)佩耳。
【CDT老版本】早期cdt版本中直接使用
TABLE
來定義合約表遂蛀,比如:TABLE my_friend{}
- 多索引表配置
合約里的表都是通過多索引來定義的,這是合約表的結(jié)構(gòu)基礎(chǔ)干厚。所以這里才是定義表名和查詢索引的地方李滴。
typedef eosio::multi_index<"friends"_n, my_friend> friends;
我們現(xiàn)在只介紹最簡(jiǎn)單的單索引的定義,以后再介紹多索引的定義方式蛮瞄,這里的"friends"_n
就是定義表名所坯,所以使用了name類型,之后my_friend
是表的結(jié)構(gòu)類型挂捅,typedef
實(shí)質(zhì)上就是聲明了一個(gè)類型別名芹助,名字是friends
的類型。
- 構(gòu)造函數(shù)
構(gòu)造函數(shù)這里并不是必須闲先,但是為了我們能在全局直接使用合約表周瞎,所以我們要在構(gòu)造函數(shù)進(jìn)行表對(duì)象的實(shí)例化。
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;
這一段是標(biāo)準(zhǔn)合約構(gòu)造函數(shù)饵蒂,hello(name receiver, name code, datastream<const char *> ds) : contract(receiver, code, ds)
,合約類型實(shí)例化時(shí)會(huì)傳入receiver也就是我們的合約賬號(hào)(一般情況下)酱讶,code就是我們的action名稱退盯,ds就是數(shù)據(jù)流。
friend_table(get_self(), get_self().value)
這一段就是對(duì)我們定義的friend_table
變量的實(shí)例化,friend_table
變量就是我們定義的多索引表的friends
類型的實(shí)例渊迁。在合約里我們就可以直接使用friend_table
變量來進(jìn)行表操作了慰照。實(shí)例化時(shí)傳遞的兩個(gè)參數(shù)正是表所在合約的名稱和表范圍(scope),這里都使用的是當(dāng)前合約的名稱琉朽。
- 查詢記錄
查詢有多種方式毒租,也就是多索引表提供了多種查詢的方式,默認(rèn)的箱叁,使用find
和get
方法是直接使用主鍵進(jìn)行查詢墅垮,下次我們會(huì)介紹使用第二、第三等索引來進(jìn)行查詢耕漱。find
返回的是指針算色,數(shù)據(jù)是否存在,需要通過判斷指針是否是指到了表末尾螟够,如果等于表末尾灾梦,就說明數(shù)據(jù)不存在,否則妓笙,指針的值就是數(shù)據(jù)對(duì)象若河。get
直接返回的就是數(shù)據(jù)對(duì)象,所以在調(diào)用get
時(shí)寞宫,就必須傳遞數(shù)據(jù)不存在時(shí)的錯(cuò)誤信息萧福。
auto friend_itr = friend_table.find(user.value);
if (friend_itr == friend_table.end())
{
//數(shù)據(jù)不存在
}else
{
//數(shù)據(jù)存在
}
我們?cè)趆i方法中先查詢了user是否存在。如果不存在淆九,我們就添加數(shù)據(jù)统锤,如果存在了,就修改數(shù)據(jù)中的visit_time字段的值為當(dāng)前時(shí)間炭庙。
- 添加記錄
多索引的表對(duì)象添加記錄使用emplace
方法饲窿,第一個(gè)參數(shù)就是內(nèi)存使用的對(duì)象,第二個(gè)參數(shù)就是添加表對(duì)象時(shí)的委托方法焕蹄。
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
{
//數(shù)據(jù)存在
}
這里先定義了一個(gè)變量now來表示當(dāng)前時(shí)間逾雄,正是使用的內(nèi)置方法current_time_point()
,這個(gè)還是用了它的sec_since_epoch()
方法腻脏,是為了直接獲取秒單位的值鸦泳。
我們查詢后發(fā)現(xiàn)這個(gè)user的數(shù)據(jù)不存在,所以就進(jìn)行插入操作永品,內(nèi)存直接使用的合約賬號(hào)的做鹰,所以使用get_self()
,然后對(duì)表數(shù)據(jù)對(duì)象進(jìn)行賦值鼎姐。
- 修改記錄
多索引的表對(duì)象修改記錄使用modify
方法钾麸,第一個(gè)參數(shù)是傳遞需要修改的數(shù)據(jù)指針更振,第二個(gè)參數(shù)是內(nèi)存使用的對(duì)象,第二個(gè)參數(shù)就是表對(duì)象修改時(shí)的委托方法饭尝。
friend_table.modify(friend_itr, get_self(), [&](auto &f) {
f.visit_time = now;
});
我們將查詢到的用戶對(duì)象的指針friend_itr
傳入肯腕,然后內(nèi)存還是使用合約賬號(hào)的,委托中钥平,我們只修改visit_time
的值(主鍵是不能修改的)实撒。
- 刪除記錄
- 多索引的表對(duì)象刪除記錄使用
erase
方法,只有一個(gè)參數(shù)涉瘾,就是要?jiǎng)h除的對(duì)象指針知态,有返回值,是刪除數(shù)據(jù)后的指針偏移睡汹,也就是下一條數(shù)據(jù)的指針肴甸。
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);
我們的示例中,將查詢到的這條數(shù)據(jù)直接刪除囚巴,并為使用變量來接收下一條數(shù)據(jù)的指針原在,在連續(xù)刪除數(shù)據(jù)時(shí),你會(huì)需要獲取下一條數(shù)據(jù)的指針彤叉,因?yàn)橐褎h除的數(shù)據(jù)的指針已經(jīng)失效了庶柿。
編譯
編譯我們?cè)僦耙灿羞^介紹,安裝了eosio.cdt后秽浇,我們就有了eosio-cpp
命令浮庐,進(jìn)入到合約文件夾中,直接執(zhí)行以下命令就會(huì)在當(dāng)前目錄生成wasm和abi文件柬焕。
eosio-cpp -abigen hello.cpp -o hello.wasm
注意:替換命令中使用的hello.cpp為實(shí)際合約代碼文件名审残,而hello.wasm為實(shí)際合約的wasm文件名。
當(dāng)然斑举,編譯不通過的時(shí)候搅轿,你就要看看錯(cuò)誤是什么了,這可能會(huì)考驗(yàn)一下你的C++功底富玷。
發(fā)布
決定了要發(fā)布的賬號(hào)后璧坟,記得要購(gòu)買足夠的內(nèi)存和抵押足夠的資源。合約的內(nèi)存消耗我們可以大致這樣估算赎懦,看下編譯好了的合約wasm文件有多大雀鹃,然后乘以10,就是你發(fā)布到鏈上大概所需的內(nèi)存大小了励两。
發(fā)布合約我們使用cleos set contract
命令黎茎,其后跟合約賬號(hào)名和合約目錄,為了方便当悔,我建議你把合約的目錄名保持和合約文件名一致工三。
cleos set contract helloworld111 ./hello -p helloworld111
這里我們給出的代碼是將hello目錄下的hello合約發(fā)布到helloworld111迁酸。我這里的文件夾是hello,里面的abi和wasm也都是hello俭正,這樣你不用手動(dòng)指定合約文件了。
總結(jié)
至此焙畔,我想大家應(yīng)該對(duì)合約的編寫有了一個(gè)大致的了解了掸读,至少你可以參照著寫個(gè)簡(jiǎn)單的合約出來了,這其中還有很多技巧和高級(jí)用法宏多,我會(huì)在后續(xù)的文章中繼續(xù)和大家分享儿惫。
原文鏈接:https://www.fishopark.com/Article/Info?id=6771aecf-6fda-4246-8dd8-71be86fc709e
更多內(nèi)容請(qǐng)關(guān)注微信公眾號(hào)