第一个 Dapp

配置开发环境

安装 Nodejs

Mac 上安装 nodejs:

1
brew install node

安装 Truffle

1
npm install -g truffle # http://truffleframework.com/

truffle 是一个用于在 Etherem 上开发 Dapp 的框架。它让我们能够用 solidity 编程语言去写 Dapp 并进行调试。

安装 Ganache

Ganache 是一个本地的 in memory blockchain,让我们用于测试自己编写的 Dapp

安装 Metamask google 插件

为了能够使用 ethereum blockchain,我们需要安装 Google Chrome 的 METAMASK 扩展插件。使用 METAMASK 就可以让我们连接到本地的 etherum blockchain (Ganache 创建的),并和我们的 smart contract 做交互。

MetaMask 能够是我们的 Chrome 浏览器变成一个 blockchain 的浏览器,一个连接到 Ethereum network 的浏览器。

快速部署 truffle 项目

truffle 给我们提供了模板(这里称作为 box),用于我们进行快速开发。所以只要使用 unbox 命令来解压模板,就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
mac@macs-macbook  ~/Code/blockchain/election  truffle unbox pet-shop
Downloading...
Unpacking...
Setting up...
Unbox successful. Sweet!

Commands:

Compile: truffle compile
Migrate: truffle migrate
Test contracts: truffle test
Run dev server: npm run dev

编写代码

示例1

分别在 contracts 和 migrations 文件夹创建如下代码:

:./contracts/Election.sol
1
2
3
4
5
6
7
8
9
pragma solidity ^0.4.11;
contract Election {
// Read cnadidate
string public candidate;
// Constructor
constructor() public {
candidate = "Candidate 1";
}
}
:./migrations/2_deploy_contracts.js
1
2
3
4
var Election = artifacts.require("./Election.sol");
module.exports = function(deployer) {
deployer.deploy(Election);
};

获取 smart contract 的 instance(实例)

Election 是我们定义的 contract 名。
首先使用如下命令,将我们定义的 smart contract 发布到 blockchain 中。

1
truffle migrate

需要注意的是,每次 deploy 一个 smart contract 都会消耗 ETH,这里消耗了 0,05 的 ETH

然后使用下面的命令,获取 smart contract 的一个 instance(实例):

1
2
3
4
5
6
7
mac@macs-macbook  ~/Code/blockchain/election  truffle console
truffle(development)> Election.deployed().then( function (instance) { app = instance })
undefined
truffle(development)> app.address
'0x139c2cafabda6bd79b41e0d484e5ad440adf4bcb'
truffle(development)> app.candidate()
'Candidate 1'

示例2

下面使用一个新的 smart contract 实现投票的 Dapp。

:./contracts/Election.sol
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
pragma solidity ^0.4.2;

contract Election {
// Model a Candidate
struct Candidate {
uint id;
string name;
uint voteCount;
}

// Store accounts that have voted
mapping(address => bool) public voters;
// Store Candidates
// Fetch Candidate
mapping(uint => Candidate) public candidates;
// Store Candidates Count
uint public candidatesCount;

// voted event
event votedEvent (
uint indexed _candidateId
);

function Election () public {
addCandidate("Tom");
addCandidate("Jerry");
}

function addCandidate (string _name) private {
candidatesCount ++;
candidates[candidatesCount] = Candidate(candidatesCount, _name, 0);
}

function vote (uint _candidateId) public {
// require that they havent voted before
require(!voters[msg.sender]);

// require a valid candidate
require(_candidateId > 0 && _candidateId <= candidatesCount);

// record that voter has voted
voters[msg.sender] = true;

// update candidate vote Count
candidates[_candidateId].voteCount ++;

// trigger voted event
votedEvent(_candidateId);
}
}

而后在 terminal 里面重新 deploy 一下该 contract。因为 contract 的代码被修改了,所以需要 reset。

1
truffle migrate --reset

之后重新进入 console 查询状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
mac@macs-macbook  ~/Code/blockchain/election  truffle console
truffle(development)> Election.deployed().then( function (instance) { app = instance })
undefined
truffle(development)> app.candidates(1)
[ BigNumber { s: 1, e: 0, c: [ 1 ] },
'Tom',
BigNumber { s: 1, e: 0, c: [ 0 ] } ]
truffle(development)> app.candidates(2)
[ BigNumber { s: 1, e: 0, c: [ 2 ] },
'Jerry',
BigNumber { s: 1, e: 0, c: [ 0 ] } ]
truffle(development)> app.candidates(99) # 这里因为没有 key 为99的 candidate,所以返回空
[ BigNumber { s: 1, e: 0, c: [ 0 ] },
'',
BigNumber { s: 1, e: 0, c: [ 0 ] } ]
truffle(development)> app.candidatesCount()
BigNumber { s: 1, e: 0, c: [ 2 ] }

获取指定 candidate 的值

在 smart contract 中,value 的获取是 async 的,所以不可以将赋值写为:candidate = app.candidates(1)。必须在回调函数里面赋值:

1
2
3
4
5
6
7
8
9
10
11
12
13
truffle(development)> app.candidates(1).then( function(c) { candidate = c;})
undefined
truffle(development)> candidate
[ BigNumber { s: 1, e: 0, c: [ 1 ] },
'Tom',
BigNumber { s: 1, e: 0, c: [ 0 ] } ]
# 获取得到的 candidate struct 里对应的 id,name,voteCount 值
truffle(development)> candidate[0].toNumber()
1
truffle(development)> candidate[1]
'Tom'
truffle(development)> candidate[2].toNumber()
0

查看 blockchain 中的 account

在 Ganache 中,我们创建了 10 个 accounts

同时这些账户可以在 truffl 的 console 里面使用 web3 查找到。

1
2
3
4
5
6
7
8
9
10
11
truffle(development)> web3.eth.accounts
[ '0xdde9664954edb28dc2afb866e668447256b15365',
'0x062f0fb7e869943884698ec2bd67d8de1e612eac',
'0x26d8d3a6e3c84775150939505b407ebef4391f8e',
'0x31de3718ca3a2d6e0df954fe585b1bee596e0642',
'0x00645cf254bdb1ff4004c1f91a17a71032eb8bbf',
'0x6c8fdd7a767e8182270b301c83974a614b6d79b3',
'0x8d29c76eb9f4d978defe7e624ca95207e926fcb8',
'0xecad6764d5eacaf402ae61726c5768866c15ee4b',
'0xa2f4ba6a56c738f968e1509851b84de27207bcc3',
'0xf654b8f4bbee0a95cab96aabf3fddb77813b493e' ]
:测试 vote
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
# solidity 允许给 function 传递除了参数以外的一些 metadata,这里传递了一个 {from: <account address>}。
# 这里 "{from: ...}" 就是指定了 msg.sender 参数。
# 最后输出的值,其实是一次 transaction 的 recipt。read blockchain 不消耗 eth,但是写 消耗,这里消耗了 49101 gas。gas 到 eth 的转化,是 gas * gas_price。这个留待解释。 TODO
truffle(development)> app.vote(1,{from: web3.eth.accounts[4]})
{ tx: '0x4d33e6442f55fbc5f11cafc4c4d33d6e8fec879d24c5d200c82bb0d2486e01d8',
receipt:
{ transactionHash: '0x4d33e6442f55fbc5f11cafc4c4d33d6e8fec879d24c5d200c82bb0d2486e01d8',
transactionIndex: 0,
blockHash: '0xf524dd8578c2ef4f0d63f24f947f0eb83c551520fa856eb02c41d6d4c8e57884',
blockNumber: 66,
gasUsed: 49101,
cumulativeGasUsed: 49101,
contractAddress: null,
logs: [ [Object] ],
status: '0x01',
logsBloom: '0x00000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000008000000000000000000000000000040000000000004000000000000000000000000000000000040000000000004000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000' },
logs:
[ { logIndex: 0,
transactionIndex: 0,
transactionHash: '0x4d33e6442f55fbc5f11cafc4c4d33d6e8fec879d24c5d200c82bb0d2486e01d8',
blockHash: '0xf524dd8578c2ef4f0d63f24f947f0eb83c551520fa856eb02c41d6d4c8e57884',
blockNumber: 66,
address: '0xeb7e9f45214c6b13dece3a14be570d9eaaa9144c',
type: 'mined',
event: 'votedEvent',
args: [Object] } ] }

编写测试用例

truffle 内部使用 mochajschaijs 进行测试

Mocha is a feature-rich JavaScript test framework running on Node.js and in the browser, making asynchronous testing simple and fun

Chai is a BDD / TDD assertion library for node and the browser that can be delightfully paired with any javascript testing framework.

:./test/election.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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
var Election = artifacts.require("./Election.sol");

// 初始化 contract 实例,传入的是 web3.eth.accounts 参数
contract("Election", function(accounts) {
var electionInstance;

it("initializes with two candidates", function() {
return Election.deployed().then(function(instance) {
return instance.candidatesCount();
}).then(function(count) {
assert.equal(count, 2);
});
});

it("it initializes the candidates with the correct values", function() {
return Election.deployed().then(function(instance) {
electionInstance = instance;
return electionInstance.candidates(1);
}).then(function(candidate) {
assert.equal(candidate[0], 1, "contains the correct id");
assert.equal(candidate[1], "Tom", "contains the correct name");
assert.equal(candidate[2], 0, "contains the correct votes count");
return electionInstance.candidates(2);
}).then(function(candidate) {
assert.equal(candidate[0], 2, "contains the correct id");
assert.equal(candidate[1], "Jerry", "contains the correct name");
assert.equal(candidate[2], 0, "contains the correct votes count");
});
});

it("allows a voter to cast a vote", function() {
return Election.deployed().then(function(instance) {
electionInstance = instance;
candidateId = 1;
return electionInstance.vote(candidateId, { from: accounts[0] });
}).then(function(receipt) {
assert.equal(receipt.logs.length, 1, "an event was triggered");
assert.equal(receipt.logs[0].event, "votedEvent", "the event type is correct");
assert.equal(receipt.logs[0].args._candidateId.toNumber(), candidateId, "the candidate id is correct");
return electionInstance.voters(accounts[0]);
}).then(function(voted) {
assert(voted, "the voter was marked as voted");
return electionInstance.candidates(candidateId);
}).then(function(candidate) {
var voteCount = candidate[2];
assert.equal(voteCount, 1, "increments the candidate's vote count");
})
});

it("throws an exception for invalid candiates", function() {
return Election.deployed().then(function(instance) {
electionInstance = instance;
return electionInstance.vote(99, { from: accounts[1] })
}).then(assert.fail).catch(function(error) {
assert(error.message.indexOf('revert') >= 0, "error message must contain revert");
return electionInstance.candidates(1);
}).then(function(candidate1) {
var voteCount = candidate1[2];
assert.equal(voteCount, 1, "candidate 1 did not receive any votes");
return electionInstance.candidates(2);
}).then(function(candidate2) {
var voteCount = candidate2[2];
assert.equal(voteCount, 0, "candidate 2 did not receive any votes");
});
});

it("throws an exception for double voting", function() {
return Election.deployed().then(function(instance) {
electionInstance = instance;
candidateId = 2;
// solidity 允许给 function 传递除了参数以外的一些 metadata,这里传递了一个 {from: <account address>}。
// 这里 "{from: ...}" 就是指定了 msg.sender 参数。
electionInstance.vote(candidateId, { from: accounts[1] });
return electionInstance.candidates(candidateId);
}).then(function(candidate) {
var voteCount = candidate[2];
assert.equal(voteCount, 1, "accepts first vote");
// Try to vote again
return electionInstance.vote(candidateId, { from: accounts[1] });
}).then(assert.fail).catch(function(error) {
assert(error.message.indexOf('revert') >= 0, "error message must contain revert");
return electionInstance.candidates(1);
}).then(function(candidate1) {
var voteCount = candidate1[2];
assert.equal(voteCount, 1, "candidate 1 did not receive any votes");
return electionInstance.candidates(2);
}).then(function(candidate2) {
var voteCount = candidate2[2];
assert.equal(voteCount, 1, "candidate 2 did not receive any votes");
});
});
});
:测试结果
1
2
3
4
5
6
7
8
9
10
11
mac@macs-macbook  ~/Code/blockchain/election  truffle test
Using network 'development'.

Contract: Election
✓ initializes with two candidates
✓ it initializes the candidates with the correct values (45ms)
✓ allows a voter to cast a vote (229ms)
✓ throws an exception for invalid candiates (117ms)
✓ throws an exception for double voting (194ms)

5 passing (658ms)

网页代码

:./src/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
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

<!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">
<!-- The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags -->
<title>Election Results</title>

<!-- Bootstrap -->
<link href="css/bootstrap.min.css" rel="stylesheet">

<!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/html5shiv/3.7.3/html5shiv.min.js"></script>
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
<![endif]-->
</head>
<body>
<div class="container" style="width: 650px;">
<div class="row">
<div class="col-lg-12">
<h1 class="text-center">Election Results</h1>
<hr/>
<br/>
<div id="loader">
<p class="text-center">Loading...</p>
</div>
<div id="content" style="display: none;">
<table class="table">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Name</th>
<th scope="col">Votes</th>
</tr>
</thead>
<tbody id="candidatesResults">
</tbody>
</table>
<hr/>
<form onSubmit="App.castVote(); return false;">
<div class="form-group">
<label for="candidatesSelect">Select Candidate</label>
<select class="form-control" id="candidatesSelect">
</select>
</div>
<button type="submit" class="btn btn-primary">Vote</button>
<hr />
</form>
<p id="accountAddress" class="text-center"></p>
</div>
</div>
</div>
</div>

<!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
<!-- Include all compiled plugins (below), or include individual files as needed -->
<script src="js/bootstrap.min.js"></script>
<script src="js/web3.min.js"></script>
<script src="js/truffle-contract.js"></script>
<script src="js/app.js"></script>
</body>
</html>
:./src/js/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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
App = {
web3Provider: null,
contracts: {},
account: '0x0',
hasVoted: false,

init: function() {
return App.initWeb3();
},

initWeb3: function() {
// TODO: refactor conditional
if (typeof web3 !== 'undefined') {
// If a web3 instance is already provided by Meta Mask.
App.web3Provider = web3.currentProvider;
web3 = new Web3(web3.currentProvider);
} else {
// Specify default instance if no web3 instance provided
App.web3Provider = new Web3.providers.HttpProvider('http://localhost:7545');
web3 = new Web3(App.web3Provider);
}
return App.initContract();
},

// 初始化 smart contract 实例
initContract: function() {
$.getJSON("Election.json", function(election) {
// Instantiate a new truffle contract from the artifact
App.contracts.Election = TruffleContract(election);
// Connect provider to interact with contract
App.contracts.Election.setProvider(App.web3Provider);

App.listenForEvents();

return App.render();
});
},

// Listen for events emitted from the contract
listenForEvents: function() {
App.contracts.Election.deployed().then(function(instance) {
// Restart Chrome if you are unable to receive this event
// This is a known issue with Metamask
// https://github.com/MetaMask/metamask-extension/issues/2393
instance.votedEvent({}, {
fromBlock: 0,
toBlock: 'latest'
}).watch(function(error, event) {
console.log("event triggered", event)
// Reload when a new vote is recorded
App.render();
});
});
},

// 渲染显示
render: function() {
var electionInstance;
var loader = $("#loader");
var content = $("#content");

loader.show();
content.hide();

// Load account data
web3.eth.getCoinbase(function(err, account) {
if (err === null) {
App.account = account;
$("#accountAddress").html("Your Account: " + account);
}
});

// Load contract data
App.contracts.Election.deployed().then(function(instance) {
electionInstance = instance;
return electionInstance.candidatesCount();
}).then(function(candidatesCount) {
var candidatesResults = $("#candidatesResults");
candidatesResults.empty();

var candidatesSelect = $('#candidatesSelect');
candidatesSelect.empty();

for (var i = 1; i <= candidatesCount; i++) {
electionInstance.candidates(i).then(function(candidate) {
var id = candidate[0];
var name = candidate[1];
var voteCount = candidate[2];

// Render candidate Result
var candidateTemplate = "<tr><th>" + id + "</th><td>" + name + "</td><td>" + voteCount + "</td></tr>"
candidatesResults.append(candidateTemplate);

// Render candidate ballot option
var candidateOption = "<option value='" + id + "' >" + name + "</ option>"
candidatesSelect.append(candidateOption);
});
}
return electionInstance.voters(App.account);
}).then(function(hasVoted) {
// Do not allow a user to vote
if(hasVoted) {
$('form').hide();
}
loader.hide();
content.show();
}).catch(function(error) {
console.warn(error);
});
},

castVote: function() {
var candidateId = $('#candidatesSelect').val();
App.contracts.Election.deployed().then(function(instance) {
return instance.vote(candidateId, { from: App.account });
}).then(function(result) {
// Wait for votes to update
$("#content").hide();
$("#loader").show();
}).catch(function(err) {
console.error(err);
});
}
};

$(function() {
$(window).load(function() {
App.init();
});
});

运行实例

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
truffle migrate --reset
npm run dev

> pet-shop@1.0.0 dev /Users/mac/Code/blockchain/election
> lite-server

** browser-sync config **
{ injectChanges: false,
files: [ './**/*.{html,htm,css,js}' ],
watchOptions: { ignored: 'node_modules' },
server:
{ baseDir: [ './src', './build/contracts' ],
middleware: [ [Function], [Function] ] } }
[Browsersync] Access URLs:
--------------------------------------
Local: http://localhost:3000
External: http://10.181.60.166:3000
--------------------------------------
UI: http://localhost:3001
UI External: http://10.181.60.166:3001
--------------------------------------
[Browsersync] Serving files from: ./src
[Browsersync] Serving files from: ./build/contracts
[Browsersync] Watching files...
18.05.03 21:38:06 200 GET /index.html
18.05.03 21:38:06 200 GET /js/bootstrap.min.js
18.05.03 21:38:06 200 GET /css/bootstrap.min.css
18.05.03 21:38:06 200 GET /js/app.js
18.05.03 21:38:06 200 GET /js/web3.min.js
18.05.03 21:38:06 200 GET /js/truffle-contract.js
18.05.03 21:38:08 200 GET /Election.json
18.05.03 21:38:08 404 GET /favicon.ico

运行之后会弹出网页,但是一直显示 loading,看不到任何从 smart contract 返回的数据。

这是因为我们的客户端程序虽然在运行,但是还没有链接到我们创建的 blockchain instance 上。我们需要打开 Ganache,找到本地 in memory blockchain 的 RPC server url address:

然后在 Google Chrome(或者 Firefox) 浏览器中 MetaMask 插件里自定义 RPC 链接:

之后就可以看到如下界面:

点击 vote 以后,可以看到如下结果:

参考

  1. How to Build Ethereum Dapp
  2. dappuniversity/election
0%