以太坊使用数字货币(通证)完成去中心化投票DApp
时间: 2018-05-04来源:OSCHINA
前景提要
「深度学习福利」大神带你进阶工程师,立即查看>>>
1、背景介绍
上一节讲了使用NodeJs创建投票DApp应用,可见:
以太坊区块链使用NodeJs、Web3开发投票DApp过程示例
主要讲了使用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 方法,此处没有调用。

科技资讯:

科技学院:

科技百科:

科技书籍:

网站大全:

软件大全:

热门排行