目录
[TOC]
1、ERC721的基础知识
1.1、什么是不可替代代币?
1.2、什么是 ERC-721?
1.3、什么是元数据
1.4、如何在链上保存NFT的图像
2、HardHat
3、创建项目
3.1、创建 NFT 市场
3.2、创建 NFT 智能合约
3.3、编写测试脚本
4、将 NFT 部署到 Rinkeby 网络,在 OpenSea 上查看
4.1、部署 NFT市场
4.2、部署 NFT 721示例
4.3、对 NFT 721示例 合约在 Rinkeby 网络进行验证
4.4、在 Rinkeby 网络铸造 NFT
4.5、在 opensea 查看刚刚铸造的NFT
5、项目源码
6、推荐阅读
1、ERC721的基础知识
1.1、什么是不可替代代币?
NFT
是独一无二的,每个令牌都有独特的特征和价值。可以成为 NFT
的东西类型有收藏卡、艺术品、机票等,它们之间都有明显的区别,不可互换。将不可替代代币 (NFT) 视为稀有收藏品;而且大多数时候,还有它的元数据属性。
1.2、什么是 ERC-721?
ERC-721
(Ethereum Request for Comments 721)由 William Entriken、Dieter Shirley、Jacob Evans 和 Nastassia Sachs 于 2018 年 1 月提出,是一种不可替代的代币标准。描述了如何在 EVM(以太坊虚拟机)兼容的区块链上构建不可替代的代币;它是不可替代代币的标准接口;它有一套规则,可以很容易地使用 NFT
。NFT
不仅是 ERC-721 类型;它们也可以是ERC-1155 令牌。
ERC-721
引入了 NFT
标准,换句话说,这种类型的 Token
是独一无二的,并且可能具有与来自同一智能合约的另一个 Token
不同的价值,可能是由于它的年龄、稀有性甚至是其他类似自定义属性等等。
所有 NFT
都有一个 uint256
类型的变量 tokenId
,因此对于任何 ERC-721
合约,该对 contract address
、uint256 tokenId
必须是全局唯一的。也就是说,一个 dApp 可以有一个“转换器”,它使用 tokenId
作为输入并输出一些很酷的东西的图像,比如僵尸、武器、技能或猫、狗一类的!
1.3、什么是元数据
所有 NFT
都有元数据。您可以在最初的ERC/EIP 721 提案中了解这一点 。 基本上,社区发现在以太坊上存储图像真的很费力而且很昂贵。如果你想存储一张 8 x 8 的图片,存储这么多数据是相当便宜的,但如果你想要一张分辨率不错的图片,你就需要花更多的 GAS
费用。
虽然 以太坊 2.0 将解决很多这些扩展难题,但目前,社区需要一个标准来帮助解决这个问题,这也就是元数据的存在原因。
{
"name": "mshk-logo-black",
"description": "mshk.top logo black",
"image": "https://bafybeihodzhbtntgml7t72maxill576ssax6md5kfu72aq4gd4p53oipn4.ipfs.infura-ipfs.io/",
"attributes": [
{
"trait_type": "customAttr",
"value": 100
}
]
}
name
,NFT的代币名称
description
,NFT的代币描述
image
,NFT图像的URL
attributes
,NFT代币的属性,可以定义多个
一旦我们将 tokenId
分配给他们的 tokenURI
,NFT
市场将能够展示你的代币,您可以在 Rinkeby
测试网上的 OpenSea
市场上看到我使用元数据更新后的效果。类似展示 NFT
的市场 还有如 Mintable、Rarible。
1.4、如何在链上保存NFT的图像
您会在上面的元数据代码示例中注意到,图像使用指向 IPFS
的 URL
,这是一种流行的图像存储方式。
IPFS
代表星际文件系统,是一种点对点超媒体协议,旨在使网络更快、更安全、更开放。它允许任何人上传文件,并且该文件被散列,因此如果它发生变化,它的散列也会发生变化。这是存储图像的理想选择,因为这意味着每次更新图像时,链上的 hash/tokenURI 也必须更改,这意味着我们可以记录元数据的历史记录。将图像添加到 IPFS
上也非常容易,并且不需要运行服务器!
推荐使用 CoinTool 中的 IPFS 工具。
2、HardHat
关于 HardHat 的介绍以及安装,可以参考文章 如何使用ERC20代币实现买、卖功能并完成Dapp部署
3、创建项目
3.1、创建 NFT 市场
进入 hardhat
项目目录,创建 contracts/ERC721/NftMarketplace.sol
文件,内容如下:
$ cat contracts/ERC721/NftMarketplace.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.14;
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
// Check out https://github.com/Fantom-foundation/Artion-Contracts/blob/5c90d2bc0401af6fb5abf35b860b762b31dfee02/contracts/FantomMarketplace.sol
// For a full decentralized nft marketplace
// 从Solidity v0.8.4开始,有一种方便且省 gas 的方式可以通过使用自定义错误向用户解释操作失败的原因。
// 错误的语法类似于 事件的语法。它们必须与revert 语句一起使用,这会导致当前调用中的所有更改都被还原并将错误数据传递回调用者
error PriceNotMet(address nftAddress, uint256 tokenId, uint256 price);
error ItemNotForSale(address nftAddress, uint256 tokenId);
error NotListed(address nftAddress, uint256 tokenId);
error AlreadyListed(address nftAddress, uint256 tokenId);
error NoProceeds();
error NotOwner();
error NotApprovedForMarketplace();
error PriceMustBeAboveZero();
contract NftMarketplace is ReentrancyGuard {
// 保存卖家地址和价格
struct Listing {
uint256 price;
address seller;
}
// 加入市场列表事件
event ItemListed(
address indexed seller,
address indexed nftAddress,
uint256 indexed tokenId,
uint256 price
);
// 更新事件
event UpdateListed(
address indexed seller,
address indexed nftAddress,
uint256 indexed tokenId,
uint256 price
);
// 取消市场列表事件
event ItemCanceled(
address indexed seller,
address indexed nftAddress,
uint256 indexed tokenId
);
// 买入事件
event ItemBuy(
address indexed buyer,
address indexed nftAddress,
uint256 indexed tokenId,
uint256 price
);
// 保存NFT列表和卖家的对应状态
mapping(address => mapping(uint256 => Listing)) private s_listings;
// 卖家地址和卖出的总金额
mapping(address => uint256) private s_proceeds;
modifier notListed(
address nftAddress,
uint256 tokenId,
address owner
) {
Listing memory listing = s_listings[nftAddress][tokenId];
if (listing.price > 0) {
revert AlreadyListed(nftAddress, tokenId);
}
_;
}
// 检查卖家是否在列表中
modifier isListed(address nftAddress, uint256 tokenId) {
Listing memory listing = s_listings[nftAddress][tokenId];
if (listing.price <= 0) {
revert NotListed(nftAddress, tokenId);
}
_;
}
// 检查 NFT 地址的 tokenId owner 是否为 spender
modifier isOwner(
address nftAddress,
uint256 tokenId,
address spender
) {
IERC721 nft = IERC721(nftAddress);
// 查找NFT的所有者,分配给零地址的 NFT 被认为是无效的,返回NFT持有者地址
address owner = nft.ownerOf(tokenId);
if (spender != owner) {
revert NotOwner();
}
_;
}
/*
* @notice 将 NFT 加入到市场列表中,external 表示这是一个外部函数
* @param nftAddress Address of NFT contract
* @param tokenId Token ID of NFT
* @param price sale price for each item
*/
function listItem(
address nftAddress,
uint256 tokenId,
uint256 price
)
external
notListed(nftAddress, tokenId, msg.sender)
isOwner(nftAddress, tokenId, msg.sender)
{
if (price <= 0) {
// 终止运行并撤销状态更改
revert PriceMustBeAboveZero();
}
IERC721 nft = IERC721(nftAddress);
// 获取单个NFT的批准地址,如果tokenId不是有效地址,抛出异常,
if (nft.getApproved(tokenId) != address(this)) {
revert NotApprovedForMarketplace();
}
// 存储智能合约状态
s_listings[nftAddress][tokenId] = Listing(price, msg.sender);
// 注册事件
emit ItemListed(msg.sender, nftAddress, tokenId, price);
}
/*
* @notice 从NFT列表中删除 卖家信息
* @param nftAddress Address of NFT contract
* @param tokenId Token ID of NFT
*/
function cancelListing(address nftAddress, uint256 tokenId)
external
isOwner(nftAddress, tokenId, msg.sender)
isListed(nftAddress, tokenId)
{
delete (s_listings[nftAddress][tokenId]);
// 注册 事件
emit ItemCanceled(msg.sender, nftAddress, tokenId);
}
/*
* @notice 允许买家使用ETH,从卖家列表中买入 NFT
* nonReentrant 方法 防止合约被重复调用
* @param nftAddress NFT 合约地址
* @param tokenId NFT 的通证 ID
*/
function buyItem(address nftAddress, uint256 tokenId)
external
payable
isListed(nftAddress, tokenId)
nonReentrant
{
// 获取卖家列表,并判断支付的ETH是否小于卖家的价格
Listing memory listedItem = s_listings[nftAddress][tokenId];
if (msg.value < listedItem.price) {
revert PriceNotMet(nftAddress, tokenId, listedItem.price);
}
// 更新卖家卖出的金额
s_proceeds[listedItem.seller] += msg.value;
// Could just send the money...
// https://fravoll.github.io/solidity-patterns/pull_over_push.html
// 从卖家列表中删除
delete (s_listings[nftAddress][tokenId]);
// 将 NFT(tokenId) 所有权从 listedItem.seller 转移到 msg.sender
IERC721(nftAddress).safeTransferFrom(
listedItem.seller,
msg.sender,
tokenId
);
//注册买家事件
emit ItemBuy(msg.sender, nftAddress, tokenId, listedItem.price);
}
/*
* @notice 卖家更新NFT在市场上的价格
* @param nftAddress Address of NFT contract
* @param tokenId Token ID of NFT
* @param newPrice Price in Wei of the item
*/
function updateListing(
address nftAddress,
uint256 tokenId,
uint256 newPrice
)
external
isListed(nftAddress, tokenId)
nonReentrant
isOwner(nftAddress, tokenId, msg.sender)
{
s_listings[nftAddress][tokenId].price = newPrice;
emit UpdateListed(msg.sender, nftAddress, tokenId, newPrice);
}
/*
* @notice 将ETH转移到其他帐号,同时设置收益余额为0
*/
function withdrawProceeds() external {
uint256 proceeds = s_proceeds[msg.sender];
if (proceeds <= 0) {
revert NoProceeds();
}
s_proceeds[msg.sender] = 0;
// 将 ETH 发送到地址的方法,关于此语法更多介绍可以参考下面链接
// https://ethereum.stackexchange.com/questions/96685/how-to-use-address-call-in-solidity
(bool success, ) = payable(msg.sender).call{value: proceeds}("");
require(success, "Transfer failed");
}
/*
* @notice 获取NFT卖家列表
*/
function getListing(address nftAddress, uint256 tokenId)
external
view
returns (Listing memory)
{
return s_listings[nftAddress][tokenId];
}
// 获取 seller 卖出的总金额
function getProceeds(address seller) external view returns (uint256) {
return s_proceeds[seller];
}
}
从 Solidity v0.8.4开始,有一种方便且省 GAS
的方式可以通过使用自定义错误向用户解释操作失败的原因。错误的语法类似于事件的语法。它们必须与 revert
语句一起使用,这会导致当前调用中的所有更改都被还原并将错误数据传递回调用者。
自定义错误是在智能合约主体之外声明的。当错误被抛出时,在 Solidity
中意味着当某些检查和条件失败,周围函数的执行被“还原”。
代码中主要内容介绍:
- notListed、isListed、isOwner是函数修饰符的应用。
- listItem方法,将
NFT
加入到列表,会做一些权限验证。其中用到了函数修饰符和事件 - cancelListing方法,从列表中删除
NFT
,将NFT
下架。 - buyItem方法,购买
NFT
,项目中主要用ETH
来交换NFT
资产,也可以用其他数字资产进行交换。同时会更新卖家余额。从listItem中下架NFT
。 - updateListing方法,更新
NFT
的价格。 - withdrawProceeds方法,将卖出的收益从合约中转移给卖家。
- getListing方法,根据
NFT
地址和tokenId
,返回卖家和价格信息。 - getProceeds方法,查看卖家卖出后的收益。
3.2、创建 NFT 智能合约
在编写测试脚本前,我们需要一个 NFT的智能合约示例
,以便我们铸造的 NFT
可以在市场上展示、销售。我们将遵守 ERC721
令牌规范,我们将从 OpenZeppelin
的 ERC721URIStorage
库继承。
进入 hardhat
项目目录,创建 contracts/ERC721/MSHK721NFT.sol
文件,内容如下:
$ cat contracts/ERC721/MSHK721NFT.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.14;
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "hardhat/console.sol";
contract MSHK721NFT is ERC721URIStorage, Ownable {
// 递增递减计数器
using Counters for Counters.Counter;
Counters.Counter private _tokenIds;
// 声明事件
event NFTMinted(uint256 indexed tokenId);
constructor() ERC721("MSHKNFT", "MyNFT") {}
/**
* 制作NFT,返回铸造的 NFT ID
* @param recipient 接收新铸造NFT的地址.
* @param tokenURI 描述 NFT 元数据的 JSON 文档
*/
function mintNFT(address recipient, string memory tokenURI)
external
onlyOwner
returns (uint256)
{
// 递增
_tokenIds.increment();
// 获取当前新的 TokenId
uint256 newTokenId = _tokenIds.current();
// 铸造NFT
_safeMint(recipient, newTokenId);
// 保存NFT URL
_setTokenURI(newTokenId, tokenURI);
// 注册事件
emit NFTMinted(newTokenId);
return newTokenId;
}
function getTokenCounter() public view returns (uint256) {
return _tokenIds.current();
}
}
上面的代码中,通过 mintNFT
方法铸造 NFT
,主要有2个参数,第1个参数是接收NFT
的地址,第2个参数是 NFT
的 URL
地址,也就是上文中提到的元数据地址。
3.3、编写测试脚本
在编写测试脚本前,我们先通过 IPFS工具,上传我们的图片和元数据文件,下面是我们已经上传好的2个元数据文件:
文件1,内容如下:
{
"name": "mshk-logo-black",
"description": "mshk.top logo black",
"image": "https://bafybeihodzhbtntgml7t72maxill576ssax6md5kfu72aq4gd4p53oipn4.ipfs.infura-ipfs.io/",
"attributes": [
{
"trait_type": "customAttr",
"value": 100
}
]
}
文件2,内容如下:
{
"name": "mshk-logo-blue",
"description": "mshk.top logo blue",
"image": "https://bafybeifxkvzedhwclmibidf5hjoodwqkk2vlbbrlhd3bxbl3wzmkmyrvpq.ipfs.infura-ipfs.io/",
"attributes": [
{
"trait_type": "customAttr",
"value": 200
}
]
}
进入 hardhat
项目目录,创建 test/ERC721/01_NFT.js
测试文件,内容如下:
const { expect } = require("chai");
const { ethers } = require("hardhat");
/**
* 运行测试方法:
* npx hardhat test test/ERC721/01_NFT.js
*/
describe("NFT MarketPlace Test", () => {
// NFT 元数据1
const TOKEN_URI1 = "https://bafybeif5jtlbetjp2nzj64gstexywpp53efr7yynxf4qxtmf5lz6seezia.ipfs.infura-ipfs.io";
// NFT 元数据2
const TOKEN_URI2 = "https://bafybeibyb2rdn6raav4ozyxub2r5w4vh3wmw46s6bi54eq7syjzfkmbjn4.ipfs.infura-ipfs.io";
let owner;
let addr1;
let addr2;
let addrs;
let nftMarketplaceContractFactory;
let nftContractFactory;
let nftMarketplaceContract;
let nftContract;
let IDENTITIES;
beforeEach(async () => {
[owner, addr1, addr2, ...addrs] = await ethers.getSigners();
IDENTITIES = {
[owner.address]: "OWNER",
[addr1.address]: "DEPLOYER",
[addr2.address]: "BUYER_1",
}
var NFTMarketplaceContractName = "NftMarketplace";
var NFTContractName = "MSHK721NFT"
// 获取 NFTMarketplace 实例
nftMarketplaceContractFactory = await ethers.getContractFactory(NFTMarketplaceContractName);
// 部署 NFTMarketplace 合约
nftMarketplaceContract = await nftMarketplaceContractFactory.deploy()
// 获取 nftContract 实例
nftContractFactory = await ethers.getContractFactory(NFTContractName);
// 部署 nftContract 合约
nftContract = await nftContractFactory.deploy()
console.log(`owner:${owner.address}`)
console.log(`addr1:${addr1.address}`)
console.log(`addr2:${addr2.address}`)
//
console.log(`${NFTMarketplaceContractName} Token Contract deployed address -> ${nftMarketplaceContract.address}`);
//
console.log(`${NFTContractName} Token Contract deployed address -> ${nftContract.address} owner:${await nftContract.owner()}`);
});
it("mint and list and buy item", async () => {
console.log(`Minting NFT for ${addr1.address}`)
// 为 addr1 铸造一个 NFT
let mintTx = await nftContract.connect(owner).mintNFT(addr1.address, TOKEN_URI1)
let mintTxReceipt = await mintTx.wait(1)
// 非常量(既不pure也不view)函数的返回值仅在函数被链上调用时才可用(即,从这个合约或从另一个合约)
// 当从链下(例如,从 ethers.js 脚本)调用此类函数时,需要在交易中执行它,并且返回值是该交易的哈希值,因为不知道交易何时会被挖掘并添加到区块链中
// 为了在从链下调用非常量函数时获得它的返回值,可以发出一个包含将要返回的值的事件
let tokenId = mintTxReceipt.events[0].args.tokenId
expect(tokenId).to.equal(1);
// 授权 市场合约 可以操作这个NFT
console.log("Approving Marketplace as operator of NFT...")
let approvalTx = await nftContract
.connect(addr1)
.approve(nftMarketplaceContract.address, tokenId)
await approvalTx.wait(1)
// NFT交易价格 10 ETH
let PRICE = ethers.utils.parseEther("10")
// 将 NFT 加入到列表
console.log("Listing NFT...")
let listItemTX = await nftMarketplaceContract
.connect(addr1)
.listItem(nftContract.address, tokenId, PRICE)
await listItemTX.wait(1)
console.log("NFT Listed with token ID: ", tokenId.toString())
const mintedBy = await nftContract.ownerOf(tokenId)
// 检查 nft 的 owner 是否为 addr1
expect(mintedBy).to.equal(addr1.address)
console.log(`NFT with ID ${tokenId} minted and listed by owner ${mintedBy} with identity ${IDENTITIES[mintedBy]}. `)
//---- Buy
// 根据 tokenId 获取 NFT
let listing = await nftMarketplaceContract.getListing(nftContract.address, tokenId)
let price = listing.price.toString()
// 使用 addr2 从 nftMarketplaceContract 买入 TOKEN_ID 为 0 的NFT
const buyItemTX = await nftMarketplaceContract
.connect(addr2)
.buyItem(nftContract.address, tokenId, {
value: price,
})
await buyItemTX.wait(1)
console.log("NFT Bought!")
const newOwner = await nftContract.ownerOf(tokenId)
console.log(`New owner of Token ID ${tokenId} is ${newOwner} with identity of ${IDENTITIES[newOwner]} `)
//---- proceeds
const proceeds = await nftMarketplaceContract.getProceeds(addr1.address)
const proceedsValue = ethers.utils.formatEther(proceeds.toString())
console.log(`Seller ${owner.address} has ${proceedsValue} eth!`)
//---- withdrawProceeds
const addr1OldBalance = await ethers.provider.getBalance(addr1.address);
await nftMarketplaceContract.connect(addr1).withdrawProceeds()
const addr1NewBalance = await ethers.provider.getBalance(addr1.address);
console.log(`${addr1.address} old:${ethers.utils.formatEther(addr1OldBalance)} eth,withdrawProceeds After:${ethers.utils.formatEther(addr1NewBalance)} eth!`)
});
it("update and cancel nft item", async () => {
// 为 addr2 铸造一个 NFT
let mintTx = await nftContract.connect(owner).mintNFT(addr2.address, TOKEN_URI2)
let mintTxReceipt = await mintTx.wait(1)
// 非常量(既不pure也不view)函数的返回值仅在函数被链上调用时才可用(即,从这个合约或从另一个合约)
// 当从链下(例如,从 ethers.js 脚本)调用此类函数时,需要在交易中执行它,并且返回值是该交易的哈希值,因为不知道交易何时会被挖掘并添加到区块链中
// 为了在从链下调用非常量函数时获得它的返回值,可以发出一个包含将要返回的值的事件
let tokenId = mintTxReceipt.events[0].args.tokenId
// 授权 市场合约 可以操作这个NFT
console.log("Approving Marketplace as operator of NFT...")
approvalTx = await nftContract.connect(addr2).approve(nftMarketplaceContract.address, tokenId)
await approvalTx.wait(1)
// NFT交易价格 0.1 ETH
PRICE = ethers.utils.parseEther("0.1")
// 将 NFT 加入到列表
console.log("Listing NFT...")
listItemTX = await nftMarketplaceContract.connect(addr2).listItem(nftContract.address, tokenId, PRICE)
await listItemTX.wait(1)
console.log("NFT Listed with token ID: ", tokenId.toString())
console.log(`Updating listing for token ID ${tokenId} with a new price`)
listing = await nftMarketplaceContract.getListing(nftContract.address, tokenId)
let oldPrice = listing.price.toString()
console.log(`oldPrice: ${ethers.utils.formatEther(oldPrice.toString())}`)
// 更新价格
const updateTx = await nftMarketplaceContract.connect(addr2).updateListing(nftContract.address, tokenId, ethers.utils.parseEther("0.5"))
// 等待链上处理
const updateTxReceipt = await updateTx.wait(1)
// 从事件中获取更新的价格
const updatedPrice = updateTxReceipt.events[0].args.price
console.log(`updated price: ${ethers.utils.formatEther(updatedPrice.toString())}`)
// 获取信息,确认价格是否有变更.
const updatedListing = await nftMarketplaceContract.getListing(
nftContract.address,
tokenId
)
console.log(`Updated listing has price of ${ethers.utils.formatEther(updatedListing.price.toString())}`)
//----------cancel
let tx = await nftMarketplaceContract.connect(addr2).cancelListing(nftContract.address, tokenId)
await tx.wait(1)
console.log(`NFT with ID ${tokenId} Canceled...`)
// Check cancellation.
const canceledListing = await nftMarketplaceContract.getListing(nftContract.address, tokenId)
console.log("Seller is Zero Address (i.e no one!)", canceledListing.seller)
});
});
上面的测试脚本中,我们分成两部分,注释比较详细,下面是简要介绍这两部分测试的功能。
第1部分:
- 为
addr1
用户铸1个NFT - 授权
NFT市场
可以操作这个addr1
的 NFT。 - 将
NFT
加入到NFT市场
,设置价格为10
ETH。 - 使用
addr2
用户购买addr1
的NFT。 - 查看
addr1
在NFT市场
的余额 - 将
NFT市场
中的余额取出到addr1
的余额,对比前后余额数据。
第2部分:
- 为
addr2
用户铸1个NFT - 授权
NFT市场
可以操作这个addr2
的 NFT。 - 将
NFT
加入到NFT市场
,设置价格为0.1
ETH。 - 将
addr2
的NFT价格从0.1
ETH 更新为0.5
ETH。进行数据对比输出。 - 从
NFT市场
中下架addr2
的 NFT。
下面是我们运行测试脚本的效果:
到目前为止,我们已经完成了 NFT
的创建,并将 NFT
加入到市场完成了买、卖、查看销售后的余额,转帐给卖家等功能。
项目的源码都保存在 Github:https://github.com/idoall/NFT-ERC721-NFTMarketPlace
克隆项目到本地后,进入 hardhat
项目目录,先执行 yarn install
下载依赖包。
$ yarn install
yarn install v1.22.19
warning package.json: No license field
warning package-lock.json found. Your project contains lock files generated by tools other than Yarn. It is advised not to mix package managers in order to avoid resolution inconsistencies caused by unsynchronized lock files. To clear this warning, remove package-lock.json.
warning hardhat-project: No license field
[1/4] Resolving packages...
[2/4] Fetching packages...
[3/4] Linking dependencies...
warning " > @nomiclabs/[email protected]" has incorrect peer dependency "@nomiclabs/hardhat-ethers@^2.0.0".
warning " > @openzeppelin/[email protected]" has incorrect peer dependency "@nomiclabs/hardhat-ethers@^2.0.0".
warning "hardhat-deploy > [email protected]" has incorrect peer dependency "ethers@~5.5.0".
[4/4] Building fresh packages...
✨ Done in 15.42s.
安装完依赖包后,运行npx hardhat test test/ERC721/01_NFT.js
命令,可以看到和上图一样的效果。
$ npx hardhat test test/ERC721/01_NFT.js
Compiled 16 Solidity files successfully
NFT MarketPlace Test
owner:0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
addr1:0x70997970C51812dc3A010C7d01b50e0d17dc79C8
addr2:0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC
NftMarketplace Token Contract deployed address -> 0x5FbDB2315678afecb367f032d93F642f64180aa3
MSHK721NFT Token Contract deployed address -> 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512 owner:0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
Minting NFT for 0x70997970C51812dc3A010C7d01b50e0d17dc79C8
Approving Marketplace as operator of NFT...
Listing NFT...
NFT Listed with token ID: 1
NFT with ID 1 minted and listed by owner 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 with identity DEPLOYER.
NFT Bought!
New owner of Token ID 1 is 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC with identity of BUYER_1
Seller 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 has 10.0 eth!
0x70997970C51812dc3A010C7d01b50e0d17dc79C8 old:9999.999797616067546951 eth,withdrawProceeds After:10009.9997570794102017 eth!
✔ mint and list and buy item (232ms)
owner:0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
addr1:0x70997970C51812dc3A010C7d01b50e0d17dc79C8
addr2:0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC
NftMarketplace Token Contract deployed address -> 0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9
MSHK721NFT Token Contract deployed address -> 0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9 owner:0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
Approving Marketplace as operator of NFT...
Listing NFT...
NFT Listed with token ID: 1
Updating listing for token ID 1 with a new price
oldPrice: 0.1
updated price: 0.5
Updated listing has price of 0.5
NFT with ID 1 Canceled...
Seller is Zero Address (i.e no one!) 0x0000000000000000000000000000000000000000
✔ update and cancel nft item (156ms)
2 passing (2s)
4、将 NFT 部署到 Rinkeby 网络,在 OpenSea 上查看
打开 hardhat.config.js
文件,编辑内容如下并保存:
- 修改里面的
RINKEBY_RPC_URL
为你的地址,如果没有帐号,可以去 alchemy.com 注册一个,以后开发区块链时会经常使用到。 - 修改
PRIVATE_KEY
为你要部署的帐号私钥。
4.1、部署 NFT市场
运行下面的命令,将 NFT市场
部署到 Rinkeby
网络:
$ npx hardhat run script/ERC721/01-deploy-NftMarketplace.js --network rinkeby
----------------------------------------------------
deployer address -> 0xbB0a92d634D7b9Ac69079ed0e521CC2e0a97c420
NftMarketplace Contract deployed address -> 0x48aD115EE899Cc01d6Fd2Ea9BC3fE5bd7d3E1B1C
在 Rinkeby
网络,查看我们创建的NFT交易市场合约,效果如下图:
4.2、部署 NFT 721示例
运行下面的命令,将 NFT示例
部署到 Rinkeby
网络:
$ npx hardhat run script/ERC721/02-deploy-MSHKNFT.js --network rinkeby
----------------------------------------------------
deployer address -> 0xbB0a92d634D7b9Ac69079ed0e521CC2e0a97c420
MSHK721NFT Contract deployed address -> 0x4b241b36D445E46dAE1916f5A0e76dfE470df115
----------------------------------------------------
记住我们创建的合约地址
0x4b241b36D445E46dAE1916f5A0e76dfE470df115
,后面我们会对合约进行线上验证。
在 Rinkeby
网络,查看我们创建的NFT721合约,效果如下图:
4.3、对 NFT 721示例 合约在 Rinkeby 网络进行验证
验证 NFT示例
合约:
$ npx hardhat verify --contract contracts/ERC721/MSHK721NFT.sol:MSHK721NFT 0x4b241b36D445E46dAE1916f5A0e76dfE470df115 --network rinkeby
Nothing to compile
Successfully submitted source code for contract
contracts/ERC721/MSHK721NFT.sol:MSHK721NFT at 0x4b241b36D445E46dAE1916f5A0e76dfE470df115
for verification on the block explorer. Waiting for verification result...
Successfully verified contract MSHK721NFT on Etherscan.
https://rinkeby.etherscan.io/address/0x4b241b36D445E46dAE1916f5A0e76dfE470df115#code
4.4、在 Rinkeby 网络铸造 NFT
我们打开 Rinkeby 网络,浏览刚刚创建的 NFT 721示例 合约,为地址 0x0BFd206c851729590DDAdfCa9439b30aD2AAbf9F
创建一个 NFT
,NFT
的元数据,使用 IPFS工具创建好的元数据地址 https://bafybeif5jtlbetjp2nzj64gstexywpp53efr7yynxf4qxtmf5lz6seezia.ipfs.infura-ipfs.io
。
操作步骤如下图:
创建 NFT
后我们可以通过 交易哈希 看到,NFT合约 0x4b241b36d445e46dae1916f5a0e76dfe470df115
,刚刚创建的 Token ID
为 1
的 Token。
4.5、在 opensea 查看刚刚铸造的NFT
浏览以下地址 https://testnets.opensea.io/assets/rinkeby/0x4b241b36d445e46dae1916f5a0e76dfe470df115/1 可以看到我们刚刚铸的NFT
图片。
在URL部分,rinkeby
表示网络名称,0x4b241b36d445e46dae1916f5a0e76dfe470df115
是 NFT721
的合约地址,1
是 Token ID
。
至此,我们完成了如何铸造NFT
,以及完善一个可以买、卖交易的 NFT市场
,包括发布到 rinkeby
网络后,在 opensea
测试网络查看。
如果发布到主网,将
rinkeby
更改为ethmainnet
即可。
5、项目源码
Github:https://github.com/idoall/NFT-ERC721-NFTMarketPlace
6、推荐阅读
常用词汇表
Solidity v0.8.4 Custom Error
博文作者:迦壹
转载声明:可以转载, 但必须以超链接形式标明文章原始出处和作者信息及版权声明,谢谢合作!