钱包是用户与 Web3 世界交互的入口。一个优秀的 DApp 需要支持多种钱包、多链切换和流畅的用户体验。本文将涵盖原生连接方案、WalletConnect 协议以及现代 React 生态中最高效的 Wagmi + RainbowKit 组合。
一、以太坊浏览器钱包生态
主流钱包概览
| 钱包 | 类型 | 特点 | 用户量 |
|---|---|---|---|
| MetaMask | 浏览器扩展/移动端 | 最早、生态最完善 | 3000万+ |
| Rabby | 浏览器扩展 | 安全警报、多链体验优化 | 增长最快 |
| Trust Wallet | 移动端为主 | Binance 生态,内置 DEX | 大规模 |
| Coinbase Wallet | 扩展 + 移动端 | Coinbase 生态集成 | 大规模 |
| Rainbow | 移动端/扩展 | 设计优秀,NFT 友好 | 中高端用户 |
| Phantom | 扩展 | Solana 起家,已支持 EVM | 多链用户 |
| OKX / Bitget | 交易所钱包 | 内置交易功能 | 交易所用户 |
EIP-1193:标准化的钱包接口
现代浏览器钱包都遵循 EIP-1193 标准,通过 window.ethereum 暴露统一 API:
interface EthereumProvider {
request(args: { method: string; params?: unknown[] }): Promise<unknown>;
on(event: string, listener: (...args: any[]) => void): void;
removeListener(event: string, listener: (...args: any[]) => void): void;
}
二、原生连接方案(Ethers.js + MetaMask)
基础连接模式
import { ethers } from "ethers";
class WalletManager {
private provider: ethers.BrowserProvider | null = null;
private signer: ethers.JsonRpcSigner | null = null;
private address: string | null = null;
async connect(): Promise<string> {
if (!window.ethereum) {
throw new Error("请安装 MetaMask 或其他兼容钱包");
}
this.provider = new ethers.BrowserProvider(window.ethereum, {
name: "mainnet",
chainId: 1,
});
// 请求用户授权连接
await this.provider.send("eth_requestAccounts", []);
this.signer = await this.provider.getSigner();
this.address = await this.signer.getAddress();
this.setupListeners();
return this.address;
}
private setupListeners() {
window.ethereum.on("accountsChanged", (accounts: string[]) => {
if (accounts.length === 0) {
this.disconnect();
} else {
this.address = accounts[0];
}
});
window.ethereum.on("chainChanged", (chainId: string) => {
// chainId 是十六进制字符串,如 "0x1"
window.location.reload(); // 推荐做法
});
}
async disconnect() {
this.provider = null;
this.signer = null;
this.address = null;
}
async switchNetwork(chainId: number): Promise<void> {
const hexChainId = `0x${chainId.toString(16)}`;
try {
await window.ethereum.request({
method: "wallet_switchEthereumChain",
params: [{ chainId: hexChainId }],
});
} catch (switchError: any) {
// 如果链未添加,需要先添加
if (switchError.code === 4902) {
await this.addNetwork(chainId);
}
}
}
async addNetwork(chainId: number): Promise<void> {
const networks: Record<number, any> = {
137: {
chainId: "0x89",
chainName: "Polygon Mainnet",
nativeCurrency: { name: "MATIC", symbol: "MATIC", decimals: 18 },
rpcUrls: ["https://polygon-rpc.com"],
blockExplorerUrls: ["https://polygonscan.com"],
},
42161: {
chainId: "0xa4b1",
chainName: "Arbitrum One",
nativeCurrency: { name: "ETH", symbol: "ETH", decimals: 18 },
rpcUrls: ["https://arb1.arbitrum.io/rpc"],
blockExplorerUrls: ["https://arbiscan.io"],
},
};
await window.ethereum.request({
method: "wallet_addEthereumChain",
params: [networks[chainId]],
});
}
}
读取当前网络信息
async function getNetworkInfo() {
const chainId = await window.ethereum.request({ method: "eth_chainId" });
const accounts = await window.ethereum.request({ method: "eth_accounts" });
return {
chainId: parseInt(chainId, 16),
isConnected: accounts.length > 0,
address: accounts[0] || null,
};
}
三、WalletConnect:通用多钱包连接器
为什么需要 WalletConnect?
原生 window.ethereum 只能连接浏览器扩展钱包,无法支持:
- 移动端钱包(如 Trust Wallet、Rainbow)
- 桌面端通过二维码扫描连接
- 多钱包同时存在时的选择
WalletConnect v2 是一个通用协议,通过 WebSocket 中继 + 端到端加密实现钱包与 DApp 的安全通信。
使用 WalletConnect v2
import { EthereumProvider } from "@walletconnect/ethereum-provider";
const provider = await EthereumProvider.init({
projectId: "YOUR_WALLETCONNECT_PROJECT_ID", // 从 cloud.walletconnect.com 获取
chains: [1, 137, 42161], // 支持的主链
showQrModal: true, // 显示二维码弹窗
methods: ["eth_sendTransaction", "personal_sign"],
events: ["chainChanged", "accountsChanged"],
});
// 启用连接
await provider.enable();
// 用法与 EIP-1193 完全一致
const ethersProvider = new ethers.BrowserProvider(provider);
const signer = await ethersProvider.getSigner();
Project ID 申请
- 访问 WalletConnect Cloud
- 注册账号并创建新项目
- 获取
projectId,免费额度通常足够中小型 DApp 使用
四、现代开发栈:Wagmi + Viem + RainbowKit
架构概览
┌─────────────────────────────────────┐
│ RainbowKit │ ← 连接按钮 + 钱包选择 UI
│ ( beautiful connect modal ) │
└────────────────┬────────────────────┘
│
┌────────────────┴────────────────────┐
│ Wagmi (React Hooks) │ ← 状态管理 + 缓存
│ useAccount, useBalance, useWrite │
└────────────────┬────────────────────┘
│
┌────────────────┴────────────────────┐
│ Viem (Core SDK) │ ← 与链交互
│ Public Client + Wallet Client │
└─────────────────────────────────────┘
安装与配置
npm install wagmi viem @rainbow-me/rainbowkit
// wagmi.ts
import { getDefaultConfig } from "@rainbow-me/rainbowkit";
import { mainnet, polygon, arbitrum, base } from "wagmi/chains";
export const config = getDefaultConfig({
appName: "My DApp",
projectId: "YOUR_WALLETCONNECT_PROJECT_ID",
chains: [mainnet, polygon, arbitrum, base],
ssr: true, // 若使用 Next.js App Router
});
// providers.tsx (Next.js App Router)
import { WagmiProvider } from "wagmi";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { RainbowKitProvider } from "@rainbow-me/rainbowkit";
import { config } from "./wagmi";
const queryClient = new QueryClient();
export function Providers({ children }: { children: React.ReactNode }) {
return (
<WagmiProvider config={config}>
<QueryClientProvider client={queryClient}>
<RainbowKitProvider>{children}</RainbowKitProvider>
</QueryClientProvider>
</WagmiProvider>
);
}
使用 Hook 进行交互
// components/WalletInfo.tsx
import { useAccount, useBalance, useChainId, useSwitchChain } from "wagmi";
import { ConnectButton } from "@rainbow-me/rainbowkit";
export function WalletInfo() {
const { address, isConnected } = useAccount();
const chainId = useChainId();
const { switchChain } = useSwitchChain();
// 自动缓存,自动刷新
const { data: balance } = useBalance({ address });
if (!isConnected) {
return <ConnectButton />;
}
return (
<div>
<p>地址: {address}</p>
<p>网络: {chainId}</p>
<p>余额: {balance?.formatted} {balance?.symbol}</p>
<button onClick={() => switchChain({ chainId: 137 })}>
切换到 Polygon
</button>
</div>
);
}
合约写入操作
// 准备写入操作
import { useWriteContract, useWaitForTransactionReceipt } from "wagmi";
import { parseUnits } from "viem";
const abi = [
{
name: "transfer",
type: "function",
inputs: [
{ name: "to", type: "address" },
{ name: "amount", type: "uint256" },
],
outputs: [{ name: "", type: "bool" }],
},
] as const;
export function TransferButton({ tokenAddress }: { tokenAddress: `0x${string}` }) {
const { writeContract, data: hash } = useWriteContract();
const { isLoading, isSuccess } = useWaitForTransactionReceipt({ hash });
const handleTransfer = () => {
writeContract({
address: tokenAddress,
abi,
functionName: "transfer",
args: ["0xRecipient...", parseUnits("100", 18)],
});
};
return (
<button onClick={handleTransfer} disabled={isLoading}>
{isLoading ? "确认中..." : isSuccess ? "转账成功" : "转账"}
</button>
);
}
自定义 RainbowKit 主题
import { RainbowKitProvider, darkTheme } from "@rainbow-me/rainbowkit";
<RainbowKitProvider
theme={darkTheme({
accentColor: "#7b3fe4",
accentColorForeground: "white",
borderRadius: "medium",
})}
coolMode // 连接时的酷炫粒子效果
>
{children}
</RainbowKitProvider>
EIP-6963:多钱包发现协议
传统方案依赖 window.ethereum(单钱包注入),多钱包共存时会产生冲突。EIP-6963 定义了标准的事件接口,让多个钱包扩展能够同时声明自己的 Provider:
// 钱包扩展广播自身
window.dispatchEvent(new CustomEvent("eip6963:announceProvider", {
detail: {
info: { uuid: " abc123", name: "MetaMask", icon: "..." },
provider: ethereumProvider,
}
}));
// DApp 监听所有可用的钱包
window.addEventListener("eip6963:announceProvider", (event) => {
const { info, provider } = event.detail;
console.log(`发现钱包: ${info.name}`);
// 用户可从列表中选择
});
// 请求钱包重新广播
dispatchEvent(new Event("eip6963:requestProvider"));
Wagmi v2 已内置 EIP-6963 支持,@rainbow-me/rainbowkit 自动聚合多钱包入口,无需手动处理事件监听。
智能合约交互安全
DApp 前端是与智能合约交互的第一道防线,安全疏忽可能导致用户资金损失:
Reentrancy 防护:
- 前端不应依赖用户自行检查合约安全
- 集成 OpenZeppelin Defender 或 Tenderly 进行合约审计标记
- 对涉及外部调用的操作(如
transfer)添加二次确认弹窗
Token Approval 风险:
- 无限授权(
approve(spender, type(uint256).max))是常见安全隐患 - 引导用户使用最小必要授权,或集成 Permit2(Uniswap)实现单次授权
- 显示当前授权额度,提供一键撤销选项
// 检查授权额度
const allowance = await erc20Contract.read.allowiance([userAddress, spenderAddress]);
const decimals = await erc20Contract.read.decimals();
const formattedAllowance = formatUnits(allowance, decimals);
// 建议用户仅授权本次需要金额
const approvalAmount = parseUnits("100", decimals); // 而非 MaxUint256
Typed Data Signing(EIP-712)
传统 personal_sign 消息用户无法直观理解内容,EIP-712 引入结构化数据签名,提升可读性与安全性:
import { createWalletClient, custom } from "viem";
const message = {
domain: {
name: "My DApp",
version: "1",
chainId: 1,
verifyingContract: "0x...",
},
types: {
Order: [
{ name: "amount", type: "uint256" },
{ name: "recipient", type: "address" },
],
},
primaryType: "Order",
message: {
amount: 1000000000000000000n,
recipient: "0x...",
},
};
const signature = await walletClient.signTypedData(message);
// 用户签名时看到结构化人类可读内容,而非十六进制字符串
Sign-In with Ethereum(SIWE)
SIWE(EIP-4361)提供无需密码的去中心化身份认证:
import { SiweMessage } from "siwe";
const message = new SiweMessage({
domain: window.location.host,
address: address,
statement: "Sign in to My DApp",
uri: window.location.origin,
version: "1",
chainId: 1,
nonce: generateNonce(), // 服务端生成防重放
});
const signature = await walletClient.signMessage({
message: message.prepareMessage(),
});
// 发送至服务端验证
const response = await fetch("/api/auth/verify", {
method: "POST",
body: JSON.stringify({ message: message.prepareMessage(), signature }),
});
SIWE 的优势:用户无需记忆密码、天然绑定 Web3 身份、可扩展为 DID(去中心化标识符)登录。
Gasless 交易与账户抽象
通过 ERC-4337(Account Abstraction),用户无需持有 ETH 支付 Gas,体验接近 Web2:
import { createSmartAccountClient } from "permissionless";
const smartAccountClient = createSmartAccountClient({
account: simpleSmartAccount,
chain: mainnet,
bundlerTransport: http(bundlerRpc),
middleware: {
sponsorUserOperation: paymasterClient.sponsorUserOperation, // Paymaster 代付 Gas
},
});
// 用户无需 ETH,Paymaster 代为支付 Gas
const txHash = await smartAccountClient.sendTransaction({
to: "0x...",
value: parseEther("0.1"),
});
Paymaster 模式适合:
- 新用户零门槛体验( sponsor 首次交易 Gas)
- 稳定币 Gas 支付(用户用 USDC 代替 ETH 付 Gas)
- 订阅制产品(每月固定额度 sponsor)
前端安全审计清单
| 检查项 | 严重级别 | 验证方法 |
|---|---|---|
| 检测钓鱼域名 | 🔴 | 集成 ChainPatrol / Blowfish |
| 合约地址白名单 | 🔴 | 前端硬编码校验 + 链上验证 |
| 交易模拟预览 | 🟠 | Tenderly Simulation API |
| 签名内容可读 | 🟠 | 强制使用 EIP-712 结构化数据 |
| HTTPS + CSP | 🟠 | 部署时验证响应头 |
| 依赖漏洞扫描 | 🟡 | Dependabot / Snyk 自动化 |
| 第三方脚本完整性 | 🟡 | Subresource Integrity (SRI) |
五、多链策略设计
常见多链模式
| 模式 | 描述 | 适用场景 |
|---|---|---|
| 单链专注 | DApp 仅在一个链上运行 | 低成本启动、特定生态 |
| 链选择器 | 用户手动切换不同链的相同合约 | DeFi 协议多链部署 |
| 统一跨链 | 使用 LayerZero / Axelar 抽象链差异 | 跨链桥、全链应用 |
| 链抽象 | 账户抽象 + 意图层,用户无感知 | 未来方向(ERC-4337 + 意图) |
推荐 RPC 提供商
| 提供商 | 免费额度 | 特色 |
|---|---|---|
| Alchemy | generous | 开发者工具全面 |
| Infura | 适中 | 以太坊基金会背景 |
| QuickNode | 适中 | 全球节点多 |
| Public Node | 免费 | 公共节点,可靠性较低 |
| Tenderly | 有限 | 强大的模拟调试功能 |
六、本章小结
钱包连接是 DApp 用户体验的第一道门槛。从原生 window.ethereum 到 WalletConnect 多钱包协议,再到 Wagmi + RainbowKit 的现代 React 开发栈,工具链不断演进。对于新项目,强烈建议直接使用 Wagmi + Viem + RainbowKit 组合,可节省大量重复开发工作,且自动获得类型安全、缓存优化和优雅 UI。同时,要注意处理网络切换、错误边界和加载状态,提供流畅的用户体验。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。