自2021年的繁荣以来,NFT已成为web3生态系统的核心组成部分,其应用已超越收藏品范畴,延伸至会员权限、资产代币化、游戏道具和数字身份等现实场景。
Phosphor等无代码平台通过直观的Creator Studio和API套件,让创作者和开发者比以往更轻松地铸造、自定义和部署NFT,无需从零开始构建自定义NFT合约。然而,对于希望深入理解Solidity和web3原生体验的开发者而言,学习如何构建NFT市场仍是一项基础技能。
本指南将带你逐步完成在Linea上开发和部署一个基础但功能完整的NFT市场的全过程。学完本指南后,你将掌握启动自己的市场所需的知识,并对智能合约开发、去中心化应用程序和web3交互有更深入的理解。
前提条件
你应具备Solidity的基础知识。
你的电脑上应已安装Nodejs和Foundry。
合约概览
本NFT市场将支持以下功能:
上架NFT出售:用户可设定价格并上架其NFT,相关信息安全存储在智能合约中。
购买NFT:买家可通过支付所需的Ether购买已上架的NFT,确保交易顺畅无阻。
批量购买NFT:可在单笔交易中购买多个NFT,以降低gas费用。
下架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-commitNFT市场智能合约完整代码
在src文件夹中创建market.sol文件,并将以下代码添加到该文件中。
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;
}
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");
_;
}
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);
}
function buyNFT(address nftContract, uint256 tokenId)
public
payable
isListed(nftContract, tokenId)
{
Listing memory listing = listings[nftContract][tokenId];
require(msg.value >= listing.price, "Insufficient funds");
delete listings[nftContract][tokenId];
(bool sent, ) = listing.seller.call{value: listing.price}("");
require(sent, "Failed to send Ether");
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]);
}
}
function delistNFT(address nftContract, uint256 tokenId)
external
isListed(nftContract, tokenId)
isSeller(nftContract, tokenId, msg.sender)
{
Listing memory listing = listings[nftContract][tokenId];
delete listings[nftContract][tokenId];
IERC721(nftContract).transferFrom(address(this), listing.seller, tokenId);
emit Delisted(nftContract, tokenId, listing.seller);
}
}
代码核心功能
Listing结构体
这对于在去中心化NFT市场中实现上架、购买和下架等功能至关重要。它提供了一种清晰高效的方式来管理挂单,同时确保市场记录每个商品的卖家和价格——这两者对市场运营都至关重要。
struct Listing {
address seller;
uint256 price;
}
Listing映射
这行代码在Solidity中定义了一个嵌套映射,用于组织和存储NFT市场的挂单信息。外层映射以NFT合约地址为键,使智能合约能够按各自的NFT集合对挂单进行分类,从而确保市场可以支持多个NFT集合。
内层映射以代币ID(每个NFT的唯一标识符)为键,实现对单个NFT合约下多个挂单的高效管理。
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");
delete listings[nftContract][tokenId];
(bool sent, ) = listing.seller.call{value: listing.price}("");
require(sent, "Failed to send Ether");
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];
delete listings[nftContract][tokenId];
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文件,并将以下代码添加到该文件中。
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;
}
function mint(address to) external onlyOwner {
uint256 tokenId = _tokenIdCounter;
_tokenIdCounter += 1;
_safeMint(to, tokenId);
emit Minted(to, tokenId);
}
function _baseURI() internal view override returns (string memory) {
return _baseTokenURI;
}
function setBaseTokenURI(string memory newBaseTokenURI) external onlyOwner {
_baseTokenURI = newBaseTokenURI;
}
function getTokenCount() external view returns (uint256) {
return _tokenIdCounter;
}
}
在test文件夹中创建market.t.sol文件,并将以下代码添加到该文件中。
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/
PRIVATE_KEY=*************************************************
将以下内容添加至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上关注我们,获取最新资讯。