eos的資源利用機(jī)制:
比特幣和以太坊中的交易手續(xù)費(fèi)機(jī)制这揣,其目的就是防止大量 垃圾交易使得系統(tǒng)擁堵遗嗽。
eos通過增發(fā),完全取消手續(xù)費(fèi)都弹,(根據(jù)賬戶中EOS通證的數(shù)量來(lái)分配系統(tǒng)資源娇豫。)
CPU與帶寬:按抵押的EOS通證比例分配CPU與帶寬。例如畅厢,如果你持有全網(wǎng)1%的EOS通證冯痢,那就可以抵押這些通證來(lái)獲得全網(wǎng)1%的CPU和帶寬。 這樣就可以隔離開所有的DAPP框杜,防止資源競(jìng)爭(zhēng)和惡意的DDOS供給浦楣,無(wú)論其他的DAPP如何擁堵, 你自己的帶寬都不受影響咪辱。
當(dāng)不再需要CPU與帶寬時(shí)振劳,抵押的EOS通證可以贖回,在贖回的時(shí)候油狂,存在三天的贖回期历恐。
與CPU和帶寬不同,要將數(shù)據(jù)存儲(chǔ)在區(qū)塊鏈中专筷,你需要基于當(dāng)前的RAM市場(chǎng)價(jià)格弱贼,用EOS通證買入 RAM,才能獲得一定數(shù)量的存儲(chǔ)字節(jié)磷蛹。當(dāng)你不再需要內(nèi)存時(shí)吮旅,也可以將內(nèi)存以當(dāng)前的RAM市場(chǎng)價(jià)格 賣出得到EOS通證
RAM的價(jià)格是基于班科(Bancor)算法,也就是說(shuō)是由市場(chǎng)供需調(diào)節(jié)的:如果 RAM供不應(yīng)求味咳,則買入RAM時(shí)就需要更多的EOS通證鸟辅,而這時(shí)賣出RAM也能獲得更多的EOS通證氛什。
內(nèi)存是消耗資源莺葫,不可贖回匪凉,只能買賣。以EOS上發(fā)幣為例捺檬,目前發(fā)幣需要200K的內(nèi)存再层,一個(gè) EOS可買20KB,按目前的存儲(chǔ)價(jià)格發(fā)一個(gè)幣需要消耗10個(gè)EOS堡纬。這是EOS內(nèi)存消耗的剛需來(lái)源
nodeos:核心程序聂受,用于啟動(dòng)eos節(jié)點(diǎn)服務(wù),在后臺(tái)運(yùn)行烤镐,可以配置不同 插件蛋济。該進(jìn)程負(fù)責(zé)賬戶管理、區(qū)塊生成炮叶、共識(shí)建立碗旅,并提供智能合約的運(yùn)行環(huán)境
keosd:錢包管理程序,負(fù)責(zé)錢包镜悉、密鑰的管理和交易的簽名
cleos:與nodeos和keosd交互的命令行工具祟辟,cleos通過RPC API 訪問nodeos和keosd
nodeos啟動(dòng)節(jié)點(diǎn)服務(wù)器
Ctrl+C或者pkill nodeos退出nodeos終端
nodes的運(yùn)行依賴于配置文件config.ini,在linux下侣肄,其默認(rèn)位置為: ~/.local/share/eosio/nodeos/config/config.ini
enable-stale-production = true # 啟用不穩(wěn)定出塊
producer-name = eosio # 出塊節(jié)點(diǎn)名
http-validate-host = false # 是否驗(yàn)證http頭信息
plugin = eosio::history_api_plugin # 啟用history api插件
plugin = eosio::chain_api_plugin # 啟用chain_api插件
access-control-allow-origin = * # CORS
http-server-address = 0.0.0.0:8888 # 監(jiān)聽地址
如果nodeos沒有正常關(guān)閉旧困,那么再次啟動(dòng)nodeos就會(huì)提示如下錯(cuò)誤:
error: database dirty flag set. replay required.
如果你需要保留之前的數(shù)據(jù),可以清除節(jié)點(diǎn)中的可逆塊稼锅,然后重放 交易來(lái)重新生成:
~ nodeos --replay-blockchain
當(dāng)然吼具,你也可以完全刪除整個(gè)數(shù)據(jù)目錄,如果不再需要其中的賬戶和交易:
~ nodeos
錢包服務(wù)器的作用是管理存儲(chǔ)私鑰的錢包矩距,并負(fù)責(zé)對(duì)交易進(jìn)行簽名拗盒。
~$ keosd 啟動(dòng)錢包服務(wù)
Ctrl+C或者pkill keosd結(jié)束keosd的運(yùn)行
keosd的運(yùn)行也依賴于配置文件,在linux下其默認(rèn)路徑為:~/eosio-wallet/config.ini
重要工具cleos剩晴,例如 創(chuàng)建錢包锣咒、創(chuàng)建密鑰對(duì)、創(chuàng)建賬戶赞弥、部署合約毅整、發(fā)送交易等等,都是 使用它绽左。
在執(zhí)行cleos命令之前悼嫉,別忘了啟動(dòng)nodeos和keosd。
EOS使用非對(duì)稱密鑰對(duì)來(lái)鑒別交易的來(lái)源:在發(fā)送 交易時(shí)拼窥,用戶需要使用私鑰簽名交易戏蔑,然后節(jié)點(diǎn)就可以使用其公鑰來(lái) 確認(rèn)該交易來(lái)源與聲稱的相符蹋凝。因此密鑰是區(qū)塊鏈身份識(shí)別的基礎(chǔ)
錢包則是用來(lái)保存私鑰的,一個(gè)錢包中可以保存多個(gè)私鑰总棵。由于錢包持有 私鑰鳍寂,因此它的另一個(gè)作用是對(duì)數(shù)據(jù)進(jìn)行簽名 —— 私鑰不用出錢包就可以 完成交易的簽名,這對(duì)資產(chǎn)安全很有好處情龄。
與其他區(qū)塊鏈不同迄汛,以太坊的賬戶是建立在區(qū)塊鏈上的,每個(gè)賬戶都對(duì)應(yīng) 于鏈上的賬戶表中的一條記錄骤视;另一點(diǎn)區(qū)別在于鞍爱,EOS的一個(gè)賬戶需要 兩組密鑰。
錢包服務(wù)器負(fù)責(zé)錢包的管理专酗,一個(gè)錢包服務(wù)器可以管理多個(gè)錢包
使用cleos的wallet creat子命令創(chuàng)建錢包
~$ cleos wallet create
PW5K8TVxnEH2ue5CFdr54NTZRzkyYxSpfg7VrMR49CsCdZavE9rhc
需要記住這個(gè)密碼睹逃,因?yàn)楹竺孢€要用它來(lái)解鎖錢包 —— 只有解鎖的錢包,才可以 用來(lái)簽名交易
可以使用-n選項(xiàng)來(lái)聲明錢包的名稱祷肯,以便創(chuàng)建額外的錢包沉填。例如:
~$ cleos wallet create -n mytest
上面命令對(duì)應(yīng)的錢包文件為~/eosio-wallet/mytest.wallet。同樣躬柬, 別忘了記錄下生成的錢包密碼拜轨。
~$ cleos wallet list查看錢包列表
錢包后面帶*號(hào)的表示當(dāng)前處于解鎖狀態(tài)
重啟keosd或者15分鐘之內(nèi)無(wú)操作,錢包都會(huì)關(guān)閉允青,需要重新解鎖unlock
當(dāng)然鎖定時(shí)間可以在keosd的配置文件中修改橄碾,例如,將其延長(zhǎng)至一小時(shí)
unlock-timeout = 3600
我們可以重啟keosd來(lái)關(guān)閉所有錢包颠锉,或者執(zhí)行wallet lock_all來(lái)鎖定所有錢包:
~ cleos wallet unlock
解鎖指定錢包$ cleos wallet unlock -n mywallet
按照命令提示輸入錢包密碼即可解鎖成功PW5JQdyKRCdH1AS51AYKm8VAVTgoh5PiaTQ2GUtQYKFR8AwvejSmr
在發(fā)送 交易時(shí),用戶需要使用私鑰簽名交易琼掠,然后節(jié)點(diǎn)就可以使用其公鑰來(lái) 確認(rèn)該交易來(lái)源與聲稱的相符拒垃。因此密鑰是區(qū)塊鏈身份識(shí)別的基礎(chǔ)
使用create key子命令創(chuàng)建一對(duì)新的密鑰
cleos create key
由于從私鑰可以推算出公鑰,因此只需要把私鑰導(dǎo)入錢包就可以了瓷蛙。下面的命令 執(zhí)行后會(huì)顯示導(dǎo)入成功的私鑰所對(duì)應(yīng)的公鑰
~$ cleos wallet import 5JoKhub5Fb3tGNv2bfDP1yz2mbrWmAa3DeMJRKu4YujTwM98m4H(這是私鑰)
在EOS中建立一個(gè)賬戶需要兩個(gè)私鑰悼瓮,分別對(duì)應(yīng)于Onwer權(quán)限和Active權(quán)限。Owner權(quán)限可以做 任何的事情艰猬,不過平時(shí)只是供著横堡,只有在Active私鑰丟失或被盜時(shí)才用來(lái)重置Active權(quán)限。 Active權(quán)限則用來(lái)進(jìn)行日常的交易簽名等操作冠桃。
因此在創(chuàng)建賬戶之前我們需要首先創(chuàng)建兩組密鑰并導(dǎo)入錢包命贴,例如:
第一組,將其用于賬戶的Owner權(quán)限:
~$ cleos create key
Private key: 5Jmw8TexQMCqderdzNBjAVYn4Tj7z9c8aBTEYou3DBrCumwnpB8
Public key: EOS7xUuBw134uoURifmSCSSScJksxok6Pp6CSCujr4AVabDGTNVit
第二組,將其用于賬戶的Active權(quán)限:
~$ cleos create key
Private key: 5JZ8LhMULSijNtaS5E8VZ4dKMwAmQpARwBk4Z4Fo2CLw7oSefqM
Public key: EOS75K9RtMbjsr5WpT1md3fgeSwbYkqUF8MwgQbZtXpGWtVCVjS8i
將兩個(gè)私鑰導(dǎo)入默認(rèn)錢包:
~ cleos wallet import 5JZ8LhMULSijNtaS5E8VZ4dKMwAmQpARwBk4Z4Fo2CLw7oSefqM
然后使用create account子命令來(lái)創(chuàng)建一個(gè)新賬號(hào)mary胸蛛,并依次輸入上面的 兩個(gè)公鑰:
~$ cleos create account eosio mary
EOS7xUuBw134uoURifmSCSSScJksxok6Pp6CSCujr4AVabDGTNVit
EOS75K9RtMbjsr5WpT1md3fgeSwbYkqUF8MwgQbZtXpGWtVCVjS8i
eosio是系統(tǒng)合約代碼托管賬戶污茵,現(xiàn)在可以簡(jiǎn)單地理解為,在賬戶eosio那里存放 著用來(lái)創(chuàng)建賬戶的系統(tǒng)代碼葬项,因此在執(zhí)行create account命令時(shí)泞当,始終需要首先 指定該賬戶。
方便的腳本:
init-wallet.sh的作用是重新初始化默認(rèn)錢包并導(dǎo)入eosio系統(tǒng)賬號(hào)的私鑰玷室。它會(huì)自動(dòng) 刪除之前創(chuàng)建的默認(rèn)錢包文件零蓉,同時(shí)也會(huì)在腳本目錄下的artifacts子目錄保存默認(rèn)錢包 的密碼,以便后續(xù)使用穷缤。該腳本也會(huì)同時(shí)將eosio賬戶的公鑰導(dǎo)入默認(rèn)錢包。
unlock-wallet.sh腳本的作用是使用腳本目錄中預(yù)先保存的錢包密碼解鎖默認(rèn)錢包
new-account.sh腳本的作用是創(chuàng)建兩組私鑰箩兽、導(dǎo)入默認(rèn)錢包并創(chuàng)建指定名稱的賬號(hào)津肛,同時(shí) 在腳本目錄的artifacts子目錄中保存賬號(hào)的私鑰和公鑰,以便后續(xù)使用汗贫。
例如身坐,下面的代碼創(chuàng)建賬號(hào)demo:
~$ new-account.sh demo
兩組密鑰對(duì)保存在~/repo/tools/artifacts/demo子目錄中。
上述腳本采用明文保存錢包密碼和私鑰落包,因此只能用于開發(fā)環(huán)境部蛇。
EOS智能合約就是運(yùn)行在EOS節(jié)點(diǎn)虛擬機(jī)上具有特定編程模式的應(yīng)用程序
在EOS中,一個(gè)交易(Transaction)由一個(gè)或多個(gè)動(dòng)作(Action)組成咐蝇。 當(dāng)用戶使用cleos向nodeos提交一個(gè)交易后涯鲁,其包含的動(dòng)作經(jīng)過分發(fā),由對(duì)應(yīng) 的智能合約對(duì)象負(fù)責(zé)處理有序。
智能合約有其特定的編程模式抹腿,一般采用狀態(tài)機(jī)模型。一個(gè)智能合約通常 會(huì)包含兩個(gè)核心部件:動(dòng)作處理器和狀態(tài)旭寿。動(dòng)作處理器是合約為外部提供的 用來(lái)更新狀態(tài)的接口警绩,而合約狀態(tài)只有在外部動(dòng)作的激發(fā)下才會(huì)發(fā)生變化。 例如盅称,一個(gè)賬戶的代幣余額就可以視為一個(gè)狀態(tài)肩祥,只有當(dāng)發(fā)生轉(zhuǎn)賬交易時(shí), 該賬戶的余額才會(huì)發(fā)生變化缩膝。
EOS的智能合約運(yùn)行在WASM虛擬機(jī)之上混狠,目前只支持采用C++語(yǔ)言開發(fā)智能合約, 不過據(jù)稱有支持solidity的計(jì)劃逞盆。
EOS之所以選擇C++是因?yàn)樗哪0?系統(tǒng)能保證合約更加安全檀蹋,同時(shí)運(yùn)行速度更快,而且通常也需要像使用C語(yǔ)言那樣 擔(dān)心內(nèi)存管理問題。
編寫counter.cpp EOS智能合約
首先建立一個(gè)~/repo/counter文件夾俯逾,并在該文件夾下創(chuàng)建文件counter.cpp:
~ touch ~/repo/counter/counter.cpp
~$ cd ~/repo/counter
在EOS中贸桶,一個(gè)智能合約類總是需要繼承eosio庫(kù)的contract類
contract類的_self成員用來(lái)記錄合約的部署賬戶,因此繼承類需要調(diào)用 contract類的構(gòu)造函數(shù)來(lái)初始化該成員變量桌肴。
include <eosiolib/eosio.hpp>
class counter_contract:public eosio::contract {
public:
//account_name 是uint64_t類型在EOS開發(fā)包中的別名皇筛,表示部署合約
//的賬戶名稱經(jīng)過base32編碼后的結(jié)果。因此在EOS中坠七,一個(gè)賬戶名最長(zhǎng)為12個(gè)字符水醋。
counter_contract(account_name self):eosio::contract(self){}
//@abi action
void increase() {
//在cleos客戶端輸出顯示指定的字符串
eosio::print("INCREASE => ",value++);
}
//@abi action
void decrease() {
eosio::print("DECREASE => ",value--);
}
private:
uint64_t value;
};
//合約入口
EOSIO_ABI(counter_contract, (increase)(decrease))
和其他C++程序一樣,我們需要先引入eosio庫(kù)的 頭文件彪置。由于EOS開發(fā)包中的大多數(shù)類型和api都定義在eosio命名空間拄踪, 因此在后續(xù)的代碼中,需要使用eosio::前綴來(lái)聲明其命名空間拳魁, 例如eosio::contract用來(lái)指代eosio命名空間中的contract類惶桐。
increase()方法上的@abi action注解用來(lái)告訴abi生成器,這個(gè)方法 是一個(gè)action處理函數(shù)潘懊,如果缺少該注解姚糊,生成的abi中可能不會(huì)出現(xiàn)該 方法的信息。
宏EOSIO_ABI是EOS開發(fā)包中用來(lái)聲明合約的ABI信息的一個(gè)預(yù)定義宏授舟, 每一個(gè)合約類都需要使用這個(gè)宏來(lái)聲明其所動(dòng)作處理能力救恨。我們將在下一 節(jié)詳細(xì)講解。
EOSIO_ABI這個(gè)預(yù)定義宏的作用有兩個(gè):
引導(dǎo)abi生成器發(fā)現(xiàn)合約的abi信息
展開生成合約模塊的入口調(diào)用释树。
EOSIO_ABI(counter_contract,(increase)(decrease))
宏調(diào)用聲明了counter_contract類中定義了increase和decrease 這兩個(gè)動(dòng)作肠槽,abi生成器將根據(jù)這些線索繼續(xù)搜索相應(yīng)的動(dòng)作實(shí)現(xiàn) 定義,例如參數(shù)列表和類型躏哩。
EOSIO_ABI宏展開后其實(shí)是一個(gè)apply()函數(shù)的實(shí)現(xiàn)署浩。類似于標(biāo)準(zhǔn)C程序中 的main()函數(shù),apply()函數(shù)是EOS中智能合約的入口函數(shù)扫尺,該函數(shù)將負(fù)責(zé) 處理分發(fā)至本合約模塊的動(dòng)作
從上面的代碼容易看出筋栋,對(duì)應(yīng)nodeos轉(zhuǎn)發(fā)來(lái)的 動(dòng)作,該函數(shù)將創(chuàng)建一個(gè)新的counter_contract實(shí)例正驻,然后調(diào)用該實(shí)例的對(duì)應(yīng) 方法弊攘。
EOSIO_ABI既是合約執(zhí)行的入口,可以引導(dǎo)abi生成器發(fā)現(xiàn)合約的abi信息
nodeos每次執(zhí)行合約方法姑曙,都會(huì)創(chuàng)建一個(gè)新的合約實(shí)例
C++編寫的合約代碼需要先進(jìn)行編譯才能在EOS虛擬機(jī)上運(yùn)行襟交。不過 和通常的C++編譯目標(biāo)不同,EOS的編譯輸出是WebAssembly格式的指令模塊 —— WASM模塊伤靠〉酚颍可以把WebAssembly視為一種匯編語(yǔ)言,只是它不依賴于特定的硬件架構(gòu), 而是運(yùn)行在WebAssembly虛擬機(jī)之上
wasm是二進(jìn)制格式焕梅,與之對(duì)應(yīng)的wast則是文本格式的迹鹅,使用wast便于開發(fā)人員 編輯與調(diào)試,這兩種格式之間可以互相轉(zhuǎn)換贞言。
EOS提供了一個(gè)命令行工具eosiocpp用來(lái)處理合約代碼斜棚。這個(gè)工具有兩個(gè)作用: 編譯生成wasm/wast、提取abi信息该窗。
使用-o或--outname選項(xiàng)來(lái)執(zhí)行eosiocpp命令弟蚀,即可生成wasm模塊及wast文件。 例如酗失,下面的代碼為counter.cpp生成wast及wasm模塊:
~/repo/counter$ eosiocpp -o counter.wast counter.cpp`
注意义钉,只需要指定輸出的wast文件名即可,eosiocpp會(huì)自動(dòng)生成對(duì)應(yīng)的wasm模塊
除了編譯出wasm模塊级零,EOS合約的構(gòu)建過程還包括另一個(gè)環(huán)節(jié):抽取合約的ABI信息断医。
ABI(Application Binary Interface),即應(yīng)用程序二進(jìn)制接口奏纪, 是JSON格式的合約接口描述文件,它支持使用不同的開發(fā)語(yǔ)言訪問智能合約斩启。 如果需要從區(qū)塊鏈外部訪問合約序调,就必須利用這個(gè)描述文件。
使用-g或--genabi參數(shù)來(lái)執(zhí)行eosiocpp命令兔簇,就可以生成指定合約類對(duì)應(yīng)的ABI 接口文件发绢。例如:
~/repo/counter$ eosiocpp -g counter.abi counter.cpp`
一旦我們生成了合約的wasm/wast文件和abi文件,就可以部署到指定的賬戶了 —— 在EOS中垄琐,合約只有部署到賬戶才可以使用边酒,而且一個(gè)賬戶最多只能部署一個(gè)合約, 當(dāng)你將一個(gè)新的合約部署到一個(gè)賬戶狸窘,該賬戶之前部署的合約將不再可用墩朦。
因此,我們可以將部署了合約代碼的賬戶稱為代碼托管賬戶翻擒,同時(shí)在EOS的文檔中氓涣, 也經(jīng)常使用code來(lái)表示部署了合約的賬戶。
在執(zhí)行以下操作之前陋气,別忘了啟動(dòng)nodeos和keosd劳吠!
首先我們使用方便腳本生成一個(gè)賬戶sodfans,你知道巩趁,它包含了密鑰創(chuàng)建痒玩、密鑰 導(dǎo)入、賬戶創(chuàng)建等多個(gè)環(huán)節(jié):
~/repo/counter cleos set contract sodfans ../counter
需要指出的是蠢古,合約目錄名需要與wasm/wast以及abi文件名一致奴曙,例如,在上面的 代碼中我們指定合約目錄名為../counter便瑟,那么set contract子命令就會(huì)在該 目錄下尋找counter.wasm缆毁、counter.wast以及counter.abi。
合約的部署是由賬戶eosio中的代碼完成的到涂,交易包含兩個(gè)動(dòng)作: setcode和setabi脊框,分別用于將合約的wasm字節(jié)碼和abi信息寫入授權(quán)執(zhí)行的 賬戶sodfans
使用cleos的push action子命令向指定的合約發(fā)送動(dòng)作。例如:
~/repo/counter$ cleos push action sodfans increase '[]' -p sodfans
容易理解践啄,我們需要在push action子命令中首先指定目標(biāo)合約浇雹, 這等價(jià)于指定部署該合約的賬戶,即sodfans屿讽;然后指定希望在 該合約上執(zhí)行的動(dòng)作increase昭灵;由于increase動(dòng)作不需要參數(shù), 因此使用[]表示一個(gè)空的參數(shù)清單伐谈。
所有的交易動(dòng)作都需要一個(gè)執(zhí)行賬戶的授權(quán)烂完,使用-p選項(xiàng)來(lái)指定授權(quán)賬戶。 在上面的調(diào)用中诵棵,我們使用sodfans賬戶進(jìn)行交易授權(quán)抠蚣,這意味著該交易 將使用sodfans的私鑰進(jìn)行簽名。
build-contract.sh腳本用來(lái)編譯指定的合約CPP代碼文件同時(shí)提取abi信息履澳,并將 結(jié)果wasm/wast和abi文件保存在當(dāng)前目錄下的build子目錄嘶窄。
下面的命令執(zhí)行后,將在當(dāng)前目錄的build/counter子目錄下生成 counter.wasm距贷、counter.wast和counter.abi文件:
~/repo/counter ls build/counter
counter.abi counter.wasm counter.wast
deploy-contrat.sh
deploy-contract.sh腳本用來(lái)部署指定的合約柄冲,參數(shù)為部署賬戶和合約目錄。 例如忠蝗,下面的命令將build/counter目錄下的合約代碼及abi部署到賬戶sodfans:
~/repo/counter$ deploy-contract.sh sodfans build/counter
以下合約代碼现横,通過eosio::require_auth(actor);校驗(yàn)提交動(dòng)作時(shí)輸入的為一個(gè)真實(shí)的賬號(hào);通過print(eosio::name{actor}," INCREASE => ",value++)來(lái)顯示的打印出是誰(shuí)提交的動(dòng)作什湘。
void increase(account_name actor){
eosio::require_auth(actor);
eosio::print(eosio::name{actor}," INCREASE => ",value++);
}
我們的計(jì)數(shù)器合約的另一個(gè)瑕疵长赞,是它其實(shí)計(jì)不了數(shù)!
無(wú)論你提交多少次increase動(dòng)作闽撤,你會(huì)看到cleos終端顯示的計(jì)數(shù)值始終為0得哆。 這是因?yàn)槲覀冊(cè)噲D使用對(duì)象成員變量value來(lái)記錄之前的值,但是哟旗,每次 處理nodeos分發(fā)過來(lái)的動(dòng)作時(shí)贩据,合約模塊總是會(huì)重新實(shí)例化一個(gè)新的合約對(duì)象栋操!
所以我們需要將計(jì)數(shù)器的值持久化到區(qū)塊鏈上。
由于EOS合約運(yùn)行在WASM虛擬機(jī)環(huán)境中饱亮,因此在EOS合約中沒有辦法使用你習(xí) 慣的文件系統(tǒng) —— 例如fstream —— 來(lái)實(shí)現(xiàn)狀態(tài)的持久化矾芙。唯一的 辦法是使用EOS提供的區(qū)塊鏈數(shù)據(jù)庫(kù) —— 多索引表(multi-index table)。
為了利用多索引表持久化近上,我們需要對(duì)increase()方法的實(shí)現(xiàn)做 簡(jiǎn)單地調(diào)整:首先需要從數(shù)據(jù)表中讀取當(dāng)前值剔宪,然后遞增,最后用新的值更新數(shù)據(jù)表壹无。
和以太坊不同葱绒,在EOS中要發(fā)行代幣并不需要編寫自己的合約,只需要使 用系統(tǒng)的eosio.token合約就可以了
代幣合約提供了三個(gè)方法:create斗锭、issue和transfer分別用于代幣 的創(chuàng)建地淀、發(fā)行與轉(zhuǎn)賬。這幾個(gè)方法的實(shí)質(zhì)就是操作兩張多索引表:stat和accounts岖是。
例如帮毁,當(dāng)提交create動(dòng)作創(chuàng)建一個(gè)代幣時(shí),就是在stat表中注冊(cè) 代幣信息豺撑;當(dāng)提交issue動(dòng)作發(fā)行一個(gè)代幣時(shí)烈疚,就是更新stat表中該代幣 的供給量,同時(shí)在accounts表中登記發(fā)行人持有的代幣總量聪轿;而提交 transfer動(dòng)作執(zhí)行轉(zhuǎn)賬時(shí)胞得,則是更新轉(zhuǎn)出和轉(zhuǎn)入賬戶的accounts表數(shù)據(jù)。
當(dāng)一個(gè)賬戶希望在EOS上發(fā)行自己的代幣時(shí)屹电,首先需要由代幣合約的托管賬戶 在系統(tǒng)中登記擬發(fā)行代幣的信息,例如代幣符號(hào)跃巡、最大發(fā)行量和授權(quán)發(fā)行人賬戶 等信息危号。這一申請(qǐng)流程是在EOS系統(tǒng)外完成的。
一旦合約托管賬戶素邪,例如eosio.token完成了代幣創(chuàng)建操作外莲,登記過的授權(quán) 發(fā)行人賬戶,例如happy.com就可以在最大發(fā)行量范圍之內(nèi)兔朦,向特定的賬戶 偷线,例如tommy發(fā)行一定數(shù)量的代幣。
任何賬戶都可以向其他賬戶轉(zhuǎn)讓其持有的代幣沽甥,也可以查詢賬戶的代幣余額声邦。
在接下來(lái)的課程中,我們將參照上圖實(shí)現(xiàn)HAPY代幣的整個(gè)流轉(zhuǎn)過程摆舟。 因此首先使用方便腳本創(chuàng)建相應(yīng)的賬號(hào):
~ new-account.sh happy.com
~ new-account jerry
由于是我們自己的測(cè)試鏈亥曹,首先需要?jiǎng)邮植渴餰osio.token系統(tǒng)合約:
~$ cleos set contract eosio.token ~/eos/build/contracts/eosio.token
現(xiàn)在邓了,賬戶eosio.token已經(jīng)部署了系統(tǒng)合約eosio.token,希望你能 分清這兩個(gè)eosio.token的不同指代
發(fā)行自己代幣的第一步媳瞪,是由代幣合約托管賬戶向合約提交create動(dòng)作來(lái)注冊(cè) 代幣信息骗炉,例如發(fā)行人賬戶、最大發(fā)行量和代幣符號(hào)蛇受。
例如句葵,下面的命令將注冊(cè)一條新的代幣信息,發(fā)行賬戶為happy.com兢仰,最大發(fā)行量 為100萬(wàn)乍丈,代幣符號(hào)為HAPY:
~$ cleos push action eosio.token create '["happy.com","1000000.00 HAPY"]' -p eosio.token
需要再一次強(qiáng)調(diào)的是,只有系統(tǒng)代幣合約的部署賬戶旨别,也就是eosio.token 才有權(quán)限執(zhí)行create動(dòng)作來(lái)注冊(cè)一個(gè)新的代幣信息诗赌。
當(dāng)代幣信息登記成功后,我們可以查詢stat表
cleos get table eosio.token HAPY stat
注意stat表是以代幣符號(hào)作為數(shù)據(jù)域來(lái)分隔不同代幣的記錄秸弛。
一旦合約賬戶注冊(cè)了代幣信息铭若,發(fā)行人就可以進(jìn)行代幣發(fā)行了。
例如递览,下面的命令向賬戶tommy發(fā)行100枚HAPY幣:
~$ cleos push action eosio.token issue '["tommy","100.00 HAPY"]' -p happy.com
注意該交易是由發(fā)行人賬戶happy.com授權(quán)的叼屠,只有在創(chuàng)建代幣時(shí)登記 的發(fā)行人賬戶,才可以執(zhí)行發(fā)行動(dòng)作绞铃。
現(xiàn)在查看stat表的內(nèi)容镜雨,你會(huì)發(fā)現(xiàn)supply已經(jīng)是100.00 HAPY了:
~$ cleos get table eosio.token HAPY stat
{
"rows": [{
"supply": "100.00 HAPY",
"max_supply": "1000000.00 HAPY",
"issuer": "happy.com"
}]
}
發(fā)行代幣動(dòng)作更新的另一個(gè)表是accounts,利用這個(gè)表可以查看指定賬戶的余額
cleos get table eosio.token tommy accounts
也可以使用cleos的get currency balance子命令查看指定代幣合約上指定賬戶 的余額儿捧,例如:
~$ cleos get currency balance eosio.token tommy
100.00 HAPY
容易理解荚坞,這個(gè)命令使用的就是accounts表中的數(shù)據(jù)。
一個(gè)賬戶可以將其持有的代幣轉(zhuǎn)給其他賬戶菲盾。
例如颓影,tommy利用下面的命令向jerry轉(zhuǎn)了2個(gè)HAPY幣:
cleos push action eosio.token transfer '["tommy","jerry","2.00 HAPY"]' -p tommy
同樣需要指出,提交轉(zhuǎn)賬交易必須獲得轉(zhuǎn)出賬戶(tommy)的授權(quán)懒鉴。
轉(zhuǎn)賬的另一種方法是使用cleos封裝過的transfer子命令诡挂,例如, 下面的命令同樣實(shí)現(xiàn)了tommy向jerry轉(zhuǎn)2個(gè)HAPY代幣:
~ cleos get currency balance eosio.token tommy
98.00 HAPY
~$ cleos get currency balance eosio.token jerry
2.00 HAPY
一切都如預(yù)期璃俗。
在前面的課程中,我們使用客戶端工具cleos與EOS智能合約交互悉默, 這很方便城豁,但是如果期望在我們自己的代碼增加EOS智能合約的訪問 功能,cleos就不夠了麦牺,我們需要挖的稍微再深入一層钮蛛。
前面提到過鞭缭,cleos其實(shí)只是一個(gè)和用戶交互的殼,絕大多數(shù)的任務(wù) 其實(shí)是在nodeos和keosd上完成的魏颓,cleos是通過訪問這兩個(gè) 服務(wù)器所提供的HTTP RPC API來(lái)實(shí)現(xiàn)具體的功能岭辣。
我們也需要這么做:跳過cleos,在代碼里直接訪問EOS節(jié)點(diǎn)旳RPC接口
例如甸饱,cleos的get info子命令可以獲取區(qū)塊鏈的統(tǒng)計(jì)信息:
~ curl http://127.0.0.1:8888/v1/chain/get_info -s | jq
jq是一個(gè)命令行json解析器偷遗,我們使用它來(lái)更友好的顯示nodeos的響應(yīng)結(jié)果。
EOS的RPC API分為幾個(gè)不同的系列驼壶,例如chain/系列的API用于操作區(qū)塊鏈氏豌, 而wallet/系列的API則用戶錢包操作
nodeos默認(rèn)是不加載任何API插件的,因此應(yīng)當(dāng)根據(jù)需要在其配置文件中加載 相應(yīng)的插件热凹,例如泵喘,在nodeos配置文件中開啟所有API插件:
plugin = chain-api-plugin
plugin = history-api-plugin
plugin = net-api-plugin
plugin = producer-api-plugin
plugin = wallet-api-plugin
一旦nodeos啟用了wallet-api-plugin,它也可以管理錢包了 —— 這意味著你可以 使用單一的nodeos來(lái)完成nodeos+keosd的工作,不需要開啟額外的keosd服務(wù)。
但是由于cleos默認(rèn)總是連接8888端口的nodeos和8900端口的keosd娩梨,因此通常 我們還是會(huì)使用單獨(dú)的在8900端口監(jiān)聽的keosd服務(wù)進(jìn)行錢包管理,以避免在命令行 顯式指定錢包服務(wù)器的地址鲜锚。
集成錢包服務(wù)的nodeos與獨(dú)立的keosd,除了默認(rèn)監(jiān)聽端口的區(qū)別苫拍,另一個(gè)差異就是 它們使用不同的目錄來(lái)保存錢包文件芜繁,因此在keosd中創(chuàng)建的錢包,默認(rèn)情況下在 nodeos的錢包服務(wù)中是找不到的绒极。
keosd默認(rèn)總是加載wallet-api-plugin浆洗,因此無(wú)須額外的配置來(lái)啟用API。
使用EOS節(jié)點(diǎn)旳RPC API集峦,基本上可以在你的程序中完成cleos能做的所有的任務(wù)。 在前面里我們用cleos命令行實(shí)現(xiàn)了代幣的轉(zhuǎn)賬功能抠刺。
整個(gè)代幣轉(zhuǎn)賬流程涉及到nodeos和keosd的多個(gè)RPC API塔淤,除了對(duì)交易進(jìn)行簽名是利用keosd完成的,其他的調(diào)用都是提交給nodeos的速妖。
假設(shè)tommy要轉(zhuǎn)給jerry一些HAPY代幣高蜂,我們首先要做的是利用chain/abi_json_to_bin 調(diào)用將這個(gè)動(dòng)作進(jìn)行序列化,以便簽名:
~$ curl http://127.0.0.1:8888/v1/chain/abi_json_to_bin -X POST -d '{
"code":"eosio.token",
"action":"transfer",
"args":{
"from": "tommy",
"to": "jerry",
"quantity":"2.00 HAPY",
"memo":"take care"
}}'
得到一個(gè)二進(jìn)制的字符串binargs罕容,在sign_transaction和push_transaction中作為 data 請(qǐng)求參數(shù):
"binargs":"00000000002f25cd00000000...4150590000000974616b652063617265"
接下來(lái)我們需要利用chain/get_info和chain/get_block這兩個(gè)調(diào)用查詢鏈ID和頭塊信息
~ curl http://127.0.0.1:8888/v1/chain/get-block -X POST -s -d '{
"block_num_or_id": 3318
}' | jq
在這個(gè)響應(yīng)中包含了我們感興趣的兩個(gè)信息 —— 時(shí)間戳timestamp和參考?jí)K前綴ref_block_prefix:
"timestamp": "2018-07-18T00:38:40.000"
...
"block_num": 3318
"ref_block_prefix": 1666290079
收集到上面信息之后备恤,我們就可以對(duì)轉(zhuǎn)賬交易進(jìn)行簽名了:
~curl http://127.0.0.1:8888/v1/chain/push_transaction -X POST -s -d '{
"compression": "none",
"transaction": {...} ,
"signatures": ["SIG_K1_KiZkwynkcgbN...aTD26todc4fRTn357328z1xTgVh1yHZk3o"]
}' | jq
響應(yīng)是我們的交易收據(jù):
"transaction_id": "6b4331de05f413b499f4050557e56a503800974daa5e1682c8294dc5c164f96c",
....
現(xiàn)在畜眨,你可以檢查tommy和jerry的代幣余額了昼牛。
你可以在~/repo/chapter6/rpc-transfer.sh中查看上述轉(zhuǎn)賬流程的bash腳本參考實(shí)現(xiàn)代碼
深入理解EOS的內(nèi)部機(jī)制,RPC API提供了很好的切入點(diǎn)胶果。但是匾嘱, 從前面的轉(zhuǎn)賬流程實(shí)現(xiàn)來(lái)看,對(duì)于應(yīng)用開發(fā)而言早抠,直接使用它實(shí)在是效率太低了霎烙。
在大多數(shù)情況下,我們應(yīng)該使用更高效一點(diǎn)的封裝開發(fā)庫(kù)蕊连,例如eosjs —— EOS官方的針對(duì)JavaScript的RPC API封裝庫(kù)悬垃, 可以用于Nodejs環(huán)境和瀏覽器環(huán)境。eosjs封裝了chain/和history/系列的API甘苍,同時(shí)可以根據(jù)abi信息 自動(dòng)為智能合約生成封裝對(duì)象尝蠕,對(duì)于應(yīng)用開發(fā)人員來(lái)講,eosjs要比直接使用RPC高效多了载庭。
首先引入eosjs包看彼,然后創(chuàng)建一個(gè)實(shí)例:
const Eos = require('eosjs')
const nodeos = Eos({
httpEndpoint: 'http://localhost:8888',
keyProvider: ['5KJ9cYKZJWsF2Q7u6HrARN5NiXXTUJmoSRVGP13jT2isfQT26ru']
})
在創(chuàng)建Eos實(shí)例時(shí),需要指定一個(gè)配置對(duì)象囚聚,其中的httpEndpoint聲明nodeos 的監(jiān)聽地址靖榕,keyProvider提供一組用來(lái)簽名交易的私鑰 —— 我們將實(shí)現(xiàn)從tommy 到j(luò)erry的轉(zhuǎn)賬,因此這里只需要提供tommy的私鑰顽铸。
一旦創(chuàng)建了Eos實(shí)例茁计,就可以使用其contract()方法構(gòu)建一個(gè)對(duì)應(yīng)于合約 的js封裝對(duì)象,該對(duì)象具有與合約動(dòng)作同名的方法:
nodeos.contract('eosio.token')
.then( contract => contract.transfer('tommy','jerry','2.00 HAPY',{authorization:['tommy']}))
.then( rsp => console.log(rsp.transaction_id))
.catch( err => console.log(err))
容易注意到在調(diào)用封裝合約對(duì)象的transfer()方法時(shí)谓松,最后一個(gè)參數(shù)對(duì)象使用 authorization聲明了授權(quán)本次交易的賬戶星压,該賬戶的私鑰必須出現(xiàn)在我們創(chuàng)建 Eos實(shí)例時(shí)提供的keyProvider列表中践剂。
eosjs很容易使用,但是在前一節(jié)的代碼中娜膘,有沒有讓你擔(dān)心的問題逊脯?
在代碼中包含私鑰安全嗎?
的確不安全劲绪,尤其當(dāng)你希望在瀏覽器中使用eosjs時(shí)男窟,更加不安全。
前一節(jié)的代碼我們應(yīng)該視為使用eosjs操作區(qū)塊鏈的概念驗(yàn)證(Proof of Concept) 代碼贾富,而不應(yīng)在生產(chǎn)環(huán)境中使用歉眷。事實(shí)上,eosjs預(yù)留了相應(yīng)擴(kuò)展接口: 使用自定義的簽名提供器
當(dāng)我們?cè)趯?shí)例化Eos對(duì)象時(shí)颤枪,如果是使用keyProvider提供的私鑰汗捡,那么eosjs 將創(chuàng)建一個(gè)默認(rèn)的簽名提供器,該提供器將根據(jù)具體交易的需求畏纲,使用這些私鑰 進(jìn)行本地離線簽名 —— 也就是說(shuō)eosjs默認(rèn)不需要使用錢包服務(wù)器keosd扇住,所以它 需要你提供發(fā)起交易的賬戶的私鑰。
但是我們可以在創(chuàng)建Eos實(shí)例對(duì)象時(shí)盗胀,指定一個(gè)自定義的簽名提供器來(lái) 避免泄漏私鑰艘蹋,例如:
const PRIVATE_KEY = '...'
const nodeos = Eos({
httpEndpoint: 'http://localhost:8888',
signProvider: function({transaction,buf,sign}){
//should return a signature
return sign(buf,PRIVATE_KEY)
}
})
簽名提供器是一個(gè)函數(shù),eosjs會(huì)傳入一個(gè)參數(shù)對(duì)象票灰,其中 transaction是要簽名的交易女阀,buf是序列化后的交易,sign是 默認(rèn)的簽名函數(shù)屑迂。簽名提供器應(yīng)當(dāng)根據(jù)這些信息來(lái)返回交易的簽名浸策, 例如,上面的代碼中使用默認(rèn)的簽名函數(shù)惹盼,用指定的私鑰進(jìn)行簽名庸汗。
如果在瀏覽器環(huán)境中使用eosjs,一種解決方案是使用eos的scatter錢包手报,這 是一個(gè)瀏覽器插件蚯舱,類似于以太坊的metamask錢包。scatter錢包會(huì)在瀏覽器 的本地存儲(chǔ)中加密保存你的私鑰掩蛤,并在你訪問任何網(wǎng)址時(shí)向?yàn)g覽器注入一個(gè)scatter對(duì)象晓淀。 使用該注入對(duì)象創(chuàng)建的eos實(shí)例,將由scatter接管交易簽名過程
const network = {
protocol:'http', // Defaults to https
blockchain:'eos',
host:'127.0.0.1', // ( or null if endorsed chainId )
port:8888, // ( or null if defaulting to 80 )
chainId:1 || 'abcd',
}
const eosOptions = {};
const eos = scatter.eos( network, Eos, eosOptions, 'https' );
scatter的簽名過程和eosjs的默認(rèn)簽名過程一樣盏档,都是使用私鑰離線簽名, 只是scatter加密保存了私鑰燥爷。
另一種解決方案是和cleos一樣蜈亩,使用keosd來(lái)進(jìn)行簽名
根據(jù)前面的描述懦窘,我們只需要在簽名提供器中調(diào)用keosd的wallet/sign_transaction 接口并返回得到簽名即可,由于不需要buf和sign稚配,我們只提取參數(shù)中的transaction:
const keosdSigner = function({transaction}){}
const nodeos = Eos({
httpEndpoint: 'http://127.0.0.1:8888',
signProvider: keosdSigner
})
由于eosjs沒有封裝wallet/*系列的接口畅涂,我們需要費(fèi)點(diǎn)事,先做這個(gè)工作:
const apiGen = require('eosjs-api/lib/apigen')
const apiDefs = {
"wallet": {
"list_wallets": {
"params": null,
"results": "string[]"
},
"sign_transaction":{
"params": "array",
"results": "signed_transaction"
}
}
}
const WalletApi = function(config) {
return apiGen('v1', apiDefs, config)
}
上面的代碼基于eosjs-api的代碼道川,對(duì)wallet/*中的部分接口進(jìn)行封裝午衰。 abiGen根據(jù)所提供的api定義生成對(duì)應(yīng)的方法調(diào)用,其中方法名會(huì)轉(zhuǎn)化為 camelCase冒萄。例如臊岸,我們可以這樣調(diào)用wallet/list_wallets接口:
const keosd = WalletApi({
httpEndpoint: 'http://127.0.0.1:8900'
})
keosd.listWallets({}).then(rsp => console.log(rsp))
NEAT.
接下來(lái)keosdSigner的實(shí)現(xiàn),只需要將傳入的transaction對(duì)象推給keosd尊流, 然后提取返回結(jié)果中的簽名:
const PUBLIC_KEY = '...'
const CHAIN_ID = '...'
const keosdSigner = function({transaction}){
const payload = [
transaction,
[PUBLIC_KEY],
CHAIN_ID
]
return keosd.signTransaction(payload)
.then( rsp => rsp.signatures[0])
}
在調(diào)用wallet/sign_transaction接口時(shí)帅戒,除了要簽名的交易,另外兩個(gè) 信息也至關(guān)重要:
交易中授權(quán)賬戶的公鑰崖技,使用PUBLIC_KEY給出逻住。例如,tommy給jerry轉(zhuǎn)代幣迎献, 那么我們就需要tommy的公鑰
區(qū)塊鏈ID瞎访,可以從chain/get_info調(diào)用返回結(jié)果中提取,對(duì)于測(cè)試鏈來(lái)講吁恍,這個(gè) 值是cf057bbfb72640471fd910bcb67639c22df9f92470936cddc1ade0e2f2e7dc4f
現(xiàn)在扒秸,我們可以進(jìn)行轉(zhuǎn)賬了:
nodeos.contract('eosio.token')
.then( contract => contract.transfer('tommy','jerry','2.00 HAPY',{authorization:['tommy']}) )
.then( rsp => console.log(rsp) )
.catch( err => console.log(err) )
開發(fā)一個(gè)基于EOS的去中心化應(yīng)用 —— 便簽DApp, 來(lái)實(shí)現(xiàn)日常任務(wù)事項(xiàng)的管理践盼,提高工作效率
在這個(gè)項(xiàng)目的開發(fā)中鸦采,我們將綜合利用前面學(xué)到的知識(shí)來(lái)開發(fā) 一個(gè)用于管理待辦事宜的智能合約,以及提供給最終用戶的 基于React開發(fā)的前端網(wǎng)頁(yè)操作界面咕幻。
整個(gè)應(yīng)用本質(zhì)上是去中心化的渔伯,在開發(fā)過程中我們使用一個(gè) web服務(wù)器來(lái)提供網(wǎng)頁(yè),但完全可以不使用web服務(wù)器而將網(wǎng)頁(yè)部署為本地文件
按照以下步驟來(lái)完成這個(gè)項(xiàng)目:
需求分析:分析便簽DApp的核心功能需求
用戶界面設(shè)計(jì):采用組件化思路設(shè)計(jì)便簽DApp的用戶界面
數(shù)據(jù)結(jié)構(gòu)設(shè)計(jì):設(shè)計(jì)便簽DApp所需要的鏈上存儲(chǔ)結(jié)構(gòu)
智能合約代碼實(shí)現(xiàn):編寫便簽智能合約
前端代碼實(shí)現(xiàn):編寫前端具體實(shí)現(xiàn)代碼
運(yùn)行調(diào)試:檢驗(yàn)最終的成果
讓我們從用戶的角度思考一下肄程,便簽DApp的使用方法:
添加待辦事項(xiàng)锣吼、關(guān)閉待辦事項(xiàng)、刪除待辦事項(xiàng)蓝厌、查看待辦事項(xiàng)清單
將用戶界面分割為幾個(gè)不同的React組件:
TodoBox組件處于最外層玄叠,將顯示標(biāo)題和統(tǒng)計(jì)數(shù)據(jù),并作為TodoList組件 和TodoEditor組件的容器拓提。
TodoList組件是TodoBox的子組件读恃,它同時(shí)也是一個(gè)簡(jiǎn)單的列表項(xiàng)容器, 其成員為TodoItem組件。
TodoEditor組件是TodoBox的另一個(gè)子組件寺惫,它負(fù)責(zé)采集用戶的輸入疹吃。
React是狀態(tài)驅(qū)動(dòng)的組件化界面庫(kù),因此我們還需要設(shè)計(jì)視圖狀態(tài):
我們可以使用一個(gè)數(shù)組tasks來(lái)保存所有的待辦事宜西雀,每一個(gè)待辦事宜 都對(duì)應(yīng)一個(gè)TodoItem組件萨驶;同時(shí)我們使用一個(gè)布爾變量loading來(lái)表示 是否正在操作區(qū)塊鏈,當(dāng)loading被置位時(shí)艇肴,在TodoBox底部的狀態(tài)欄 我們將顯示額外的文字來(lái)提醒用戶目前正在操作區(qū)塊鏈腔呜。
對(duì)于一個(gè)待辦事宜,只需要兩個(gè)字段就可以表示了:描述文本和是否完成 的標(biāo)志再悼,分別使用一個(gè)字符串和一個(gè)布爾變量即可核畴。
下面是一個(gè)示例狀態(tài)樹,內(nèi)容恰好對(duì)應(yīng)上面的示例界面:
state = {
tasks: [
{desc:'搭建EOS開發(fā)環(huán)境',done:false},
{desc:'發(fā)行HAPY代幣',done:false},
{desc:'成功ICO',done:false}
],
loading: false
}
容易理解帮哈,當(dāng)我們點(diǎn)擊TodoEditor中的保存按鈕膛檀,或者點(diǎn)擊TodoItem中的 刪除按鈕,都會(huì)觸發(fā)對(duì)狀態(tài)樹的修改娘侍,進(jìn)而重新驅(qū)動(dòng)視圖的更新咖刃。
在大多數(shù)情況下,React組件都不應(yīng)該持有自己的狀態(tài)憾筏,而是盡可能使用 外部傳入的屬性來(lái)調(diào)整自己的行為嚎杨。這可以讓組件更輕量,嵌套更方便氧腰, 也更便于調(diào)試與跟蹤枫浙。
因此我們將實(shí)現(xiàn)一個(gè)單獨(dú)的狀態(tài)樹類來(lái)管理便簽應(yīng)用的狀態(tài):
除了兩個(gè)狀態(tài)節(jié)點(diǎn)loading和tasks,一個(gè)TodoStore類還將實(shí)現(xiàn) 修改狀態(tài)的方法古拴,例如:
createTask():創(chuàng)建待辦事宜
deleteTask():刪除待辦事宜
toggleTask():切換待辦事宜完成狀態(tài)
setLoading():設(shè)置加載狀態(tài)
考慮到可能需要從網(wǎng)絡(luò)加載初識(shí)狀態(tài)箩帚,因此我們?yōu)門odoStore增加一個(gè) 額外的方法initState()來(lái)初始化狀態(tài),而不是在構(gòu)造函數(shù)中完成黄痪。
為了應(yīng)用狀態(tài)樹紧帕,我們需要將其與一個(gè)頂層React組件關(guān)聯(lián)起來(lái),以便利用其 setState()方法來(lái)驅(qū)動(dòng)整個(gè)視圖的重繪桅打,私有成員_host用來(lái)保存這個(gè) 關(guān)聯(lián)的React組件對(duì)象是嗜。
class TodoMemStore{
constructor(host){
this._host = host
this.loading = false,
this.tasks = []
this.initState = () => {}
this.toggleTask = id => {
let idx = this._find(id)
if(idx <0) return
this.tasks[idx].done = ! this.tasks[idx].done
this._host.setState({tasks:this.tasks})
}
this.removeTask = id => {
let idx = this._find(id)
if(idx < 0) return
this.tasks.splice(idx,1)
this._host.setState({tasks:this.tasks})
}
this.createTask = desc => {
let id = Date.now()
this.tasks.push({id,desc,done:false})
this._host.setState({tasks:this.tasks})
}
this._find = id => {
for(let i=0;i<this.tasks.length;i++){
if(this.tasks[i].id === id) return i
}
return -1
}
}
}
視圖的狀態(tài)需要傳播到相關(guān)的組件,同時(shí)相關(guān)的組件也需要更新視圖的狀態(tài)挺尾, 我們將確定如何管理視圖狀態(tài)的傳遞與更新鹅搪。
第一種方案是采用標(biāo)準(zhǔn)的逐級(jí)傳遞方案:
在這種方案中,狀態(tài)從組件樹的根節(jié)點(diǎn)(TodoBox)作為屬性流入遭铺,然后 逐級(jí)傳遞到后代組件丽柿,而后代組件對(duì)狀態(tài)的修改恢准,也需要通過事件逐級(jí)上傳 至根組件,由根組件最終完成甫题。這種方案非常規(guī)范有序顷歌,但可以想像,需要不少的膠水代碼來(lái)完成逐級(jí)傳遞 的任務(wù)幔睬。
另一種方案是采用全局上下文對(duì)象,每個(gè)組件都可以直接訪問到狀態(tài)樹芹扭。
由于每個(gè)組件都可以訪問到狀態(tài)樹麻顶,因此會(huì)帶來(lái)更簡(jiǎn)潔的代碼。在本項(xiàng)目的 實(shí)現(xiàn)中舱卡,我們將采用這種方案來(lái)傳遞和更新視圖狀態(tài)辅肾。
出于簡(jiǎn)單化考慮,在這個(gè)項(xiàng)目中我們將使用React內(nèi)置的Context接口來(lái) 向各組件注入狀態(tài)樹轮锥。當(dāng)然矫钓,你也可以使用redux。
Context(上下文)是軟件開發(fā)中經(jīng)常遇到的一個(gè)詞舍杜⌒履龋基本上,任何時(shí)候你想 避免繁瑣的多層嵌套函數(shù)的參數(shù)傳遞既绩,都可以借助于一個(gè)保有全局信息的 上下文對(duì)象概龄。
React的Context API采用發(fā)布訂閱模型,由發(fā)布者組件在頂層提供上下文對(duì)象饲握, 其他組件使用訂閱者組件接收上下文對(duì)象
首先創(chuàng)建一對(duì)組件:發(fā)布者/訂閱者私杜,并設(shè)置初始上下文對(duì)象為null:
const {Provider,Consumer} = React.createContext(null)
然后我們定義一個(gè)頂層組件來(lái)封裝發(fā)布者,并將其上下文設(shè)置為TodoStore對(duì)象:
//TodoProvider.jsx
export default class TodoProvider extend React.Component{
constructor(props){
super(props)
this.state = new TodoMemStore(this)
}
componentDidMount(){
this.state.initState()
}
render(){
return <Provider value={this.state}>{this.props.children}</Provider>
}
}
現(xiàn)在可以在使用訂閱者組件來(lái)將上下文注入其他組件救欧。例如衰粹,對(duì)于TodoBox組件:
//TodoBox.jsx
class TodoBox extends React.Component {...}
export default props => (
<Consumer>
{ store => <TodoBox {...props} store={store}/>}
</Consumer>
)
現(xiàn)在整合到一起,渲染到DOM樹:
ReactDOM.render(
<TodoProvider><TodoBox/></TodoProvider>,
document.getElementById('app'));
import React from 'react'
import TodoStore from '../services/TodoEosStore'
//import TodoStore from '../services/TodoMemStore'
export const TodoContext = React.createContext({})
export class TodoProvider extends React.Component{
constructor(props){
super(props)
this.state = new TodoStore(this)
}
componentDidMount(){
this.state.initState()
}
render(){
return (
<TodoContext.Provider value={this.state}>
{this.props.children}
</TodoContext.Provider>
)
}
}
設(shè)計(jì)便簽合約的狀態(tài)和動(dòng)作:
便簽合約的核心是狀態(tài)表todos笆怠,它存儲(chǔ)所有的待辦事宜铝耻,合約的三個(gè) 方法create、remove和toggle則用于操作這個(gè)狀態(tài)表骑疆。
首先我們需要一張數(shù)據(jù)表來(lái)保存每個(gè)賬戶的便簽記錄田篇,記錄的內(nèi)容包括 記錄序號(hào)、待辦事宜文本及是否完成的標(biāo)志:
//@abi table todos
struct todo{
uint64_t id;
std::string desc;
bool done;
auto primary_key() const { return id; }
EOSIO_SERIALIZE(todo,(id)(desc)(done))
}
typedef multi_index<N(todos),todo> todo_table;
注意我們?cè)谧⒔庵新暶髁吮砻麨閠odos箍铭,因此在聲明多索引表類型時(shí)泊柬, 也要使用這個(gè)名字,即N(todos)诈火。
然后我們需要提供三個(gè)動(dòng)作供用戶來(lái)操作便簽數(shù)據(jù)表:創(chuàng)建兽赁、刪除和切換狀態(tài)状答。
create:創(chuàng)建待辦事宜
create動(dòng)作用于將一個(gè)新的待辦事宜添加到數(shù)據(jù)表中:
//@abi action
void create(account_name author,uint64_t id,std::string desc){
require_auth(author);
todo_table todos(_self,author);
todos.emplace(author,[&](auto& record){
record.id = id;
record.desc = desc;
record.done = false;
});
}
新添加的待辦事宜總是未完成的,因此create方法只需要兩個(gè)參數(shù):序號(hào)和描述文本刀崖。 由于我們需要分別保存每個(gè)賬戶的待辦事宜惊科,因此在聲明數(shù)據(jù)表變量todos時(shí),需要將其scope參數(shù)設(shè)置為提交動(dòng)作的賬戶亮钦。
remove:刪除待辦事宜
remove動(dòng)作用于從數(shù)據(jù)表中刪除指定序號(hào)的待辦事宜:
//@abi action
void remove(account_name author,uint64_t id){
require_auth(author);
todo_table todos(_self,author);
auto iter = todos.find(id);
eosio_assert(iter != todos.end(),"not found");
todos.erase(iter);
}
remove動(dòng)作處理邏輯很簡(jiǎn)單:找到指定記錄馆截,然后刪除。在上面 的代碼中我們使用eosio_assert()函數(shù)來(lái)確保指定序號(hào)的記錄存在蜂莉, 否則將拋出異常并停止繼續(xù)執(zhí)行蜡娶。
toggle:切換待辦事宜的狀態(tài)
toggle動(dòng)作用于切換數(shù)據(jù)表中指定序號(hào)待辦事宜的完成標(biāo)志:
//@abi action
void toggle(account_name author,uint64_t id){
require_auth(author);
todo_table todos(_self,author);
auto iter = todos.find(id);
eosio_assert(iter != todos.end(),"not found");
todos.modify(iter,author,[&](auto& record){
record.done = ! record.done;
});
}
合約
include <eosiolib/eosio.hpp>
class todo_contract : public eosio::contract {
public:
todo_contract(account_name self):eosio::contract(self){}
// @abi action
void create(account_name author, const uint64_t id, const std::string& desc) {
require_auth(author);
todo_table todos(_self,author);
todos.emplace(author, [&](auto& record) {
record.id = id;
record.desc = desc;
record.done = false;
});
eosio::print("todo#", id, " created");
}
// @abi action
void remove(account_name author, const uint64_t id) {
require_auth(author);
todo_table todos(_self,author);
auto iter = todos.find(id);
todos.erase(iter);
eosio::print("todo#", id, " deleted");
}
// @abi action
void toggle(account_name author, const uint64_t id) {
require_auth(author);
todo_table todos(_self,author);
auto iter = todos.find(id);
eosio_assert(iter != todos.end(), "Todo does not exist");
todos.modify(iter, author, [&](auto& record) {
record.done = ! record.done;
});
eosio::print("todo#", id, " toggle todo state");
}
private:
// @abi table todos i64
struct todo {
uint64_t id;
std::string desc;
有了TodoMemStore的實(shí)現(xiàn),有了完成的合約映穗,現(xiàn)在我們可以實(shí)現(xiàn)基于 EOS的TodoStore了窖张。
出于簡(jiǎn)單化考慮,我們將固定使用一個(gè)賬戶來(lái)操作區(qū)塊鏈蚁滋,同時(shí)使用離線 簽名方式宿接,因此使用一個(gè)變量保存這個(gè)信息:
const wallet = {
account: 'sodfans',
privateKey: '5JHZdDQ7cfHQ8PnoNqvW9kWbdsCZDTUiBBs3YLxowJEbjCKfvET'
}
在構(gòu)造函數(shù)中,我們使用一個(gè)額外的參數(shù)options來(lái)允許調(diào)用者自定義節(jié)點(diǎn)url 等信息辕录,默認(rèn)情況下睦霎,使用的合約為todo.user,表為todos:
constructor(host,options){
const defaults= {
code: 'todo.user',
table: 'todos',
wallet: wallet,
nodeosUrl: NODEOS_URL ? NODEOS_URL : 'http://127.0.0.1:8888',
keosdUrl: KEOSD_URL ? KEOSD_URL : 'http://127.0.0.1:8900',
}
this._options = Object.assign({},defaults,options)
this._host = host
this._nodeos = Eos({
httpEndpoint: this._options.nodeosUrl,
keyProvider: [this._options.wallet.privateKey],
})
}
根據(jù)合并的參數(shù)踏拜,在構(gòu)造函數(shù)中同時(shí)創(chuàng)建_nodeos成員用于后續(xù)的EOS操作碎赢。
切換待辦事宜操作將執(zhí)行合約的toggle動(dòng)作:
this.toggleTask = id => {
const opts= {authorization:this._options.wallet.account}
return this._nodeos.contract(this._options.code)
.then( contract => contract.toggle(this._options.wallet.account,id,opts) )
}
刪除待辦事宜操作將執(zhí)行合約的remove動(dòng)作:
this.removeTask = id => {
const opts= {authorization:this._options.wallet.account}
return this._nodeos.contract(this._options.code)
.then( contract => contract.remove(this._options.wallet.account,id,opts) )
}
創(chuàng)建待辦事宜操作將執(zhí)行合約的create動(dòng)作,簡(jiǎn)單地使用時(shí)間戳作為記錄id:
this.createTask = desc => {
const opts= {authorization:this._options.wallet.account}
const id = Date.now()
return this._nodeos.contract(this._options.code)
.then( contract => contract.create(this._options.wallet.account,id,desc,opts) )
}