在 Linea 上构建 NFT 市场

了解如何在 Linea 上构建和部署去中心化 NFT 市场。

阅读时长:10 分钟
在 Linea 上构建 NFT 市场

自2021年的繁荣以来,NFT已成为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集合。

内层映射以代币ID(每个NFT的唯一标识符)为键,实现对单个NFT合约下多个挂单的高效管理。

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

NFT上架

listNFT函数允许用户在市场上上架其NFT出售。它接受三个参数:NFT合约、代币ID和价格。NFT合约是指用户希望上架NFT所属集合的地址,代币ID是该NFT在集合中的唯一标识符,价格是用户希望出售该NFT的金额。

该函数首先验证用户是否为NFT的所有者,并确保价格大于零。通过这些检查后,NFT将从卖家转移至市场,listings映射随之更新,并以正确的参数触发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的价格。验证通过后,挂单将从listings映射中移除,款项安全转账给卖家,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用于测试。部署了一个带有mint函数的模拟NFT合约(NFT)用于生成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,传入代币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 Okonkwo

    Kingsley 是一位经认证的以太坊区块链开发者,现任 Consensys 技术写作工程师,专注于开发者工具领域。加入 Consensys 之前,Kingsley 曾在 GigsterBraintrust 等多个平台担任自由职业后端开发者。他拥有 5 年软件开发经验,熟练掌握 JavaScript(Node.js 和 React)、GoLang、SQL 及 Docker 等技术工具。Kingsley 擅长撰写分步教程和操作指南,善用最新 Web3 技术帮助开发者构建更优质的 dapp。他目前定居迪拜,业余时间喜欢打篮球和踢足球。

    阅读所有文章