八、用区块链投票
在该系列文章的最开始部分我们说过,区块链是一种去中心化的数据库。数据库大家都理解,就是用来存数据,而基本上不需要关注存的是什么。即然如此,它的作用就不仅仅是发行代币这一种用法。这次,我们就来做一个用区块链投票的应用。我们会编写一个智能合约,其中包含一个 mapping 对象,其 key 是候选人代码,value 是我们自己定义的候选人对象,内部会包含候选人姓名、所得票数等。同时要记录已经投过票的人,保证其不能重复投票。最后,该应用可以列表展示每位候选人获得的票数,接下来让我们来看一下这个 DAPP。
注:该案例来源于幕课网的谦益,因为在原文章中有些内容做了一些省略,所以此处除了做重现,也将这部分内容一并补全。
先放一张图片
1、开发中要用到的工具
- node # Truffle 及 Ganache-cli 将运行在 node 之上
- npm # 开发中的项目构建,包管理及临时服务器等需要 npm 工具
- web3.js # 可以通过 JavaScript 调用智能合约的工具
- Truffle # 智能合约开发框架以及构建工具。提供了合约抽象接口,通过 web3.js 可在 JavaScript 中直接操作合约函数。其还对客户端做了深度集成,开发,测试,部署一行命令都可以搞定。
- Solidity # 智能合约语言
- Ganache-cli # 私链本地仿真工具,比 Geth 挖矿要快,是 etherumjs-testrpc 的官方替代版
其中 node 及 npm 在前面的章节中已经安装,此处不再重复。而 solidity 及 web3js 会在 truffle 中包含,无需单独安装,所以此处只需进一步全局安装 ganache-cil 和 truffle。
sudo npm install -g ganache-cli truffle # 安装
ganache-cli # 启动 ganache-cli
2、初始化工程
这里偷个懒,在官方的 pet-shop 项目上进行修改。新建一个文件夹,并在该文件夹内执行下面的命令
mkdir pet-shop & cd pet-shop
truffle unbox pet-shop
这样就通过 truffle 的 unbox 工具初始化了一个官方的宠物商店 Dapp,接下来将在其上开发区块链投票相关的合约及项目展示层。
3、编写合约
在包 contracts 下面新建一个 Election.sol 的合约文件,并在其中写入如下代码:
pragma solidity ^0.4.23;
contract election {
// 结构体
struct Candidate {
uint id;
string name;
uint voteCount;
}
// 事件
event votedEvent(uint indexed_candidateId);
// 存储结构体
mapping(uint => Candidate) public candidates;
// 是否已经投票了
mapping(address => bool) public voters;
// 总数量
uint public candidateCount;
// 构造函数
constructor() public {
addCandidate("张三");
addCandidate("李四");
}
// 添加候选人
function addCandidate(string _name) private {
candidateCount++;
candidates[candidateCount] = Candidate(candidateCount, _name, 0);
}
// 投票
function vote(uint _candidateId) public {
// 过滤
require(!voters[msg.sender]);
require(_candidateId > 0 && _candidateId <= candidateCount);
// 记录用户已经投票了
voters[msg.sender] = true;
candidates[_candidateId].voteCount++;
emit votedEvent(_candidateId);
}
}
这里对以上代码做一些解释:
struct:是 solidity 的关键字,用于声明结构体,这里相当于定义了一个内部类,封装出我们需要的 候选人对象;
event:用于在 solidity 智能合约中定义事件,其可以帮助反过来调用 JavaScript 中监听了该事件的回调。
mapping:一种键值对的映射关系存储结构。可以被视为一个哈希表,其中所有可能的键已被虚拟化的创建,被映射到一个默认值,是由二进制表示的零。并且 mapping 没有长度,键集合,值集合的概念。
constructor:合约的构造函数,是 solidity v0.4.23 后推荐的写的法,原来的首字母大写且与合约名相同来作为构造函数的写法已被废弃。
emit:solidity 0.4.23 后,调用事件时要加此前缀。
4、部署合约
前面说过,本项目我们使用 Truffle 框架,可以使用其提供的功能方便的进行开发,测试,部署等工作。这里我们使用它的 migration 功能来部署合约。
在 migrations 目录下,新建一个名为 2_deploy_contract.js 的文件,并在其中录入如下内容:
var Election = artifacts.require("./Election.sol");
module.exports = function (deployer) {
deployer.deploy(Election);
};
现在一切准备就绪,只需要在终端上执行下面的命令,就能部署好了:
truffle migrate --reset
这个命令会执行所有位于 migrations 目录内的移植脚本,如果之前已经有过成功移植,再次执行 truffle migrate
仅会执行新创建的移植。如果没有新的移植脚本,这个命令不会执行任何操作。而 --reset
就是告诉框架从头执行移植脚本。
truffle migrate
在执行时会自动调用 truffle compile
进行编译,编译结果以 json 格式存放在 build
目录中。
5、编写页面
本次我们做的是一个投票项目,因此,一定是有页面的,以供用户操作使用。这里我们是在 pet-shop
项目上进行改造,所以只需要去修改 src/index.html
内容就可以了,使用以下代码替换原有内容:
<!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">
<title>区块链投票</title>
<link href="css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container">
<div class="row">
<div class="col-xs-12 col-sm-8 col-sm-push-2">
<h1 class="text-center">区块链投票</h1>
<hr/>
<br/>
<div id="loader"><p class="test-center">Loading...</p></div>
<div id="content" style="display: none;">
<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="cadidatesSelect">选择你要投的名字:</label>
<select class="form-control" id="cadidatesSelect"> </select>
</div>
<button type="submit" class="btn btn-primary">投票</button>
</form>
</div>
<hr/>
<p id="accountAddress" class="test-center"></p></div>
</div>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
<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>
<style> .test-center {
text-align: center;
}
table {
width: 100%;
}
table tr {
border-bottom: 2px solid #efefef;
height: 40px;
}
</style>
页面十分简单,就是 <table>
绘制表格,<form onsubmit="App.castVote();return false;">
进行投票。
6、编写 js文件
js 部分是该 Dapp 比较麻烦的地方,因为其中使用了一个新工具 web3.js
,还要和智能合约的事件相结合,这里还是先将代码贴出来,再进行解释:
App = {
web3Provider: null,
contracts: {},
account: '0x0',
init: function () {
return App.initWeb3();
},
initWeb3: function () {
if (typeof web3 !== 'undefined') {
App.web3Provider = web3.currentProvider;
console.warn("Meata");
} else {
App.web3Provider = new Web3.providers.HttpProvider('http://localhost:8545/');
}
web3 = new Web3(App.web3Provider);
return App.initContract();
},
initContract: function () {
$.getJSON("Election.json", function (election) {
App.contracts.Election = TruffleContract(election);
App.contracts.Election.setProvider(App.web3Provider);
App.listenForEvents();
return App.reander();
})
},
reander: function () {
var electionInstance;
var $loader = $("#loader");
var $content = $("#content");
$loader.show();
$content.hide();
// 获得账号信息
web3.eth.getCoinbase(function (err, account) {
if (err === null) {
App.account = account;
$("#accountAddress").html("您当前的账号: " + account);
}
});
// 加载数据
App.contracts.Election.deployed().then(function (instance) {
electionInstance = instance;
return electionInstance.candidateCount();
}).then(function (candidatesCount) {
var $candidatesResults = $("#candidatesResults");
$candidatesResults.empty();
var $cadidatesSelect = $("#cadidatesSelect");
$cadidatesSelect.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];
var candidateTemplate = "<tr><th>" + id + "</th><td>" + name + "</td><td>" + voteCount + "</td></tr>";
$candidatesResults.append(candidateTemplate);
// 投票
var cadidateOption = "<option value='" + id + "'>" + name + "</option>";
$cadidatesSelect.append(cadidateOption);
});
}
return electionInstance.voters(App.account);
}).then(function (hasVoted) {
if (hasVoted) {
$('form').hide();
}
$loader.hide();
$content.show();
}).catch(function (err) {
console.warn(err);
});
},
// 投票
castVote: function () {
var $loader = $("#loader");
var $content = $("#content");
var candidateId = $('#cadidatesSelect').val();
App.contracts.Election.deployed().then(function (instance) {
return instance.vote(candidateId, {from: App.account});
}).then(function (result) {
$content.hide();
$loader.show();
}).catch(function (err) {
console.warn(err);
});
},
// 监听事件
listenForEvents: function () {
App.contracts.Election.deployed().then(function (instance) {
instance.votedEvent({}, {formBlock: 0, toBlock: 'latest'}).watch(function (error, event) {
console.log("event triggered", event);
App.reander();
});
})
}
};
$(function () {
$(window).load(function () {
App.init();
});
});
方法 initWeb3,是对 web3.js 进行初始化。
initContract 方法中,先通过 getJSON 读取本地 json 格式的合约文件,再调用 Truffle 的 TruffleContract 方法进行合约初始化,之后通过 setProvider 为 js 中的 合约设置代理,即,之后会通过 web3 的 httpProvider 执行真实的合约调用。
App.contracts.Election.deployed().then(function (instance)
是如果合约已经实例化了,则调用 then
中的方法,之后再通过 ES6 的链式语法执行后面的 then 方法。
7、运行项目
完成上面的代码后,一切准备就绪,现在需要运行起来:
npm run dev
该命令会执行前端项目的编译,并启动一个轻量的 http 服务器,这需要消耗一点时间,之后会自动打开浏览器,并跳转到项目的地址,默认端口是 3000。若使用 chrome 浏览器,此时会停在 “loading” 界面,接下来我们配置 MetaMask,一款浏览器插件钱包。
8、安装 MetaMask
打开 chrome 浏览器,地址栏输入 https://chrome.google.com/webstore/category/extensions,在出现的页面中搜索 metamask 并安装插件。
安装成功后,浏览器右侧会显示 “小狐狸” 图标,如未显示出来,可将 chrome 地址栏的右边界向左拉。
点击小狐狸图标,切换到本地私有网络:
再次点击 小狐狸,首先是提示界面,点击“Accept”,进入下一步,下一步也是声明,需要拉到底部才能点击“Accept”。
然后会看到此界面,请输入两次密码一定不能忘记,当然如果只是用作开发,就没关系,我们可以随时创建新的。
在创建账号的时候为了防止账号密码丢失,这里提供用于找回的助记词功能,一共是12个单词,切记,这一步很重要,一定要把这安全码记录下来方便恢复账号。
然后系统会生成一个以太坊的地址,并进入钱包页面。
9、进行投票
登录钱包后,刷新投票页面,此时可以正确显示投票页面了,可以看到目前所有候选人票数都为 “0”,点击 “投票”,会 MetaMask 钱包页面进行支付,这时会提示我们钱包余额不足,且我们账户的余额为 “0”。
这里是对合约进行投票,又不是做以太币转移,为什么会提示余额不足呢。结合前面的章节,稍作思考可以知道,对合约的投票行为属于以太坊交易,虽然不需要转移以太币,但以太坊上任何交易都需要花费手续费 gas,而此时我们钱包为零,所以当然会提示余额不足。
接下来我们在私链上为自己的钱包充值。点击 MetaMask 支付界面 Account 1 账户下面的地址进行拷贝,打开 Ethereum Wallet 钱包,并连接到我们的私链。
- Mac 下:
/Applications/Ethereum\ Wallet.app/Contents/MacOS/Ethereum\ Wallet --rpc http://localhost:8545/
点击 Send 按钮,在 to 中添入刚刚拷贝的 MetaMask 钱包账户地址,From 随意选择一个有余额的账户,Amount 输入 10,最后点击页面最底部的 SEND 按钮。
Ethereum Wallet 钱包会提示输入 From 账户的密码以进行解锁并支付,由于目前是在 Ganache-cli 私链上,它的设计是方便用于开发,而非用做生产网络,因此这里可以不输入密码,直接点击 SEND TRANSACTION。
再次回到投票页面,点击 “投票”,可以看到此时账户中已有 10ETH 余额,且不会再提示余额不足,点击 “Submit” 确认提交我们的投票交易。
提交后页面会自动刷新,可以看到候选人已经有了一张选票。
想直接看效果的小伙伴可以直接下载笔者的代码:Gitee 用区块链投票