钱包是用户与 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>
五、多链策略设计
常见多链模式
| 模式 | 描述 | 适用场景 |
|---|---|---|
| 单链专注 | 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。同时,要注意处理网络切换、错误边界和加载状态,提供流畅的用户体验。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。