DApp一日通——web3.js、Truffle与Dfinity篇

本文在Truffle框架的基础上给出一个可快速上手DApp的教程,并非工业化流程,其中对于真实的生产环境做了大量的精简,仅保留最关键的几个部分,对于初学者足以了解一个DApp的开发工作流程,但是对于工业级生产则远远不够。这里仅作为一个入门来一窥DApp的基本概念。

DApp基本技术栈

一个DApp和常规的App最大的区别就是存在与去中心的区块链进行合约交互的环节,在以太坊区块链上可以运行的App实际上就是指合约。一个仅有合约的应用架构显然是很局限的,所以所谓的DApp通常都不是纯粹的去中心,其开发体系与web2的应用基本无异,最大的不同除了链上合约调用外,服务端也成为了可选的部分。

DApp Architecture

关键组件:

  1. Web3 Provider: 负责与区块链通信的角色,可以对合约进行调用。常见的包含: MetaMask、web3.js等
    • Relay: Provider背后真正负责与区块链交互的服务器集群,web3的XaaS;本地练习暂时不必要
  2. Smart Contract: Ethereum Virtual Machine上运行的合约bytecode代码,提供图灵完备的计算能力,可以做信息储存、NFT资产发行、投票等
    • Oracle: 智能合约背后与外部世界建立联系的方式,简单说可以看成API,在链上通过Oracle Contract完成数据交互
  3. UI: DApp的关键组件,方便用户操作。就用React/Vue即可
  4. Server: 不是必要的,比如Uniswap。对于特殊需要的,比如数据追踪,可以拉起一个后端做只读服务;另外,对于涉及到钱包登陆的DApp,为了安全性考虑也会将一些验证逻辑封装在后端
  5. web3常用编程语言: Solidity,Rust,Go,JS Family(TypeScript)。性能够用的情况下,尽可能选用开发效率高的,人生苦短,兵贵神速

安装与初始化本地Ethereum

我们首先要在本地搭建一个私有链进行测试用,也就是自动mining的区块链调试网络。使用homebrew安装以太坊,命令如下:

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

而后,我们需要在一个以太坊目录(用户自定)下,创建名为genesis.json的文件,并填写如下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"config": {
"chainId": 33,
"homesteadBlock": 0,
"eip150Block": 0,
"eip155Block": 0,
"eip158Block": 0
},
"nonce": "0x0000000000000033",
"timestamp": "0x0",
"parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"gasLimit": "0x8000000",
"difficulty": "0x100",
"mixhash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"coinbase": "0x3333333333333333333333333333333333333333",
"alloc": {}
}

上述内容定义了创世区块的基本属性,其含义为:

接下来我们对于此私链来做一点测试训练,首先启动区块链进入dev模式:

1
2
$ geth --datadir data init genesis.json
$ geth --dev console 2>> geth-log

一些常用的指令与示例:

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
Welcome to the Geth JavaScript console!

instance: Geth/v1.10.9-stable/darwin-amd64/go1.17.1
coinbase: 0x405f684ab16943a63cf462e0826e247116158eae
at block: 0 (Thu Jan 01 1970 08:00:00 GMT+0800 (CST))
datadir:
modules: admin:1.0 clique:1.0 debug:1.0 eth:1.0 miner:1.0 net:1.0 personal:1.0 rpc:1.0 txpool:1.0 web3:1.0

To exit, press ctrl-d or type exit

# 查看区块高度,当前高度是0,只有创世区块
> eth.blockNumber
0

# 返回钱包管理的账户地址列表
> eth.accounts
["0x405f684ab16943a63cf462e0826e247116158eae"]

# 使用newAccount指令创建账户,参数为账户的锁定密码,创建2个试验账户
> personal.newAccount('123456')
"0xbe23c61668a1412b2db768c9bc114e416beb8d4b"
> personal.newAccount('123456')
"0x741c8176606257421cef9eeb2e64c5fb7785d982"
# 可以为账户设置别名
> user1=eth.accounts[0]
"0x405f684ab16943a63cf462e0826e247116158eae"
> user2=eth.accounts[1]
"0xbe23c61668a1412b2db768c9bc114e416beb8d4b"

# 查看余额,通常默认地址有很多初始化的token,新建账户则为0
> eth.getBalance(user1)
1.15792089237316195423570985008687907853269984665640564039457584007913129639927e+77
> eth.getBalance(user2)
0

# 解锁账户,默认密码为空
# 解锁后,发起转账
> personal.unlockAccount(user1)
Unlock account 0x405f684ab16943a63cf462e0826e247116158eae
Passphrase:
true
> eth.sendTransaction({from:user1,to:user2,value:web3.toWei(3,"ether")})
"0x91cf001e2eb691186522349c12e6f64f1993104d1ffd4a729bf0a02daec405d5"

#从user1向user2转账3个以太币
#命令运行后,提交交易立马回出发挖矿
eth.sendTransaction({from:user1,to:user2,value:web3.toWei(3,"ether")})

# miner默认为自动挖矿,有交易才会挖矿
> miner.stop()
> miner.start()

# 查看矿工的地址
> eth.coinbase

# 设置矿工地址
> miner.setEtherbase(eth.coinbase)

智能合约的编写

本地开发,推荐VSCode配合插件的形式。在线开发,可以尝试Remix IDE。这里我们需要尝试写一点Solidity,有关内容可以参考这个Tutorial。我们将会使用下面的测试合约与本地的Geth进行交互:

1
2
3
4
5
6
7
8
9
10
11
pragma solidity ^0.8.7;

contract Test {
string private content;
function set(string calldata _msg) public {
content = _msg;
}
function get() view public returns(string memory){
return content;
}
}

Truffle Suite

我们使用Truffle框架来集成智能合约。在给定的架构内对Solidity进行编写、编译、部署等相关的工作。

1
2
3
4
$ npm install -g truffle # 安装truffle
$ mkdir test
$ cd test
$ truffle init # 新建一个truffle项目

contracts文件夹下创建我们自己的合约:

Solidity

migrations文件夹下编写部署脚本:

部署JS

最后修改truffle-config.js指定部署配置(注意:图中的gas太高会导致部署失败,设置为3000000即可):

truffle_config.js

在部署前需要先启动Geth注意要包含--allow-insecure-unlock并解锁账户:

1
2
3
$ geth --datadir data0 --networkid 1108  --networkid 786 --http --http.api 'web3,eth,net,debug,personal' --http.corsdomain '*' --dev --allow-insecure-unlock console 2>>eth_log
$ >personal.unlockAccount(user)
$ >miner.start()

使用如下指令编译与部署合约:

1
2
$ truffle compile # 编译
$ truffle migrate # 部署

我们可以得到如下结果:

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
Compiling your contracts...
===========================
> Everything is up to date, there is nothing to compile.


Starting migrations...
======================
> Network name: 'development'
> Network id: 786
> Block gas limit: 11500000 (0xaf79e0)

...

2_deploy_contracts.js
=====================

Deploying 'Test'
----------------
> transaction hash: 0x11253af06954ee68476d0edb6359f636b1af68dc867bf567cf7790f991150378
> Blocks: 0 Seconds: 0
> contract address: 0x8DEd5fEb94f27b8DBEeF8894100C841e4b8478Ea
> block number: 3
> block timestamp: 1653303134
> account: 0x373241c533AF160A2129b4eE784DeDA383840C52
> balance: 115792089237316195423570985008687907853269984665640564039457.583576222027150341
> gas used: 262922 (0x4030a)
> gas price: 3.174859949 gwei
> value sent: 0 ETH
> total cost: 0.000834740527510978 ETH

> Saving migration to chain.
> Saving artifacts
-------------------------------------
> Total cost: 0.000834740527510978 ETH

Summary
=======
> Total deployments: 2
> Final cost: 0.001679010277510978 ETH

调用已经部署的合约

使用现有的web3相关库在DApp中调用合约很方便,这里我们先用truffle控制台来进行合约的调用演示:

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
# 启动控制台
$ truffle console

# 拿到合约实例
$ let ins = await Test.deployed()

truffle(development)> let accounts = await web3.eth.getAccounts()
truffle(development)> accounts
[
'0x373241c533AF160A2129b4eE784DeDA383840C52',
'0x9f8917fc0e9ebCf86e4DDc901775Ece98C08A44a'
]

# 调用合约中的方法
truffle(development)> ins.set("hello truffle")
{
tx: '0x86248c6f2cbf4ac864087d3cb140d703b3da2ba392791bb040bc5bd23abef3ea',
receipt: {
blockHash: '0xfb8095fb11e8fa170f00d7ed50d72a1d3f51d690b1ac1e015388c2b10b46b57e',
blockNumber: 11,
contractAddress: null,
cumulativeGasUsed: 29753,
effectiveGasPrice: '0xa32a5d08',
from: '0x373241c533af160a2129b4ee784deda383840c52',
gasUsed: 29753,
logs: [],
logsBloom: '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
status: true,
to: '0x8ded5feb94f27b8dbeef8894100c841e4b8478ea',
transactionHash: '0x86248c6f2cbf4ac864087d3cb140d703b3da2ba392791bb040bc5bd23abef3ea',
transactionIndex: 0,
type: '0x2',
rawLogs: []
},
logs: []
}
truffle(development)> ins.get()
'hello truffle'

DApp开发

以上,我们有了一个可供交互的合约,现在我们可以为这个合约开发DApp了。实际上,这个合约已经是EVM上可以执行的去中心应用了,但是这个应用没有界面,我们需要为其制作一个方便用户操作的界面,这里就和web2的开发没有任何区别了。DApp可以是纯web前端的应用,也可以包含一个中心部署的后端,这取决于最终需求,只是一旦包含了中心化部署的后端那么这个DApp就不在是一个纯去中心的应用了,可以算作web2.5

这里我们会需要用到web3.js这个基础的库。web3.js是什么?答:Ethereum JavaScript API,是一系列合约交互库的集合,支持IPC、HTTP和WebSocket。下面演示一个最基本的web3.js + html的合约调用DApp:

在HTML文件中引入web3.js

1
<script src="https://cdn.jsdelivr.net/npm/web3@latest/dist/web3.min.js"></script>

初始化如下内容:

  1. web3 provider:他在DApp中负责与区块链通信,也包括合约调用交互(产品级provider可以选用Metamask)
  2. ABI:合约的方法接口,一般为JSON-RPC Provider提供
  3. 合约地址:例子中为0x8DEd5fEb94f27b8DBEeF8894100C841e4b8478Ea

完整的测试代码如下:

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Test Solidity</title>
<script src="https://cdn.jsdelivr.net/npm/web3@latest/dist/web3.min.js"></script>
</head>
<body>
<input id="input_bar" name="input_bar" type="text" value="Default">
<input type="button" value="Test Contract">
<script>
// JSON-RPC provider
var web3 = new Web3(new Web3.providers.HttpProvider("http://127.0.0.1:8545"));
var abi=[
{
"constant": false,
"inputs": [
{
"name": "_msg",
"type": "string"
}
],
"name": "set",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "get",
"outputs": [
{
"name": "",
"type": "string"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
}
]
var addr="0x8DEd5fEb94f27b8DBEeF8894100C841e4b8478Ea"

document.querySelectorAll("input")[1].onclick = function() {
var contract = new web3.eth.Contract(abi, addr); // 基本上常见的eth交互接口都在web3.eth这里
var text = document.getElementById("input_bar").value;
// 通常对于会改变状态的调用使用send
contract.methods.set(text).send({from: "0x373241c533AF160A2129b4eE784DeDA383840C52"}, function(err, res) {
// 对于不改变状态的调用使用call
contract.methods.get().call(function(err, res) {
document.getElementById("result").innerHTML = "call get: " + res;
});
});
}
</script>
<div id="result"></div>
</body>
</html>

web3.js

以上,我们将DApp开发中最为核心的合约开发与部署,部署合约的前端交互等两个部分都讲解完了。

使用Dfinity

Dfinity,有个宏图伟志Internet Computer,不禁让我想起曾经的EOS。同样是依托WASM提供显著优于单纯智能合约的计算能力,但是一个作为L1网络,一个则是基于ETH。上述流程揭露了如果我们要开发一款基于ETH的DApp,不可避免的会存在一定程度上中心化的部分,但是如果我们选择了Dfinity那么,一个「纯」web3 DApp将成为可能。

凭借WASMActor Model,我们可以将自己的应用完全部署到Dfinity网络当中,具体请参考官方的整个例子。要特别注意到,Dfinity的Smart Contract已经和其DApp有机的融合在一起了,而不是ETH这样需要Provider桥接App和Smart Contract的模式,从技术上讲Difinity这种流程其实更符合一款web3应用的开发过程。

Demo

我们还需要ETH吗?这个问题的答案还是要看生态,生态和资产是核心,一方面ETH上积累的资产和生态是最多的,另一方面100% on-chain的web3应用从迭代、可维护性和自主性(中心化一定程度上还是有很重要的作用)等方面都值得探讨,所以两者还有很长时间的共存状态。而且当下区块链网络还有诸多的核心痛点需要解决,首当其冲就是Infra实在太匮乏,开发过程也比web2繁琐了很多。而且信息的追踪和索引能力基本没有,如果想做推广,依然得借助web2的平台,庆幸的是已经存在很多类似DMail这样的明星项目了,我们仍然需要web3的Google Search,web3的tiktok,web3的cloud service,web3的Oracle DB等。

千言万语一句话:BUIDL~

后记

可以关注的内容:

  1. 关于DApp开发更加贴近工业规范的内容,推荐guoyu的这篇文章
  2. Geth和Truffle都不是必须选用的,也可以尝试HardhatGanache
  3. Truffle官方的指南
  4. 尽可能多去查阅web3.js的官方文档
  5. web3.js中call与send的区别
  6. 真•web3: Difinity
  7. zkSync: L2 Scaling 解决方案
  8. Cardano: 全套基础设施Haskell语言开发的区块链
  9. tezos: 用OCaml语言开发的区块链
  10. Avalanche: DApp的开发生态目前还很不友好,Avalanche作为一个自称eco-friendly的项目值得了解