Ethereum Dapp Tutorial — Push Button

yaohsin
Taipei Ethereum Meetup
15 min readAug 7, 2017

此教學將演示如何使用 Truffle 從頭到尾開發出一個 Dapp,涵蓋下列範圍

Dapp說明

美劇 Lost 中,Jack 一行人在迷樣的島上發現神秘基地,裏頭的實驗室有一台電腦與倒數計時器,每 108 分鐘就必須在鍵盤上輸入一串神秘數字 4、8、15、16、23、42,並按下 Enter,否則將會迎來世界末日,原先的操作員已經落跑了,底該怎麼辦才好?這會是一場鬧劇嗎?還是某種行為心理學實驗?

就在眾人胡思亂想之際,身為 Ethereum 愛好者的你,心中盤算的卻是怎麼能依賴一個或一群人身上來保護世界,如此的重責大任應該要是去中心化的,讓地球上每個人都可以挺身而出,貢獻一己之力。想到這裡,你抬頭注意到倒數計時器上的數字慢慢遞減,嗯,距離下一次 Deadline 大概還剩下 87 分鐘,足夠開發出一個 Dapp,事不宜遲開始動手吧。

設定開發環境

在使用 Truffle 與 Testrpc 之前,要先安裝

  1. node.js v6+ LTS
  2. Git

接著透過下列指令分別安裝模擬 ethereum client 的 Testrpc 與 Truffle

  • npm install -g ethereumjs-testrpc
  • npm install -g truffle

開啟Truffle新專案

有別於之前使用 truffle init 指令來初始化專案,在 Truffle 推出 Boxes 功能之後,我們可以直接套用稱作 react-box 的樣板,此樣板已經整合 create-react-app,可以馬上開發 react web,省下專案設定的時間。我們將這tutorial 專案稱作 push-button。

// 建立目錄
mkdir push-button
// 進入目錄
cd push-button
// 建立react-box專案
truffle unbox react-box

目錄結構

  • /contracts: 存放智能合約原始碼的地方,可以看到裡面已經有放兩個 sol檔案。我們開發的 PushButton.sol 也會放在這邊。
  • /migrations: 這是 Truffle 用來部署智能合約的功能,待會我們會修改2_deploy_contracts.js 來部署 PushButton.sol。
  • /test: 測試智能合約的程式碼放這目錄,支援 js 與 sol 測試。
  • /public、/src: 存放 react web 的地方,後面用到會再說明。
  • truffle.js: Truffle 的設定檔案。

撰寫PushButton.sol智能合約

首先在 /contracts 目錄裡開一個 PushButton.sol 檔案,填入下列內容。

pragma solidity ^0.4.13;contract PushButton {  function PushButton () {

}
}

要先指定 solidity 的版本,^符號表示0.4.13或以後的版本都可。function 名稱與合約名稱相同代表這是 constractor,合約部署之後會先執行一次,可用來初始化變數。

定義合約變數

Solidity 語言提供像是 Boolean、Integer、Address、Array 等型別,詳情可以參考這裡,PushButton 總共需要五個變數,變數宣告在 contract PushButton {} 裡。

contract PushButton {  uint public startBlock; // 記錄合約起始block  uint public interval = 108 * 60 / 4; // 108分鐘 (Kovan testnet)  uint public nextTimeoutBlock; // 下一次timeout block  uint public totalPush; // 總共按了幾次  string public title; // 依據totalPush可獲得不同title, just for fun  //以下省略}

注意 public 關鍵字表示會自動生成 getter function,可供任何人查詢。

修改constructor來初始化變數

  function PushButton() {    startBlock = block.number; // 紀錄合約部署當下的block number    nextTimeoutBlock = startBlock + interval; // 計算timeout    totalPush = 0;  }

其中 block 是一個 globally available variables,有很多 member 可以存取,包括 block.number,以取得當下的 block number。詳情看這裡

撰寫Push function

PushButton 合約最主要的 function 就是 push(),按下按鈕之後,計數器就會更新 nextTimeoutBlock、totalPush、title,並且發出 event 通知其他人。

function push() isTimeout() returns (bool) {  totalPush += 1; // total push +1  nextTimeoutBlock = getBlock() + interval; // 更新nextTimeoutBlock  checkTitle(); // 更新title, just for fun  ButtonPushed(msg.sender, totalPush, nextTimeoutBlock); //發event  return true;}

這邊使用 modifier isTimeout()來確認是否已經 timeout,若還沒 timeout 才能繼續執行,否則就取消執行,並丟出 exception,詳情看此。modifier 很好用,當有多個 function 都需要相同的檢查時,可以重複使用,詳情看此

modifier isTimeout() {  require( getBlock() <= nextTimeoutBlock );  _;}

require(…) 裡的判斷式要為 true,否則會丟出 exception。

另外寫了一個輔助 function getBlock() 用來取得當下的 block,注意修飾字為 constant 表示這是靜態查詢的 function,不需要發出交易,不用花 gas 即可使用。

function getBlock() constant returns (uint) {  return block.number;}

最後發出 Event 通知外界,帶出執行 push function 的 address、totalPush、及 nextTimeoutBlock。

event ButtonPushed(address indexed _address, uint _totalPush, uint _nextTimeoutBlock);

完整的 PushButton.sol 如下

編譯合約

我們必須將 Solidity 合約編譯成 EVM bytecode 才能部署至 Ethereum network,執行truffle compile就可以編譯合約,編譯完的結果會寫到 /build/contracts/ 目錄底下,如 PushButton.json,裡面包含後續開發 Dapp所需的資料。因此,當合約內容有更新時,都要重新執行一次 truffle compile 否則會找不到對應的功能。

部署合約

Truffle 使用 Migration module 來控制合約的部署,在 /migrations 目錄裡有兩個檔案,1_initial_migration.js 負責部署 Migrations.sol,這個合約會用來觀察後續的合約部署狀況。我們修改 2_deploy_contracts.js 來指定接下來要部署的 PushBotton.sol。

var PushButton = artifacts.require("./PushButton.sol");module.exports = function(deployer) {  deployer.deploy(PushButton);};

修改完之後,開啟一個 console 視窗執行 testrpc 指令,這會啟動一個虛擬的 ethereum client,透過 localhost:8545 來溝通。接著在另一個 console 視窗執行 truffle migration 指令,就會看到合約成功部署至 testrpc 上,如下畫面。

撰寫測試程式

除了使用 JavaScript 寫測試程式之外,Truffle 也可以用 Solidity 語言寫測試程式,非常方便。首先在 /test 目錄下建立 TestPushButton.sol,內容如下

pragma solidity ^0.4.13;import "truffle/Assert.sol";import "truffle/DeployedAddresses.sol";import "../contracts/PushButton.sol";contract TestPushButton {  PushButton pb = PushButton(DeployedAddresses.PushButton());}
  • Assert.sol 提供各種好用的 assertion,讓我們可以在測試程式中使用,完整的assertion請看這邊
  • DeployedAddresses.sol 紀錄已部署的合約地址,每次測試都會重新部署合約。
  • PushButton.sol 就是我們開發的合約。

宣告好 PushButton 變數之後,就可以開始寫 test function。首先來測試合約在部署後是否能夠正確的初始化變數,有 interval、nextTimeoutBlock、totalPush。

function testInitialUsingDeployedContract() {  Assert.equal(pb.interval(), 1620, "interval should be 1620 blocks long");  Assert.equal(pb.nextTimeoutBlock(), pb.startBlock() + 1620, "nextTimeoutBlock should be sum of startblock and interval");  Assert.equal(pb.totalPush(), 0, "totalPush should be 0");}

接著測試 Push function,這邊除了要檢查 push() 是否能正確執行外,還有totalPush、nextTimeoutBlock 是否有如預期般更新。

function testUserCanPush() {  uint totalPush = pb.totalPush();  uint currentBlock = pb.getBlock();  Assert.equal(pb.push(), true, "push should success");  Assert.equal(pb.totalPush(), totalPush + 1, "totalPush should increase 1");  Assert.equal(pb.nextTimeoutBlock(), currentBlock + pb.interval(), "nextTimeoutBlock shoud be reset");}

最後是測試當 timeout 之後,是否還能執行 push()。但由於我們無法控制testrpc 的 block number,因此需要引入一個 PushButtonMock.sol 合約,其目的是為了模擬 block number,做法很簡單只要 overwrite PushButton.sol合約的 getBlock function 即可。我們將 PushButtonMock.sol 放在 /test/helpers/ 目錄中。

有了 PushButtonMock.sol 之後就可以來寫最後一個測試,我們利用setMockedBlockNumber() 將故意將 block number 設定成nextTimeoutBlock() +1,即可測試 push() 是否回傳 false。

function testUserCannotPushAfterTimeout() {  PushButtonMock pb = new PushButtonMock();  pb.setMockedBlockNumber(pb.nextTimeoutBlock() + 1);  Assert.equal(pb.push(), false, "push should be fail");}

這樣就完成 TestPushButton.sol 測試案例撰寫,最後執行 truffle test指令開始測試,記得此時 testrpc 依然要開著。結果如下

大家一定有注意到最後一個測試噴出錯誤,這樣是正常的,因為 push() 沒有通過 modifier isTimeout() 的檢查。[若有人知道怎麼正確的測試require、revert、throw等,請教教我,謝謝XD]

開發web與合約互動

到目前為止我們已經完成合約的開發,可以使用如 parity、mist、browser-solidity 等工具將合約發佈至 blockchain,並透過他們提供的 UI 來與合約互動。接下來將重點說明如何在 web 中呼叫合約 function,最後開發出自己喜歡的 web UI。

web 的程式碼主要放在 /src 目錄,結構如下。

  • getWeb3.js — 用來初始化 web3,並且設定 Provider。這邊會先偵測 web3是否已經被嵌入至網頁,如 MetaMask。否則連結到特定的 Provider,如http://localhost:8545。web3 的細節可以參考這邊
  • App.js — 此教學的UI功能都寫在這邊。

呼叫 getWeb3 取得 web3 instance 之後,接著使用 truffle-contract 建立contract instance。先初始化 pushButton,再設定 Provider,就可以使用 at() 指定已經部署的合約位址,最後取得 pushButtonInstance 即可呼叫合約定義的功能。若要呼叫 constant function 則需加上 .call()。

另一個重要的 function 為 push(),先透過 web3.eth.getAccounts 取得使用者的 accounts,再呼叫 pushButtonInstance.push({from: accounts[0]}) ,其中from 用來指定發出交易的帳號地址,最後印出交易 id。

其餘程式碼就請各位自行參考。

測試 Web 功能

我們使用 Chrome 瀏覽器,需要安裝 MetaMask plugin。我通常習慣使用browser-solidity + MetaMask 來部署合約至 Kovan testnet,做法可參考這篇。取得合約地址之後貼至 App.js 中的 const contractAddress = 0x... 接著執行 npm run start指令啟動 web server,一切都沒問題的話,就會看到下列畫面。

點下紅色按鈕後,會跳出 MetaMask 的 confirm transaction 視窗,若沒跳出來請檢查是否有安裝 MetaMask 以及登入 MetaMask。

transaction 被確認之後,即可看到 UI 資訊已經被更新。

將 Dapp 發佈至 Heroku

好不容易將 Push Button Dapp 開發完成,當然不能只有自己看到,發佈至Heroku 方便又免費。先來安裝 heroku CLI tool,請參考這邊。接著在 push-button 專案目錄下新增一個 static.json 檔案,用來指定 root,內容如下:

{  "root": "build_webpack/"}

最後依序執行下列指令

後記

身為 Ethereum 愛好者的你順利開發完 Dapp,且還做了改進,現在只需要按下一個紅色按鈕就可以暫時解除世界毀滅的危機,你迫不及待將網頁連結發送至 Taipei Ethereum Meetup 社團,並默默祈禱永遠會有人按下紅色按鈕。

Push Button連結 — https://lost-button.herokuapp.com/
(記得MetaMask要選擇Kovan testnet)

Source code放在這邊

--

--