使用Truffle開發以太坊投票DAPP
投票最擔心的是暗箱操作、利用區塊鏈的去中心化技術,來實現一個DAPP保證投票公平公正,來看看如何實現,通過本文可以瞭解到對映 mapping
、結構體 struct
及事件 event
的使用。
投票需求分及實現效果
要實現一個投票DApp,對於合約來說有兩個基本需求:
- 每人(賬號)只能投一票;
- 記錄下一共有多少候選人
- 記錄每個候選人的得票數。
在介面上,需要看到每個候選人的得票數, 已經選擇投票人進行投票,來看看實現的效果圖:
投票合約實現
資料儲存
每人(賬號)只能投一票很容易實現,只需要使用一個 mapping
來記錄每個地址的投票資訊
定義一個 mapping 記錄投票記錄:
mapping(address => bool) public voters;
記錄候選人及得票數, 我們思考下,如何合約中表示一個候選人,這裡我們用一個結構體來表示候選人:
struct Candidate { uint id; string name;// 候選人的名字 uint voteCount; }
在Candidate結構體中,用 voteCount
表示得票數。我們還需要記錄下一共有多少個候選人,直覺是儲存到一個數組,前端需要候選人列表時,直接把這個陣列返回給前端。
基於EVM的限制,外部函式是沒法返回動態的內容,更多可閱讀Solidity陣列,所以這裡我們需要使用一個變通的方案。
用一個變數儲存一共有多少個候選人 uint public candidatesCount
,然後定義一個對映:
mapping(uint => Candidate) public candidates
通過id作為 key
訪問對映candidates來取候選人。
投票功能實現
接下來就是新增功能: 主要是兩個功能: 新增候選人及投票。
每新增一個候選人就加入到candidates對映中,同時候選人數量加1,新增候選人addCandidate函式實現為:
function addCandidate (string memory _name) private { candidatesCount ++; candidates[candidatesCount] = Candidate(candidatesCount, _name, 0); }
我們在合約建立的時候,就把候選人新增好,在建構函式中,呼叫addCandidate,建構函式實現如下:
constructor () public { addCandidate("Tiny 熊"); addCandidate("Big 熊"); }
投票就是在對應的候選人的voteCount加1,同時這個函式需要一個引數即給哪一個候選人投票,另外需要進行一些合法性檢查: 候選人是有效的,投票人必須沒有投過票,投票vote函式實現如下:
function vote (uint _candidateId) public { require(!voters[msg.sender]); require(_candidateId > 0 && _candidateId <= candidatesCount); voters[msg.sender] = true; candidates[_candidateId].voteCount ++; }
事件Event
為了有更好的前端體驗, 在使用者投票之後,應該及時的重新整理頁面, 這就需要用到事件了。
你還可以閱讀另外一篇文章: 詳解 Solidity 事件Event 瞭解更多事件知識。
先定義一個事件:
// voted event event votedEvent ( uint indexed _candidateId );
然後在投票vote函式中最後一行加入發出事件:
emit votedEvent(_candidateId);
合約部分程式碼編寫完了, 訂閱 小專欄 獲取完整原始碼。
合約部署
為合約Election,新增一個部署指令碼:
var Election = artifacts.require("./Election.sol"); module.exports = function(deployer) { deployer.deploy(Election); };
在部署之前,還需要開啟以太坊的模擬節點Ganache,並確保Truffle配置檔案truffle.js 連結節點的地址和埠與Ganache 一致。
Ganache 的安裝使用可閱讀開發、部署第一個DApp
如果要部署到以太坊正式網路可閱讀 用Truffle 開發一個鏈上記事本
然後執行一下命令進行部署:
truffle migrate
前端介面
有一個html table
標籤顯示候選人列表:
<table class="table"> <thead> <tr> <th scope="col">#</th> <th scope="col">候選人</th> <th scope="col">得票數</th> </tr> </thead> <tbody id="candidatesResults"> </tbody> </table>
candidatesResults的內容,需要使用web3.js從合約中讀取候選人資訊後動態填入。
使用 form
標籤執行投票操作:
<form onSubmit="App.castVote(); return false;"> <div class="form-group"> <label for="candidatesSelect">選擇候選人</label> <select class="form-control" id="candidatesSelect"> </select> </div> <button type="submit" class="btn btn-primary">投票</button> <hr /> </form>
合約互動
分三個部分:
- 初始化 web3 及合約
- 獲取候選人填充到前端頁面
- 使用者提交投票
初始化
在 initWeb3
函式中,完成web3的初始化
initWeb3: async function() { if (window.ethereum) { App.web3Provider = window.ethereum; try { await window.ethereum.enable(); } catch (error) { console.error("User denied account access") } } else if (window.web3) { App.web3Provider = window.web3.currentProvider; } else { App.web3Provider = new Web3.providers.HttpProvider('http://localhost:9545'); } web3 = new Web3(App.web3Provider); return App.initContract(); }
web3的初始化,呼叫App.initContract進行合約初始化:
initContract: function() { $.getJSON("Election.json", function(election) { App.contracts.Election = TruffleContract(election); App.contracts.Election.setProvider(App.web3Provider); App.listenForEvents(); return App.render(); }); }
監聽投票事件
listenForEvents: function() {App.contracts.Election.deployed().then(function(instance) { instance.votedEvent({}, { fromBlock: 0, toBlock: 'latest' }).watch(function(error, event) { App.render(); }); }); }
這裡有一份 web3.js 監聽事件 API文件說明
候選人介面渲染
render: function() { var electionInstance; App.contracts.Election.deployed().then(function(instance) { electionInstance = instance; return electionInstance.candidatesCount();// ❶ }).then(function(candidatesCount) { var candidatesResults = $("#candidatesResults"); candidatesResults.empty(); var candidatesSelect = $('#candidatesSelect'); candidatesSelect.empty(); for (var i = 1; i <= candidatesCount; i++) { electionInstance.candidates(i).then(function(candidate) {// ❷ var id = candidate[0]; var name = candidate[1]; var voteCount = candidate[2]; // Render candidate Result var candidateTemplate = "<tr><th>" + id + "</th><td>" + name + "</td><td>" + voteCount + "</td></tr>" candidatesResults.append(candidateTemplate); // ❸ // Render candidate ballot option var candidateOption = "<option value='" + id + "' >" + name + "</ option>" candidatesSelect.append(candidateOption);// ❹ }); } }
- ❶ 獲取候選人數量
- ❷ 依次獲取某一個候選人資訊
- ❸ 候選人資訊寫入候選人表格內
- ❹ 候選人資訊寫入投票選項
執行DApp
使用以下命令,啟動DApp 服務:
npm run dev
在瀏覽器開啟 http://localhost:3000
, 瀏覽器的MetaMask 小狐狸外掛需要連線到Ganache網路, 因為只有網路一致才可以讀取到網路上的合約資料。
如何設定MetaMask 可閱讀 開發、部署第一個去中心化應用 。
本文為保持主幹清晰,程式碼有刪減, 網站程式碼請訂閱 小專欄 檢視。
參考文件
加我微信:xlbxiong 備註:DApp, 加入以太坊DApp開發微信群。
加入知識星球 成長比別人快一點。
深入淺出區塊鏈 - 系統學習區塊鏈,學區塊鏈的都在這裡,打造最好的區塊鏈技術部落格。