NFT Mint 网站
NFT Mint 网站
本页内容正在整理中,欢迎贡献。
概述
本教程构建一个完整的 NFT 铸造网站:用户连接钱包后可以铸造 NFT,支持公开铸造和白名单铸造,带有实时状态显示。涵盖 ERC-721 合约 + Next.js 前端的全栈开发。
主要内容
项目结构
nft-mint-site/├── contracts/│ └── MyNFT.sol # ERC-721 合约├── scripts/│ └── deploy.ts # 部署脚本├── test/│ └── MyNFT.test.ts└── frontend/ # Next.js 前端 ├── app/ │ ├── page.tsx # 首页(Mint 页面) │ └── providers.tsx └── components/ └── MintSection.tsx合约实现(简化版)
// SPDX-License-Identifier: MITpragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";import "@openzeppelin/contracts/access/Ownable.sol";import "@openzeppelin/contracts/utils/Strings.sol";
contract MyNFT is ERC721, Ownable { uint256 public constant MAX_SUPPLY = 1000; uint256 public constant MINT_PRICE = 0.01 ether; uint256 public constant MAX_PER_WALLET = 3;
uint256 private _nextTokenId; string private _baseTokenURI; bool public publicMintOpen;
mapping(address => uint256) public mintedPerWallet;
event Minted(address indexed to, uint256 indexed tokenId);
constructor(address initialOwner) ERC721("My NFT Collection", "MNFT") Ownable(initialOwner) {}
function publicMint(uint256 quantity) external payable { require(publicMintOpen, "Public mint not open"); require(quantity > 0 && quantity <= MAX_PER_WALLET, "Invalid quantity"); require(mintedPerWallet[msg.sender] + quantity <= MAX_PER_WALLET, "Exceeds per-wallet limit"); require(_nextTokenId + quantity <= MAX_SUPPLY, "Sold out"); require(msg.value >= MINT_PRICE * quantity, "Insufficient ETH");
mintedPerWallet[msg.sender] += quantity;
for (uint256 i = 0; i < quantity; ) { uint256 tokenId = _nextTokenId++; _safeMint(msg.sender, tokenId); emit Minted(msg.sender, tokenId); unchecked { ++i; } }
// 退还多余 ETH if (msg.value > MINT_PRICE * quantity) { payable(msg.sender).transfer(msg.value - MINT_PRICE * quantity); } }
function setPublicMintOpen(bool open) external onlyOwner { publicMintOpen = open; }
function setBaseURI(string calldata baseURI) external onlyOwner { _baseTokenURI = baseURI; }
function withdraw() external onlyOwner { payable(owner()).transfer(address(this).balance); }
function totalMinted() external view returns (uint256) { return _nextTokenId; }
function _baseURI() internal view override returns (string memory) { return _baseTokenURI; }}前端实现(Next.js + wagmi)
"use client";import { useState } from "react";import { useAccount, useReadContracts, useWriteContract, useWaitForTransactionReceipt} from "wagmi";import { ConnectButton } from "@rainbow-me/rainbowkit";import { parseEther, formatEther } from "viem";import { MY_NFT_ADDRESS, MY_NFT_ABI } from "@/lib/contracts";
export function MintSection() { const { address, isConnected } = useAccount(); const [quantity, setQuantity] = useState(1);
// 读取合约状态 const { data, refetch } = useReadContracts({ contracts: [ { address: MY_NFT_ADDRESS, abi: MY_NFT_ABI, functionName: "totalMinted" }, { address: MY_NFT_ADDRESS, abi: MY_NFT_ABI, functionName: "MAX_SUPPLY" }, { address: MY_NFT_ADDRESS, abi: MY_NFT_ABI, functionName: "MINT_PRICE" }, { address: MY_NFT_ADDRESS, abi: MY_NFT_ABI, functionName: "publicMintOpen" }, { address: MY_NFT_ADDRESS, abi: MY_NFT_ABI, functionName: "mintedPerWallet", args: address ? [address] : undefined, query: { enabled: !!address }, }, ], });
const [totalMinted, maxSupply, mintPrice, publicMintOpen, mintedByUser] = data ?? [];
// 铸造 const { writeContract, data: txHash, isPending } = useWriteContract(); const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({ hash: txHash, onSuccess: () => refetch(), });
const handleMint = () => { writeContract({ address: MY_NFT_ADDRESS, abi: MY_NFT_ABI, functionName: "publicMint", args: [BigInt(quantity)], value: mintPrice?.result ? mintPrice.result * BigInt(quantity) : 0n, }); };
const isSoldOut = totalMinted?.result >= maxSupply?.result; const isMinting = isPending || isConfirming;
return ( <div className="max-w-md mx-auto p-6 bg-white rounded-xl shadow"> <h1 className="text-2xl font-bold mb-4">My NFT Collection</h1>
{/* 铸造进度 */} <div className="mb-4"> <div className="flex justify-between text-sm text-gray-600"> <span>已铸造</span> <span>{totalMinted?.result?.toString() ?? "..."} / {maxSupply?.result?.toString() ?? "..."}</span> </div> <div className="w-full bg-gray-200 rounded-full h-2 mt-1"> <div className="bg-blue-600 h-2 rounded-full" style={{ width: `${((Number(totalMinted?.result ?? 0n) / Number(maxSupply?.result ?? 1n)) * 100)}%` }} /> </div> </div>
{/* 价格 */} <p className="text-gray-600 mb-4"> 铸造价格:{mintPrice?.result ? formatEther(mintPrice.result) : "..."} ETH / 个 </p>
{!isConnected ? ( <ConnectButton /> ) : isSoldOut ? ( <button className="w-full bg-gray-400 text-white py-3 rounded-lg" disabled> 已售罄 </button> ) : !publicMintOpen?.result ? ( <button className="w-full bg-gray-400 text-white py-3 rounded-lg" disabled> 铸造尚未开放 </button> ) : ( <div> {/* 数量选择 */} <div className="flex items-center gap-3 mb-4"> <button onClick={() => setQuantity(Math.max(1, quantity - 1))} className="px-3 py-1 border rounded">-</button> <span className="text-xl font-bold">{quantity}</span> <button onClick={() => setQuantity(Math.min(3, quantity + 1))} className="px-3 py-1 border rounded">+</button> <span className="text-sm text-gray-500">最多 3 个</span> </div>
<button onClick={handleMint} disabled={isMinting} className="w-full bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 text-white py-3 rounded-lg font-bold" > {isPending ? "确认中..." : isConfirming ? "上链中..." : `铸造 ${quantity} 个 NFT`} </button>
{isSuccess && ( <p className="mt-3 text-green-600 text-center">🎉 铸造成功!</p> )} </div> )} </div> );}运行项目
# 1. 安装依赖npm install
# 2. 运行本地测试npx hardhat test
# 3. 部署到 Sepolianpx hardhat run scripts/deploy.ts --network sepolia
# 4. 启动前端cd frontend && npm run dev