Nic Lin's Blog

喜歡在地上滾的工程師

用 Truffle 開發 DApp 以太坊投票程序應用 Part 1

這篇文會建構一個去中心化的 (Decentralized) 投票應用。利用這個投票應用,用戶可以在不可信 (trustless) 的分布環境對特定候選人進行投票,每次投票都會被紀錄在區塊鏈上。

所謂去中心化應用 (DApp: Decentralized Application), 就是一個不存在中心服務器的應用。在網路中成百上千的電腦上,都可以運行該應用的副本,這使他幾乎不可能出現當機的情況。

安裝節點仿真器

為了便於開發和測試,我會用 ganache 來模擬區塊鏈節點,以變快速開發並測試應用,這樣可以把注意力集中在邏輯開發和理解上,不然真的要跑一個測試鏈或是私有鏈,還是稍嫌麻煩的。

npm install -g ganache-cli

直接用 command line 輸入 ganache-cli

ganache 會輸出以下訊息:

Ganache CLI v6.0.3 (ganache-core: 2.0.2)

Available Accounts
==================
(0) 0x5c252a0c0475f9711b56ab160a1999729eccce97
(1) 0x353d310bed379b2d1df3b727645e200997016ba3
(2) 0xa3ddc09b5e49d654a43e161cae3f865261cabd23
(3) 0xa8a188c6d97ec8cf905cc1dd1cd318e887249ec5
(4) 0xc0aa5f8b79db71335dacc7cd116f357d7ecd2798
(5) 0xda695959ff85f0581ca924e549567390a0034058
(6) 0xd4ee63452555a87048dcfe2a039208d113323790
(7) 0xc60c8a7b752d38e35e0359e25a2e0f6692b10d14
(8) 0xba7ec95286334e8634e89760fab8d2ec1226bf42
(9) 0x208e02303fe29be3698732e92ca32b88d80a2d36

Private Keys
==================
(0) a6de9563d3db157ed9926a993559dc177be74a23fd88ff5776ff0505d21fed2b
(1) 17f71d31360fbafbc90cad906723430e9694daed3c24e1e9e186b4e3ccf4d603
(2) ad2b90ce116945c11eaf081f60976d5d1d52f721e659887fcebce5c81ee6ce99
(3) 68e2288df55cbc3a13a2953508c8e0457e1e71cd8ae62f0c78c3a5c929f35430
(4) 9753b05bd606e2ffc65a190420524f2efc8b16edb8489e734a607f589f0b67a8
(5) 6e8e8c468cf75fd4de0406a1a32819036b9fa64163e8be5bb6f7914ac71251cc
(6) c287c82e2040d271b9a4e071190715d40c0b861eb248d5a671874f3ca6d978a9
(7) cec41ef9ccf6cb3007c759bf3fce8ca485239af1092065aa52b703fd04803c9d
(8) c890580206f0bbea67542246d09ab4bef7eeaa22c3448dcb7253ac2414a5362a
(9) eb8841a5ae34ff3f4248586e73fcb274a7f5dd2dc07b352d2c4b71132b3c73f0

HD Wallet
==================
Mnemonic:   cancel better shock lady capable main crunch alcohol derive alarm duck umbrella
Base HD Path: m/44'/60'/0'/0/{account_index}

Listening on localhost:8545

為了測試, ganache 會默認自動建立 10 個帳戶,並且每個帳戶已經有 100 ETH 可以使用。

最後一句話 Listening on localhost:8545 是告訴我們節點仿真器的肩聽地址和端口為 localhost:8545,在使用 web3.js 時,需要傳入這個地址來告訴 web3.js 應當連接到哪一個節點上。

安裝 Truffle

Truffle 是一個 DAPP 開發框架,他簡化了一些建構和管理上的麻煩。

npm install -g truffle

Truffle 提供了一些項目模版,可以快速搭建骨架代碼,如下,我們可以使用 webpack 項目模版來進行開發

~$ mkdir -p ~/tfapp
~$ cd ~/tfapp
~/tfapp$ truffle unbox webpack

由於模版會生成範例合約,我們暫時不需要,可以刪除 contracts 目錄中除了 Migrations.sol 之外的文件

~/tfapp$ rm contracts/ConvertLib.sol contracts/MetaCoin.sol

Migrations.sol 該合約是用來管理應用合約的部屬,請勿刪除。

建立投票合約

新增投票合約

touch contracts/Voting.sol

並且編輯他

pragma solidity ^0.4.18;

contract Voting {

  mapping (bytes32 => uint8) public votesReceived;
  bytes32[] public candidateList;

  constructor (bytes32[] candidateNames) public {
    candidateList = candidateNames;
  }

  function totalVotesFor(bytes32 candidate) public view returns (uint8) {
    require(validCandidate(candidate));
    return votesReceived[candidate];
  }

  function voteForCandidate(bytes32 candidate) public {
    require(validCandidate(candidate));
    votesReceived[candidate]  += 1;
  }

  function validCandidate(bytes32 candidate) view public returns (bool) {
    for(uint i = 0; i < candidateList.length; i++) {
      if (candidateList[i] == candidate) {
        return true;
      }
    }
    return false;
   }
}

這是一個非常簡單的智能合約,投票就記錄。

簡單說明一下

  • constructor (bytes32[] candidateNames): 合約構造函數,部屬時執行 (以前是用 function 寫,後來就要改用 constructor 了)
  • votesReceived: 記錄所有候選人票數的字典
  • candidateList: 記錄全部候選人名稱的數組
  • voteForCandidate: 投票給指定名稱的候選人
  • totalVotesFor: 讀取指定名稱候選人的票數
  • validCandidate: 在 Solidity 中無法像在 JS 中直接使用 votesReceived.keys 可以獲取所有候選人姓名,所以在這裡單獨管理所有候選人名稱

將自動生成的 app/index.html 替換如下

<!DOCTYPE html>
<html>
<head>
  <title>Hello World DApp</title>
  <link href='https://fonts.googleapis.com/css?family=Open+Sans:400,700' rel='stylesheet' type='text/css'>
  <link href='https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css' rel='stylesheet' type='text/css'>
</head>
<body class="container">
  <h1>A Simple Hello World Voting Application</h1>
  <div id="address"></div>
  <div class="table-responsive">
    <table class="table table-bordered">
      <thead>
        <tr>
          <th>Candidate</th>
          <th>Votes</th>
        </tr>
      </thead>
      <tbody>
        <tr>
          <td>Rama</td>
          <td id="candidate-1"></td>
        </tr>
        <tr>
          <td>Nick</td>
          <td id="candidate-2"></td>
        </tr>
        <tr>
          <td>Jose</td>
          <td id="candidate-3"></td>
        </tr>
      </tbody>
    </table>
    <div id="msg"></div>
  </div>
  <input type="text" id="candidate" />
  <a href="#" onclick="voteForCandidate()" class="btn btn-primary">Vote</a>
</body>
<script src="https://cdn.rawgit.com/ethereum/web3.js/develop/dist/web3.js"></script>
<script src="https://code.jquery.com/jquery-3.1.1.slim.min.js"></script>
<script src="app.js"></script>
</html>

修改 app/scripts/index.js 如下

// Import the page's CSS. Webpack will know what to do with it.
import "../styles/app.css";

// Import libraries we need.
import { default as Web3 } from 'web3';
import { default as contract } from 'truffle-contract'

import voting_artifacts from '../../build/contracts/Voting.json'

var Voting = contract(voting_artifacts);

let candidates = {"Rama": "candidate-1", "Nick": "candidate-2", "Jose": "candidate-3"}

window.voteForCandidate = function(candidate) {
 let candidateName = $("#candidate").val();
 try {
  $("#msg").html("Vote has been submitted. The vote count will increment as soon as the vote is recorded on the blockchain. Please wait.")
  $("#candidate").val("");

  Voting.deployed().then(function(contractInstance) {
   contractInstance.voteForCandidate(candidateName, {gas: 140000, from: web3.eth.accounts[0]}).then(function() {
    let div_id = candidates[candidateName];
    return contractInstance.totalVotesFor.call(candidateName).then(function(v) {
     $("#" + div_id).html(v.toString());
     $("#msg").html("");
    });
   });
  });
 } catch (err) {
  console.log(err);
 }
}

$( document ).ready(function() {
 if (typeof web3 !== 'undefined') {
  console.warn("Using web3 detected from external source like Metamask")
  // Use Mist/MetaMask's provider
  window.web3 = new Web3(web3.currentProvider);
 } else {
  console.warn("No web3 detected. Falling back to http://localhost:8545. You should remove this fallback when you deploy live, as it's inherently insecure. Consider switching to Metamask for development. More info here: http://truffleframework.com/tutorials/truffle-and-metamask");
  // fallback - use your fallback strategy (local node / hosted node + in-dapp id mgmt / fail)
  window.web3 = new Web3(new Web3.providers.HttpProvider("http://127.0.0.1:8545"));
 }

 Voting.setProvider(web3.currentProvider);
 let candidateNames = Object.keys(candidates);
 for (var i = 0; i < candidateNames.length; i++) {
  let name = candidateNames[i];
  Voting.deployed().then(function(contractInstance) {
   contractInstance.totalVotesFor.call(name).then(function(v) {
    $("#" + candidates[name]).html(v.toString());
   });
  })
 }
});

修改 Migration 腳本

migrations/2_deploy_contracts.js 修改如下

var Voting = artifacts.require("./Voting.sol");
module.exports = function(deployer) {
 deployer.deploy(Voting, ['Rama', 'Nick', 'Jose']);
};

更新 truffle 配置文件

truffle.js

require('babel-register')
module.exports = {
 networks: {
  development: {
   host: 'localhost',
   port: 8545,
   network_id: '*'
  }
 }
}

當以上的檔案都改好後,請執行 webpack 重新編譯。

合約編譯 & 部屬

使用 Truffle 中的 compile 指令來編譯合約

~/tfapp$ truffle compile
Compiling ./contracts/Voting.sol...
Writing artifacts to ./build/contracts

執行 migrate 指令來將編譯過後的合約部屬到鏈上

~/tfapp$ truffle migrate
Using network 'development'.

Running migration: 1_initial_migration.js
  Deploying Migrations...
  ... 0xcf5f46a3908ccbcde24d4b451a56a50e8ba853445049e919cf4c17cb94efe494
  Migrations: 0x49814b10dbfb863b167fb182323c5a66805630e0
Saving successful migration to network...
  ... 0x6457a27f9be92e4207cb9ba46e52318f5b7a864503189090472f39f945ad3657
Saving artifacts...
Running migration: 2_deploy_contracts.js
  Deploying Voting...
  ... 0x043473aeb633f02fc4293b78e33c82957ab4e38d411f8955755ca62d1b156983
  Voting: 0x591d6672962920606322a8897a2a91614dd4e60b
Saving successful migration to network...
  ... 0x04cf551c36e60926bfdae64935ade35942dfad44747beabac7d4936c6b9d81cb
Saving artifacts...

補充:

  1. 這邊要注意,必須有一個窗口正在執行 ganache,否則可能會部署失敗

  2. 如果 compile 總是出現 Warning: Defining constructors as functions with the same name as the contract is deprecated. Use "constructor(...) { ... }" instead.

那是因為新版的語言編譯規則改變了,只要將 migrations/1_initial_migration.js 中的

  function Migrations() public {
    owner = msg.sender;
  }

改為

  constructors() public {
    owner = msg.sender;
  }

就可以了。

使用 Truffle 控制台

輸入 truffle console 進入控制台,準備來和我們剛剛部屬的合約進行交互。

~/tfapp$ truffle console

truffle(development)> Voting.deployed().then(function(contractInstance) {contractInstance.voteForCandidate('Rama').then(function(v) {console.log(v)})})

truffle(development)> { tx:
   '0x9a40357643b6bd0dcc91653d4bedadb2ee755d0f33f616bcdcfced7e5e8a4363',
  receipt:
   { transactionHash:
      '0x9a40357643b6bd0dcc91653d4bedadb2ee755d0f33f616bcdcfced7e5e8a4363',
     transactionIndex: 0,
     blockHash:
      '0x823af2482bf2d2216b11af76d7eddcabfb8a61958eb359aef8ee364271381b61',
     blockNumber: 15,
     ...
     ...
     # 太長以下省略
     
truffle(default)> Voting.deployed().then(function(contractInstance) {contractInstance.totalVotesFor.call('Rama').then(function(v) {console.log(v)})})
{ [String: '1'] s: 1, e: 0, c: [ 1] }

# 可以看到 c:[ 1] 的部分已經變成票數 1 了,如果重複執行上面的指令,可以看到票數會變為 2

網頁交互

這時候只要執行 npm run dev 後,在瀏覽器中打開 http://localhost:8080/

應該就可以看到一個簡單的 Voting DAPP 了。

在 Input 中輸入 Candidate 的名字進行投票試試吧

這邊值得注意的是,如果看不到票數,表示很可能 Web3 沒有連線到你的 localhost 仿真節點器,又或是你沒有將他打開。

也有一種可能是,你的 MetaMask 插件導致 provider 導向了正式的主網路。

MataMask 裡面可以選擇節點網路來自於 localhost:8545,選擇後重新整理就可以了。

那麼,MetaMask 裡面沒有足夠的 ETH 能夠投票,怎麼辦?

還記得一開始使用的 ganache 嗎?每一組帳號都有對應一組 private key,可以直接在 MetaMask 中點選 MyAccounts -> Import Account -> Select Type 選擇 Private Key -> 將測試用的 Key 導入,應該就可以看到 100 ETH 了。

參考資源

comments powered by Disqus