目錄
[TOC]
1壹哺、ERC721的基礎(chǔ)知識
1.1抄伍、什么是不可替代代幣艘刚?
1.2管宵、什么是 ERC-721?
1.3攀甚、什么是元數(shù)據(jù)
1.4箩朴、如何在鏈上保存NFT的圖像
2、HardHat
3秋度、創(chuàng)建項目
3.1炸庞、創(chuàng)建 NFT 市場
3.2、創(chuàng)建 NFT 智能合約
3.3荚斯、編寫測試腳本
4埠居、將 NFT 部署到 Rinkeby 網(wǎng)絡(luò),在 OpenSea 上查看
4.1事期、部署 NFT市場
4.2滥壕、部署 NFT 721示例
4.3、對 NFT 721示例 合約在 Rinkeby 網(wǎng)絡(luò)進(jìn)行驗證
4.4兽泣、在 Rinkeby 網(wǎng)絡(luò)鑄造 NFT
4.5绎橘、在 opensea 查看剛剛鑄造的NFT
5、項目源碼
6唠倦、推薦閱讀
1称鳞、ERC721的基礎(chǔ)知識
1.1、什么是不可替代代幣稠鼻?
NFT
是獨一無二的冈止,每個令牌都有獨特的特征和價值『虺荩可以成為 NFT
的東西類型有收藏卡靶瘸、藝術(shù)品、機票等毛肋,它們之間都有明顯的區(qū)別怨咪,不可互換。將不可替代代幣 (NFT) 視為稀有收藏品润匙;而且大多數(shù)時候诗眨,還有它的元數(shù)據(jù)屬性。
1.2孕讳、什么是 ERC-721匠楚?
ERC-721
(Ethereum Request for Comments 721)由 William Entriken巍膘、Dieter Shirley、Jacob Evans 和 Nastassia Sachs 于 2018 年 1 月提出芋簿,是一種不可替代的代幣標(biāo)準(zhǔn)峡懈。描述了如何在 EVM(以太坊虛擬機)兼容的區(qū)塊鏈上構(gòu)建不可替代的代幣;它是不可替代代幣的標(biāo)準(zhǔn)接口与斤;它有一套規(guī)則肪康,可以很容易地使用 NFT
。NFT
不僅是 ERC-721 類型撩穿;它們也可以是ERC-1155 令牌磷支。
ERC-721
引入了 NFT
標(biāo)準(zhǔn),換句話說食寡,這種類型的 Token
是獨一無二的雾狈,并且可能具有與來自同一智能合約的另一個 Token
不同的價值,可能是由于它的年齡抵皱、稀有性甚至是其他類似自定義屬性等等善榛。
所有 NFT
都有一個 uint256
類型的變量 tokenId
,因此對于任何 ERC-721
合約呻畸,該對 contract address
移盆、uint256 tokenId
必須是全局唯一的。也就是說擂错,一個 dApp 可以有一個“轉(zhuǎn)換器”味滞,它使用 tokenId
作為輸入并輸出一些很酷的東西的圖像,比如僵尸钮呀、武器剑鞍、技能或貓、狗一類的爽醋!
1.3蚁署、什么是元數(shù)據(jù)
所有 NFT
都有元數(shù)據(jù)。您可以在最初的ERC/EIP 721 提案中了解這一點 蚂四。 基本上光戈,社區(qū)發(fā)現(xiàn)在以太坊上存儲圖像真的很費力而且很昂貴。如果你想存儲一張 8 x 8 的圖片遂赠,存儲這么多數(shù)據(jù)是相當(dāng)便宜的久妆,但如果你想要一張分辨率不錯的圖片,你就需要花更多的 GAS
費用跷睦。
雖然 以太坊 2.0 將解決很多這些擴展難題筷弦,但目前,社區(qū)需要一個標(biāo)準(zhǔn)來幫助解決這個問題,這也就是元數(shù)據(jù)的存在原因烂琴。
{
"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
測試網(wǎng)上的 OpenSea
市場上看到我使用元數(shù)據(jù)更新后的效果。類似展示 NFT
的市場 還有如 Mintable号醉、Rarible反症。
1.4、如何在鏈上保存NFT的圖像
您會在上面的元數(shù)據(jù)代碼示例中注意到扣癣,圖像使用指向 IPFS
的 URL
惰帽,這是一種流行的圖像存儲方式憨降。
IPFS
代表星際文件系統(tǒng)父虑,是一種點對點超媒體協(xié)議,旨在使網(wǎng)絡(luò)更快授药、更安全士嚎、更開放。它允許任何人上傳文件悔叽,并且該文件被散列莱衩,因此如果它發(fā)生變化,它的散列也會發(fā)生變化娇澎。這是存儲圖像的理想選擇笨蚁,因為這意味著每次更新圖像時,鏈上的 hash/tokenURI 也必須更改趟庄,這意味著我們可以記錄元數(shù)據(jù)的歷史記錄括细。將圖像添加到 IPFS
上也非常容易,并且不需要運行服務(wù)器戚啥!
2、HardHat
關(guān)于 HardHat 的介紹以及安裝猫十,可以參考文章 如何使用ERC20代幣實現(xiàn)買览濒、賣功能并完成Dapp部署
3、創(chuàng)建項目
3.1拖云、創(chuàng)建 NFT 市場
進(jìn)入 hardhat
項目目錄贷笛,創(chuàng)建 contracts/ERC721/NftMarketplace.sol
文件,內(nèi)容如下:
$ 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 語句一起使用杉允,這會導(dǎo)致當(dāng)前調(diào)用中的所有更改都被還原并將錯誤數(shù)據(jù)傳遞回調(diào)用者
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列表和賣家的對應(yīng)狀態(tài)
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 被認(rèn)為是無效的席里,返回NFT持有者地址
address owner = nft.ownerOf(tokenId);
if (spender != owner) {
revert NotOwner();
}
_;
}
/*
* @notice 將 NFT 加入到市場列表中,external 表示這是一個外部函數(shù)
* @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) {
// 終止運行并撤銷狀態(tài)更改
revert PriceMustBeAboveZero();
}
IERC721 nft = IERC721(nftAddress);
// 獲取單個NFT的批準(zhǔn)地址拢驾,如果tokenId不是有效地址奖磁,拋出異常,
if (nft.getApproved(tokenId) != address(this)) {
revert NotApprovedForMarketplace();
}
// 存儲智能合約狀態(tài)
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 方法 防止合約被重復(fù)調(diào)用
* @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) 所有權(quán)從 listedItem.seller 轉(zhuǎn)移到 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轉(zhuǎn)移到其他帳號,同時設(shè)置收益余額為0
*/
function withdrawProceeds() external {
uint256 proceeds = s_proceeds[msg.sender];
if (proceeds <= 0) {
revert NoProceeds();
}
s_proceeds[msg.sender] = 0;
// 將 ETH 發(fā)送到地址的方法稠腊,關(guān)于此語法更多介紹可以參考下面鏈接
// 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
語句一起使用吞彤,這會導(dǎo)致當(dāng)前調(diào)用中的所有更改都被還原并將錯誤數(shù)據(jù)傳遞回調(diào)用者。
??自定義錯誤是在智能合約主體之外聲明的叹放。當(dāng)錯誤被拋出時饰恕,在 Solidity
中意味著當(dāng)某些檢查和條件失敗,周圍函數(shù)的執(zhí)行被“還原”井仰。
代碼中主要內(nèi)容介紹:
- notListed埋嵌、isListed、isOwner是函數(shù)修飾符的應(yīng)用俱恶。
- listItem方法,將
NFT
加入到列表雹嗦,會做一些權(quán)限驗證。其中用到了函數(shù)修飾符和事件 - cancelListing方法合是,從列表中刪除
NFT
了罪,將NFT
下架。 - buyItem方法端仰,購買
NFT
捶惜,項目中主要用ETH
來交換NFT
資產(chǎn),也可以用其他數(shù)字資產(chǎn)進(jìn)行交換荔烧。同時會更新賣家余額吱七。從listItem中下架NFT
。 - updateListing方法鹤竭,更新
NFT
的價格踊餐。 - withdrawProceeds方法,將賣出的收益從合約中轉(zhuǎn)移給賣家臀稚。
- getListing方法吝岭,根據(jù)
NFT
地址和tokenId
,返回賣家和價格信息。 - getProceeds方法窜管,查看賣家賣出后的收益散劫。
3.2、創(chuàng)建 NFT 智能合約
在編寫測試腳本前幕帆,我們需要一個 NFT的智能合約示例
获搏,以便我們鑄造的 NFT
可以在市場上展示、銷售失乾。我們將遵守 ERC721
令牌規(guī)范常熙,我們將從 OpenZeppelin
的 ERC721URIStorage
庫繼承。
進(jìn)入 hardhat
項目目錄碱茁,創(chuàng)建 contracts/ERC721/MSHK721NFT.sol
文件裸卫,內(nèi)容如下:
$ 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 {
// 遞增遞減計數(shù)器
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 元數(shù)據(jù)的 JSON 文檔
*/
function mintNFT(address recipient, string memory tokenURI)
external
onlyOwner
returns (uint256)
{
// 遞增
_tokenIds.increment();
// 獲取當(dāng)前新的 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個參數(shù)墓贿,第1個參數(shù)是接收NFT
的地址,第2個參數(shù)是 NFT
的 URL
地址退个,也就是上文中提到的元數(shù)據(jù)地址募壕。
3.3调炬、編寫測試腳本
在編寫測試腳本前语盈,我們先通過 IPFS工具,上傳我們的圖片和元數(shù)據(jù)文件,下面是我們已經(jīng)上傳好的2個元數(shù)據(jù)文件:
文件1缰泡,內(nèi)容如下:
{
"name": "mshk-logo-black",
"description": "mshk.top logo black",
"image": "https://bafybeihodzhbtntgml7t72maxill576ssax6md5kfu72aq4gd4p53oipn4.ipfs.infura-ipfs.io/",
"attributes": [
{
"trait_type": "customAttr",
"value": 100
}
]
}
文件2刀荒,內(nèi)容如下:
{
"name": "mshk-logo-blue",
"description": "mshk.top logo blue",
"image": "https://bafybeifxkvzedhwclmibidf5hjoodwqkk2vlbbrlhd3bxbl3wzmkmyrvpq.ipfs.infura-ipfs.io/",
"attributes": [
{
"trait_type": "customAttr",
"value": 200
}
]
}
進(jìn)入 hardhat
項目目錄,創(chuàng)建 test/ERC721/01_NFT.js
測試文件棘钞,內(nèi)容如下:
const { expect } = require("chai");
const { ethers } = require("hardhat");
/**
* 運行測試方法:
* npx hardhat test test/ERC721/01_NFT.js
*/
describe("NFT MarketPlace Test", () => {
// NFT 元數(shù)據(jù)1
const TOKEN_URI1 = "https://bafybeif5jtlbetjp2nzj64gstexywpp53efr7yynxf4qxtmf5lz6seezia.ipfs.infura-ipfs.io";
// NFT 元數(shù)據(jù)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)函數(shù)的返回值僅在函數(shù)被鏈上調(diào)用時才可用(即缠借,從這個合約或從另一個合約)
// 當(dāng)從鏈下(例如,從 ethers.js 腳本)調(diào)用此類函數(shù)時宜猜,需要在交易中執(zhí)行它泼返,并且返回值是該交易的哈希值,因為不知道交易何時會被挖掘并添加到區(qū)塊鏈中
// 為了在從鏈下調(diào)用非常量函數(shù)時獲得它的返回值,可以發(fā)出一個包含將要返回的值的事件
let tokenId = mintTxReceipt.events[0].args.tokenId
expect(tokenId).to.equal(1);
// 授權(quán) 市場合約 可以操作這個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
// 根據(jù) 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)函數(shù)的返回值僅在函數(shù)被鏈上調(diào)用時才可用(即姨拥,從這個合約或從另一個合約)
// 當(dāng)從鏈下(例如绅喉,從 ethers.js 腳本)調(diào)用此類函數(shù)時,需要在交易中執(zhí)行它叫乌,并且返回值是該交易的哈希值,因為不知道交易何時會被挖掘并添加到區(qū)塊鏈中
// 為了在從鏈下調(diào)用非常量函數(shù)時獲得它的返回值柴罐,可以發(fā)出一個包含將要返回的值的事件
let tokenId = mintTxReceipt.events[0].args.tokenId
// 授權(quán) 市場合約 可以操作這個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())}`)
// 獲取信息,確認(rèn)價格是否有變更.
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)
});
});
上面的測試腳本中憨奸,我們分成兩部分革屠,注釋比較詳細(xì),下面是簡要介紹這兩部分測試的功能。
??第1部分:
- 為
addr1
用戶鑄1個NFT - 授權(quán)
NFT市場
可以操作這個addr1
的 NFT似芝。 - 將
NFT
加入到NFT市場
那婉,設(shè)置價格為10
ETH。 - 使用
addr2
用戶購買addr1
的NFT党瓮。 - 查看
addr1
在NFT市場
的余額 - 將
NFT市場
中的余額取出到addr1
的余額吧恃,對比前后余額數(shù)據(jù)。
第2部分:
- 為
addr2
用戶鑄1個NFT - 授權(quán)
NFT市場
可以操作這個addr2
的 NFT麻诀。 - 將
NFT
加入到NFT市場
痕寓,設(shè)置價格為0.1
ETH。 - 將
addr2
的NFT價格從0.1
ETH 更新為0.5
ETH蝇闭。進(jìn)行數(shù)據(jù)對比輸出呻率。 - 從
NFT市場
中下架addr2
的 NFT。
下面是我們運行測試腳本的效果:
到目前為止呻引,我們已經(jīng)完成了 NFT
的創(chuàng)建礼仗,并將 NFT
加入到市場完成了買、賣逻悠、查看銷售后的余額元践,轉(zhuǎn)帳給賣家等功能。
項目的源碼都保存在 Github:https://github.com/idoall/NFT-ERC721-NFTMarketPlace
克隆項目到本地后童谒,進(jìn)入 hardhat
項目目錄单旁,先執(zhí)行 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/hardhat-waffle@2.0.3" has incorrect peer dependency "@nomiclabs/hardhat-ethers@^2.0.0".
warning " > @openzeppelin/hardhat-upgrades@1.19.0" has incorrect peer dependency "@nomiclabs/hardhat-ethers@^2.0.0".
warning "hardhat-deploy > zksync-web3@0.4.0" 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 網(wǎng)絡(luò)愉豺,在 OpenSea 上查看
打開 hardhat.config.js
文件,編輯內(nèi)容如下并保存:
- 修改里面的
RINKEBY_RPC_URL
為你的地址,如果沒有帳號茫因,可以去 alchemy.com 注冊一個蚪拦,以后開發(fā)區(qū)塊鏈時會經(jīng)常使用到。 - 修改
PRIVATE_KEY
為你要部署的帳號私鑰冻押。
4.1驰贷、部署 NFT市場
運行下面的命令,將 NFT市場
部署到 Rinkeby
網(wǎng)絡(luò):
$ npx hardhat run script/ERC721/01-deploy-NftMarketplace.js --network rinkeby
----------------------------------------------------
deployer address -> 0xbB0a92d634D7b9Ac69079ed0e521CC2e0a97c420
NftMarketplace Contract deployed address -> 0x48aD115EE899Cc01d6Fd2Ea9BC3fE5bd7d3E1B1C
在 Rinkeby
網(wǎng)絡(luò)翼雀,查看我們創(chuàng)建的NFT交易市場合約饱苟,效果如下圖:
4.2、部署 NFT 721示例
運行下面的命令狼渊,將 NFT示例
部署到 Rinkeby
網(wǎng)絡(luò):
$ npx hardhat run script/ERC721/02-deploy-MSHKNFT.js --network rinkeby
----------------------------------------------------
deployer address -> 0xbB0a92d634D7b9Ac69079ed0e521CC2e0a97c420
MSHK721NFT Contract deployed address -> 0x4b241b36D445E46dAE1916f5A0e76dfE470df115
----------------------------------------------------
記住我們創(chuàng)建的合約地址
0x4b241b36D445E46dAE1916f5A0e76dfE470df115
,后面我們會對合約進(jìn)行線上驗證箱熬。
在 Rinkeby
網(wǎng)絡(luò)类垦,查看我們創(chuàng)建的NFT721合約,效果如下圖:
4.3城须、對 NFT 721示例 合約在 Rinkeby 網(wǎng)絡(luò)進(jìn)行驗證
驗證 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 網(wǎng)絡(luò)鑄造 NFT
我們打開 Rinkeby 網(wǎng)絡(luò),瀏覽剛剛創(chuàng)建的 NFT 721示例 合約糕伐,為地址 0x0BFd206c851729590DDAdfCa9439b30aD2AAbf9F
創(chuàng)建一個 NFT
砰琢,NFT
的元數(shù)據(jù),使用 IPFS工具創(chuàng)建好的元數(shù)據(jù)地址 https://bafybeif5jtlbetjp2nzj64gstexywpp53efr7yynxf4qxtmf5lz6seezia.ipfs.infura-ipfs.io
良瞧。
操作步驟如下圖:
創(chuàng)建 NFT
后我們可以通過 交易哈希 看到陪汽,NFT合約 0x4b241b36d445e46dae1916f5a0e76dfe470df115
,剛剛創(chuàng)建的 Token ID
為 1
的 Token褥蚯。
4.5挚冤、在 opensea 查看剛剛鑄造的NFT
瀏覽以下地址 https://testnets.opensea.io/assets/rinkeby/0x4b241b36d445e46dae1916f5a0e76dfe470df115/1 可以看到我們剛剛鑄的NFT
圖片。
在URL部分赞庶,rinkeby
表示網(wǎng)絡(luò)名稱训挡,0x4b241b36d445e46dae1916f5a0e76dfe470df115
是 NFT721
的合約地址,1
是 Token ID
歧强。
至此澜薄,我們完成了如何鑄造NFT
,以及完善一個可以買摊册、賣交易的 NFT市場
肤京,包括發(fā)布到 rinkeby
網(wǎng)絡(luò)后,在 opensea
測試網(wǎng)絡(luò)查看丧靡。
如果發(fā)布到主網(wǎng)蟆沫,將
rinkeby
更改為ethmainnet
即可。
5温治、項目源碼
Github:https://github.com/idoall/NFT-ERC721-NFTMarketPlace
6、推薦閱讀
常用詞匯表
??Solidity v0.8.4 Custom Error
博文作者:迦壹
轉(zhuǎn)載聲明:可以轉(zhuǎn)載, 但必須以超鏈接形式標(biāo)明文章原始出處和作者信息及版權(quán)聲明戒悠,謝謝合作熬荆!