手把手帶你走上智能合約編程之路

手把手帶你走上智能合約編程之路

譯註:原文首發於ConsenSys開發者博客,原做者爲Eva以及ConsenSys的開發團隊。若是您想要獲取更多及時信息,能夠訪問ConsenSys首頁點擊左下角Newsletter訂閱郵件。本文的翻譯得到了ConsenSys創始人Lubin先生的受權。javascript

有些人說以太坊太難對付,因而咱們(譯註:指Consensys, 下同)寫了這篇文章來幫助你們學習如何利用以太坊編寫智能合約和應用。這裏所用到的工具,錢包,應用程序以及整個生態系統仍處於開發狀態,它們未來會更好用!html

  • 第一部分概述,討論了關鍵概念,幾大以太坊客戶端以及寫智能合約用到的編程語言。
  • 第二部分討論了整體的工做流程,以及目前流行的一些DApp框架和工具。
  • 第三部分主要關於編程,咱們將學習如何使用Truffle來爲智能合約編寫測試和構建DApp。

第一部分. 概述

若是你對諸如比特幣以及其工做原理等密碼學貨幣的概念徹底陌生,咱們建議你先看看Andreas Antonopoulos所著的Bitcoin Book的頭幾章,而後讀一下以太坊白皮書。(譯註:以太坊白皮書中文版請看http://ethfans.org/posts/ethereum-whitepaper)前端

若是你以爲白皮書中的章節太晦澀,也能夠直接動手來熟悉以太坊。在以太坊上作開發並不要求你理解全部那些「密碼經濟計算機科學」(crypto economic computer science),而白皮書的大部分是關於以太坊想對於比特幣架構上的改進。java

新手教程

ethereum.org提供了官方的新手入門教程,以及一個代幣合約和衆籌合約的教程。合約語言Solidity也有官方文檔。學習智能合約的另外一份不錯的資料(也是個人入門資料)是dappsForBeginners,不過如今可能有些過期了。node

這篇文章的目的是成爲上述資料的補充,同時介紹一些基本的開發者工具,使入門以太坊,智能合約以及構建DApps(decentralized apps, 分佈式應用)更加容易。我會試圖按照我本身(依然是新手)的理解來解釋工做流程中的每一步是在作什麼,我也獲得了ConsenSys酷酷的開發者們的許多幫助。python

基本概念

瞭解這些名詞是一個不錯的開始:git

公鑰加密系統。 Alice有一把公鑰和一把私鑰。她能夠用她的私鑰建立數字簽名,而Bob能夠用她的公鑰來驗證這個簽名確實是用Alice的私鑰建立的,也就是說,確實是Alice的簽名。當你建立一個以太坊或者比特幣錢包的時候,那長長的0xdf...5f地址實質上是個公鑰,對應的私鑰保存某處。相似於Coinbase的在線錢包能夠幫你保管私鑰,你也能夠本身保管。若是你弄丟了存有資金的錢包的私鑰,你就等於永遠失去了那筆資金,所以你最好對私鑰作好備份。過來人表示:經過踩坑學習到這一點是很是痛苦的...github

點對點網絡。 就像BitTorrent, 以太坊分佈式網絡中的全部節點都地位平等,沒有中心服務器。(將來會有半中心化的混合型服務出現爲用戶和開發者提供方便,這咱們後面會講到。)web

區塊鏈。 區塊鏈就像是一個全球惟一的賬簿,或者說是數據庫,記錄了網絡中全部交易歷史。chrome

以太坊虛擬機(EVM)。 它讓你能在以太坊上寫出更強大的程序(比特幣上也能夠寫腳本程序)。它有時也用來指以太坊區塊鏈,負責執行智能合約以及一切。

節點。 你能夠運行節點,經過它讀寫以太坊區塊鏈,也即便用以太坊虛擬機。徹底節點須要下載整個區塊鏈。輕節點仍在開發中。

礦工。 挖礦,也就是處理區塊鏈上的區塊的節點。這個網頁能夠看到當前活躍的一部分以太坊礦工:stats.ethdev.com

工做量證實。 礦工們老是在競爭解決一些數學問題。第一個解出答案的(算出下一個區塊)將得到以太幣做爲獎勵。而後全部節點都更新本身的區塊鏈。全部想要算出下一個區塊的礦工都有與其餘節點保持同步,而且維護同一個區塊鏈的動力,所以整個網絡老是能達成共識。(注意:以太坊正計劃轉向沒有礦工的權益證實系統(POS),不過那不在本文討論範圍以內。)

以太幣。 縮寫ETH。一種你能夠購買和使用的真正的數字貨幣。這裏是能夠交易以太幣的其中一家交易所的走勢圖。在寫這篇文章的時候,1個以太幣價值65美分。

Gas. (汽油) 在以太坊上執行程序以及保存數據都要消耗必定量的以太幣,Gas是以太幣轉換而成。這個機制用來保證效率。

DApp. 以太坊社區把基於智能合約的應用稱爲去中心化的應用程序(Decentralized App)。DApp的目標是(或者應該是)讓你的智能合約有一個友好的界面,外加一些額外的東西,例如IPFS(能夠存儲和讀取數據的去中心化網絡,不是出自以太坊團隊但有相似的精神)。DApp能夠跑在一臺能與以太坊節點交互的中心化服務器上,也能夠跑在任意一個以太坊平等節點上。(花一分鐘思考一下:與通常的網站不一樣,DApp不能跑在普通的服務器上。他們須要提交交易到區塊鏈而且從區塊鏈而不是中心化數據庫讀取重要數據。相對於典型的用戶登陸系統,用戶有可能被表示成一個錢包地址而其它用戶數據保存在本地。許多事情都會與目前的web應用有不一樣架構。)

若是想看看從另外一個新手視角怎麼理解這些概念,請讀Just Enough Bitcoin for Ethereum

以太坊客戶端,智能合約語言

編寫和部署智能合約並不要求你運行一個以太坊節點。下面有列出基於瀏覽器的IDE和API。但若是是爲了學習的話,仍是應該運行一個以太坊節點,以便理解其中的基本組件,況且運行節點也不難。

運行以太坊節點可用的客戶端

以太坊有許多不一樣語言的客戶端實現(即多種與以太坊網絡交互的方法),包括C++, Go, Python, Java, Haskell等等。爲何須要這麼多實現?不一樣的實現能知足不一樣的需求(例如Haskell實現的目標是能夠被數學驗證),能使以太坊更加安全,能豐富整個生態系統。

在寫做本文時,我使用的是Go語言實現的客戶端geth (go-ethereum),其餘時候還會使用一個叫testrpc的工具, 它使用了Python客戶端pyethereum。後面的例子會用到這些工具。

注: 我曾經使用過C++的客戶端,如今仍然在用其中的ethminer組件和geth配合挖礦,所以這些不一樣的組件是能夠一塊兒工做的。
關於挖礦:挖礦頗有趣,有點像精心照料你的室內盆栽,同時又是一種瞭解整個系統的方法。雖然以太幣如今的價格可能連電費都補不齊,但之後誰知道呢。人們正在創造許多酷酷的DApp, 可能會讓以太坊愈來愈流行。

交互式控制檯。 客戶端運行起來後,你就能夠同步區塊鏈,創建錢包,收發以太幣了。使用geth的一種方式是經過Javascript控制檯(JavaScript console, 相似你在chrome瀏覽器裏面按F12出來的那個,只不過是跑在終端裏)。此外還可使用相似cURL的命令經過JSON RPC來與客戶端交互。本文的目標是帶你們過一邊DApp開發的流程,所以這塊就很少說了。可是咱們應該記住這些命令行工具是調試,配置節點,以及使用錢包的利器。

在測試網絡運行節點。 若是你在正式網絡運行geth客戶端,下載整個區塊鏈與網絡同步會須要至關時間。(你能夠經過比較節點日誌中打印的最後一個塊號和stats.ethdev.com上列出的最新塊來肯定是否已經同步。) 另外一個問題是在正式網絡上跑智能合約須要實實在在的以太幣。在測試網絡上運行節點的話就沒有這個問題。此時也不須要同步整個區塊鏈,建立一個本身的私有鏈就勾了,對於開發來講更省時間。

testrpc. 用geth能夠建立一個測試網絡,另外一種更快的建立測試網絡的方法是使用testrpc. Testrpc能夠在啓動時幫你建立一堆存有資金的測試帳戶。它的運行速度也更快所以更適合開發和測試。你能夠從testrpc起步,而後隨着合約慢慢成型,轉移到geth建立的測試網絡上 - 啓動方法很簡單,只須要指定一個networkid:geth --networkid "12345"。這裏是testrpc的代碼倉庫,下文咱們還會再講到它。

接下來咱們來談談可用的編程語言,以後就能夠開始真正的編程了。

寫智能合約用的編程語言

用Solidity就好。 要寫智能合約有好幾種語言可選:有點相似Javascript的Solidity, 文件擴展名是.sol. 和Python接近的Serpent, 文件名以.se結尾。還有相似Lisp的LLL。Serpent曾經流行過一段時間,但如今最流行並且最穩定的要算是Solidity了,所以用Solidity就好。據說你喜歡Python? 用Solidity。

solc編譯器。 用Solidity寫好智能合約以後,須要用solc來編譯。它是一個來自C++客戶端實現的組件(又一次,不一樣的實現產生互補),這裏是安裝方法。若是你不想安裝solc也能夠直接使用基於瀏覽器的編譯器,例如Solidity real-time compiler或者Cosmo。後文有關編程的部分會假設你安裝了solc。

注意:以太坊正處於積極的開發中,有時候新的版本之間會有不一樣步。確認你使用的是最新的dev版本,或者穩定版本。若是遇到問題能夠去以太坊項目對應的Gitter聊天室或者forums.ethereum.org上問問其餘人在用什麼版本。

web3.js API. 當Solidity合約編譯好而且發送到網絡上以後,你可使用以太坊的web3.js JavaScript API來調用它,構建能與之交互的web應用。

以上就是在以太坊上編寫智能合約和構建與之交互的DApp所需的基本工具。

第二部分. DApp框架,工具以及工做流程

DApp開發框架

雖然有上文提到的工具就能夠進行開發了,可是使用社區大神們創造的框架會讓開發更容易。

Truffle and Embark. 是Truffle把我領進了門。在Truffle出現以前的那個夏天,我目擊了一幫有天分的學生是如何不眠不休的參加一個hackathon(編程馬拉松)活動的,雖然結果至關不錯,但我仍是嚇到了。而後Truffle出現了,幫你處理掉大量可有可無的小事情,讓你能夠迅速進入寫代碼-編譯-部署-測試-打包DApp這個流程。另一個類似的DApp構建與測試框架是Embark。我只用過Truffle, 可是兩個陣營都擁有很多DApp大神。

Meteor. 許多DApp開發者使用的另外一套開發棧由web3.js和Meteor組成,Meteor是一套通用webapp開發框架(ethereum-meteor-wallet項目提供了一個很棒的入門實例,而SilentCiero正在構建大量Meteor與web3.js和DApp集成的模板)。我下載並運行過一些不錯的DApp是以這種方式構造的。在11月9日至13日的以太坊開發者大會ÐΞVCON1上將有一些有趣的討論,是關於使用這些工具構建DApp以及相關最佳實踐的(會議將會在YouTube上直播)。

APIs. BlockApps.net打算提供一套RESTful API給DApp使用以避免去開發者運行本地節點的麻煩,這個中心化服務是基於以太坊Haskell實現的。這與DApp的去中心化模型背道而馳,可是在本地沒法運行以太坊節點的場合很是有用,好比在你但願只有瀏覽器或者使用移動設備的用戶也能使用你的DApp的時候。BlockApps提供了一個命令行工具bloc,註冊一個開發者賬號以後就可使用。

許多人擔憂須要運行以太坊節點才能使用DApp的話會把用戶嚇跑,其實包括BlockApps在內的許多工具都能解決這個問題。Metamask容許你在瀏覽器裏面使用以太坊的功能而無需節點,以太坊官方提供的AlethZero或者AlethOne是正在開發中有易用界面的客戶端,ConsenSys正在打造一個輕錢包LightWallet,這些工具都會讓DApp的使用變得更容易。輕客戶端和水平分片(sharding)也在計劃和開發之中。這是一個能進化出混合架構的P2P生態系統。

智能合約集成開發環境 (IDE)

IDE. 以太坊官方出品了用來編寫智能合約的Mix IDE,我還沒用過但會盡快一試。

基於瀏覽器的IDE. Solidity real-time compilerCosmo均可以讓你快速開始在瀏覽器中編寫智能合約。你甚至可讓這些工具使用你的本地節點,只要讓本地節點開一個端口(注意安全!這些工具站點必須可信,並且千萬不要把你的所有身家放在這樣一個本地節點裏面!Cosmo UI上有如何使用geth作到這一點的指引)。在你的智能合約調試經過以後,能夠用開發框架來給它添加用戶界面和打包成DApp,這正是Truffle的工做,後面的編程章節會有詳細講解。

Ether.Camp正在開發另外一個強大的企業級瀏覽器IDE。他們的IDE將支持沙盒測試網絡,自動生成用於測試的用戶界面(取代後文將展現的手動編寫測試),以及一個測試交易瀏覽器test.ether.camp。當你的合約準備正式上線以前,使用他們的測試網絡會是確保你的智能合約在一個接近真實的環境工做正常的好方法。他們也爲正式網絡提供了一個交易瀏覽器frontier.ether.camp,上面能夠看到每一筆交易的細節。在本文寫做時Ether.Camp的IDE還只能經過邀請註冊,預計很快會正式發佈。

合約和Dapp示例。 在Github上搜索DApp倉庫和.sol文件能夠看到進行中的有趣東西。這裏有一個DApp大列表:dapps.ethercasts.com,不過其中一些項目已通過時。Ether.fund/contracts上有一些Solidity和Serpent寫的合約示例,可是不清楚這些例子有沒有通過測試或者正確性驗證。11月12日的開發者大會ÐΞVCON1將會有一成天的DApp主題演講。

部署智能合約的流程

流程以下:

  1. 啓動一個以太坊節點 (例如geth或者testrpc)。
  2. 使用solc*編譯*智能合約。 => 得到二進制代碼。
  3. 將編譯好的合約部署到網絡。(這一步會消耗以太幣,還須要使用你的節點的默認地址或者指定地址來給合約簽名。) => 得到合約的區塊鏈地址和ABI(合約接口的JSON表示,包括變量,事件和能夠調用的方法)。(譯註:做者在這裏把ABI與合約接口弄混了。ABI是合約接口的二進制表示。)
  4. 用web3.js提供的JavaScript API來調用合約。(根據調用的類型有可能會消耗以太幣。)

下圖詳細描繪了這個流程:

你的DApp能夠給用戶提供一個界面先部署所需合約再使用之(如圖1到4步),也能夠假設合約已經部署了(常見方法),直接從使用合約(如圖第6步)的界面開始。

第三部分. 編程

在Truffle中進行測試

Truffle用來作智能合約的測試驅動開發(TDD)很是棒,我強烈推薦你在學習中使用它。它也是學習使用JavaScript Promise的一個好途徑,例如deferred和異步調用。Promise機制有點像是說「作這件事,若是結果是這樣,作甲,若是結果是那樣,作乙... 與此同時不要在那兒乾等着結果返回,行不?」。Truffle使用了包裝web3.js的一個JS Promise框架Pudding(所以它爲爲你安裝web3.js)。(譯註:Promise是流行於JavaScript社區中的一種異步調用模式。它很好的封裝了異步調用,使其可以靈活組合,而不會陷入callback hell.)

Transaction times. Promise對於DApp很是有用,由於交易寫入以太坊區塊鏈須要大約12-15秒的時間。即便在測試網絡上看起來沒有那麼慢,在正式網絡上卻可能會要更長的時間(例如你的交易可能用光了Gas,或者被寫入了一個孤兒塊)。

下面讓咱們給一個簡單的智能合約寫測試用例吧。

使用Truffle

首先確保你 1.安裝好了solc以及 2.testrpc。(testrpc須要Pythonpip。若是你是Python新手,你可能須要用virtualenv來安裝,這能夠將Python程序庫安裝在一個獨立的環境中。)

接下來安裝 3.Truffle(你可使用NodeJS's npm來安裝:npm install -g truffle,-g開關可能會須要sudo)。安裝好以後,在命令行中輸入truffle list來驗證安裝成功。而後建立一個新的項目目錄(我把它命名爲'conference'),進入這個目錄,運行truffle init。該命令會創建以下的目錄結構:

如今讓咱們在另外一個終端裏經過執行testrpc來啓動一個節點(你也能夠用geth):

回到以前的終端中,輸入truffle deploy。這條命令會部署以前truffle init產生的模板合約到網絡上。任何你可能遇到的錯誤信息都會在testrpc的終端或者執行truffle的終端中輸出。

在開發過程當中你隨時可使用truffle compile命令來確認你的合約能夠正常編譯(或者使用solc YourContract.sol),truffle deploy來編譯和部署合約,最後是truffle test來運行智能合約的測試用例。

第一個合約

下面是一個針對會議的智能合約,經過它參會者能夠買票,組織者能夠設置參會人數上限,以及退款策略。本文涉及的全部代碼均可以在這個代碼倉庫找到。

contract Conference {
  address public organizer;
  mapping (address => uint) public registrantsPaid;
  uint public numRegistrants;
  uint public quota;

  event Deposit(address _from, uint _amount);  // so you can log these events
  event Refund(address _to, uint _amount); 

  function Conference() { // Constructor
    organizer = msg.sender;
    quota = 500;
    numRegistrants = 0;
  }
  function buyTicket() public returns (bool success) {
    if (numRegistrants >= quota) { return false; }
    registrantsPaid[msg.sender] = msg.value;
    numRegistrants++;
    Deposit(msg.sender, msg.value);
    return true;
  }
  function changeQuota(uint newquota) public {
    if (msg.sender != organizer) { return; }
    quota = newquota;
  }
  function refundTicket(address recipient, uint amount) public {
    if (msg.sender != organizer) { return; }
    if (registrantsPaid[recipient] == amount) { 
      address myAddress = this;
      if (myAddress.balance >= amount) { 
        recipient.send(amount);
        registrantsPaid[recipient] = 0;
        numRegistrants--;
        Refund(recipient, amount);
      }
    }
  }
  function destroy() { // so funds not locked in contract forever
    if (msg.sender == organizer) { 
      suicide(organizer); // send funds to organizer
    }
  }
}

接下來讓咱們部署這個合約。(注意:本文寫做時我使用的是Mac OS X 10.10.5, solc 0.1.3+ (經過brew安裝),Truffle v0.2.3, testrpc v0.1.18 (使用venv))

部署合約

(譯註:圖中步驟翻譯以下:)

使用truffle部署智能合約的步驟:
1. truffle init (在新目錄中) => 建立truffle項目目錄結構
2. 編寫合約代碼,保存到contracts/YourContractName.sol文件。
3. 把合約名字加到config/app.json的'contracts'部分。
4. 啓動以太坊節點(例如在另外一個終端裏面運行testrpc)。
5. truffle deploy(在truffle項目目錄中)

添加一個智能合約。 在truffle init執行後或是一個現有的項目目錄中,複製粘帖上面的會議合約到contracts/Conference.sol文件中。而後打開config/app.json文件,把'Conference'加入'deploy'數組中。

啓動testrpc。 在另外一個終端中啓動testrpc

編譯或部署。 執行truffle compile看一下合約是否能成功編譯,或者直接truffle deploy一步完成編譯和部署。這條命令會把部署好的合約的地址和ABI(應用接口)加入到配置文件中,這樣以後的truffle testtruffle build步驟可使用這些信息。

出錯了? 編譯是否成功了?記住,錯誤信息便可能出如今testrpc終端也可能出如今truffle終端。

重啓節點後記得從新部署! 若是你中止了testrpc節點,下一次使用任何合約以前切記使用truffle deploy從新部署。testrpc在每一次重啓以後都會回到徹底空白的狀態。

合約代碼解讀

讓咱們從智能合約頭部的變量聲明開始:

address public organizer;
mapping (address => uint) public registrantsPaid;
uint public numRegistrants;
uint public quota;

address. 地址類型。第一個變量是會議組織者的錢包地址。這個地址會在合約的構造函數function Conference()中被賦值。不少時候也稱呼這種地址爲'owner'(全部人)。

uint. 無符號整型。區塊鏈上的存儲空間很緊張,保持數據儘量的小。

public. 這個關鍵字代表變量能夠被合約以外的對象使用。private修飾符則表示變量只能被本合約(或者衍生合約)內的對象使用。若是你想要在測試中經過web3.js使用合約中的某個變量,記得把它聲明爲public

Mapping或數組。(譯註:Mapping相似Hash, Directory等數據類型,不作翻譯。)在Solidity加入數組類型以前,你們都使用相似mapping (address => uint)的Mapping類型。這個聲明也能夠寫做address registrantsPaid[],不過Mapping的存儲佔用更小(smaller footprint)。這個Mapping變量會用來保存參加者(用他們的錢包地址表示)的付款數量以便在退款時使用。

關於地址。 你的客戶端(好比testrpc或者geth)能夠生成一個或多個帳戶/地址。testrpc啓動時會顯示10個可用地址:

第一個地址, accounts[0],是發起調用的默認地址,若是沒有特別指定的話。

組織者地址 vs. 合約地址。 部署好的合約會在區塊鏈上擁有本身的地址(與組織者擁有的是不一樣的地址)。在Solidity合約中可使用this來訪問這個合約地址,正如refundTicket函數所展現的:address myAddress = this;

Suicide, Solidity的好東西。(譯註:suicide意爲'自殺', 爲Solidity提供的關鍵字,不作翻譯。)轉給合約的資金會保存於合約(地址)中。最終這些資金經過destroy函數被釋放給了構造函數中設置的組織者地址。這是經過suicide(orgnizer);這行代碼實現的。沒有這個,資金可能被永遠鎖定在合約之中(reddit上有些人就遇到過),所以若是你的合約會接受資金必定要記得在合約中使用這個方法!

若是想要模擬另外一個用戶或者對手方(例如你是賣家想要模擬一個買家),你可使用可用地址數組中另外的地址。假設你要以另外一個用戶,accounts[1], 的身份來買票,能夠經過from參數設置:

conference.buyTicket({ from: accounts[1], value: some_ticket_price_integer });

函數調用能夠是交易。 改變合約狀態(修改變量值,添加記錄,等等)的函數調用自己也是轉帳交易,隱式的包含了發送人和交易價值。所以web3.js的函數調用能夠經過指定{ from: __, value: __ }參數來發送以太幣。在Solidity合約中,你能夠經過msg.sendermsg.value來獲取這些信息:

function buyTicket() public {
    ...
    registrantsPaid[msg.sender] = msg.value;
    ...
}

事件(Event)。 可選的功能。合約中的Deposit(充值)和Send(發送)事件是會被記錄在以太坊虛擬機日誌中的數據。它們實際上沒有任何做用,可是用事件(Event)把交易記錄進日誌是好的作法。

好了,如今讓咱們給這個智能合約寫一個測試,來確保它能工做。

寫測試

把項目目錄test/中的example.js文件重命名爲conference.js,文件中全部的'Example'替換爲'Conference'。

contract('Conference', function(accounts) { it("should assert true", function(done) { var conference = Conference.at(Conference.deployed_address); assert.isTrue(true); done(); // stops tests at this point }); });

在項目根目錄下運行truffle test,你應該看到測試經過。在上面的測試中truffle經過Conference.deployed_address得到合約部署在區塊鏈上的地址。

讓咱們寫一個測試來初始化一個新的Conference,而後檢查變量都正確賦值了。將conference.js中的測試代碼替換爲:

contract('Conference', function(accounts) { it("Initial conference settings should match", function(done) { var conference = Conference.at(Conference.deployed_address); // same as previous example up to here Conference.new({ from: accounts[0] }) .then(function(conference) { conference.quota.call().then( function(quota) { assert.equal(quota, 500, "Quota doesn't match!"); }).then( function() { return conference.numRegistrants.call(); }).then( function(num) { assert.equal(num, 0, "Registrants should be zero!"); return conference.organizer.call(); }).then( function(organizer) { assert.equal(organizer, accounts[0], "Owner doesn't match!"); done(); // to stop these tests earlier, move this up }).catch(done); }).catch(done); }); });

構造函數。 Conference.new({ from: accounts[0] })經過調用合約構造函數創造了一個新的Conference實例。因爲不指定from時會默認使用accounts[0],它其實能夠被省略掉:

Conference.new({ from: accounts[0] }); // 和Conference.new()效果相同

Promise. 代碼中的那些thenreturn就是Promise。它們的做用寫成一個深深的嵌套調用鏈的話會是這樣:

conference.numRegistrants.call().then( function(num) { assert.equal(num, 0, "Registrants should be zero!"); conference.organizer.call().then( function(organizer) { assert.equal(organizer, accounts[0], "Owner doesn't match!"); }).then( function(...)) }).then( function(...)) // Because this would get hairy...

Promise減小嵌套,使代碼變得扁平,容許調用異步返回,而且簡化了表達「成功時作這個」和「失敗時作那個」的語法。Web3.js經過回調函數實現異步調用,所以你不須要等到交易完成就能夠繼續執行前端代碼。Truffle藉助了用Promise封裝web3.js的一個框架,叫作Pudding,這個框架自己又是基於Bluebird的,它支持Promise的高級特性。

call. 咱們使用call來檢查變量的值,例如conference.quota.call().then(...,還能夠經過傳參數,例如call(0), 來獲取mapping在index 0處的元素。Solidity的文檔說這是一種特殊的「消息調用」由於 1.不會爲礦工記錄和 2.不須要從錢包帳戶/地址發起(所以它沒有被帳戶持有者私鑰作簽名)。另外一方面,交易/事務(Transaction)會被礦工記錄,必須來自於一個帳戶(也就是有簽名),會被記錄到區塊鏈上。對合約中數據作的任何修改都是交易。僅僅是檢查一個變量的值則不是。所以在讀取變量時不要忘記加上call()!不然會發生奇怪的事情。(此外若是在讀取變量是遇到問題別忘記檢查它是不是public。)call()也能用於調用不是交易的函數。若是一個函數原本是交易,但你卻用call()來調用,則不會在區塊鏈上產生交易。

斷言。 標準JS測試中的斷言(若是你不當心拼成了複數形式'asserts',truffle會報錯,讓你一頭霧水),assert.equal是最經常使用的,其餘類型的斷言能夠在Chai的文檔中找到。

再一次運行truffle test確保一切工做正常。

測試合約函數調用

如今咱們測試一下改變quote變量的函數能工做。在tests/conference.js文件的contract('Conference', function(accounts) {...};)的函數體中添加以下測試用例:

it("Should update quota", function(done) { var c = Conference.at(Conference.deployed_address); Conference.new({from: accounts[0] }).then( function(conference) { conference.quota.call().then( function(quota) { assert.equal(quota, 500, "Quota doesn't match!"); }).then( function() { return conference.changeQuota(300); }).then( function(result) { // result here is a transaction hash console.log(result); // if you were to print this out it’d be long hex - the transaction hash return conference.quota.call() }).then( function(quota) { assert.equal(quota, 300, "New quota is not correct!"); done(); }).catch(done); }).catch(done); });

這裏的新東西是調用changeQuota函數的那一行。console.log對於調試頗有用,用它能在運行truffle的終端中輸出信息。在關鍵點插入console.log能夠查看執行到了哪一步。記得把Solidity合約中changeQuota函數被聲明爲public,不然你不能調用它:

function changeQuota(uint newquota) public {  }

測試交易

如今讓咱們調用一個須要發起人發送資金的函數。

Wei. 以太幣有不少種單位(這裏有個頗有用的轉換器),在合約中一般用的是Wei,最小的單位。Web3.js提供了在各單位與Wei之間互相轉換的便利方法,形如web3.toWei(.05, 'ether')。JavaScript在處理很大的數字時有問題,所以web3.js使用了程序庫BigNumber,並建議在代碼各處都以Wei作單位,直到要給用戶看的時候(文檔

帳戶餘額。 Web3.js提供了許多提供方便的方法,其中另外一個會在下面測試用到的是web3.eth.getBalance(some_address)。記住發送給合約的資金會由合約本身持有直到調用suicide

contract(Conference, function(accounts) {...};)的函數體中插入下面的測試用例。在高亮顯示的方法中,測試用例讓另外一個用戶(accounts[1])以ticketPrice的價格買了一張門票。而後它檢查合約的帳戶餘額增長了ticketPrice,以及購票用戶被加入了參會者列表。

這個測試中的buyTicket是一個交易函數:

it("Should let you buy a ticket", function(done) { var c = Conference.at(Conference.deployed_address); Conference.new({ from: accounts[0] }).then( function(conference) { var ticketPrice = web3.toWei(.05, 'ether'); var initialBalance = web3.eth.getBalance(conference.address).toNumber(); conference.buyTicket({ from: accounts[1], value: ticketPrice }).then( function() { var newBalance = web3.eth.getBalance(conference.address).toNumber(); var difference = newBalance - initialBalance; assert.equal(difference, ticketPrice, "Difference should be what was sent"); return conference.numRegistrants.call(); }).then(function(num) { assert.equal(num, 1, "there should be 1 registrant"); return conference.registrantsPaid.call(accounts[1]); }).then(function(amount) { assert.equal(amount.toNumber(), ticketPrice, "Sender's paid but is not listed"); done(); }).catch(done); }).catch(done); });

交易須要簽名。 和以前的函數調用不一樣,這個調用是一個會發送資金的交易,在這種狀況下購票用戶(accounts[1])會用他的私鑰對buyTicket()調用作簽名。(在geth中用戶須要在發送資金以前經過輸入密碼來批准這個交易或是解鎖錢包的帳戶。)

toNumber(). 有時咱們須要把Solidity返回的十六進制結果轉碼。若是結果多是個很大的數字能夠用web3.toBigNumber(numberOrHexString)來處理由於JavaScript直接對付大數要糟。

測試包含轉帳的合約

最後,爲了完整性,咱們確認一下refundTicket方法能正常工做,並且只有會議組織者能調用。下面是測試用例:

it("Should issue a refund by owner only", function(done) { var c = Conference.at(Conference.deployed_address); Conference.new({ from: accounts[0] }).then( function(conference) { var ticketPrice = web3.toWei(.05, 'ether'); var initialBalance = web3.eth.getBalance(conference.address).toNumber(); conference.buyTicket({ from: accounts[1], value: ticketPrice }).then( function() { var newBalance = web3.eth.getBalance(conference.address).toNumber(); var difference = newBalance - initialBalance; assert.equal(difference, ticketPrice, "Difference should be what was sent"); // same as before up to here // Now try to issue refund as second user - should fail return conference.refundTicket(accounts[1], ticketPrice, {from: accounts[1]}); }).then( function() { var balance = web3.eth.getBalance(conference.address).toNumber(); assert.equal(web3.toBigNumber(balance), ticketPrice, "Balance should be unchanged"); // Now try to issue refund as organizer/owner - should work return conference.refundTicket(accounts[1], ticketPrice, {from: accounts[0]}); }).then( function() { var postRefundBalance = web3.eth.getBalance(conference.address).toNumber(); assert.equal(postRefundBalance, initialBalance, "Balance should be initial balance"); done(); }).catch(done); }).catch(done); });

這個測試用例覆蓋的Solidity函數以下:

function refundTicket(address recipient, uint amount) public returns (bool success) {
  if (msg.sender != organizer) { return false; }
  if (registrantsPaid[recipient] == amount) { 
    address myAddress = this;
    if (myAddress.balance >= amount) { 
      recipient.send(amount);
      Refund(recipient, amount);
      registrantsPaid[recipient] = 0;
      numRegistrants--;
      return true;
    }
  }
  return false;
}

合約中發送以太幣。 address myAddress = this展現瞭如何獲取該會議合約實例的地址,以變接下來檢查這個地址的餘額(或者直接使用this.balance)。合約經過recipient.send(amount)方法把資金髮回了購票人。

交易沒法返回結果給web3.js. 注意這一點!refundTicket函數會返回一個布爾值,可是這在測試中沒法檢查。由於這個方法是一個交易函數(會改變合約內數據或是發送以太幣的調用),而web3.js獲得的交易運行結果是一個交易哈希(若是打印出來是一個長長的十六進制/怪怪的字符串)。既然如此爲何還要讓refundTicket返回一個值?由於在Solidity合約內能夠讀到這個返回值,例如當另外一個合約調用refundTicket()的時候。也就是說Solidity合約能夠讀取交易運行的返回值,而web3.js不行。另外一方面,在web3.js中你能夠用事件機制(Event, 下文會解釋)來監控交易運行,而合約不行。合約也沒法經過call()來檢查交易是否修改了合約內變量的值。

關於sendTransaction(). 當你經過web3.js調用相似buyTicket()或者refundTicket()的交易函數時(使用web3.eth.sendTransaction),交易並不會當即執行。事實上交易會被提交到礦工網絡中,交易代碼直到其中一位礦工產生一個新區塊把交易記錄進區塊鏈以後才執行。所以你必須等交易進入區塊鏈而且同步回本地節點以後才能驗證交易執行的結果。用testrpc的時候可能看上去是實時的,由於測試環境很快,可是正式網絡會比較慢。

事件/Event. 在web3.js中你應該監聽事件而不是返回值。咱們的智能合約示例定義了這些事件:

event Deposit(address _from, uint _amount);
event Refund(address _to, uint _amount);

它們在buyTicket()refundTicket()中被觸發。觸發時你能夠在testrpc的輸出中看到日誌。要監聽事件,你可使用web.js監聽器(listener)。在寫本文時我還不能在truffle測試中記錄事件,可是在應用中沒問題:

Conference.new({ from: accounts[0] }).then( function(conference) { var event = conference.allEvents().watch({}, ''); // or use conference.Deposit() or .Refund() event.watch(function (error, result) { if (error) { console.log("Error: " + error); } else { console.log("Event: " + result.event); } }); // ...

過濾器/Filter. 監聽全部事件可能會產生大量的輪詢,做爲替代可使用過濾器。它們能夠更靈活的開始或是中止對事件的監聽。更多過濾器的信息可查看Solidity文檔

總的來講,使用事件和過濾器的組合比檢查變量消耗的Gas更少,於是在驗證正式網絡的交易運行結果時很是有用。

Gas. (譯註:以太坊上的燃料,由於代碼的執行必須消耗Gas。直譯爲汽油比較突兀,故保留原文作專有名詞。)直到如今咱們都沒有涉及Gas的概念,由於在使用testrpc時一般不須要顯式的設置。當你轉向geth和正式網絡時會須要。在交易函數調用中能夠在{from: __, value: __, gas: __}對象內設置Gas參數。Web3.js提供了web3.eth.gasPrice調用來獲取當前Gas的價格,Solidity編譯器也提供了一個參數讓你能夠從命令行獲取合約的Gas開銷概要:solc --gas YouContract.sol。下面是Conference.sol的結果:

爲合約建立DApp界面

下面的段落會假設你沒有網頁開發經驗。

上面編寫的測試用例用到的都是在前端界面中也能夠用的方法。你能夠把前端代碼放到app/目錄中,運行truffle build以後它們會和合約配置信息一塊兒編譯輸出到build/目錄。在開發時可使用truffle watch命令在app/有任何變更時自動編譯輸出到build/目錄。而後在瀏覽器中刷新頁面便可看到build/目錄中的最新內容。(truffle serve能夠啓動一個基於build/目錄的網頁服務器。)

app/目錄中有一些樣板文件幫助你開始:

index.html會加載app.js

所以咱們只須要添加代碼到app.js就能夠了。

默認的app.js會在瀏覽器的console(控制檯)中輸出一條"Hello from Truffle!"的日誌。在項目根目錄中運行truffle watch,而後在瀏覽器中打開build/index.html文件,再打開瀏覽器的console就能夠看到。(大部分瀏覽器例如Chrome中,單擊右鍵 -> 選擇Inspect Element而後切換到Console便可。)

app.js中,添加一個在頁面加載時會運行的window.onload調用。下面的代碼會確認web3.js已經正常載入並顯示全部可用的帳戶。(注意:你的testrpc節點應該保持運行。)

window.onload = function() { var accounts = web3.eth.accounts; console.log(accounts); }

看看你的瀏覽器console中看看是否打印出了一組帳戶地址。

如今你能夠從tests/conference.js中複製一些代碼過來(去掉只和測試有關的斷言),將調用返回的結果輸出到console中以確認代碼能工做。下面是個例子:

window.onload = function() { var accounts = web3.eth.accounts; var c = Conference.at(Conference.deployed_address); Conference.new({ from: accounts[0] }).then( function(conference) { var ticketPrice = web3.toWei(.05, 'ether'); var initialBalance = web3.eth.getBalance(conference.address).toNumber(); console.log("The conference's initial balance is: " + initialBalance); conference.buyTicket({ from: accounts[1], value: ticketPrice }).then( function() { var newBalance = web3.eth.getBalance(conference.address).toNumber(); console.log("After someone bought a ticket it's: " + newBalance); return conference.refundTicket(accounts[1], ticketPrice, {from: accounts[0]}); }).then( function() { var balance = web3.eth.getBalance(conference.address).toNumber(); console.log("After a refund it's: " + balance); }); }); };

上面的代碼應該輸出以下:

(console輸出的warning信息可忽略。)

如今起你就可使用你喜歡的任何前端工具,jQuery, ReactJS, Meteor, Ember, AngularJS,等等等等,在app/目錄中構建能夠與以太坊智能合約互動的DApp界面了!接下來咱們給出一個極其簡單基於jQuery的界面做爲示例。

這裏是index.html的代碼,這裏是app.js的代碼

經過界面測試了智能合約以後我意識到最好加入檢查以保證相同的用戶不能註冊兩次。另外因爲如今是運行在testrpc節點上,速度很快,最好是切換到geth節點並確認交易過程依然能及時響應。不然的話界面上就應該顯示提示信息而且在處理交易時禁用相關的按鈕。

嘗試geth. 若是你使用geth, 能夠嘗試如下面的命令啓動 - 在我這兒(geth v1.2.3)工做的很好:

build/bin/geth --rpc --rpcaddr="0.0.0.0" --rpccorsdomain="*" --mine --unlock='0 1' --verbosity=5 --maxpeers=0 --minerthreads='4'  --networkid '12345' --genesis test-genesis.json

這條命令解鎖了兩個帳戶, 01。1. 在geth控制檯啓動後你可能須要輸入這兩個帳戶的密碼。2. 你須要在test-genesis.json文件裏面的'alloc'配置中加入你的這兩個帳戶,而且給它們充足的資金。3. 最後,在建立合約實例時加上gas參數:

Conference.new({from: accounts[0], gas: 3141592})

而後把整個truffle deploytruffle build流程重來一遍。

教程中的代碼。 在這篇基礎教程中用到的全部代碼均可以在這個代碼倉庫中找到。

自動爲合約生成界面。 SilentCicero製做了一個叫作DApp Builder的工具,能夠用Solidity合約自動生成HTML, jQuery和web.js的代碼。這種模式也正在被愈來愈多的正在開發中的開發者工具採用。

教程到此結束! 最後一章咱們僅僅學習了一套工具集,主要是Truffle和testrpc. 要知道即便在ConsenSys內部,不一樣的開發者使用的工具和框架也不盡相同。你可能會發現更適合你的工具,這裏所說的工具可能很快也會有改進。可是本文介紹的工做流程幫助我走上了DApp開發之路。

(⊙ω⊙) wonk wonk

感謝Joseph Chow的校閱和建議,Christian Lundkvist, Daniel Novy, Jim Berry, Peter Borah和Tim Coulter幫我修改文字和debug,以及Tim Coulter, Nchinda Nchinda和Mike Goldin對DApp前端步驟圖提供的幫助。

相關文章
相關標籤/搜索