DApp 开发与 NFT 示例 DApp 由智能合约、链上网络、钱包连接、前端应用和链下基础设施共同组成。开发重点不是单独完成合约或页面,而是保证 ABI、地址、链 ID、交易状态和权限边界在各层保持一致。
总体架构 1 2 3 4 5 6 7 8 9 10 11 flowchart LR U[用户] --> W[浏览器钱包] U --> F[前端应用] F --> W W --> R[RPC 节点] F --> R R --> C[智能合约] C --> E[事件日志] E --> I[索引服务] I --> F F --> M[IPFS 或对象存储]
开发流程
明确业务状态、参与角色和信任边界。
编写最小合约并建立单元测试与不变量测试。
编写部署脚本,固定网络、构造参数与管理员地址。
导出 ABI 和部署地址,生成前端类型。
实现钱包连接、读操作、交易模拟、签名和回执跟踪。
在本地网络与测试网执行端到端测试。
完成权限复核、安全审计、合约验证和前端部署。
建立事件监控、错误告警与升级处置方案。
项目结构 1 2 3 4 5 6 7 8 9 10 11 12 dapp/ ├── contracts/ │ └── NoteNFT.sol ├── scripts/ │ └── deploy.ts ├── test/ │ └── NoteNFT.test.ts ├── frontend/ │ ├── src/ │ └── public/ ├── deployments/ └── hardhat.config.ts
合约、测试和部署脚本应位于同一可重复构建的工程中。前端通过构建步骤读取 ABI 与部署清单,避免手工复制导致版本不一致。
NFT 合约示例 以下示例使用成熟库实现基础 ERC-721 铸造,并通过访问控制限制铸造权限。依赖版本应在项目锁文件中固定。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import {ERC721URIStorage} from "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol"; import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol"; contract NoteNFT is ERC721URIStorage, AccessControl { bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); uint256 private nextTokenId; constructor(address admin) ERC721("Note NFT", "NOTE") { _grantRole(DEFAULT_ADMIN_ROLE, admin); _grantRole(MINTER_ROLE, admin); } function mint(address receiver, string calldata metadataUri) external onlyRole(MINTER_ROLE) returns (uint256 tokenId) { tokenId = nextTokenId++; _safeMint(receiver, tokenId); _setTokenURI(tokenId, metadataUri); } function supportsInterface(bytes4 interfaceId) public view override(ERC721URIStorage, AccessControl) returns (bool) { return super.supportsInterface(interfaceId); } }
生产合约还应明确最大供应量、元数据是否可变、管理员轮换方式、暂停机制和版税规则。若元数据依赖中心化 URL,资产长期可用性将受服务商影响。
测试重点 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import { expect } from "chai" ;import { ethers } from "hardhat" ;it ("allows only a minter to mint" , async () => { const [admin, user, receiver] = await ethers.getSigners (); const factory = await ethers.getContractFactory ("NoteNFT" ); const nft = await factory.deploy (admin.address ); await expect ( nft.connect (user).mint (receiver.address , "ipfs://metadata" ) ).to .be .reverted ; await expect (nft.mint (receiver.address , "ipfs://metadata" )) .to .emit (nft, "Transfer" ) .withArgs (ethers.ZeroAddress , receiver.address , 0n ); });
至少覆盖权限、零地址、重复操作、供应量边界、元数据、接口支持和安全转账接收器。测试通过只能证明已覆盖行为,不等同于合约不存在漏洞。
部署脚本 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import { ethers } from "hardhat" ;async function main ( ) { const [deployer] = await ethers.getSigners (); const factory = await ethers.getContractFactory ("NoteNFT" ); const contract = await factory.deploy (deployer.address ); await contract.waitForDeployment (); console .log (JSON .stringify ({ chainId : (await ethers.provider .getNetwork ()).chainId .toString (), address : await contract.getAddress (), deployer : deployer.address , })); } main ().catch ((error ) => { console .error (error); process.exitCode = 1 ; });
部署后应保存链 ID、合约地址、交易哈希、编译器版本、优化参数和 Git 提交。地址不能只写在前端源码中,应生成按网络划分的部署清单。
前端读取与写入 读取操作不需要用户发送交易,可通过公共 RPC 客户端完成。写操作需要连接钱包、确认网络、模拟交易并等待回执。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import { BrowserProvider , Contract } from "ethers" ;import abi from "./abi/NoteNFT.json" ;const provider = new BrowserProvider (window .ethereum );await provider.send ("eth_requestAccounts" , []);const signer = await provider.getSigner ();const contract = new Contract (contractAddress, abi, signer);const transaction = await contract.mint ( await signer.getAddress (), "ipfs://bafy.../metadata.json" ); const receipt = await transaction.wait ();if (receipt?.status !== 1 ) { throw new Error ("transaction failed" ); }
前端需要展示等待钱包确认、交易已广播、链上确认和交易失败等不同状态。用户拒签、余额不足、链错误、nonce 冲突和交易被替换都应有明确提示。
交易状态 1 2 3 4 5 6 7 8 9 10 11 12 stateDiagram-v2 [*] --> Preparing Preparing --> AwaitingSignature AwaitingSignature --> Rejected AwaitingSignature --> Pending Pending --> Confirmed Pending --> Replaced Pending --> Failed Replaced --> Confirmed Confirmed --> [*] Rejected --> [*] Failed --> [*]
元数据 ERC-721 的 tokenURI 通常指向 JSON 元数据。JSON 中的图片 URL 也应采用稳定存储,并保持正确的媒体类型。
1 2 3 4 5 6 7 8 { "name" : "Note NFT 1" , "description" : "A verifiable note asset" , "image" : "ipfs://bafy.../image.png" , "attributes" : [ { "trait_type" : "Category" , "value" : "Learning" } ] }
上传后应先验证网关可读取内容,再执行铸造。若元数据不可变,应固定内容地址并避免后续覆盖。
安全检查
使用成熟标准库,避免自行实现代币标准。
管理员权限采用多签或延迟执行,避免单一私钥长期控制。
前端发起交易前展示目标合约、网络、金额和关键参数。
不信任前端校验,合约必须独立执行权限与状态检查。
RPC 密钥不等同于钱包私钥,但仍应限制域名、额度与接口范围。
监听合约事件时处理链重组,不要只依赖未确认日志。
升级合约时审查存储布局与初始化函数。
上线与维护 上线顺序通常为合约部署、源码验证、部署清单发布、索引器启动、前端部署和监控启用。发生异常时,应能够暂停前端入口、告警管理员并提供链上交易证据。若合约不可升级,则修复通常需要迁移资产和用户状态,因此发布前的测试与审计尤为重要。