全栈Voting Dapp Demo

本篇其实算是一篇译文,但不是直译,而是神译。(意会一下就ok了。。)
原文地址

上一篇详细介绍了官方Voting例子,
本篇趁热打铁,介绍下这样的一个Voting在实际中怎么使用。

本篇所要介绍的是一个简单的web应用,首先初始化一批候选人,然后所有登录页面的人都可以为这些候选人投票,并展示每个候选人的得票数。

先来看下整个Application的架构图:
Voting App

部署开发环境

开发都离不开测试,更离不开测试环境,尤其是区块链相关的开发,如果直接在公网上测试,那损失的可真的是真金白银。
所以我们先搞个开发环境,这个测试环境比较简单,也容易安装。它就是ganache,是一个基于内存的区块链测试工具。

ganache是nodejs的一个模块,安装命令为cnpm install ganache-cli web3@0.20.2

这里使用cnpm,是安装了淘宝的nodejs源,访问国外的太慢

执行node_modules/.bin/ganache-cli开启测试区块链,此命令为默认生成10测试账号,每个账号有100ETH,如下:
ganache-cli

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
pragma solidity ^0.4.25;
// We have to specify what version of compiler this code will compile with
contract Voting {
/* mapping field below is equivalent to an associative array or hash.
The key of the mapping is candidate name stored as type bytes32 and value is
an unsigned integer to store the vote count
*/
mapping (bytes32 => uint8) public votesReceived;
/* Solidity doesn't let you pass in an array of strings in the constructor (yet).
We will use an array of bytes32 instead to store the list of candidates
*/
bytes32[] public candidateList;
/* This is the constructor which will be called once when you
deploy the contract to the blockchain. When we deploy the contract,
we will pass an array of candidates who will be contesting in the election
*/
constructor(bytes32[] candidateNames) public {
candidateList = candidateNames;
}
// This function returns the total votes a candidate has received so far
function totalVotesFor(bytes32 candidate) view public returns (uint8) {
require(validCandidate(candidate));
return votesReceived[candidate];
}
// This function increments the vote count for the specified candidate. This
// is equivalent to casting a vote
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;
}
}

把上面的代码写入Voting.sol文件,然后编译合约并部署到ganache blockchain中。

命令行部署调用合约

这里是在node命令行中编译合约代码,所以需要安装solc依赖包,命令cnpm install solc

首先在node命令行中初始化web3对象,代码如下:

1
2
3
4
5
6
7
hadoop@ubuntu:~/blockchain/ethereum/project/voting-app$ node
> Web3 = require('web3')
{ [Function: Web3]
providers:
{ HttpProvider: [Function: HttpProvider],
IpcProvider: [Function: IpcProvider] } }
> web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545"));

web3对象初始化之后,就可以与ganache blockchain的测试环境进行交互,执行以下命令check下是否成功:

1
2
3
4
5
6
7
8
9
10
11
> web3.eth.accounts
[ '0xc8d9d73906b06412f3768c5e09efe04704ac5f49',
'0x6b0a0169df2e5e5c576ae92cbe1a173a12662e97',
'0x0aef426a8dfc5627a6a16cd2118e2b10fdba9644',
'0x11dc712466b7dd60af4ccc82bcb273853338337b',
'0xc2c9f86b94d6e64dd60e8c1cf8f87355b7a05eb1',
'0xf851f0b2a52024ceedee6d33d9331e6f628c4525',
'0x06b0ab24104065384026f938a627c5183d477a47',
'0xffcc3f6b4747e922b61fb49d9e1608a28c0523fd',
'0x3847f4b5bc251da9bfc7050e51a8e33cae78080a',
'0x6c67bb1596e56cc5054296e4bf05a6e42d4fba04' ]

成功之后,开始编译合约。调用fs方法从Voting.sol文件中读取代码并编译。

1
2
3
> code = fs.readFileSync('Voting.sol').toString()
> solc = require('solc')
> compiledCode = solc.compile(code)

上述代码运行成功之后,Voting合约就算编译成功了。其中有两个值比较重要,一个是btyecode另一个是ABI。
通过compiledCode.contracts[':Voting'].bytecode得到合约的bytecode,此值是合约编译之后的二进制码,将会部署到区块链中。
通过compiledCode.contracts[':Voting'].interface得到合约的ABI,此值是在合约调用中起作用,用来告诉合约用户哪些方法可以使用。

下面我们开始部署吧。
第一步 创建合约对象,代码如下:

1
2
3
4
5
6
> abiDefinition = JSON.parse(compiledCode.contracts[':Voting'].interface)
> VotingContract = web3.eth.contract(abiDefinition)
> byteCode = compiledCode.contracts[':Voting'].bytecode
> deployedContract = VotingContract.new(['Rama','Nick','Jose'],{data: byteCode, from: web3.eth.accounts[0], gas: 4700000})
> deployedContract.address
> contractInstance = VotingContract.at(deployedContract.address)

部署合约到区块链的关键代码是VotingContract.new,但是在部署合约之前,要先声明一个合约对象web3.eth.contract(abiDefinition),传入的参数是ABI。
VotingContract.new的第一个参数是合约构造函数传入的候选者数组,
第二个函数是一个json,json中data是合约编译之后的二进制码,from代表的是合约是又谁部署的,gas就代表要消耗多少钱。

执行完上述代码,就代表着我们已经部署了一个合约实例,并且我们可以与合约进行交互了。
交互时是使用deployedContract.address进行区分,执行以下命令,感受下与合约的交互。

1
2
3
4
5
6
7
8
9
10
> contractInstance.totalVotesFor.call('Rama')
BigNumber { s: 1, e: 0, c: [ 0 ] }
> contractInstance.voteForCandidate('Rama', {from: web3.eth.accounts[0]})
'0x718a9b2bfdb301a838ab39d8926e5881ff6c0da775eb933f944203c3353b1cfa'
> contractInstance.voteForCandidate('Rama', {from: web3.eth.accounts[0]})
'0x6c2e893552f316a4d37046baa2d030751db21bf7580559df5c051b10e71c0248'
> contractInstance.voteForCandidate('Rama', {from: web3.eth.accounts[0]})
'0xf211d536961f58fcca70d4b199bb814debdb445c3fff255eefb828008b0d9aa5'
> contractInstance.totalVotesFor.call('Rama').toLocaleString()
'3'

我们给Rama投了3张票,可以看到Rama的票数确实增加了,而且每次投票都会返回一个字符串,这个字符串是transaction id。

web操作合约

现在我们写个简单的html来把整个与合约交互的流程可视化。页面包括候选者的名字和票数,以及投票功能。
这里有一个index.html和一个index.js文件,代码分别如下:

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
<!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 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>
<input type="text" id="candidate" />
<a href="#" onclick="voteForCandidate()" class="btn btn-primary">Vote</a>
</body>
<!-- 国内这源可能访问不了,换成本地的web3.js文件 -->
<!-- <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="./node_modules/web3/dist/web3.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
Web3 = require('web3');
web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545"));
abi = JSON.parse('[{"constant":true,"inputs":[{"name":"candidate","type":"bytes32"}],"name":"totalVotesFor","outputs":[{"name":"","type":"uint8"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"candidate","type":"bytes32"}],"name":"validCandidate","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"","type":"bytes32"}],"name":"votesReceived","outputs":[{"name":"","type":"uint8"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"","type":"uint256"}],"name":"candidateList","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"candidate","type":"bytes32"}],"name":"voteForCandidate","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"candidateNames","type":"bytes32[]"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"}]')
VotingContract = web3.eth.contract(abi);
// In your nodejs console, execute contractInstance.address to get the address at which the contract is deployed and change the line below to use your deployed address
contractInstance = VotingContract.at('0xd018d4ed72e54fc3d66f254dfead8965577403dd');
candidates = {"Rama": "candidate-1", "Nick": "candidate-2", "Jose": "candidate-3"}
var account;
function voteForCandidate() {
candidateName = $("#candidate").val();
contractInstance.voteForCandidate(candidateName, {from: account}, function() {
let div_id = candidates[candidateName];
$("#" + div_id).html(contractInstance.totalVotesFor.call(candidateName).toString());
});
}
$(document).ready(function() {
web3.eth.getAccounts(function (err, accs) {
if (err != null) {
alert('There was an error fetching your accounts.')
return
}
if (accs.length === 0) {
alert("Couldn't get any accounts! Make sure your Ethereum client is configured correctly.")
return
}
account = accs[0]
})
candidateNames = Object.keys(candidates);
for (var i = 0; i < candidateNames.length; i++) {
let name = candidateNames[i];
let val = contractInstance.totalVotesFor.call(name).toString()
$("#" + candidates[name]).html(val);
}
});

这里注意下index.js文件,这里展示了如何使用ABI和address与合约进行交互。

代码搞好之后,安装一个web server,这里安装http-server,命令cnpm install -g http-server
安装成功之后,在项目目录下运行http-server启动程序,输出如下:

1
2
3
4
5
Starting up http-server, serving ./
Available on:
http://127.0.0.1:8080
http://192.168.234.138:8080
Hit CTRL-C to stop the server

访问上述的网址,可见如下页面:
web页面
在文本框中输入候选人的名字,然后点击Vote提交,就可以在看见对应候选人的票数加1。

总结

一个完整的Dapp Demo结束了,是不是感觉也没有想象中的神秘,但我想说这只是上层的使用,真正神秘的东西是底层的技术支撑,慢慢探秘吧。

您的肯定,是我装逼的最大的动力!