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

目錄

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/[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 創建一個 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


轉載聲明:可以轉載, 但必須以超鏈接形式標明文章原始出處和作者信息及版權聲明,謝謝合作!


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章