投票DApp

1
2
3
4
5
6
7
8
9
#创建项目目录
mkdir voting
cd voting
#安装ganache,web3,solc
npm init
npm install ganache-cli web3@^0.20.0 solc
#开启ganache
node_modules/.bin/ganache-cli
#当然我们也可以安装可视化的Ganache,我比较推荐

项目目录如下:

1
2
3
4
5
Mode                LastWriteTime         Length Name
---- ------------- ------ ----
d----- 2020/5/20 17:06 node_modules
-a---- 2020/5/20 17:06 132029 package-lock.json
-a---- 2020/5/20 17:06 300 package.json

Solidity合约

Voting.sol

Voting合约包含以下内容:

  • 用于初始化候选者的构造函数
  • 用于投票的方法
  • 返回候选者获得票数的方法

代码如下,然后将代码保存在voting目录下:

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
pragma solidity >=0.4.22;

contract Voting{
bytes32[] public candidateList; //候选人数组
mapping(bytes32 => uint) public votesReceived;
constructor(bytes32[] candidateListName) public{
candidateList = candidateListName;
}

//检测是否为有效的投票地址
function validateCandidate(bytes32 candidateName) internal view returns(bool){
for(uint i = 0; i < candidateList.length; i++){
if (candidateName == candidateList[i])
return true;
}
return false;
}

//投票函数
function vote(bytes32 candidateName) public {
require(validateCandidate(candidateName));
votesReceived[candidateName] += 1;
}

//返回某个候选人的得票
function totalVotesFor(bytes32 candidateName) public view returns(uint){
require(validateCandidate(candidateName));
return votesReceived[candidateName];
}

}

终端输入命令查看安装版本:

1
2
npm list web3
npm list solc

编译合约

我们现在我们启动node控制台:

1
node

进行web3和solc编译:

1
2
3
4
#输入命令
node
> var Web3 = require('web3')
> var solc = require('solc')

连接我们的gananche,

1
2
3
4
#定义在node环境里的web3对象,这里的localhost的端口是ganache的地址,默认是8545,我是7545
> var web3 = new Web3(new Web3.providers.HttpProvider('http://localhost:7545'))
#我们可以使用web3.eth.accounts查看ganache生成的10个地址
> web3.isConnected()#这里返回的是false证明你没有开启ganache,开启的话是返回true

保存并查看我们的合约文件

1
2
3
> var sourceCode = fs.readFileSync('Voting.sol').toString()
#查看我们的合约文件
> sourceCode

现在我们可以对合约进行编译了

1
2
3
#我们现在有了源代码,就可以进行编译了
> var compileCode = solc.compile(sourceCode)
#我们可以输入compileCode查看编译后的信息,以供下边部署使用

部署合约

编译完成后进行合约的部署,我们需要ABI和字节码

1
2
3
4
5
6
#ABI
> var abi = compileCode.contracts[':Voting'].interface
#这里的abi是一段字符串,而我们需要的并不是字符串,因此需要转一下
> var abi = JSON.parse(abi)#也可以将两步合二为一
#需要的bytecode是字符串类型的,因此不需要类型转换,但是我们需要
> var byteCode = compileCode.contracts[':Voting'].bytecode

创建合约对象

1
> var VotingContract = web3.eth.contract(abi)

构建一个交易

1
> var deployTxObj = {data:byteCode,from:web3.eth.accounts[0],gas:3000000}

创建一个部署合约的交易

1
> var contractInstance = VotingContract.new(['Alice', 'Bob', 'Cary'],deployTxObj)

这时候我们看一眼ganache,发现这笔合约交易已经被部署到了本地区块链上。

我们输入contractInstance.address可以查看这个合约的地址

1
2
3
> contractInstance.address
#返回的信息与ganache显示的一致
'0x58cf20f3011ad77ef87d0d8b2a657c109b396290'

那我们现在就算是完成了合约的编译和部署的内容。

入股按照上面的步骤最终在DApp调试是报错,大家可以试试下面的步骤:

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
> var Web3 = require('web3')
undefined
> var web3 = new Web3(new Web3.providers.HttpProvider('http://localhost:7545'))
undefined
> var solc = require('solc');
undefined
> var fs = require('fs');
undefined
> var solPath = 'Voting.sol';
undefined
> var contractName = 'Voting';
undefined
> var contractSource = fs.readFileSync(solPath, 'utf-8');
undefined
> var contractObj = solc.compile(contractSource).contracts[':' + contractName];
undefined
> var abi = JSON.parse(contractObj.interface);
undefined
> var bytecode = contractObj.bytecode;
undefined
> var VotingContract = web3.eth.contract(abi)
undefined
> var deployTxObj = {data:bytecode, from:web3.eth.accounts[0], gas:3000000}
undefined
> var contractInstance = VotingContract.new(['Alice', 'Bob', 'Cary'], deployTxObj)
undefined

功能使用

调用vote方法进行投票

1
2
3
> contractInstance.vote.sendTransaction("Alice",{from:web3.eth.accounts[0]})
#这里会进行投票,vote就是合约的投票函数,我们需要告知谁投票以及投给谁,投票后返回交易的哈希,大家可以理解为交易的ID,我们也可以在ganache中看到这笔交易。
'0x602c247d07b95056b3ad97c328f36f39dce35a9d1bff7f7420b67acfeae9d5cf'

调用totalVotesFor方法查看当前投票数

1
2
3
4
5
6
> contractInstance.totalVotesFor.call("Alice")
#这里s代表符号,1为正,e代表10的几次幂,c表示真实有效数位,我们可以使用toString()查看的更清晰
BigNumber { s: 1, e: 0, c: [ 1 ] }
> contractInstance.totalVotesFor.call("Alice").toString()
#这里不需要加入from,因为这并不是一笔交易
'1'

这里注意:查询用call,交易用sendTransaction,但也可以不用,是因为这里会自动判断它是call还是sendTransaction,建议加上增强记忆

前端交互

如果前面的任务都顺利完成了,那么我们就可以开始做前端进行交互了。

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
<!DOCTYPE html>
<html>

<head>
<meta charset="utf-8">
<title>投票DApp</title>
<link href='https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css' rel='stylesheet' type='text/css'>
</head>

<body class="container">
<h1>简单区块链投票系统</h1>
<div class="table-responsive">
<table class="table table-bordered">
<!-- 表头 -->
<thead>
<tr>
<th>候选人</th>
<th>得票数</th>
</tr>
</thead>
<!-- 内容 -->
<tbody>
<tr>
<td>Alice</td>
<td id="candidate-1"></td>
</tr>
<tr>
<td>Bob</td>
<td id="candidate-2"></td>
</tr>
<tr>
<td>Cary</td>
<td id="candidate-3"></td>
</tr>
</tbody>
</table>


</div>
<input type="text" id="candidate" />
<a href="#" onclick="voteForCandidate()" class="btn btn-primary">投票</a>
</body>
<script src="https://cdn.jsdelivr.net/gh/ethereum/web3.js/dist/web3.min.js"></script>
<script src="https://libs.baidu.com/jquery/2.1.1/jquery.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
var web3 = new Web3(new Web3.providers.HttpProvider('http://localhost:7545'));

var abi = JSON.parse('[{"constant":true,"inputs":[{"name":"candidateName","type":"bytes32"}],"name":"totalVotesFor","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"","type":"bytes32"}],"name":"votesReceived","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"candidateName","type":"bytes32"}],"name":"vote","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"","type":"uint256"}],"name":"candidateList","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"inputs":[{"name":"candidateListName","type":"bytes32[]"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"}]');
var contractAddress = "0x44bbe5703662b6d4ff559f6b226b30ab7cb6538d";
var contractInstance = new web3.eth.Contract(abi, contractAddress);

//候选人和其ID对应关系
var candidates = { "Alice": "candidate-1", "Bob": "candidate-2", "Cary": "candidate-3" };
//候选人名数组
var candidateNames;
//渲染初始页面
$(document).ready(function() {
//获得所有的候选人名字keys
candidateNames = Object.keys(candidates);
//遍历去查询(call)当前的票数
for (let i = 0; i < candidateNames.length; i++) {
let name = candidateNames[i];
contractInstance.methods.totalVotesFor(web3.utils.toHex(name)).call((err, res) => {
if (err) console.log(err);
else {
//渲染到屏幕上
$("#" + candidates[name]).html(res.toString());
}
});
}
})

//开始投票
function voteForCandidate() {
//获取当前输入的内容
let testName = $("#candidate").val();

//判断输入
if (!isVaildName(testName)) {
alert("输入有效姓名!");
return;
}
//调用合约函数-投票
contractInstance.methods.vote(web3.utils.toHex(testName)).send({ from: '0x6109EE77fd98bE01A4dF09AB441da90c9870aD49' }, (err, res) => {
if (err) console.log(err);
else {
//!!!注意这里不能直接加1显示,而是必须要到区块链中查询
contractInstance.methods.totalVotesFor(web3.utils.toHex(testName)).call((err, res) => {
if (err) console.log(err);
else {
//渲染到屏幕上
$("#" + candidates[testName]).html(res.toString());
}
});
}
})
}


//检查输入
function isVaildName(name) {
for (let i = 0; i < candidateNames.length; i++) {
if (name == candidateNames[i]) {
return true;
}
}
return false;
}

问题

  1. 在remix中需要输入[]bytes32格式,必须要如下才行:
1
["0x7465737400000000000000000000000000000000000000000000000000000000","0x7465737400000000000000000000000000000000000000000000000000000001","0x7465737400000000000000000000000000000000000000000000000000000002"]
  1. 候选人的名称尽量使用英文,用中文应该会有意想不到的错误。