SVG NFT 全面实践 ── scaffold-eth loogies-svg-nft 项目完整指南
注:这篇文章是我投稿于“李大狗Leeduckgo”公众号的文章,原文地址:SVG NFT 全面实践 | Web3.0 dApp 开发(六)。
loogies-svg-nft 是 scaffold-eth 提供的一个简单的 NFT 铸造和展示的项目,在本教程中,我们将带领大家一步步分析和实现这个项目。
由于项目的 loogies-svg-nft
分支与 master
分支在组件库和主页上有一些变化,故先将 master
分支代码与 loogies-svg-nft
分支进行了合并,解决冲突,得到一份基新组件库的全新的代码。可以参考项目地址: https://github.com/qiwihui/scaffold-eth.git 的 loogies-svg-nft
分支。本文以下内容将基于这些代码进行部署和分析。
本地运行和测试
首先我们先运行项目查看我们将要分析实现的功能。
本地运行
首先我们在本地运行项目:
clone 项目并切换到 loogies-svg-nft
分支:
git clone https://github.com/qiwihui/scaffold-eth.git loogies-svg-nft
cd loogies-svg-nft
git checkout loogies-svg-nft
安装依赖包
yarn install
运行前端
yarn start
在第二个终端窗口中,运行本地测试链
yarn chain
在第三个终端窗口中,运行部署合约
yarn deploy
此时在浏览器中访问 http://localhost:3000
,就可以看到程序了。
本地测试
-
首先在 MetaMask 钱包中添加本地网络,并切换到本地网络;
- 网络名称:
Localhost 8545
- 新增 RPC URL:
http://localhost:8545
- 链 ID:
31337
- Currency Symbol:
ETH
- 网络名称:
-
创建一个新的本地钱包账号;
-
复制钱包地址,在页面左下角给这个地址发送一些测试 ETH;
-
点击在页面右上角
connect
连接钱包; -
点击 Mint 铸造;
-
当交易成功后,可以看到新铸造的 NFT;
下面,我们开始对项目合约进行分析。
Loogies 合约分析
NFT 与 ERC721
NFT,全称为Non-Fungible Token,指非同质化代币,对应于以太坊上 ERC-721 标准。 一般在智能合约中,NFT 的定义包含 tokenId
和 tokenURI
,每一个 NFT 的 tokenId
是唯一的, tokenURI
对于保存了NFT的元数据,可以是图像URL、描述、属性等。如果一个 NFT 想在 NFT 市场上进行展示和销售,则 tokenURI
内容需要对应符合 NFT 市场的标准,比如,在 NFT 市场 OpenSea 元数据标准中,就指出了 NFT 展示需要设置的属性。
OpenSea 中 NFT 元数据与展示对应关系
合约概览
loogies-svg-nft 项目的合约文件在 packages/hardhat/contracts/
路径下,包含以下三个文件:
packages/hardhat/contracts/
├── HexStrings.sol
├── ToColor.sol
└── YourCollectible.sol
HexString.sol
:生成地址字符串;ToColor.sol
:生成颜色编码字符串;YourCollectible.sol
:Loogies
NFT的合约文件,主要功能涉及合约铸造和元数据生成。
合约的主要结构和方法为:
contract YourCollectible is ERC721, Ownable {
// 构造函数
constructor() public ERC721("Loogies", "LOOG") {
}
// 铸造 NFT
function mintItem()
public
returns (uint256)
{
...
}
// 获取 tokenId 对应 tokeURI
function tokenURI(uint256 id) public view override returns (string memory) {
...
}
// 生成 tokenId 对应 svg 代码
function generateSVGofTokenById(uint256 id) internal view returns (string memory) {
...
}
// 生成 tokenId 对应 svg 代码,主要用于绘制图像
function renderTokenById(uint256 id) public view returns (string memory) {
...
}
}
构造函数
constructor() public ERC721("Loogies", "LOOG") {
// RELEASE THE LOOGIES!
}
代币符号: Loogies
代币名称: LOOG
合约继承自 OpenZeppelin 的 ERC721.sol
,这是 OpenZeppelin 提供的基本合约代码,可以方便开发者使用。
应用库函数
合约中分别对 uint256
, uint160
和 bytes3
等应用了不同库函数,扩展对应功能:
// 使 uint256 具有 toHexString 功能
using Strings for uint256;
// 使 uint160 具有自定义 toHexString 功能
using HexStrings for uint160;
// 使 bytes3 可以方便生成前端颜色表示
using ToColor for bytes3;
// 计数功能
using Counters for Counters.Counter;
Mint 期限
以下代码是 Mint 时间限制:
uint256 mintDeadline = block.timestamp + 24 hours;
function mintItem()
public
returns (uint256)
{
require( block.timestamp < mintDeadline, "DONE MINTING");
...
合约在部署之后的24小时内可以铸造,超过24小时则会引发异常。这个机制类似于预售,由于这个合约比较简单,所以没有使用白名单机制,一般在实际情况,会使用预售和白名单的方式来控制 NFT 的发行。
Mint 铸造
铸造 NFT 其实就是在合约中设置两个信息:
tokenId
及其owner
tokenId
及其tokenURI
我们首先看铸造函数 mintItem
:
// 用于保存每一个铸造的 Loogies 的特征,其中,color 表示颜色,chubbiness 表示胖瘦
mapping (uint256 => bytes3) public color;
mapping (uint256 => uint256) public chubbiness;
...
function mintItem()
public
returns (uint256)
{
require( block.timestamp < mintDeadline, "DONE MINTING");
// 每次铸造前自增 _tokenIds,确保 _tokenIds 唯一
_tokenIds.increment();
uint256 id = _tokenIds.current();
// 铸造者与 tokenId 绑定
_mint(msg.sender, id);
// 随机生成对应 tokenId 的属性
bytes32 predictableRandom = keccak256(abi.encodePacked( blockhash(block.number-1), msg.sender, address(this), id ));
color[id] = bytes2(predictableRandom[0]) | ( bytes2(predictableRandom[1]) >> 8 ) | ( bytes3(predictableRandom[2]) >> 16 );
chubbiness[id] = 35+((55*uint256(uint8(predictableRandom[3])))/255);
return id;
}
其中:
tokenId
在每次铸造时会自增,确保tokenId
唯一;_mint
函数绑定tokenId
及其owner
;- 每一个
tokenId
对应的属性通过随机方式生成,具体为:- 通过前一个区块的哈希(
blockhash(block.number-1)
),当前铸造账户(msg.sender
),合约地址(address(this)
)和tokenId
生成哈希predictableRandom
; - 计算 NFT 颜色:按位或
predictableRandom
前三位得到颜色,颜色表示用 bytes3 表示,其中bytes2(predictableRandom[0])
对应最低位蓝色数值,( bytes2(predictableRandom[1]) >> 8 )
对应中间位绿色数值,( bytes3(predictableRandom[2]) >> 16 )
对应最高位红色数值; - 计算 NFT 胖瘦:
35+((55*uint256(uint8(predictableRandom[3])))/255);
,uint8(predictableRandom[3])
介于0~255,故最小值为35,最大值为 35+55 = 90;
- 通过前一个区块的哈希(
例如: color
为 0x4cc4c1
, chubbiness
为 88 时对应的 NFT 图片为:
tokenURI 函数
函数 tokenURI
接受 tokenId
参数,返回编码之后的元数据字符串:
function tokenURI(uint256 id) public view override returns (string memory) {
// 检查 id 是否存在
require(_exists(id), "not exist");
string memory name = string(abi.encodePacked('Loogie #',id.toString()));
string memory description = string(abi.encodePacked('This Loogie is the color #',color[id].toColor(),' with a chubbiness of ',uint2str(chubbiness[id]),'!!!'));
// 生成图片的svg base64 编码
string memory image = Base64.encode(bytes(generateSVGofTokenById(id)));
return
string(
abi.encodePacked(
'data:application/json;base64,',
// 通过 base64 编码元数据
Base64.encode(
bytes(
abi.encodePacked(
'{"name":"',
name,
'", "description":"',
description,
'", "external_url":"https://burnyboys.com/token/',
id.toString(),
'", "attributes": [{"trait_type": "color", "value": "#',
color[id].toColor(),
'"},{"trait_type": "chubbiness", "value": ',
uint2str(chubbiness[id]),
'}], "owner":"',
(uint160(ownerOf(id))).toHexString(20),
'", "image": "',
'data:image/svg+xml;base64,',
image,
'"}'
)
)
)
)
);
}
// 生成的 SVG 字符串
function generateSVGofTokenById(uint256 id) internal view returns (string memory) {
...
}
// 绘制图像
// Visibility is `public` to enable it being called by other contracts for composition.
function renderTokenById(uint256 id) public view returns (string memory) {
...
}
其中, generateSVGofTokenById
函数返回 tokenId
对应的颜色和胖瘦属性生成的 SVG 字符串, renderTokenById
用户绘制图像。
我们可以看到,NFT 元数据中包含的属性有:
- name:名称
- description: 描述
- external_url:外部链接
- attributes:属性
- color 颜色
- chubbiness:胖瘦
- owner:所有者,以太坊地址16进制形式
- image:图片对应 SVG 的 base64 编码
这里,我们通过实际数据了解一下什么是 SVG。tokenId
为 1 时对应的 tokenURI
结果为:
data:application/json;base64,eyJuYW1lIjoiTG9vZ2llICMxIiwiZGVzY3JpcHRpb24iOiJUaGlzIExvb2dpZSBpcyB0aGUgY29sb3IgIzRjYzRjMSB3aXRoIGEgY2h1YmJpbmVzcyBvZiA4OCEhISIsImV4dGVybmFsX3VybCI6Imh0dHBzOi8vYnVybnlib3lzLmNvbS90b2tlbi8xIiwiYXR0cmlidXRlcyI6W3sidHJhaXRfdHlwZSI6ImNvbG9yIiwidmFsdWUiOiIjNGNjNGMxIn0seyJ0cmFpdF90eXBlIjoiY2h1YmJpbmVzcyIsInZhbHVlIjo4OH1dLCJvd25lciI6IjB4MTY5ODQxYWEzMDI0Y2ZhNTcwMDI0ZWI3ZGQ2YmY1Zjc3NDA5MjA4OCIsImltYWdlIjoiZGF0YTppbWFnZS9zdmcreG1sO2Jhc2U2NCxQSE4yWnlCM2FXUjBhRDBpTkRBd0lpQm9aV2xuYUhROUlqUXdNQ0lnZUcxc2JuTTlJbWgwZEhBNkx5OTNkM2N1ZHpNdWIzSm5Mekl3TURBdmMzWm5JajQ4WnlCcFpEMGlaWGxsTVNJK1BHVnNiR2x3YzJVZ2MzUnliMnRsTFhkcFpIUm9QU0l6SWlCeWVUMGlNamt1TlNJZ2NuZzlJakk1TGpVaUlHbGtQU0p6ZG1kZk1TSWdZM2s5SWpFMU5DNDFJaUJqZUQwaU1UZ3hMalVpSUhOMGNtOXJaVDBpSXpBd01DSWdabWxzYkQwaUkyWm1aaUl2UGp4bGJHeHBjSE5sSUhKNVBTSXpMalVpSUhKNFBTSXlMalVpSUdsa1BTSnpkbWRmTXlJZ1kzazlJakUxTkM0MUlpQmplRDBpTVRjekxqVWlJSE4wY205clpTMTNhV1IwYUQwaU15SWdjM1J5YjJ0bFBTSWpNREF3SWlCbWFXeHNQU0lqTURBd01EQXdJaTgrUEM5blBqeG5JR2xrUFNKb1pXRmtJajQ4Wld4c2FYQnpaU0JtYVd4c1BTSWpOR05qTkdNeElpQnpkSEp2YTJVdGQybGtkR2c5SWpNaUlHTjRQU0l5TURRdU5TSWdZM2s5SWpJeE1TNDRNREEyTlNJZ2FXUTlJbk4yWjE4MUlpQnllRDBpT0RnaUlISjVQU0kxTVM0NE1EQTJOU0lnYzNSeWIydGxQU0lqTURBd0lpOCtQQzluUGp4bklHbGtQU0psZVdVeUlqNDhaV3hzYVhCelpTQnpkSEp2YTJVdGQybGtkR2c5SWpNaUlISjVQU0l5T1M0MUlpQnllRDBpTWprdU5TSWdhV1E5SW5OMloxOHlJaUJqZVQwaU1UWTRMalVpSUdONFBTSXlNRGt1TlNJZ2MzUnliMnRsUFNJak1EQXdJaUJtYVd4c1BTSWpabVptSWk4K1BHVnNiR2x3YzJVZ2NuazlJak11TlNJZ2NuZzlJak1pSUdsa1BTSnpkbWRmTkNJZ1kzazlJakUyT1M0MUlpQmplRDBpTWpBNElpQnpkSEp2YTJVdGQybGtkR2c5SWpNaUlHWnBiR3c5SWlNd01EQXdNREFpSUhOMGNtOXJaVDBpSXpBd01DSXZQand2Wno0OEwzTjJaejQ9In0=
通过 base64 解码 data:application/json;base64,
之后的字符串可以得到如下 json(以下 json 经过了格式化,方便阅读):
{
"name": "Loogie #1",
"description": "This Loogie is the color #4cc4c1 with a chubbiness of 88!!!",
"external_url": "https://burnyboys.com/token/1",
"attributes": [
{
"trait_type": "color",
"value": "#4cc4c1"
},
{
"trait_type": "chubbiness",
"value": 88
}
],
"owner": "0x169841aa3024cfa570024eb7dd6bf5f774092088",
"image": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAwIiBoZWlnaHQ9IjQwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48ZyBpZD0iZXllMSI+PGVsbGlwc2Ugc3Ryb2tlLXdpZHRoPSIzIiByeT0iMjkuNSIgcng9IjI5LjUiIGlkPSJzdmdfMSIgY3k9IjE1NC41IiBjeD0iMTgxLjUiIHN0cm9rZT0iIzAwMCIgZmlsbD0iI2ZmZiIvPjxlbGxpcHNlIHJ5PSIzLjUiIHJ4PSIyLjUiIGlkPSJzdmdfMyIgY3k9IjE1NC41IiBjeD0iMTczLjUiIHN0cm9rZS13aWR0aD0iMyIgc3Ryb2tlPSIjMDAwIiBmaWxsPSIjMDAwMDAwIi8+PC9nPjxnIGlkPSJoZWFkIj48ZWxsaXBzZSBmaWxsPSIjNGNjNGMxIiBzdHJva2Utd2lkdGg9IjMiIGN4PSIyMDQuNSIgY3k9IjIxMS44MDA2NSIgaWQ9InN2Z181IiByeD0iODgiIHJ5PSI1MS44MDA2NSIgc3Ryb2tlPSIjMDAwIi8+PC9nPjxnIGlkPSJleWUyIj48ZWxsaXBzZSBzdHJva2Utd2lkdGg9IjMiIHJ5PSIyOS41IiByeD0iMjkuNSIgaWQ9InN2Z18yIiBjeT0iMTY4LjUiIGN4PSIyMDkuNSIgc3Ryb2tlPSIjMDAwIiBmaWxsPSIjZmZmIi8+PGVsbGlwc2Ugcnk9IjMuNSIgcng9IjMiIGlkPSJzdmdfNCIgY3k9IjE2OS41IiBjeD0iMjA4IiBzdHJva2Utd2lkdGg9IjMiIGZpbGw9IiMwMDAwMDAiIHN0cm9rZT0iIzAwMCIvPjwvZz48L3N2Zz4="
}
我们对 image
字段进行解码并格式化就得到图片的 SVG:
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="400" height="400">
<g id="eye1">
<ellipse stroke-width="3" ry="29.5" rx="29.5" id="svg_1" cy="154.5" cx="181.5" stroke="#000" fill="#fff" />
<ellipse ry="3.5" rx="2.5" id="svg_3" cy="154.5" cx="173.5" stroke-width="3" stroke="#000" fill="#000000" />
</g>
<g id="head">
<ellipse fill="#4cc4c1" stroke-width="3" cx="204.5" cy="211.80065" id="svg_5" rx="88" ry="51.80065" stroke="#000" />
</g>
<g id="eye2">
<ellipse stroke-width="3" ry="29.5" rx="29.5" id="svg_2" cy="168.5" cx="209.5" stroke="#000" fill="#fff" />
<ellipse ry="3.5" rx="3" id="svg_4" cy="169.5" cx="208" stroke-width="3" fill="#000000" stroke="#000" />
</g>
</svg>
SVG是一种用 XML 定义的语言,用来描述二维矢量及矢量/栅格图形。它可以任意放大图形显示,也不会牺牲图像质量,它可以使用代码进行描述,方便编辑,因此被广泛使用。
从上面的代码结合以下的图像可以看出,这个 SVG 包含如下内容:
- 第一行为 XML 声明,标明版本和编码类型,之后是SVG 的宽度和高度;
eye1
:由两个椭圆(ellipse)绘制的眼圈和黑色眼珠;head
:填充#4cc4c1
颜色的椭圆作为身体;eye2
:与eye1
一致,位置不同;
eye1
,head
和eye2
依次叠加得到最终的图形:
辅助函数解析
uint2str
将uint
转变为字符串,例如123
变为'123'
function uint2str(uint _i) internal pure returns (string memory _uintAsString) {
if (_i == 0) {
return "0";
}
uint j = _i;
// uint 位数
uint len;
while (j != 0) {
len++;
j /= 10;
}
bytes memory bstr = new bytes(len);
uint k = len;
while (_i != 0) {
k = k-1;
// _i 个位数字
uint8 temp = (48 + uint8(_i - _i / 10 * 10));
bytes1 b1 = bytes1(temp);
bstr[k] = b1;
_i /= 10;
}
return string(bstr);
}
ToColor.sol
库:将byte3
类型转换为前端颜色字符串,例如:输入0x4cc4c1
输出'4cc4c1'
library ToColor {
bytes16 internal constant ALPHABET = '0123456789abcdef';
function toColor(bytes3 value) internal pure returns (string memory) {
bytes memory buffer = new bytes(6);
for (uint256 i = 0; i < 3; i++) {
buffer[i*2+1] = ALPHABET[uint8(value[i]) & 0xf];
buffer[i*2] = ALPHABET[uint8(value[i]>>4) & 0xf];
}
return string(buffer);
}
}
HexStrings.sol
库:主要作用是将uint
按length
位提取,对应于生成公钥时截取前20位的功能:(*uint160*(ownerOf(id))).toHexString(20)
,此表达式生成对应tokenId
所有者的地址。
library HexStrings {
bytes16 internal constant ALPHABET = '0123456789abcdef';
function toHexString(uint256 value, uint256 length) internal pure returns (string memory) {
bytes memory buffer = new bytes(2 * length + 2);
buffer[0] = '0';
buffer[1] = 'x';
for (uint256 i = 2 * length + 1; i > 1; --i) {
buffer[i] = ALPHABET[value & 0xf];
value >>= 4;
}
return string(buffer);
}
}
至此,合约源码分析完成。
下面我们将对前端的逻辑进行简要分析,然后我们将一步步实现 NFT 铸造和展示的功能。将代码切换到前端代码提交之前,按照以下的步骤一步步添加功能。
git checkout a98156f6a03a0bc8fc98c8c77cef6fbf59f03b31
前端逻辑分析
项目前端文件在 packages/react-app
内,以下文章中涉及文件的位置都将在这个文件中寻找。
我们首先来看一下 src/App.jsx
,这是项目的主要页面,我们可以利用代码编辑器查看这个文件的主要部分:
其中包含的功能和组件包括:
Header
:标题栏,显示标题NetworkDisplay
:所处网络状态Menu
,Switch
:菜单切换ThemeSwitch
:右下角明暗主题切换Account
:右上角账户信息组件- 接下来的两个
Row
对应左下角的 Gas 显示、支持和本地的水龙头
下面我们主要看一下 NetworkDisplay
和 Account
的逻辑实现,以及 Menu
, Switch
中的功能。
NetworkDisplay
组件位置: src/components/NetworkDisplay.jsx
主要包含两个功能:
- 显示当前所选择的网络名称;
- 如果当前钱包所在网络与项目中网络设置不一致,则提示警告,其中,当选择本地网络是,网络 ID 需要设置为
31337
。
function NetworkDisplay({
NETWORKCHECK,
localChainId,
selectedChainId,
targetNetwork,
USE_NETWORK_SELECTOR,
logoutOfWeb3Modal,
}) {
let networkDisplay = "";
if (NETWORKCHECK && localChainId && selectedChainId && localChainId !== selectedChainId) {
const networkSelected = NETWORK(selectedChainId);
const networkLocal = NETWORK(localChainId);
if (selectedChainId === 1337 && localChainId === 31337) {
// 提示错误的网络ID
...
} else {
// 提示网络错误
...
}
} else {
networkDisplay = USE_NETWORK_SELECTOR ? null : (
// 显示网络名称
<div style={{ zIndex: -1, position: "absolute", right: 154, top: 28, padding: 16, color: targetNetwork.color }}>
{targetNetwork.name}
</div>
);
}
console.log({ networkDisplay });
return networkDisplay;
}
Account
组件位置: src/components/Account.jsx
主要包含两个功能:
- 显示当前钱包
- 显示钱包余额
- 显示
Connect
或者Logout
其中,当用户点击 Connect
时,前端调用 loadWeb3Modal
,代码如下,这个函数的需要功能是与MetaMask等钱包进行连接,并监听钱包的 chainChanged
,accountsChanged
和 disconnect
事件,即当我们在钱包中切换网络,选择连接账户以及取消连接时对应修改显示状态。
const loadWeb3Modal = useCallback(async () => {
// 连接钱包
const provider = await web3Modal.connect();
setInjectedProvider(new ethers.providers.Web3Provider(provider));
// 监听切换网络
provider.on("chainChanged", chainId => {
console.log(`chain changed to ${chainId}! updating providers`);
setInjectedProvider(new ethers.providers.Web3Provider(provider));
});
// 监听切换账户
provider.on("accountsChanged", () => {
console.log(`account changed!`);
setInjectedProvider(new ethers.providers.Web3Provider(provider));
});
// 监听断开连接
// Subscribe to session disconnection
provider.on("disconnect", (code, reason) => {
console.log(code, reason);
logoutOfWeb3Modal();
});
// eslint-disable-next-line
}, [setInjectedProvider]);
同理,在连接钱包情况下,用户点击 Logout
会调用 logoutOfWeb3Modal
功能,
const logoutOfWeb3Modal = async () => {
// 清楚缓存的网络提供商,并断开连接
await web3Modal.clearCachedProvider();
if (injectedProvider && injectedProvider.provider && typeof injectedProvider.provider.disconnect == "function") {
await injectedProvider.provider.disconnect();
}
setTimeout(() => {
window.location.reload();
}, 1);
};
Menu
, Switch
这两个分别对应显示菜单和对应切换菜单功能,这些菜单包括:
App Home
:项目希望我们将需要实现的功能放在这个菜单中,比如我们将要实现的 NFT 的铸造和展示功能;Debug Contracts
:调试自己编写的合约功能,将会根据合约的 ABI 文件 展示可以合约的状态变量和可以调用的函数;Hints
:编程提示ExampleUI
:示例UI,可以做为编程使用Mainnet DAI
:以太坊主网DAI
的合约状态和可用函数,与Debug Contracts
功能一直Subgraph
:使用 The Graph 协议对合约中的事件进行监听和查询。
调试信息
App.jsx
中还包含了打印当前页面状态的调试信息,可以在开发的过程中实时查看当前状态变量。
//
// 🧫 DEBUG 👨🏻🔬
//
useEffect(() => {
if (
DEBUG &&
mainnetProvider &&
address &&
selectedChainId &&
yourLocalBalance &&
yourMainnetBalance &&
readContracts &&
writeContracts &&
mainnetContracts
) {
console.log("_____________________________________ 🏗 scaffold-eth _____________________________________");
console.log("🌎 mainnetProvider", mainnetProvider);
console.log("🏠 localChainId", localChainId);
console.log("👩💼 selected address:", address);
console.log("🕵🏻♂️ selectedChainId:", selectedChainId);
console.log("💵 yourLocalBalance", yourLocalBalance ? ethers.utils.formatEther(yourLocalBalance) : "...");
console.log("💵 yourMainnetBalance", yourMainnetBalance ? ethers.utils.formatEther(yourMainnetBalance) : "...");
console.log("📝 readContracts", readContracts);
console.log("🌍 DAI contract on mainnet:", mainnetContracts);
console.log("💵 yourMainnetDAIBalance", myMainnetDAIBalance);
console.log("🔐 writeContracts", writeContracts);
}
}, [
mainnetProvider,
address,
selectedChainId,
yourLocalBalance,
yourMainnetBalance,
readContracts,
writeContracts,
mainnetContracts,
localChainId,
myMainnetDAIBalance,
]);
查看完主页的基本功能,下面我们开始实现 NFT 铸造和展示 NFT 列表这两个功能。
NFT 功能实现
我们将主要实现以下三个部分功能:
- 铸造 NFT;
- 展示 NFT 列表;
- 展示 NFT 合约接口列表。
铸造 NFT
首先我们找到 App Home
对应使用的组件,从下面的代码中可以看到,对应使用 Home
组件,所在位置为 src/views/Home.jsx
。
...
<Switch>
<Route exact path="/">
{/* pass in any web3 props to this Home component. For example, yourLocalBalance */}
<Home yourLocalBalance={yourLocalBalance} readContracts={readContracts} />
</Route>
....
删除 Home.jsx
中内容,添加以下 Mint 按钮:
import React, { useState } from "react";
import { Button, Card, List } from "antd";
function Home({
isSigner,
loadWeb3Modal,
tx,
writeContracts,
}) {
return (
<div>
{/* Mint button */}
<div style={{ maxWidth: 820, margin: "auto", marginTop: 32, paddingBottom: 32 }}>
{isSigner?(
<Button type={"primary"} onClick={()=>{
tx( writeContracts.YourCollectible.mintItem() )
}}>MINT</Button>
):(
<Button type={"primary"} onClick={loadWeb3Modal}>CONNECT WALLET</Button>
)}
</div>
</div>
);
}
export default Home;
同时将 Switch
中对应组件使用修改为:
...
<Switch>
<Route exact path="/">
{/* pass in any web3 props to this Home component. For example, yourLocalBalance */}
<Home
isSigner={userSigner}
loadWeb3Modal={loadWeb3Modal}
tx={tx}
writeContracts={writeContracts}
/>
...
效果图为:
点击 Mint 之后,我们可以看到交易成功发出,这时,虽然我们成功 mint 了 NFT,但是我们还需要添加列表来展示我们的 NFT。
展示 NFT 列表
添加列表展示,其中包含 NFT 的转移功能可以将对应的 NFT 发送给其他地址。
import React, { useState } from "react";
import { Button, Card, List } from "antd";
import { useContractReader } from "eth-hooks";
import { Address, AddressInput} from "../components";
function Home({
isSigner,
loadWeb3Modal,
yourCollectibles,
address,
blockExplorer,
mainnetProvider,
tx,
readContracts,
writeContracts,
}) {
const [transferToAddresses, setTransferToAddresses] = useState({});
return (
<div>
{/* Mint 按钮 */}
...
{/* 列表 */}
<div style={{ width: 820, margin: "auto", paddingBottom: 256 }}>
<List
bordered
dataSource={yourCollectibles}
renderItem={item => {
const id = item.id.toNumber();
console.log("IMAGE",item.image)
return (
<List.Item key={id + "_" + item.uri + "_" + item.owner}>
<Card
title={
<div>
<span style={{ fontSize: 18, marginRight: 8 }}>{item.name}</span>
</div>
}
>
<a href={"https://opensea.io/assets/"+(readContracts && readContracts.YourCollectible && readContracts.YourCollectible.address)+"/"+item.id} target="_blank">
<img src={item.image} />
</a>
<div>{item.description}</div>
</Card>
{/* NFT 转移 */}
<div>
owner:{" "}
<Address
address={item.owner}
ensProvider={mainnetProvider}
blockExplorer={blockExplorer}
fontSize={16}
/>
<AddressInput
ensProvider={mainnetProvider}
placeholder="transfer to address"
value={transferToAddresses[id]}
onChange={newValue => {
const update = {};
update[id] = newValue;
setTransferToAddresses({ ...transferToAddresses, ...update });
}}
/>
<Button
onClick={() => {
console.log("writeContracts", writeContracts);
tx(writeContracts.YourCollectible.transferFrom(address, transferToAddresses[id], id));
}}
>
Transfer
</Button>
</div>
</List.Item>
);
}}
/>
</div>
{/* 信息提示 */}
<div style={{ maxWidth: 820, margin: "auto", marginTop: 32, paddingBottom: 256 }}>
🛠 built with <a href="https://github.com/austintgriffith/scaffold-eth" target="_blank">🏗 scaffold-eth</a>
🍴 <a href="https://github.com/austintgriffith/scaffold-eth" target="_blank">Fork this repo</a> and build a cool SVG NFT!
</div>
</div>
);
}
export default Home;
对应组件使用修改为:
...
<Switch>
<Route exact path="/">
{/* pass in any web3 props to this Home component. For example, yourLocalBalance */}
<Home
isSigner={userSigner}
loadWeb3Modal={loadWeb3Modal}
yourCollectibles={yourCollectibles}
address={address}
blockExplorer={blockExplorer}
mainnetProvider={mainnetProvider}
tx={tx}
writeContracts={writeContracts}
readContracts={readContracts}
/>
...
效果图为:
但是我们发现,当我们再次 mint 时,列表并不会更新,还是原来的样子,因此我们需要在 App.jsx
中添加事件监听,一旦我们铸造 NFT 之后,列表将刷新:
// 跟踪当前 NFT 数量
const balance = useContractReader(readContracts, "YourCollectible", "balanceOf", [address]);
console.log("🤗 balance:", balance);
const yourBalance = balance && balance.toNumber && balance.toNumber();
const [yourCollectibles, setYourCollectibles] = useState();
//
// 🧠 这个 effect 会在 balance 变化时更新 yourCollectibles
//
useEffect(() => {
const updateYourCollectibles = async () => {
const collectibleUpdate = [];
for (let tokenIndex = 0; tokenIndex < balance; tokenIndex++) {
try {
console.log("GEtting token index", tokenIndex);
const tokenId = await readContracts.YourCollectible.tokenOfOwnerByIndex(address, tokenIndex);
console.log("tokenId", tokenId);
const tokenURI = await readContracts.YourCollectible.tokenURI(tokenId);
const jsonManifestString = atob(tokenURI.substring(29))
console.log("jsonManifestString", jsonManifestString);
try {
const jsonManifest = JSON.parse(jsonManifestString);
console.log("jsonManifest", jsonManifest);
collectibleUpdate.push({ id: tokenId, uri: tokenURI, owner: address, ...jsonManifest });
} catch (e) {
console.log(e);
}
} catch (e) {
console.log(e);
}
}
setYourCollectibles(collectibleUpdate.reverse());
};
updateYourCollectibles();
}, [address, yourBalance]);
此时,当我们再次 Mint 时,就是自动更新列表,显示最新铸造的 NFT 了。
展示 NFT 合约接口列表
这个功能比较简单,只需要修改对应 debug 部分即可:
<Route exact path="/debug">
{/*
🎛 this scaffolding is full of commonly used components
this <Contract/> component will automatically parse your ABI
and give you a form to interact with it locally
*/}
<Contract
name="YourCollectible"
price={price}
signer={userSigner}
provider={localProvider}
address={address}
blockExplorer={blockExplorer}
contractConfig={contractConfig}
/>
更新之后,可以在 Debug Contracts
菜单下看到合约的可以调用的函数。
至此,我们就完成了一个简单 NFT 铸造和展示的 DApp 了。
总结
通过这个项目,我们可以学习并了解以下知识:
- NFT 合约基本内容以及如何在 Opensea 等市场中展示 NFT;
- 前端如何连接诸如 MetaMask 等钱包;
- 前端如何调用合约函数。