Men6d656e/08_nft_with_frontend_by_myself

GitHub: Men6d656e/08_nft_with_frontend_by_myself

一个零 OpenZeppelin 依赖、从零实现 ERC-721 标准的全栈 NFT 去中心化应用,涵盖自研 Solidity 合约、Next.js 前端及 IPFS 集成。

Stars: 0 | Forks: 0

# ManualNFT dApp 一个生产级、单仓库(**monorepo**)去中心化应用 (dApp),包含一个从零开始构建、带有完整 NatSpec 文档的自定义 ERC-721 NFT 合约以及 Next.js 前端。**零 OpenZeppelin 依赖** —— 每一个合约原语都是基于第一性原理构建的。 图片会在铸造 (mint) 时上传至 IPFS —— 你可以使用 **IPFS Desktop**(本地,用于 Anvil 开发)或 **Pinata**(远程,用于 Sepolia/生产环境),通过环境变量进行配置。 ## 🏗️ 架构 ``` 08_nft_with_frontend_by_freebuff/ ├── contracts/ # Foundry workspace (Solidity) │ ├── src/ │ │ └── ManualNFT.sol # Custom ERC-721 (no OZ) │ ├── script/ │ │ └── DeployManualNFT.s.sol # Interactive deployment │ ├── test/ │ │ └── ManualNFT.t.sol # 34 fuzz + unit tests │ ├── broadcast/ # Deployment receipts (auto-generated) │ ├── foundry.toml │ └── remappings.txt ├── frontend/ # Next.js 16 App Router + TypeScript │ ├── src/ │ │ ├── app/ │ │ │ ├── layout.tsx # Root layout with Web3Providers │ │ │ ├── page.tsx # Dashboard (create, mint, inventory) │ │ │ └── globals.css # TailwindCSS v4 styles │ │ ├── components/ │ │ │ └── Providers.tsx # Wagmi + RainbowKit + React Query │ │ ├── hooks/ │ │ │ └── useInventory.ts # Token inventory fetcher │ │ ├── lib/ │ │ │ ├── wagmi.ts # Wagmi/RainbowKit config │ │ │ └── ipfs.ts # IPFS upload + resolution (local & Pinata) │ │ ├── generated.ts # Type-safe hooks (auto-generated) │ │ └── __tests__/ # Frontend tests │ ├── wagmi.config.ts # @wagmi/cli → foundry plugin │ └── vitest.config.ts ├── scripts/ │ └── extract-address.sh # Auto-extract contract address post-deploy ├── Makefile # Single-command orchestration └── README.md ``` ## 📸 截图 | 连接钱包 | 仪表盘与铸造 | 代币库存 | |:--------------:|:----------------:|:---------------:| | ![Connect Wallet](https://static.pigsec.cn/wp-content/uploads/repos/cas/70/70f39ee1da53539128fd6a87bb1932695f54fae7089f90f79efa0f9708b484ee.png) | ![Dashboard](https://static.pigsec.cn/wp-content/uploads/repos/cas/c4/c4fbd2217327c322fd9e2da4716dc3d14182ea0afce9efb90487c80b3454a55b.png) | ![Token Showcase](https://static.pigsec.cn/wp-content/uploads/repos/cas/53/536b7b4758280f4dc59973ec4ae97cce302301a16a16097ad8bbed809f900ef6.png) | ## 🛠️ 技术栈 | 层级 | 技术 | | --------------- | ----------------------------------------------------- | | **链 (Chain)** | Anvil (本地) / Sepolia (测试网) | | **合约 (Contracts)** | Solidity ^0.8.20, Foundry (forge, cast, anvil) | | **前端 (Frontend)** | Next.js 16 (App Router), TypeScript, TailwindCSS v4 | | **Web3** | Wagmi v2 + RainbowKit + Viem v2 | | **IPFS** | IPFS Desktop (本地) / Pinata (生产环境) | | **类型生成** | @wagmi/cli Foundry 插件 → React hooks | | **测试 (Testing)** | Forge (合约), Vitest + RTL (前端) | ## 🚀 快速开始 ### 前置条件 ``` # 安装 Foundry curl -L https://foundry.paradigm.xyz | bash foundryup # Node.js >= 18 node --version ``` ### 1. 安装所有内容 ``` make install-all ``` 只需一条命令即可安装 Foundry 依赖 (`forge-std`) 以及所有 npm 包。 ### 2. 配置环境 ``` cp frontend/.env.example frontend/.env.local cp .env.example .env ``` **必需变量:** | 变量 | 获取途径 | |---|---| | `NEXT_PUBLIC_WC_PROJECT_ID` | [WalletConnect Cloud](https://cloud.walletconnect.com) | | `NEXT_PUBLIC_SEPOLIA_RPC` | [Alchemy](https://alchemy.com) 或 [Infura](https://infura.io) | | `SEPOLIA_RPC` | 相同的 RPC URL,但在根目录的 `.env` 中为 Foundry 配置 | ### 3. 启动 Anvil 节点 ``` anvil # 保持此终端打开 — Anvil 在 :8545 上运行本地 Ethereum 节点 ``` ### 4. 启动 IPFS Desktop 确保 **IPFS Desktop** 正在运行。它将暴露: - **API**: `http://127.0.0.1:5001`(供应用上传使用) - **网关**: `http://127.0.0.1:8080`(供应用显示图片使用) 无需额外配置 —— 应用默认使用这些 endpoint。 ### 5. 部署合约(本地) ``` make deploy-anvil ``` 系统会提示你粘贴 **默认的 Anvil 私钥**: ``` 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 ``` 这一条命令将: 1. ✅ 构建合约 2. ✅ 通过 `forge script --broadcast` 部署 ManualNFT 3. ✅ 从广播日志中自动提取已部署的合约地址 4. ✅ 将 `NEXT_PUBLIC_CONTRACT_ADDRESS` 写入 `frontend/.env.local` 5. ✅ 重新生成 Wagmi 类型安全的 hooks ### 6. 启动前端 ``` make frontend-dev ``` 打开 [http://localhost:3000](http://localhost:3000),连接你的钱包(在 RainbowKit 中设置 Anvil 链),然后开始铸造! ## 🖼️ IPFS 集成 ### 工作原理 当你点击 **Mint Token** 时,流程如下: ``` 1. User uploads image ↓ 2. App uploads image to IPFS (via local API or Pinata) ↓ ← NEXT_PUBLIC_IPFS_PROVIDER selects which 3. IPFS returns CID ← ipfs://QmX... ↓ 4. Metadata JSON built with image: "ipfs://" ↓ 5. Metadata base64-encoded → stored on-chain as tokenURI ↓ 6. Wallet confirms transaction → Token minted! ``` 元数据 JSON(名称、描述、ipfs:// 图片 URL)作为 base64 编码的数据 URI 存储 **在链上** —— 这使得元数据不可变且成本低廉。只有图片文件会被上传至 IPFS。 ### 提供商选择 | 变量 | Anvil (本地) | Sepolia (生产环境) | |---|---|---| | `NEXT_PUBLIC_IPFS_PROVIDER` | `local` | `pinata` | | IPFS 服务 | IPFS Desktop(在你的机器上运行) | Pinata 云服务 | | API Endpoint | `http://127.0.0.1:5001` | `https://api.pinata.cloud` | | 网关 | `http://127.0.0.1:8080` | `https://gateway.pinata.cloud` | 在 `frontend/.env.local` 中设置环境变量以在提供商之间切换。 ### 本地 IPFS Desktop(用于 Anvil) 1. 下载并安装 [IPFS Desktop](https://docs.ipfs.tech/install/ipfs-desktop/) 2. 启动应用程序 —— 它会自动启动 API 和网关 3. 验证它是否正在运行: curl http://127.0.0.1:5001/api/v0/version # {"Version":"0.29.0","Commit":"...","Repo":"17","System":"amd64/linux","Golang":"go1.22.5"} 4. 默认端口:`5001` (API) 和 `8080` (网关) —— 可通过环境变量配置 无需更改环境变量 —— 应用默认使用带有标准端口的 `local` 提供商。 ### Pinata(用于 Sepolia) 1. 在 [Pinata](https://app.pinata.cloud) 创建一个免费账户 2. 进入 **API Keys** → **New Key** → 启用 `pinFileToIPFS` 3. 复制 **JWT** token 4. 在 `frontend/.env.local` 中设置: NEXT_PUBLIC_IPFS_PROVIDER=pinata NEXT_PUBLIC_PINATA_JWT=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... ## 🔄 完整开发工作流 ### 单命令开发循环 ``` make dev-env ``` 这会构建合约、生成 wagmi 类型并启动前端开发服务器。 ### 部署到 Sepolia ``` make deploy-sepolia ``` 系统会提示你输入 Sepolia 部署者私钥。合约地址会被自动提取并为前端配置好。 ## 🧪 测试 ### 合约测试(34 个测试) ``` make contracts-test ``` 测试套件涵盖: - **Mint**: 基本铸造、供应量跟踪、事件触发 - **Transfer**: 标准 + 安全转账、所有权变更 - **Approvals**: 单个授权 (`approve`/`getApproved`) + 操作员授权 (`setApprovalForAll`/`isApprovedForAll`) - **Enumeration**: `totalSupply`, `tokenByIndex`, `tokenOfOwnerByIndex` - **ERC-165**: 接口检测 - **Edge cases**: 零地址回滚、访问不存在的 token、自我授权、防止重复铸造 - **Receiver contracts**: ERC-721 receiver 接收,ERC-721 拒绝者合约回滚 - **Fuzz tests**: 多地址铸造 + 枚举 (`vm.assume`) ### 前端测试 ``` make frontend-test ``` 运行 Vitest + React Testing Library 冒烟测试: - 验证“连接钱包”落地页是否正确渲染 - 检查是否存在正确的标题和 CTA 按钮 ## 📜 智能合约:ManualNFT ### 工作原理(技术深潜) 每一个 ERC-721 函数都是手工实现的 —— **没有 OpenZeppelin 导入**: ``` ManualNFT ├── State Ledgers │ ├── _owners: tokenId → address (ownership) │ ├── _balances: owner → uint256 (balance tracking) │ ├── _tokenApprovals: tokenId → address (per-token approval) │ ├── _operatorApprovals: owner → mapping (operator approvals) │ ├── _tokenURIs: tokenId → string (metadata URIs) │ ├── _ownedTokens: owner → array (enumeration helper) │ ├── _ownedTokensIndex: tokenId → index (enumeration helper) │ ├── _allTokens: array (global token list) │ └── _allTokensIndex: tokenId → index (global index) │ ├── Core Operations │ ├── mint(to, uri) → tokenId │ ├── transferFrom(from, to, tokenId) │ ├── safeTransferFrom(from, to, tokenId, data) │ ├── approve(to, tokenId) │ └── setApprovalForAll(operator, approved) │ ├── View Functions │ ├── name() / symbol() │ ├── tokenURI(tokenId) │ ├── balanceOf(owner) │ ├── ownerOf(tokenId) │ ├── getApproved(tokenId) │ ├── isApprovedForAll(owner, operator) │ ├── supportsInterface(interfaceId) → ERC-165 │ ├── totalSupply() → Enumerable │ ├── tokenByIndex(index) → Enumerable │ └── tokenOfOwnerByIndex(owner, index) → Enumerable │ └── Internal ├── _mint(to, tokenId) ├── _beforeTokenTransfer(from, to, tokenId) ├── _checkOnERC721Received(operator, from, to, tokenId, data) └── _baseURI() ``` **Custom errors**(gas 优化,无字符串断言): - `NFT__NotAuthorized()` — 调用者缺乏权限 - `NFT__NonexistentToken()` — 查询已销毁/不存在的 token - `NFT__InvalidOwner()` — owner 地址不匹配 - `NFT__InvalidRecipient()` — 铸造/转账至零地址 - `NFT__NonERC721Receiver()` — 合约未实现 `onERC721Received` ### 部署脚本:`--interactives` 工作原理 ``` // Priority: // 1. PRIVATE_KEY environment variable (CI/automated) // 2. Interactive prompt (--interactives 1, secure) uint256 deployerPrivateKey = vm.envOr("PRIVATE_KEY", uint256(0)); if (deployerPrivateKey == 0) { string memory pkHex = vm.promptSecret("Enter private key..."); deployerPrivateKey = vm.parseUint(pkHex); } ``` `vm.promptSecret` 是一个 Foundry cheatcode,可以安全读取输入,而不会将其回显到终端或存储在 bash 历史记录中。 ## 🖥️ 前端架构 ### 组件树 ``` Providers (WagmiProvider → QueryClient → RainbowKitProvider) └── Home (page.tsx) ├── Mount guard (hydration safety) ├── [Not connected] Connect Wallet screen └── [Connected] Dashboard ├── Header (stats bar, network badge, wallet button) ├── Tab bar (Create & Mint | Inventory) ├── Create & Mint (AssetCreationForm) │ ├── Title / Description / Image upload (drag & drop) │ ├── IPFS upload (local Desktop or Pinata) │ ├── Live preview panel │ └── Mint button + toast notifications └── Inventory (InventoryPanel) ├── Token grid with IPFS-resolved images └── Refresh button ``` ### IPFS 上传流程(详细) ``` ┌─────────────────────────────────────────────────────────┐ │ handleMint() │ │ ┌───────────────────────────────────────────────────┐ │ │ │ 1. setIsUploadingIPFS(true) │ │ │ │ 2. uploadToIPFS(selectedFile) │ │ │ │ ├─ provider === "local" │ │ │ │ │ └─ POST /api/v0/add to 127.0.0.1:5001 │ │ │ │ └─ provider === "pinata" │ │ │ │ └─ POST pinFileToIPFS to api.pinata.cloud │ │ │ │ 3. Returns { cid, uri: "ipfs://" } │ │ │ │ 4. Build metadata: { image: "ipfs://" } │ │ │ │ 5. Base64-encode metadata → tokenURI │ │ │ │ 6. mintToken({ args: [address, uri] }) │ │ │ └───────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────┘ ┌─────────────────────────────────────────────────────────┐ │ useInventory() (fetchInventory) │ │ ┌───────────────────────────────────────────────────┐ │ │ │ 1. balanceOf(address) → count │ │ │ │ 2. For each token: │ │ │ │ ├─ tokenOfOwnerByIndex(owner, i) → tokenId │ │ │ │ ├─ tokenURI(tokenId) → "data:..." │ │ │ │ ├─ atob() → JSON.parse → metadata │ │ │ │ └─ resolveIPFS(metadata.image) → gateway URL │ │ │ └───────────────────────────────────────────────────┘ │ │ Returns TokenInfo[] with resolved image URLs │ └─────────────────────────────────────────────────────────┘ ``` ### 状态管理 - **钱包状态**: Wagmi 的 `useAccount()` —— 响应式地址 + 连接状态 - **合约读取**: 生成的 `useReadManualNft*` hooks(在地址/链更改时自动重新获取) - **合约写入**: 生成的 `useWriteManualNftMint` hook(返回交易哈希) - **IPFS 上传**: 自定义 `uploadToIPFS()` —— 异步,显示加载状态,错误会传播到 toast - **库存**: 自定义 `useInventory` hook —— 遍历每个 token 的 `tokenOfOwnerByIndex` + `tokenURI`,解析 IPFS URI - **本地状态**: 用于表单字段、toast、UI 标签页的 React `useState` ### 类型安全的合约绑定 (`@wagmi/cli`) 文件 `frontend/wagmi.config.ts` 使用 Foundry 插件读取 `contracts/out/ManualNFT.sol/ManualNFT.json` 并生成 `frontend/src/generated.ts`。这意味着: - ✅ **完整的 TypeScript 类型** —— 函数名称、输入、输出、事件、错误 - ✅ **React hooks** —— `useReadManualNft*`, `useWriteManualNft*`, `useSimulateManualNft*`, `useWatchManualNft*` - ✅ **零 ABI 复制粘贴** —— 在任何合约更改后通过 `make wagmi-gen` 重新生成 ## 🔄 部署流水线(自动地址提取) 当你运行 `make deploy-anvil` 或 `make deploy-sepolia` 时: 1. **`forge script`** 部署合约并将广播日志写入 `contracts/broadcast/DeployManualNFT.s.sol//run-latest.json` 2. **`scripts/extract-address.sh`** 解析 JSON 并提取 `contractAddress` 3. 脚本将 `NEXT_PUBLIC_CONTRACT_ADDRESS=
` 写入 `frontend/.env.local` 4. **`make wagmi-gen`** 重新生成类型安全的 hooks 你永远不需要手动复制粘贴合约地址。 ## 📦 Makefile 命令 | 命令 | 描述 | |---|---| | **`make install-all`** | 安装所有依赖(合约 + 前端) | | **`make contracts-build`** | 使用 forge 编译 Solidity | | **`make contracts-test`** | 运行 34 个合约测试 (+vvvv) | | **`make wagmi-gen`** | 构建合约 + 生成 React hooks | | **`make deploy-anvil`** | 部署到 localhost:8545 + 注入地址 | | **`make deploy-sepolia`** | 部署到 Sepolia + 注入地址 | | **`make post-deploy-setup`** | 提取地址 + 重新生成类型 | | **`make frontend-dev`** | 启动 Next.js 开发服务器 | | **`make frontend-build`** | 生产环境构建 | | **`make frontend-test`** | 运行 Vitest 前端测试 | | **`make dev-env`** | 构建 → 类型 → 开发服务器 | | **`make clean`** | 移除构建产物 | ## ⚙️ 环境变量 ### `frontend/.env.local` | 变量 | 必需 | 默认值 | 描述 | |---|---|---|---| | `NEXT_PUBLIC_WC_PROJECT_ID` | ✅ 是 | — | WalletConnect Project ID | | `NEXT_PUBLIC_ANVIL_RPC` | ❌ 否 | `http://localhost:8545` | Anvil RPC URL | | `NEXT_PUBLIC_SEPOLIA_RPC` | 用于 Sepolia | — | Sepolia RPC URL | | `NEXT_PUBLIC_CONTRACT_ADDRESS` | 部署后 | — | 由 `make deploy-*` 自动设置 | | `NEXT_PUBLIC_IPFS_PROVIDER` | ❌ 否 | `local` | `local` 或 `pinata` | | `NEXT_PUBLIC_LOCAL_IPFS_API` | ❌ 否 | `http://127.0.0.1:5001` | IPFS Desktop API URL | | `NEXT_PUBLIC_LOCAL_IPFS_GATEWAY` | ❌ 否 | `http://127.0.0.1:8080` | IPFS Desktop 网关 URL | | `NEXT_PUBLIC_PINATA_JWT` | 用于 Pinata | — | Pinata JWT token | | `NEXT_PUBLIC_PINATA_GATEWAY` | ❌ 否 | `https://gateway.pinata.cloud` | Pinata 网关 URL | ### 根目录 `.env` | 变量 | 必需 | 描述 | |---|---|---| | `SEPOL_RPC` | 用于 Sepolia 部署 | `forge script` 使用的 RPC URL |
标签:DApp, NFT, Solidity, Web3.0, 以太坊, 区块链, 智能合约, 自动化攻击