如何使用Solidity和Hardhat構建你自己的NFT以及NFT交易市場

Lion發表於2022-08-05

目錄

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(以太坊虛擬機器)相容的區塊鏈上構建不可替代的代幣;它是不可替代代幣的標準介面;它有一套規則,可以很容易地使用 NFTNFT 不僅是 ERC-721 型別;它們也可以是ERC-1155 令牌。

ERC-721 引入了 NFT 標準,換句話說,這種型別的 Token 是獨一無二的,並且可能具有與來自同一智慧合約的另一個 Token 不同的價值,可能是由於它的年齡、稀有性甚至是其他類似自定義屬性等等。

所有 NFT 都有一個 uint256 型別的變數 tokenId,因此對於任何 ERC-721 合約,該對 contract addressuint256 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 分配給他們的 tokenURINFT 市場將能夠展示你的代幣,您可以在 Rinkeby 測試網上的 OpenSea 市場上看到我使用後設資料更新後的效果。類似展示 NFT 的市場 還有如 MintableRarible
mshk.top

1.4、如何在鏈上儲存NFT的影像

您會在上面的後設資料程式碼示例中注意到,影像使用指向 IPFSURL,這是一種流行的影像儲存方式。

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 令牌規範,我們將從 OpenZeppelinERC721URIStorage 庫繼承。

進入 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個引數是 NFTURL 地址,也就是上文中提到的後設資料地址。

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。
  • 檢視addr1NFT市場 的餘額
  • 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。

下面是我們執行測試指令碼的效果:
mshk.top

到目前為止,我們已經完成了 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/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 網路,在 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 建立一個 NFTNFT 的後設資料,使用 IPFS工具建立好的後設資料地址 https://bafybeif5jtlbetjp2nzj64gstexywpp53efr7yynxf4qxtmf5lz6seezia.ipfs.infura-ipfs.io

操作步驟如下圖:
mshk.top

建立 NFT 後我們可以透過 交易雜湊 看到,NFT合約 0x4b241b36d445e46dae1916f5a0e76dfe470df115,剛剛建立的 Token ID1的 Token。

4.5、在 opensea 檢視剛剛鑄造的NFT

瀏覽以下地址 https://testnets.opensea.io/assets/rinkeby/0x4b241b36d445e46dae1916f5a0e76dfe470df115/1 可以看到我們剛剛鑄的NFT 圖片。
在URL部分,rinkeby 表示網路名稱,0x4b241b36d445e46dae1916f5a0e76dfe470df115NFT721 的合約地址,1Token ID
mshk.top

至此,我們完成了如何鑄造NFT,以及完善一個可以買、賣交易的 NFT市場,包括髮布到 rinkeby 網路後,在 opensea 測試網路檢視。

如果釋出到主網,將 rinkeby 更改為 ethmainnet 即可。

5、專案原始碼

Github:https://github.com/idoall/NFT-ERC721-NFTMarketPlace

6、推薦閱讀

常用詞彙表
  Solidity v0.8.4 Custom Error


轉載宣告:可以轉載, 但必須以超連結形式標明文章原始出處和作者資訊及版權宣告,謝謝合作!


相關文章