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

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 方法,此处没有调用。

            

转载于:https://my.oschina.net/ruoli/blog/1806939

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值