歡迎光臨
我們一直在努力

區塊鏈:教程 | 以太坊智慧合約程式設計之菜鳥教程

文章摘要: 如果你想要在測試中通過web3.js使用合約中的某個變數這個地址會在合約的建構函式 function Conference() 中被賦值

區塊鏈:教程 | 以太坊智慧合約程式設計之菜鳥教程。

這篇介紹以太坊合約的文章寫得很好,在查詢了這麼多資料,進行對比之後,感覺閱讀這一篇就可以大體理解以太坊程式設計的原理,如果對個別的知識點還有點含糊,可以相應地去查一查,就是以這篇為主幹,別的資料為輔。稍微整理了一下格式,以及修改了一些半形符號。

譯註: 原文 首發於ConsenSys開發者部落格,原作者為Eva以及ConsenSys的開發團隊。如果您想要獲取更多及時資訊,可以訪問 ConsenSys 首頁點選左下角Newsletter訂閱郵件。本文的翻譯獲得了ConsenSys創始人Lubin先生的授權。

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

  • 概述,討論了關鍵概念,幾大以太坊客戶端以及寫智慧合約用到的程式語言。
  • 討論了總體的工作流程,以及目前流行的一些DApp框架和工具。
  • 主要關於程式設計,我們將學習如何使用Truffle來為智慧合約編寫測試和構建DApp。

第一部分 概述

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

如果你覺得白皮書中的章節太晦澀,也可以直接動手來熟悉以太坊。在以太坊上做開發並不要求你理解所有那些「密碼經濟電腦科學」(crypto economic computer science),而白皮書的大部分是關於以太坊想對於比特幣架構上的改進。

新手教程

ethereum.org 提供了官方的新手入門教程,以及一個代幣合約和眾籌合約的教程。合約語言Solidity也有 官方文件 。學習智慧合約的另一份不錯的資料(也是我的入門資料)是 dappsForBeginners ,不過現在可能有些過時了。

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

基本概念

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

公鑰加密系統。Alice有一把公鑰和一把私鑰。她可以用她的私鑰建立數字簽名,而Bob可以用她的公鑰來驗證這個簽名確實是用Alice的私鑰建立的,也就是說,確實是Alice的簽名。當你建立一個以太坊或者比特幣錢包的時候,那長長的 0xdf...5f 地址實質上是個公鑰,對應的私鑰儲存某處。類似於Coinbase的線上錢包可以幫你保管私鑰,你也可以自己保管。如果你弄丟了存有資金的錢包的私鑰,你就等於永遠失去了那筆資金,因此你最好對私鑰做好備份。過來人表示:通過踩坑學習到這一點是非常痛苦的…

點對點網路。就像BitTorrent, 以太坊分佈式網絡中的所有節點都地位平等,沒有中心伺服器。(未來會有半中心化的混合型服務出現為使用者和開發者提供方便,這我們後面會講到。)

區塊鏈。區塊鏈就像是一個全球唯一的帳簿,或者說是資料庫,記錄了網路中所有交易歷史。

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

節點。你可以執行節點,通過它讀寫以太坊區塊鏈,也即使用以太坊虛擬機器。完全節點需要下載整個區塊鏈。輕節點仍在開發中。

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

工作量證明。礦工們總是在競爭解決一些數學問題。第一個解出答案的(算出下一個區塊)將獲得以太幣作為獎勵。然後所有節點都更新自己的區塊鏈。所有想要算出下一個區塊的礦工都有與其他節點保持同步,並且維護同一個區塊鏈的動力,因此整個網路總是能達成共識。(注意:以太坊正計劃轉向沒有礦工的權益證明系統(POS),不過那不在本文討論範圍之內。)

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

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

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

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

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

編寫和部署智慧合約並不要求你執行一個以太坊節點。下面有列出。但如果是爲了學習的話,還是應該執行一個以太坊節點,以便理解其中的基本元件,何況執行節點也不難。

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

以太坊有許多不同語言的客戶端實現(即多種與以太坊網路互動的方法),包括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 compiler 和 Cosmo 都可以讓你快速開始在瀏覽器中編寫智慧合約。你甚至可以讓這些工具使用你的本地節點,只要讓本地節點開一個埠(注意安全!這些工具站點必須可信,而且千萬不要把你的全部身家放在這樣一個本地節點裏面! 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需要 Python 和 pip 。如果你是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 deploy , truffle 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前端步驟圖提供的幫助。

未經允許不得轉載:頭條楓林網 » 區塊鏈:教程 | 以太坊智慧合約程式設計之菜鳥教程