以太坊使用数字货币(通证)完成去中心化投票DApp


声明:本文转载自https://my.oschina.net/ruoli/blog/1806939,转载目的在于传递更多信息,仅供学习交流之用。如有侵权行为,请联系我,我会及时删除。

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="请输入投票人地址" />       &nbsp;<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'>查&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;看</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 方法,此处没有调用。

            

本文发表于2018年05月05日 19:39
(c)注:本文转载自https://my.oschina.net/ruoli/blog/1806939,转载目的在于传递更多信息,并不代表本网赞同其观点和对其真实性负责。如有侵权行为,请联系我们,我们会及时删除.

阅读 2514 讨论 1 喜欢 0

抢先体验

扫码体验
趣味小程序
文字表情生成器

闪念胶囊

你要过得好哇,这样我才能恨你啊,你要是过得不好,我都不知道该恨你还是拥抱你啊。

直抵黄龙府,与诸君痛饮尔。

那时陪伴我的人啊,你们如今在何方。

不出意外的话,我们再也不会见了,祝你前程似锦。

这世界真好,吃野东西也要留出这条命来看看

快捷链接
网站地图
提交友链
Copyright © 2016 - 2021 Cion.
All Rights Reserved.
京ICP备2021004668号-1