【Web3技术分享系列专题】- dapp开发实践

举报
yd_292720799 发表于 2023/04/25 16:08:36 2023/04/25
【摘要】 一、认识DApp技术栈 1.1 传统的App架构  首先,必须有一个地方来存储基本数据,也就是数据库;  其次,要有后端代码(用 Node.js、Java 或 Python 等语言编写),用于定义业务逻辑;  第三,还要有前端代码(通常用 JavaScript、HTML 和 CSS 编写),用于实现 UI 和交互; 1.2DApp架构  与传统的 App(包括 Web App 与 Mobi...

一、认识DApp技术栈

1.1 传统的App架构

  首先,必须有一个地方来存储基本数据,也就是数据库;

  其次,要有后端代码(用 Node.js、Java 或 Python 等语言编写),用于定义业务逻辑;

  第三,还要有前端代码(通常用 JavaScript、HTML 和 CSS 编写),用于实现 UI 和交互;
image.png

1.2DApp架构

  与传统的 App(包括 Web App 与 Mobile App)最大的不同点在于,DApp 的大量功能依赖直接与智能合约(以下简称合约)进行交互。
  DAPP主要包含以下技术栈,
  智能合约:通常指代运行在 EVM 兼容网络中的 Solidity 或其他合约语言代码,他们负责与用户交易我们发行的资产并储存 DApp 的链上状态。
  DApp:整合合约接口以及其他功能的应用程序界面,目前,它们大部分是 Web App,你可以用流行的框架例如 React/Vue 来进行编写。
  服务端(可选):大部分 DApp 仍然有他们的服务端逻辑,这意味着,你需要自己搭建服务环境,或使用流行的 BasS/FaaS 服务。当然,你也可以挑战完全不依赖服务端的方式来构建 DApp,就像 Uniswap 所做的那样。
  中间件(可选):将所有内容都存储在区块链上是很昂贵的,更新数据都需要收费,所以还有一些去中心化的链下存储解决方案如IPFS。更轻松地查询以太坊区块链上的数据,还有一些链下索引解决方案如The Graph。

二、智能合约

  就编程语言而言,在目前的 EVM 兼容链上,你可以使用 Solidity 或 Vyper 进行开发,在其他 L1s 区块链上,例如 Solana,你可以使用 Rust 来进行合约的开发;在 Layer2 方案 StarkNet 中,你可以使用 Cairo 来进行开发;
  在这些百花齐放的方案中,实际上存在着两种不同的合约运行环境,EVM 或非 EVM 方案,前者的代码都会被编译成 EVM bytecode,而后者则会采用各种各样的 runtime,各显神通。
  没有人能断言哪种合约编程语言的地位会成为 Web 世界的 JavaScript。
  本次分享主要使用 Solidity(以下简称 Sol) 作为范例阐述智能合约编码中应当注意的问题。
  •事务性:我们可以将区块链看成是一个事务性数据库,这意味着,要么我们在合约中编写的函数全部被执行,状态依次被修改,要么,所有的状态都会回滚到当初未曾被修改的样子。这意味着,我们在对智能合约进行编码的过程中,要十分注意函数 API 的设计,在具体的函数中,不应当对参数进行重载。同时,也意味着我们在进行错误处理时要十分小心。
  •错误处理:我们可以选择两种常用的错误处理方式,require(condition, ERR_MESSAGE) 或者 revert customError(),前者传入一个字符串代表错误,后者可以自定义错误类型。两种方式并无本质上的不同,并且都会导致 tx 失败。对于前端而言,我们都需要自定义错误类型来捕获这两种错误。
  •运行成本:合约的状态储存会消耗 Gas 费用(区块链的激励机制,作为付给运行节点的计算与储存费用)为此,在设计储存对象时,如何善用声明的内存是需要被考虑的问题之一。简单的法则是,不要为不需要的状态声明过多的内存空间,如果你需要优化一个合约的运行成本,可以考虑参考许多合约使用内联汇编来优化内存占用。为此,合约中的复杂数据结构必须声明储存空间位置,例如 storage, memory, calldata,每种位置所产生的费用会有很大不同。合约的函数也会有对应的函数类型声明,view 函数 与 pure 函数在外部调用时不需要承担 gas 费用,但改变状态的函数都需要消耗 gas。
  •注意:由于合约运行和储存成本高,许多对外部白名单进行管理的最佳实践是使用 MerkleProof。
  •合约事件:由于合约的函数调用是事务性的,并且无法为外部调用者(指代 DApp 或钱包用户)提供返回值,合约引入了事件的概念。
  事件通过向日志系统中写入特定数据的方式来实现函数修改的记录。我们可以通过监听和查询的方式列出一个合约注册的所有事件,实现对函数异步结果的查询和前端 UI 状态变更。合约事件以某个单一合约为 key 来进行索引,同时,在声明事件时,我们可以指定不多于三个 index key 来确保 DApp 前端对这些索引 key 的查询效率,例如:
event ModuleProposalCreated( address indexed module, bytes32 indexed id, address indexed sender, uint256 timestamp );
  如果你期望的查询是非常复杂的,包括一系列相关联的合约事件,更好的方法是采用 Relay 提供的 graph/webhook 来进行查询。

  •创建合约:我们可以通过合约创建其他合约,这意味着,合约可以成为其他合约的工厂合约或者代理合约。我们也可以通过外部调用者(钱包账户)向 0x00 地址发送合约创建操作来新建网络上的合约,这是我们进行测试和依赖工作流创建合约的方法。
  创建合约需要消耗大量 Gas 费用,通常,我们会使用特定工具在创建合约前预估并计算费用,

  •权限和可见性:合约不同于服务端代码,它对网络中的所有人是透明的,这里的透明不仅指的是合约的字节码,还包括它的公开和私有状态。这意味着,你不应当在合约中储存任何敏感数据,也不应当依赖区块当中的任何状态(比如区块高度和时间戳)作为核心业务逻辑的判断基准。

  •为此,发布一个未经权限控制的合约是十分危险的,任何外部账户都可以轻易地对某个合约进行修改,并通过发送消耗合约指令将合约中的资产转走。所以,除了特定的治理合约不受权限管制以外,推荐任何合约都必须至少依赖 Ownable 来进行基本的权限配置,同时,复杂合约可以使用 AccessControl 来进行管理。

  •安全性:如上所述,合约的安全性是非常重要和严谨的问题,在将合约发布到生产环境网络之前,确保你已阅读 ConsenSys 编写的合约代码安全最佳实践指南并遵守其中所有的约定,同时,确保合约有足够的测试用例并且较高的测试覆盖度。请不要带有侥幸心理发布未经任何测试的合约代码,并主观地希望它能够正常工作。
  •调用:合约可以调用其他合约,只需知道地址和 ABI,我们就可以在合约内部调用其他合约,需要注意的是,调用合约也是事务性操作,因此,你不需要通过手动管理异步操作的方式来等待返回结果。在合约内部调用其他合约需要消耗额外的 Gas 费用。调用合约可能由于 ABI 错误或者不支持某个函数方法而导致失败,但 Gas 费用并不会返还,我们需要确保在调用其他第三方合约前理解对方合约的接口(包括参数类型,顺序,返回结构)

  合约编程虽然不复杂,但大量的运行时限制和非冗余的设计,导致我们在进行合约编码时,不得不参考许多优秀的合约代码,才能保证我们的合约代码质量。

  使用 OpenZeppelin 来帮助进行合约开发,即可以提高代码的安全性,又可以提高开发效率。•使用 OpenZeppelin 来帮助进行合约开发,即可以提高代码的安全性,又可以提高开发效率。
  该代码库包含了经过社区审查的ERC代币标准、安全协议以及很多的辅助工具库,这些代码可以帮助开发者专注业务逻辑的,而无需重新发明轮子。
  虽然,OZ 的合约在 Gas 费用和效率上存在一些问题,但他们在安全性、代码完成度、可维护性、注释和测试方面都做的很好,是值得信赖的合约基础库。

  OpenZeppelin 的 Ownable合约提供的onlyOwner 修饰器是用来限制某些特定合约函数的访问权限。
我们很多时候需要这样做,因此这个模式在以太坊智能合约开发中非常流行。

  Ownable合约的部署账号会被当做合约的拥有者(owner),某些合约函数,例如转移所有权,就限制在只允许拥有者(owner)调用。

  进行访问控制另一个相对于Ownable合约 更高级一些的是使用 Roles 库, 它可以定义多个角色,对于需要多个访问层次的控制时,应当考虑使用Roles库。

  SafeMath库的作用是帮我们进行算术运中进行必要的检查,避免代码中因算术运算(如溢出)而引入漏洞。

  作为一个智能合约开发者,我们常常会思考如何减少合约的执行时间以及空间,节约代码空间的一个办法就是使用更少位数的整数类型。 但不幸的是,如果你使用uint8作为变量类型,那么在调用SafeMath库函数之前,就必须先将其转换为uint256类型,然后在调用SafeMath库函数之后,还需要再转换回uint8类型。SafeCast库的作用就在于可以帮你完成这些转换而无需担心溢出问题。

  有时候在Solidity合约中需要了解一个地址是普通钱包地址还是合约地址。 OpenZeppelin的Address库提供了一个方法isContract()可以帮我们解决这个问题。

  ERC721A 是知名 NFT 项目 Azuki 发布的 ERC721 改善版本,通过特定的位操作,他们实现了内存占用的优化,带来了批量 mint 低 Gas 费用的优势。如果你的项目涉及到大量 NFT 的铸造,可以参考它的合约代码来进行实现。

三、开发工作流与单元测试

  很多项目使用Hardhat来支持本地开发工作流。Hardhat 提供了一种简单的方式创建本地 EVM 兼容区块链开发的环境,并且支持直观的 debug 方式,此外,还有丰富的插件社区,帮助开发者完成一系列特定的需求。
  依赖 Hardhat,我们可以在本地创建 block 快速确认的开发环境,使用公开私钥的调试钱包作为测试用户,编译合约并发布到本地测试网络,编写并在内存网络中快速运行单元测试.
  Hardhat 还支持配置不同的区块链网络并通过工作流部署合约到生产环境,或者将某个高度的区块链 fork 到本地创建集成测试环境。它提供丰富齐全的文档,可以在他们的官网进行参考。

  单元测试
  编写合约的第二步是编写合约的单元测试,hardhat会自动寻找./test文件夹下的单元测试并运行它们。可以通过修改配置文件来修改该路径。

  hardhat支持很多插件来改善测试效率。

  hardhat-deploy 插件支持使用 evm_snapshot 快速地跳转到某个高度的区块链状态,因此,我们可以使用它在单元测试中维护测试前、中、后以及各种特定高度状态,极大地加快测试速度。
  hardhat-gas-reporter 插件帮助你了解运行单元测试中部署和执行合约方法消耗的 gas 费用,如果在本地环境变量中提供 COINMARKETCAP_API_KEY,它会自动将这些成本折算为美元或其他法币计价。
  solidity-coverage 插件提供单元测试覆盖率报告,这有助于开发团队理解合约是否得到了应有的测试。

四、客户端与前端开发

  许多 DApp 并不提供 Mobile App 版本,部分原因是由于构建一个跨平台钱包方案过于复杂,以及大部分 Web3 领域内的用户都在使用诸如 MetaMask 这类浏览器插件钱包,而它的移动端 App 体验并不好用。
  当然可以使用托管钱包服务来进行开发,让更多不熟悉 Web3 的用户使用邮箱或者密码登录,可以选择采用 Web3Auth 或 MagicLink 的方案。但这会带来一系列的安全问题与风险,况且,就算我们能够使用简单易用的钱包降低用户准入门槛,在许多国家,用户仍需要复杂的 kyc 才能获取到某些 token。

  使用何种视图框架并不会影响你的 DApp 体验,但是,这会影响到开发效率.
  ethers.js和web3.js,这两者都是构建 Web 前端的基础类库。
  事实上,相较于Vue 而言,React 的生态系统中目前拥有较多的活跃 Web3 开发者和相关依赖库,如果你没有特殊的偏好,可以尝试先采用 React 作为框架来进行开发。
  大量的重复工作都建立在优秀的开源项目基础上,在 DApp 编写过程中,推荐你使用一些优秀的前端库来减少工作量,并实现更好的代码交付质量。

  客户端开发的方案比较多样,流行的方案是 React Native(跨平台)Flutter(跨平台)Swift(iOS)和 Java (Android) 这些方案都有一些流行的依赖库可以借鉴。
  对于flutter而言,可以使用web3dart。
  对于 Swift(iOS)而言,可以使用 Argent labs 团队提供的web3.swift方案。
  对于 Java (Android) 而言,流行的方案之一是 Web3j。

五、服务端开发

  服务端一直是 DApp 被认为没那么「去中心化」的原因之一。

  服务端方面,你可以使用任何你喜欢的编程语言,运行环境和软件架构,没有什么特殊的限制,只要保证你选择的技术栈能和本地节点或者(通常是)Relay Network 进行交互即可。
  一些 SDK 和对应服务可以帮助我们更方便地在服务端与合约进行通信,Thirdweb 提供了一个 SaaS 合约开发平台,你可以通过它的前端 App 发布预设功能的合约,例如 NFT Drop 或者 NFT 交易平台,也可以使用它提供的第三方 access key 与已发布的合约进行通信(而无需依赖合约的 ABI)

  编写服务端并不意味着我们需要做完所有事,通常,我们使用 DApp 的服务端代码来储存没必要储存在合约中的「链下状态」。在合约中储存数据是十分昂贵的选择(至少目前看来)这种昂贵不仅涉及到我们部署合约中产生的费用,还涉及到每一次修改状态的函数请求带来的,用户需要付出的 gas 成本。所以,大部分时候,我们会使用自己的服务端来储存这些「链下状态」

  我们需要编写服务端 API 的原因之一是,链上状态储存的成本过高,以及反复地签名与交互对用户来说体验不佳。另外一些原因是,一些不重要的,可以被丢弃的数据并不需要放在合约中储存。

六、合约部署方案

  在 DApp 开发中,与传统产品最大的不同点之一是我们需要决定将产品核心逻辑的智能合约发布到那个网络(或者哪些网络)这意味着,DApp 需要有「跨平台跨网络」的支持能力。

  对于传统互联网开发人员来说,我们很容易理解「跨平台」,它是指我们需要为 App 界面提供 PC Web/Mobile Web,iOS/Android App 的各种版本。「跨网络」在 DApp 的开发中指的是,我们需要让 DApp 前端/客户端支持多个区块链网络。在那之前,我们需要决定哪些区块链网络是我们首选的发布环境。
  我们可以选择发布到某个区块链网络,或者发布到所有支持 EVM 兼容的网络中。不过,不同的区块链网络中的合约无法直接通信,资产也无法随意互换(可以采用跨链桥合约进行锁定和重新铸造)目前,大部分 DApp 只会选择某一个区块链网络进行发布。
  如果你的项目涉及到 NFT,我会推荐发布到 ETH 主网或储存了相当数量资产的网络,如果你的项目涉及 GameFi,可以考虑 TPS 高的区块链网络。如果你考虑 TPS 又同时注重资产安全,可以考虑使用 Layer2 网络。
  因此,选择部署的网络不存在绝对的最佳实践,可以参考个人的需求进行选择。

本文参考或涉及到的网站或链接:

【1】https://guoyu.mirror.xyz/RD-xkpoxasAU7x5MIJmiCX4gll3Cs0pAd5iM258S1Ek
【2】https://mp.weixin.qq.com/s/m15Cirid11cm8i4A8GoMPQ
【3】https://caos.me/dapp--01
【4】https://www.bilibili.com/video/BV1rF411F7eP/?vd_source=251a95ab5f1d3b193390605f21293f10

【版权声明】本文为华为云社区用户原创内容,未经允许不得转载,如需转载请自行联系原作者进行授权。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。