NFT市场集成
NFT市场是连接创作者和收藏家的桥梁,是NFT生态系统中不可或缺的组成部分。本章将详细介绍如何将NFT集成到现有的市场平台或构建自己的NFT市场,包括市场API的使用、NFT展示、交易集成、拍卖功能以及版税设置等核心内容。
市场API
NFT市场通常提供API接口,允许开发者集成市场功能到自己的应用中。本节将介绍主要NFT市场的API特点和使用方法。
OpenSea API
OpenSea是最大的NFT市场之一,提供了全面的API接口。
API特点
- 支持NFT查询、创建、购买、出售等核心功能
- 提供Webhook事件通知
- 支持测试网络和主网
- 提供SDK简化开发流程
基本使用示例
// 使用OpenSea API获取NFT集合信息
const axios = require('axios');
const OPENSEA_API_KEY = 'your_api_key';
const BASE_URL = 'https://api.opensea.io/api/v1';
async function getCollectionInfo(collectionSlug) {
try {
const response = await axios.get(
`${BASE_URL}/collection/${collectionSlug}`,
{
headers: {
'X-API-KEY': OPENSEA_API_KEY
}
}
);
return response.data;
} catch (error) {
console.error('Error fetching collection info:', error);
return null;
}
}
// 获取NFT列表
async function getNFTAssets(collectionSlug, limit = 20, offset = 0) {
try {
const response = await axios.get(
`${BASE_URL}/assets`,
{
params: {
collection: collectionSlug,
limit: limit,
offset: offset
},
headers: {
'X-API-KEY': OPENSEA_API_KEY
}
}
);
return response.data.assets;
} catch (error) {
console.error('Error fetching NFT assets:', error);
return [];
}
}
// 使用示例
async function main() {
const collectionInfo = await getCollectionInfo('boredapeyachtclub');
console.log('Collection Name:', collectionInfo.name);
console.log('Floor Price:', collectionInfo.collection.stats.floor_price);
const nfts = await getNFTAssets('boredapeyachtclub', 5);
nfts.forEach(nft => {
console.log(`NFT #${nft.token_id}: ${nft.name}`);
console.log(`Current Price: ${nft.current_price ? nft.current_price / 10**18 : 'Not for sale'} ETH`);
});
}
main();
创建NFT出售订单
// 使用OpenSea SDK创建出售订单
const { OpenSeaSDK, Chain } = require('opensea-js');
const { Web3Provider } = require('@ethersproject/providers');
async function createSellOrder(nftContractAddress, tokenId, priceInETH, walletPrivateKey) {
// 初始化Web3提供者
const provider = new Web3Provider(window.ethereum);
await provider.send('eth_requestAccounts', []);
const signer = provider.getSigner();
// 初始化OpenSea SDK
const seaport = new OpenSeaSDK(provider, {
chain: Chain.Mainnet,
apiKey: 'your_api_key'
});
try {
// 创建出售订单
const order = await seaport.createListing({
asset: {
tokenId: tokenId.toString(),
tokenAddress: nftContractAddress
},
startAmount: priceInETH,
// 可选:设置结束时间(如果是拍卖)
// endAmount: priceInETH * 0.9,
// expirationTime: Math.floor(Date.now() / 1000 + 86400), // 24小时后过期
listingTime: Math.floor(Date.now() / 1000)
});
console.log('Sell order created:', order);
return order;
} catch (error) {
console.error('Error creating sell order:', error);
return null;
}
}
Rarible API
Rarible是另一个流行的NFT市场,提供了开源的协议和API。
API特点
- 支持多链(以太坊、Polygon、Flow等)
- 开源协议,允许任何人构建市场前端
- 提供GraphQL API
- 支持自定义市场和聚合器
GraphQL API使用示例
// 使用Rarible GraphQL API查询NFT
const axios = require('axios');
const RARIBLE_GRAPHQL_URL = 'https://api.rarible.org/v0.1/graphql';
async function getRaribleNFTs(collection, first = 10) {
const query = `
query GetNFTs {
tokens(
first: ${first}
filter: {
collection: { eq: "${collection}" }
}
sort: { lastSale: DESC }
) {
nodes {
id
name
collection {
name
}
meta {
name
description
image {
url
}
attributes {
key
value
}
}
supply
lastSale {
price {
value
currency {
name
symbol
}
}
date
}
}
}
}
`;
try {
const response = await axios.post(RARIBLE_GRAPHQL_URL, {
query: query
});
return response.data.data.tokens.nodes;
} catch (error) {
console.error('Error fetching Rarible NFTs:', error);
return [];
}
}
// 使用示例
async function main() {
const nfts = await getRaribleNFTs('0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D'); // BAYC contract
nfts.forEach(nft => {
console.log(`NFT: ${nft.meta.name || 'Unnamed'}`);
if (nft.lastSale) {
console.log(`Last Sale: ${nft.lastSale.price.value} ${nft.lastSale.price.currency.symbol}`);
}
});
}
main();
Foundation API
Foundation是一个高质量的NFT艺术平台,专注于数字艺术。
API特点
- 专注于艺术NFT
- 提供基本的查询API
- 支持创作者验证
- 集成拍卖功能
API使用示例
// 使用Foundation API获取拍卖中的NFT
const axios = require('axios');
const FOUNDATION_API_URL = 'https://api.foundation.app/graphql';
async function getFoundationAuctions(first = 10) {
const query = `
query GetAuctions {
tokens(
first: ${first}
filter: {
isAuction: true
}
sort: {
createdAt: DESC
}
) {
nodes {
id
tokenId
name
description
imageUrl
currentBid {
amount
currency {
symbol
}
}
endTime
seller {
address
username
}
contract {
address
name
}
}
}
}
`;
try {
const response = await axios.post(FOUNDATION_API_URL, {
query: query
});
return response.data.data.tokens.nodes;
} catch (error) {
console.error('Error fetching Foundation auctions:', error);
return [];
}
}
NFT展示
在应用中有效展示NFT是提升用户体验的关键。本节将介绍NFT展示的最佳实践和实现方法。
NFT元数据获取
展示NFT前,首先需要获取其元数据。
// 使用Ethers.js获取NFT元数据
const { ethers } = require('ethers');
// NFT合约ABI(简化版)
const NFT_ABI = [
"function tokenURI(uint256 tokenId) public view returns (string memory)",
"function name() public view returns (string memory)",
"function symbol() public view returns (string memory)",
"function ownerOf(uint256 tokenId) public view returns (address)"
];
// IPFS网关
const IPFS_GATEWAY = 'https://ipfs.io/ipfs/';
// 从URI获取元数据
async function fetchMetadataFromURI(uri) {
try {
// 处理IPFS URI
if (uri.startsWith('ipfs://')) {
const cid = uri.substring(7);
const url = `${IPFS_GATEWAY}${cid}`;
const response = await fetch(url);
return await response.json();
}
// 处理HTTP URI
else if (uri.startsWith('http')) {
const response = await fetch(uri);
return await response.json();
}
// 处理Base64编码的URI
else if (uri.startsWith('data:application/json;base64,')) {
const base64Data = uri.substring(29);
const jsonData = Buffer.from(base64Data, 'base64').toString('utf8');
return JSON.parse(jsonData);
}
console.error('Unsupported URI format:', uri);
return null;
} catch (error) {
console.error('Error fetching metadata:', error);
return null;
}
}
// 获取NFT完整信息
async function getNFTInfo(provider, contractAddress, tokenId) {
const contract = new ethers.Contract(contractAddress, NFT_ABI, provider);
try {
// 获取基本信息
const [tokenURI, name, symbol, owner] = await Promise.all([
contract.tokenURI(tokenId),
contract.name(),
contract.symbol(),
contract.ownerOf(tokenId)
]);
// 获取元数据
const metadata = await fetchMetadataFromURI(tokenURI);
return {
tokenId: tokenId,
contractAddress: contractAddress,
name: name,
symbol: symbol,
owner: owner,
tokenURI: tokenURI,
metadata: metadata
};
} catch (error) {
console.error('Error getting NFT info:', error);
return null;
}
}
NFT展示组件
以下是一个React组件示例,用于展示NFT:
// React NFT展示组件
import React, { useState, useEffect } from 'react';
import { ethers } from 'ethers';
const NFTViewer = ({ contractAddress, tokenId }) => {
const [nftInfo, setNftInfo] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
// IPFS网关
const IPFS_GATEWAY = 'https://ipfs.io/ipfs/';
// 处理IPFS URL
const formatImageUrl = (url) => {
if (url && url.startsWith('ipfs://')) {
const cid = url.substring(7);
return `${IPFS_GATEWAY}${cid}`;
}
return url;
};
// 获取NFT信息
useEffect(() => {
const fetchNFTInfo = async () => {
if (!contractAddress || !tokenId) return;
setLoading(true);
setError(null);
try {
// 连接到以太坊
const provider = new ethers.providers.Web3Provider(window.ethereum);
// NFT合约ABI
const abi = [
"function tokenURI(uint256 tokenId) public view returns (string memory)",
"function name() public view returns (string memory)",
"function symbol() public view returns (string memory)",
"function ownerOf(uint256 tokenId) public view returns (address)"
];
// 创建合约实例
const contract = new ethers.Contract(contractAddress, abi, provider);
// 获取基本信息
const [tokenURI, name, symbol, owner] = await Promise.all([
contract.tokenURI(tokenId),
contract.name(),
contract.symbol(),
contract.ownerOf(tokenId)
]);
// 获取元数据
let metadata = null;
if (tokenURI.startsWith('ipfs://')) {
const cid = tokenURI.substring(7);
const response = await fetch(`${IPFS_GATEWAY}${cid}`);
metadata = await response.json();
} else if (tokenURI.startsWith('http')) {
const response = await fetch(tokenURI);
metadata = await response.json();
}
setNftInfo({
tokenId,
contractAddress,
name,
symbol,
owner,
tokenURI,
metadata
});
} catch (err) {
console.error('Error fetching NFT info:', err);
setError('Failed to load NFT information');
} finally {
setLoading(false);
}
};
fetchNFTInfo();
}, [contractAddress, tokenId]);
if (loading) {
return <div className="nft-viewer loading">Loading NFT...</div>;
}
if (error || !nftInfo) {
return <div className="nft-viewer error">{error || 'NFT not found'}</div>;
}
const { metadata } = nftInfo;
const imageUrl = formatImageUrl(metadata?.image || metadata?.animation_url);
return (
<div className="nft-viewer">
<div className="nft-image-container">
{imageUrl && (
<img
src={imageUrl}
alt={metadata?.name || `NFT #${nftInfo.tokenId}`}
className="nft-image"
/>
)}
</div>
<div className="nft-info">
<h2 className="nft-name">{metadata?.name || `NFT #${nftInfo.tokenId}`}</h2>
<p className="nft-collection">{nftInfo.name} ({nftInfo.symbol})</p>
<p className="nft-description">{metadata?.description}</p>
{metadata?.attributes && metadata.attributes.length > 0 && (
<div className="nft-attributes">
<h3>Attributes</h3>
<ul>
{metadata.attributes.map((attr, index) => (
<li key={index}>
<span className="trait-type">{attr.trait_type}:</span>
<span className="trait-value">{attr.value}</span>
</li>
))}
</ul>
</div>
)}
<div className="nft-owner">
<span>Owner: </span>
<span className="owner-address">{nftInfo.owner}</span>
</div>
</div>
</div>
);
};
export default NFTViewer;
批量NFT展示
对于集合页面或市场列表,需要高效地展示多个NFT:
// React批量NFT展示组件
import React, { useState, useEffect } from 'react';
import { ethers } from 'ethers';
import NFTViewer from './NFTViewer';
const NFTCollectionGrid = ({ contractAddress, count = 20, startIndex = 0 }) => {
const [nfts, setNfts] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchNFTs = async () => {
if (!contractAddress) return;
setLoading(true);
setError(null);
try {
// 连接到以太坊
const provider = new ethers.providers.Web3Provider(window.ethereum);
// ERC721Enumerable ABI(用于获取集合中的NFT)
const abi = [
"function tokenByIndex(uint256 index) external view returns (uint256)",
"function tokenOfOwnerByIndex(address owner, uint256 index) external view returns (uint256)",
"function totalSupply() external view returns (uint256)",
"function tokenURI(uint256 tokenId) public view returns (string memory)",
"function name() public view returns (string memory)",
"function symbol() public view returns (string memory)"
];
// 创建合约实例
const contract = new ethers.Contract(contractAddress, abi, provider);
// 获取NFT ID列表
const nftIds = [];
const totalSupply = await contract.totalSupply();
const endIndex = Math.min(startIndex + count, totalSupply.toNumber());
for (let i = startIndex; i < endIndex; i++) {
const tokenId = await contract.tokenByIndex(i);
nftIds.push(tokenId.toNumber());
}
// 获取每个NFT的基本信息
const nftPromises = nftIds.map(async (tokenId) => {
try {
const tokenURI = await contract.tokenURI(tokenId);
// 注意:在实际应用中,可能需要获取元数据
// 但为了性能,这里我们只获取基本信息
return {
tokenId,
contractAddress,
tokenURI
};
} catch (err) {
console.warn(`Failed to fetch NFT #${tokenId}:`, err);
return { tokenId, contractAddress, error: err.message };
}
});
const nftResults = await Promise.all(nftPromises);
setNfts(nftResults);
} catch (err) {
console.error('Error fetching NFT collection:', err);
setError('Failed to load NFT collection');
} finally {
setLoading(false);
}
};
fetchNFTs();
}, [contractAddress, count, startIndex]);
if (loading) {
return <div className="nft-collection-grid loading">Loading NFTs...</div>;
}
if (error) {
return <div className="nft-collection-grid error">{error}</div>;
}
return (
<div className="nft-collection-grid">
{nfts.map((nft) => (
<div key={`${nft.contractAddress}-${nft.tokenId}`} className="nft-card">
<NFTViewer
contractAddress={nft.contractAddress}
tokenId={nft.tokenId}
/>
</div>
))}
</div>
);
};
export default NFTCollectionGrid;
交易集成
交易集成是NFT市场的核心功能,本节将介绍如何实现NFT的购买、出售和转移等交易功能。
NFT购买功能
// 使用Ethers.js实现NFT购买
const { ethers } = require('ethers');
// NFT合约ABI(简化版)
const NFT_ABI = [
"function safeTransferFrom(address from, address to, uint256 tokenId) external payable",
"function priceOf(uint256 tokenId) external view returns (uint256)",
"function isApprovedForAll(address owner, address operator) external view returns (bool)"
];
// 检查并设置批准
async function ensureApproval(signer, nftContractAddress, marketplaceAddress) {
const nftContract = new ethers.Contract(nftContractAddress, NFT_ABI, signer);
const isApproved = await nftContract.isApprovedForAll(await signer.getAddress(), marketplaceAddress);
if (!isApproved) {
console.log('Setting approval for marketplace...');
const tx = await nftContract.setApprovalForAll(marketplaceAddress, true);
await tx.wait();
console.log('Approval set:', tx.hash);
}
return isApproved;
}
// 购买NFT
async function buyNFT(signer, nftContractAddress, marketplaceContractAddress, tokenId) {
try {
// 创建NFT合约实例
const nftContract = new ethers.Contract(nftContractAddress, NFT_ABI, signer);
// 获取NFT价格
const price = await nftContract.priceOf(tokenId);
console.log(`NFT #${tokenId} price: ${ethers.utils.formatEther(price)} ETH`);
// 确保市场合约已获批准
await ensureApproval(signer, nftContractAddress, marketplaceContractAddress);
// 创建市场合约实例(假设使用标准接口)
const marketplaceAbi = [
"function buyNFT(address nftContract, uint256 tokenId) external payable"
];
const marketplaceContract = new ethers.Contract(marketplaceContractAddress, marketplaceAbi, signer);
// 执行购买交易
console.log('Executing purchase transaction...');
const tx = await marketplaceContract.buyNFT(nftContractAddress, tokenId, {
value: price
});
// 等待交易确认
const receipt = await tx.wait();
console.log('NFT purchased successfully!', receipt.transactionHash);
return {
success: true,
transactionHash: receipt.transactionHash,
tokenId: tokenId,
price: price
};
} catch (error) {
console.error('Error buying NFT:', error);
return {
success: false,
error: error.message
};
}
}
NFT出售功能
// 上架NFT出售
async function listNFTForSale(signer, nftContractAddress, marketplaceContractAddress, tokenId, priceInWei) {
try {
// 创建市场合约实例
const marketplaceAbi = [
"function listNFT(address nftContract, uint256 tokenId, uint256 price) external"
];
const marketplaceContract = new ethers.Contract(marketplaceContractAddress, marketplaceAbi, signer);
// 确保市场合约已获批准
await ensureApproval(signer, nftContractAddress, marketplaceContractAddress);
// 执行上架交易
console.log(`Listing NFT #${tokenId} for ${ethers.utils.formatEther(priceInWei)} ETH...`);
const tx = await marketplaceContract.listNFT(nftContractAddress, tokenId, priceInWei);
// 等待交易确认
const receipt = await tx.wait();
console.log('NFT listed successfully!', receipt.transactionHash);
return {
success: true,
transactionHash: receipt.transactionHash,
tokenId: tokenId,
price: priceInWei
};
} catch (error) {
console.error('Error listing NFT:', error);
return {
success: false,
error: error.message
};
}
}
// 取消NFT出售
async function cancelNFTListing(signer, marketplaceContractAddress, listingId) {
try {
// 创建市场合约实例
const marketplaceAbi = [
"function cancelListing(uint256 listingId) external"
];
const marketplaceContract = new ethers.Contract(marketplaceContractAddress, marketplaceAbi, signer);
// 执行取消上架交易
console.log(`Cancelling listing #${listingId}...`);
const tx = await marketplaceContract.cancelListing(listingId);
// 等待交易确认
const receipt = await tx.wait();
console.log('NFT listing cancelled successfully!', receipt.transactionHash);
return {
success: true,
transactionHash: receipt.transactionHash,
listingId: listingId
};
} catch (error) {
console.error('Error cancelling NFT listing:', error);
return {
success: false,
error: error.message
};
}
}
交易历史查询
// 查询NFT交易历史
const { ethers } = require('ethers');
// 获取NFT交易历史
async function getNFTTransactionHistory(provider, nftContractAddress, tokenId) {
try {
// 获取合约创建后的所有Transfer事件
const contract = new ethers.Contract(nftContractAddress, [
"event Transfer(address indexed from, address indexed to, uint256 indexed tokenId)"
], provider);
// 查询特定tokenId的Transfer事件
const filter = contract.filters.Transfer(null, null, tokenId);
const events = await contract.queryFilter(filter);
// 格式化交易历史
const transactionHistory = events.map(event => ({
blockNumber: event.blockNumber,
transactionHash: event.transactionHash,
from: event.args.from,
to: event.args.to,
tokenId: event.args.tokenId.toNumber(),
timestamp: (await provider.getBlock(event.blockNumber)).timestamp
}));
// 按时间排序(最新的在前)
transactionHistory.sort((a, b) => b.timestamp - a.timestamp);
return transactionHistory;
} catch (error) {
console.error('Error fetching transaction history:', error);
return [];
}
}
// 使用示例
async function main() {
const provider = new ethers.providers.AlchemyProvider(
'homestead',
'your_alchemy_api_key'
);
const contractAddress = '0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D'; // BAYC contract
const tokenId = 1;
const history = await getNFTTransactionHistory(provider, contractAddress, tokenId);
console.log(`Transaction history for BAYC #${tokenId}:`);
history.forEach((tx, index) => {
const date = new Date(tx.timestamp * 1000);
console.log(`${index + 1}. From: ${tx.from} To: ${tx.to} Date: ${date.toLocaleDateString()}`);
console.log(` Tx Hash: ${tx.transactionHash}`);
});
}
main();
拍卖功能
拍卖是NFT交易的重要形式,本节将介绍如何实现NFT拍卖功能。
英式拍卖(递增拍卖)
英式拍卖是最常见的拍卖形式,价格随竞拍者出价递增。
// 英式拍卖合约实现
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract EnglishAuction is Ownable {
// 拍卖状态
enum AuctionStatus {
NOT_STARTED,
LIVE,
ENDED,
CANCELLED
}
// 拍卖信息
struct Auction {
address nftContract;
uint256 tokenId;
address seller;
uint256 startingPrice;
uint256 currentBid;
address currentBidder;
uint256 startTime;
uint256 endTime;
AuctionStatus status;
}
// 存储拍卖信息
mapping(uint256 => Auction) public auctions;
// 拍卖ID计数器
uint256 public nextAuctionId;
// 佣金比例(百分比)
uint8 public platformFeePercentage = 2;
// 事件
event AuctionCreated(uint256 indexed auctionId, address indexed seller, uint256 startingPrice);
event BidPlaced(uint256 indexed auctionId, address indexed bidder, uint256 amount);
event AuctionEnded(uint256 indexed auctionId, address indexed winner, uint256 amount);
event AuctionCancelled(uint256 indexed auctionId);
// 创建拍卖
function createAuction(
address nftContract,
uint256 tokenId,
uint256 startingPrice,
uint256 durationSeconds
) external {
// 检查NFT所有权
IERC721 nft = IERC721(nftContract);
require(nft.ownerOf(tokenId) == msg.sender, "You are not the owner of this NFT");
// 检查NFT是否已授权给合约
require(nft.isApprovedForAll(msg.sender, address(this)) ||
nft.getApproved(tokenId) == address(this),
"Contract not approved to transfer NFT");
// 创建拍卖
uint256 auctionId = nextAuctionId;
nextAuctionId++;
auctions[auctionId] = Auction({
nftContract: nftContract,
tokenId: tokenId,
seller: msg.sender,
startingPrice: startingPrice,
currentBid: 0,
currentBidder: address(0),
startTime: block.timestamp,
endTime: block.timestamp + durationSeconds,
status: AuctionStatus.LIVE
});
emit AuctionCreated(auctionId, msg.sender, startingPrice);
}
// 出价
function placeBid(uint256 auctionId) external payable {
Auction storage auction = auctions[auctionId];
// 检查拍卖状态
require(auction.status == AuctionStatus.LIVE, "Auction is not live");
require(block.timestamp < auction.endTime, "Auction has ended");
// 检查出价金额
uint256 minBid = auction.currentBid == 0 ? auction.startingPrice : auction.currentBid * 110 / 100; // 10%增量
require(msg.value >= minBid, "Bid amount too low");
// 退还之前最高出价者的资金
if (auction.currentBidder != address(0)) {
payable(auction.currentBidder).transfer(auction.currentBid);
}
// 更新最高出价
auction.currentBid = msg.value;
auction.currentBidder = msg.sender;
// 延长拍卖时间(最后10分钟内出价延长5分钟)
if (block.timestamp > auction.endTime - 10 minutes) {
auction.endTime = block.timestamp + 5 minutes;
}
emit BidPlaced(auctionId, msg.sender, msg.value);
}
// 结束拍卖
function endAuction(uint256 auctionId) external {
Auction storage auction = auctions[auctionId];
// 检查拍卖状态
require(auction.status == AuctionStatus.LIVE, "Auction is not live");
require(block.timestamp >= auction.endTime, "Auction has not ended yet");
auction.status = AuctionStatus.ENDED;
if (auction.currentBidder != address(0)) {
// 计算平台佣金
uint256 platformFee = auction.currentBid * platformFeePercentage / 100;
uint256 sellerProceeds = auction.currentBid - platformFee;
// 转移NFT给最高出价者
IERC721(auction.nftContract).safeTransferFrom(
auction.seller,
auction.currentBidder,
auction.tokenId
);
// 支付卖家收入
payable(auction.seller).transfer(sellerProceeds);
// 支付平台佣金
payable(owner()).transfer(platformFee);
emit AuctionEnded(auctionId, auction.currentBidder, auction.currentBid);
}
}
// 取消拍卖
function cancelAuction(uint256 auctionId) external {
Auction storage auction = auctions[auctionId];
// 只有卖家或合约所有者可以取消拍卖
require(msg.sender == auction.seller || msg.sender == owner(), "Only seller or owner can cancel");
require(auction.status == AuctionStatus.LIVE, "Auction is not live");
// 如果已有出价,不允许取消
require(auction.currentBidder == address(0), "Cannot cancel auction with active bids");
auction.status = AuctionStatus.CANCELLED;
emit AuctionCancelled(auctionId);
}
}
荷兰式拍卖(递减拍卖)
荷兰式拍卖价格随时间递减,第一个接受当前价格的竞拍者获得NFT。
// 荷兰式拍卖合约实现
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract DutchAuction is Ownable {
// 拍卖状态
enum AuctionStatus {
NOT_STARTED,
LIVE,
ENDED,
CANCELLED
}
// 拍卖信息
struct Auction {
address nftContract;
uint256 tokenId;
address seller;
uint256 startPrice;
uint256 endPrice;
uint256 startTime;
uint256 endTime;
uint256 priceDecrementPerSecond;
AuctionStatus status;
}
// 存储拍卖信息
mapping(uint256 => Auction) public auctions;
// 拍卖ID计数器
uint256 public nextAuctionId;
// 佣金比例(百分比)
uint8 public platformFeePercentage = 2;
// 事件
event AuctionCreated(uint256 indexed auctionId, address indexed seller, uint256 startPrice, uint256 endPrice);
event AuctionEnded(uint256 indexed auctionId, address indexed winner, uint256 amount);
event AuctionCancelled(uint256 indexed auctionId);
// 创建拍卖
function createAuction(
address nftContract,
uint256 tokenId,
uint256 startPrice,
uint256 endPrice,
uint256 durationSeconds
) external {
// 检查NFT所有权
IERC721 nft = IERC721(nftContract);
require(nft.ownerOf(tokenId) == msg.sender, "You are not the owner of this NFT");
// 检查NFT是否已授权给合约
require(nft.isApprovedForAll(msg.sender, address(this)) ||
nft.getApproved(tokenId) == address(this),
"Contract not approved to transfer NFT");
// 检查价格设置
require(startPrice > endPrice, "Start price must be higher than end price");
require(durationSeconds > 0, "Duration must be positive");
// 计算每秒价格递减量
uint256 priceDecrementPerSecond = (startPrice - endPrice) / durationSeconds;
// 创建拍卖
uint256 auctionId = nextAuctionId;
nextAuctionId++;
auctions[auctionId] = Auction({
nftContract: nftContract,
tokenId: tokenId,
seller: msg.sender,
startPrice: startPrice,
endPrice: endPrice,
startTime: block.timestamp,
endTime: block.timestamp + durationSeconds,
priceDecrementPerSecond: priceDecrementPerSecond,
status: AuctionStatus.LIVE
});
emit AuctionCreated(auctionId, msg.sender, startPrice, endPrice);
}
// 获取当前价格
function getCurrentPrice(uint256 auctionId) public view returns (uint256) {
Auction storage auction = auctions[auctionId];
if (auction.status != AuctionStatus.LIVE) {
return 0;
}
if (block.timestamp >= auction.endTime) {
return auction.endPrice;
}
uint256 elapsedTime = block.timestamp - auction.startTime;
uint256 priceDecrement = elapsedTime * auction.priceDecrementPerSecond;
uint256 currentPrice = auction.startPrice - priceDecrement;
return currentPrice < auction.endPrice ? auction.endPrice : currentPrice;
}
// 接受当前价格并购买
function buyNow(uint256 auctionId) external payable {
Auction storage auction = auctions[auctionId];
// 检查拍卖状态
require(auction.status == AuctionStatus.LIVE, "Auction is not live");
// 获取当前价格
uint256 currentPrice = getCurrentPrice(auctionId);
// 检查支付金额
require(msg.value >= currentPrice, "Insufficient payment");
// 更新拍卖状态
auction.status = AuctionStatus.ENDED;
// 计算平台佣金
uint256 platformFee = currentPrice * platformFeePercentage / 100;
uint256 sellerProceeds = currentPrice - platformFee;
// 转移NFT给买家
IERC721(auction.nftContract).safeTransferFrom(
auction.seller,
msg.sender,
auction.tokenId
);
// 支付卖家收入
payable(auction.seller).transfer(sellerProceeds);
// 支付平台佣金
payable(owner()).transfer(platformFee);
// 退还超额支付
if (msg.value > currentPrice) {
payable(msg.sender).transfer(msg.value - currentPrice);
}
emit AuctionEnded(auctionId, msg.sender, currentPrice);
}
// 取消拍卖
function cancelAuction(uint256 auctionId) external {
Auction storage auction = auctions[auctionId];
// 只有卖家或合约所有者可以取消拍卖
require(msg.sender == auction.seller || msg.sender == owner(), "Only seller or owner can cancel");
require(auction.status == AuctionStatus.LIVE, "Auction is not live");
auction.status = AuctionStatus.CANCELLED;
emit AuctionCancelled(auctionId);
}
}
前端拍卖界面
以下是一个React组件示例,用于显示拍卖信息和允许用户出价:
// React拍卖界面组件
import React, { useState, useEffect } from 'react';
import { ethers } from 'ethers';
const AuctionInterface = ({ auctionContractAddress, auctionId }) => {
const [auction, setAuction] = useState(null);
const [currentPrice, setCurrentPrice] = useState(null);
const [bidAmount, setBidAmount] = useState('');
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [walletConnected, setWalletConnected] = useState(false);
const [signer, setSigner] = useState(null);
// 拍卖合约ABI(简化版)
const auctionAbi = [
"function auctions(uint256) view returns (address,uint256,address,uint256,uint256,address,uint256,uint256,uint8)",
"function placeBid(uint256) external payable",
"function getCurrentPrice(uint256) external view returns (uint256)"
];
// NFT合约ABI(简化版)
const nftAbi = [
"function tokenURI(uint256) public view returns (string memory)",
"function name() public view returns (string memory)",
"function symbol() public view returns (string memory)"
];
// IPFS网关
const IPFS_GATEWAY = 'https://ipfs.io/ipfs/';
// 连接钱包
const connectWallet = async () => {
if (window.ethereum) {
try {
await window.ethereum.request({ method: 'eth_requestAccounts' });
const provider = new ethers.providers.Web3Provider(window.ethereum);
setSigner(provider.getSigner());
setWalletConnected(true);
} catch (err) {
console.error('Error connecting wallet:', err);
setError('Failed to connect wallet');
}
} else {
setError('Please install MetaMask');
}
};
// 获取拍卖信息
useEffect(() => {
const fetchAuctionInfo = async () => {
if (!auctionContractAddress || !auctionId) return;
setLoading(true);
setError(null);
try {
// 连接到以太坊
const provider = new ethers.providers.Web3Provider(window.ethereum);
// 创建拍卖合约实例
const auctionContract = new ethers.Contract(auctionContractAddress, auctionAbi, provider);
// 获取拍卖数据
const auctionData = await auctionContract.auctions(auctionId);
const [
nftContractAddress,
tokenId,
seller,
startingPrice,
currentBid,
currentBidder,
startTime,
endTime,
status
] = auctionData;
// 格式化拍卖信息
const auctionInfo = {
nftContractAddress: nftContractAddress,
tokenId: tokenId.toNumber(),
seller: seller,
startingPrice: ethers.utils.formatEther(startingPrice),
currentBid: ethers.utils.formatEther(currentBid),
currentBidder: currentBidder,
startTime: startTime.toNumber(),
endTime: endTime.toNumber(),
status: status
};
// 获取NFT信息
const nftContract = new ethers.Contract(nftContractAddress, nftAbi, provider);
const tokenURI = await nftContract.tokenURI(tokenId);
// 获取元数据
let metadata = null;
if (tokenURI.startsWith('ipfs://')) {
const cid = tokenURI.substring(7);
const response = await fetch(`${IPFS_GATEWAY}${cid}`);
metadata = await response.json();
}
auctionInfo.metadata = metadata;
// 获取当前价格(如果是荷兰式拍卖)
try {
const price = await auctionContract.getCurrentPrice(auctionId);
auctionInfo.currentPrice = ethers.utils.formatEther(price);
setCurrentPrice(auctionInfo.currentPrice);
} catch (err) {
// 如果合约不支持getCurrentPrice方法,使用currentBid
auctionInfo.currentPrice = auctionInfo.currentBid;
setCurrentPrice(auctionInfo.currentBid);
}
setAuction(auctionInfo);
// 如果是实时价格拍卖,设置定时器更新价格
if (auctionInfo.status === 1) { // LIVE状态
const updatePriceInterval = setInterval(async () => {
try {
const price = await auctionContract.getCurrentPrice(auctionId);
const formattedPrice = ethers.utils.formatEther(price);
setCurrentPrice(formattedPrice);
} catch (err) {
console.warn('Error updating price:', err);
}
}, 5000); // 每5秒更新一次
// 清理函数
return () => clearInterval(updatePriceInterval);
}
} catch (err) {
console.error('Error fetching auction info:', err);
setError('Failed to load auction information');
} finally {
setLoading(false);
}
};
fetchAuctionInfo();
}, [auctionContractAddress, auctionId]);
// 处理出价
const handleBid = async () => {
if (!signer || !auction || !bidAmount) return;
try {
// 创建拍卖合约实例
const auctionContract = new ethers.Contract(auctionContractAddress, auctionAbi, signer);
// 转换出价金额为wei
const bidAmountWei = ethers.utils.parseEther(bidAmount);
// 执行出价交易
const tx = await auctionContract.placeBid(auctionId, {
value: bidAmountWei
});
// 显示交易状态
setLoading(true);
console.log('Placing bid...', tx.hash);
// 等待交易确认
await tx.wait();
console.log('Bid placed successfully!');
// 刷新拍卖信息
// ... 可以调用fetchAuctionInfo来刷新数据 ...
// 重置出价金额
setBidAmount('');
} catch (err) {
console.error('Error placing bid:', err);
setError('Failed to place bid');
} finally {
setLoading(false);
}
};
if (loading) {
return <div className="auction-interface loading">Loading auction...</div>;
}
if (error || !auction) {
return <div className="auction-interface error">{error || 'Auction not found'}</div>;
}
// 处理IPFS URL
const formatImageUrl = (url) => {
if (url && url.startsWith('ipfs://')) {
const cid = url.substring(7);
return `${IPFS_GATEWAY}${cid}`;
}
return url;
};
// 获取拍卖状态文本
const getStatusText = () => {
switch (auction.status) {
case 0: return 'Not Started';
case 1: return 'Live';
case 2: return 'Ended';
case 3: return 'Cancelled';
default: return 'Unknown';
}
};
// 计算剩余时间
const getTimeRemaining = () => {
if (auction.status !== 1) return null;
const now = Math.floor(Date.now() / 1000);
const remaining = auction.endTime - now;
if (remaining <= 0) {
return 'Ended';
}
const hours = Math.floor(remaining / 3600);
const minutes = Math.floor((remaining % 3600) / 60);
const seconds = remaining % 60;
return `${hours}h ${minutes}m ${seconds}s`;
};
return (
<div className="auction-interface">
<div className="auction-nft">
{auction.metadata?.image && (
<img
src={formatImageUrl(auction.metadata.image)}
alt={auction.metadata.name || `NFT #${auction.tokenId}`}
className="auction-nft-image"
/>
)}
<div className="auction-nft-info">
<h2>{auction.metadata?.name || `NFT #${auction.tokenId}`}</h2>
<p>{auction.metadata?.description}</p>
</div>
</div>
<div className="auction-details">
<div className="auction-status">
<span>Status: </span>
<span className={`status-${auction.status}`}>{getStatusText()}</span>
</div>
{auction.status === 1 && (
<div className="auction-timer">
<span>Time Remaining: </span>
<span>{getTimeRemaining()}</span>
</div>
)}
<div className="auction-price">
<h3>Current Price</h3>
<p className="price-value">{currentPrice} ETH</p>
</div>
{auction.status === 1 && (
<div className="auction-bid">
<input
type="text"
placeholder="Enter bid amount in ETH"
value={bidAmount}
onChange={(e) => setBidAmount(e.target.value)}
className="bid-input"
/>
{walletConnected ? (
<button
onClick={handleBid}
disabled={loading}
className="bid-button"
>
{loading ? 'Placing Bid...' : 'Place Bid'}
</button>
) : (
<button onClick={connectWallet} className="connect-button">
Connect Wallet to Bid
</button>
)}
</div>
)}
<div className="auction-history">
<h3>Auction History</h3>
<p>Seller: {auction.seller}</p>
{auction.currentBidder !== ethers.constants.AddressZero && (
<p>Current Bidder: {auction.currentBidder}</p>
)}
<p>Starting Price: {auction.startingPrice} ETH</p>
</div>
</div>
</div>
);
};
export default AuctionInterface;
版税设置
版税是NFT创作者重要的收入来源,本节将介绍如何为NFT设置和实现版税功能。
EIP-2981标准
EIP-2981是NFT版税的标准接口,定义了如何查询NFT销售应支付的版税。
// EIP-2981标准接口
interface IERC2981 {
// 计算销售时应支付的版税
// 参数:
// - tokenId: NFT的唯一标识符
// - salePrice: NFT的销售价格
// 返回:
// - 接收版税的地址
// - 应支付的版税金额
function royaltyInfo(uint256 tokenId, uint256 salePrice) external view returns (address receiver, uint256 royaltyAmount);
}
实现EIP-2981版税功能
以下是在NFT合约中实现EIP-2981版税功能的示例:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/introspection/ERC165.sol";
// 实现EIP-2981版税接口
contract RoyaltiesNFT is ERC721, Ownable, ERC165 {
// 版税接收地址
address private _royaltyReceiver;
// 版税比例(基础点,10000 = 100%)
uint16 private _royaltyBasisPoints;
// 默认版税比例(10%)
uint16 public constant DEFAULT_ROYALTY_BASIS_POINTS = 1000;
// 事件
event RoyaltyInfoUpdated(address indexed receiver, uint16 basisPoints);
// 构造函数
constructor(
string memory name,
string memory symbol,
address royaltyReceiver
) ERC721(name, symbol) {
_royaltyReceiver = royaltyReceiver != address(0) ? royaltyReceiver : owner();
_royaltyBasisPoints = DEFAULT_ROYALTY_BASIS_POINTS;
}
// 实现EIP-2981的royaltyInfo函数
function royaltyInfo(uint256 tokenId, uint256 salePrice)
external view
returns (address receiver, uint256 royaltyAmount)
{
require(_exists(tokenId), "ERC2981Royalties: NFT does not exist");
// 计算版税金额
royaltyAmount = salePrice * _royaltyBasisPoints / 10000;
receiver = _royaltyReceiver;
return (receiver, royaltyAmount);
}
// 更新版税信息
function setRoyaltyInfo(address receiver, uint16 basisPoints) external onlyOwner {
require(receiver != address(0), "Receiver address cannot be zero");
require(basisPoints <= 10000, "Royalty basis points cannot exceed 10000");
_royaltyReceiver = receiver;
_royaltyBasisPoints = basisPoints;
emit RoyaltyInfoUpdated(receiver, basisPoints);
}
// 支持的接口
function supportsInterface(bytes4 interfaceId)
public view virtual override(ERC721, ERC165)
returns (bool)
{
// EIP-2981接口ID
return interfaceId == type(IERC2981).interfaceId || super.supportsInterface(interfaceId);
}
// 铸造NFT
function mint(address to, uint256 tokenId) external onlyOwner {
_safeMint(to, tokenId);
}
}
为单个NFT设置不同的版税
有时需要为不同的NFT设置不同的版税比例或接收地址:
// 为单个NFT设置不同版税的实现
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/introspection/ERC165.sol";
// 版税信息结构
struct RoyaltyInfo {
address receiver;
uint16 basisPoints;
}
contract CustomRoyaltiesNFT is ERC721, Ownable, ERC165 {
// 默认版税接收地址
address private _defaultRoyaltyReceiver;
// 默认版税比例
uint16 private _defaultRoyaltyBasisPoints;
// 存储单个NFT的版税信息
mapping(uint256 => RoyaltyInfo) private _tokenRoyalties;
// 是否为特定NFT启用了自定义版税
mapping(uint256 => bool) private _hasCustomRoyalty;
// 事件
event RoyaltyInfoUpdated(address indexed receiver, uint16 basisPoints);
event TokenRoyaltyUpdated(uint256 indexed tokenId, address indexed receiver, uint16 basisPoints);
// 构造函数
constructor(
string memory name,
string memory symbol,
address defaultRoyaltyReceiver,
uint16 defaultRoyaltyBasisPoints
) ERC721(name, symbol) {
_defaultRoyaltyReceiver = defaultRoyaltyReceiver != address(0) ? defaultRoyaltyReceiver : owner();
_defaultRoyaltyBasisPoints = defaultRoyaltyBasisPoints;
}
// 实现EIP-2981的royaltyInfo函数
function royaltyInfo(uint256 tokenId, uint256 salePrice)
external view
returns (address receiver, uint256 royaltyAmount)
{
require(_exists(tokenId), "ERC2981Royalties: NFT does not exist");
// 检查是否有自定义版税
if (_hasCustomRoyalty[tokenId]) {
RoyaltyInfo memory royalty = _tokenRoyalties[tokenId];
royaltyAmount = salePrice * royalty.basisPoints / 10000;
receiver = royalty.receiver;
} else {
// 使用默认版税
royaltyAmount = salePrice * _defaultRoyaltyBasisPoints / 10000;
receiver = _defaultRoyaltyReceiver;
}
return (receiver, royaltyAmount);
}
// 更新默认版税信息
function setDefaultRoyaltyInfo(address receiver, uint16 basisPoints) external onlyOwner {
require(receiver != address(0), "Receiver address cannot be zero");
require(basisPoints <= 10000, "Royalty basis points cannot exceed 10000");
_defaultRoyaltyReceiver = receiver;
_defaultRoyaltyBasisPoints = basisPoints;
emit RoyaltyInfoUpdated(receiver, basisPoints);
}
// 为特定NFT设置版税信息
function setTokenRoyaltyInfo(
uint256 tokenId,
address receiver,
uint16 basisPoints
) external onlyOwner {
require(_exists(tokenId), "NFT does not exist");
require(receiver != address(0), "Receiver address cannot be zero");
require(basisPoints <= 10000, "Royalty basis points cannot exceed 10000");
_tokenRoyalties[tokenId] = RoyaltyInfo({
receiver: receiver,
basisPoints: basisPoints
});
_hasCustomRoyalty[tokenId] = true;
emit TokenRoyaltyUpdated(tokenId, receiver, basisPoints);
}
// 移除特定NFT的自定义版税
function removeTokenRoyalty(uint256 tokenId) external onlyOwner {
require(_exists(tokenId), "NFT does not exist");
_hasCustomRoyalty[tokenId] = false;
emit TokenRoyaltyUpdated(tokenId, address(0), 0);
}
// 支持的接口
function supportsInterface(bytes4 interfaceId)
public view virtual override(ERC721, ERC165)
returns (bool)
{
// EIP-2981接口ID
return interfaceId == type(IERC2981).interfaceId || super.supportsInterface(interfaceId);
}
// 铸造NFT
function mint(address to, uint256 tokenId) external onlyOwner {
_safeMint(to, tokenId);
}
}
市场中的版税处理
以下是市场合约中处理版税支付的示例代码:
// 市场合约中处理版税支付
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
// EIP-2981接口定义
interface IERC2981 {
function royaltyInfo(uint256 tokenId, uint256 salePrice) external view returns (address receiver, uint256 royaltyAmount);
}
contract NFTMarketplace is Ownable {
// 平台佣金比例(基础点,10000 = 100%)
uint16 public platformFeeBasisPoints = 200; // 2%
// 交易结构
struct Listing {
address seller;
address nftContract;
uint256 tokenId;
uint256 price;
bool active;
}
// 存储所有挂牌信息
mapping(uint256 => Listing) public listings;
// 挂牌ID计数器
uint256 public nextListingId;
// 事件
event ListingCreated(uint256 indexed listingId, address indexed seller, address indexed nftContract, uint256 tokenId, uint256 price);
event ListingCancelled(uint256 indexed listingId);
event NFTSold(uint256 indexed listingId, address indexed seller, address indexed buyer, uint256 price);
// 创建挂牌
function createListing(address nftContract, uint256 tokenId, uint256 price) external {
// 检查NFT所有权
IERC721 nft = IERC721(nftContract);
require(nft.ownerOf(tokenId) == msg.sender, "You are not the owner of this NFT");
// 检查NFT是否已授权给合约
require(nft.isApprovedForAll(msg.sender, address(this)) ||
nft.getApproved(tokenId) == address(this),
"Contract not approved to transfer NFT");
// 检查价格是否大于0
require(price > 0, "Price must be greater than 0");
// 创建挂牌
uint256 listingId = nextListingId;
nextListingId++;
listings[listingId] = Listing({
seller: msg.sender,
nftContract: nftContract,
tokenId: tokenId,
price: price,
active: true
});
emit ListingCreated(listingId, msg.sender, nftContract, tokenId, price);
}
// 购买NFT
function buyNFT(uint256 listingId) external payable {
Listing storage listing = listings[listingId];
// 检查挂牌是否有效
require(listing.active, "Listing is not active");
require(msg.value == listing.price, "Incorrect payment amount");
// 标记挂牌为非活动
listing.active = false;
// 检查NFT合约是否支持EIP-2981版税
address nftContract = listing.nftContract;
uint256 tokenId = listing.tokenId;
uint256 salePrice = msg.value;
// 初始化版税变量
address royaltyReceiver = address(0);
uint256 royaltyAmount = 0;
// 检查并计算版税
try IERC2981(nftContract).royaltyInfo(tokenId, salePrice) returns (address receiver, uint256 amount) {
if (receiver != address(0) && amount > 0) {
royaltyReceiver = receiver;
royaltyAmount = amount;
}
} catch {
// 如果合约不支持EIP-2981,跳过版税支付
// 可以选择记录日志或发出警告
}
// 计算平台佣金
uint256 platformFee = salePrice * platformFeeBasisPoints / 10000;
// 计算卖家收入
uint256 sellerProceeds = salePrice - platformFee - royaltyAmount;
// 转移NFT给买家
IERC721(nftContract).safeTransferFrom(listing.seller, msg.sender, tokenId);
// 支付卖家收入
payable(listing.seller).transfer(sellerProceeds);
// 支付版税
if (royaltyReceiver != address(0) && royaltyAmount > 0) {
payable(royaltyReceiver).transfer(royaltyAmount);
}
// 支付平台佣金
payable(owner()).transfer(platformFee);
emit NFTSold(listingId, listing.seller, msg.sender, salePrice);
}
// 取消挂牌
function cancelListing(uint256 listingId) external {
Listing storage listing = listings[listingId];
// 检查挂牌是否属于调用者
require(listing.seller == msg.sender, "You are not the seller");
require(listing.active, "Listing is not active");
// 标记挂牌为非活动
listing.active = false;
emit ListingCancelled(listingId);
}
// 更新平台佣金
function setPlatformFeeBasisPoints(uint16 basisPoints) external onlyOwner {
require(basisPoints <= 10000, "Platform fee cannot exceed 100%");
platformFeeBasisPoints = basisPoints;
}
}
查询NFT版税信息
以下是如何在前端查询NFT的版税信息:
// 查询NFT版税信息
const { ethers } = require('ethers');
// EIP-2981接口定义
const ERC2981_ABI = [
"function royaltyInfo(uint256 tokenId, uint256 salePrice) external view returns (address receiver, uint256 royaltyAmount)"
];
// 检查合约是否支持EIP-2981
async function supportsEIP2981(provider, contractAddress) {
try {
const contract = new ethers.Contract(
contractAddress,
["function supportsInterface(bytes4 interfaceId) external view returns (bool)"],
provider
);
// EIP-2981接口ID:0x2a55205a
const supports = await contract.supportsInterface('0x2a55205a');
return supports;
} catch (error) {
console.error('Error checking EIP-2981 support:', error);
return false;
}
}
// 获取NFT版税信息
async function getRoyaltyInfo(provider, contractAddress, tokenId, salePrice = ethers.utils.parseEther('1')) {
try {
// 检查是否支持EIP-2981
const isSupported = await supportsEIP2981(provider, contractAddress);
if (!isSupported) {
console.log('Contract does not support EIP-2981');
return null;
}
// 创建合约实例
const contract = new ethers.Contract(contractAddress, ERC2981_ABI, provider);
// 查询版税信息
const [receiver, royaltyAmount] = await contract.royaltyInfo(tokenId, salePrice);
return {
receiver: receiver,
royaltyAmount: royaltyAmount,
royaltyAmountEth: ethers.utils.formatEther(royaltyAmount),
basisPoints: Math.round((royaltyAmount * 10000) / salePrice),
percentage: (royaltyAmount * 100) / salePrice
};
} catch (error) {
console.error('Error fetching royalty info:', error);
return null;
}
}
// 使用示例
async function main() {
const provider = new ethers.providers.AlchemyProvider(
'homestead',
'your_alchemy_api_key'
);
const contractAddress = '0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D'; // BAYC contract
const tokenId = 1;
const royaltyInfo = await getRoyaltyInfo(provider, contractAddress, tokenId);
if (royaltyInfo) {
console.log('Royalty Receiver:', royaltyInfo.receiver);
console.log('Royalty Amount for 1 ETH sale:', royaltyInfo.royaltyAmountEth, 'ETH');
console.log('Royalty Percentage:', royaltyInfo.percentage, '%');
}
}
main();
NFT市场最佳实践
成功的NFT市场需要考虑多个方面,包括用户体验、安全性、性能和可扩展性。本节将介绍一些NFT市场集成的最佳实践。
安全性最佳实践
-
智能合约安全
- 进行专业安全审计
- 使用经过验证的库和组件
- 实现多重签名机制
- 设置紧急暂停功能
-
前端安全
- 防止钓鱼攻击
- 验证合约地址
- 安全处理用户数据
- 实现防重放攻击机制
-
交易安全
- 使用安全的随机数生成
- 实现交易确认机制
- 防止抢先交易(Front-running)
- 加密敏感交易数据
// 防止钓鱼攻击的合约地址验证
function validateContractAddress(address, expectedAddress) {
// 转换为小写进行比较
const normalizedAddress = address.toLowerCase();
const normalizedExpected = expectedAddress.toLowerCase();
// 严格比较
if (normalizedAddress !== normalizedExpected) {
throw new Error('Contract address mismatch. Possible phishing attempt.');
}
return true;
}
// 实现交易确认提示
function showTransactionConfirmationDialog(transactionDetails) {
// 在实际应用中,应使用UI组件显示交易确认对话框
const { from, to, value, tokenId, contractAddress } = transactionDetails;
console.log('Please confirm the following transaction:');
console.log(`From: ${from}`);
console.log(`To: ${to}`);
if (value) console.log(`Value: ${ethers.utils.formatEther(value)} ETH`);
if (tokenId) console.log(`Token ID: ${tokenId}`);
if (contractAddress) console.log(`Contract: ${contractAddress}`);
// 返回Promise,在用户确认后解析
return new Promise((resolve) => {
// 模拟用户确认
// 在实际应用中,应等待用户交互
setTimeout(() => resolve(true), 1000);
});
}
性能优化
-
前端性能优化
- 实现NFT懒加载
- 使用图片CDN和缓存
- 优化组件渲染
- 减少不必要的区块链调用
-
区块链交互优化
- 批量调用合约方法
- 使用GraphQL API聚合数据
- 实现本地缓存策略
- 优化Gas使用
// NFT图片懒加载示例
const lazyLoadNFTImages = () => {
const nftImages = document.querySelectorAll('.nft-image[data-src]');
const imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.removeAttribute('data-src');
observer.unobserve(img);
}
});
});
nftImages.forEach(img => {
imageObserver.observe(img);
});
};
// 批量获取NFT数据
async function batchFetchNFTs(provider, contractAddress, tokenIds) {
// 创建合约实例
const nftAbi = ["function tokenURI(uint256) public view returns (string memory)"];
const contract = new ethers.Contract(contractAddress, nftAbi, provider);
// 批量调用tokenURI方法
const promises = tokenIds.map(async (tokenId) => {
try {
const tokenURI = await contract.tokenURI(tokenId);
// 可以在这里获取元数据
return { tokenId, tokenURI };
} catch (error) {
console.error(`Error fetching NFT #${tokenId}:`, error);
return { tokenId, error: error.message };
}
});
// 并行处理所有请求
const results = await Promise.all(promises);
return results;
}
用户体验优化
-
钱包集成
- 支持多种钱包
- 实现自动连接
- 提供明确的连接状态指示
- 处理网络切换
-
交易体验
- 提供清晰的交易状态反馈
- 实现Gas价格推荐
- 提供交易历史记录
- 优化错误处理和用户反馈
-
搜索和发现
- 实现高级搜索功能
- 提供个性化推荐
- 优化NFT展示
- 支持收藏和关注
// 实现钱包连接和网络检查
async function connectWalletAndCheckNetwork() {
if (!window.ethereum) {
alert('Please install MetaMask to use this dApp');
return null;
}
try {
// 请求账户访问
const accounts = await window.ethereum.request({
method: 'eth_requestAccounts'
});
// 获取当前网络ID
const chainId = await window.ethereum.request({
method: 'eth_chainId'
});
// 检查是否在正确的网络上
const expectedChainId = '0x1'; // 主网
if (chainId !== expectedChainId) {
// 请求切换网络
try {
await window.ethereum.request({
method: 'wallet_switchEthereumChain',
params: [{ chainId: expectedChainId }]
});
} catch (error) {
console.error('User rejected network switch:', error);
alert('Please switch to the Ethereum mainnet');
return null;
}
}
// 创建provider和signer
const provider = new ethers.providers.Web3Provider(window.ethereum);
const signer = provider.getSigner();
return { provider, signer, address: accounts[0] };
} catch (error) {
console.error('Error connecting wallet:', error);
alert('Failed to connect wallet. Please try again.');
return null;
}
}
// 实现Gas价格推荐
async function getRecommendedGasPrice(provider) {
try {
// 获取当前Gas价格
const currentGasPrice = await provider.getGasPrice();
// 计算推荐价格(1.2倍当前价格)
const recommendedGasPrice = currentGasPrice.mul(120).div(100);
// 格式化显示
const currentPriceGwei = ethers.utils.formatUnits(currentGasPrice, 'gwei');
const recommendedPriceGwei = ethers.utils.formatUnits(recommendedGasPrice, 'gwei');
return {
current: currentGasPrice,
recommended: recommendedGasPrice,
currentGwei: parseFloat(currentPriceGwei).toFixed(2),
recommendedGwei: parseFloat(recommendedPriceGwei).toFixed(2)
};
} catch (error) {
console.error('Error fetching gas price:', error);
// 返回默认值
const defaultPrice = ethers.utils.parseUnits('30', 'gwei');
return {
current: defaultPrice,
recommended: defaultPrice,
currentGwei: '30.00',
recommendedGwei: '30.00'
};
}
}
可扩展性设计
-
多链支持
- 设计支持多条区块链
- 实现链间数据同步
- 提供统一的API接口
- 处理不同链的特性差异
-
微服务架构
- 将功能拆分为微服务
- 实现事件驱动架构
- 优化数据库设计
- 考虑未来功能扩展
-
API设计
- 设计RESTful API
- 实现GraphQL接口
- 提供Webhook支持
- 考虑API限流和认证
// 多链支持的配置示例
const chainConfigurations = {
ethereum: {
chainId: '0x1',
chainName: 'Ethereum Mainnet',
rpcUrls: ['https://mainnet.infura.io/v3/YOUR_INFURA_PROJECT_ID'],
blockExplorerUrls: ['https://etherscan.io'],
nativeCurrency: {
name: 'Ether',
symbol: 'ETH',
decimals: 18
}
},
polygon: {
chainId: '0x89',
chainName: 'Polygon Mainnet',
rpcUrls: ['https://polygon-rpc.com'],
blockExplorerUrls: ['https://polygonscan.com'],
nativeCurrency: {
name: 'MATIC',
symbol: 'MATIC',
decimals: 18
}
},
// 可以添加更多链的配置
};
// 动态切换链
async function switchChain(chainName) {
if (!window.ethereum) {
throw new Error('MetaMask not installed');
}
const chainConfig = chainConfigurations[chainName];
if (!chainConfig) {
throw new Error(`Chain ${chainName} not supported`);
}
try {
// 尝试切换到现有链
await window.ethereum.request({
method: 'wallet_switchEthereumChain',
params: [{ chainId: chainConfig.chainId }]
});
} catch (error) {
// 如果链未添加,请求添加链
if (error.code === 4902) {
await window.ethereum.request({
method: 'wallet_addEthereumChain',
params: [chainConfig]
});
} else {
throw error;
}
}
console.log(`Successfully switched to ${chainName}`);
}
总结
NFT市场集成是一个复杂但有价值的过程,需要综合考虑智能合约开发、前端实现、用户体验和安全性等多个方面。通过遵循本章介绍的API使用方法、交易集成技术、拍卖功能实现和版税设置等内容,开发者可以构建功能完善、用户友好的NFT市场应用。
随着区块链技术的不断发展,NFT市场也在持续演进。未来,我们可能会看到更多创新的交易模式、更高效的链上操作和更好的跨链兼容性,为NFT生态系统带来更多可能性。