系列文章目录
使用Pinata在IPFS上存储NFT图片的实践🚪
scaffold-eth-2使用详细教程🚪
文章目录
前言
近年来,非同质化代币(NFT)在区块链领域掀起了一股热潮,成为数字艺术、收藏品和虚拟资产的代名词。NFT 的铸造是将独一无二的数字资产记录在区块链上的过程。本文将通过一个简单的智能合约示例,带你了解如何在以太坊上铸造 NFT,并解释为什么这些 NFT 即便没有被上架,也能在平台(如 OpenSea)上看到。
一、使用到的 OpenZeppelin 库
OpenZeppelin
库中的 ERC721
、ERC721URIStorage
和 Counters
合约是开发 ERC-721 标准代币的常用组件。下面分别介绍这几个合约:
1.1. ERC721 合约
官方文档🚪
ERC721
是 OpenZeppelin 提供的标准 ERC-721 实现。ERC-721 是不可替代代币(NFT)的标准,每一个代币都有唯一的标识符。
ERC721
实现了 ERC-721 标准中的大部分功能,包括:
balanceOf
: 查询某个地址拥有的 NFT 数量。ownerOf
: 查询某个 tokenId 的所有者。transferFrom
: 转移 NFT。approve
和setApprovalForAll
: 授权转移 NFT。safeTransferFrom
: 安全转移 NFT,确保接收方有能力处理 NFT。
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
contract MyNFT is ERC721 {
constructor() ERC721("MyNFT", "MNFT") {
_mint(msg.sender, 1); // 铸造一个 tokenId 为 1 的 NFT 给合约创建者
}
}
1.2. ERC721URIStorage 合约
官方文档🚪
ERC721URIStorage
继承自 ERC721
,并添加了对 tokenURI
的存储管理。tokenURI
通常指向一个包含该 NFT 元数据的 JSON 文件(例如存储在 IPFS 上)。相比 ERC721
,ERC721URIStorage
允许为每个 token 存储和更新 tokenURI
。
_setTokenURI(uint256 tokenId, string memory _tokenURI)
: 为某个 token 设置 URI,这个URI通常是指向链外存储的资源,如IPFS上存储的JSON文件或者图像,相当于将id
和URI
绑定起来tokenURI(uint256 tokenId)
: 返回指定 tokenId 的URI。每个代币都有自己独立的URI,它通常指向链外存储的元数据(例如一张图片、JSON文件等)_burn
: 燃烧某个 token 的时候,会删除对应的 tokenURI。
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
contract MyNFTWithURI is ERC721URIStorage {
constructor() ERC721("MyNFTWithURI", "MNFTU") {}
function mintWithURI(address to, uint256 tokenId, string memory tokenURI) public {
_mint(to, tokenId); // 铸造 NFT
_setTokenURI(tokenId, tokenURI); // 设置对应的 URI
}
}
现在用ERC721URIStorage合约就可以了,不用再继承ERC721
1.3. Counters 合约
Counters
是一个简单的计数器库,用于管理增量 ID。它可以被用于 NFT 的 tokenId
自动递增,确保每个新创建的 NFT 拥有唯一的 ID。
increment
: 增加计数器的值。decrement
: 减少计数器的值。current
: 获取当前计数器的值。
import "@openzeppelin/contracts/utils/Counters.sol";
contract MyNFTWithAutoId is ERC721URIStorage {
using Counters for Counters.Counter;
Counters.Counter private _tokenIds;
constructor() ERC721("MyNFTWithAutoId", "MNFTA") {}
function mintWithAutoId(address to, string memory tokenURI) public {
_tokenIds.increment(); // 增加 tokenId
uint256 newTokenId = _tokenIds.current(); // 获取新的 tokenId
_mint(to, newTokenId); // 铸造 NFT
_setTokenURI(newTokenId, tokenURI); // 设置 URI
}
}
二、编写合约代码
2.1. 准备NFT元数据
在之前的博客🚪中我讲了如何存储文件到IPFS中,现在要上传一个
json
文件与图片文件在IPFS中,一个NFT有自己唯一的tokenid
,由于通过_setTokenURI
函数将tokenid
和tokenURI
绑定在一起,这个tokenURI
指的就是json元数据文件在IPFS中的网址URL
,而在元数据文件中又存在图片文件的IPFS网址URL
json文件格式如下,name
为NFT的名字,description
为描述,image
为存储在IPFS中的图片URL
,attributes
为特征:
{
"name": "hjyToken #0",
"description": "this is my first nft number 0",
"image": "https://你自己的ipfs网关/ipfs/图片文件的cid",
"attributes": [
{
"trait_type": "language",
"value": "solidity"
},
{
"trait_type": "os",
"value": "window"
},
{
"trait_type": "speed",
"value": "fast"
}
]
}
2.2. 实现tokenid唯一
import "@openzeppelin/contracts/utils/Counters.sol"
引入 OpenZeppelin 库中的 Counters
工具,为每个新铸造的NFT生成唯一的tokenId。
每铸造一次,就对计数器调用increment()
方法自增就可以实现每个NFT的tokenid
都是唯一的
2.3. mintToken函数铸造NFT
铸造 NFT 的过程就是创建一个新的代币并将其记录在区块链上。我们的智能合约通过 mintToken
函数来实现这一过程,这里的传入参数tokenURI
实际上为json元数据文件的网址URL
// 铸造
function mintToken(string memory tokenURI) public returns (uint256) {
require(!tokenURIExists(tokenURI), "Token URI already exists");
_tokenIds.increment();
uint256 newTokenId = _tokenIds.current();
_safeMint(msg.sender, newTokenId);
_setTokenURI(newTokenId, tokenURI);
_usedTokenURIs[tokenURI] = true;
_idToNftItem[newTokenId] = NftItem(newTokenId, 0, msg.sender, false);
return newTokenId;
}
在这个过程中,我们做了以下几件事:
- 检查
tokenURI
是否已经被使用,确保每个 NFT 都是独一无二的。 - 调用
_safeMint
将新的tokenId
铸造给调用者(即msg.sender
)。 - 使用
_setTokenURI
关联每个tokenId
和其元数据(tokenURI
),这些数据通常包含了 NFT 的图片、描述等信息。 - 记录这个 NFT 的
tokenId
和其他信息,比如创作者地址和是否上架(初始值为false
)。
2.4. listNft函数发布NFT
如果用户想将铸造好的 NFT 发布并设置出售价格
// 发布
function listNft(uint256 tokenId, uint256 price) public payable {
require(ownerOf(tokenId) == msg.sender, "You are not the owner of this NFT");
require(price > 0, "Price must be at least 1 wei");
require(msg.value == listingPrice, "Price must be equal to listing price");
_idToNftItem[tokenId].price = price;
_idToNftItem[tokenId].isListed = true;
_listedItems.increment();
emit NftItemCreated(tokenId, price, msg.sender);
}
这个函数的核心是确保 NFT 的合法拥有者才可以发布,并且用户必须支付发布费用(listingPrice
)。当 NFT 被成功发布时,合约会触发 NftItemCreated 事件,通知前端监听器或其他区块链观察者该 NFT 已被列为可出售。
完整的 NFT市场
合约代码如下:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.27;
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
contract NftMarket is ERC721URIStorage {
using Counters for Counters.Counter;
struct NftItem {
uint256 tokenId;
uint256 price; // 价格
address creator; // 创作者
bool isListed; // 是否上架
}
// 上架费用
uint256 public listingPrice = 0.025 ether;
Counters.Counter private _listedItems; // 已发布的nft数目
Counters.Counter private _tokenIds; // 已铸造的nft数目
mapping(string => bool) private _usedTokenURIs;
mapping(uint256 => NftItem) private _idToNftItem;
event NftItemCreated(
uint256 tokenId,
uint256 price,
address owner
);
constructor() ERC721("HJYToken", "HJYT") {}
// 根据tokenid查看nft
function getNftItem(uint256 tokenId) public view returns (NftItem memory) {
return _idToNftItem[tokenId];
}
// 已上架nft数量
function listedItemsCount() public view returns (uint256) {
return _listedItems.current();
}
// tokenURI是否重复
function tokenURIExists(string memory tokenURI) public view returns (bool) {
return _usedTokenURIs[tokenURI] == true;
}
// 铸造
function mintToken(string memory tokenURI) public returns (uint256) {
require(!tokenURIExists(tokenURI), "Token URI already exists");
_tokenIds.increment();
uint256 newTokenId = _tokenIds.current();
_safeMint(msg.sender, newTokenId);
_setTokenURI(newTokenId, tokenURI);
_usedTokenURIs[tokenURI] = true;
_idToNftItem[newTokenId] = NftItem(newTokenId, 0, msg.sender, false);
return newTokenId;
}
// 发布
function listNft(uint256 tokenId, uint256 price) public payable {
require(ownerOf(tokenId) == msg.sender, "You are not the owner of this NFT");
require(price > 0, "Price must be at least 1 wei");
require(msg.value == listingPrice, "Price must be equal to listing price");
_idToNftItem[tokenId].price = price;
_idToNftItem[tokenId].isListed = true;
_listedItems.increment();
emit NftItemCreated(tokenId, price, msg.sender);
}
}
三、部署合约
有两种部署合约的方法,一个是直接用remix在线网站部署,另一个则是像hardhat
,truffle
框架一样部署合约
3.1. remix部署
打开remix网站🚪新建一个NftMarket.sol
文件,放入上面的完整代码,在第三个选项中编译完合约后到,第四个部署操作中,连接到metamask
钱包的Sepolia
测试网络中
输入json元数据文件的网址URL
,提交之后就可以在OpenSea
测试网网站上查看了
3.2. scaffold-eth-2部署
scaffold-eth详细教程🚪,新建项目基础使用我都已经在之前的博客中写好了,在放入自己的
NftMarket
合约之后同样和remix网站的操作几乎一样,都是调用mintToken
函数
- 拉取项目代码并下载依赖:
git clone https://github.com/scaffold-eth/scaffold-eth-2.git
cd scaffold-eth-2
yarn install
- 将
NftMarket.sol
和01_deploy_NftMarket.ts
分别放在packages/hardhat/contracts/
和packages/hardhat/deploy/
文件夹下
// 01_deploy_NftMarket.ts
import { HardhatRuntimeEnvironment } from "hardhat/types";
import { DeployFunction } from "hardhat-deploy/types";
import { Contract } from "ethers";
/**
* Deploys a contract named "YourContract" using the deployer account and
* constructor arguments set to the deployer address
*
* @param hre HardhatRuntimeEnvironment object.
*/
const deployYourContract: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
/*
On localhost, the deployer account is the one that comes with Hardhat, which is already funded.
When deploying to live networks (e.g `yarn deploy --network sepolia`), the deployer account
should have sufficient balance to pay for the gas fees for contract creation.
You can generate a random account with `yarn generate` which will fill DEPLOYER_PRIVATE_KEY
with a random private key in the .env file (then used on hardhat.config.ts)
You can run the `yarn account` command to check your balance in every network.
*/
const { deployer } = await hre.getNamedAccounts();
const { deploy } = hre.deployments;
await deploy("NftMarket", {
from: deployer,
// Contract constructor arguments
args: [],
log: true,
// autoMine: can be passed to the deploy function to make the deployment process faster on local networks by
// automatically mining the contract deployment transaction. There is no effect on live networks.
autoMine: true,
});
// Get the deployed contract to interact with it after deploying.
const yourContract = await hre.ethers.getContract<Contract>("NftMarket", deployer);
// console.log("👋 Initial contract:", await yourContract.target);
console.log("👋 Initial listedItemsCount:", await yourContract.listedItemsCount());
};
export default deployYourContract;
// Tags are useful if you have multiple deploy files and only want to run one of them.
// e.g. yarn deploy --tags NftMarket
deployYourContract.tags = ["YourContract"];
- 修改配置文件
把hardhat部署网络从本地链改为测试网Sepolia
把前端显示的网络从hardhat改为Sepolia
- 创建以供测试网使用的账户,转点余额到里面
输入yarn generate
生成账户,私钥保存在packages/hardhat/.env
文件中
在metamask钱包中用私钥导入新账户,转1个币到账户里面(没有币可以用这个网站🚪每天领取0.1个)
- 启动项目,分三个终端窗口输入命令
yarn chain
yarn deploy
yarn start
浏览器输入http://localhost:3000/debug
,就可以看到测试的窗口,调用mintToken
函数,输入json元数据文件的网址URL
四、查看NFT
为什么铸造后的 NFT 可以被 OpenSea 看到?
OpenSea 能够显示铸造完成的 NFT,主要依赖于 ERC-721 标准中的Transfer
事件。OpenSea 监听这个事件,并自动将新的 NFT 索引到它的平台上,在执行_safeMint
时,会触发一个Transfer
事件,从0x0
地址(表示合约中创建的新代币)转移到用户的地址。这是 ERC-721 标准的一部分:
event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
打开metamask钱包,导入NFT
部署的合约地址和tokenid
,代币ID为1是因为这是第一个铸造的NFT
打开刚刚导入的NFT,选择在OpenSea
测试网中查看
总结
通过本文,我们详细介绍了NFT铸造的过程,并探讨了将铸造和上架分开的重要性。文章从基础概念入手,讲解了NFT的创建流程,特别是在以太坊网络上如何通过智能合约实现安全、透明的铸造。我们还详细说明了如何将铸造和上架这两个步骤分离,确保在NFT铸造后能够灵活决定何时上架销售,并提供了相关的代码示例和操作指南。希望这篇文章能帮助你更好地理解NFT的铸造过程。如果你有任何疑问或建议,欢迎在评论区留言讨论🌹