Ethereum Dapp Tutorial — Part 2

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

Part 1中,我们用ganache在开发环境中构建了一个简单的投票应用程序。现在在真正的区块链上获得这个应用程序。

以太坊有2个公共区块链。

  • Testnet(也叫Ropsten):这是一个测试区块链。可以把它看作一个QA或一个临时服务器,它仅用于测试目的。
  • Mainnet(也叫Homestead):这是全世界真实交易的区块链。在这个网络上使用以太网是有实际价值的。

学习目标:

  1. 安装geth用于下载区块链的客户端软件,并在本地计算机上运行以太坊节点
  2. 安装名为Truffle的Ethereum dapp框架,将用于编译和部署智能合约
  3. 对投票应用程序进行小小的更新,使其用Truffle
  4. 将合约编译并部署到Ropsten testnet
  5. 通过Truffle控制台,然后通过网页与合约进行交互

安装geth并同步区块链

安装非常简单:

在Mac上

1
2
$ brew tap ethereum / ethereum mahesh 
$ brew install ethereum

在Ubuntu上

1
2
3
4
$ sudo apt-get install software-properties-common 
$ sudo add-apt-repository -y ppa:ethereum / ethereum
$ sudo apt-get update
$ sudo apt-get install ethereum

在这有各种平台的安装说明:https://github.com/ethereum/go-ethereum/wiki/Building-Ethereum

安装geth后,在命令行控制台中运行下面的命令:

1
$ geth --testnet --syncmode "fast" --rpc --rpcapi db,eth,net,web3,personal --cache=1024  --rpcport 8545 --rpcaddr 127.0.0.1 --rpccorsdomain "*" --bootnodes "enode://20c9ad97c081d63397d7b685a412227a40e23c8bdc6688c6f37e97cfbc22d2b4d1db1510d8f61e6a8866ad7f0e17c02b14182d37ea7c3c8b9c2683aeb6b733a1@52.169.14.227:30303,enode://6ce05930c72abc632c58e2e4324f7c7ea478cec0ed4fa2528982cf34483094e9cbc9216e7aa349691242576d552a2a56aaeae426c5303ded677ce455ba1acd9d@13.84.180.240:30303"

这将启动以太坊节点,连接到其他对等节点并开始下载区块链。下载区块链所需的时间取决于各种因素,例如您的网络连接速度,计算机上的RAM,硬盘驱动器的类型等等。在一台拥有8GB RAM和50Mbps连接的计算机上花了我10-15分钟时间。

在您正在运行的控制台中,您将看到如下所示的输出。寻找粗体的块号。当区块链完全同步时,区块编号和此页面上的区块编号接近:https://ropsten.etherscan.io/

1
2
3
4
5
I0130 22:18:15.116332 core/blockchain.go:1064] imported   32 blocks,    49 txs (  6.256 Mg) in 185.716ms (33.688 Mg/s). #445097 [e1199364… / bce20913…]
I0130 22:18:20.267142 core/blockchain.go:1064] imported 1 blocks, 1 txs ( 0.239 Mg) in 11.379ms (20.963 Mg/s). #445097 [b4d77c46…]
I0130 22:18:21.059414 core/blockchain.go:1064] imported 1 blocks, 0 txs ( 0.000 Mg) in 7.807ms ( 0.000 Mg/s). #445098 [f990e694…]
I0130 22:18:34.367485 core/blockchain.go:1064] imported 1 blocks, 0 txs ( 0.000 Mg) in 4.599ms ( 0.000 Mg/s). #445099 [86b4f29a…]
I0130 22:18:42.953523 core/blockchain.go:1064] imported 1 blocks, 2 txs ( 0.294 Mg) in 9.149ms (32.136 Mg/s). #445100 [3572f223…]

安装Truffle框架

1
npm install -g truffle

建立voting合约

首先建立一个truffle project

1
2
3
4
5
6
7
8
9
10
11
12
13
$ mkdir voting
$ cd voting
$ npm install -g webpack
$ truffle unbox webpack
$ ls
README.md contracts node_modules test webpack.config.js truffle.js
app migrations package.json
$ ls app/
index.html javascripts stylesheets
$ ls contracts/
ConvertLib.sol MetaCoin.sol Migrations.sol
$ ls migrations/
1_initial_migration.js 2_deploy_contracts.js

truffle创建了运行dapp所需的必要文件和目录。truffle还创建了一个示例应用程序,让你快速上手(我们不会在本教程中使用它)。可以删除contracts目录中的ConvertLib.solMetaCoin.sol文件。

migrations目录很重要。这些migrations里文件用于将合约部署到区块链。(在上一篇文章中,我们使用了VotingContract.new将合约部署到区块链,现在不需要这样做)。

1_initial_migration.js将一个名为Migrations的合约部署到区块链中,并用于存储已部署的最新合约。每次运行migration时,truffle都会查询区块链以获取已部署的最后一个合约,然后部署尚未部署的任何合约。然后更新Migrations合约中的last_completed_migration字段,以指示部署的最新合约。可以简单地将其视为一个名为Migration的数据库表,其中名为last_completed_migration的列始终保持最新状态。更多

现在用上面的教程中所写的所有代码更新项目,下面将对其进行一些更改。

首先,将Voting.sol从前一个教程复制到contracts目录(这个文件没有改变)。

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.18;
// 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) public {
candidateList = candidateNames;
}

// This function returns the total votes a candidate has received so far
function totalVotesFor(bytes32 candidate) view public returns (uint8) {
require(validCandidate(candidate));
return votesReceived[candidate];
}

// This function increments the vote count for the specified candidate. This
// is equivalent to casting a vote
function voteForCandidate(bytes32 candidate) public {
require(validCandidate(candidate));
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;
}
}
1
2
$ ls contracts/
Migrations.sol Voting.sol

接下来,将migrations目录中的2_deploy_contracts.js的内容替换为以下内容:

1
2
3
4
var Voting = artifacts.require("./Voting.sol");
module.exports = function(deployer) {
deployer.deploy(Voting, ['Rama', 'Nick', 'Jose'], {gas: 6700000});
};

也可以在truffle.js中将气体值设置为全局设置。继续添加如下所示的gas选项,如果忘记将gas设置为特定的迁移文件,则默认使用全局值。

1
2
3
4
5
6
7
8
9
10
11
require('babel-register')
module.exports = {
networks: {
development: {
host: 'localhost',
port: 8545,
network_id: '*',
gas: 470000
}
}
}

用下面的内容替换app/javascripts/app.js的内容

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70

// Import the page's CSS. Webpack will know what to do with it.
import "../stylesheets/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 = {"Rama": "candidate-1", "Nick": "candidate-2", "Jose": "candidate-3"}

window.voteForCandidate = function(candidate) {
let candidateName = $("#candidate").val();
try {
$("#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("");

/* 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, {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("");
});
});
});
} catch (err) {
console.log(err);
}
}

$( 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);
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());
});
})
}
});

app/index.html的内容替换为以下内容。即使这个文件与上一章几乎相同,除了包含的js文件是41行的app.js

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
<!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 id="address"></div>
<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 id="msg"></div>
</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="app.js"></script>
</html>

将合同部署到Ropsten testnet

在部署合同之前需要一个账号和一些ether。当我们使用ganache时,它创建了10个测试帐户,并预装了100个测试用例。但是对于testnet和mainnet,我们必须创建帐户并自己添加一些ether。

在您的命令行终端中,执行以下操作:

1
2
3
4
5
6
$ truffle console
truffle(default)> web3.personal.newAccount('verystrongpassword')
'0x95a94979d86d9c32d1d2ab5ace2dcc8d1b446fa1'
truffle(default)> web3.eth.getBalance('0x95a94979d86d9c32d1d2ab5ace2dcc8d1b446fa1')
{ [String: '0'] s: 1, e: 0, c: [ 0 ] }
truffle(default)> web3.personal.unlockAccount('0x95a94979d86d9c32d1d2ab5ace2dcc8d1b446fa1', 'verystrongpassword', 15000)

在之前的文章中,我们启动了一个节点控制台并初始化了web3对象。当我们执行truffle控制台,所有这一切都为我们完成,我们得到一个web3对象准备使用。我们现在有一个地址为“0x95a94979d86d9c32d1d2ab5ace2dcc8d1b446fa1”的帐户(您将在您的情况下有不同的地址),余额将为0。

可以通过传递一个额外的option--mine来运行geth节点来挖掘一些ether。建议更简单是从reddit thread获得ether 或ping我,我给你一些。再次尝试web3.eth.getBalance,以确保有ether。也可以在ropsten.etherscan.io上输入地址以查看帐户余额

现在你已经有了一些了,继续编译并将合约部署到区块链。下面是运行命令和输出

  • 在部署合同之前,记得解锁账户
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ truffle migrate
Compiling Migrations.sol...
Compiling Voting.sol...
Writing artifacts to ./build/contracts
Running migration: 1_initial_migration.js
Deploying Migrations...
Migrations: 0x3cee101c94f8a06d549334372181bc5a7b3a8bee
Saving successful migration to network...
Saving artifacts...
Running migration: 2_deploy_contracts.js
Deploying Voting...
Voting: 0xd24a32f0ee12f5e9d233a2ebab5a53d4d4986203
Saving successful migration to network...
Saving artifacts...
$

与voting合约交互

成功部署合同后,现在应该能够获取投票计数并通过truffle控制台进行投票。

1
2
3
4
5
6
7
8
9
10
11
$ truffle console
truffle(default)> Voting.deployed().then(function(contractInstance) {contractInstance.voteForCandidate('Rama').then(function(v) {console.log(v)})})
// After a few seconds, you should see a transaction receipt like this:
receipt:
{ blockHash: '0x7229f668db0ac335cdd0c4c86e0394a35dd471a1095b8fafb52ebd7671433156',
blockNumber: 469628,
contractAddress: null,
....
....
truffle(default)> Voting.deployed().then(function(contractInstance) {contractInstance.totalVotesFor.call('Rama').then(function(v) {console.log(v)})})
{ [String: '1'] s: 1, e: 0, c: [ 1] }

现在启动服务器

1
$ npm run dev

应该在localhost:8080看到投票页面,并且能够投票并看到所有候选人的投票数量。由于我们正在处理一个真正的区块链,所以每次写入区块链(voteForCandidate)将需要几秒钟的时间(矿工必须将您的交易包括在区块中,区块中包含区块链)。



如果你看到这个页面并且能够投票,你就可以在公共测试网络上建立一个完整的以太坊应用程序,Congratulation!

由于所有的交易都是公开的,可以在这里查看:https://testnet.etherscan.io/。只需输入帐户地址,就会显示所有的交易与时间戳。

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