构建一个简单的以太坊+IPFS+React.js DApp

我们为什么要构建这个?在以太坊区块链上存储大量数据是非常昂贵的。根据以太坊的黄皮书,它是大约20,0000gas,256bit/8字节(1字)。基于02/28/2018 gas价格为4gwei/gas。请参阅:https://ethgasstation.info了解当前价格。

每个交易8个字节20,000gas*4gwei/gas=80,000gwei

8,000字节80,000gwei*1000bytes/8=10,000,000gwei/kB=0.01`以太。

0.01以太/kB*1000kB=10以太存储1Mb,价格为860美元/以太=8600.00美元!在以太坊区块链上存储1GB文件需要花费8,600,000.00美元!

存储以太坊的38页PDF黄皮书(520Kb)=4472美元。请参阅: http://eth-converter.com/进行转换计算。

如果我们只能在区块链上存储几Kb的数据,那么我们仍然需要依靠集中式服务器来存储数据。值得庆幸的是,可以使用称为InterPlanetary files system 星际文件系统IPFS的去中心化网络上存储数据的解决方案。请参阅:https://ipfs.io/了解更多信息。在IPFS中查找文件时,你要求网络查找将内容存储在唯一哈希后面的节点。来自IPFS自己的网站:

“IPFS和Blockchain完美匹配!你可以使用IPFS处理大量数据,并将不可变的永久IPFS链接放入区块链交易中。这个时间戳和保护你的内容,而不必将数据放在链本身上。“

我们构建什么?

一个简单的DApp,用于将文档上载到IPFS,然后将IPFS哈希存储在以太坊区块链上。一旦IPFS哈希号被发送到以太坊区块链,用户将收到交易收据。我们将使用Create-React-App框架来构建前端。此Dapp适用于在浏览器中安装了MetaMask的任何用户。

这就是我们完成后DApp的样子:

如何建立它:

注意:如果你只是想要代码,请参阅我的github

在我们开始之前,这些是我做出的假设:

  • 关于用户的假设:用户安装了Metamask以与DApp交互。
  • 关于你/开发人员的假设:你对JavaScript、React.js以及Node.js/NPM有一定的了解。重要说明:请确保你运行当前版本的Node和NPM。对于本教程,我正在运行:node v8.9.4和NPM 5.6.0。
  • 安装MetaMask。如果尚未安装MetaMask,请访问https://metamask.io/并按照说明操作。此DApp将假定用户已安装MetaMask。
  • 创建一个目录来存储我们的DApp。对于本教程,我将其称为eth-ipfs
  • 使用NPM安装Create-React-App和其他依赖项。使用NPM并安装以下内容:
1
2
3
4
5
npm i create-react-app
npm install react-bootstrap
npm install fs-extra
npm install ipfs-api
npm install web3@^1.0.0-beta.26

输入eth-ipfs目录,键入npm startCreate-React-App应自动在http://localhost:3000/上呈现。

注意:如果你到目前为止尚未使用create-react-app,则可能必须先在全局安装它

    1. sudo npm install -g create-react-app或者npm install -g create-react-app
    1. create-react-app eth-ipfs
    1. cd进入eth-ipfs然后运行npm start

在Rinkeby testnet上使用Remix部署以下Solidity代码。请参阅https://remix.ethereum.org。你需要一些Rinkeby测试以太,如果你还没有Rinkeby faucet的一些免费测试以太话。https://www.rinkeby.io/#faucet

1
2
3
4
5
6
7
8
9
10
11
12
pragma solidity ^0.4.17;
contract Contract {
string ipfsHash;

function sendHash(string x) public {
ipfsHash = x;
}

function getHash() public view returns (string x) {
return ipfsHash;
}
}

保存部署它的合约地址和应用程序二进制接口(ABI)。要获得合约的ABI,请在Remix中转到你的合约地址:

单击“Compile”选项卡,然后单击灰色的“Details”按钮。

这将打开“Details”窗口。复制“ABI”,它是一个JSON文件。

我个人更喜欢将ABI JSON放入格式化程序,例如https://jsonformatter.org,并在我的javascript代码中使用之前检查它是否有效。保存合约地址和ABI以供日后使用。

在我们的“eth-ipfs/src”目录中,创建以下文件: web3.jsipfs.jsstorehash.js。我们的大部分代码都在App.js中。

web3.js

我们想使用1.0版本的web3.js,因为与0.20版本不同,1.0允许我们在我们的javascript中使用async并等待而不是promises。目前,MetaMask的默认web3.js提供程序是0.20版本。所以,让我们确保我们覆盖Metamask的web3版本0.20的默认版本,并使用我们的1.0。这是代码:

1
2
3
4
5
6
//为我们的1.0版本覆盖metamask v0.2。 
//1.0让我们使用async和await而不是promises
import Web3 from ‘web3’;

const web3 = new Web3(window.web3.currentProvider);
export default web3;

storehash.js

为了让web3.js能够访问我们之前部署到以太坊的Rinkeby testnet的合约,你需要以下内容:1)合约地址和2)合约中的ABI。一定要从/src目录中导入web3.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
import web3 from './web3';
//access our local copy to contract deployed on rinkeby testnet
//use your own contract address
const address = '0xb84b12e953f5bcf01b05f926728e855f2d4a67a9';
//use the ABI from your contract
const abi = [
{
"constant": true,
"inputs": [],
"name": "getHash",
"outputs": [
{
"name": "x",
"type": "string"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "x",
"type": "string"
}
],
"name": "sendHash",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
}
]
export default new web3.eth.Contract(abi, address);

ipfs.js

在本教程中,我们将运行ipfs.infura.io节点以连接到IPFS,而不是在我们自己的计算机上运行IPFS守护程序。在代码注释中,如果将IPFS安装为全局依赖项,则还可以选择运行自己的IPFS守护程序。有关使用其节点的更多信息,请参阅https://infura.io/。这是代码:

1
2
3
4
5
6
7
//using the infura.io node, otherwise ipfs requires you to run a //daemon on your own computer/server.
const IPFS = require(‘ipfs-api’);
const ipfs = new IPFS({ host: ‘ipfs.infura.io’, port: 5001, protocol: ‘https’ });
//run with local daemon
// const ipfsApi = require(‘ipfs-api’);
// const ipfs = new ipfsApi(‘localhost’, ‘5001’, {protocol:‘http’});
export default ipfs;

App.js

这是App.js中的操作顺序:

  • 1.设置状态变量。
  • 2.捕获用户的文件。
  • 3.将文件转换为缓冲区。
  • 4.将缓冲的文件发送到IPFS。
  • 5.IPFS返回一个哈希值。
  • 6.获取用户的MetaMask以太坊地址
  • 7.发送IPFS以便在以太坊上存储。
  • 8.使用MetaMask,用户将确认交易到以太坊。
  • 9.以太坊合约将返回一个交易哈希数。
  • 10.交易哈希值可用于生成具有诸如所使用的gas量和块编号之类的信息的交易收据。
  • 11.IPFS和以太坊信息将在使用Bootstrap for CSS的表中呈现。注意:我没有创建一个isLoading类型变量来自动重新呈现blockNumber和gasUsed变量的状态。因此,现在,你必须再次单击或实现自己的加载图标。 描述变量和函数的表,后面是代码本身如下:

最后,这是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
131
132
133
134
135
136
137
import React, { Component } from ‘react’;
//import logo from ‘./logo.svg’;
import ‘./App.css’;
import web3 from ‘./web3’;
import ipfs from ‘./ipfs’;
import storehash from ‘./storehash’;
class App extends Component {

state = {
ipfsHash:null,
buffer:'',
ethAddress:'',
blockNumber:'',
transactionHash:'',
gasUsed:'',
txReceipt: ''
};
captureFile =(event) => {
event.stopPropagation()
event.preventDefault()
const file = event.target.files[0]
let reader = new window.FileReader()
reader.readAsArrayBuffer(file)
reader.onloadend = () => this.convertToBuffer(reader)
};
convertToBuffer = async(reader) => {
//file is converted to a buffer for upload to IPFS
const buffer = await Buffer.from(reader.result);
//set this buffer -using es6 syntax
this.setState({buffer});
};
onClick = async () => {
try{
this.setState({blockNumber:"waiting.."});
this.setState({gasUsed:"waiting..."});
//get Transaction Receipt in console on click
//See: https://web3js.readthedocs.io/en/1.0/web3-eth.html#gettransactionreceipt
await web3.eth.getTransactionReceipt(this.state.transactionHash, (err, txReceipt)=>{
console.log(err,txReceipt);
this.setState({txReceipt});
}); //await for getTransactionReceipt
await this.setState({blockNumber: this.state.txReceipt.blockNumber});
await this.setState({gasUsed: this.state.txReceipt.gasUsed});
} //try
catch(error){
console.log(error);
} //catch
} //onClick
onSubmit = async (event) => {
event.preventDefault();
//bring in user's metamask account address
const accounts = await web3.eth.getAccounts();

console.log('Sending from Metamask account: ' + accounts[0]);
//obtain contract address from storehash.js
const ethAddress= await storehash.options.address;
this.setState({ethAddress});
//save document to IPFS,return its hash#, and set hash# to state
//https://github.com/ipfs/interface-ipfs-core/blob/master/SPEC/FILES.md#add
await ipfs.add(this.state.buffer, (err, ipfsHash) => {
console.log(err,ipfsHash);
//setState by setting ipfsHash to ipfsHash[0].hash
this.setState({ ipfsHash:ipfsHash[0].hash });
// call Ethereum contract method "sendHash" and .send IPFS hash to etheruem contract
//return the transaction hash from the ethereum contract
//see, this https://web3js.readthedocs.io/en/1.0/web3-eth-contract.html#methods-mymethod-send

storehash.methods.sendHash(this.state.ipfsHash).send({
from: accounts[0]
}, (error, transactionHash) => {
console.log(transactionHash);
this.setState({transactionHash});
}); //storehash
}) //await ipfs.add
}; //onSubmit
render() {

return (
<div className="App">
<header className="App-header">
<h1> Ethereum and IPFS with Create React App</h1>
</header>

<hr />
<Grid>
<h3> Choose file to send to IPFS </h3>
<Form onSubmit={this.onSubmit}>
<input
type = "file"
onChange = {this.captureFile}
/>
<Button
bsStyle="primary"
type="submit">
Send it
</Button>
</Form>
<hr/>
<Button onClick = {this.onClick}> Get Transaction Receipt </Button>
<Table bordered responsive>
<thead>
<tr>
<th>Tx Receipt Category</th>
<th>Values</th>
</tr>
</thead>

<tbody>
<tr>
<td>IPFS Hash # stored on Eth Contract</td>
<td>{this.state.ipfsHash}</td>
</tr>
<tr>
<td>Ethereum Contract Address</td>
<td>{this.state.ethAddress}</td>
</tr>
<tr>
<td>Tx Hash # </td>
<td>{this.state.transactionHash}</td>
</tr>
<tr>
<td>Block Number # </td>
<td>{this.state.blockNumber}</td>
</tr>
<tr>
<td>Gas Used</td>
<td>{this.state.gasUsed}</td>
</tr>

</tbody>
</Table>
</Grid>
</div>
);
} //render
} //App
export default App;

我在src/App.css中添加了一些CSS,使它看起来更容易一些:

1
2
3
4
5
6
7
8
9
10
11
12
/*some css I added*/
input[type=”file”] {
display: inline-block;
}
.table {
max-width: 90%;
margin: 10px;
}
.table th {
text-align: center;
}
/*end of my css*/

并向src/index.js添加一些导入:

1
2
3
/*https://github.com/facebook/create-react-app/blob/master/packages/react-scripts/template/README.md#adding-a-stylesheet*/
import ‘bootstrap/dist/css/bootstrap.css’;
import ‘bootstrap/dist/css/bootstrap-theme.css’;

这就对了!你的DApp应该完成。所以你需要做的就是选择一个文件,发送它,并获得一个交易收据。如果你通过localhost:3000连接到IPFS节点,那么你应该能够在其中一个IPFS网关上看到你的文件。https://gateway.ipfs.io/ipfs/+你的IPFS哈希。

例如: https://gateway.ipfs.io/ipfs/QmYjh5NsDc6LwU3394NbB42WpQbGVsueVSBmod5WACvpte

关于IPFS的一个注意事项是,除非你的文件被另一个节点接收或者你将其固定,否则IPFS最终将垃圾收集你的文件。他们的网站上有很多关于此的内容。

======================================================================

分享一些以太坊、EOS、比特币等区块链相关的交互式在线编程实战教程:

  • java以太坊开发教程,主要是针对java和android程序员进行区块链以太坊开发的web3j详解。
  • python以太坊,主要是针对python工程师使用web3.py进行区块链以太坊开发的详解。
  • php以太坊,主要是介绍使用php进行智能合约开发交互,进行账号创建、交易、转账、代币开发以及过滤器和交易等内容。
  • 以太坊入门教程,主要介绍智能合约与dapp应用开发,适合入门。
  • 以太坊开发进阶教程,主要是介绍使用node.js、mongodb、区块链、ipfs实现去中心化电商DApp实战,适合进阶。
  • C#以太坊,主要讲解如何使用C#开发基于.Net的以太坊应用,包括账户管理、状态与交易、智能合约开发与交互、过滤器和交易等。
  • EOS教程,本课程帮助你快速入门EOS区块链去中心化应用的开发,内容涵盖EOS工具链、账户与钱包、发行代币、智能合约开发与部署、使用代码与智能合约交互等核心知识点,最后综合运用各知识点完成一个便签DApp的开发。
  • java比特币开发教程,本课程面向初学者,内容即涵盖比特币的核心概念,例如区块链存储、去中心化共识机制、密钥与脚本、交易与UTXO等,同时也详细讲解如何在Java代码中集成比特币支持功能,例如创建地址、管理钱包、构造裸交易等,是Java工程师不可多得的比特币开发学习课程。
  • php比特币开发教程,本课程面向初学者,内容即涵盖比特币的核心概念,例如区块链存储、去中心化共识机制、密钥与脚本、交易与UTXO等,同时也详细讲解如何在Php代码中集成比特币支持功能,例如创建地址、管理钱包、构造裸交易等,是Php工程师不可多得的比特币开发学习课程。
  • tendermint区块链开发详解,本课程适合希望使用tendermint进行区块链开发的工程师,课程内容即包括tendermint应用开发模型中的核心概念,例如ABCI接口、默克尔树、多版本状态库等,也包括代币发行等丰富的实操代码,是go语言工程师快速入门区块链开发的最佳选择。

汇智网原创翻译,转载请标明出处。这里是原文构建一个简单的以太坊+IPFS+React.js DApp