基于React+truffle的完整智能合约构建

内容:使用solidity的truffle框架开发智能合约,前端使用react框架,最终完成智能合约从前端到后端,从开发到部署的完整流程。

1、truffle安装

在nodejs安装完成的环境下,全局安装truffle:cnpm i -g truffle

Truffle Boxes是truffle框架集成的脚手架工具,可以使用这个脚手架快捷的生成完备的DAPP的项目结构,其中集成了前端视图、编译压缩工具等。可以在http://truffleframework.com/boxes/中查看并选择合适的模板来进行项目初始化。

2、项目初始化

这里我在http://truffleframework.com/boxes/上选择react模板来开发DAPP,直接在终端执行truffle unbox React即可完成项目的初始化,期间需要安装nodejs依赖,耐心等待即可(都没个安装进度提示,差评!)。

1
2
3
4
5
ludis@MacBook -> ~/Desktop/golang -> mkdir truffle && cd truffle
ludis@MacBook -> ~/Desktop/golang/truffle -> truffle unbox React
Downloading...
Unpacking...
Setting up...

项目初始化完成之后,目录结构如下:

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
.
├── box-img-lg.png
├── box-img-sm.png
├── config
│   ├── env.js
│   ├── jest
│   ├── paths.js
│   ├── polyfills.js
│   ├── webpack.config.dev.js
│   └── webpack.config.prod.js
├── contracts
│   ├── Migrations.sol
│   └── SimpleStorage.sol
├── migrations
│   ├── 1_initial_migration.js
│   └── 2_deploy_contracts.js
├── package.json
├── public
│   ├── favicon.ico
│   └── index.html
├── scripts
│   ├── build.js
│   ├── start.js
│   └── test.js
├── src
│   ├── App.css
│   ├── App.js
│   ├── App.test.js
│   ├── css
│   ├── fonts
│   ├── index.css
│   ├── index.js
│   └── utils
├── test
│   ├── TestSimpleStorage.sol
│   └── simplestorage.js
├── tmp-8766pK0xGOxZkgsX
│   └── box
├── truffle-config.js
└── truffle.js
  • contractsmigrations文件夹上篇已经介绍过,分别为solidity合约文件及相应配置文件。
  • src目录下是react前端代码。

3、合约编写、编译和部署

在项目初始化完成后,有一个默认的合约示例SimpleStorage.sol,我们直接用这个示例做测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
pragma solidity ^0.4.18;

contract SimpleStorage {
uint storedData;

function set(uint x) public {
storedData = x;
}

function get() public view returns (uint) {
return storedData;
}
}

这个合约代码是最简单的读和写两个操作,也是最基础最常用的操作。

编译过程非常简单,在项目根目录下执行truffle compile即可,会在根目录生成build编译目录。这里的编译与部署合约无关,只是为了后续前端代码的调用。

接着将合约部署到以太坊网络上,同理,可以选择部署到正式网络/测试网络/本地,这里我们选择测试网络,部署过程详见区块链学习 - solidity合约部署,小狐狸选择测试网络,将代码粘贴至左侧编辑器,右侧选择对应选项后即可部署成功,部署成功后我们便能得到部署的十六位的合约地址,以我部署的地址0x2dc69582315624fba54ae69655abea27fd48468e为例。

合约部署

4、前端与合约交互

上步部署完合约后,需要将合约地址记下,后续对合约的所有操作都需要通过合约地址进行。

src目录下便是react的前端代码,index.js作为入口文件:

1
2
3
4
5
6
7
8
9
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'

ReactDOM.render(
<App />,
document.getElementById('root')
);

可以看到index.js中将App.js作为主组件引入,打开App.js,头部引用为:

1
2
3
import React, { Component } from 'react'
import SimpleStorageContract from '../build/contracts/SimpleStorage.json'
import getWeb3 from './utils/getWeb3'

通过头部引用可以看出前端与合约交互的流程:

  1. 首先前端使用了react框架;
  2. 第二句import SimpleStorageContract from '../build/contracts/SimpleStorage.json'导入的实际上是上步合约代码编译后产生的json文件,该文件中包含两个重要信息:一个是abi字段,通俗讲就是合约代码编译后产生的二进制接口,其中会声明合约代码暴露的可供调用的接口;另一个是bytecode字段,为合约代码的十六进制码。通过这两个重要信息就可以对一个合约进行相应操作。
  3. 导入了web3.jsweb3.js是以太坊提供的一个JavaScript库,他封装了以太坊的JSON RPC API,提供了一系列与区块链交互的JavaScript对象和函数,包括查看网络状态,查看本地账户、查看交易和区块、发送交易、编译/部署智能合约、调用智能合约等,其中最重要的就是与智能合约交互的API。

明白了前端与合约交互的原理,剩下来的就是使用react语法结合web3.js提供的接口对合约操作即可。

App.js文件进行一些修改,实现:在网页输入框中输入值,点击确定后可以修改合约中的值(写操作),写入成功后读取合约中的新值,并自动显示到网页(读操作),也就是通过前端网页对上述合约中定义的的set()get()两个方法的调用。

最终App.js如下,主要修改分以下几点:

  • 将部署完成的合约地址作为变量contractAddress保存,以供后续调用合约使用。
  • 声明simpleStorageInstance变量,作用是通过合约地址得到合约实例后将合约实例保存起来,方便后续调用。
  • 添加输入框及按钮,点击按钮时读取输入框的值,通过合约实例及web3提供的方法修改合约中的storedData值。
  • 使用Promise语法,当写操作成功后,执行读操作,并在读取成功后,修改react的组件状态state中的变量值,使前端显示自动更新
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
import React, { Component } from 'react'
import SimpleStorageContract from '../build/contracts/SimpleStorage.json'
import getWeb3 from './utils/getWeb3'

import './css/oswald.css'
import './css/open-sans.css'
import './css/pure-min.css'
import './App.css'

const contractAddress = "0x2dc69582315624fba54ae69655abea27fd48468e"; // 合约地址
var simpleStorageInstance; // 合约实例

class App extends Component {
constructor(props) {
super(props)

this.state = {
storageValue: 0,
web3: null
}
}

componentWillMount() {
// Get network provider and web3 instance.
// See utils/getWeb3 for more info.

getWeb3
.then(results => {
this.setState({
web3: results.web3
})

// Instantiate contract once web3 provided.
this.instantiateContract()
})
.catch(() => {
console.log('Error finding web3.')
})
}

instantiateContract() {
/*
* SMART CONTRACT EXAMPLE
*
* Normally these functions would be called in the context of a
* state management library, but for convenience I've placed them here.
*/

const contract = require('truffle-contract')
const simpleStorage = contract(SimpleStorageContract)
simpleStorage.setProvider(this.state.web3.currentProvider)

// Declaring this for later so we can chain functions on SimpleStorage.


// Get accounts.
this.state.web3.eth.getAccounts((error, accounts) => {

// 获取合约地址获取合约实例并保存
simpleStorage.at(contractAddress).then((instance) => {
simpleStorageInstance = instance

// Stores a given value, 5 by default.
// return simpleStorageInstance.set(5, {from: accounts[0]})
return;
}).then((result) => {
// Get the value from the contract to prove it worked.
return simpleStorageInstance.get.call(accounts[0])
}).then((result) => {
// Update state with the result.
console.log(result);
return this.setState({ storageValue: result.c[0] })
})
})
}

render() {
return (
<div className="App">
<nav className="navbar pure-menu pure-menu-horizontal">
<a href="#" className="pure-menu-heading pure-menu-link">Truffle Box</a>
</nav>

<main className="container">
<div className="pure-g">
<div className="pure-u-1-1">
<h1>Good to Go!</h1>
<p>Your Truffle Box is installed and ready.</p>
<h2>Smart Contract Example</h2>
<p>If your contracts compiled and migrated successfully, below will show a stored value of 5 (by default).</p>
<p>Try changing the value stored on <strong>line 59</strong> of App.js.</p>
<p>The stored value is: {this.state.storageValue}</p>
</div>
</div>
</main>

<input ref="myvalue" className="myinput"/>
<button
className="mybutton"
onClick={() => {
var num = Number(this.refs.myvalue.value);
console.log("点击了button");
console.log(num);
simpleStorageInstance.set(num, {from: this.state.web3.eth.accounts[0]}).then(() => {
console.log("数据修改成功");
simpleStorageInstance.get.call(this.state.web3.eth.accounts[0]).then((result) => {
console.log("数据读取成功");
console.log(result);
// 修改状态变量的值,在react中,一旦状态变量的值发生变化,就会调用render函数重新渲染UI
this.setState({ storageValue: result.c[0] })
});

})

}}
style={{height: 40,marginLeft: 50}}>修改合约数据</button>

</div>
);
}
}

export default App

5、启动项目,查看效果

启动本地的react项目,直接终端执行npm start即可。会用node在本地启一个3000端口的服务,浏览器会自动跳转到http://localhost:3000,就会显示react编写的前端页面,在输入框修改相应数值,点击确定,然后小狐狸会弹出支付提示(记得小狐狸切换到测试网络)(所有的写操作均需要支付手续费),确认支付后耐心等待被打包成功,就能看到网页上数值的变化了。

本地测试

6、总结

一个完整的覆盖前后端的DAPP实际上就两点,跟传统互联网项目的前后端类似:

  1. 合约编写、部署
  2. 前端调用

基于以太坊开发DAPP实际上比较简单,重点是在合约的逻辑性、安全性上。从这也可以看出来以太坊生态的强大和完整,便捷完备的开发语言、工具,确实是目前继大饼之后最牛的项目。

Author

Ludis

Posted on

2018-02-22

Updated on

2018-12-10

Licensed under

Comments