2

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

 2 years ago
source link: https://blog.niclin.tw/2018/08/12/%E7%94%A8-truffle-%E9%96%8B%E7%99%BC-dapp-%E4%BB%A5%E5%A4%AA%E5%9D%8A%E6%8A%95%E7%A5%A8%E7%A8%8B%E5%BA%8F%E6%87%89%E7%94%A8-part-1/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

這篇文會建構一個去中心化的 (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 了。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK