本文在Truffle框架的基础上给出一个可快速上手DApp的教程,并非工业化流程,其中对于真实的生产环境做了大量的精简,仅保留最关键的几个部分,对于初学者足以了解一个DApp的开发工作流程,但是对于工业级生产则远远不够。这里仅作为一个入门来一窥DApp
的基本概念。
DApp基本技术栈
一个DApp和常规的App最大的区别就是存在与去中心的区块链进行合约交互的环节,在以太坊区块链上可以运行的App实际上就是指合约。一个仅有合约的应用架构显然是很局限的,所以所谓的DApp通常都不是纯粹的去中心,其开发体系与web2
的应用基本无异,最大的不同除了链上合约调用外,服务端也成为了可选的部分。
关键组件:
- Web3 Provider: 负责与区块链通信的角色,可以对合约进行调用。常见的包含: MetaMask、web3.js等
- Relay: Provider背后真正负责与区块链交互的服务器集群,web3的XaaS;本地练习暂时不必要
- Smart Contract: Ethereum Virtual Machine上运行的合约bytecode代码,提供图灵完备的计算能力,可以做信息储存、NFT资产发行、投票等
- Oracle: 智能合约背后与外部世界建立联系的方式,简单说可以看成API,在链上通过Oracle Contract完成数据交互
- UI: DApp的关键组件,方便用户操作。就用React/Vue即可
- Server: 不是必要的,比如Uniswap。对于特殊需要的,比如数据追踪,可以拉起一个后端做只读服务;另外,对于涉及到钱包登陆的DApp,为了安全性考虑也会将一些验证逻辑封装在后端
- 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
文件夹下创建我们自己的合约:
在migrations
文件夹下编写部署脚本:
最后修改truffle-config.js
指定部署配置(注意:图中的gas太高会导致部署失败,设置为3000000即可):
在部署前需要先启动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>
|
初始化如下内容:
- web3 provider:他在DApp中负责与区块链通信,也包括合约调用交互(产品级provider可以选用Metamask)
- ABI:合约的方法接口,一般为JSON-RPC Provider提供
- 合约地址:例子中为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> 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); var text = document.getElementById("input_bar").value; contract.methods.set(text).send({from: "0x373241c533AF160A2129b4eE784DeDA383840C52"}, function(err, res) { contract.methods.get().call(function(err, res) { document.getElementById("result").innerHTML = "call get: " + res; }); }); } </script> <div id="result"></div> </body> </html>
|
以上,我们将DApp开发中最为核心的合约开发与部署,部署合约的前端交互等两个部分都讲解完了。
使用Dfinity
Dfinity,有个宏图伟志Internet Computer,不禁让我想起曾经的EOS。同样是依托WASM提供显著优于单纯智能合约的计算能力,但是一个作为L1网络,一个则是基于ETH。上述流程揭露了如果我们要开发一款基于ETH的DApp,不可避免的会存在一定程度上中心化的部分,但是如果我们选择了Dfinity
那么,一个「纯」web3 DApp将成为可能。
凭借WASM
和Actor 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~
后记
可以关注的内容:
- 关于
DApp
开发更加贴近工业规范的内容,推荐guoyu的这篇文章
- Geth和Truffle都不是必须选用的,也可以尝试
Hardhat
、Ganache
等
- Truffle官方的指南
- 尽可能多去查阅web3.js的官方文档
- web3.js中call与send的区别
- 真•web3: Difinity
- zkSync: L2 Scaling 解决方案
- Cardano: 全套基础设施Haskell语言开发的区块链
- tezos: 用OCaml语言开发的区块链
- Avalanche: DApp的开发生态目前还很不友好,Avalanche作为一个自称eco-friendly的项目值得了解