這篇文會建構一個去中心化的 (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...
補充:
這邊要注意,必須有一個窗口正在執行 ganache,否則可能會部署失敗
如果
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 了。