本文将带领你从零构建一个完整的 Web3 应用:一个允许用户铸造和查看 NFT 的 DApp。我们将覆盖智能合约开发、前端交互、钱包连接和部署的完整链路。
Web3 开发与传统 Web2 的最大区别在于信任模型:智能合约部署后不可篡改,所有状态变更透明可审计;用户通过钱包自主管理资产身份,无需中心化服务器存储敏感数据。这种架构带来了去中心化的优势,也对开发者的安全意识和工程实践提出了更高要求。
Web3 vs Web2 信任模型对比
Web2 架构中,用户将数据和资产托管给中心化平台,信任建立在服务提供商的品牌和法律条款之上。一旦平台发生安全漏洞或恶意行为,用户往往束手无策。Web3 通过密码学和去中心化共识重构了这一模型:智能合约以代码为法律(Code is Law),部署到区块链后逻辑公开透明、不可单方面篡改;用户通过私钥自主掌控钱包内的资产与链上身份,无需向任何第三方泄露敏感凭证。这种信任最小化(Trust Minimization)的设计大幅降低了单点故障和审查风险。
然而,去中心化也带来了新的安全维度。智能合约一旦部署代码即难以回滚,任何逻辑漏洞都可能导致资金永久损失;前端界面虽然运行在用户本地,但仍可能被恶意域名或钓鱼攻击替换;Gas 费用和交易确认延迟也对用户体验提出了更高要求。因此,Web3 开发者必须在架构设计阶段就将安全审计、前端校验和应急响应机制纳入核心考量,而非事后补救。
一、项目概述与架构
Full Stack DApp 架构:
┌─────────────────────────────────────────────────┐
│ Frontend (Next.js + React + TailwindCSS) │
│ ├─ wagmi: 区块链交互 Hook │
│ ├─ RainbowKit: 钱包连接 UI │
│ ├─ viem: 底层 SDK │
│ └─ 部署: Vercel │
└────────────────┬────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ Smart Contract (Solidity + Hardhat) │
│ ├─ OpenZeppelin ERC-721 标准实现 │
│ ├─ 铸造功能 + 元数据管理 │
│ └─ 部署: Sepolia Testnet │
└─────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ Storage & RPC │
│ ├─ IPFS (Pinata): NFT 元数据和图片存储 │
│ └─ Alchemy: Sepolia RPC 节点 │
└─────────────────────────────────────────────────┘
二、智能合约开发
项目初始化
mkdir nft-minter-dapp && cd nft-minter-dapp
mkdir contract && cd contract
npm init -y
npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox
npx hardhat init # 选择 "Create a TypeScript project"
合约代码
// contracts/NFTMinter.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
contract NFTMinter is ERC721URIStorage, Ownable {
using Counters for Counters.Counter;
Counters.Counter private _tokenIds;
uint256 public mintPrice = 0.001 ether; // 铸造价格
uint256 public maxSupply = 1000; // 最大供应量
string public baseTokenURI; // 基础元数据 URI
mapping(uint256 => string) private _tokenNames;
event NFTMinted(address indexed minter, uint256 indexed tokenId, string tokenURI);
constructor(string memory _baseURI) ERC721("DApp NFT Collection", "DAPP") {
baseTokenURI = _baseURI;
}
function mint(string memory _tokenURI) external payable returns (uint256) {
require(msg.value >= mintPrice, "Insufficient payment");
require(_tokenIds.current() < maxSupply, "Max supply reached");
_tokenIds.increment();
uint256 newTokenId = _tokenIds.current();
_safeMint(msg.sender, newTokenId);
_setTokenURI(newTokenId, _tokenURI);
emit NFTMinted(msg.sender, newTokenId, _tokenURI);
return newTokenId;
}
function batchMint(string[] memory _tokenURIs) external payable returns (uint256[] memory) {
require(_tokenURIs.length > 0, "Empty batch");
require(msg.value >= mintPrice * _tokenURIs.length, "Insufficient payment");
require(_tokenIds.current() + _tokenURIs.length <= maxSupply, "Exceeds max supply");
uint256[] memory tokenIds = new uint256[](_tokenURIs.length);
for (uint256 i = 0; i < _tokenURIs.length; i++) {
_tokenIds.increment();
uint256 newTokenId = _tokenIds.current();
_safeMint(msg.sender, newTokenId);
_setTokenURI(newTokenId, _tokenURIs[i]);
tokenIds[i] = newTokenId;
emit NFTMinted(msg.sender, newTokenId, _tokenURIs[i]);
}
return tokenIds;
}
function withdraw() external onlyOwner {
uint256 balance = address(this).balance;
require(balance > 0, "No funds to withdraw");
(bool success, ) = payable(owner()).call{value: balance}("");
require(success, "Transfer failed");
}
function setMintPrice(uint256 _newPrice) external onlyOwner {
mintPrice = _newPrice;
}
function totalSupply() external view returns (uint256) {
return _tokenIds.current();
}
function tokensOfOwner(address _owner) external view returns (uint256[] memory) {
uint256 tokenCount = balanceOf(_owner);
uint256[] memory tokenIds = new uint256[](tokenCount);
uint256 counter = 0;
for (uint256 i = 1; i <= _tokenIds.current(); i++) {
if (ownerOf(i) == _owner) {
tokenIds[counter] = i;
counter++;
}
}
return tokenIds;
}
}
Hardhat 配置与部署脚本
// hardhat.config.ts
import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox";
import * as dotenv from "dotenv";
dotenv.config();
const config: HardhatUserConfig = {
solidity: {
version: "0.8.19",
settings: {
optimizer: {
enabled: true,
runs: 200,
},
},
},
networks: {
sepolia: {
url: process.env.ALCHEMY_SEPOLIA_URL || "",
accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [],
},
},
etherscan: {
apiKey: process.env.ETHERSCAN_API_KEY,
},
gasReporter: {
enabled: true,
currency: "USD",
},
};
export default config;
// scripts/deploy.ts
import { ethers } from "hardhat";
async function main() {
const baseURI = "ipfs://QmYourBaseURI/"; // 实际部署时替换
const NFTMinter = await ethers.getContractFactory("NFTMinter");
const nftMinter = await NFTMinter.deploy(baseURI);
await nftMinter.waitForDeployment();
const address = await nftMinter.getAddress();
console.log(`NFTMinter deployed to: ${address}`);
// 验证合约(可选)
console.log(`Verify with: npx hardhat verify --network sepolia ${address} "${baseURI}"`);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
# 部署到 Sepolia
npx hardhat run scripts/deploy.ts --network sepolia
# 验证合约源码
npx hardhat verify --network sepolia DEPLOYED_ADDRESS "ipfs://QmYourBaseURI/"
测试代码
// test/NFTMinter.test.ts
import { expect } from "chai";
import { ethers } from "hardhat";
describe("NFTMinter", function () {
let nftMinter: any;
let owner: any;
let addr1: any;
beforeEach(async function () {
[owner, addr1] = await ethers.getSigners();
const NFTMinter = await ethers.getContractFactory("NFTMinter");
nftMinter = await NFTMinter.deploy("ipfs://base/");
});
it("Should mint an NFT with correct payment", async function () {
const mintPrice = await nftMinter.mintPrice();
await expect(
nftMinter.connect(addr1).mint("ipfs://token1", { value: mintPrice })
)
.to.emit(nftMinter, "NFTMinted")
.withArgs(addr1.address, 1, "ipfs://token1");
expect(await nftMinter.ownerOf(1)).to.equal(addr1.address);
expect(await nftMinter.tokenURI(1)).to.equal("ipfs://token1");
});
it("Should reject insufficient payment", async function () {
await expect(
nftMinter.connect(addr1).mint("ipfs://token1", { value: 0 })
).to.be.revertedWith("Insufficient payment");
});
it("Should allow owner to withdraw", async function () {
const mintPrice = await nftMinter.mintPrice();
await nftMinter.connect(addr1).mint("ipfs://token1", { value: mintPrice });
const initialBalance = await ethers.provider.getBalance(owner.address);
await nftMinter.connect(owner).withdraw();
const finalBalance = await ethers.provider.getBalance(owner.address);
expect(finalBalance).to.be.gt(initialBalance);
});
});
Solidity 智能合约安全最佳实践
Solidity 智能合约一旦部署就难以修改,安全缺陷可能引发灾难性后果。以下是开发中必须遵循的核心安全原则。
重入攻击防护。当合约向外部地址转账时,接收方的 fallback 或 receive 函数可能回调到原合约,重复执行关键逻辑。历史上 The DAO 事件因重入漏洞被盗取超过六千万美元。防护手段包括遵循 Checks-Effects-Interactions 模式:先校验条件,再更新状态,最后进行外部调用;或使用 OpenZeppelin 的 ReentrancyGuard 修饰符。本示例中的 withdraw 函数已采用 checks-effects-interactions 模式,先读取余额再执行转账,同时 Solidity 0.8.x 的 call 语义也降低了重入风险。
整数溢出防护。Solidity 0.8.x 版本已内置溢出检查,算术运算默认会回滚。若使用较早版本,务必引入 SafeMath 库。批量铸造函数中的 _tokenIds.current() + _tokenURIs.length <= maxSupply 校验即是防止溢出边界条件的典型做法。
访问控制与权限管理。本合约使用 OpenZeppelin 的 Ownable 修饰符限制 withdraw 和 setMintPrice 的调用者。但生产环境中,单一 owner 地址存在私钥泄露风险。建议将关键权限转移至多签钱包(如 Gnosis Safe),对资金提取、参数变更等操作设置多签阈值。对于更复杂的场景,可考虑基于角色的访问控制(AccessControl)模型,为不同操作分配独立角色。
输入校验与状态约束。所有外部函数应对输入参数进行边界校验:maxSupply 限制防止无限增发,mintPrice 防止零成本攻击,_tokenURIs.length 防止空批量消耗 Gas。事件日志(event)的合理运用也有助于事后审计和前端状态同步。
Gas 优化策略
Gas 费用直接决定了用户交互成本,优化策略应从存储布局和计算效率两个维度入手。
存储布局优化。EVM 中存储(storage)每槽位(slot)三十二字节,相邻的相同类型变量应尽量紧凑排列。本合约中的 uint256 类型天然对齐到完整槽位。mapping 的每个键值对独立占槽,无法跨槽压缩。对频繁读写的状态变量,优先考虑内存(memory)或调用数据(calldata)替代存储写入。例如批量铸造函数返回的 tokenIds 数组声明为 memory,避免在链上持久化中间结果。
事件日志替代状态变量。当数据仅用于链下展示而无需合约内部逻辑引用时,优先使用事件(event)而非状态变量。日志存储成本远低于 storage:每个 topic 消耗三百七十五 Gas,数据段每字节仅八 Gas,而 storage 写操作高达两万 Gas。本示例中的 NFTMinted 事件即用于记录铸造历史,前端可通过 RPC 过滤事件日志来展示用户铸造记录,无需每次遍历 tokensOfOwner。
批量操作与循环优化。batchMint 函数比多次单条调用更省 Gas,因为循环内部的重复检查可以共享,calldata 的编码开销更低。但循环体内部应最小化 storage 写入次数:若在循环中反复更新同一变量,可先在 memory 中累加,循环结束后再一次性写入 storage。此外,unchecked 块可用于移除编译器自动插入的算术溢出检查,但前提是已通过前置条件确保不会溢出。
编译器优化器。Hardhat 配置中启用了 optimizer,runs 参数设置两百意味着在合约体积和执行效率之间取平衡。对于部署后调用频率高的函数,可适当提高 runs 值以获得更优的执行 Gas。
三、前端开发(Next.js + Wagmi + RainbowKit)
项目初始化
cd ..
mkdir frontend && cd frontend
npx create-next-app@latest . --typescript --tailwind --eslint --app --src-dir
npm install wagmi viem @rainbow-me/rainbowkit @tanstack/react-query
链配置与 Provider
// src/app/wagmi.ts
import { getDefaultConfig } from "@rainbow-me/rainbowkit";
import { sepolia } from "wagmi/chains";
export const config = getDefaultConfig({
appName: "NFT Minter DApp",
projectId: process.env.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID!,
chains: [sepolia],
ssr: true,
});
// 合约配置
export const CONTRACT_ADDRESS = "0xYourDeployedContractAddress" as const;
export const CONTRACT_ABI = [
{
inputs: [{ name: "_tokenURI", type: "string" }],
name: "mint",
outputs: [{ name: "", type: "uint256" }],
stateMutability: "payable",
type: "function",
},
{
inputs: [{ name: "owner", type: "address" }],
name: "tokensOfOwner",
outputs: [{ name: "", type: "uint256[]" }],
stateMutability: "view",
type: "function",
},
{
inputs: [],
name: "mintPrice",
outputs: [{ name: "", type: "uint256" }],
stateMutability: "view",
type: "function",
},
{
inputs: [],
name: "totalSupply",
outputs: [{ name: "", type: "uint256" }],
stateMutability: "view",
type: "function",
},
{
inputs: [{ name: "tokenId", type: "uint256" }],
name: "tokenURI",
outputs: [{ name: "", type: "string" }],
stateMutability: "view",
type: "function",
},
] as const;
// src/app/providers.tsx
"use client";
import { WagmiProvider } from "wagmi";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { RainbowKitProvider } from "@rainbow-me/rainbowkit";
import { config } from "./wagmi";
import "@rainbow-me/rainbowkit/styles.css";
const queryClient = new QueryClient();
export function Providers({ children }: { children: React.ReactNode }) {
return (
<WagmiProvider config={config}>
<QueryClientProvider client={queryClient}>
<RainbowKitProvider>{children}</RainbowKitProvider>
</QueryClientProvider>
</WagmiProvider>
);
}
// src/app/layout.tsx
import { Providers } from "./providers";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="zh-CN">
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}
铸造页面组件
// src/app/components/NFTMinter.tsx
"use client";
import { useState } from "react";
import { useAccount, useWriteContract, useReadContract, useWaitForTransactionReceipt } from "wagmi";
import { ConnectButton } from "@rainbow-me/rainbowkit";
import { parseEther } from "viem";
import { CONTRACT_ADDRESS, CONTRACT_ABI } from "../wagmi";
export function NFTMinter() {
const { isConnected } = useAccount();
const [tokenURI, setTokenURI] = useState("");
const { data: mintPrice } = useReadContract({
address: CONTRACT_ADDRESS,
abi: CONTRACT_ABI,
functionName: "mintPrice",
});
const { data: totalSupply } = useReadContract({
address: CONTRACT_ADDRESS,
abi: CONTRACT_ABI,
functionName: "totalSupply",
});
const { writeContract, data: hash } = useWriteContract();
const { isLoading, isSuccess } = useWaitForTransactionReceipt({ hash });
const handleMint = () => {
if (!tokenURI) return;
writeContract({
address: CONTRACT_ADDRESS,
abi: CONTRACT_ABI,
functionName: "mint",
args: [tokenURI],
value: mintPrice || parseEther("0.001"),
});
};
return (
<div className="max-w-2xl mx-auto p-6">
<h1 className="text-3xl font-bold mb-8 text-center">NFT Minter</h1>
<div className="flex justify-center mb-8">
<ConnectButton />
</div>
{isConnected && (
<div className="space-y-6">
<div className="bg-gray-50 rounded-lg p-4">
<p className="text-sm text-gray-600">
已铸造: {totalSupply?.toString() || "0"} / 1000
</p>
<p className="text-sm text-gray-600">
铸造价格: {mintPrice ? `${Number(mintPrice) / 1e18} ETH` : "加载中..."}
</p>
</div>
<div>
<label className="block text-sm font-medium mb-2">
Token URI (IPFS 链接)
</label>
<input
type="text"
value={tokenURI}
onChange={(e) => setTokenURI(e.target.value)}
placeholder="ipfs://Qm..."
className="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
/>
</div>
<button
onClick={handleMint}
disabled={isLoading || !tokenURI}
className="w-full bg-blue-600 text-white py-3 rounded-lg font-medium
disabled:bg-gray-400 hover:bg-blue-700 transition"
>
{isLoading ? "铸造中..." : isSuccess ? "铸造成功!" : "铸造 NFT"}
</button>
{hash && (
<p className="text-sm text-center">
交易哈希:{" "}
<a
href={`https://sepolia.etherscan.io/tx/${hash}`}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline"
>
{hash.slice(0, 10)}...{hash.slice(-8)}
</a>
</p>
)}
</div>
)}
</div>
);
}
// src/app/page.tsx
import { NFTMinter } from "./components/NFTMinter";
export default function Home() {
return (
<main className="min-h-screen bg-white">
<NFTMinter />
</main>
);
}
前端与链交互的 UX 设计
Web3 前端的核心挑战在于将异步、不可逆且高成本的链上操作转化为用户友好的交互体验。交易从发起到最终确认可能历经数秒甚至数十秒,期间网络拥塞、Gas 波动或合约回滚都可能中断流程,因此完善的反馈机制至关重要。
交易状态全生命周期反馈。用户点击铸造按钮后,前端应依次展示三个状态:交易已提交(pending)、已上链等待确认(confirming)、已完成(success)或失败(rejected)。本示例中的 isLoading 和 isSuccess 状态配合按钮文案变化即为基础实现。生产环境中,建议增加更细粒度的状态机,例如区分钱包签名等待、广播等待、区块确认进度。通过 useWaitForTransactionReceipt 的 confirmations 参数,可等待多个区块确认后再标记为最终成功,降低链重组风险带来的状态不一致。
错误处理与用户引导。链上交互常见的失败原因包括:余额不足、网络错误、Gas 估算失败、合约回滚、用户拒绝签名等。每种错误应给出清晰的中文提示和下一步操作建议。例如 Gas 不足时提示用户切换至正确网络或补充 Gas;合约回滚时捕获 revert reason 并展示具体原因。网络切换可采用 Wagmi 的 useSwitchChain 自动引导用户从主网切换至目标测试网或侧链,避免手动配置 RPC 的繁琐。
防重复提交与幂等性。由于交易提交到最终确认存在延迟,用户可能在等待期间多次点击按钮,导致重复铸造。前端应在 writeContract 调用后锁定交互,直到交易哈希返回或超时释放。对于批量操作,更应校验合约层的前置条件是否满足,而非仅依赖前端状态判断。
网络切换与多链支持。随着 Layer2 的兴起,DApp 常需支持多链部署。RainbowKit 和 Wagmi 已内置网络切换 UI,开发者可通过链配置数组自动展示支持的网络列表。对于跨链合约地址不一致的场景,建议将链 ID 与合约地址的映射关系封装为配置对象,在前端根据当前连接网络动态加载对应的 ABI 和地址。
四、IPFS 元数据管理
上传脚本
// scripts/upload-to-ipfs.ts
import pinataSDK from "@pinata/sdk";
import fs from "fs";
import path from "path";
const pinata = new pinataSDK({
pinataApiKey: process.env.PINATA_API_KEY!,
pinataSecretApiKey: process.env.PINATA_SECRET!,
});
async function uploadNFTMetadata(imagePath: string, name: string, description: string) {
// 1. 上传图片
const imageStream = fs.createReadStream(imagePath);
const imageResult = await pinata.pinFileToIPFS(imageStream, {
pinataMetadata: { name: `${name}-image` },
});
const imageURI = `ipfs://${imageResult.IpfsHash}`;
// 2. 构建并上传元数据
const metadata = {
name,
description,
image: imageURI,
attributes: [
{ trait_type: "Created By", value: "DApp Minter" },
{ trait_type: "Rarity", value: "Common" },
],
};
const metadataResult = await pinata.pinJSONToIPFS(metadata, {
pinataMetadata: { name: `${name}-metadata` },
});
return `ipfs://${metadataResult.IpfsHash}`;
}
// 使用示例
uploadNFTMetadata("./assets/nft1.png", "My First NFT", "A cool NFT minted via DApp")
.then(uri => console.log("Metadata URI:", uri));
五、部署与运维
Vercel 部署配置
// vercel.json
{
"framework": "nextjs",
"env": {
"NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID": "@wc-project-id"
}
}
# 环境变量
# .env.local(不提交到 Git)
NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID=your_project_id
# 部署
vercel --prod
Web3 生产环境的常见陷阱与监控
将 DApp 部署到生产环境仅是开始,持续运营中面临的威胁维度远超开发阶段。MEV 攻击、供应链污染、合约升级困境和监控盲区都是需要系统性应对的真实挑战。
MEV 攻击与交易排序风险。以太坊等公链的区块构建由验证者或区块构建者控制,他们可通过重新排列、插入或审查交易来提取最大可提取价值(MEV)。对于本示例中的 NFT 铸造,当某个稀有资产即将被铸造时,攻击者可能在用户交易待确认时以更高 Gas 抢跑(front-run)或三明治攻击。防护手段包括:使用 Flashbots Protect 等隐私交易通道提交交易,避免交易内容提前暴露在公共内存池;对于价格敏感操作(如 DEX 交易),设置 slippage 容忍度和 deadline 参数,防止交易在不利条件下执行过久。
前端运行时代码风险。Web3 前端高度依赖外部库和 RPC 节点,供应链攻击风险显著。恶意 npm 包可能篡改钱包交互逻辑,将交易重定向至攻击者地址。务实的防护手段包括:将关键依赖锁定版本并审计升级,合约地址硬编码而非动态获取,对敏感操作增加二次确认弹窗展示完整交易详情。此外,内容安全策略(CSP)和 Subresource Integrity(SRI)可限制非法脚本注入。若 DApp 使用服务端渲染(如 Next.js),还需警惕服务端注入恶意脚本到页面中的风险。
合约升级策略。本示例中的合约未实现升级功能,主网部署后发现漏洞将无法修复。常见的升级方案包括代理模式(Proxy Pattern):使用透明代理或 UUPS 代理将业务逻辑与状态存储分离,业务逻辑合约可替换而用户数据和地址保持不变。OpenZeppelin Upgrades Plugins 提供了 Hardhat 集成,可在部署和升级时代理模式的安全校验。另一种方案是合约注册表模式:主合约持有业务逻辑地址引用,升级时更新引用指向新实现。
监控与告警体系。生产级 DApp 需要实时监控合约异常行为和前端健康状态。链上监控工具 Tenderly 可设置警报规则,当特定函数调用、Gas 异常或资金流出触发阈值时自动通知。Forta 网络提供去中心化监控,社区开发的检测机器人可识别重入攻击、大额转账等异常模式。前端层面,应监控错误上报、钱包连接成功率和交易失败率,及时发现兼容性问题或 RPC 节点故障。建议至少配置两个独立的 RPC 提供商(如 Alchemy 和 Infura),在请求失败时自动切换,避免单点故障导致全站不可用。
密钥与资产管理。部署合约的私钥绝不应存储于开发环境或 CI/CD 服务器。推荐流程是:开发环境仅持有测试网密钥;主网部署使用硬件钱包(如 Ledger)离线签名,或部署后通过多签钱包接管合约所有权。CI/CD 流程中,用于验证合约的 Etherscan API Key 和 Pinata 密钥应通过安全的密钥管理服务(如 GitHub Secrets、HashiCorp Vault)注入,避免硬编码泄漏。
生产检查清单
| 检查项 | 状态 | 说明 |
|---|---|---|
| 合约审计 | ✅ | 主网部署前必须完成 |
| 多签管理 | ✅ | 合约所有权转移至多签钱包 |
| 时间锁 | ✅ | 关键参数变更延迟执行 |
| 前端 HTTPS | ✅ | Vercel 默认提供 |
| RPC 备用 | ✅ | 至少两个 RPC 提供商 |
| 监控告警 | ✅ | Tenderly / Forta |
六、本章小结
一个完整的 Web3 DApp 需要三层协同工作:
- 智能合约层:使用 Solidity + OpenZeppelin 编写安全的 NFT 合约,Hardhat 进行测试和部署
- 前端层:Next.js + Wagmi + RainbowKit 提供类型安全的钱包连接和链交互
- 基础设施层:IPFS/Pinata 存储元数据,Alchemy 提供 RPC 节点
通过本实战项目,你已经掌握了从合约开发到前端集成再到生产部署的完整链路。这是 Web3 全栈开发的基础范式,在此之上可以扩展更复杂的 DeFi、DAO 治理等高级功能。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。