一般投票泛指普通的選舉,例如透過一人一票來選出國家總統。
有一種加權投票(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;
}
}
準備就緒
- 先 compile
truffle compile
- 編譯網頁
webpack
- 確認
ganache-cli
已經開啟 - 部屬合約
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 應該就可以直接操作了