目錄
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
上也非常容易,並且不需要運行服務器!
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
轉載聲明:可以轉載, 但必須以超鏈接形式標明文章原始出處和作者信息及版權聲明,謝謝合作!