7

用 Truffle 開發 DApp 以太坊投票程序應用 Part 2

 2 years ago
source link: https://blog.niclin.tw/2018/08/13/%E7%94%A8-truffle-%E9%96%8B%E7%99%BC-dapp-%E4%BB%A5%E5%A4%AA%E5%9D%8A%E6%8A%95%E7%A5%A8%E7%A8%8B%E5%BA%8F%E6%87%89%E7%94%A8-part-2/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Nic Lin's Blog

喜歡在地上滾的工程師

一般投票泛指普通的選舉,例如透過一人一票來選出國家總統。

有一種加權投票(weighted voting)常常用於上市交易的公司。這些公司股東的投票權取決於其持有的股票數量。

假設說你有 10,000 股的公司股票,那你就有 10,000 個投票權(這就和普通投票一人一票是不同的)

這個應用場景,我們可以實做一個 DAPP 來發行公司股票,該應用允許任何人購買股票成為股東。

股東基於擁有的股票數為候選人投票,例如你有 10,000 股,那你可以一個候選人投 5000 股,另一個 3000 股,第三個候選人 2000 股之類的。

我們預期透過應用,能夠調用 buy() 購買股票,然後利用 voteForCandidate() 為特定人選投票。

  • voterInfo: 投票人信息字典
  • totalTokens: 發行股票的總量
  • balanceTokens: 股票剩餘數量
  • tokenPrice: 股票單價
  • buy() payable: 購買股票

投票人信息: solidity 的 struct 類型可以將相關數據組織在一起。用 struct 來儲存投票的信息非常好,如果你不瞭解 struct,可以把他當成沒有 method 的 class。

struct voter {
  address voterAddress; //投票人帳戶地址
  uint tokensBought;    //投票人持有的股票數量
  uint[] tokensUsedPerCandidate; //為每個候選人消耗的股票數量
}

股票 Token

  • totalToken 保證總量
  • balanceTokens 保證餘額
  • tokenPrice 保證價格

任何人都可以調用 buy() 來買 token, 這裡有 payable 的修飾符,在 sodility 的合約中,只有聲明為 payable 的 method 才可以接受支付的貨幣 (msg.value)

example:

contract Voting{
  function buy() payable public returns (uint) {
    //利用 msg.value 來讀取用戶的支付金額,這方法必須要有 payable 聲明。
  }
}

購買 Token

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; //返回本次購買的通證數量
}

call buy()

contract.buy({
  value:web3.toWei('1','ether'), //購買者支付的以太幣金額
  from:web3.eth.accounts[1] //購買者賬戶地址
})

透過 console 調用

truffle(development)> Voting.deployed().then(function(contract) {contract.buy({value: web3.toWei('1', 'ether'), from: web3.eth.accounts[1]})})
~$ mkdir ~/tkapp
~$ cd ~/tkapp
~/tkapp$ truffle unbox webpack
~/tkapp$ rm contracts/ConvertLib.sol contracts/MetaCoin.sol

修改 app/index.html 如下

<!DOCTYPE html>
<html>
<head>
  <title>Hello World DApp</title>
  <link href='https://fonts.googleapis.com/css?family=Open+Sans:400,700' rel='stylesheet' type='text/css'>
  <link href='https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css' rel='stylesheet' type='text/css'>
  <style>
    .margin-top-3 {
      margin-top: 3em;
    }
  </style>
</head>
<body class="container">
  <h1>A Simple Hello World Voting Application</h1>
  <div class="col-sm-7 margin-top-3">
    <h2>Candidates</h2>
    <div class="table-responsive">
      <table class="table table-bordered">
        <thead>
          <tr>
            <th>Candidate</th>
            <th>Votes</th>
          </tr>
        </thead>
        <tbody id="candidate-rows">
        </tbody>
      </table>
    </div>
    <div class="container-fluid">
      <h2>Vote for Candidate</h2>
      <div id="msg"></div>
      <input type="text" id="candidate" placeholder="Enter the candidate name"/>
      <br>
      <br>
      <input type="text" id="vote-tokens" placeholder="Total no. of tokens to vote"/>
      <br>
      <br>
      <a href="#" onclick="voteForCandidate()" class="btn btn-primary">Vote</a>
    </div>
  </div>
  <div class="col-sm-offset-1 col-sm-4 margin-top-3">
    <div class="row">
      <h2>Token Stats</h2>
      <div class="table-responsive">
        <table class="table table-bordered">
          <tr>
            <td>Tokens For Sale</td>
            <td id="tokens-total"></td>
          </tr>
          <tr>
            <td>Tokens Sold</td>
            <td id="tokens-sold"></td>
          </tr>
          <tr>
            <td>Price Per Token</td>
            <td id="token-cost"></td>
          </tr>
          <tr>
            <td>Balance in the contract</td>
            <td id="contract-balance"></td>
          </tr>
        </table>
      </div>
    </div>
    <div class="row margin-top-3">
      <h2>Purchase Tokens</h2>
      <div class="col-sm-12">
        <div id="buy-msg"></div>
        <input type="text" id="buy" class="col-sm-8" placeholder="Number of tokens to buy"/> 
        <a href="#" onclick="buyTokens()" class="btn btn-primary">Buy</a>
      </div>
    </div>
    <div class="row margin-top-3">
      <h2>Lookup Voter Info</h2>
      <div class="col-sm-12">
        <input type="text" id="voter-info", class="col-sm-8" placeholder="Enter the voter address" /> 
        <a href="#" onclick="lookupVoterInfo()" class="btn btn-primary">Lookup</a>
        <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>
</body>
<script src="https://code.jquery.com/jquery-3.1.1.slim.min.js"></script>
<script src="app.js"></script>
</html>

修改 app/scripts/index.js

// Import the page's CSS. Webpack will know what to do with it.
import "../styles/app.css";

// Import libraries we need.
import { default as Web3} from 'web3';
import { default as contract } from 'truffle-contract'

/*
 * When you compile and deploy your Voting contract,
 * truffle stores the abi and deployed address in a json
 * file in the build directory. We will use this information
 * to setup a Voting abstraction. We will use this abstraction
 * later to create an instance of the Voting contract.
 * Compare this against the index.js from our previous tutorial to see the difference
 * https://gist.github.com/maheshmurthy/f6e96d6b3fff4cd4fa7f892de8a1a1b4#file-index-js
 */

import voting_artifacts from '../../build/contracts/Voting.json'

var Voting = contract(voting_artifacts);

let candidates = {}

let tokenPrice = null;

window.voteForCandidate = function(candidate) {
  let candidateName = $("#candidate").val();
  let voteTokens = $("#vote-tokens").val();
  $("#msg").html("Vote has been submitted. The vote count will increment as soon as the vote is recorded on the blockchain. Please wait.")
  $("#candidate").val("");
  $("#vote-tokens").val("");

  /* Voting.deployed() returns an instance of the contract. Every call
   * in Truffle returns a promise which is why we have used then()
   * everywhere we have a transaction call
   */
  Voting.deployed().then(function(contractInstance) {
    contractInstance.voteForCandidate(candidateName, voteTokens, {gas: 140000, from: web3.eth.accounts[0]}).then(function() {
      let div_id = candidates[candidateName];
      return contractInstance.totalVotesFor.call(candidateName).then(function(v) {
        $("#" + div_id).html(v.toString());
        $("#msg").html("");
      });
    });
  });
}

/* The user enters the total no. of tokens to buy. We calculate the total cost and send it in
 * the request. We have to send the value in Wei. So, we use the toWei helper method to convert
 * from Ether to Wei.
 */

window.buyTokens = function() {
  let tokensToBuy = $("#buy").val();
  let price = tokensToBuy * tokenPrice;
  $("#buy-msg").html("Purchase order has been submitted. Please wait.");
  Voting.deployed().then(function(contractInstance) {
    contractInstance.buy({value: web3.toWei(price, 'ether'), from: web3.eth.accounts[0]}).then(function(v) {
      $("#buy-msg").html("");
      web3.eth.getBalance(contractInstance.address, function(error, result) {
        $("#contract-balance").html(web3.fromWei(result.toString()) + " Ether");
      });
    })
  });
  populateTokenData();
}

window.lookupVoterInfo = function() {
  let address = $("#voter-info").val();
  Voting.deployed().then(function(contractInstance) {
    contractInstance.voterDetails.call(address).then(function(v) {
      $("#tokens-bought").html("Total Tokens bought: " + v[0].toString());
      let votesPerCandidate = v[1];
      $("#votes-cast").empty();
      $("#votes-cast").append("Votes cast per candidate: <br>");
      let allCandidates = Object.keys(candidates);
      for(let i=0; i < allCandidates.length; i++) {
        $("#votes-cast").append(allCandidates[i] + ": " + votesPerCandidate[i] + "<br>");
      }
    });
  });
}

/* Instead of hardcoding the candidates hash, we now fetch the candidate list from
 * the blockchain and populate the array. Once we fetch the candidates, we setup the
 * table in the UI with all the candidates and the votes they have received.
 */
function populateCandidates() {
  Voting.deployed().then(function(contractInstance) {
    contractInstance.allCandidates.call().then(function(candidateArray) {
      for(let i=0; i < candidateArray.length; i++) {
        /* We store the candidate names as bytes32 on the blockchain. We use the
         * handy toUtf8 method to convert from bytes32 to string
         */
        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(function (candidate) {
    $("#candidate-rows").append("<tr><td>" + candidate + "</td><td id='" + candidates[candidate] + "'></td></tr>");
  });
}

/* Fetch the total tokens, tokens available for sale and the price of
 * each token and display in the UI
 */
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 {
    console.warn("No web3 detected. Falling back to http://localhost:8545. You should remove this fallback when you deploy live, as it's inherently insecure. Consider switching to Metamask for development. More info here: http://truffleframework.com/tutorials/truffle-and-metamask");
    // fallback - use your fallback strategy (local node / hosted node + in-dapp id mgmt / fail)
    window.web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545"));
  }

  Voting.setProvider(web3.currentProvider);
  populateCandidates();

});

建立合約 touch contracts/Voting.sol

pragma solidity ^0.4.18;

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;
 }

 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));

  if (voterInfo[msg.sender].tokensUsedPerCandidate.length == 0) {
   for(uint i = 0; i < candidateList.length; i++) {
    voterInfo[msg.sender].tokensUsedPerCandidate.push(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);
 }

 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(address(this).balance);
 }

 function allCandidates() view public returns (bytes32[]) {
  return candidateList;
 }
}
  1. 先 compile truffle compile
  2. 編譯網頁 webpack
  3. 確認 ganache-cli 已經開啟
  4. 部屬合約 truffle migrate
  • 一個候選人(比如 Nick)有多少投票?
truffle(development)> Voting.deployed().then(function(instance) {instance.totalVotesFor.call('Nick').then(function(i) {console.log(i)})})
  • 一共初始化發行了多少 Token?
truffle(development)> Voting.deployed().then(function(instance) {console.log(instance.totalTokens().then(function(v) {console.log(v)}))})
  • 已經售出了多少 Token?
truffle(development)> Voting.deployed().then(function(instance) {console.log(instance.tokensSold().then(function(v) {console.log(v)}))})
  • 購買 100 個 Token
truffle(development)> Voting.deployed().then(function(instance) {console.log(instance.buy({value: web3.toWei('1', 'ether')}).then(function(v) {console.log(v)}))})
  • 購買以後賬戶餘額是多少?
truffle(development)> web3.eth.getBalance(web3.eth.accounts[0])
  • 已經售出了多少 Token?
Voting.deployed().then(function(instance) {console.log(instance.tokensSold().then(function(v) {console.log(v)}))})
  • 給 Jose 投 25 個 Token,給 Rama 和 Nick 各投 10 個 Token。
truffle(development)> Voting.deployed().then(function(instance) {console.log(instance.voteForCandidate('Jose', 25).then(function(v) {console.log(v)}))})
truffle(development)> Voting.deployed().then(function(instance) {console.log(instance.voteForCandidate('Rama', 10).then(function(v) {console.log(v)}))})
truffle(development)> Voting.deployed().then(function(instance) {console.log(instance.voteForCandidate('Nick', 10).then(function(v) {console.log(v)}))})
  • 查詢你所投賬戶的投票人信息(除非用了其他賬戶,否則你的賬戶默認是 web3.eth.accounts[0])
truffle(development)> Voting.deployed().then(function(instance) {console.log(instance.voterDetails('0x004ee719ff5b8220b14acb2eac69ab9a8221044b').then(function(v) {console.log(v)}))})
  • 現在候選人 Rama 有多少投票?
truffle(development)> Voting.deployed().then(function(instance) {instance.totalVotesFor.call('Rama').then(function(i) {console.log(i)})})

如果到這邊都 ok, 那就只剩下網站的交互了

不過前面的 index 都一次放上了,直接 npm run dev 後,將 metamask 指向 localhost:8545 應該就可以直接操作了


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK