Replies: 2 comments 1 reply
-
awesome!征集为优秀笔记,可以在扩充下嘿嘿!!细聊 |
Beta Was this translation helpful? Give feedback.
0 replies
-
催更催更,哈哈~ |
Beta Was this translation helpful? Give feedback.
1 reply
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
本笔记内容围绕着「有前端开发基础的智能合约纯小白如何开发出自己的第一个 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