# 简单投票 DApp
# 项目搭建
$ mkdir simple_voting_dapp
$ cd simple_voting_dapp
$ npm init -y
$ npm install ganache-cli web3@0.20.1 solc
$ node_modules/.bin/ganache-cli
ganache 前身是 testRPC 非常方便的起一个 geth 测试网络节点,默认有 10 个账户,有 10 个私钥
// ganache-cli 显示的是
Ganache CLI v6.12.2 (ganache-core: 2.13.2)
Available Accounts
==================
(0) 0xFBe5384d8E2E7C71319f65B830D446229D89a766 (100 ETH)
(1) 0xb764972488675dABe5a2B303FE51C13D94d5E53F (100 ETH)
(2) 0x21fCD36B68f9af35F9210568D524b9582C0b008d (100 ETH)
(3) 0x413f5C8239c4a7563db696EFD45133B0B421B18E (100 ETH)
(4) 0x06bF225158b2a7B12bC6F98ea6355e77B7EBbef9 (100 ETH)
(5) 0xb12467b5e7fee6465CC514CBE1db9FDe9cCa6686 (100 ETH)
(6) 0x1575A3a7F161A4870c7621a79319b1dd7F4b343F (100 ETH)
(7) 0x2A9A8E42ca621f9D5002Dd5551EDD9Ea76cD7cc7 (100 ETH)
(8) 0x9E358B2f3467Daf792A23571625EA1d41560A04a (100 ETH)
(9) 0xCf2FEC11a0ec8B34caB1F6FCf6aEf650C738Fd4b (100 ETH)
Private Keys
==================
(0) 0xb96282cfeb8bcd2e2cdab359ee7016314a8e43eb0c21142983889435d571179a
(1) 0x12b4604ba1b3e66be7f2998763ab349ad3bf53e856c98507ab19f234ffa2c3c2
(2) 0xbbae32ad5aaed73e7f052e7f5f268a5660bda26fffacb78bc41fa1b2629e5641
(3) 0x740d393e7ab0f9acbb30a640b98736978a79035a26d7b439195f3b1b6ee66097
(4) 0x29323e389d48dad39a91d21437810df72367c148b3f4f18f18edf6cd1c26cf7a
(5) 0xefee58e509709bdd3efec936499c2d137a9ae86bd48f47c57a99da6e5254b38f
(6) 0x1189345e2bbc54da9da35fc7f5867e30a2fd7299340e74514107733c3dc56f78
(7) 0xb8b6a0d4cb05c57abf4e28e5cfe193c3a8db90d67fe2ee57bb66b1056576dc6d
(8) 0xe209aa8a84086989c43a0637c8395f0fa4938723f0451252cf2a692a4a9f8532
(9) 0x069d1971b0d631877a97fdb1d8afad3edf1fba3260cba5ae18d162a397da2792
HD Wallet
==================
Mnemonic: frost laugh prevent innocent find shell shoulder dinner noise soon garment coyote
Base HD Path: m/44'/60'/0'/0/{account_index}
Gas Price
==================
20000000000 // 20Gwei
Gas Limit
==================
6721975
Call Gas Limit
==================
9007199254740991
Listening on 127.0.0.1:8545
└─(09:47:05)──> node_modules/.bin/ganache-cli --help ──(一, 524)─┘
Network:
-p, --port Port number to listen on [数字] [默认值: 8545]
-h, --host, --hostname Hostname to listen on [字符串] [默认值: "127.0.0.1"]
--keepAliveTimeout The number of milliseconds of inactivity a server
needs to wait for additional incoming data, after it
has finished writing the last response, before a
socket will be destroyed. [数字] [默认值: 5000]
Accounts:
-a, --accounts Number of accounts to generate at startup
[数字] [默认值: 10]
-e, --defaultBalanceEther Amount of ether to assign each test account
[数字] [默认值: 100]
--account Account data in the form
'<private_key>,<initial_balance>', can be
specified multiple times. Note that private
keys are 64 characters long and must be
entered as an 0x-prefixed hex string. Balance
can either be input as an integer, or as a
0x-prefixed hex string with either form
specifying the initial balance in wei. [数组]
--account_keys_path, --acctKeys saves generated accounts and private keys as
JSON object in specified file
[字符串] [默认值: null]
-n, --secure Lock available accounts by default (good for
third party transaction signing)
[布尔] [默认值: false]
-u, --unlock Comma-separated list of accounts or indices
to unlock [数组]
--hdPath, --hd_path The hierarchical deterministic path to use
when generating accounts. Default:
"m/44'/60'/0'/0/" [字符串]
Chain:
-k, --hardfork Allows users to specify which hardfork should be
used. Supported hardforks are `byzantium`,
`constantinople`, `petersburg`, `istanbul` and
`muirGlacier` (default).
[字符串] [默认值: "muirGlacier"]
-f, --fork Fork from another currently running Ethereum
client at a given block. Input should be the
HTTP location and port of the other client, e.g.
'http://localhost:8545' or optionally provide a
block number 'http://localhost:8545@1599200'
[字符串] [默认值: false]
--forkCacheSize The maximum size, in bytes, of the in-memory
cache for queries on a chain fork. Defaults to
`1_073_741_824` bytes (1 gigabyte). You can set
this to `0` to disable caching (not
recommended), or to `-1` for unlimited (will be
limited by your node process).
[数字] [默认值: 1073741824]
--db Directory of chain database; creates one if it
doesn't exist [字符串] [默认值: null]
-s, --seed Arbitrary data to generate the HD wallet
mnemonic to be used
[字符串] [默认值: Random value, unless -d is specified]
-d, --deterministic Generate deterministic addresses based on a
pre-defined mnemonic. [布尔]
-m, --mnemonic bip39 mnemonic phrase for generating a PRNG
seed, which is in turn used for hierarchical
deterministic (HD) account generation [字符串]
--noVMErrorsOnRPCResponse Do not transmit transaction failures as RPC
errors. Enable this flag for error reporting
behaviour which is compatible with other clients
such as geth and Parity. [布尔] [默认值: false]
-b, --blockTime Block time in seconds for automatic mining. Will
instantly mine a new block for every transaction
if option omitted. Avoid using unless your test
cases require a specific mining interval. [数字]
-i, --networkId The Network ID ganache-cli will use to identify
itself.
[数字] [默认值: System time at process start or Network ID of forked
blockchain if configured.]
--chainId The Chain ID ganache-cli will use for
`eth_chainId` RPC and the `CHAINID` opcode.
[数字] [默认值: For legacy reasons, the default is currently `1337` for
`eth_chainId` RPC and `1` for the `CHAINID` opcode. This will be fixed in the
next major version of ganache-cli and ganache-core!]
-g, --gasPrice The price of gas in wei
[数字] [默认值: 20000000000]
-l, --gasLimit The block gas limit in wei
[数字] [默认值: 6721975]
--callGasLimit Sets the transaction gas limit for `eth_call`
and `eth_estimateGas` calls. Must be specified
as a hex string. Defaults to "0x1fffffffffffff"
(Number.MAX_SAFE_INTEGER)
[数字] [默认值: 9007199254740991]
--allowUnlimitedContractSize Allows unlimited contract sizes while debugging.
By enabling this flag, the check within the EVM
for contract size limit of 24KB (see EIP-170) is
bypassed. Enabling this flag *will* cause
ganache-cli to behave differently than
production environments. [布尔] [默认值: false]
-t, --time Date (ISO 8601) that the first block should
start. Use this feature, along with the
evm_increaseTime method to test time-dependent
code. [字符串]
Other:
--debug Output VM opcodes for debugging [布尔] [默认值: false]
-v, --verbose Log all requests and responses to stdout [布尔] [默认值: false]
--mem Only show memory output, not tx history [布尔] [默认值: false]
-q, --quiet Run ganache quietly (no logs) [布尔] [默认值: false]
选项:
--help, -? 显示帮助信息 [布尔]
--version 显示版本号 [布尔]
// Voting.sol
// SPDX-License-Identifier: MIT
pragma solidity >0.4.22;
contract Voting {
// 状态变量
mapping (bytes32=>uint8) public votesReceived;
bytes32[] public candidateList;
// bool isValid; 在程序设计的安全性上有问题
constructor(bytes32[] memory candidateNames) {
candidateList = candidateNames;
}
// modifier validateCandidate(bytes32 candidateName){
// for(uint8 i=0;i<candidateList.length;i++){
// if(candidateName == candidateList[i]){
// isValid = true;
// }
// }
// ifValid = false;
// _;
// }
function totalVotesFor(bytes32 candidate) view public returns (uint8){
require(validCandidate(candidate));
return votesReceived[candidate];
}
// 投票
function voteForCandidate(bytes32 candidate) public {
require(validCandidate(candidate));
votesReceived[candidate] +=1;
}
function validCandidate(bytes32 candidate) view public returns (bool){
// candidateList 是变长数组,for不太好
for(uint i=0; i<candidateList.length; i++){
if(candidateList[i] ==candidate){
return true;
}
}
return false;
}
}
/// compile.js 读取contract .sol文件
const path = require("path");
const fs = require("fs");
const solc = require("solc");
const votingPath = path.resolve(__dirname, "contracts", "Voting.sol");
const source = fs.readFileSync(votingPath, "utf8");
const input = {
language: "Solidity",
sources: {
"Voting.sol": {
content: source,
},
},
settings: {
outputSelection: {
"*": {
"*": ["*"],
},
},
},
};
const output = JSON.parse(solc.compile(JSON.stringify(input)));
Object.keys(output.contracts).forEach((symbol) => {
Object.keys(output.contracts[symbol]).forEach((name) => {
const contractName = name.replace(/^:/, "");
const filePath = path.resolve(
__dirname,
"./compiled",
`${contractName}.json`
);
fs.unlink(filePath, (err) => {
if (!err) {
fs.writeFile(
filePath,
JSON.stringify(output.contracts[symbol][name]),
"utf8",
(err) => {
if (err) {
console.log(err);
} else {
console.log(`save compiled`);
}
}
);
}
});
});
});
module.exports = output.contracts["Voting.sol"].Voting;
/// create_contract.js 创建 及 使用 合约
const Web3 = require("web3");
const compiledFile = require("./compile");
const web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545"));
const accounts = web3.eth.accounts;
const abiDefinition = compiledFile.abi;
const VotingContract = web3.eth.contract(abiDefinition);
const byteCode = compiledFile.evm.bytecode.object;
// // VotingContract.new 将一个合约部署到区块链
// const deployedContract = VotingContract.new(["Alice", "Bob", "Cary"], {
// data: byteCode, // 编译后部署到区块链上的字节码
// from: web3.eth.accounts[0], // 谁部署了这个合约
// gas: 4700000, // 需要花费的金额
// });
// const deployedContractAddress = deployedContract.address;
// console.log(deployedContractAddress);
// 0x69917f5f485468f082b63e48038ba4c62a3af99c
const contractInstance = VotingContract.at(
"0x69917f5f485468f082b63e48038ba4c62a3af99c"
);
var _from = accounts[0];
// web3.personal.unlockAccount(_from, "123456", 2000000);
// 必须,要不然报 Invalid Address
web3.eth.defaultAccount = _from;
let a = contractInstance.totalVotesFor.call("Alice").toLocaleString();
console.log(a);
contractInstance.voteForCandidate("Alice", { from: web3.eth.accounts[0] });
a = contractInstance.totalVotesFor.call("Alice").toLocaleString();
console.log(a);
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Voting DApp</title>
</head>
<body>
<h1>A Simple Voting Application</h1>
<table>
<thead>
<tr>
<th>Candidate</th>
<th>Votes</th>
</tr>
</thead>
<tbody>
<tr>
<td>Alice</td>
<td id="candidate-1">-</td>
</tr>
<tr>
<td>Bob</td>
<td id="candidate-2">-</td>
</tr>
<tr>
<td>Cary</td>
<td id="candidate-3">-</td>
</tr>
</tbody>
</table>
<input type="text" id="candidate" />
<a href="#" onclick="voteForCandidate()" class="btn btn-primary">Vote</a>
<script src="https://cdn.jsdelivr.net/gh/ethereum/web3.js/dist/web3.mi
n.js"></script>
<script src="https://code.jquery.com/jquery-3.1.1.slim.min.js"></script>
<script>
console.log(Web3.version); // 1.3.4
let userAccount;
let contractInstace;
const web3 = new Web3(
new Web3.providers.HttpProvider("http://localhost:8545")
);
const candidates = {
Alice: "candidate-1",
Bob: "candidate-2",
Cary: "candidate-3",
};
$(document).ready(function async() {
web3.eth.getAccounts().then((accounts) => {
userAccount = accounts;
const abi = JSON.parse(
'[{"inputs":[{"internalType":"bytes32[]","name":"candidateNames","type":"bytes32[]"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"candidateList","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"candidate","type":"bytes32"}],"name":"totalVotesFor","outputs":[{"internalType":"uint8","name":"","type":"uint8"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"candidate","type":"bytes32"}],"name":"validCandidate","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"candidate","type":"bytes32"}],"name":"voteForCandidate","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"name":"votesReceived","outputs":[{"internalType":"uint8","name":"","type":"uint8"}],"stateMutability":"view","type":"function"}]'
);
contractInstace = new web3.eth.Contract(
abi,
"0x69917f5f485468f082b63e48038ba4c62a3af99c",
{
from: userAccount[0],
gasPrice: "20000000000",
}
);
const candidateNames = Object.keys(candidates);
for (var i = 0; i < candidateNames.length; i++) {
(function(i) {
let name = candidateNames[i];
contractInstace.methods
.totalVotesFor(web3.utils.fromAscii(name, 32))
.call()
.then((val) => {
$(`#candidate-${i + 1}`).html(val.toString());
});
})(i);
}
});
});
async function voteForCandidate() {
const candidateName = $("#candidate").val();
console.log(candidateName);
try {
contractInstace.methods
.voteForCandidate(web3.utils.fromAscii(candidateName, 32))
.send({
from: web3.eth.accounts[0],
})
.on("error", (error) => {})
.on("transactionHash", (transactionHash) => {
console.log(transactionHash);
// })
// .then(function (receipt) {
// console.log(receipt);
let div_id = candidates[candidateName];
contractInstace.methods
.totalVotesFor(web3.utils.fromAscii(candidateName, 32))
.call()
.then((val) => {
console.log(candidateName, div_id);
console.log(val);
$(`#${div_id}`).html(val);
});
});
} catch (err) {
console.log(err);
}
}
</script>
</body>
</html>
// server.js
var http = require("http");
var fs = require("fs");
var url = require("url");
var path = require("path");
http
.createServer(function(req, res) {
var pathName = url.parse(req.url).pathname;
console.log("request for: " + pathName + " received. ");
const _path = path.resolve(__dirname, pathName.substr(1));
console.log(_path);
var data = fs.readFileSync(_path, "utf-8");
console.log(!111);
res.writeHead(200, { "Content-Type": "text/html" });
res.write(data.toString());
res.end();
})
.listen(3000);
// web3.js
const Web3 = require("web3");
const web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545"));
const path = require("path");
const filePath = path.resolve(__dirname, "./compiled/Voting.json");
const {
abi,
evm: {
bytecode: { object: mbyteCode },
},
} = require(filePath);
(async () => {
let accounts = await web3.eth.getAccounts();
console.time("deploy time");
//
let result = await new web3.eth.Contract(abi)
.deploy({
data: mbyteCode,
arguments: [
[
web3.utils.fromAscii("Alice", 32),
web3.utils.fromAscii("Bob", 32),
web3.utils.fromAscii("Cary", 32),
],
],
})
.send({
from: accounts[0],
gas: "1000000",
});
console.timeEnd("deploy time end");
console.log("合约部署成功:", result.options.address);
})();
参考地址 https://github.com/XiangXaoLong/EthereumWorkshop/tree/43d4d11cea737780a7baaac8ba70ad0dbc466c03/DApp