文|李偉志
? ? ?? 關(guān)于“以太貓”的流行啡邑,相信不少人都有所耳聞,甚至入手養(yǎng)過幾只昂勒。從游戲性來說蜀细,其本質(zhì)就是一個(gè)簡單的收集交換類游戲,然鵝戈盈,是區(qū)塊鏈賦予了它魅力奠衔,讓用戶每一只貓永遠(yuǎn)不會(huì)消失、不被篡改奕谭,更重要的是可以炒(滑稽臉)涣觉,于是今天借此機(jī)會(huì)一探以太坊應(yīng)用DApp的開發(fā)過程以及開發(fā)中遇到的坑。
????????以太坊DApp介紹
????????以太坊是一個(gè)區(qū)塊鏈公有鏈平臺(tái)血柳,和比特幣類似官册,以太坊也有其代幣--以太幣,可在挖礦难捌、交易中獲得膝宁,然而,說到以太坊和比特幣的區(qū)別就是其支持智能合約根吁,一個(gè)智能合約由代碼和數(shù)據(jù)組成员淫,和其他編程語言中的類類似,一個(gè)以太坊分布式應(yīng)用DApp由眾多智能合約組成击敌,每個(gè)智能合約都有其獨(dú)特的地址介返,可以看做以太坊上的一個(gè)賬戶,可以存取以太幣沃斤,作用就像一個(gè)裁判圣蝎、中間人。一個(gè)簡單但不是很恰當(dāng)?shù)睦泳褪琴€博衡瓶,我和小明打賭明天會(huì)下雨徘公,輸?shù)娜私o贏的人一百塊,這種情況我們?cè)诂F(xiàn)實(shí)中一般會(huì)以下面兩種方法實(shí)現(xiàn):
????????依靠朋友間的信任哮针。等明天到了关面,根據(jù)下雨與否進(jìn)行交易。但這種方法一般不可行十厢,因?yàn)楫吘故桥笥训忍數(shù)娜藭?huì)自動(dòng)把昨天的打賭作為玩笑話,而贏的人也礙于面子不好意思要錢寿烟,所以交易無法達(dá)成澈驼。
????????依靠公證的第三方。OK筛武,我們這次認(rèn)真點(diǎn)缝其,找一個(gè)彼此都認(rèn)識(shí)的朋友小方作為公證,把我和小明的賭注一百塊都先存著徘六,等明天到了再給贏的人兩百塊内边。這種方法確實(shí)比第一種要好,但還是害怕就是第三方拿著兩個(gè)人的賭注夾帶私逃了待锈,這對(duì)交易雙方的損失更大漠其。
? ? ?? OK,智能合約就是為了解決以上的信任問題而誕生的竿音,由于智能合約存放于區(qū)塊鏈和屎,而區(qū)塊鏈具有的不可抵賴和不可篡改性,使得智能合約比現(xiàn)實(shí)中任意一個(gè)機(jī)構(gòu)的公信力都強(qiáng)春瞬。其實(shí)柴信,區(qū)塊鏈去中心化思想最大的優(yōu)勢(shì)就是解決了信任問題,而現(xiàn)實(shí)中最常見需要解決信任問題的場景莫過于涉及貨幣交易宽气,從以太坊的眾多DApp列表https://www.stateofthedapps.com/中看到随常,大多數(shù)都是關(guān)于交易、賭博性質(zhì)的應(yīng)用萄涯,可以說“以太貓”的橫空出世刷新了人們對(duì)于區(qū)塊鏈應(yīng)用的固有認(rèn)知绪氛。
開發(fā)準(zhǔn)備
開發(fā)以太坊DApp需要安裝以下環(huán)境或工具,以Mac OS X為例
$ brew install node以太坊DApp其他開發(fā)工具都是通過npm安裝的涝影,node.js大法好枣察,mac用戶可通過homebrew安裝。
$ npm install ethereumjs-testrpc以太坊提供的區(qū)塊鏈測試環(huán)境燃逻,所有節(jié)點(diǎn)都是虛擬的存在內(nèi)存中序目,啟動(dòng)后默認(rèn)創(chuàng)建10個(gè)賬戶。讀者也可以選擇安裝geth搭建私有鏈唆樊,使用真實(shí)節(jié)點(diǎn)存儲(chǔ)宛琅。
$ npm install web3以太坊提供讀寫區(qū)塊鏈數(shù)據(jù)的JavaScript接口,源碼地址:https://github.com/ethereum/web3.js/逗旁,通過web3.js我們可以訪問各個(gè)賬戶嘿辟、部署智能合約、調(diào)用合約方法片效、發(fā)起交易等等红伦。
$ npm install truffle第三方提供的開源以太坊DApp集成工具,源碼地址:https://github.com/trufflesuite/truffle淀衣,truffle工具會(huì)幫助我們編譯昙读、測試、打包和部署DApp項(xiàng)目中的所有合約膨桥,類似的還有Meteor(官方推薦工具蛮浑,但實(shí)用下來感覺沒有truffle方便唠叛,而且文檔也較少)。
以下是非必需工具
$ npm install truffle-contract基于web3.js封裝的JavaScript與智能合約交互接口沮稚,通過鏈?zhǔn)秸{(diào)用將對(duì)合約的各個(gè)操作串聯(lián)在一起艺沼,具體API參考源碼地址:https://github.com/trufflesuite/truffle-contract
$ npm install expressnode.js社區(qū)中基于connect流行的服務(wù)器開發(fā)框架,本文使用該框架搭建后臺(tái)服務(wù)器蕴掏,讀者可自行選擇其他框架障般。
編程語言
編寫一個(gè)DApp可以說是包括兩部分,合約部分和業(yè)務(wù)邏輯部分盛杰。
智能合約
Solidity挽荡,類JavaScript,這是以太坊推薦的旗艦語言即供,也是最流行的智能合約語言定拟,具體用法參考http://solidity.readthedocs.io/en/latest/,本文所有合約都使用該語言編寫募狂,另外測試办素、調(diào)試Solidity有一個(gè)非常好的在線IDE--Remixhttps://remix.ethereum.org/,由以太坊團(tuán)隊(duì)推出的祸穷。
Serpent性穿,類Python。
LLL雷滚,類Lisp需曾。
業(yè)務(wù)邏輯
業(yè)務(wù)邏輯部分即提供客戶端與智能合約交互的接口,相當(dāng)于目前BS結(jié)構(gòu)中的后臺(tái)邏輯祈远,因此業(yè)務(wù)邏輯部分可部署在中心服務(wù)器中呆万,而且在以太坊中每個(gè)智能合約函數(shù)的每一行代碼都有固定的gas費(fèi)用以及延時(shí)的,一些簡單的邏輯應(yīng)該交由業(yè)務(wù)邏輯處理车份,編寫業(yè)務(wù)邏輯目前提供有以下幾種語言:
JavaScript谋减,主要是基于Web3.js這個(gè)庫調(diào)用智能合約,本文例子也是使用JavaScript編寫的扫沼。
Go出爹,上述提到的以太坊私鏈搭建工具geth就是使用Go編寫的。
Python
Java
Ruby
Haskell
Rust
DApp實(shí)踐
廢話不多說缎除,下面我們通過一個(gè)DApp例子來窺探一下區(qū)塊鏈智能合約的魅力严就,demo源碼地址:https://github.com/Dave1991/QzoneBlockPet。
Demo功能介紹
該demo是一個(gè)卡片收集類游戲器罐,業(yè)務(wù)場景為每個(gè)用戶都擁有一只隨機(jī)的寵物梢为,用戶通過收集卡片作用于寵物身上進(jìn)行裝扮,而卡片的收集來源分三種:
系統(tǒng)定期為隨機(jī)用戶生成卡片
與其他用戶交換卡片
在卡片商城中購買卡片
Demo目錄結(jié)構(gòu)
我們通過$ truffle init命令創(chuàng)建一個(gè)DApp項(xiàng)目,truffle會(huì)幫我們組織好一個(gè)DApp的目錄結(jié)構(gòu)铸董,如下所示祟印,其中app目錄為筆者添加的,用于存放業(yè)務(wù)邏輯代碼袒炉。
app
業(yè)務(wù)邏輯代碼旁理,后面再展開討論
varMigrations=artifacts.require("./Migrations.sol");varPetCard=artifacts.require("./PetCard.sol");varUserCenter=artifacts.require("./UserCenter.sol");module.exports=function(deployer){deployer.deploy(Migrations);deployer.deploy(PetCard);deployer.deploy(UserCenter);};
build
合約編譯生成目錄樊零,不要手動(dòng)修改
contracts
合約目錄我磁,后面展開討論
migrationstruffle部署配置文件,新的合約需要部署需要修改里面的配置文件1_initial_migration.js驻襟,該demo包含兩個(gè)合約夺艰,加上truffle部署時(shí)需要使用的合約,一共三個(gè)合約沉衣,代碼如下所示郁副,當(dāng)添加一個(gè)合約時(shí)需要在該文件中添加合約變量而且需要通過deployer部署到區(qū)塊鏈,需要注意的是這里當(dāng)前目錄是contracts目錄豌习。
test
合約的測試文件存谎,我們可以在該目錄中存放各個(gè)合約的測試代碼,類似于其他編程語言中的單元測試肥隆,該文章不展開討論既荚。
module.exports={networks:{development:{host:"localhost",port:8545,network\_id:"\*"http:// Match any network id}}};
truffle.js
區(qū)塊鏈網(wǎng)絡(luò)配置文件,在truffle部署合約時(shí)會(huì)使用該文件定義的地址栋艳,目前配的是testrpc默認(rèn)測試環(huán)境恰聘,如下所示:
Demo運(yùn)行方式
安裝上述提到的依賴(包括非必需)
$ testrpc啟動(dòng)區(qū)塊鏈測試環(huán)境,可以看到testrpc在內(nèi)存中為我們創(chuàng)建了10個(gè)虛擬賬戶以及對(duì)應(yīng)的私鑰吸占。
image.png
$ truffle compile編譯智能合約晴叨,底層調(diào)用的是solc編譯器,該編譯方式是增量的矾屯,如果要全量編譯兼蕊,可加上--all參數(shù)。
image.png
$ truffle migrate --reset部署所有智能合約件蚕,部署的環(huán)境由truffle.js定義孙技,和compile類似,migrate也是增量部署骤坐,如果要重新部署所有合約绪杏,可加上--reset參數(shù)。
image.png
$ cd app
$ npm start啟動(dòng)服務(wù)器
瀏覽器訪問localhost:8080纽绍,目前提供的接口詳見INTERFACE.md文件蕾久,下面展示其中兩個(gè)接口。
生成卡片
接口名稱方法路由參數(shù)
createRandomCardGET無
例子返回
/createRandomCard{"cardId":"2","code":"0x616161666","owner":"0x5727b589bca4500e896ffc82e3fedf56cae7017f","value":"52"}
獲取用戶所有卡片
接口名稱方法路由參數(shù)
getAllCardsForUserGET/:address
例子返回
/getAllCardsForUser/0xc3d9b7ea1e42b04dddf3475b464bb1abd5f8451f{"cardId":"0","code":"0x616161666","value":"4"}
需要注意的是上面兩個(gè)方法調(diào)用前都需要設(shè)置gas(以太坊交易手續(xù)費(fèi))拌夏,不過由于demo運(yùn)行在testrpc中所有賬戶的balance都是虛擬的僧著,業(yè)務(wù)邏輯直接從接口調(diào)用方賬戶扣除了gas履因,對(duì)其屏蔽了該過程,但如果正式部署到生產(chǎn)環(huán)境我們需要先詢問用戶是否愿意付該筆gas然后再真正調(diào)用合約接口盹愚,因此栅迄,以太坊的web3.js提供了estimateGas方法來預(yù)估合約函數(shù)執(zhí)行所需的gas。
編寫智能合約
智能合約使用Solidity語言編寫皆怕,語法有點(diǎn)類似于JavaScript毅舆,文件名以.sol結(jié)尾,通常來說一個(gè).sol文件定義一個(gè)合約愈腾,相當(dāng)于Java中一個(gè)文件定義一個(gè)public class憋活。一個(gè)合約通常包含兩部分,成員變量和成員函數(shù)虱黄。
進(jìn)入本demo的contracts目錄悦即,可以看見里面包含了以下文件:
Migrations.sol:truffle創(chuàng)建目錄時(shí)創(chuàng)建的合約,用于部署DApp
PetCard.sol:本demo核心合約橱乱,定義了寵物卡片合約
strings.sol:第三方定義的字符串類庫辜梳,本demo主要使用了其分割字符串的函數(shù)
UserCenter.sol:用戶中心合約,用于注冊(cè)用戶和查詢用戶
下面展示的是寵物卡片合約的部分代碼泳叠。
pragma solidity^0.4.17;contract PetCard{struct Card{bytes32 code;//卡片代碼作瞄,決定卡片的功能uint256 value;address owner;bool isSelling;uint sellingPrice;uint cardId;}enumErrorCode{ERROR_NO_ERROR,ERROR_INDEX_OUT_OF_RANGE,ERROR_WRONG_OWNER,ERROR_CARD_IS_SELLING,ERROR_CARD_IS_NOT_SELLING,ERROR_PRICE_NOT_ENOUGH}Card[]cards;address CEO;functionPetCard()publicpayable{CEO=msg.sender;}// 匿名函數(shù),當(dāng)外部調(diào)用找不到時(shí)調(diào)用該函數(shù)eventFallbackTrigged(bytes data);function()publicpayable{FallbackTrigged(msg.data);}eventBuyCardEvent(uint cardId,bool isSuccess,ErrorCode errorCode);// 從卡片商城中購買卡片functionbuyCard(uint cardId)publicpayable{address buyer=msg.sender;// 判斷card下標(biāo)是否合法析二,不合法時(shí)退款給買家if(cardId>=cards.length||cardId<0){buyer.transfer(msg.value);BuyCardEvent(cardId,false,ErrorCode.ERROR_INDEX_OUT_OF_RANGE);return;}Card storage card=cards[cardId];// 判斷消費(fèi)金額是否小于card價(jià)格if(msg.value=card.sellingPrice){card.owner.transfer(card.sellingPrice);}card.owner=buyer;card.isSelling=false;card.sellingPrice=0;BuyCardEvent(cardId,true,ErrorCode.ERROR_NO_ERROR);}// 獲取用戶所有卡片functiongetAllCardsForUser()publicconstantreturns(uint[]cardIds,bytes32[]codes,uint[]values,uint len){cardIds=newuint[](cards.length);codes=newbytes32[](cards.length);values=newuint[](cards.length);// codes = new string[](cards.length);len=0;for(uint i=0;i
定義卡片結(jié)構(gòu)與成員變量
合約內(nèi)部可以定義多個(gè)結(jié)構(gòu)體粉洼,關(guān)鍵字為struct,結(jié)構(gòu)體內(nèi)部也可定義成員變量叶摄,允許的類型和合約一樣属韧。此外,合約支持?jǐn)?shù)據(jù)類型包括以下幾種:
整型蛤吓,uintx / intx宵喂,其中x代表整型所占用的位數(shù),從8到256会傲,步長為8锅棕,如果我們直接使用uint / int,則與uint256 / int256等價(jià)淌山。
布爾型裸燎,bool,有true/false兩個(gè)值泼疑。
浮點(diǎn)型德绿,fixedMxN / ufixedMxN,浮點(diǎn)數(shù)在Solidity中支持得不是很好,它與其他語言中的浮點(diǎn)數(shù)并不一樣移稳,Solidity中浮點(diǎn)數(shù)在聲明時(shí)就必須確定長度蕴纳,而其他語言是可變的,M代表的是浮點(diǎn)數(shù)占用的總位數(shù)个粱,從8到256古毛,步長為8,N代表小數(shù)部分的長度都许,范圍是0-80稻薇。
定長字節(jié)型,bytesx梭稚,其中x代表變量所占字節(jié)長度颖低,范圍是1-32,當(dāng)變量打印出來時(shí)弧烤,顯示的是十六進(jìn)制。
變長字節(jié)型蹬敲,bytes或string暇昂,兩者區(qū)別在于bytes使用十六進(jìn)制標(biāo)識(shí),string是用UTF-8表示伴嗡。
地址急波,address, 等價(jià)于bytes20,而且Solidity為地址變量預(yù)設(shè)了幾個(gè)方法瘪校,例如澄暮,balance方法獲取地址對(duì)應(yīng)賬戶的余額,transfer方法轉(zhuǎn)賬以太幣到地址對(duì)應(yīng)的賬戶中阱扬,轉(zhuǎn)賬者為調(diào)用者泣懊,收款者為address,另一個(gè)方法send類似于transfer也是轉(zhuǎn)賬麻惶,但值得注意的是馍刮,當(dāng)transfer失敗時(shí),會(huì)回滾交易并拋出異常窃蹋,而send方法則不會(huì)卡啰。
枚舉,enum警没,和其他語言一樣匈辱,Solidity也支持枚舉值,語法也類似杀迹,可參考代碼中錯(cuò)誤碼枚舉值的定義亡脸。
根據(jù)上述的數(shù)據(jù)類型,我們定義卡片的結(jié)構(gòu)體,包括卡片代碼梗掰、卡片價(jià)值嵌言、卡片擁有者、卡片是否正在出售及穗、卡片出售價(jià)格以及卡片id摧茴。然后,定義了函數(shù)執(zhí)行可能會(huì)發(fā)生的錯(cuò)誤碼埂陆,還有一個(gè)卡片的集合以及合約的創(chuàng)建者CEO苛白。
struct Card{bytes32 code;//卡片代碼,決定卡片的功能uint256 value;address owner;bool isSelling;uint sellingPrice;uint cardId;}enumErrorCode{ERROR_NO_ERROR,ERROR_INDEX_OUT_OF_RANGE,ERROR_WRONG_OWNER,ERROR_CARD_IS_SELLING,ERROR_CARD_IS_NOT_SELLING,ERROR_PRICE_NOT_ENOUGH}Card[]cards;address CEO;
函數(shù)
在Solidity中函數(shù)的定義語法是
function函數(shù)名(參數(shù)列表) 修飾符returns(返回值列表)
這里值得注意的是焚虱,在函數(shù)生命中返回值列表我們可以聲明返回值的名字购裙,類似于形參,當(dāng)在函數(shù)體中給返回值變量賦值后鹃栽,我們可以不用寫return躏率,但如果寫了還是以return為主,同時(shí)民鼓,一個(gè)函數(shù)返回值支持多個(gè)薇芝,調(diào)用者拿到的將是一個(gè)返回值數(shù)組,和python有點(diǎn)像丰嘉。
另外夯到,EVM會(huì)給每個(gè)合約的函數(shù)傳入一個(gè)名為msg的對(duì)象,該對(duì)象包含幾個(gè)屬性饮亏,如sender是調(diào)用者賬戶地址耍贾、value是調(diào)用者執(zhí)行該函數(shù)支付的以太幣(單位是wei)、data是函數(shù)調(diào)用的描述路幸。除了data外荐开,其他屬性的值是由調(diào)用者傳入,詳見業(yè)務(wù)邏輯代碼的介紹劝赔。
構(gòu)建函數(shù)和匿名函數(shù)
和大部分語言一樣誓焦,Solidity中每個(gè)合約也有構(gòu)建函數(shù),在構(gòu)建函數(shù)中我們可以做一些初始化的操作着帽,在下面的代碼中我們注意到函數(shù)后有兩個(gè)修飾符杂伟,分別是public和payable,其中public說明該函數(shù)外部合約也可見仍翰,對(duì)應(yīng)的還有external赫粥,private,internal予借,要說到這四者的區(qū)別越平,需要查看函數(shù)的調(diào)用方式和可見性频蛔,本文就不展開了。然后payable說明該函數(shù)會(huì)涉及貨幣交易秦叛,同時(shí)當(dāng)我們?cè)谝粋€(gè)合約的其他函數(shù)中調(diào)用了轉(zhuǎn)賬操作晦溪,那么構(gòu)建函數(shù)必須也得聲明為payable。
匿名函數(shù)挣跋,也就是沒有名字的函數(shù)三圆,每個(gè)合約中最多可定義一個(gè),當(dāng)其他地方調(diào)用該合約不存在的函數(shù)或者出現(xiàn)異常時(shí)避咆,EVM(以太坊智能合約執(zhí)行虛擬機(jī))會(huì)自動(dòng)調(diào)用合約的匿名函數(shù)舟肉,同樣地,當(dāng)合約內(nèi)其他函數(shù)有轉(zhuǎn)賬操作時(shí)匿名函數(shù)也需要加上payable修飾查库。
functionPetCard()publicpayable{CEO=msg.sender;}// 匿名函數(shù)路媚,當(dāng)外部調(diào)用找不到時(shí)調(diào)用該函數(shù)eventFallbackTrigged(bytes data);function()publicpayable{FallbackTrigged(msg.data);}
事件
代碼中我們定義了多個(gè)event,每個(gè)event只需要定義其名字和參數(shù)列表即可以樊销,其作用相當(dāng)于其他語言中的log整慎,在函數(shù)中傳入實(shí)參即可記錄,雖說event的作用和log一樣现柠,但在Solidity中作用卻非同小可院领,因?yàn)楫?dāng)一個(gè)函數(shù)是以transaction的形式被調(diào)用,調(diào)用者是無法拿到返回值的够吩,因?yàn)閠ransaction的調(diào)用是異步的,EVM無法立刻執(zhí)行給出返回值丈氓,所以調(diào)用者只能通過event的記錄取得函數(shù)執(zhí)行后的數(shù)據(jù)周循,具體操作流程見業(yè)務(wù)邏輯代碼的介紹。
購買卡片
定義購買卡片的函數(shù)万俗,函數(shù)一開始我們寫了三個(gè)是否合法的判斷湾笛,這里可以使用require關(guān)鍵字對(duì)這些條件進(jìn)行限定,但由于筆者希望調(diào)用者可以接收到錯(cuò)誤信息闰歪,這里就使用了四個(gè)if判斷嚎研,并且使用了事件通知調(diào)用者,同時(shí)當(dāng)條件不滿足時(shí)我們需要做一些回滾操作库倘,例如將金額退還給調(diào)用者賬戶临扮。而當(dāng)條件滿足后,我們將卡片定價(jià)轉(zhuǎn)給賣家教翩,轉(zhuǎn)移卡片擁有者杆勇。
eventBuyCardEvent(uint cardId,bool isSuccess,ErrorCode errorCode);// 從卡片商城中購買卡片functionbuyCard(uint cardId)publicpayable{address buyer=msg.sender;// 判斷card下標(biāo)是否合法,不合法時(shí)退款給買家if(cardId>=cards.length||cardId<0){buyer.transfer(msg.value);BuyCardEvent(cardId,false,ErrorCode.ERROR_INDEX_OUT_OF_RANGE);return;}Card storage card=cards[cardId];// 判斷消費(fèi)金額是否小于card價(jià)格if(msg.value=card.sellingPrice){card.owner.transfer(card.sellingPrice);}card.owner=buyer;card.isSelling=false;card.sellingPrice=0;BuyCardEvent(cardId,true,ErrorCode.ERROR_NO_ERROR);}
遍歷卡片
該函數(shù)的作用是獲取所有屬于調(diào)用者賬戶的卡片饱亿,值得注意的是蚜退,該函數(shù)在EVM中是一個(gè)昂貴的操作闰靴,首先我們聲明了三個(gè)定長數(shù)組(定長是和臨時(shí)變量存儲(chǔ)的地方有關(guān)),每個(gè)長度都等于所有卡片數(shù)組的大小钻注,因此每個(gè)數(shù)組都已經(jīng)開銷了不少gas蚂且,然后遍歷又是一個(gè)耗時(shí)操作,又需要花費(fèi)gas幅恋,而且函數(shù)在編譯時(shí)并不知道cards的長度杏死,所以即使調(diào)用者使用estimategas函數(shù)預(yù)估該函數(shù)所需gas也是不準(zhǔn)確的,這對(duì)于調(diào)用者是危險(xiǎn)的佳遣,隨時(shí)都可能因?yàn)間as不夠而執(zhí)行失敗识埋。
functiongetAllCardsForUser()publicconstantreturns(uint[]cardIds,bytes32[]codes,uint[]values,uint len){cardIds=newuint[](cards.length);codes=newbytes32[](cards.length);//這里不能用string,solidity不支持定長的變長數(shù)組values=newuint[](cards.length);// codes = new string[](cards.length);len=0;for(uint i=0;i
生成卡片
這里生成卡片的邏輯交給業(yè)務(wù)層零渐,合約只負(fù)責(zé)根據(jù)參數(shù)創(chuàng)建一個(gè)新的卡片窒舟,最后通知調(diào)用者即業(yè)務(wù)層。
eventCreateNewCardEvent(uint cardId,bytes32 code,address owner,uint value);// 給用戶掉落新卡片functioncreateNewCardForUser(bytes32 code,uint value)public{Card memory card=Card({code:code,value:value,owner:msg.sender,isSelling:false,cardId:cards.length,sellingPrice:0});cards.push(card);CreateNewCardEvent(card.cardId,card.code,card.owner,card.value);}
編寫業(yè)務(wù)邏輯
合約編寫完成后诵盼,可先到Remix上測試惠豺,測試通過后再使用truffle編譯和部署到區(qū)塊鏈上。之后风宁,便是業(yè)務(wù)邏輯的編寫了洁墙。
由于truffle,web3等都是依賴于node.js戒财,為了一致性與方便性热监,本demo也是使用node.js構(gòu)建業(yè)務(wù)服務(wù)器,主要依賴的模塊是express和truffle-contract饮寞,前者用于更方便的業(yè)務(wù)路由和模塊化孝扛,后者用于更方便調(diào)用合約。
打開app目錄幽崩,我們會(huì)看到一下的文件結(jié)構(gòu):
PetCard.js:寵物卡片業(yè)務(wù)路由處理以及合約交互
UserCenter.js:用戶中心苦始,負(fù)責(zé)用戶注冊(cè)和獲取所有用戶的上層調(diào)用
UserCenterCore.js:用戶中心核心,負(fù)責(zé)業(yè)務(wù)層與合約層交互
Web3Provider.js:定義Web3連接的是區(qū)塊鏈地址
package.json:定義npm運(yùn)行所需要的命令和依賴
server.js:業(yè)務(wù)層總?cè)肟诨派辏?fù)責(zé)默認(rèn)頁面陌选、404頁面處理,以及各業(yè)務(wù)模塊的中轉(zhuǎn)路由蹄溉,還有定義服務(wù)器綁定的端口
下面我們主要看PetCard.js中業(yè)務(wù)層是如何與合約層進(jìn)行交互的咨油。
獲取合約示例
這一步我們首先獲取寵物卡片合約和用戶中心合約的實(shí)例,便于下面調(diào)用合約类缤,這里我們需要依賴truffle-contract還有本地的Web3Provider模塊臼勉。而truffle-contract的用法都是鏈?zhǔn)秸{(diào)用,通過then函數(shù)連接起來餐弱。
contract=require('truffle-contract');provider=require('./Web3Provider.js');express=require("express");constPetCard=contract(require('../../build/contracts/PetCard.json'));PetCard.setProvider(provider);varpetCard;PetCard.deployed().then(function(instance){petCard=instance;});varuserCenter;require('./UserCenterCore.js').then(function(instance){userCenter=instance;});varapp=module.exports=express();
購買卡片
從下面代碼中可以看到宴霸,業(yè)務(wù)層接受客戶端傳遞的路由參數(shù)囱晴,再傳入合約層,這里合約層函數(shù)的參數(shù)分兩種瓢谢,一種是自定義參數(shù)畸写,另一種就是EVM預(yù)設(shè)參數(shù),而預(yù)設(shè)參數(shù)是一個(gè)對(duì)象氓扛,需要在最后傳入枯芬,正如上面Solidity函數(shù)介紹,預(yù)設(shè)參數(shù)對(duì)象需要包括from為調(diào)用者地址采郎,value為傳入合約的以太幣千所。最后,由于這是直接通過合約實(shí)例調(diào)用函數(shù)蒜埋,是一個(gè)transaction操作淫痰,因此如上面Solidity事件介紹,我們需要從返回值的日志中獲取合約執(zhí)行后的數(shù)據(jù)整份。由于日志拿到的事件參數(shù)是一個(gè)對(duì)象待错,所以我們直接以json形式返回給客戶端即可,例如下面的返回就表示卡片購買失敗烈评,原因是卡片當(dāng)前不在銷售:{"cardId":"1","isSuccess":false,"errorCode":"4"}火俄。
app.get('/buyCard/:address/:cardId/:price',function(req,res){petCard.buyCard(req.params.cardId,{from:req.params.address,value:req.params.price}).then(function(result){if(result.logs.length>0){vareventObj=result.logs[0].args;res.send(JSON.stringify(eventObj));}});});
遍歷所有卡片
遍歷卡片的操作并不涉及永久寫入合約數(shù)據(jù)的操作,因此遍歷卡片這里我們不使用transaction讲冠,而使用call的形式瓜客,因此我們可以直接拿到函數(shù)的返回值,然后由于函數(shù)返回多個(gè)值竿开,因此result是一個(gè)數(shù)組忆家。這里需要注意的是,上面我們說到遍歷卡片時(shí)合約需要?jiǎng)?chuàng)建三個(gè)未知長度的數(shù)組,而且遍歷的次數(shù)也是未知的辈灼,因此许帐,estimategas函數(shù)預(yù)估的gas會(huì)不準(zhǔn)確,我們這里直接給一個(gè)比較大的gas值控淡。該接口返回的例子如:{"cardId":"0","code":"0x616161666","value":"4"}。
app.get('/getAllCardsForUser/:address',function(req,res){// 因?yàn)檫@需要?jiǎng)?chuàng)建未知長度數(shù)組,estimate 估計(jì)的gas會(huì)不準(zhǔn)確肌毅,該方法慎調(diào)petCard.getAllCardsForUser.call({from:req.params.address,gas:3000000}).then(function(result){if(result.length>=4){varcardIds=result[0],codes=result[1],values=result[2];varlen=result[3];varcards=[];for(vari=0;i
生成卡片
生成卡片的邏輯是在所有用戶隨機(jī)挑選一個(gè)用戶作為卡片的擁有者,然后卡片的code這里先簡單地寫死了一串姑原,后續(xù)可以想更好玩的code生成邏輯悬而,接著就是調(diào)用estimateGas函數(shù)估計(jì)所需的gas,最后才是真正調(diào)用合約函數(shù)锭汛,傳入預(yù)估的gas笨奠,其實(shí)比較好的交互應(yīng)該像以太貓那樣袭蝗,在進(jìn)行真正的調(diào)用之前告知用戶交易所需的gas,并可以讓用戶調(diào)整般婆,用戶確認(rèn)后再執(zhí)行合約函數(shù)到腥。下面是生成卡片調(diào)用后返回的一個(gè)例子:{"cardId":"2","code":"0x616161666","owner":"0x5727b589bca4500e896ffc82e3fedf56cae7017f","value":"52"}。
app.get('/createRandomCard',function(req,res){varallUsers,randomUser;userCenter.showAllPlayers.call().then(function(result){allUsers=result;randomIdx=Math.floor(Math.random()*allUsers.length);randomUser=allUsers[randomIdx];if(randomUser!=undefined){varcardCode="aaaforestlinbbb";varcardValue=Math.floor(Math.random()*100+1);petCard.createNewCardForUser.estimateGas(cardCode,cardValue).then(function(esti_gas){returnpetCard.createNewCardForUser(cardCode,cardValue,{from:randomUser,gas:esti_gas});}).then(function(rest){if(rest.logs.length>0){vareventObj=rest.logs[0].args;res.send(JSON.stringify(eventObj));}});}else{res.send("random user is undefined");}});});
總結(jié)DApp開發(fā)中遇到的坑
一個(gè)DApp開發(fā)流程介紹到此結(jié)束蔚袍,下面總結(jié)一下開發(fā)中值得注意的地方:
Solidity這個(gè)語言目前還不是很完善乡范,版本還是0.4.x,而且文檔相對(duì)其他語言較少啤咽,這里除了官網(wǎng)晋辆,還推薦兩個(gè)論壇區(qū)塊鏈技術(shù)博客和以太坊愛好者供大家參考。
合約函數(shù)中慎用未知長度的數(shù)組以及遍歷操作宇整,比較耗費(fèi)gas瓶佳,而且對(duì)于調(diào)用者極不友好,無法預(yù)估gas没陡。
對(duì)于不需要寫操作的函數(shù)涩哟,我們可以加上constant修飾符或者調(diào)用時(shí)使用call的方法而非直接調(diào)用,不產(chǎn)生transaction盼玄,也就不需要寫入?yún)^(qū)塊鏈贴彼。
對(duì)于不需要的數(shù)組我們可以使用delete操作刪除整個(gè)數(shù)組或者某個(gè)元素,可以歸還一些gas埃儿,但是最好復(fù)用器仗,使用指示器標(biāo)記當(dāng)前使用的長度,因?yàn)閐elete操作本身也是需要耗費(fèi)gas的童番。
合約內(nèi)不適合做業(yè)務(wù)過重的操作精钮,如上面的生成卡片操作,應(yīng)該將邏輯放在業(yè)務(wù)層剃斧,畢竟在EVM中沒執(zhí)行一行代碼都是需要gas的轨香,合約應(yīng)該只有讀寫區(qū)塊鏈的操作。