从零开始:手把手教你使用 Polygon 区块链构建 NFT Marketplace

·

关键词:NFT 交易平台、非同质化代币、Polygon Mumbai 测试网、Hardhat 开发、Ethers.js、智能合约部署、ERC-721、区块链应用

一条完整可运行的 NFT 市场交易链,可以由「合约代码 + 开发工具 + 实战脚本」三步完成。本篇将带你用 30 分钟在 Polygon 测试网(Mumbai)搭好最小可用版的 非同质化代币交易平台。你只需要一台电脑、Node.js 与一条稳定 RPC 节点即可开始。


本文路线图

  1. Polygon NFT 交易场景全景速览
  2. 环境初始化:Hardhat + Ethers.js + 钱包
  3. 撰写 NFT 与 Marketplace 合约
  4. 本地单元测试
  5. 部署到 Mumbai 测试网
  6. 用脚本验证「挂卖—购买—所有权转移」
  7. 常见问题与下一步扩展

1. Polygon NFT 交易场景全景速览

非同质化代币 (NFT) 正在成为数字资产的新货币形态。游戏道具、头像、门票、证书…任何需要“独特性”的场景都能用 NFT 在 区块链 上呈现。
Polygon 提供了 低 gas、高 TPS、EVM 兼容 的侧链环境,让独立开发者也能在几分钟内跑通整套 NFT 交易平台

👉 想边学边上手?先来体验实时交互 demo。


2. 环境初始化

所需软件与账户

安装 Hardhat

mkdir marketplace-hardhat && cd marketplace-hardhat
npm install -D hardhat
npx hardhat

按提示选择「JavaScript 项目」模板。

继续安装依赖:

npm install @openzeppelin/contracts dotenv ethers@^5.7
npm install -D @nomicfoundation/hardhat-toolbox @nomiclabs/hardhat-etherscan

配置 .env

PRIVATE_KEY_ACCOUNT_1=
PRIVATE_KEY_ACCOUNT_2=
RPC_URL=https://rpc.ankr.com/polygon_mumbai
POLYGONSCAN_API_KEY=

3. 撰写 NFT 与 Marketplace 合约

NFT 合约(ERC-721 示例)

路径 contracts/NFT.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract NFT is ERC721, ERC721URIStorage, Ownable {
    using Counters for Counters.Counter;
    Counters.Counter private _tokenIdCounter;

    constructor() ERC721("MyNFT", "MYNFT") {}

    function safeMint(address to, string memory uri) public onlyOwner {
        uint256 tokenId = _tokenIdCounter.current();
        _tokenIdCounter.increment();
        _safeMint(to, tokenId);
        _setTokenURI(tokenId, uri);
    }

    function _burn(uint256 tokenId) internal override(ERC721, ERC721URIStorage) {
        super._burn(tokenId);
    }

    function tokenURI(uint256 tokenId) public view override(ERC721, ERC721URIStorage) returns (string memory) {
        return super.tokenURI(tokenId);
    }
}

Marketplace 合约(主逻辑)

路径 contracts/Marketplace.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Counters.sol";

contract Marketplace is ReentrancyGuard, Ownable {
    using Counters for Counters.Counter;
    Counters.Counter private marketplaceIds;
    Counters.Counter private itemsSold;

    struct Listing {
        uint256 id;
        address nftContract;
        uint256 tokenId;
        address payable seller;
        address payable owner;
        uint256 price;
    }

    mapping(uint256 => Listing) private idToListing;
    event ListingCreated(uint indexed id, address indexed nftContract, uint indexed tokenId, address seller, uint price);

    function createListing(address nftContract, uint256 tokenId, uint256 price) public nonReentrant {
        require(price > 0, "Price >0");
        IERC721(nftContract).transferFrom(msg.sender, address(this), tokenId);
        marketplaceIds.increment();
        uint256 newId = marketplaceIds.current();
        idToListing[newId] = Listing(newId, nftContract, tokenId, payable(msg.sender), payable(address(0)), price);
        emit ListingCreated(newId, nftContract, tokenId, msg.sender, price);
    }

    function buyListing(uint256 listingId) external payable nonReentrant {
        Listing storage item = idToListing[listingId];
        require(msg.value == item.price, "Send exact price");
        item.seller.transfer(msg.value);
        IERC721(item.nftContract).transferFrom(address(this), msg.sender, item.tokenId);
        item.owner = payable(msg.sender);
        itemsSold.increment();
    }

    function getListing(uint listingId) public view returns (Listing memory) {
        return idToListing[listingId];
    }

    function fetchMyListed() public view returns (Listing[] memory) {
        uint total = marketplaceIds.current();
        uint count;
        for (uint i = 1; i <= total; i++) if (idToListing[i].owner == msg.sender) count++;
        
        Listing[] memory items = new Listing[](count);
        uint index;
        for (uint i = 1; i <= total; i++) {
            if (idToListing[i].owner == msg.sender) items[index++] = idToListing[i];
        }
        return items;
    }
}

4. 本地单元测试

路径 test/marketplace-test.js

const { expect } = require("chai");
const { ethers } = require("hardhat");

describe("Marketplace Flow", async () => {
  it("List & Buy NFT", async () => {
    const [acc1, acc2] = await ethers.getSigners();
    const ListNFT = await ethers.getContractFactory("NFT");
    const nft = await ListNFT.deploy();
    await nft.deployed();
    const Market = await ethers.getContractFactory("Marketplace");
    const market = await Market.deploy();
    await market.deployed();

    await nft.safeMint(acc1.address, "ipfs://metadata");
    await nft.approve(market.address, 0);
    await market.createListing(nft.address, 0, ethers.utils.parseEther("0.01"));
    await market.connect(acc2).buyListing(1, { value: ethers.utils.parseEther("0.01") });
    const item = await market.getListing(1);
    expect(item.owner).equal(acc2.address);
  });
});

运行:

npx hardhat compile
npx hardhat test

5. 部署到 Mumbai 测试网

修改 hardhat.config.js

require("dotenv").config();
require("@nomicfoundation/hardhat-toolbox");
require("@nomiclabs/hardhat-etherscan");

module.exports = {
  solidity: "0.8.9",
  networks: {
    mumbai: {
      url: process.env.RPC_URL,
      accounts: [process.env.PRIVATE_KEY_ACCOUNT_1]
    }
  },
  etherscan: {
    apiKey: { polygonMumbai: process.env.POLYGONSCAN_API_KEY }
  }
};

部署脚本 scripts/deploy.js

async function main() {
  const [deployer] = await ethers.getSigners();
  console.log("Deployer:", deployer.address);

  const NFT = await ethers.getContractFactory("NFT");
  const nft = await NFT.deploy();
  await nft.deployed();
  console.log("NFT deployed to:", nft.address);

  const Marketplace = await ethers.getContractFactory("Marketplace");
  const market = await Marketplace.deploy();
  await market.deployed();
  console.log("Marketplace deployed to:", market.address);
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});
npx hardhat run scripts/deploy.js --network mumbai

6. 用脚本验证「挂卖—购买—所有权转移」

创建 scripts/interact.js

async function main() {
  const [acc1] = await ethers.getSigners();
  const acc2 = new ethers.Wallet(process.env.PRIVATE_KEY_ACCOUNT_2, acc1.provider);

  const nft = await ethers.getContractAt("NFT", "YOUR_NFT_ADDRESS");
  const market = await ethers.getContractAt("Marketplace", "YOUR_MARKET_ADDRESS");

  // 1) 铸造测试 NFT
  const mintTx = await nft.safeMint(acc1.address, "ipfs://QmeHash");
  await mintTx.wait();
  const tokenId = 0;

  // 2) 授权 Marketplace 代管 NFT
  await nft.approve(market.address, tokenId);

  // 3) 上架
  const listTx = await market.createListing(nft.address, tokenId, ethers.utils.parseEther("0.02"));
  await listTx.wait();

  // 4) 另一个账户购买
  const buyTx = await market.connect(acc2).buyListing(1, { value: ethers.utils.parseEther("0.02") });
  await buyTx.wait();

  console.log("✅ 完整交易流程完成");
}

main();

运行:

npx hardhat run scripts/interact.js --network mumbai

7. 常见问题 & 下一步扩展

❓ 合约验证失败怎么办?

确保已安装 @nomiclabs/hardhat-etherscan,并正确设置 POLYGONSCAN_API_KEY
通过命令验证:

npx hardhat verify --network mumbai YOUR_CONTRACT_ADDRESS

❓ 上传到主网需要改什么?

  1. RPC 换成 Polygon 主网节点
  2. accounts[0] 填主网私钥(严禁泄露!)
  3. gasPrice / gas 使用自动估算即可

❓ 如何添加手续费?

在 Marketplace 合约中新增 uint public feePercent = 250;,buyListing 时把手续费部分转给合约 owner。

❓ 前端调用示例?


FAQ

Q1:必须要 QuickNode 节点吗?
A:任何 Mumbai RPC 均可。QuickNode 提供企业级稳定与日志追踪,也可自建节点。

Q2:ERC-1155 能直接用到本合约吗?
A:可以,但需要额外修改 transfer 逻辑以兼容 id/amount 模式。

Q3:上架后想改价怎么办?
A:可新增 updatePrice(listingId, newPrice) 函数,仅当 seller == msg.sender 时执行。

Q4:如何限制只有白名单 NFT Collections 可上架?
A:在 createListing 前检查 nftContract 是否在白名单映射内,若无则 revert。

Q5:是否可以一次上架多张 NFT?
A:批量版合约可结合数组参数循环执行 safeTransferFrom,同时记录多维 listingId。

Q6:NFT 被盗怎么办?
A:所有转移均需要用户手动授权(approve),合约并无法任意操作。如果钱包私钥泄露,请立即转移资产到新生成地址。


把今天的代码拿去做实验,你在 24 小时内就能看到自己的 Polygon NFT 交易市场 运行在生产环境!当你准备接入主网或直接上线 DApp 时,再来回看本文第 6 步的脚本,替换网络与参数即可。祝开发顺利!