Replies: 2 comments 1 reply
-
awesome!征集为优秀笔记,可以在扩充下嘿嘿!!细聊 |
Beta Was this translation helpful? Give feedback.
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
-
本笔记内容围绕着「有前端开发基础的智能合约纯小白如何开发出自己的第一个 NFT market dApp」去写,也就是说,会涵盖 task 3、task 4 和 task 5。
由于我是刚向 Web3 转型的初学者,很多东西不太懂,以下内容仅代表个人理解,有错漏谬误欢迎指出。
我认为「智能合约」这个名字源于它所起到的业务作用,而对于开发者来说,它仅仅是软件程序而已,需要用某种编程语言去写代码。
所以,要想编写以太坊的智能合约,就得学习并了解 Solidity 语法、ERC 及链上交互流程,这几个理解了代码就能写对了,剩下的是部署。
学习 Solidity
编程经验丰富的人只要搂一眼就知道 Solidity 是面向对象的静态类型语言,虽说有一些陌生的关键字,但不妨碍我把它整体看作是披着「合约」外衣的「类」。
因而,对 TS、Java 等有类型的基于类的编程语言熟悉的话,能够通过建立映射关系很快地初步了解 Solidity。
contract
关键字可认为是class
关键字的领域特定变形,更加语义化地表达「合约」这个概念,因而写一个合约相当于写类。状态变量用于存储合约内的数据,相当于类的成员变量,即类属性。
函数既可定义在合约内部,也可在外部——前者相当于类的成员函数,即类方法;后者则是普通函数,通常是一些工具函数。
不像 TS 和 Java,在 Solidity 中访问可见性标识不是在最前面,而且对变量与函数来说位置是不一致的,这有点反直觉。
private
与public
的语义跟其他语言是一样的,但没有protected
,取而代之的是internal
,另外还多了一个表示仅供外部调用的external
。函数修饰符相当于 TS 装饰器或 Java 注解,可以进行面向切面编程,即 AOP;函数与函数修饰符都可被衍生的合约覆盖。
以下几种类型都可看作是 ES 中的对象,但使用场景有所不同:
struct
)用于定义实体;enum
)是有限选项的集合;mapping
)则是无限的选项。Solidity 支持多重继承与函数多态性,能够更好地组合复用;由于合约的开发有 ERC 驱动的倾向,多重继承的副作用应该不会像在其他语言中那么严重。
鉴于 Solidity 是为区块链而生,以及区块链本身及应用场景的特性,通过事件与外部通信和遇到错误时回滚之前的操作可以说是「刚需」,所以在语法层面支持事件与错误相关处理。
require()
这个函数的用法对我来说也是有点特别的,require(initialValue > 999, "Initial supply must be greater than 999.");
就相当于以下 ES 代码的简明语义化版:了解 ERC
在以太坊中,「ERC」的全称为「Ethereum Request for Comments」,是 EIP(Ethereum Improvement Proposal)的一个类型,定义了智能合约应用程序相关标准和约定。
由于 Web3 所推崇的是去中心化与开放性,保障智能合约应用程序的互操作性就成了基本要求,因此作为这方面标准的 ERC 就显得十分重要。
以太坊智能合约应用程序开发中最基本的 ERC 有以下两个:
实际上,可把 ERC 看作是权威的 API 文档。
编写智能合约
开发智能合约应用程序时,需要选择一个框架来辅助,貌似用 Hardhat 和 Foundry 的比较多——我选用前者,因为它对 JS 技术栈友好,即对从前端开发转型的人友好。
在 IDE 的选择上,很多人会去使用以太坊官方提供的 Remix,而我则继续使用 VS Code,主要是想在刚入门时尽量减少学习成本。
对 Hardhat 不了解的话,可按照官方教程选择性地一步步搭建运行环境,所生成的目录结构中除了
hardhat.config.ts
这个配置文件外,基本只需关注 4 个文件夹及其文件:contracts
——智能合约源码;artifacts
——通过hardhat compile
生成的编译后文件;ignition
——基于 Hardhat Ignition 部署智能合约用的;test
——智能合约功能测试代码。在
ignition
中也会生成编译后的文件,但与artifacts
不同,是跟被部署的目标链绑定的,也就是生成到要部署的链 ID 的文件夹下。作为训练营作业的那 3 个 task,都涉及到 ERC-20 代币、ERC-721 代币和 NFT 市场这 3 个合约,其中前两个代币合约可借助经过验证的 OpenZeppelin Contracts,以其为基础进行扩展。
我的 ERC-20 代币 RaiCoin 的实现代码如下:
最好是在初始化时就 mint 一定量的代币(通常数目很大),并把拥有者设为自己的账户地址,否则在过后进行交易时会提示没有余额,处理起来更麻烦。
上面代码中的
msg.sender
在constructor()
中时实际上是部署合约的账户地址,如果是用自己的账户地址部署,那初始代币就全进自己账户中了。由于自己的 ERC-20 代币只是随便玩玩的性质,并不会增值,可以考虑覆盖 OpenZeppelin 中的
decimals()
而把数值设置小点。下面是 ERC-721 代币 RaiE 的实现代码:
我只额外实现了一个
mint()
,且不带任何参数,只是单纯地发币,这是为什么呢?NFT 不是该有相应的图片吗?具体原因下文会说。这两个代币合约算是白给的,自己无需写多少代码,真正需要思考的地方主要集中在 NFT 市场合约当中,比如——
市场中的 NFT 列表是否要分页?
分页的话,每次翻页时的延迟会比较明显,前端的用户体验不好;但不分页的话,NFT 数量多时也会有这种问题。
NFT 的图片 URL 该存哪里?是 NFT 合约还是市场合约中?
理论上该存进 NFT 合约,但若如此,获取 NFT 列表时就会频繁通过外部调用的方式访问 NFT 合约,影响性能与用户体验。
应该在 NFT 合约中维护一个「谁拥有哪些代币」的可被外部获取的列表吗?
若要有,数据与市场合约中相比是冗余的,会显得 NFT 合约很是臃肿;若没有,就无法显性地知道都有哪些代币,分别属于谁。
可以看出,仅依赖区块链相关技术去做一个产品级的应用,就目前而言是有很大局限性的,用户体验会很差!
也就是说,产品的性能和体验还是得靠以往的应用架构去支撑,区块链仅作为身份验证及部分数据的「备份」用。
因此,我暂时放弃了以做产品为导向的思维方式,不去纠结哪里是否合理之类的事情,转变为先满足作业要求为主——只要有相关功能就行。
这样一来,决策就很容易做了——怎样能更快地完成作业就怎么来!于是,上面的 3 个疑惑很快就消除了:
在实现 NFT 市场 RaiGallery 时我发现,只有数组是可被遍历的,
mapping
不行,并且初始化时指定长度的数组不能用.push()
添加元素,只能用索引:调试智能合约
写完智能合约源码,就得先写测试代码过一遍,把一些基础的问题暴露出来并解决掉。
如上文所述,在 Hardhat 项目中测试代码是放在
test
文件夹下的,基本是每个文件对应一个合约,当然也可将不同文件间的可复用逻辑提取出来放到额外的文件中,如helper.ts
。测试代码是基于 Mocha 和 Chai 的 API 去写,在真正开始测试合约功能之前,需要先部署合约到本地环境中,可以是内置的
hardhat
,也可启动一个本地节点localhost
,我暂且选择前者。这时,部署的方式能够复用 Hardhat Ignition 模块,但我还没搞懂它是怎么用的,就采用更容易理解的
loadFixture()
。搞测试还挺费劲的,感觉差不多一天的时间都耗进去了,但在这个过程中我对 ERC-20 代币、ERC-721 代币、NFT 市场及用户这四方之间该如何交互有了更深的了解,如:
合约实例.connect(某个账户)
后再去调用才能模拟与用户间的操作;.setApprovalForAll(市场合约地址, true)
把自己的全部 NFT 授权给 NFT 市场后才能在市场中上架出售。觉得智能合约的单方测试差不多了,就该部署到本地节点与前端进行联调了,这回要用到 Hardhat Ignition 模块了。
在去看文档学习时,感觉有点晦涩难懂,看着看着就想睡觉的那种;但现在再回过头看,每个模块实际上就是在描述部署该模块对应的合约时该如何初始化。
Hardhat Ignition 支持子模块,通过
.useModule()
使用,能够在编译并部署模块时把子模块一同处理了,也就是说——假设我有
RaiCoin.ts
、RaiE.ts
和RaiGallery.ts
三个模块,其中RaiGallery.ts
在部署时需要RaiCoin.ts
部署后返回的地址,那就可将RaiCoin.ts
作为RaiGallery.ts
的子模块:这样的话,
RaiE.ts
是单独部署,而在部署RaiGallery.ts
时会级联部署RaiCoin.ts
,所以只执行两次部署命令即可。接着,把
hardhat.config.ts
中的defaultNetwork
配置项改为'localhost'
,在 Hardhat 项目根目录下执行npx hardhat node
启动本地节点,再开启一个终端窗口部署智能合约:npx hardhat ignition deploy ./ignition/modules/RaiE.ts
部署 ERC-721 代币合约;npx hardhat ignition deploy ./ignition/modules/RaiGallery.ts
部署 ERC-20 代币合约和 NFT 市场合约。全部部署成功后,会在
ignition/deployments/chain-31337
文件夹(「31337」是本地节点的链 ID)中生成编译后的合约相关文件:deployed_addresses.json
中罗列了合约地址;artifacts
文件夹下的 JSON 文件中包含了合约的 ABI。上述两项关键信息需要复制粘贴到前端项目的全局共用变量中,以供联调时使用。
在开始联调之前,需在 MetaMask 钱包中做两件事:
我在前端部分所依赖的第三方库和框架主要有 Vite、React、Ant Design Web3 和 Wagmi;由于前端是我所熟悉的,没啥心得体会,就不多赘述了。
但是,在开发前端部分时,有一个点让我纠结了一段时间——
虽说程序上是要先 mint 出一个新的 NFT 才能上架到市场进行交易,但在界面上的体现应该是一步到位的,即填完 NFT 相关信息点「确定」后就直接上架了。
而作业的要求是先 mint 后上架的两步操作,这让我觉得有点不合理,或者说用户体验不好。
最终还是因自己对 Wagmi 使用不熟而实在没想出实现方案,且急于交作业,就没再继续纠结下去……😂😂😂
联调时若遇到问题卡住,可按下面步骤依次排查:
setApprovalForAll
对市场合约进行授权,以托管市场代为转移 NFT;parseUnits
转换为符合自己 ERC-20 代币合约中定义的decimals()
的数(默认是18
);approve
对市场合约进行授权,以托管市场代为转账。联调也结束了,终于,到了最后一个环节——部署到 Sepolia 测试网!
这需要有 Sepolia 的以太币,一般的获取方式是到那些「水龙头」一滴一滴地接,每天只能弄一丁点儿,多亏 @Mika-Lahtinen 提供了一种 PoW 的方式,详见 @zer0fire 的笔记《🚀极简拧水龙头教程 - 无需交易记录或账户余额》。
此时,将目光移回到 Hardhat 项目中,打开
hardhat.config.ts
文件,将defaultNetwork
临时改为'sepolia'
,并在networks
中添加一个sepolia
:其中,Sepolia endpoint 可通过注册 Infura 或 Alchemy 账号获得。
然后,按照上文中部署到本地节点的流程再走一遍,在前端把测试网环境的功能验证通过后就可以提交作业啦啦啦啦啦!
结语
我把 NFT market 这个 dApp 相关的代码全部在
ourai/my-first-nft-market
中开源了,打算日后把上文谈及所纠结的点尽量都解决掉,并打造成这类 demo 的标杆。由于里面已经配置了 Sepolia 合约地址,可直接本地运行操作,欢迎参考,探讨和指点。
Beta Was this translation helpful? Give feedback.
All reactions