在上一篇文章中,通过和传统的 web程序相比较解释了以太坊平台的结构。作为一个开发者,学习新技术的最好的方式就是构建一个玩具程序。
在这篇文章中我们将会构建一个简单的“hello word”程序,这个程序是一个投票程序。
这个程序非常简单,包括:初始化一个参加者集合,让任何人为候选人投票,显示每一个候选人获得的投票数。我们的目的不仅仅是编写一个应用,我们的目的是学习应用编译,部署,交互的过程。
我故意的避免使用任何Dapp框架来构建这个应用,因为框架抽象了很多细节,这样你就不能够很好的理解系统的细节。而且,当你使用框架的时候,会评估这个框架给你解决的繁重的工作。
总的来说这章是对上一篇文章的延续,如果你是刚接触Ethereum,我建议你最好读一读上一篇文章。
我们本次练习的目的:
1,搭建开发环境
2,学习在开发环境下编写,编译,部署合约。
3,在区块链上通过node.js控制台利用合约进行交互。
4,通过一个简单的web页面利用合约来交互,通过这个页面显示投票数,以及每个候选人的获得的投票数。
整个应用部署与构建在一个新的ubuntu 16.04 机器上,我在macos上也很好的启动运行这个程序。
下面是形象化构建我们现在的应用程序的方式。
1,搭建开发环境:
我们这里不是基于活跃的区块链的开发app,而是使用一个叫做testrpc的内存区块链。在第二部分的教程中,我们将会在真正的区块链上进行交互,
下面来安装testrpc,web3js以及在linux环境中启动一个测试区块链。这个结构同样可以完全运行在在macos系统下。
对于windows用户来说你可以使用下面的方式:https://medium.com/@PrateeshNanada/steps-to-install-testrpc-in-windows-10-96989a6cd594
注意:这个教程当前工作的web3js的版本是0.20.1,通常我们会运行
npm install ethereumjs-testrpc web3@0.20.1
而不是运行
npm install ethereumjs-testrpc web3
,在web3js的1.0文档版发布之后我会更新这个教程。
注意testrpc在自动运行的时候会自动创建10个测试帐号。这些帐号都预装了100个假的以太网节点。
2,简单的投票合约:
我们使用solidity语言来编写我们的合约,如果你对面向对象语言熟悉的话,学solidity编写智能合约将会很容易。
我们编写的智能合约叫做Voting(在你熟悉的面相对象语言中想象合约就是一个类),Voting有一个初始化候选人的数组结构。
我们将会写两个方法,一个是返回候选人获得的总选票,另一个是给候选人加票的方法。
注意:当我们把合约部署到区块链上的时候,构造函数只能被调用一次,和web世界不同,web世界中你的代码部署的时候你可以使用新代码来覆盖以前的老代码,
但是在区块链上部署的代码是不可更改的。如果你更新合约重新部署代码,旧的合约以及数据依然在区块链上。新部署的将会创建一个新的合约实例。
下面是一个投票合约的代码,每一行都有注释:
pragma solidity ^0.4.;
// We have to specify what version of compiler this code will compile with contract Voting {
/* mapping field below is equivalent to an associative array or hash.
The key of the mapping is candidate name stored as type bytes32 and value is
an unsigned integer to store the vote count
*/ mapping (bytes32 => uint8) public votesReceived; /* Solidity doesn't let you pass in an array of strings in the constructor (yet).
We will use an array of bytes32 instead to store the list of candidates
*/ bytes32[] public candidateList; /* This is the constructor which will be called once when you
deploy the contract to the blockchain. When we deploy the contract,
we will pass an array of candidates who will be contesting in the election
*/
function Voting(bytes32[] candidateNames) {
candidateList = candidateNames;
} // This function returns the total votes a candidate has received so far
function totalVotesFor(bytes32 candidate) returns (uint8) {
if (validCandidate(candidate) == false) throw;
return votesReceived[candidate];
} // This function increments the vote count for the specified candidate. This
// is equivalent to casting a vote
function voteForCandidate(bytes32 candidate) {
if (validCandidate(candidate) == false) throw;
votesReceived[candidate] += ;
} function validCandidate(bytes32 candidate) returns (bool) {
for(uint i = ; i < candidateList.length; i++) {
if (candidateList[i] == candidate) {
return true;
}
}
return false;
}
}
拷贝下面代码到hello_world_voting目录的文件Voting.sol文件中,现在让我们来编译代码,并把它部署到testrps上面。
编译solidity代码,首先要通过npm安装npm module:solc
mahesh@projectblockchain:~/hello_world_voting$ npm install solc
我们将会使用这个带有node.js控制台的包,来编译我们的智能合约,从上一章我们知道,web3js是一个让你通过RPC来和区块链交互的包。
我们将会使用web3js这个包来进行部署与交互。
首先,在控制台运行node命令来调用node控制台,并初始化solc和web3js对象。下面所有的代码片段,都需要在node的交互式环境下。
mahesh@projectblockchain:~/hello_world_voting$ node > Web3 = require('web3')
> web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545"));
为了确保web3js对象被初始化了,并且可以和区块链通信,让我们来查询所有在区块链上的账户。你会看到类似下面的输出结果:
> web3.eth.accounts
['0x9c02f5c68e02390a3ab81f63341edc1ba5dbb39e',
'0x7d920be073e92a590dc47e4ccea2f28db3f218cc',
'0xf8a9c7c65c4d1c0c21b06c06ee5da80bd8f074a9',
'0x9d8ee8c3d4f8b1e08803da274bdaff80c2204fc6',
'0x26bb5d139aa7bdb1380af0e1e8f98147ef4c406a',
'0x622e557aad13c36459fac83240f25ae91882127c',
'0xbf8b1630d5640e272f33653e83092ce33d302fd2',
'0xe37a3157cb3081ea7a96ba9f9e942c72cf7ad87b',
'0x175dae81345f36775db285d368f0b1d49f61b2f8',
'0xc26bda5f3370bdd46e7c84bdb909aead4d8f35f3']
编译智能合约:通过从Voting.sol文件中加载智能合约到一个字符串变量中,然后编译他。
> code = fs.readFileSync('Voting.sol').toString()
> solc = require('solc')
> compiledCode = solc.compile(code)
当你成功编译了代码,打印了合约对象(仅仅是在控制台中查看到的上面compiledCode类型的内容),你会发现这里有两个重要的字段,理解他们十分重要:
1,compiledCode.contracts[‘:Voting’].bytecode:当源文件Voting.sol被编译,我们得到的是二进制代码。这个就是将要部署到区块链上的代码。
2,compiledCode.contracts[‘:Voting’].interface:这是一个智能合约接口或者是智能合约模版(叫做abi),他告诉合约使用者,合约中可以使用的方法。
在将来无论你在什么时候要与智能合约交互,你都会用到这个abi的定义。你可以在这里查看更多关于 abi的详细描述:https://github.com/ethereum/wiki/wiki/Ethereum-Contract-ABI
现在我们来部署智能合约。首先你要创建一个合约对象(下面的VotingContract),这个合约对象用来在区块链上部署和初始化合约。
> 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[], gas: })
> deployedContract.address
> contractInstance = VotingContract.at(deployedContract.address)
上面的VotingContract.new用来在区块链上部署智能合约。第一个参数是候选人数组,这些候选人在选举竞争中都是相对简单的。让我们来看一下第二个参数的hash中有什么内容:
1,data:这是一个我们在区块链上部署的编译后的二进制代码。
2,from:区块链必须记录谁部署了这个智能合约。在这个例子中我们选择第一个账户来作为这个智能合约的拥有者(将会部署这个合约到区块链上)。
这第一个账户我们是通过调用web3.eth.accounts来获取的。记得上面代码web3.eth.accounts返回一个数组,数组里面包含10个由testrpc创建的测试账户,
这10个账户是当我们在启动测试区块链的时候创建的。在真实活跃的区块链中,在没创建之前,你不能使用任何账户。你必须在交易(通信/交流)前拥有这个账户,并解锁他。
当你在创建账户的时候被要求填写密码,这个密码用来证明你和账户的关系。为了方便testrpc默认解锁了10个账户。
3,gas:和区块链交互花费的钱,这些钱是给矿工的,矿工的所有工作是在区块链上引入你的代码。你必须指定你将会支付多少钱给把你的代码包含到区块链上的人。
这些钱就是通过设置gas的值来指定的。你的上面代码from中的账户的以太坊余额可以用来购买gas。gas的价格由网络来设定。
现在我们已经部署了智能合约并有了一个合约实例(上面的contractInstance变量)。我们可以使用这个合约来进行交互。
区块链上有成千上万的合约部署在上面。但是,在区块链上怎么辨别自己的合约呢?答案就是deployedContract.address。
当你利用你的合约进行交互的时候,你需要这个部署地址,以及我们上面提到的abi的描述。
3,在nodes控制台和合约进行交互
> contractInstance.totalVotesFor.call('Rama')
{ [String: ''] s: , e: , c: [ ] }
> contractInstance.voteForCandidate('Rama', {from: web3.eth.accounts[]})
'0xdedc7ae544c3dde74ab5a0b07422c5a51b5240603d31074f5b75c0ebc786bf53'
> contractInstance.voteForCandidate('Rama', {from: web3.eth.accounts[]})
'0x02c054d238038d68b65d55770fabfca592a5cf6590229ab91bbe7cd72da46de9'
> contractInstance.voteForCandidate('Rama', {from: web3.eth.accounts[]})
'0x3da069a09577514f2baaa11bc3015a16edf26aad28dffbcd126bde2e71f2b76f'
> contractInstance.totalVotesFor.call('Rama').toLocaleString()
''
在你的node交互控制台上试试上面的命令,你将会看到投票的数量增加。每次当你为一个候选人投票,你就会获得一个交易的ID:
例如:上面的:‘0xdedc7ae544c3dde74ab5a0b07422c5a51b5240603d31074f5b75c0ebc786bf53’这个交易ID是交易发生的证据。
将来你可以在任何时间来返回去查看他(数据可追踪)。这个交易是不可更改的。这种不可更改的特性是以太坊这种区块链的很大优势之一。
在接下来的教程中,我们将会利用她的不可更改性来构建应用。
4,web页面链接区块链和投票
现在所有的工作都完成了,现在我们所要做的就是构建一个包含候选人的简单的html文件。
并在一个js文件中调用投票命令(这个投票命令我们已经在前面的node控制台中测试过了)。下面你会看到html代码和js文件。
在hello_world_voting文件件下复制他们,并在你的浏览器中打开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);
// In your nodejs console, execute contractInstance.address to get the address at which the contract is deployed and change the line below to use your deployed address
contractInstance = VotingContract.at('0x2a9c1d265d06d47e8f7b00ffa987c9185aecf672');
candidates = {"Rama": "candidate-1", "Nick": "candidate-2", "Jose": "candidate-3"} function voteForCandidate() {
candidateName = $("#candidate").val();
contractInstance.voteForCandidate(candidateName, {from: web3.eth.accounts[0]}, function() {
let div_id = candidates[candidateName];
$("#" + div_id).html(contractInstance.totalVotesFor.call(candidateName).toString());
});
} $(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);
}
});
不知道你是否记得前面我们说过和任何合约进行交互必须需要abi和地址。在上面的index.js文件中你将会看到他们是怎么使用合约来进行交互的
下面是你在浏览器中打开index.html文件。
如果你可以进入上面文本框的候选人名字,并投票并且会看到投票增加。你已经成功构建了你的第一个应用。恭喜。
总结:你搭建了你的环境,编写了你的简单合约,编译以及部署合约到区块链上。
能够通过nodejs控制台进行交互,同时也能通过web页面进行同样交互。
在第二章,我们将会部署这个合约到一个公共的测试网络,这样整个世界都会看到他。并给候选人进行投票。
我们都已经很熟悉了并使用truffle框架来开发(不要使用node的控制台来管理整个过程)。
希望这个教程能够帮助你在以太坊平台上开发去中心化的应用的时候提供指导意义。
翻译自:https://medium.com/@mvmurthy/full-stack-hello-world-voting-ethereum-dapp-tutorial-part-1-40d2d0d807c2