跳至內容

在 Linea 上建立 NFT 市場

了解如何在 Linea 上建立並部署去中心化 NFT 市場。

閱讀時間:10 分鐘
在 Linea 上建立 NFT 市場

NFT 自 2021 年熱潮以來,已成為 web3 生態系統的核心組成部分,其應用範疇早已超越收藏品,延伸至會員資格存取、資產代幣化、遊戲道具及數位身份等現實世界的使用場景。

Phosphor 等無程式碼平台透過直覺化的 Creator Studio 與 API 套件,讓創作者和開發者比以往更輕鬆地鑄造、自訂及部署 NFT,無需從頭建立自訂 NFT 合約。然而,對於希望深入理解 Solidity 與 web3 原生體驗的開發者而言,學習如何建構 NFT 市場仍是一項基礎技能。

本指南將帶您逐步完成在 Linea 上開發並部署一個基礎但功能完整的 NFT 市場的流程。完成後,您將具備啟動自己的市場所需的知識,並對智能合約開發、去中心化應用程式及 web3 互動有更深入的掌握。

前置條件

合約概覽

此 NFT 市場將支援以下功能:

  1. 上架 NFT 出售:使用者可設定價格並上架其 NFT,相關資訊安全儲存於智能合約中。

  2. 購買 NFT:買家可支付所需的 Ether 購買已上架的 NFT,確保交易流暢無阻。

  3. 批次購買 NFT:可在單筆交易中購買多個 NFT,以降低 gas 費用。

  4. 下架 NFT:擁有者可將其 NFT 從出售清單中移除,使其返回自己的錢包。

專案設定

本專案將使用 Foundry 框架。安裝 Foundry 後,執行以下指令以建立新專案:

forge init market

在 Vscode 或您慣用的程式碼編輯器中開啟 market 資料夾,並刪除 scripts/counter.s.sol、src/counter.sol 及 test/counter.t.sol。

安裝所有相依套件

forge install foundry-rs/forge-std --no-commit && forge install OpenZeppelin/openzeppelin-contracts --no-commit

NFT 市場智能合約完整程式碼

在 src 資料夾中建立 market.sol 檔案,並將以下程式碼加入該檔案。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
import "lib/openzeppelin-contracts/contracts/token/ERC721/IERC721.sol";
import {Test, console} from "forge-std/Test.sol";
contract NFTMarketplace  {
    struct Listing {
        address seller;
        uint256 price;
    }
    // NFT Contract => Token ID => Listing
    mapping(address => mapping(uint256 => Listing)) public listings;
    event Listed(address indexed nftContract, uint256 indexed tokenId, address seller, uint256 price);
    event Purchase(address indexed nftContract, uint256 indexed tokenId, address buyer, uint256 price);
    event Delisted(address indexed nftContract, uint256 indexed tokenId, address seller);
    modifier isSeller(address nftContract, uint256 tokenId, address spender) {
        require(listings[address(nftContract)][tokenId].seller == spender, "Not the seller");
        _;
    }
    modifier isListed(address nftContract, uint256 tokenId) {
        require(listings[nftContract][tokenId].price > 0, "Not listed");
        _;
    }
    modifier notListed(address nftContract, uint256 tokenId) {
        require(listings[nftContract][tokenId].price == 0, "Already listed");
        _;
    }
    /**
     * @dev List an NFT for sale.
     * @param nftContract The address of the NFT contract.
     * @param tokenId The ID of the token to list.
     * @param price The price of the NFT in wei.
     */
    function listNFT(address nftContract, uint256 tokenId, uint256 price)
        external
        notListed(nftContract, tokenId)
    {
        require(price > 0, "Price must be greater than zero");
        require(IERC721(nftContract).ownerOf(tokenId) == msg.sender, "not owner");
        IERC721(nftContract).transferFrom(msg.sender, address(this), tokenId);
        listings[nftContract][tokenId] = Listing(msg.sender, price);
        emit Listed(nftContract, tokenId, msg.sender, price);
    }
    /**
     * @dev Buy a listed NFT.
     * @param nftContract The address of the NFT contract.
     * @param tokenId The ID of the token to buy.
     */
    function buyNFT(address nftContract, uint256 tokenId)
        public
        payable
        isListed(nftContract, tokenId)
    {
        Listing memory listing = listings[nftContract][tokenId];
        require(msg.value >= listing.price, "Insufficient funds");
        // Remove listing
        delete listings[nftContract][tokenId];
        // Transfer payment to the seller
        (bool sent, ) = listing.seller.call{value: listing.price}("");
        require(sent, "Failed to send Ether");
        // Transfer NFT to the buyer
        IERC721(nftContract).transferFrom(address(this), msg.sender, tokenId);
        emit Purchase(nftContract, tokenId, msg.sender, listing.price);
    }

    function buyMultipleNFT(address nftContract, uint256 [] memory tokenIds)
        external
        payable
    {
        uint256 totalAmount;
        for (uint256 i; i < tokenIds.length; i++){
           Listing memory listing = listings[nftContract][tokenIds[i]];
           totalAmount += listing.price;
        }
        require(msg.value >= totalAmount, "Insufficient funds");
        for (uint256 i; i < tokenIds.length; i++){
           buyNFT(nftContract, tokenIds[i]);   
        }
    }
    /**
     * @dev Delist an NFT from the marketplace.
     * @param nftContract The address of the NFT contract.
     * @param tokenId The ID of the token to delist.
     */
    function delistNFT(address nftContract, uint256 tokenId)
        external
        isListed(nftContract, tokenId)
        isSeller(nftContract, tokenId, msg.sender)
    {
        Listing memory listing = listings[nftContract][tokenId];

        // Remove listing
        delete listings[nftContract][tokenId];
        // Return NFT to the seller
        IERC721(nftContract).transferFrom(address(this), listing.seller, tokenId);
        emit Delisted(nftContract, tokenId, listing.seller);
    }
}
複製

程式碼主要功能

Listing 結構體 這對於在去中心化 NFT 市場中實現上架、購買及下架等功能至關重要。它提供了一種清晰且高效的方式來管理上架清單,同時確保市場保有每個項目的賣家與價格記錄——這兩者對市場運作都不可或缺。

struct Listing {
        address seller; // NFT seller
        uint256 price;  // NFT price
    }
複製

Listing 映射 這行程式碼在 Solidity 中定義了一個巢狀映射,用於組織和儲存 NFT 市場的上架清單。外層映射以 NFT 合約地址作為鍵值,讓智能合約能依各自的 NFT 集合對上架清單進行分類,確保市場可支援多個 NFT 集合。

內層映射以 token ID(每個 NFT 的唯一識別碼)作為鍵值,能夠有效管理單一 NFT 合約下的多筆上架清單。

 // NFT Contract => Token ID => Listing
    mapping(address => mapping(uint256 => Listing)) public listings;
複製

NFT 上架

listNFT 函式允許使用者在市場上架其 NFT 出售。它接受三個參數:NFT 合約、token ID 及價格。NFT 合約是指使用者欲上架 NFT 所屬集合的地址,token ID 是該集合中 NFT 的唯一識別碼,價格則是使用者希望出售 NFT 的金額。

函式首先驗證使用者是否為該 NFT 的擁有者,並確保價格大於零。通過這些檢查後,NFT 將從賣家轉移至市場合約,上架映射隨之更新,並以正確的參數觸發 Listed 事件。

function listNFT(address nftContract, uint256 tokenId, uint256 price)
        external
        notListed(nftContract, tokenId)
    {
        require(price > 0, "Price must be greater than zero");
        require(IERC721(nftContract).ownerOf(tokenId) == msg.sender, "not owner");
        IERC721(nftContract).transferFrom(msg.sender, address(this), tokenId);
        listings[nftContract][tokenId] = Listing(msg.sender, price);
        emit Listed(nftContract, tokenId, msg.sender, price);
    }
複製

購買 NFT

buyNFT 函式允許使用者購買市場上已上架的 NFT。它接受兩個參數:nftContract(NFT 智能合約的地址)及 tokenId(欲購買 NFT 的唯一識別碼)。

函式首先驗證買家是否已傳送足夠的 Ether 以支付 NFT 的價格。驗證通過後,該上架清單將從映射中移除,款項安全轉移至賣家,NFT 則從市場合約轉移至買家地址,完成購買流程。

function buyNFT(address nftContract, uint256 tokenId)
        public
        payable
        isListed(nftContract, tokenId)
    {
        Listing memory listing = listings[nftContract][tokenId];
        require(msg.value >= listing.price, "Insufficient funds");
        // Remove listing
        delete listings[nftContract][tokenId];
        // Transfer payment to the seller
        (bool sent, ) = listing.seller.call{value: listing.price}("");
        require(sent, "Failed to send Ether");
        // Transfer NFT to the buyer
        IERC721(nftContract).transferFrom(address(this), msg.sender, tokenId);
        emit Purchase(nftContract, tokenId, msg.sender, listing.price);
    }
複製

批次購買 NFT

buyMultipleNFT 函式允許使用者在單筆交易中購買多個 NFT。它首先透過加總 tokenIds 陣列中所有 NFT 的價格來計算總金額,然後驗證買家是否已傳送足夠的資金以支付總額。

資金驗證通過後,函式會遍歷 tokenIds 陣列,對每個 NFT 呼叫 buyNFT 函式。這確保每個 NFT 都能單獨完成購買、款項轉移至賣家,且 NFT 發送至買家——遵循與 buyNFT 函式相同的流程。

function buyMultipleNFT(address nftContract, uint256 [] memory tokenIds)
        external
        payable
    {
        uint256 totalAmount;
        for (uint256 i; i < tokenIds.length; i++){
           Listing memory listing = listings[nftContract][tokenIds[i]];
           totalAmount += listing.price;
        }
        require(msg.value >= totalAmount, "Insufficient funds");
        for (uint256 i; i < tokenIds.length; i++){
           buyNFT(nftContract, tokenIds[i]);   
        }
    }
複製

下架 NFT

delistNFT 函式允許賣家將其 NFT 從市場移除。它首先驗證 NFT 是否已上架,以及呼叫者是否為合法擁有者。驗證通過後,上架清單將從市場中刪除,有效地將 NFT 從出售清單中移除。

函式接著將 NFT 轉回給賣家,確保他們重新取得資產的所有權。最後,觸發 Delisted 事件以確認 NFT 已成功從市場中移除。

function delistNFT(address nftContract, uint256 tokenId)
        external
        isListed(nftContract, tokenId)
        isSeller(nftContract, tokenId, msg.sender)
    {
        Listing memory listing = listings[nftContract][tokenId];

        // Remove listing
        delete listings[nftContract][tokenId];
        // Return NFT to the seller
        IERC721(nftContract).transferFrom(address(this), listing.seller, tokenId);
        emit Delisted(nftContract, tokenId, listing.seller);
    }
複製

主要事件

合約會針對市場的重要操作觸發事件:

  • Listed:當 NFT 上架時觸發,記錄賣家、價格及 NFT 資訊等詳細資料。

  • Purchase:當買家購買 NFT 時觸發,確保交易可追溯性。

  • Delisted:表示 NFT 已成功從市場中移除,維護透明度。

event Listed(address indexed nftContract, uint256 indexed tokenId, address seller, uint256 price);
    event Purchase(address indexed nftContract, uint256 indexed tokenId, address buyer, uint256 price);
    event Delisted(address indexed nftContract, uint256 indexed tokenId, address seller
複製

測試套件概覽

在 src 資料夾中建立 NFT.sol 檔案,並將以下程式碼加入該檔案。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
import "lib/openzeppelin-contracts/contracts/token/ERC721/ERC721.sol";
import "lib/openzeppelin-contracts/contracts/access/Ownable.sol";
contract NFT is ERC721, Ownable {
    uint256 private _tokenIdCounter;
    string private _baseTokenURI;
    event Minted(address indexed to, uint256 tokenId);
    constructor(string memory name, string memory symbol, string memory baseTokenURI) ERC721(name, symbol) Ownable(msg.sender) {
        _baseTokenURI = baseTokenURI;
    }
    /// @dev Function to mint a new NFT
    function mint(address to) external onlyOwner {
        uint256 tokenId = _tokenIdCounter;
        _tokenIdCounter += 1;
        _safeMint(to, tokenId);
        emit Minted(to, tokenId);
    }
    /// @dev Override base token URI for metadata
    function _baseURI() internal view override returns (string memory) {
        return _baseTokenURI;
    }
    /// @dev Function to update the base token URI
    function setBaseTokenURI(string memory newBaseTokenURI) external onlyOwner {
        _baseTokenURI = newBaseTokenURI;
    }
    /// @dev Function to retrieve the current token ID count
    function getTokenCount() external view returns (uint256) {
        return _tokenIdCounter;
    }
}
複製

在 test 資料夾中建立 market.t.sol 檔案,並將以下程式碼加入該檔案。

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import {Test, console} from "forge-std/Test.sol";
import {NFTMarketplace} from "../src/Market.sol";
import {NFT} from "../src/NFT.sol";
contract MarketTest is Test {
    NFTMarketplace public market;
    NFT public nft;
    address public user1 = makeAddr("user1");
    address public user2 = makeAddr("user2");
    address public user3 = makeAddr("user3");
    function setUp() public {
        nft = new NFT("TestNFT", "tnft", "");
        market = new NFTMarketplace();
        nft.mint(user1);
        nft.mint(user1);
        nft.mint(user1);
    }
    function test_Listing() public {
        vm.startPrank(user1);
        nft.approve(address(market), 0);
        market.listNFT(address(nft), 0, 1 ether);

    }

     function test_BuyingNFT() public {
        vm.startPrank(user1);
        nft.approve(address(market), 0);
        market.listNFT(address(nft), 0, 1 ether);

        vm.startPrank(user2);
        deal(user2, 10 ether);
        market.buyNFT{value: 1 ether}(address(nft), 0);
        assert(nft.ownerOf(0) == user2);

    }

      function test_BuyingMultipleNFT() public {
        vm.startPrank(user1);
        nft.approve(address(market), 0);
        nft.approve(address(market), 1);
        nft.approve(address(market), 2);
        market.listNFT(address(nft), 0, 1 ether);
        market.listNFT(address(nft), 1, 1 ether);
        market.listNFT(address(nft), 2, 1 ether);

        vm.startPrank(user2);
        deal(user2, 10 ether);
        uint256 [] memory nfts = new uint256[] (3);
        nfts[0] = 0;
        nfts[1] = 1;
        nfts[2] = 2; 
        market.buyMultipleNFT{value: 3 ether}(address(nft), nfts);     
    }

     function test_DelistingNFT() public {
        vm.startPrank(user1);
        nft.approve(address(market), 0);
        market.listNFT(address(nft), 0, 1 ether);
        assert(nft.ownerOf(0) == address(market));
        market.delistNFT(address(nft), 0);
        assert(nft.ownerOf(0) == address(user1));

    }
}
複製

上述測試套件確保 NFT 市場合約在其核心功能上能如預期運作。以下是各測試的說明:

設定測試環境

setUp 函式透過部署 NFTMarketplace 和 NFT 合約來初始化環境,並為 user1 鑄造三個 NFT 供測試使用。模擬 NFT 合約(NFT)部署時附帶 mint 函式以生成 NFT,同時實例化市場合約(NFTMarketplace)。User1 擁有所有已鑄造的 NFT,以便測試上架與下架功能。

測試 NFT 上架

test_Listing 函式確保 user1 可以在市場上架 NFT 出售。

  • User1 授權市場合約轉移 tokenId 0。

  • User1 以 1 Ether 的價格上架該 NFT。

  • 測試確認 listNFT 函式運作正常,且 NFT 已轉移至市場合約。

測試單筆 NFT 購買

test_BuyingNFT 函式模擬 user2 購買已上架 NFT 的流程。

  • User1 以 1 Ether 上架 tokenId 0。

  • User2 收到 10 Ether 並呼叫 buyNFT 函式,傳送 1 Ether 購買該 NFT。

  • 購買完成後,測試驗證 user2 是 tokenId 0 的新擁有者。

測試批次 NFT 購買

test_BuyingMultipleNFT 函式測試在單筆交易中購買多個 NFT 的能力。

  • User1 以每個 1 Ether 的價格上架三個 NFT(tokenId 0、1、2)。

  • User2 呼叫 buyMultipleNFT,傳入 token ID 並傳送 3 Ether。

  • 測試確保 user2 成為所有三個 NFT 的擁有者,且市場正確處理多筆購買。

測試 NFT 下架

test_DelistingNFT 函式驗證 user1 可以將其 NFT 從市場下架。

  • User1 以 1 Ether 上架 tokenId 0 後呼叫 delistNFT。

  • NFT 返回給 user1,測試確認所有權已恢復至 user1。

執行測試

forge test

在 Linea 上部署與驗證

將所需變數加入 .env 檔案,確保其位於專案的根目錄層級:

LINEA_RPC_URL=https://linea-mainnet.infura.io/v3/ // Infura RPC endpoint for connecting to Linea 
PRIVATE_KEY=************************************************* // Wallet private key for signing transactions LINEA_API_KEY=************************* // API key for accessing Linea services via Infura
複製

將以下內容加入 foundry.toml:

[etherscan]
linea = { key = "${LINEA_API_KEY}", url = "https://api-sepolia.lineascan.build/api" }
複製

建立 Makefile 並加入以下內容:

deploy:
    forge create src/Market.sol:NFTMarketplace --rpc-url $(LINEA_RPC_URL) --private-key $(PRIVATE_KEY)

verify:; forge verify-contract --rpc-url $(LINEA_RPC_URL) --chain linea <contract address> src/Market.sol:NFTMarketplace
複製

部署

make deploy

驗證

make verify

結語

在 Linea 上建構 NFT 市場,讓開發者能夠親身實踐智能合約開發、Solidity 程式設計及 web3 互動。本指南涵蓋了去中心化市場的核心功能,包括上架、購買、批次購買及下架 NFT,同時確保交易安全且高效。

我們也逐步介紹了使用 Foundry 實作與測試智能合約的流程,以及在 Linea 上的部署與驗證過程。透過本指南,您現在已具備建構、測試及部署自己的 NFT 市場所需的基礎知識。

如需更多實作教學及區塊鏈開發深度解析,歡迎訂閱 MetaMask Developer YouTube 頻道,並追蹤我們的 Twitter 以掌握最新動態。

AI 翻譯。可能包含錯誤。請務必核實資訊。

評價此翻譯

本文作者:

  • Kingsley Okonkwo

    Kingsley 是一位經認證的以太坊區塊鏈開發者,目前擔任 Consensys 的技術寫作員,專注於開發者工具領域。在加入 Consensys 之前,Kingsley 曾在多個平台上擔任自由接案後端開發者,包括 Gigster 和 Braintrust。他擁有 5 年的軟體開發經驗,熟練掌握 JavaScript(Node.js 與 React)、GoLang、SQL 及 Docker 等工具。Kingsley 擅長撰寫步驟式教學與操作指南,善用最新的 Web3 技術,協助開發者社群打造更優質的 dapp。他目前定居於杜拜,閒暇時喜歡打籃球和踢足球。

    閱讀所有文章