以太网利用数字货币完成去中心化投票 DApp (通证)
1、背景介绍
使用 NodeJs 创建投票 DApp 应用,是非常基础的一些操作,包括编译和部署都是使用 web3 最基础的方法,这有助于加深对基础知识的了解,现在对此实例进行升级,使用 Truffle 开发框架,并添加通证进行改造,改造后的投票 DApp 功能主要为:每个投票者需要先使用以太币购买投票通证,购买的越多则可以投票的数量也就越多,相当于股票 所拥有的股票越多,则在董事会的投票权也就越多。
提供网页操作,可以查看自己当前对每个人的投票数量,已经自己剩余的投票数,开发完成后效果预览如下:
2、环境准备
准备开发前需要准备如下工作
- 本地环境安装最新版本 NodeJS
- 熟悉 Truffle 框架的基本操作
- 本地环境安装 Ganache 模拟节点环境
- 熟悉 web3 常见 API
新建目录 Voting-Truffle-Token 作为工作目录。
在此目录下使用 Truffle 初始化 webpack 模板,在 contracts 目录下删除原有的 ConvertLib.sol、MetaCoin.sol 两个文件。
3、智能合约编写
在 contracts 目录中新建 Voting.sol 合约文件,并在 Remix 环境中进行编写,编写完成后内容如下:
pragma solidity ^0.4.18;
// 使用通证改造后的投票DApp
// 2018/05/04
// Ruoli
contract Voting {
//-------------------------------------------------------------------------
//存储每个投票人的信息
struct voter {
address voterAddress; //投票人账户地址
uint tokensBought;//投票人持有的投票通证数量
uint[] tokensUsedPerCandidate;//为每个候选人消耗的股票通证数量
}
//投票人信息
mapping (address => voter) public voterInfo;
//-------------------------------------------------------------------------
//每个候选人获得的投票
mapping (bytes32 => uint) public votesReceived;
//候选人名单
bytes32[] public candidateList;
//发行的投票通证总量
uint public totalTokens;
//投票通证剩余数量
uint public balanceTokens;
//投票通证单价
uint public tokenPrice;
//构造方法,合约部署时执行一次, 初始化投票通证总数量、通证单价、所有候选人信息
constructor(uint tokens, uint pricePerToken, bytes32[] candidateNames) public {
candidateList = candidateNames;
totalTokens = tokens;
balanceTokens = tokens;
tokenPrice = pricePerToken;
}
//购买投票通证,此方法使用 payable 修饰,在Sodility合约中,
//只有声明为payable的方法, 才可以接收支付的货币(msg.value值)
function buy() payable public returns (uint) {
uint tokensToBuy = msg.value / tokenPrice; //根据购买金额和通证单价,计算出购买量
require(tokensToBuy <= balanceTokens); //继续执行合约需要确认合约的通证余额不小于购买量
voterInfo[msg.sender].voterAddress = msg.sender; //保存购买人地址
voterInfo[msg.sender].tokensBought += tokensToBuy; //更新购买人持股数量
balanceTokens -= tokensToBuy; //将售出的通证数量从合约的余额中剔除
return tokensToBuy; //返回本次购买的通证数量
}
//获取候选人获得的票数
function totalVotesFor(bytes32 candidate) view public returns (uint) {
return votesReceived[candidate];
}
//为候选人投票,并使用一定数量的通证表示其支持力度
function voteForCandidate(bytes32 candidate, uint votesInTokens) public {
//判断被投票候选人是否存在
uint index = indexOfCandidate(candidate);
require(index != uint(-1));
//初始化 tokensUsedPerCandidate
if (voterInfo[msg.sender].tokensUsedPerCandidate.length == 0) {
for(uint i = 0; i < candidateList.length; i++) {
voterInfo[msg.sender].tokensUsedPerCandidate.push(0);
}
}
//验证投票人的余额是否足够(购买总额-已花费总额>0)
uint availableTokens = voterInfo[msg.sender].tokensBought -
totalTokensUsed(voterInfo[msg.sender].tokensUsedPerCandidate);
require (availableTokens >= votesInTokens);
votesReceived[candidate] += votesInTokens;
voterInfo[msg.sender].tokensUsedPerCandidate[index] += votesInTokens;
}
// 计算 投票人总共花费了多少 投票通证
function totalTokensUsed(uint[] _tokensUsedPerCandidate) private pure returns (uint) {
uint totalUsedTokens = 0;
for(uint i = 0; i < _tokensUsedPerCandidate.length; i++) {
totalUsedTokens += _tokensUsedPerCandidate[i];
}
return totalUsedTokens;
}
//获取候选人的下标
function indexOfCandidate(bytes32 candidate) view public returns (uint) {
for(uint i = 0; i < candidateList.length; i++) {
if (candidateList[i] == candidate) {
return i;
}
}
return uint(-1);
}
//方法声明中的 view 修饰符,这表明该方法是只读的,即方法的执行
//并不会改变区块链的状态,因此执行这些交易不会耗费任何gas
function tokensSold() view public returns (uint) {
return totalTokens - balanceTokens;
}
function voterDetails(address user) view public returns (uint, uint[]) {
return (voterInfo[user].tokensBought, voterInfo[user].tokensUsedPerCandidate);
}
//将合约里的资金转移到指定账户
function transferTo(address account) public {
account.transfer(this.balance);
}
function allCandidates() view public returns (bytes32[]) {
return candidateList;
}
}
修改 migrations/2_deploy_contracts.js 文件,内容如下:
var Voting = artifacts.require("./Voting.sol");
module.exports = function(deployer) {
//初始化合约,提供10000个投票通证,每隔通证单价 0.01 ether,候选人为 'Rama', 'Nick', 'Jose'
deployer.deploy(Voting,10000, web3.toWei('0.01', 'ether'), ['Rama', 'Nick', 'Jose']);
};
至此合约的编写完成。
4、智能合约编译
执行 truffle compile 命令进行编译操作,如下:
PS C:\Workspace\Ruoli-Code\Voting-Truffle-Token> truffle compile
Compiling .\contracts\Migrations.sol...
Compiling .\contracts\Voting.sol...
Compilation warnings encountered:
/C/Workspace/Ruoli-Code/Voting-Truffle-Token/contracts/Migrations.sol:11:3: Warning: Defining constructors as functions with the same name as the contract is deprecated. Use "constructor(...) { ... }" instead.
function Migrations() public {
^ (Relevant source part starts here and spans across multiple lines).
,/C/Workspace/Ruoli-Code/Voting-Truffle-Token/contracts/Voting.sol:104:22: Warning: Using contract member "balance" inherited from the address type is deprecated. Convert the contract to "address" type to access the member, for example use "address(contract).balance" instead.
account.transfer(this.balance);
^----------^
Writing artifacts to .\build\contracts
没有提示错误,编译成功,在当前目录下出现了 build 目录。
5、合约的部署与测试
部署前需要先启动 Ganache 模拟节点,并且修改 truffle.js 文件,内容如下:
// Allows us to use ES6 in our migrations and tests.
require('babel-register')
module.exports = {
networks: {
development: {
host: '127.0.0.1',
port: 7545,
network_id: '*' // Match any network id
}
}
}
执行 truffle deploy 进行部署操作,如下:
PS C:\Workspace\Ruoli-Code\Voting-Truffle-Token> truffle deploy
Using network 'development'.
Running migration: 1_initial_migration.js
Deploying Migrations...
... 0x6b327c157804151269c5db193507a51a2cff40f64f81bd39ee3bcc567e6d93ce
Migrations: 0xb81237dd01159a36a5ac3c760d227bbafe3341ea
Saving successful migration to network...
... 0xc5be542ec02f5513ec21e441c54bd31f0c86221da26ed518a2da25c190faa24b
Saving artifacts...
Running migration: 2_deploy_contracts.js
Deploying Voting...
... 0xf836862d3fccbbd971ea61cca1bb41fe25f4665b80ac6c2498396cfeb1633141
Voting: 0x6ba286f3115994baf1fed1159e81f77c9e1cd4fa
Saving successful migration to network...
... 0xc8d5533c11181c87e6b60d4863cdebb450a2404134aea03a573ce6886905a00b
Saving artifacts...
PS C:\Workspace\Ruoli-Code\Voting-Truffle-Token>
查看 Ganache 中第一个账户的以太币余额略有减少,说明部署成功,下面编写测试代码对合约进行测试,在 test 目录先删除原有的所有文件,新建 TestVoting.js 文件,内容如下:
var Voting = artifacts.require("./Voting.sol");
contract('Voting',(accounts) => {
it("投票合约应该有10000个预售投票通证", function() {
return Voting.deployed().then(function(instance) {
return instance.totalTokens.call();
}).then((balance)=>{
assert.equal(balance.valueOf(), 10000, "10000个预售投票通证 不符合预期 :"+balance.valueOf());
});
});
it("投票合约已经售出的投票通证应该为0", function() {
return Voting.deployed().then(function(instance) {
return instance.tokensSold.call();
}).then((balance)=>{
assert.equal(balance.valueOf(), 0, "投票合约已经售出的投票通证数量 不符合预期 :"+balance.valueOf());
});
});
it("购买 100个通证,总价值 1 ether ", function() {
return Voting.deployed().then(function(instance) {
return instance.buy.call({value:web3.toWei('1', 'ether')});
}).then((balance)=>{
assert.equal(balance.valueOf(), 100, "购买100个通证 不符合预期 :"+balance.valueOf());
});
});
it("投票合约已经售出的投票通证应该为100", function() {
return Voting.deployed().then(function(instance) {
return instance.tokensSold.call();
}).then((balance)=>{
assert.equal(balance.valueOf(), 100, "投票合约已经售出的投票通证应该为100 不符合预期 :"+balance.valueOf());
});
});
});
在根目录执行 truffle test 即可针对此单元测试文件进行测试,如下图:
PS C:\Workspace\Ruoli-Code\Voting-Truffle-Token> truffle test
Using network 'development'.
Contract: Voting
√ 投票合约应该有10000个预售投票通证
√ 投票合约已经售出的投票通证应该为0
√ 购买 100个通证,总价值 1 ether
1) 投票合约已经售出的投票通证应该为100
> No events were emitted
3 passing (161ms)
1 failing
1) Contract: Voting 投票合约已经售出的投票通证应该为100:
AssertionError: 投票合约已经售出的投票通证应该为100 不符合预期 :0: expected '0' to equal 100
at C:/Workspace/Ruoli-Code/Voting-Truffle-Token/test/TestVoting.js:33:14
at <anonymous>
at process._tickCallback (internal/process/next_tick.js:188:7)
至此,测试完成。
6、前端网页编写
在 app 目录新建 index.html ,内容如下:
<!DOCTYPE html>
<html>
<head>
<title>Decentralized Voting App</title>
<link rel="stylesheet" href="https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css" >
<script src="https://cdn.bootcss.com/jquery/1.12.4/jquery.min.js"></script>
<style type="text/css">
hr{
margin-top: 7px;
margin-bottom: 7px;
}
</style>
</head>
<body class="row">
<h3 class="text-center banner">去中心化投票应用
<span class="glyphicon glyphicon-question-sign" style="font-size: 20px;color: #a1a1a1"></span>
</h3>
<hr>
<div class="container">
<div class="row margin-top-3">
<div class="col-sm-6">
<h4>候选人信息</h4>
<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-5">
<h4>通证信息</h4>
<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="token-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-6 form">
<h4>参与投票</h4>
<div class="alert alert-success" role="alert" id="msg" style="display: none;">投票成功,已更新得票总数</div>
<input type="text" id="candidate" class="form-control" placeholder="候选人名称"/>
<br>
<input type="text" id="vote-tokens" class="form-control" placeholder="投票通证数量"/>
<br>
<a href="#" id="voter-send" class="btn btn-primary">发起投票</a>
</div>
<div class="col-sm-offset-1 col-sm-5">
<div class="col-sm-12 form">
<h4>购买投票通证</h4>
<div class="alert alert-success" role="alert" id="buy-msg" style="display: none;">购买成功,已更新通证数据</div>
<div class="input-group">
<input type="text" class="form-control" id="buy" placeholder="请输入购买通证数量">
<span class="input-group-addon btn btn-primary" id="voter_buyTokens" onclick="buyTokens()">确认购买</span>
</div>
</div>
<div class="col-sm-12 margin-top-3 form">
<h4 class="">查看投票人信息</h4>
<!-- <input type="text" id="voter-info", class="col-sm-8" placeholder="请输入投票人地址" />
<a href="#" onclick="lookupVoterInfo(); return false;" class="btn btn-primary">查看</a> -->
<div class="input-group">
<input type="text" class="form-control" id="voter-info" placeholder="请输入投票人地址">
<span class="input-group-addon btn btn-primary" id='voter-lookup-btn'>查 看</span>
</div>
<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="./app.js"></script>
</html>
在 app/javascripts 目录下新建 app.js ,内容如下:
// Import the page's CSS. Webpack will know what to do with it.
//import "../stylesheets/app.css";
// Import libraries we need.
import { default as Web3} from 'web3';
import { default as contract } from 'truffle-contract'
import voting_artifacts from '../../build/contracts/Voting.json'
let Voting = contract(voting_artifacts);
let candidates = {}
let tokenPrice = null;
function populateCandidates() {
Voting.deployed().then((contractInstance) => {
//查询所有候选人
contractInstance.allCandidates.call().then((candidateArray) => {
for(let i=0; i < candidateArray.length; i++) {
candidates[web3.toUtf8(candidateArray[i])] = "candidate-" + i;
}
setupCandidateRows();
populateCandidateVotes();
populateTokenData();
});
});
}
function populateCandidateVotes() {
let candidateNames = Object.keys(candidates);
for (var i = 0; i < candidateNames.length; i++) {
let name = candidateNames[i];
Voting.deployed().then(function(contractInstance) {
contractInstance.totalVotesFor.call(name).then(function(v) {
$("#" + candidates[name]).html(v.toString());
});
});
}
}
function setupCandidateRows() {
Object.keys(candidates).forEach( (candidate) => {
$("#candidate-rows").append("<tr><td>" + candidate + "</td><td id='" + candidates[candidate] + "'></td></tr>");
});
}
function populateTokenData() {
Voting.deployed().then(function(contractInstance) {
contractInstance.totalTokens().then(function(v) {
$("#tokens-total").html(v.toString());
});
contractInstance.tokensSold.call().then(function(v) {
$("#tokens-sold").html(v.toString());
});
contractInstance.tokenPrice().then(function(v) {
tokenPrice = parseFloat(web3.fromWei(v.toString()));
$("#token-cost").html(tokenPrice + " Ether");
});
web3.eth.getBalance(contractInstance.address, function(error, result) {
$("#contract-balance").html(web3.fromWei(result.toString()) + " Ether");
});
});
}
//初始化加载
$( document ).ready(function() {
if (typeof web3 !== 'undefined') {
console.warn("Using web3 detected from external source like Metamask")
// Use Mist/MetaMask's provider
window.web3 = new Web3(web3.currentProvider);
} else {
window.web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:7545"));
}
Voting.setProvider(web3.currentProvider);
populateCandidates();
//初始化查看投票人事件
$("#voter-lookup-btn").click(() => {
let address = $("#voter-info").val();
Voting.deployed().then((contractInstance) => {
//获取投票人信息
contractInstance.voterDetails.call(address).then( (v) => {
$("#tokens-bought").html("<br>总共购买投票通证数量: " + v[0].toString());
let votesPerCandidate = v[1];
$("#votes-cast").empty();
$("#votes-cast").append("通证已经用于投票记录如下: <br>");
let table_data="<table class='table table-striped table-bordered table-condensed'>";
let allCandidates = Object.keys(candidates);
for(let i=0; i < allCandidates.length; i++) {
table_data+="<tr><td>"+allCandidates[i]+"</td><td>"+votesPerCandidate[i]+"</td></tr>";
}
table_data+="</table>";
$("#votes-cast").append(table_data);
});
});
});
//发起投票操作事件
$("#voter-send").click(() => {
let candidateName = $("#candidate").val(); //获取被投票的候选人
let voteTokens = $("#vote-tokens").val(); //获取票数
$("#candidate").val("");
$("#vote-tokens").val("");
Voting.deployed().then( (contractInstance) => {
contractInstance.voteForCandidate(candidateName, voteTokens, {gas: 140000, from: web3.eth.accounts[1]}).then( () => {
let div_id = candidates[candidateName];
return contractInstance.totalVotesFor.call(candidateName).then( (v) => {
//更新候选人票数
$("#" + div_id).html(v.toString());
$("#msg").fadeIn(300);
setTimeout(() => $("#msg").fadeOut(1000),1000);
});
});
});
});
//绑定购买通证事件
$("#voter_buyTokens").click(() => {
let tokensToBuy = $("#buy").val();
let price = tokensToBuy * tokenPrice;
Voting.deployed().then(function(contractInstance) {
contractInstance.buy({value: web3.toWei(price, 'ether'), from: web3.eth.accounts[1]}).then(function(v) {
web3.eth.getBalance(contractInstance.address, function(error, result) {
$("#contract-balance").html(web3.fromWei(result.toString()) + " Ether");
});
$("#buy-msg").fadeIn(300);
setTimeout(() => $("#buy-msg").fadeOut(1000),1000);
})
});
populateTokenData();
});
});
添加完成这连个文件,前端页面开发完成
7、页面测试
在根目录输入 npm run dev 启动此工程,如下:
> truffle-init-webpack@0.0.2 dev C:\Workspace\Ruoli-Code\Voting-Truffle-Token
> webpack-dev-server
Project is running at http://localhost:8081/
webpack output is served from /
Hash: 311e234883b64483e595
Version: webpack 2.7.0
Time: 1322ms
Asset Size Chunks Chunk Names
app.js 1.79 MB 0 [emitted] [big] main
index.html 3.5 kB [emitted]
chunk {0} app.js (main) 1.77 MB [entry] [rendered]
[71] ./app/javascripts/app.js 4.68 kB {0} [built]
[72] (webpack)-dev-server/client?http://localhost:8081 7.93 kB {0} [built]
[73] ./build/contracts/Voting.json 163 kB {0} [built]
[109] ./~/loglevel/lib/loglevel.js 7.86 kB {0} [built]
[117] ./~/strip-ansi/index.js 161 bytes {0} [built]
[154] ./~/truffle-contract-schema/index.js 5.4 kB {0} [built]
[159] ./~/truffle-contract/index.js 2.64 kB {0} [built]
[193] ./~/url/url.js 23.3 kB {0} [built]
[194] ./~/url/util.js 314 bytes {0} [built]
[195] ./~/web3/index.js 193 bytes {0} [built]
[229] (webpack)-dev-server/client/overlay.js 3.67 kB {0} [built]
[230] (webpack)-dev-server/client/socket.js 1.08 kB {0} [built]
[231] (webpack)/hot nonrecursive ^./log$ 160 bytes {0} [built]
[232] (webpack)/hot/emitter.js 77 bytes {0} [built]
[233] multi (webpack)-dev-server/client?http://localhost:8081 ./app/javascripts/app.js 40 bytes {0} [built]
+ 219 hidden modules
webpack: Compiled successfully.
启动完成后,在浏览器中访问 http://localhost:8081/ ,即可看到页面最开始展示的效果,可以用于购买通证,发起投票以及查看每个账户的投票记录信息。
由于是使用 Ganache 中第一个账户进行部署的合约,上述代码中是使用 Ganache 第二个账户进行购买通证及发起投票的,所以在打开 Ganache 主页,即可发现由于购买通证,第二个账户的以太币已经减少,但为什么减少的以太币没有转入第一个账户,这个需要进行一下合约账户余额转出操作,对应合约中的 transferTo 方法,此处没有调用。