惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

阮一峰的网络日志
阮一峰的网络日志
Last Week in AI
Last Week in AI
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
U
Unit 42
J
Java Code Geeks
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
罗磊的独立博客
月光博客
月光博客
腾讯CDC
Stack Overflow Blog
Stack Overflow Blog
小众软件
小众软件
B
Blog
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
美团技术团队
Y
Y Combinator Blog
T
Tailwind CSS Blog
宝玉的分享
宝玉的分享
酷 壳 – CoolShell
酷 壳 – CoolShell
博客园_首页
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
爱范儿
爱范儿
B
Blog RSS Feed
V
Visual Studio Blog
MyScale Blog
MyScale Blog

Nic Lin's Blog

謝明真 - 高效領導力的課後筆記 NFT 開發實戰!基礎智能合約入門 (3) NFT 開發實戰!基礎智能合約入門 (2) NFT 開發實戰!基礎智能合約入門 (1) 如何自我檢測 log4j CVE 漏洞 Rails 如何在資料寫入時記錄來源 IP 位置 如何經營工程師 Youtube 頻道 - Part 8 營收篇 如何經營工程師 Youtube 頻道 - Part 7 酸民文化篇 如何經營工程師 Youtube 頻道 - Part 5 設備器材篇 如何經營工程師 Youtube 頻道 - Part 4 後製剪輯篇 如何經營工程師 Youtube 頻道 - Part 3 文案企劃篇 如何經營工程師 Youtube 頻道 - Part 2 設備器材篇 如何經營工程師 Youtube 頻道 - Part 1 制訂頻道方向篇 如何經營工程師 Youtube 頻道 - Part 0 Rails 中避免 race condition 的最佳實踐(二) Rails 中避免 race condition 的最佳實踐(一) 10 分鐘整合 google sheet 做自動化開發功能週報 經營 Side Project 300 天所帶來的收穫及挑戰 我的 Youtube 影片製作流程 API 設計時必須注意的 HTTP header 底線問題 如何提升你的程式可讀性之實務技巧(三) 如何提升你的程式可讀性之實務技巧(二) 如何提升你的程式可讀性之實務技巧(一) Ruby 中使用 freeze 優化效能的時機 避免 React 中的 useEffect 無限 render 在 Rails 內輕量使用 Vue Component 的最佳實踐 如何在區域網路用 Docker 架設有 SSL 的 Gitlab 從被問到問人,那些我常問的面試問題 [Rails] 如何漂亮寫出可維護的 query (Maintainable Rails Query) 在已知長度情況下優化 slice 的性能
用 Truffle 開發 DApp 以太坊投票程序應用 Part 2
Nic Lin · 2018-08-13 · via 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"/>&nbsp;
        <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" />&nbsp;
        <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 應該就可以直接操作了