关键词:NFT 交易平台、非同质化代币、Polygon Mumbai 测试网、Hardhat 开发、Ethers.js、智能合约部署、ERC-721、区块链应用
一条完整可运行的 NFT 市场交易链,可以由「合约代码 + 开发工具 + 实战脚本」三步完成。本篇将带你用 30 分钟在 Polygon 测试网(Mumbai)搭好最小可用版的 非同质化代币交易平台。你只需要一台电脑、Node.js 与一条稳定 RPC 节点即可开始。
本文路线图
- Polygon NFT 交易场景全景速览
- 环境初始化:Hardhat + Ethers.js + 钱包
- 撰写 NFT 与 Marketplace 合约
- 本地单元测试
- 部署到 Mumbai 测试网
- 用脚本验证「挂卖—购买—所有权转移」
- 常见问题与下一步扩展
1. Polygon NFT 交易场景全景速览
非同质化代币 (NFT) 正在成为数字资产的新货币形态。游戏道具、头像、门票、证书…任何需要“独特性”的场景都能用 NFT 在 区块链 上呈现。
Polygon 提供了 低 gas、高 TPS、EVM 兼容 的侧链环境,让独立开发者也能在几分钟内跑通整套 NFT 交易平台。
2. 环境初始化
所需软件与账户
- Node.js ≥18,npm 包管理器
- 任意代码编辑器(VS Code 最佳)
- MetaMask 钱包,并准备 两个账户 的私钥
- Mumbai MATIC (测试币,可通过公共水龙领取)
安装 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 test5. 部署到 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 mumbai6. 用脚本验证「挂卖—购买—所有权转移」
创建 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 mumbai7. 常见问题 & 下一步扩展
❓ 合约验证失败怎么办?
确保已安装 @nomiclabs/hardhat-etherscan,并正确设置 POLYGONSCAN_API_KEY。
通过命令验证:
npx hardhat verify --network mumbai YOUR_CONTRACT_ADDRESS❓ 上传到主网需要改什么?
- RPC 换成 Polygon 主网节点
- accounts[0] 填主网私钥(严禁泄露!)
- gasPrice / gas 使用自动估算即可
❓ 如何添加手续费?
在 Marketplace 合约中新增 uint public feePercent = 250;,buyListing 时把手续费部分转给合约 owner。
❓ 前端调用示例?
- 祝你阅读顺利 👉 领取免费 SDK 开始 UI 集成
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 步的脚本,替换网络与参数即可。祝开发顺利!