以太坊dApp開發教程(如何一步步構造一個全棧式去中心化應用)(四)實現投票功能
阿新 • • 發佈:2019-01-11
一、更新智慧合約
增加投票功能後的智慧合約如下:
pragma solidity ^0.4.2; contract Election { //候選者結構體 struct Candidate { uint id; string name; uint voteCount; } //候選者id到結構體的對映 mapping(uint => Candidate) public candidates; //投票者地址到是否投票的對映 mapping(address => bool) public voters; //總共多少候選者 uint public candidatesCount; //建構函式 constructor() public { addCandidate("Candidate 1"); addCandidate("Candidate 2"); } //新增候選者 function addCandidate(string _name) private { candidatesCount ++; candidates[candidatesCount] = Candidate(candidatesCount,_name,0); } //投票函式 function vote(uint _candidateId) public { //要求投票者從沒投過票 require(!voters[msg.sender]); //msg.sender是呼叫這個函式的賬戶 //要求候選的Id合法 require(_candidateId > 0 &&_candidateId <= candidatesCount); //確定投票 voters[msg.sender] = true; //更新候選者票數 candidates[_candidateId].voteCount ++; } }
更新對應的測試檔案test.js:
var Election = artifacts.require("./Election.sol"); contract("Election", function(accounts) { var electionInstance; //初始化有兩個候選者 it("initializes with two candidates", function() { return Election.deployed().then(function(instance) { return instance.candidatesCount(); }).then(function(count) { assert.equal(count, 2); }); }); //候選者初始化資訊是否正確 it("it initializes the candidates with the correct values", function() { return Election.deployed().then(function(instance) { electionInstance = instance; return electionInstance.candidates(1); }).then(function(candidate) { assert.equal(candidate[0], 1, "contains the correct id"); assert.equal(candidate[1], "Candidate 1", "contains the correct name"); assert.equal(candidate[2], 0, "contains the correct votes count"); return electionInstance.candidates(2); }).then(function(candidate) { assert.equal(candidate[0], 2, "contains the correct id"); assert.equal(candidate[1], "Candidate 2", "contains the correct name"); assert.equal(candidate[2], 0, "contains the correct votes count"); }) }); //測試是否允許投票者進行投票 it("allows a voter to cast a vote", function() { return Election.deployed().then(function(instance) { electionInstance = instance; candidateId = 1; return electionInstance.vote(candidateId, { from: accounts[0] }); }).then(function(receipt) { return electionInstance.voters(accounts[0]); }).then(function(voted) { assert(voted, "the voter was marked as voted"); return electionInstance.candidates(candidateId); }).then(function(candidate) { var voteCount = candidate[2]; assert.equal(voteCount, 1, "increments the candidate's vote count"); }) }); //測試對於非合法候選者進行投票 it("throws an exception for invalid candidates", function() { return Election.deployed().then(function(instance) { electionInstance = instance; return electionInstance.vote(99, { from: accounts[1] }) }).then(assert.fail).catch(function(error) { assert(error.message.indexOf('revert') >= 0, "error message must contain revert"); return electionInstance.candidates(1); }).then(function(candidate1) { var voteCount = candidate1[2]; assert.equal(voteCount, 1, "candidate 1 did not receive any votes"); return electionInstance.candidates(2); }).then(function(candidate2) { var voteCount = candidate2[2]; assert.equal(voteCount, 0, "candidate 2 did not receive any votes"); }); }); //測試能否重複投票 it("throws an exception for double voting", function() { return Election.deployed().then(function(instance) { electionInstance = instance; candidateId = 2; electionInstance.vote(candidateId, { from: accounts[1] }); return electionInstance.candidates(candidateId); }).then(function(candidate) { var voteCount = candidate[2]; assert.equal(voteCount, 1, "accepts first vote"); // Try to vote again return electionInstance.vote(candidateId, { from: accounts[1] }); }).then(assert.fail).catch(function(error) { assert(error.message.indexOf('revert') >= 0, "error message must contain revert"); return electionInstance.candidates(1); }).then(function(candidate1) { var voteCount = candidate1[2]; assert.equal(voteCount, 1, "candidate 1 did not receive any votes"); return electionInstance.candidates(2); }).then(function(candidate2) { var voteCount = candidate2[2]; assert.equal(voteCount, 1, "candidate 2 did not receive any votes"); }); }); });
二、重新部署合約並執行測試
三、修改客戶端檔案
我們先修改index.html,增加一個投票輸入框和投票按鈕,程式碼如下:
<form onSubmit="App.castVote(); return false;"> <div class="form-group"> <label for="candidatesSelect">Select Candidate</label> <select class="form-control" id="candidatesSelect"> </select> </div> <button type="submit" class="btn btn-primary">Vote</button> <hr /> </form>
- 我們建立了一個擁有空白選擇框的表單,投票者的票選將在"app.js"中進行計算。
- 表單有一個"onSubmit"事件處理器,會呼叫app.js中的"castVote"函式。
現在更新app.js檔案,首先把智慧合約中的候選者列在前臺的選擇框中,然後一旦投票則隱藏投票表單,render函式修改如下:
render: function() {
var electionInstance;
var loader = $("#loader");
var content = $("#content");
loader.show();
content.hide();
// Load account data
web3.eth.getCoinbase(function(err, account) {
if (err === null) {
App.account = account;
$("#accountAddress").html("Your Account: " + account);
}
});
// Load contract data
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);
});
}
return electionInstance.voters(App.account);
}).then(function(hasVoted) {
// Do not allow a user to vote
if(hasVoted) {
$('form').hide();
}
loader.hide();
content.show();
}).catch(function(error) {
console.warn(error);
});
}
我們還在app.js中增加一個響應表單提交事件的函式,程式碼如下:
castVote: function() {
var candidateId = $('#candidatesSelect').val();
App.contracts.Election.deployed().then(function(instance) {
return instance.vote(candidateId, { from: App.account });
}).then(function(result) {
// Wait for votes to update
$("#content").hide();
$("#loader").show();
}).catch(function(err) {
console.error(err);
});
}
我們首先獲取使用者選擇的candidateId,然後傳id和當前賬戶地址來呼叫vote方法,這是一個非同步呼叫,完成後,我們顯示loader,隱藏content。
最終的完整的 index.html和app.js如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags -->
<title>Election Results</title>
<!-- Bootstrap -->
<link href="css/bootstrap.min.css" rel="stylesheet">
<!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/html5shiv/3.7.3/html5shiv.min.js"></script>
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
<![endif]-->
</head>
<body>
<div class="container" style="width: 650px;">
<div class="row">
<div class="col-lg-12">
<h1 class="text-center">Election Results</h1>
<hr/>
<br/>
<div id="loader">
<p class="text-center">Loading...</p>
</div>
<div id="content" style="display: none;">
<table class="table">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Name</th>
<th scope="col">Votes</th>
</tr>
</thead>
<tbody id="candidatesResults">
</tbody>
</table>
<hr/>
<form onSubmit="App.castVote(); return false;">
<div class="form-group">
<label for="candidatesSelect">Select Candidate</label>
<select class="form-control" id="candidatesSelect">
</select>
</div>
<button type="submit" class="btn btn-primary">Vote</button>
<hr />
</form>
<p id="accountAddress" class="text-center"></p>
</div>
</div>
</div>
</div>
<!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
<!-- Include all compiled plugins (below), or include individual files as needed -->
<script src="js/bootstrap.min.js"></script>
<script src="js/web3.min.js"></script>
<script src="js/truffle-contract.js"></script>
<script src="js/app.js"></script>
</body>
</html>
App = {
web3Provider: null,
contracts: {},
account: '0x0',
hasVoted: false,
init: function() {
return App.initWeb3();
},
initWeb3: function() {
if (typeof web3 !== 'undefined') {
// If a web3 instance is already provided by Meta Mask.
App.web3Provider = web3.currentProvider;
web3 = new Web3(web3.currentProvider);
} else {
// Specify default instance if no web3 instance provided
App.web3Provider = new Web3.providers.HttpProvider('http://localhost:7545');
web3 = new Web3(App.web3Provider);
}
return App.initContract();
},
initContract: function() {
$.getJSON("Election.json", function(election) {
// Instantiate a new truffle contract from the artifact
App.contracts.Election = TruffleContract(election);
// Connect provider to interact with contract
App.contracts.Election.setProvider(App.web3Provider);
return App.render();
});
},
render: function() {
var electionInstance;
var loader = $("#loader");
var content = $("#content");
loader.show();
content.hide();
// Load account data
web3.eth.getCoinbase(function(err, account) {
if (err === null) {
App.account = account;
$("#accountAddress").html("Your Account: " + account);
}
});
// Load contract data
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);
});
}
return electionInstance.voters(App.account);
}).then(function(hasVoted) {
// Do not allow a user to vote
if (hasVoted) {
$('form').hide();
}
loader.hide();
content.show();
}).catch(function(error) {
console.warn(error);
});
},
castVote: function() {
var candidateId = $('#candidatesSelect').val();
App.contracts.Election.deployed().then(function(instance) {
return instance.vote(candidateId, { from: App.account });
}).then(function(result) {
// Wait for votes to update
$("#content").hide();
$("#loader").show();
}).catch(function(err) {
console.error(err);
});
}
};
$(function() {
$(window).load(function() {
App.init();
});
});
四、執行客戶端
修改完成後,重新在git bash中輸入 npm run dev命令啟動客戶端,在瀏覽器中可以看到修改後的頁面。
如果投票,metamask會詢問是否確認交易,點confirm後交易成功,你會看到 loading的介面,重新整理後,你會發現同一個賬戶不能再投票了。
至此,我們可以實現投票功能,並且不能重複投票