基于Token的投票DApp实现

Token相当于代币,它除了可以代表一份权益,还可以代表一种荣誉、积分,也可以转化为物权。本文对基于Token的投票DApp做一个实现。

与不使用Token的投票不同,基于Token的投票可以选择使用ETH购买Token,对候选者进行加权投票,因此投票合约会与之前基于Truffle的简单投票有所不同。基于Truffle的投票DApp:https://night-scholar.github.io/2020/12/12/%E5%9F%BA%E4%BA%8ETruffle%E7%9A%84%E6%8A%95%E7%A5%A8DApp%E5%AE%9E%E7%8E%B0/

创建项目

和之前一样,我们需要安装Truffle和创建项目。

安装Truffle

1
npm install -g truffle 

创建项目

1
2
3
4
mkdir Voting_By_Truffle_DApp
cd Voting_By_Truffle_DApp
npm install -g webpack
truffle unbox webpack

项目实现

Voting.sol

首先肯定和之前一样,删除contracts下除了Migration.sol以外的其他合约并新建Voting.sol,代码实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
// SPDX-License-Identifier: MIT
pragma solidity >=0.4.21 <0.7.0;

contract Voting {
struct voter {
address voterAddress;
uint256 tokenNum;
uint256[] tokensVoteForCandidates;
}
uint256 public totalTokens;
uint256 public tokenBalance;
uint256 public tokenPrice;
bytes32[] candidateList;
mapping(bytes32 => uint256) public votesReceived;
mapping(address => voter) public voterInfo;

constructor(
uint256 totalSupply,
uint256 price,
bytes32[] memory candidateNames
) public {
totalTokens = totalSupply;
tokenBalance = totalSupply;
tokenPrice = price;
candidateList = candidateNames;
}

function buy() public payable returns (uint256) {
uint256 tokensToBuy = msg.value / tokenPrice;
require(tokensToBuy <= tokenBalance);
voterInfo[msg.sender].voterAddress = msg.sender;
voterInfo[msg.sender].tokenNum += tokensToBuy;
tokenBalance -= tokensToBuy;
return tokensToBuy;
}

function voteForCandidate(bytes32 candidate, uint256 voteTokens) public {
uint256 index = indexOfCandidate(candidate);
require(index != uint256(-1));
if (voterInfo[msg.sender].tokensVoteForCandidates.length == 0) {
for (uint256 i = 0; i < candidateList.length; i++) {
voterInfo[msg.sender].tokensVoteForCandidates.push(0);
}
}
uint256 availableTokens = voterInfo[msg.sender].tokenNum -
totalUsedTokens(voterInfo[msg.sender].tokensVoteForCandidates);
require(availableTokens >= voteTokens);
votesReceived[candidate] += voteTokens;
voterInfo[msg.sender].tokensVoteForCandidates[index] += voteTokens;
}

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

function totalUsedTokens(uint256[] memory votesForCandidate)
public
pure
returns (uint256)
{
uint256 voterTotalTokens = 0;
for (uint256 i = 0; i < votesForCandidate.length; i++) {
voterTotalTokens += votesForCandidate[i];
}
return uint256(voterTotalTokens);
}

function indexOfCandidate(bytes32 candidate) public view returns (uint256) {
for (uint256 i = 0; i < candidateList.length; i++) {
if (candidate == candidateList[i]) {
return i;
}
}
return uint256(-1);
}

function tokenSold() public view returns (uint256) {
return totalTokens - tokenBalance;
}

function voterDetails(address voterAddr)
public
view
returns (uint256, uint256[] memory)
{
return (
voterInfo[voterAddr].tokenNum,
voterInfo[voterAddr].tokensVoteForCandidates
);
}

function allCandidate() public view returns (bytes32[] memory) {
return candidateList;
}

function transfer(address payable _to) public {
_to.transfer(address(this).balance);
}
}

2_deploy_contracts.js

修改migrations下的2_deploy_contracts.js文件,如下:

1
2
3
4
5
var Voting = artifacts.require('./Voting.sol')

module.exports = function(deployer) {
deployer.deploy(Voting, 10000, web3.utils.toWei(0.01.toString(), 'ether'), ['Alice', 'Bob', 'Cary'].map(x => web3.utils.asciiToHex(x)), { gas: 2000000 })
}

10000, web3.utils.toWei(0.01.toString(), ‘ether’), [‘Alice’, ‘Bob’, ‘Cary’].map(x => web3.utils.asciiToHex(x))分别对应合约构造函数下的各个变量。

合约测试

编译合约

1
truffle compile

部署合约

这一步注意修改truffle.config.js下的development,端口号要与geth、ganache或者truffle开启的区块链端口保持一致。

1
truffle migrate

部署完毕后,看一下是否生成了块,如果生成了则部署成功,geth需要挖矿才能出块。

进入控制台

1
truffle console

购买Token

1
Voting.deployed().then(inst => inst.buy({value:web3.utils.toWei((10).toString(),'ether')}).then(res=>console.log(res)))

查看购买的Token数量

1
2
addresses = await web3.eth.getAccounts()
Voting.deployed().then(inst => inst.voterDetails(addresses[0]).then(res=>console.log(res[0].toString())))

查看出售了的Token数量

1
Voting.deployed().then(inst => inst.tokenSold().then(res=>console.log(res.toString())))

查看剩余可出售的Token数量

1
Voting.deployed().then(inst => inst.tokenBalance().then(res=>console.log(res.toString())))

查看各候选者名称

1
2
3
Voting.deployed().then(inst => inst.allCandidate().then(res=>console.log(web3.utils.hexToUtf8(res[0]))))
Voting.deployed().then(inst => inst.allCandidate().then(res=>console.log(web3.utils.hexToUtf8(res[1]))))
Voting.deployed().then(inst => inst.allCandidate().then(res=>console.log(web3.utils.hexToUtf8(res[2]))))

使用Token投票

1
Voting.deployed().then(inst => inst.voteForCandidate(web3.utils.asciiToHex("Alice"),10).then(res=>console.log(res)))

查看Alice得票数

1
Voting.deployed().then(inst=>inst.totalVotesFor(web3.utils.asciiToHex("Alice")).then(res=>console.log(res.toString())))

无法直接查询剩余票数,要根据

1
2
3
addresses = await web3.eth.getAccounts()
Voting.deployed().then(inst => inst.voterDetails(addresses[0]).then(res=>console.log(res[0].toString())))
>1000 #购买的总票数

1
2
3
4
5
6
Voting.deployed().then(inst => inst.voterDetails(addresses[0]).then(res=>console.log(res[1][0].toString())))
>10 #投Alice的10票
Voting.deployed().then(inst => inst.voterDetails(addresses[0]).then(res=>console.log(res[1][1].toString())))
>0
Voting.deployed().then(inst => inst.voterDetails(addresses[0]).then(res=>console.log(res[1][2].toString())))
>0

得出的结果相减

Index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
<!DOCTYPE html>
<html>

<head>
<title>Token投票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'>
<style>

</style>
</head>

<body class="row">
<h1 class="text-center banner">Token投票DApp</h1>
<div class="container">
<div class="row margin-top-3">
<div class="col-sm-12">
<h3>使用教程</h3>
<strong>第一步</strong>: 安装metamask插件并导入账户。
<br><br>
<strong>第二步</strong>: 购买Token
<br><br>
<strong>第三步</strong>: 进行投票
<br><br>
<strong>第四步</strong>: 根据自己账户地址查看给谁投票了
<br><br>
</div>
</div>
<div class="row margin-top-3">
<div class="col-sm-7">
<h2>投票情况</h2>
<div class="table-responsive">
<table class="table table-bordered">
<thead>
<tr>
<th>候选人</th>
<th>投票数</th>
</tr>
</thead>
<tbody id="candidate-rows">
</tbody>
</table>
</div>
</div>
<div class="col-sm-offset-1 col-sm-4">
<h2>Token代币</h2>
<div class="table-responsive">
<table class="table table-bordered">
<tr>
<th>代币信息</th>
<th>详情</th>
</tr>
<tr>
<td>总代币(包括已经出售的)</td>
<td id="tokens-total"></td>
</tr>
<tr>
<td>代币已售</td>
<td id="tokens-sold"></td>
</tr>
<tr>
<td>代币价格</td>
<td id="tokens-cost"></td>
</tr>
<tr>
<td>合同余额</td>
<td id="contract-balance"></td>
</tr>
</table>
</div>
</div>
</div>
<hr>
<div class="row margin-bottom-3">
<div class="col-sm-7 form">
<h2>投票</h2>
<div id="msg"></div>
<input type="text" id="candidate" class="form-control" placeholder="请输入候选人姓名" />
<br>
<br>
<input type="text" id="vote-tokens" class="form-control" placeholder="请输入投票数" />
<br>
<br>
<a href="#" onclick="voteForCandidate(); return false;" class="btn btn-primary">投票</a>
</div>
<div class="col-sm-offset-1 col-sm-4">
<div class="col-sm-12 form">
<h2>购买Token代币</h2>
<div id="buy-msg"></div>
<input type="text" id="buy" class="col-sm-8" placeholder="请输入需要购买的代币数量" />
<a href="#" onclick="buyTokens(); return false;" class="btn btn-primary">购买</a>
</div>
<div class="col-sm-12 margin-top-3 form">
<h2 class="">查看自身详情</h2>
<input type="text" id="voter-info" , class="col-sm-8" placeholder="请输入你的地址" />
<a href="#" onclick="lookupVoterInfo(); return false;" class="btn btn-primary">查询</a>
<div class="voter-details row text-left">
<div id="tokens-bought" class="margin-top-3 col-md-12"></div>
<div id="votes-cast" class="col-md-12"></div>
</div>
</div>
</div>
</div>
</div>
</body>
<script src="https://code.jquery.com/jquery-3.1.1.slim.min.js"></script>
<script src="index.js"></script>

</html>

Index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
import { default as Web3 } from 'web3';
import { default as contract } from 'truffle-contract';
import voting_artifacts from '../../build/contracts/Voting.json';

const Voting = contract(voting_artifacts);
//设置为空,需要从合约中拿出来的值进行赋值
let candidates = {};
let tokenPrice = null;

$(document).ready(function() {
var web3Provider;
if (window.ethereum) {
web3Provider = window.ethereum;
try {
// 请求用户授权
window.ethereum.enable();
} catch (error) {
// 用户不授权时
console.error("User denied account access")
}
} else if (window.web3) { // 老版 MetaMask Legacy dapp browsers...
web3Provider = window.web3.currentProvider;
} else {
web3Provider = new Web3.providers.HttpProvider('http://localhost:7545');
}
web3 = new Web3(web3Provider);
Voting.setProvider(web3.currentProvider);
populateCandidates();


})

window.buyTokens = async function() {
let accAddrs = await web3.eth.getAccounts()
let tokensToBuy = $('#buy').val();
let _value = tokensToBuy * tokenPrice;
Voting.deployed().then(VotingInst => {
VotingInst.buy({ value: web3.utils.toWei(_value.toString(), 'ether'), from: accAddrs[0] }).then(() => {
VotingInst.tokenSold().then(amount => {
$("#tokens-sold").html(amount.toString());
});
web3.eth.getBalance(VotingInst.address).then(balance => {
$("#contract-balance").html(web3.utils.fromWei(balance, 'ether').toString() + "ETH");
});
});
});
}

window.lookupVoterInfo = function() {
let _address = $("#voter-info").val();
Voting.deployed().then(VotingInst => {
VotingInst.voterDetails(_address).then(res => {
$("#tokens-bought").html("购买代币总数" + " : " + res[0].toString());
let candidateNames = Object.keys(candidates);
$("#votes-cast").empty();
$("#votes-cast").append("投票详情 : <br>");

for (let i = 0; i < candidateNames.length; i++) {
$("#votes-cast").append(candidateNames[i] + ":" + res[1][i].toString() + "<br>");
}
});
});
}


window.voteForCandidate = async function() {
let candidateName = $("#candidate").val();
let voteTokens = $("#vote-tokens").val();
let accAddrs = await web3.eth.getAccounts()
$("candidate").val("");
$("vote-tokens").val("");
Voting.deployed().then(VotingInst => {
VotingInst.voteForCandidate(web3.utils.asciiToHex(candidateName), voteTokens, { gas: 140000, from: accAddrs[0] }).then(() => {
VotingInst.totalVotesFor(web3.utils.asciiToHex(candidateName)).then(count => {
$("#" + candidates[candidateName]).html(count.toString());
});
});
});
}

function populateCandidates() {
Voting.deployed().then(VotingInst => {
VotingInst.allCandidate().then(candidateArray => {
for (let i = 0; i < candidateArray.length; i++) {
let candidateName = web3.utils.hexToUtf8(candidateArray[i]);
console.log(candidateName);
candidates[candidateName] = 'candidate-' + i;
}
setupCandidateRows();
populateCandidateVotes();
populateTokenData();
});
});
}

function setupCandidateRows() {
Object.keys(candidates).forEach(candidate => {
$("#candidate-rows").append("<tr><td>" + candidate + "</td><td id ='" + candidates[candidate] + "'></td></tr>");
});
}

function populateCandidateVotes() {
let candidateNames = Object.keys(candidates);
for (let i = 0; i < candidateNames.length; i++) {
Voting.deployed().then(VotingInst => {
VotingInst.totalVotesFor(web3.utils.asciiToHex(candidateNames[i])).then(count => {
$("#" + candidates[candidateNames[i]]).html(count.toString());
});
});
}
}

function populateTokenData() {
Voting.deployed().then(VotingInst => {
VotingInst.totalTokens().then(amount => {
$("#tokens-total").html(amount.toString());
});
VotingInst.tokenSold().then(amount => {
$("#tokens-sold").html(amount.toString());
});
VotingInst.tokenPrice().then(price => {
tokenPrice = web3.utils.fromWei(price.toString(), 'ether');
$("#tokens-cost").html(web3.utils.fromWei(price, 'ether').toString() + "ETH");
});
web3.eth.getBalance(VotingInst.address).then(balance => {
$("#contract-balance").html(web3.utils.fromWei(balance, 'ether').toString() + "ETH");
})
});
}

到这里项目就制作完成了,进入app目录下npm run dev,运行没问题后则投票DApp制作完成。

问题总结:

  1. web3的1.0版本以前使用web3.eth.accounts查看地址,而现在要使用web3.utils.getAccounts()查看地址。
  2. 在web3中使用web3.utils.getAccounts()要异步await,同时所在的函数要使用async保证同步。
  3. 本文中大量使用了web3.utils,这都是1.0版本更新后导致的。
  4. 新版的metamask和老版的授权方式不同,新版是window.ethereum。老版是window.web3,要注意。
  5. 候选者姓名存储到链上后是以bytes32格式存储的,我们前端显示的时候要使用web3.utils.hexToUtf8。
  6. 候选者姓名在存储到链上时也要使用bytes32格式,我们对字符串类型要使用web3.utils.asciiToHex。
  7. 在运行项目是输入npm run dev报错,如果报错Can’t resolve ‘truffle-contract’则需要 npm install –save truffle-contract。