随着区块链技术的快速发展,Web3前端开发已经成为前端开发者必须掌握的新技能。本指南将帮助你从传统Web开发平滑过渡到Web3前端开发,构建真正的去中心化应用(DApp)。
2 篇博文 含有标签「web3」
查看所有标签NFT市场开发指南:从智能合约到前端完整实现
NFT(非同质化代币)作为区块链技术的重要应用,已经在艺术、游戏、收藏品等领域展现出巨大潜力。本文将带你从零开始构建一个功能完整的NFT市场。
NFT基础概念
什么是NFT?
NFT(Non-Fungible Token)是非同质化代币,每个代币都是独一无二的,不可互换。这与比特币等同质化代币形成对比。
ERC721标准
ERC721是以太坊上NFT的标准协议,定义了NFT的基本接口:
interface IERC721 {
function balanceOf(address owner) external view returns (uint256 balance);
function ownerOf(uint256 tokenId) external view returns (address owner);
function safeTransferFrom(address from, address to, uint256 tokenId) external;
function transferFrom(address from, address to, uint256 tokenId) external;
function approve(address to, uint256 tokenId) external;
function getApproved(uint256 tokenId) external view returns (address operator);
function setApprovalForAll(address operator, bool approved) external;
function isApprovedForAll(address owner, address operator) external view returns (bool);
}
构建NFT合约
1. 基础NFT合约
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
contract MyNFT is ERC721, ERC721Enumerable, ERC721URIStorage, Ownable {
using Counters for Counters.Counter;
Counters.Counter private _tokenIds;
uint256 public maxSupply = 10000;
uint256 public mintPrice = 0.01 ether;
event Minted(address indexed to, uint256 indexed tokenId, string tokenURI);
constructor(string memory name, string memory symbol)
ERC721(name, symbol)
Ownable(msg.sender)
{}
function mintNFT(string memory tokenURI) public payable returns (uint256) {
require(_tokenIds.current() < maxSupply, "Max supply reached");
require(msg.value >= mintPrice, "Insufficient payment");
_tokenIds.increment();
uint256 newTokenId = _tokenIds.current();
_safeMint(msg.sender, newTokenId);
_setTokenURI(newTokenId, tokenURI);
emit Minted(msg.sender, newTokenId, tokenURI);
return newTokenId;
}
function totalMinted() public view returns (uint256) {
return _tokenIds.current();
}
function withdraw() public onlyOwner {
uint256 balance = address(this).balance;
require(balance > 0, "No funds to withdraw");
payable(owner()).transfer(balance);
}
// Override required functions
function _beforeTokenTransfer(address from, address to, uint256 tokenId, uint256 batchSize)
internal
override(ERC721, ERC721Enumerable)
{
super._beforeTokenTransfer(from, to, tokenId, batchSize);
}
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);
}
function supportsInterface(bytes4 interfaceId)
public
view
override(ERC721, ERC721Enumerable, ERC721URIStorage)
returns (bool)
{
return super.supportsInterface(interfaceId);
}
}
2. 带有版税的NFT合约
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Royalty.sol";
contract RoyaltyNFT is MyNFT, ERC721Royalty {
uint96 public royaltyFee = 250; // 2.5% royalty (250 / 10000)
constructor(string memory name, string memory symbol)
MyNFT(name, symbol)
ERC721Royalty()
{
_setDefaultRoyalty(msg.sender, royaltyFee);
}
function mintNFT(string memory tokenURI) public payable returns (uint256) {
uint256 tokenId = super.mintNFT(tokenURI);
return tokenId;
}
function setRoyalty(address receiver, uint96 feeNumerator) external onlyOwner {
_setDefaultRoyalty(receiver, feeNumerator);
}
// Override required functions
function _beforeTokenTransfer(address from, address to, uint256 tokenId, uint256 batchSize)
internal
override(MyNFT, ERC721Enumerable)
{
super._beforeTokenTransfer(from, to, tokenId, batchSize);
}
function _burn(uint256 tokenId)
internal
override(MyNFT, ERC721URIStorage, ERC721Royalty)
{
super._burn(tokenId);
}
function supportsInterface(bytes4 interfaceId)
public
view
override(MyNFT, ERC721Royalty)
returns (bool)
{
return super.supportsInterface(interfaceId);
}
}
构建NFT市场合约
1. 基础市场合约
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/security/Pausable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract NFTMarketplace is ReentrancyGuard, Pausable, Ownable, IERC721Receiver {
struct Listing {
address seller;
address nftContract;
uint256 tokenId;
uint256 price;
bool isActive;
}
struct Auction {
address seller;
address nftContract;
uint256 tokenId;
uint256 startingPrice;
uint256 highestBid;
address highestBidder;
uint256 endTime;
bool isActive;
}
mapping(uint256 => Listing) public listings;
mapping(uint256 => Auction) public auctions;
mapping(address => uint256) public pendingWithdrawals;
uint256 public listingFee = 0.001 ether;
uint256 public auctionFee = 0.002 ether;
uint256 public marketplaceFee = 250; // 2.5% (250/10000)
uint256 public nextListingId;
uint256 public nextAuctionId;
event NFTListed(uint256 indexed listingId, address indexed seller, address indexed nftContract, uint256 tokenId, uint256 price);
event NFTPurchased(uint256 indexed listingId, address indexed buyer, uint256 price);
event NFTDelisted(uint256 indexed listingId);
event AuctionCreated(uint256 indexed auctionId, address indexed seller, address indexed nftContract, uint256 tokenId, uint256 startingPrice, uint256 endTime);
event BidPlaced(uint256 indexed auctionId, address indexed bidder, uint256 amount);
event AuctionEnded(uint256 indexed auctionId, address indexed winner, uint256 amount);
constructor() Ownable(msg.sender) {}
// List NFT for sale
function listNFT(address _nftContract, uint256 _tokenId, uint256 _price) external payable nonReentrant whenNotPaused {
require(msg.value >= listingFee, "Insufficient listing fee");
require(_price > 0, "Price must be greater than 0");
IERC721 nft = IERC721(_nftContract);
require(nft.ownerOf(_tokenId) == msg.sender, "Not the owner");
require(nft.getApproved(_tokenId) == address(this), "Marketplace not approved");
uint256 listingId = nextListingId++;
listings[listingId] = Listing({
seller: msg.sender,
nftContract: _nftContract,
tokenId: _tokenId,
price: _price,
isActive: true
});
nft.safeTransferFrom(msg.sender, address(this), _tokenId);
emit NFTListed(listingId, msg.sender, _nftContract, _tokenId, _price);
}
// Purchase listed NFT
function purchaseNFT(uint256 _listingId) external payable nonReentrant {
Listing storage listing = listings[_listingId];
require(listing.isActive, "Listing not active");
require(msg.value >= listing.price, "Insufficient payment");
listing.isActive = false;
uint256 fee = (listing.price * marketplaceFee) / 10000;
uint256 sellerProceeds = listing.price - fee;
pendingWithdrawals[listing.seller] += sellerProceeds;
pendingWithdrawals[owner()] += fee;
if (msg.value > listing.price) {
pendingWithdrawals[msg.sender] += msg.value - listing.price;
}
IERC721(listing.nftContract).safeTransferFrom(address(this), msg.sender, listing.tokenId);
emit NFTPurchased(_listingId, msg.sender, listing.price);
}
// Create auction
function createAuction(address _nftContract, uint256 _tokenId, uint256 _startingPrice, uint256 _duration) external payable nonReentrant whenNotPaused {
require(msg.value >= auctionFee, "Insufficient auction fee");
require(_startingPrice > 0, "Starting price must be greater than 0");
require(_duration >= 3600 && _duration <= 604800, "Duration must be between 1 hour and 7 days");
IERC721 nft = IERC721(_nftContract);
require(nft.ownerOf(_tokenId) == msg.sender, "Not the owner");
require(nft.getApproved(_tokenId) == address(this), "Marketplace not approved");
uint256 auctionId = nextAuctionId++;
auctions[auctionId] = Auction({
seller: msg.sender,
nftContract: _nftContract,
tokenId: _tokenId,
startingPrice: _startingPrice,
highestBid: _startingPrice,
highestBidder: address(0),
endTime: block.timestamp + _duration,
isActive: true
});
nft.safeTransferFrom(msg.sender, address(this), _tokenId);
emit AuctionCreated(auctionId, msg.sender, _nftContract, _tokenId, _startingPrice, block.timestamp + _duration);
}
// Place bid
function placeBid(uint256 _auctionId) external payable nonReentrant {
Auction storage auction = auctions[_auctionId];
require(auction.isActive, "Auction not active");
require(block.timestamp < auction.endTime, "Auction ended");
require(msg.value > auction.highestBid, "Bid too low");
if (auction.highestBidder != address(0)) {
pendingWithdrawals[auction.highestBidder] += auction.highestBid;
}
auction.highestBid = msg.value;
auction.highestBidder = msg.sender;
emit BidPlaced(_auctionId, msg.sender, msg.value);
}
// End auction
function endAuction(uint256 _auctionId) external nonReentrant {
Auction storage auction = auctions[_auctionId];
require(auction.isActive, "Auction not active");
require(block.timestamp >= auction.endTime, "Auction not ended");
auction.isActive = false;
if (auction.highestBidder != address(0)) {
uint256 fee = (auction.highestBid * marketplaceFee) / 10000;
uint256 sellerProceeds = auction.highestBid - fee;
pendingWithdrawals[auction.seller] += sellerProceeds;
pendingWithdrawals[owner()] += fee;
IERC721(auction.nftContract).safeTransferFrom(address(this), auction.highestBidder, auction.tokenId);
emit AuctionEnded(_auctionId, auction.highestBidder, auction.highestBid);
} else {
IERC721(auction.nftContract).safeTransferFrom(address(this), auction.seller, auction.tokenId);
}
}
// Withdraw funds
function withdraw() external nonReentrant {
uint256 amount = pendingWithdrawals[msg.sender];
require(amount > 0, "No funds to withdraw");
pendingWithdrawals[msg.sender] = 0;
payable(msg.sender).transfer(amount);
}
// Admin functions
function setListingFee(uint256 _fee) external onlyOwner {
listingFee = _fee;
}
function setAuctionFee(uint256 _fee) external onlyOwner {
auctionFee = _fee;
}
function setMarketplaceFee(uint256 _fee) external onlyOwner {
require(_fee <= 1000, "Fee too high"); // Max 10%
marketplaceFee = _fee;
}
function pause() external onlyOwner {
_pause();
}
function unpause() external onlyOwner {
_unpause();
}
// Emergency functions
function emergencyWithdraw() external onlyOwner {
payable(owner()).transfer(address(this).balance);
}
// IERC721Receiver implementation
function onERC721Received(address, address, uint256, bytes calldata) external pure override returns (bytes4) {
return this.onERC721Received.selector;
}
receive() external payable {}
}
2. 批量交易功能
contract NFTMarketplaceBatch is NFTMarketplace {
struct BatchListing {
address nftContract;
uint256[] tokenIds;
uint256[] prices;
}
event BatchNFTListed(uint256 indexed batchId, address indexed seller, uint256 totalItems);
event BatchNFTPurchased(uint256 indexed batchId, address indexed buyer, uint256 totalPrice);
function listBatch(BatchListing calldata batchListing) external payable nonReentrant whenNotPaused {
require(batchListing.tokenIds.length == batchListing.prices.length, "Arrays length mismatch");
require(batchListing.tokenIds.length > 0, "Empty batch");
require(msg.value >= listingFee * batchListing.tokenIds.length, "Insufficient listing fee");
IERC721 nft = IERC721(batchListing.nftContract);
for (uint i = 0; i < batchListing.tokenIds.length; i++) {
require(batchListing.prices[i] > 0, "Price must be greater than 0");
require(nft.ownerOf(batchListing.tokenIds[i]) == msg.sender, "Not the owner");
uint256 listingId = nextListingId++;
listings[listingId] = Listing({
seller: msg.sender,
nftContract: batchListing.nftContract,
tokenId: batchListing.tokenIds[i],
price: batchListing.prices[i],
isActive: true
});
nft.safeTransferFrom(msg.sender, address(this), batchListing.tokenIds[i]);
}
emit BatchNFTListed(nextListingId - 1, msg.sender, batchListing.tokenIds.length);
}
function purchaseBatch(uint256[] calldata listingIds) external payable nonReentrant {
uint256 totalPrice = 0;
for (uint i = 0; i < listingIds.length; i++) {
Listing storage listing = listings[listingIds[i]];
require(listing.isActive, "Listing not active");
totalPrice += listing.price;
}
require(msg.value >= totalPrice, "Insufficient payment");
for (uint i = 0; i < listingIds.length; i++) {
Listing storage listing = listings[listingIds[i]];
listing.isActive = false;
uint256 fee = (listing.price * marketplaceFee) / 10000;
uint256 sellerProceeds = listing.price - fee;
pendingWithdrawals[listing.seller] += sellerProceeds;
pendingWithdrawals[owner()] += fee;
IERC721(listing.nftContract).safeTransferFrom(address(this), msg.sender, listing.tokenId);
}
if (msg.value > totalPrice) {
pendingWithdrawals[msg.sender] += msg.value - totalPrice;
}
emit BatchNFTPurchased(listingIds[0], msg.sender, totalPrice);
}
}
前端集成
1. 连接钱包
import { ethers } from 'ethers';
import NFTMarketplace from './artifacts/contracts/NFTMarketplace.sol/NFTMarketplace.json';
const MARKETPLACE_ADDRESS = "0x...";
export async function connectWallet() {
if (typeof window.ethereum !== 'undefined') {
try {
await window.ethereum.request({ method: 'eth_requestAccounts' });
const provider = new ethers.providers.Web3Provider(window.ethereum);
const signer = provider.getSigner();
const address = await signer.getAddress();
const marketplace = new ethers.Contract(
MARKETPLACE_ADDRESS,
NFTMarketplace.abi,
signer
);
return { provider, signer, address, marketplace };
} catch (error) {
console.error('Failed to connect wallet:', error);
throw error;
}
} else {
throw new Error('MetaMask not installed');
}
}
2. 列出NFT
export async function listNFT(marketplace, nftContract, tokenId, price) {
try {
const tx = await marketplace.listNFT(
nftContract,
tokenId,
ethers.utils.parseEther(price.toString()),
{ value: ethers.utils.parseEther("0.001") } // listing fee
);
await tx.wait();
console.log('NFT listed successfully');
return tx.hash;
} catch (error) {
console.error('Failed to list NFT:', error);
throw error;
}
}
3. 购买NFT
export async function purchaseNFT(marketplace, listingId, price) {
try {
const tx = await marketplace.purchaseNFT(listingId, {
value: ethers.utils.parseEther(price.toString())
});
await tx.wait();
console.log('NFT purchased successfully');
return tx.hash;
} catch (error) {
console.error('Failed to purchase NFT:', error);
throw error;
}
}
部署和测试
1. Hardhat配置
// hardhat.config.js
require("@nomiclabs/hardhat-waffle");
require("@nomiclabs/hardhat-etherscan");
require("dotenv").config();
module.exports = {
solidity: "0.8.19",
networks: {
goerli: {
url: `https://goerli.infura.io/v3/${process.env.INFURA_PROJECT_ID}`,
accounts: [process.env.PRIVATE_KEY]
},
mainnet: {
url: `https://mainnet.infura.io/v3/${process.env.INFURA_PROJECT_ID}`,
accounts: [process.env.PRIVATE_KEY]
}
},
etherscan: {
apiKey: process.env.ETHERSCAN_API_KEY
}
};
2. 部署脚本
// scripts/deploy.js
const hre = require("hardhat");
async function main() {
const NFTMarketplace = await hre.ethers.getContractFactory("NFTMarketplace");
const marketplace = await NFTMarketplace.deploy();
await marketplace.deployed();
console.log("NFTMarketplace deployed to:", marketplace.address);
// 验证合约
if (network.name !== "localhost" && network.name !== "hardhat") {
await hre.run("verify:verify", {
address: marketplace.address,
constructorArguments: [],
});
}
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
总结
通过构建这个NFT市场,我们学习了:
- ERC721标准: NFT的基本实现
- 市场合约: 买卖NFT的核心逻辑
- 拍卖功能: 竞价机制的实现
- 版税机制: 创作者收益的保障
- 批量交易: 提高用户体验
- 前端集成: Web3应用的完整流程
下一步
- 优化gas费用: 使用代理模式降低部署成本
- 添加更多功能: 报价、分期付款等
- 多链支持: 支持多个区块链网络
- IPFS集成: 去中心化存储NFT元数据
- 移动应用: 开发移动端DApp
继续探索NFT的无限可能!