以太坊搭建去中心化的投票系统(二)开发
原文链接:http://skycoin.top/index.php/archives/19/
编写solidity合约
如果你熟悉面向对象编程,学习用 solidity 写合约应该非常简单。我们会写一个叫做 Voting 的合约(可以把合约看成是面对对象编程语言的一个类),这个合约有以下内容:
- 一个构造函数,用来初始化一些候选者。
- 一个用来投票的方法(对投票数加 1)
- 一个返回候选者所获得的总票数的方法
当你把合约部署到区块链的时候,就会调用构造函数,并只调用一次。与 web 世界里每次部署代码都会覆盖旧代码不同,在区块链上部署的合约是不可改变的,也就是说,如果你更新合约并再次部署,旧的合约仍然会在区块链上存在,并且数据仍在。新的部署将会创建合约的一个新的实例。
solidity 的详细介绍可以在这里找到 http://solidity.readthedocs.io/en/latest/index.html
新建voting.sol ,并写入一下代码
pragma solidity ^0.4.18;
contract Voting {
mapping (bytes32 => uint8) public votesReceived;
//在很多编程语言中,仅仅通过 votesReceived.keys 就可以获取所有的候选者姓名。但是,但是在 solidity 中没有这样的方法,所以我们必须单独管理一个候选者数组 candidateLis
bytes32[] public candidateList;
function Voting(bytes32[] candidateNames) public {
candidateList = candidateNames;
}
function totalVotesFor(bytes32 candidate) view public returns (uint8) {
require(validCandidate(candidate));
return votesReceived[candidate];
}
function voteForCandidate(bytes32 candidate) public {
require(validCandidate(candidate));
//votesReceived[key] 有一个默认值 0,所以你不需要将其初始化为 0,直接加1 即可
votesReceived[candidate] += 1;
}
function validCandidate(bytes32 candidate) view public returns (bool) {
for(uint i = 0; i < candidateList.length; i++) {
if (candidateList[i] == candidate) {
return true;
}
}
return false;
}
}
你也会注意到每个函数有个可见性说明符(visibility specifier)(比如本例中的 public)。这意味着,函数可以从合约外调用。如果你不想要其他任何人调用这个函数,你可以把它设置为私有(private)函数。如果你不指定可见性,编译器会抛出一个警告。最近 solidity 编译器进行了一些改进,如果用户忘记了对私有函数进行标记导致了外部可以调用私有函数,编译器会捕获这个问题
你也会在一些函数上看到一个修饰符 view。它通常用来告诉编译器函数是只读的(也就是说,调用该函数,区块链状态并不会更新)
编译合约
node
In the node console
> Web3 = require('web3')
> web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545"));
> web3.eth.accounts
['0x5c252a0c0475f9711b56ab160a1999729eccce97'
'0x353d310bed379b2d1df3b727645e200997016ba3'
'0xa3ddc09b5e49d654a43e161cae3f865261cabd23'
'0xa8a188c6d97ec8cf905cc1dd1cd318e887249ec5'
'0xc0aa5f8b79db71335dacc7cd116f357d7ecd2798'
'0xda695959ff85f0581ca924e549567390a0034058'
'0xd4ee63452555a87048dcfe2a039208d113323790'
'0xc60c8a7b752d38e35e0359e25a2e0f6692b10d14'
'0xba7ec95286334e8634e89760fab8d2ec1226bf42'
'0x208e02303fe29be3698732e92ca32b88d80a2d36']
> code = fs.readFileSync('Voting.sol').toString()
> solc = require('solc')
> compiledCode = solc.compile(code)
我们将会使用上一节安装的 solc 库来编译代码。如果你还记得的话,之前我们提到过 web3js 是一个库,它能够让你通过 RPC 与区块链进行交互。我们将会在 node 控制台里用这个库部署合约,并与区块链进行交互。
编译合约
首先,在终端中运行 node
进入 node 控制台,初始化 web3 对象,并向区块链查询获取所有的账户。
确保与此同时
ganache
已经在另一个窗口中运行
为了编译合约,先从 Voting.sol
中加载代码并绑定到一个 string 类型的变量,然后像右边这样对合约进行编译。
当你成功地编译好合约,打印 compiledCode
对象(直接在 node 控制台输入 compiledCode 就可以看到内容),你会注意到有两个重要的字段,它们很重要,你必须要理解:
-
compiledCode.contracts[':Voting'].bytecode
: 这就是Voting.sol
编译好后的字节码。也是要部署到区块链上的代码。 -
compiledCode.contracts[':Voting'].interface
: 这是一个合约的接口或者说模板(叫做 abi 定义),它告诉了用户在这个合约里有哪些方法。在未来无论何时你想要跟任意一个合约进行交互,你都会需要这个 abi 定义。你可以在这里 看到 ABI 的更多内容。
在接下来的几节课,我们将会使用 truffle 框架来管理编译和与区块链的交互。但是,在使用任何框架之前,深入了解它的工作方式还是大有裨益的,因为框架会将这些内容抽象出去。
部署
Execute this in your node console:
> abiDefinition = JSON.parse(compiledCode.contracts[':Voting'].interface)
> VotingContract = web3.eth.contract(abiDefinition)
> byteCode = compiledCode.contracts[':Voting'].bytecode
> deployedContract = VotingContract.new(['Rama','Nick','Jose'],{data: byteCode, from: web3.eth.accounts[0], gas: 4700000})
> deployedContract.address
'0x0396d2b97871144f75ba9a9c8ae12bf6c019f610' <- Your address will be different
> contractInstance = VotingContract.at(deployedContract.address)
先通过传入 abi 定义来创建一个合约对象 VotingContract。然后用这个对象在链上部署并初始化合约。
VotingContract.new 将合约部署到区块链。
第一个参数是一个候选者数组,候选者们会竞争选举,这很容易理解。让我们来看一下第二个参数里面都是些什么:
data: 这是我们编译后部署到区块链上的字节码。
from: 区块链必须跟踪是谁部署了这个合约。在这种情况下,我们仅仅是从调用 web3.eth.accounts 返回的第一个账户,作为部署这个合约的账户。记住,web3.eth.accounts 返回一个 ganache 所创建10 个测试账号的数组。在交易之前,你必须拥有这个账户,并对其解锁。创建一个账户时,你会被要求输入一个密码,这就是你用来证明你对账户所有权的东西。在下一节,我们将会进行详细介绍。为了方便起见,ganache 默认会解锁 10 个账户。
gas: 与区块链进行交互需要花费金钱。这笔钱用来付给矿工,因为他们帮你把代码包含了在区块链里面。你必须指定你愿意花费多少钱让你的代码包含在区块链中,也就是设定 “gas” 的值。你的 “from” 账户里面的 ETH 余额将会被用来购买 gas。gas 的价格由网络设定。
我们已经部署了合约,并有了一个合约实例(变量 contractInstance),我们可以用这个实例与合约进行交互。
在区块链上有上千个合约。那么,如何识别你的合约已经上链了呢?
回答:deployedContract.address. 当你需要跟合约进行交互时,就需要这个部署地址和我们之前谈到的 abi 定义
用控制台与合约交互
In your node console:
> contractInstance.totalVotesFor.call('Rama')
{ [String: '0'] s: 1, e: 0, c: [ 0 ] }
> contractInstance.voteForCandidate('Rama', {from: web3.eth.accounts[0]})
'0xdedc7ae544c3dde74ab5a0b07422c5a51b5240603d31074f5b75c0ebc786bf53'
> contractInstance.voteForCandidate('Rama', {from: web3.eth.accounts[0]})
'0x02c054d238038d68b65d55770fabfca592a5cf6590229ab91bbe7cd72da46de9'
> contractInstance.voteForCandidate('Rama', {from: web3.eth.accounts[0]})
'0x3da069a09577514f2baaa11bc3015a16edf26aad28dffbcd126bde2e71f2b76f'
> contractInstance.totalVotesFor.call('Rama').toLocaleString()
'3'
{ [String: '0'] s: 1, e: 0, c: [ 0 ] } 是数字 0 的科学计数法表示. 你可以在这里了解更多 科学计数法.
在你的 node 控制台里调用 voteForCandidate 和 totalVotesFor 方法并查看结果。
每为一位候选者投一次票,你就会得到一个交易 id:
比如:
‘0xdedc7ae544c3dde74ab5a0b07422c5a51b5240603d31074f5b75c0ebc786bf53’。这个交易 id 就是交易发生的凭据,你可以在将来的任何时候引用这笔交易。这笔交易是不可改变的
网页与合约交互
至此,大部分的工作都已完成,我们还需要做的事情就是创建一个简单的 html,里面有候选者姓名并调用投票命令(我们已经在 nodejs 控制台里试过)。你可以在右侧找到 html 代码和 js 代码。将它们放到 chapter1 目录,并在浏览器中打开 index.html。
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'>
</head>
<body class="container">
<h1>A Simple Hello World Voting Application</h1>
<div class="table-responsive">
<table class="table table-bordered">
<thead>
<tr>
<th>Candidate</th>
<th>Votes</th>
</tr>
</thead>
<tbody>
<tr>
<td>Rama</td>
<td id="candidate-1"></td>
</tr>
<tr>
<td>Nick</td>
<td id="candidate-2"></td>
</tr>
<tr>
<td>Jose</td>
<td id="candidate-3"></td>
</tr>
</tbody>
</table>
</div>
<input type="text" id="candidate" />
<a href="#" onclick="voteForCandidate()" class="btn btn-primary">Vote</a>
</body>
<script src="https://cdn.rawgit.com/ethereum/web3.js/develop/dist/web3.js"></script>
<script src="https://code.jquery.com/jquery-3.1.1.slim.min.js"></script>
<script src="./index.js"></script>
</html>
为了简化项目,我们已经硬编码了候选者姓名。如果你喜欢的话,可以调整代码使其动态选择候选者。
index.js
web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545"));
abi = JSON.parse('[{"constant":false,"inputs":[{"name":"candidate","type":"bytes32"}],"name":"totalVotesFor","outputs":[{"name":"","type":"uint8"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"candidate","type":"bytes32"}],"name":"validCandidate","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"","type":"bytes32"}],"name":"votesReceived","outputs":[{"name":"","type":"uint8"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"x","type":"bytes32"}],"name":"bytes32ToString","outputs":[{"name":"","type":"string"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"","type":"uint256"}],"name":"candidateList","outputs":[{"name":"","type":"bytes32"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"candidate","type":"bytes32"}],"name":"voteForCandidate","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"contractOwner","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"inputs":[{"name":"candidateNames","type":"bytes32[]"}],"payable":false,"type":"constructor"}]')
VotingContract = web3.eth.contract(abi);
contractInstance = VotingContract.at('0x329f5c190380ebcf640a90d06eb1db2d68503a53');
candidates = {"Rama": "candidate-1", "Nick": "candidate-2", "Jose": "candidate-3"}
function voteForCandidate(candidate) {
candidateName = $("#candidate").val();
try {
contractInstance.voteForCandidate(candidateName, {from: web3.eth.accounts[0]}, function() {
let div_id = candidates[candidateName];
$("#"+div_id).html(contractInstance.totalVotesFor.call(candidateName).toString());
});
} catch (err) {
}
}
$(document).ready(function() {
candidateNames = Object.keys(candidates);
for (var i = 0; i < candidateNames.length; i++) {
let name = candidateNames[i];
let val = contractInstance.totalVotesFor.call(name).toString()
$("#"+candidates[name]).html(val);
}
});
在第 4 行,用你自己的合约地址替换代码中的合约地址。合约地址可见 4.4 节,deployedContract.address
如果一切顺利的话,你应该能够在文本框中输入候选者姓名,然后投票数应该加 1 。
总结
总结一下,下面是你到目前为止已经完成的事情:
- 通过安装 node, npm 和 ganache,你已经配置好了开发环境。
- 你编码了一个简单的投票合约,编译并部署到区块链上。
- 你通过 nodejs 控制台与网页与合约进行了交互。
原文链接:http://skycoin.top/index.php/archives/19/