Web3-Solidity

Solidity学习笔记(5) — 使用Web3.js实现与Smart Contract的交互

作者:孔令坤,转载请注明出处

Web3官方API:https://github.com/ethereum/wiki/wiki/JavaScript-API

Youtube环境搭配视频:https://coursetro.com/courses/20/Developing-Ethereum-Smart-Contracts-for-Beginners

在本文中,我写了一个很简单的存取钱的智能合约,并借助web3.js提供的接口写了一个非常简单 的网页,来实现用户通过网页与smart contract的交互。

1.环境配置

首先,用户需要在google chrome中配置MetaMask

之后,用户需要安装web3.js,具体安装方式见官方安装文档

接下来,为了能在本地端实现web3.js与metamask中的账号以及smart contract的交互,我们需要在本地为网页文件创造一个虚拟的服务器环境。这里,我们使用lite-server来帮助我们实现这个需求。具体安装方法如下:

  1. 在装好web.js的文件夹目录下输入command line code:
1
>> npm install lite-server --save-dev

如果用户之前是全局安装web.js的话,需要先输入npm init

  1. 打开目录中的 package.json文件,在其中的scripts中添加:
1
2
3
"scripts": {    
"dev": "lite-server"
},
  1. 在目录中已有index.html文件的前提下,输入如下语句即可正常运行网页中的js脚本:
1
>> npm run dev

可能这里我讲得有点粗糙,但是不用担心。有人已经在youtube上提供了非常详细的安装流程,见Youtube环境搭配视频

2.smart contract开发

如果大家已经看过了我之前的博客,相信这一部分对于大家而言将不存在任何理解上的困难。

所以这里我简单的直接放上代码:

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
pragma solidity ^0.4.21;

contract SaveMoney{
address public user;
string public userName;
uint public deposits;

enum State {Unoccupied, Occupied}
State public state;

event accountInited(uint m, string name);
event moneyWithdrawed();

modifier condition(bool _condition) {
require(_condition);
_;
}

modifier onlyUser() {
require(msg.sender == user);
_;
}

modifier inState(State _state) {
require(state == _state);
_;
}
// initiate an account
function initAccount(uint _money, string _userName)
public
inState(State.Unoccupied)
condition(msg.value > _money) // since initiation costs some gas
payable
{
user = msg.sender;
userName = _userName;
deposits = _money;
state = State.Occupied;
user.transfer(this.balance);
emit accountInited(_money, _userName);
}
// withdraw the money from the contract
function withDraw()
public
inState(State.Occupied)
onlyUser
{
deposits = 0;
user.transfer(this.balance);
state = State.Unoccupied;
emit moneyWithdrawed();
}
}

3.网页开发与js脚本编写

首先,我放出网页的主体框架与css文件,其中大量借鉴了Developing-Ethereum-Smart-Contracts-for-Beginners中的框架。整体比较粗糙,但是非常便于大家理解代码。

main.css文件:

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
body {
background-color:#F0F0F0;
padding: 2em;
font-family: 'Raleway','Source Sans Pro', 'Arial';
}
.container {
width: 50%;
margin: 0 auto;
}
label {
display:block;
margin-bottom:10px;
}
input {
padding:10px;
width: 50%;
margin-bottom: 1em;
}
button {
margin: 2em 0;
padding: 1em 4em;
display:block;
}

#instructor {
padding:1em;
background-color:#fff;
margin: 1em 0;
}

#loader {
width: 100px;
display:none;
}

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>

<link rel="stylesheet" type="text/css" href="main.css">

<script src="./node_modules/web3/dist/web3.min.js"></script>

</head>
<body>
<div class="container">

<h1>Money Bank</h1>

<h2 id="User"></h2>
<span id="userTrans"></span>
<hr>

<img id="loader" src="https://loading.io/spinners/double-ring/lg.double-ring-spinner.gif">

<label for="userName" class="col-lg-2 control-label">Name</label>
<input id="userName" type="text">

<label for="money" class="col-lg-2 control-label"># of Money</label>
<input id="money" type="text">

<button id="button1">Save Money</button>
<button id="button2">Withdraw</button>
</div>

<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js"></script>
<script>
// Put Scripts Here...
</script>

</body>
</html>

下面,我将分布挑重点来介绍一下我的js代码。

首先,我们在代码中放入:

1
2
3
4
5
6
if (typeof web3 !== 'undefined') {
web3 = new Web3(web3.currentProvider);
} else {
// set the provider you want from Web3.providers
web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545"));
}

这一段是官方提供的初始化连接web3的代码。如果你使用了metamask,那么currentProvider就会是来自你的metamask的网络以及账号。这个时候,在remix上,你的smart contract创建方式应当选择Injected Web3。

若你没有使用metamask,那么你可以在remix上使用Web3 Provider的方式来创建smart contract。这里"http://localhost:8545"其实来自另一个好用的eth smart contract开发应用,其安装方式如下:

1
>> npm install -g ethereumjs-testrpc

通过>> testrpc来启动这个provider并通过"http://localhost:8545"来进行数据的发送。

接下来,我们选择web3应用中的账户。

1
web3.eth.defaultAccount = web3.eth.accounts[0];

接下来,我们建立网页与合约之间的直接关联:

1
2
var saveMoney = web3.eth.contract(ABI OF THE CONTRACT);
var saveMoneyContract = saveMoney.at('ADDR OF THE CONTRACT');

ABI即smart contract编译后产生的一个文件,在remix中点击Start to compile按钮,然后点击Details按钮,即可复制smart contract的ABI,大致格式如下:

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
[
{
"constant": false,
"inputs": [],
"name": "withDraw",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "deposits",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
......
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"name": "m",
"type": "uint256"
},
{
"indexed": false,
"name": "name",
"type": "string"
}
],
"name": "accountInited",
"type": "event"
},
{
"anonymous": false,
"inputs": [],
"name": "moneyWithdrawed",
"type": "event"
}
]

然后smart contract的address的获取就比较简单了,就是创建好smart contract后,copy地址即可,e.g. 0x8f7e0a306b1a1f1205fa47b2bf2fefabaf056dae

之后,设置点击save money和 withdraw按钮的触发事件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$("#button1").click(function() {
$("#loader").show();
saveMoneyContract.initAccount($("#money").val(), $("#userName").val(), {value: $("#money").val()+100000} ,(err, res) => {
if (err) {
$("#loader").hide();
}
});
});

$("#button2").click(function() {
$("#loader").show();
saveMoneyContract.withDraw((err, res) => {
if (err) {
$("#loader").hide();
}
});
});

其中需要注意的是initAccount函数中参数的设定。由于这个函数是一个带value的payable的函数,我们需要在编程时不仅传递函数的参数,还得传递一定数量的以太币。在查阅了官方的api文件后,可以用如上的方式传递value,即

1
function(param1, param2, ..., paramk, {value: # of eth})

这里我在设置value时使用了$("#money").val()+100000,这是因为提交函数需要消耗一定量的gas,+100000wei是为了防止由于gas的消耗,用户无法在账户中存入数量为$("#money").val()的金额,导致交易失败。

最后,我们通过监听smart contract中的event来在网页上输出数据,代码如下:

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
var saveEvent = saveMoneyContract.accountInited({}, 'latest');

saveEvent.watch(function(error, result){
if (!error){
$("#loader").hide();
$("#userTrans").html('Block hash: ' + result.blockHash);
console.log(result);
$("#User").html(result.args.name +' saved ' + result.args.m.c + ' wei.');
} else {
$("#loader").hide();
console.log(error);
}
});

var withDrawEvent = saveMoneyContract.moneyWithdrawed({}, 'latest');

withDrawEvent.watch(function(err, res){
if (!err){
$("#loader").hide();
$("#userTrans").html('Block hash: ' + res.blockHash);
$("#User").html('Money has been withdrawed!');
} else {
$("#loader").hide();
console.log(err);
}
});

最后,我附上了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
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
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>

<link rel="stylesheet" type="text/css" href="main.css">

<script src="./node_modules/web3/dist/web3.min.js"></script>

</head>
<body>
<div class="container">

<h1>Money Bank</h1>

<h2 id="User"></h2>
<span id="userTrans"></span>
<hr>

<img id="loader" src="https://loading.io/spinners/double-ring/lg.double-ring-spinner.gif">

<label for="userName" class="col-lg-2 control-label">Name</label>
<input id="userName" type="text">

<label for="money" class="col-lg-2 control-label"># of Money</label>
<input id="money" type="text">

<button id="button1">Save Money</button>
<button id="button2">Withdraw</button>
</div>

<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js"></script>

<script>
if (typeof web3 !== 'undefined') {
web3 = new Web3(web3.currentProvider);
} else {
// set the provider you want from Web3.providers
web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545"));
}

web3.eth.defaultAccount = web3.eth.accounts[0];

var saveMoney = web3.eth.contract([
{
"constant": false,
"inputs": [],
"name": "withDraw",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "deposits",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "user",
"outputs": [
{
"name": "",
"type": "address"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "userName",
"outputs": [
{
"name": "",
"type": "string"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "state",
"outputs": [
{
"name": "",
"type": "uint8"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_money",
"type": "uint256"
},
{
"name": "_userName",
"type": "string"
}
],
"name": "initAccount",
"outputs": [],
"payable": true,
"stateMutability": "payable",
"type": "function"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"name": "m",
"type": "uint256"
},
{
"indexed": false,
"name": "name",
"type": "string"
}
],
"name": "accountInited",
"type": "event"
},
{
"anonymous": false,
"inputs": [],
"name": "moneyWithdrawed",
"type": "event"
}
]);
var saveMoneyContract = saveMoney.at('0x8f7e0a306b1a1f1205fa47b2bf2fefabaf056dae');
console.log(saveMoneyContract);

var saveEvent = saveMoneyContract.accountInited({}, 'latest');

saveEvent.watch(function(error, result){
if (!error){
$("#loader").hide();
$("#userTrans").html('Block hash: ' + result.blockHash);
console.log(result);
$("#User").html(result.args.name +' saved ' + result.args.m.c + ' wei.');
} else {
$("#loader").hide();
console.log(error);
}
});


var withDrawEvent = saveMoneyContract.moneyWithdrawed({}, 'latest');

withDrawEvent.watch(function(err, res){
if (!err){
$("#loader").hide();
$("#userTrans").html('Block hash: ' + res.blockHash);
$("#User").html('Money has been withdrawed!');
} else {
$("#loader").hide();
console.log(err);
}
});

$("#button1").click(function() {
$("#loader").show();
saveMoneyContract.initAccount($("#money").val(), $("#userName").val(), {value: $("#money").val()+100000} ,(err, res) => {
if (err) {
$("#loader").hide();
}
});
});


$("#button2").click(function() {
$("#loader").show();
saveMoneyContract.withDraw((err, res) => {
if (err) {
$("#loader").hide();
}
});
});

</script>

</body>
</html>