Ethereum Dapp Tutorial — Part 1

  1. Part 0
  2. Part 1
  3. Part 2
  4. Part 3

上一篇文章中,通过和传统的 web程序相比较解释了以太坊平台的结构。作为一个开发者,学习新技术的最好的方式就是构建一个玩具程序。

在这篇文章中构建一个简单的“hello word”程序,这个程序是一个投票程序。

这个程序非常简单,包括:初始化一个参加者集合让任何人为候选人投票显示每一个候选人获得的投票数。我们的目的不仅仅是编写一个应用,目的是学习应用编译,部署,交互的过程。

总的来说这章是对上一篇文章的延续,如果你是刚接触Ethereum,我建议你最好读一读上一篇文章。

练习目的:

  1. 搭建开发环境
  2. 学习在开发环境下编写,编译,部署合约。
  3. 在区块链上通过node.js控制台利用合约进行交互。
  4. 通过一个简单的web页面利用合约来交互,通过这个页面显示投票数,以及每个候选人的获得的投票数。

整个应用部署在ubuntu 16.04上,在macos上也行。

构建应用程序



搭建开发环境

这里不是基于活跃的区块链的开发app,而是使用一个叫做testrpc的内存区块链。在Part 2中,我们将会在真正的区块链上进行交互,
下面来安装testrpc,web3js以及在linux环境中启动一个测试区块链。对于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个假的以太网节点。

简单的投票合约

用solidity语言来编写合约。

编写的智能合约叫做Voting(在你熟悉的面相对象语言中想象合约就是一个类),Voting有一个初始化候选人的数组结构。
有两个方法,一个是返回候选人获得的总选票,另一个是给候选人加票的方法。

注意:把合约部署到区块链上的时候,构造函数只能被调用一次,和web世界不同,web世界中你的代码部署的时候你可以使用新代码来覆盖以前的老代码,但是在区块链上部署的代码是不可更改的。如果你更新合约重新部署代码,旧的合约以及数据依然在区块链上。新部署的将会创建一个新的合约实例。

下面是一个投票合约的代码,每一行都有注释:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
pragma solidity ^0.4.11;
// 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] += 1;
}

function validCandidate(bytes32 candidate) returns (bool) {
for(uint i = 0; i < candidateList.length; i++) {
if (candidateList[i] == candidate) {
return true;
}
}
return false;
}
}

拷贝下面代码到hello_world_voting目录的文件Voting.sol文件中,现在编译代码,并把它部署到testrps上面。

编译solidity代码,首先要通过npm安装npm modulesolc

1
npm install solc

用带有node.js控制台的包,来编译智能合约,从上一章知道,web3js是一个让你通过RPC来和区块链交互的包。
web3js这个包来进行部署与交互。

首先,在控制台运行node命令来调用node控制台,并初始化solc和web3js对象。下面所有的代码片段,都需要在node的交互式环境下。

1
2
3
4
$ node

> Web3 = require('web3')
> web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545"));

为了确保web3js对象被初始化了,并且可以和区块链通信,查询所有在区块链上的账户。你会看到类似下面的输出结果:

1
2
3
4
5
6
7
8
9
10
11
> web3.eth.accounts
['0x9c02f5c68e02390a3ab81f63341edc1ba5dbb39e',
'0x7d920be073e92a590dc47e4ccea2f28db3f218cc',
'0xf8a9c7c65c4d1c0c21b06c06ee5da80bd8f074a9',
'0x9d8ee8c3d4f8b1e08803da274bdaff80c2204fc6',
'0x26bb5d139aa7bdb1380af0e1e8f98147ef4c406a',
'0x622e557aad13c36459fac83240f25ae91882127c',
'0xbf8b1630d5640e272f33653e83092ce33d302fd2',
'0xe37a3157cb3081ea7a96ba9f9e942c72cf7ad87b',
'0x175dae81345f36775db285d368f0b1d49f61b2f8',
'0xc26bda5f3370bdd46e7c84bdb909aead4d8f35f3']

编译智能合约:通过从Voting.sol文件中加载智能合约到一个字符串变量中,然后编译。

1
2
3
> code = fs.readFileSync('Voting.sol').toString()
> solc = require('solc')
> compiledCode = solc.compile(code)

当你成功编译了代码,打印了合约对象(仅仅是在控制台中查看到的上面compiledCode类型的内容),你会发现这里有两个重要的字段,理解他们十分重要:

  1. compiledCode.contracts[‘:Voting’].bytecodeVoting.sol编译得到的是二进制代码。这个就是将要部署到区块链上的代码。
  2. compiledCode.contracts[‘:Voting’].interface:这是一个智能合约接口或者是智能合约模版(叫做abi),他告诉合约使用者,合约中可以使用的方法。
    在将来无论你在什么时候要与智能合约交互,你都会用到这个abi的定义。你可以在这里查看更多关于abi的详细描述

现在来部署智能合约。首先你要创建一个合约对象(下面的VotingContract),这个合约对象用来在区块链上部署和初始化合约。

1
2
3
4
5
6
> 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
> 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的描述。

在nodes控制台和合约进行交互

1
2
3
4
5
6
7
8
9
10
> 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'

在你的node交互控制台上试试上面的命令,你将会看到投票的数量增加。每次当你为一个候选人投票,你就会获得一个交易的ID:
例如:上面的: '0xdedc7ae544c3dde74ab5a0b07422c5a51b5240603d31074f5b75c0ebc786bf53' 这个交易ID是交易发生的证据。
将来你可以在任何时间来返回去查看他(数据可追踪)。这个交易是不可更改的。这种不可更改的特性是以太坊这种区块链的很大优势之一。
在接下来的教程中,会用其不可更改性来构建应用。

web页面链接区块链和投票

现在所有的工作都完成了,现在要做的就是构建一个包含候选人的简单的html文件。
并在一个js文件中调用投票命令(这个投票命令已经在前面的node控制台中测试过了)。下面你会看到html代码和js文件。

hello_world_voting文件夹下复制,并在你的浏览器中打开index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<!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文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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文件。



如果你可以进入上面文本框的候选人名字,并投票并且会看到投票增加。你已经成功构建了你的第一个应用。Congratulation!

总结

  • 搭建环境
  • 编写简单合约
  • 编译及部署合约到区块链上
  • 能够通过nodejs控制台进行交互,同时也能通过web页面进行同样交互。

Part 2会部署这个合约到一个公共的测试网络,这样整个世界都会看到并给候选人进行投票。

Part 2使用truffle框架来开发(不要使用node的控制台来管理整个过程)。

参考:
https://medium.com/@mvmurthy/full-stack-hello-world-voting-ethereum-dapp-tutorial-part-1-40d2d0d807c2

当前网速较慢或者你使用的浏览器不支持博客特定功能,请尝试刷新或换用Chrome、Firefox等现代浏览器