Web3 钱包连接方案
一、需求分析
1.1 什么是 Web3 钱包
Web3 钱包是用户与区块链交互的入口,核心功能是管理私钥和签署交易。与传统 Web 应用中的"登录系统"不同,Web3 钱包不依赖中心化服务器验证身份,而是通过密码学证明用户对链上资产的所有权。
面试中被问到"Web3 钱包是什么"时,应从私钥管理、交易签名、去中心化身份三个维度回答,展示对 Web3 身份体系的理解。
1.2 钱包分类
| 维度 | 类型 | 说明 | 代表产品 |
|---|---|---|---|
| 存储方式 | 热钱包 (Hot Wallet) | 私钥存储在联网设备,使用方便但安全性较低 | MetaMask、Phantom、Trust Wallet |
| 存储方式 | 冷钱包 (Cold Wallet) | 私钥离线存储,安全性高但操作不便 | Ledger、Trezor |
| 账户类型 | EOA (Externally Owned Account) | 由私钥直接控制的外部账户,是最基础的账户类型 | MetaMask 默认账户 |
| 账户类型 | 合约钱包 (Contract Wallet) | 由智能合约控制的账户,支持多签、社交恢复等高级功能 | Safe (Gnosis)、Argent |
| 连接方式 | 浏览器扩展 | 以浏览器插件形式运行,注入 Provider 到页面 | MetaMask、OKX Wallet |
| 连接方式 | 移动端 App | 通过 WalletConnect 或 Deep Link 与 DApp 交互 | Trust Wallet、Rainbow |
| 连接方式 | 嵌入式钱包 | 集成在 DApp 内部,通过社交登录创建 | Privy、Magic、Web3Auth |
1.3 核心概念
| 概念 | 说明 | 安全级别 |
|---|---|---|
| 助记词 | 12/24 个英文单词,根据 BIP-39 标准生成,可派生出无限个私钥 | 最高机密,泄露即丢失所有资产 |
| 私钥 | 256 位随机数(64 位十六进制字符串),用于签署交易 | 绝密,不可泄露 |
| 公钥 | 由私钥通过椭圆曲线算法(secp256k1)推导,不可逆向推出私钥 | 可公开 |
| 地址 | 公钥经 Keccak-256 哈希后取末 20 字节(以太坊),是链上身份标识 | 可公开,类似银行账号 |
前端代码永远不应接触用户的私钥和助记词。所有签名操作都在钱包内部完成,DApp 仅接收签名结果。
1.4 设计目标
| 设计目标 | 说明 | 关键指标 |
|---|---|---|
| 多钱包兼容 | 支持主流钱包(MetaMask、WalletConnect、Coinbase 等) | 覆盖 90%+ 用户 |
| 多链支持 | 同时支持 EVM(Ethereum/BSC/Polygon)、Solana、TON 等 | 3+ 链生态 |
| 连接稳定性 | 自动重连、账户/链切换监听、错误处理 | 断线重连 < 3s |
| 安全性 | 钓鱼防护、交易预览、域名校验 | 零私钥泄露 |
| 开发体验 | 简洁的 API、TypeScript 类型完备、React Hooks 支持 | 10 行代码接入 |
二、整体架构
2.1 分层架构
2.2 核心模块
| 模块 | 职责 | 关键技术 |
|---|---|---|
| UI 层 | 连接按钮、链切换、交易确认等用户界面 | React 组件、RainbowKit |
| Hooks 层 | 封装钱包操作为 React Hooks,管理状态 | wagmi hooks、React Query |
| Adapter 适配层 | 统一不同链/钱包的接口差异 | 适配器模式、策略模式 |
| Connector 连接层 | 实现各钱包的具体连接逻辑 | EIP-1193、WalletConnect SDK |
| Provider 协议层 | 实现钱包通信标准协议 | JSON-RPC、WebSocket |
| 钱包层 | 用户实际使用的钱包应用 | 浏览器扩展、移动 App |
三、钱包连接标准
3.1 EIP-1193: Provider API
EIP-1193 定义了 DApp 与钱包通信的标准接口,所有 EVM 钱包都应实现这个协议。
/** EIP-1193 Provider 接口定义 */
interface EIP1193Provider {
/** 发送 JSON-RPC 请求 */
request(args: RequestArguments): Promise<unknown>;
/** 监听事件 */
on(event: string, listener: (...args: unknown[]) => void): void;
/** 移除事件监听 */
removeListener(event: string, listener: (...args: unknown[]) => void): void;
}
interface RequestArguments {
method: string;
params?: unknown[] | Record<string, unknown>;
}
/** 常用 RPC 方法 */
type RPCMethod =
| 'eth_requestAccounts' // 请求连接账户
| 'eth_accounts' // 获取已连接账户
| 'eth_chainId' // 获取当前链 ID
| 'eth_sendTransaction' // 发送交易
| 'personal_sign' // 个人签名
| 'eth_signTypedData_v4' // EIP-712 结构化签名
| 'wallet_switchEthereumChain' // 切换链
| 'wallet_addEthereumChain' // 添加链
| 'wallet_watchAsset'; // 添加代币到钱包
- 所有通信基于 JSON-RPC 2.0 协议
request是唯一的请求方法(取代了旧版的send、sendAsync)- 事件系统支持
accountsChanged、chainChanged、connect、disconnect - Provider 通过
window.ethereum注入到页面(旧方式,已被 EIP-6963 改进)
3.2 EIP-6963: 多钱包发现协议
当用户安装了多个钱包扩展时(如 MetaMask + OKX Wallet),它们会争夺 window.ethereum,导致只有一个钱包可用("钱包覆盖问题")。EIP-6963 通过事件机制解决了这个问题。
/** 钱包信息 */
interface EIP6963ProviderInfo {
uuid: string; // 唯一标识符
name: string; // 钱包名称,如 "MetaMask"
icon: string; // 钱包图标 (data URI)
rdns: string; // 反向域名标识,如 "io.metamask"
}
/** 钱包详情(含 Provider 实例) */
interface EIP6963ProviderDetail {
info: EIP6963ProviderInfo;
provider: EIP1193Provider;
}
/** DApp 发现钱包 */
function discoverWallets(): Promise<EIP6963ProviderDetail[]> {
const wallets: EIP6963ProviderDetail[] = [];
return new Promise((resolve) => {
// 监听钱包响应事件
window.addEventListener('eip6963:announceProvider', (event: Event) => {
const detail = (event as CustomEvent<EIP6963ProviderDetail>).detail;
wallets.push(detail);
});
// DApp 发起发现请求
window.dispatchEvent(new Event('eip6963:requestProvider'));
// 给钱包一些响应时间
setTimeout(() => resolve(wallets), 200);
});
}
3.3 WalletConnect 协议
WalletConnect 是一个开放协议,允许 DApp 与移动端钱包安全通信。v2 版本基于 Relay 中继架构。
| 特性 | WalletConnect v1 | WalletConnect v2 |
|---|---|---|
| 中继架构 | 单一 Bridge Server | 分布式 Relay Network |
| 协议 | WebSocket + 自定义协议 | WebSocket + JSON-RPC |
| 多链支持 | 仅 EVM | EVM + Solana + Cosmos + 更多 |
| 会话管理 | 一个 Session 绑定一条链 | 一个 Session 支持多链 (Namespace) |
| 配对方式 | 二维码 / Deep Link | 二维码 / Deep Link / URI |
| 加密 | 对称加密 (AES-256-CBC) | X25519 + ChaCha20-Poly1305 |
| 状态 | 已废弃 | 当前版本 |
四、连接流程详解
4.1 完整连接流程
4.2 Connector 抽象设计
/** 钱包连接状态 */
type ConnectionStatus = 'disconnected' | 'connecting' | 'connected' | 'reconnecting';
/** 链信息 */
interface Chain {
id: number;
name: string;
rpcUrls: string[];
nativeCurrency: {
name: string;
symbol: string;
decimals: number;
};
blockExplorers?: { name: string; url: string }[];
}
/** 连接结果 */
interface ConnectResult {
accounts: string[];
chainId: number;
}
/** Connector 基类 —— 所有钱包连接器的抽象 */
abstract class BaseConnector {
abstract readonly id: string;
abstract readonly name: string;
protected status: ConnectionStatus = 'disconnected';
protected chains: Chain[];
constructor(chains: Chain[]) {
this.chains = chains;
}
/** 连接钱包 */
abstract connect(params?: { chainId?: number }): Promise<ConnectResult>;
/** 断开连接 */
abstract disconnect(): Promise<void>;
/** 获取当前账户 */
abstract getAccounts(): Promise<string[]>;
/** 获取当前链 ID */
abstract getChainId(): Promise<number>;
/** 获取 Provider 实例 */
abstract getProvider(): Promise<EIP1193Provider>;
/** 切换链 */
abstract switchChain(chainId: number): Promise<Chain>;
/** 监听账户变更 */
abstract onAccountsChanged(callback: (accounts: string[]) => void): void;
/** 监听链变更 */
abstract onChainChanged(callback: (chainId: number) => void): void;
/** 监听断开 */
abstract onDisconnect(callback: (error?: Error) => void): void;
}
4.3 Injected Connector 实现
/** 注入式钱包连接器(MetaMask、OKX 等浏览器扩展钱包) */
class InjectedConnector extends BaseConnector {
readonly id = 'injected';
readonly name = 'Injected';
private provider: EIP1193Provider | null = null;
/** 使用 EIP-6963 发现钱包 */
async detectProviders(): Promise<EIP6963ProviderDetail[]> {
const wallets: EIP6963ProviderDetail[] = [];
return new Promise((resolve) => {
const handler = (event: Event) => {
const detail = (event as CustomEvent<EIP6963ProviderDetail>).detail;
wallets.push(detail);
};
window.addEventListener('eip6963:announceProvider', handler);
window.dispatchEvent(new Event('eip6963:requestProvider'));
setTimeout(() => {
window.removeEventListener('eip6963:announceProvider', handler);
resolve(wallets);
}, 200);
});
}
async connect(params?: { chainId?: number }): Promise<ConnectResult> {
this.status = 'connecting';
try {
const provider = await this.getProvider();
// 请求账户授权(会弹出钱包确认窗口)
const accounts = (await provider.request({
method: 'eth_requestAccounts',
})) as string[];
const chainIdHex = (await provider.request({
method: 'eth_chainId',
})) as string;
let chainId = parseInt(chainIdHex, 16);
// 如果指定了目标链且与当前链不同,则切换
if (params?.chainId && params.chainId !== chainId) {
await this.switchChain(params.chainId);
chainId = params.chainId;
}
// 注册事件监听
this.setupListeners(provider);
this.status = 'connected';
return { accounts, chainId };
} catch (error) {
this.status = 'disconnected';
throw error;
}
}
async switchChain(chainId: number): Promise<Chain> {
const provider = await this.getProvider();
const hexChainId = `0x${chainId.toString(16)}`;
try {
await provider.request({ method: 'wallet_switchEthereumChain', params: [{ chainId: hexChainId }] });
} catch (error: unknown) {
const switchError = error as { code: number };
// 链不存在(错误码 4902),尝试添加
if (switchError.code === 4902) {
const chain = this.chains.find((c) => c.id === chainId);
if (!chain) throw new Error(`Chain ${chainId} not configured`);
await provider.request({
method: 'wallet_addEthereumChain',
params: [{
chainId: hexChainId,
chainName: chain.name,
rpcUrls: chain.rpcUrls,
nativeCurrency: chain.nativeCurrency,
blockExplorerUrls: chain.blockExplorers?.map((e) => e.url),
}],
});
} else {
throw error;
}
}
return this.chains.find((c) => c.id === chainId)!;
}
async disconnect(): Promise<void> {
this.status = 'disconnected';
this.provider = null;
}
async getAccounts(): Promise<string[]> {
const provider = await this.getProvider();
return (await provider.request({ method: 'eth_accounts' })) as string[];
}
async getChainId(): Promise<number> {
const provider = await this.getProvider();
const hex = (await provider.request({ method: 'eth_chainId' })) as string;
return parseInt(hex, 16);
}
async getProvider(): Promise<EIP1193Provider> {
if (this.provider) return this.provider;
if (typeof window !== 'undefined' && window.ethereum) {
this.provider = window.ethereum as EIP1193Provider;
return this.provider;
}
throw new Error('No injected provider found. Please install a wallet extension.');
}
private setupListeners(provider: EIP1193Provider): void {
provider.on('accountsChanged', (accounts: unknown) => {
const accs = accounts as string[];
if (accs.length === 0) {
this.status = 'disconnected';
}
});
provider.on('chainChanged', (_chainId: unknown) => {
// 链切换后通常需要刷新页面状态
});
provider.on('disconnect', () => {
this.status = 'disconnected';
});
}
onAccountsChanged(callback: (accounts: string[]) => void): void {
this.getProvider().then((p) => p.on('accountsChanged', callback as (...args: unknown[]) => void));
}
onChainChanged(callback: (chainId: number) => void): void {
this.getProvider().then((p) =>
p.on('chainChanged', ((hexId: string) => callback(parseInt(hexId, 16))) as (...args: unknown[]) => void)
);
}
onDisconnect(callback: (error?: Error) => void): void {
this.getProvider().then((p) => p.on('disconnect', callback as (...args: unknown[]) => void));
}
}
五、交易签名
5.1 签名类型对比
| 签名类型 | RPC 方法 | 用途 | EIP 标准 |
|---|---|---|---|
| 个人签名 | personal_sign | 登录验证、简单消息签名 | - |
| 结构化签名 | eth_signTypedData_v4 | 链下订单、授权、Permit | EIP-712 |
| 交易签名 | eth_sendTransaction | 转账、合约调用 | - |
5.2 personal_sign(登录签名)
Web3 中最常见的登录方式是 Sign-In with Ethereum (SIWE),通过让用户签名一段消息来证明地址所有权。
/** SIWE 登录签名流程 */
async function signInWithEthereum(
provider: EIP1193Provider,
address: string
): Promise<{ message: string; signature: string }> {
// 1. 从服务端获取 nonce(防止重放攻击)
const nonce = await fetch('/api/auth/nonce').then((r) => r.text());
// 2. 构造 SIWE 消息
const message = [
`login.example.com wants you to sign in with your Ethereum account:`,
address,
'',
'Sign in to Example App',
'',
`URI: https://login.example.com`,
`Version: 1`,
`Chain ID: 1`,
`Nonce: ${nonce}`,
`Issued At: ${new Date().toISOString()}`,
`Expiration Time: ${new Date(Date.now() + 10 * 60 * 1000).toISOString()}`,
].join('\n');
// 3. 请求钱包签名
const signature = (await provider.request({
method: 'personal_sign',
params: [
`0x${Buffer.from(message, 'utf8').toString('hex')}`,
address,
],
})) as string;
// 4. 将签名发送到服务端验证
const verifyRes = await fetch('/api/auth/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message, signature, address }),
});
if (!verifyRes.ok) throw new Error('Signature verification failed');
return { message, signature };
}
5.3 EIP-712 结构化签名
EIP-712 定义了结构化数据的签名标准,让用户在签名时能看到可读的结构化数据,而非一串难以理解的十六进制字符串。
/** EIP-712 类型化数据 - 以 NFT 市场订单为例 */
const typedData = {
// 域信息 - 标识签名来源,防止跨合约重放
domain: {
name: 'NFT Marketplace',
version: '1',
chainId: 1,
verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC' as const,
},
// 类型定义
types: {
Order: [
{ name: 'maker', type: 'address' },
{ name: 'tokenId', type: 'uint256' },
{ name: 'price', type: 'uint256' },
{ name: 'expiry', type: 'uint256' },
{ name: 'nonce', type: 'uint256' },
],
},
// 主类型
primaryType: 'Order' as const,
// 实际数据
message: {
maker: '0xYourAddress...',
tokenId: '1234',
price: '1000000000000000000', // 1 ETH in wei
expiry: '1735689600',
nonce: '0',
},
};
/** 请求 EIP-712 签名 */
async function signTypedData(
provider: EIP1193Provider,
address: string
): Promise<string> {
const signature = (await provider.request({
method: 'eth_signTypedData_v4',
params: [address, JSON.stringify(typedData)],
})) as string;
return signature;
}
- 用户可读:钱包会以结构化方式展示签名内容,用户能看懂在签什么
- 防重放:通过
domain中的chainId和verifyingContract防止签名被跨链/跨合约使用 - 链下可验证:签名结果可在智能合约中用
ecrecover验证,也可在服务端验证 - Gas 优化:链下签名 + 链上验证的模式可节省大量 Gas(如 Permit2)
5.4 发送交易
/** 交易参数 */
interface TransactionRequest {
from: string;
to: string;
value?: string; // Wei 数量(十六进制)
data?: string; // 合约调用数据
gas?: string; // Gas 限制
maxFeePerGas?: string; // EIP-1559 最大费用
maxPriorityFeePerGas?: string; // EIP-1559 优先费
}
/** 发送 ETH 转账交易 */
async function sendTransaction(
provider: EIP1193Provider,
from: string,
to: string,
valueInEther: string
): Promise<string> {
// 将 ETH 转换为 Wei(1 ETH = 10^18 Wei)
const valueInWei = BigInt(Math.floor(parseFloat(valueInEther) * 1e18));
const tx: TransactionRequest = {
from,
to,
value: `0x${valueInWei.toString(16)}`,
};
// 发送交易(钱包会弹出确认窗口)
const txHash = (await provider.request({
method: 'eth_sendTransaction',
params: [tx],
})) as string;
console.log('Transaction hash:', txHash);
return txHash;
}
/** 等待交易确认 */
async function waitForTransaction(
provider: EIP1193Provider,
txHash: string,
confirmations: number = 1
): Promise<boolean> {
return new Promise((resolve) => {
const checkReceipt = async () => {
const receipt = (await provider.request({
method: 'eth_getTransactionReceipt',
params: [txHash],
})) as { status: string; blockNumber: string } | null;
if (receipt) {
// status 为 "0x1" 表示成功
resolve(receipt.status === '0x1');
} else {
setTimeout(checkReceipt, 2000);
}
};
checkReceipt();
});
}
六、多链适配
6.1 Chain 抽象层
/** 链配置定义 */
interface ChainConfig {
id: number;
name: string;
network: string;
nativeCurrency: {
name: string;
symbol: string;
decimals: number;
};
rpcUrls: {
default: { http: string[] };
public: { http: string[] };
};
blockExplorers: {
default: { name: string; url: string };
};
testnet: boolean;
}
/** 预定义的链配置 */
const ethereum: ChainConfig = {
id: 1,
name: 'Ethereum',
network: 'homestead',
nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
rpcUrls: {
default: { http: ['https://eth.llamarpc.com'] },
public: { http: ['https://eth.llamarpc.com'] },
},
blockExplorers: {
default: { name: 'Etherscan', url: 'https://etherscan.io' },
},
testnet: false,
};
const polygon: ChainConfig = {
id: 137,
name: 'Polygon',
network: 'matic',
nativeCurrency: { name: 'MATIC', symbol: 'MATIC', decimals: 18 },
rpcUrls: {
default: { http: ['https://polygon-rpc.com'] },
public: { http: ['https://polygon-rpc.com'] },
},
blockExplorers: {
default: { name: 'PolygonScan', url: 'https://polygonscan.com' },
},
testnet: false,
};
const bsc: ChainConfig = {
id: 56,
name: 'BNB Smart Chain',
network: 'bsc',
nativeCurrency: { name: 'BNB', symbol: 'BNB', decimals: 18 },
rpcUrls: {
default: { http: ['https://bsc-dataseed.binance.org'] },
public: { http: ['https://bsc-dataseed.binance.org'] },
},
blockExplorers: {
default: { name: 'BscScan', url: 'https://bscscan.com' },
},
testnet: false,
};
6.2 多链 Provider 管理
/** 链生态类型 */
type ChainEcosystem = 'evm' | 'solana' | 'ton';
/** 统一的多链账户信息 */
interface MultiChainAccount {
ecosystem: ChainEcosystem;
chainId: number | string;
address: string;
connected: boolean;
}
/** 多链管理器 */
class MultiChainManager {
private connectors: Map<ChainEcosystem, BaseConnector> = new Map();
private activeEcosystem: ChainEcosystem = 'evm';
/** 注册链生态连接器 */
registerConnector(ecosystem: ChainEcosystem, connector: BaseConnector): void {
this.connectors.set(ecosystem, connector);
}
/** 连接指定生态 */
async connect(ecosystem: ChainEcosystem): Promise<MultiChainAccount> {
const connector = this.connectors.get(ecosystem);
if (!connector) throw new Error(`No connector for ${ecosystem}`);
const result = await connector.connect();
this.activeEcosystem = ecosystem;
return {
ecosystem,
chainId: result.chainId,
address: result.accounts[0],
connected: true,
};
}
/** 切换链(EVM 生态内切换) */
async switchChain(chainId: number): Promise<void> {
const connector = this.connectors.get('evm');
if (!connector) throw new Error('EVM connector not found');
await connector.switchChain(chainId);
}
/** 获取当前连接信息 */
async getCurrentAccount(): Promise<MultiChainAccount | null> {
const connector = this.connectors.get(this.activeEcosystem);
if (!connector) return null;
try {
const accounts = await connector.getAccounts();
const chainId = await connector.getChainId();
return {
ecosystem: this.activeEcosystem,
chainId,
address: accounts[0],
connected: true,
};
} catch {
return null;
}
}
}
七、状态管理
7.1 钱包状态设计
/** 钱包全局状态 */
interface WalletState {
/** 连接状态 */
status: ConnectionStatus;
/** 当前连接的钱包 ID */
connectorId: string | null;
/** 当前账户地址列表 */
accounts: string[];
/** 当前链 ID */
chainId: number | null;
/** 错误信息 */
error: Error | null;
}
/** 状态操作 */
interface WalletActions {
connect: (connectorId: string, chainId?: number) => Promise<void>;
disconnect: () => Promise<void>;
switchChain: (chainId: number) => Promise<void>;
}
/** 使用 Zustand 管理钱包状态(简化示例) */
function createWalletStore() {
let state: WalletState = {
status: 'disconnected',
connectorId: null,
accounts: [],
chainId: null,
error: null,
};
const listeners = new Set<(state: WalletState) => void>();
function setState(partial: Partial<WalletState>): void {
state = { ...state, ...partial };
listeners.forEach((listener) => listener(state));
}
function subscribe(listener: (state: WalletState) => void): () => void {
listeners.add(listener);
return () => listeners.delete(listener);
}
function getState(): WalletState {
return state;
}
return { setState, subscribe, getState };
}
7.2 事件监听与自动重连
/** 事件监听管理 */
class WalletEventManager {
private provider: EIP1193Provider | null = null;
private store: ReturnType<typeof createWalletStore>;
constructor(store: ReturnType<typeof createWalletStore>) {
this.store = store;
}
/** 绑定 Provider 事件 */
bindEvents(provider: EIP1193Provider): void {
this.provider = provider;
// 账户变更(用户在钱包中切换账户)
provider.on('accountsChanged', (accounts: unknown) => {
const accs = accounts as string[];
if (accs.length === 0) {
// 用户在钱包中断开了连接
this.store.setState({
status: 'disconnected',
accounts: [],
connectorId: null,
});
} else {
this.store.setState({ accounts: accs });
}
});
// 链变更(用户在钱包中切换网络)
provider.on('chainChanged', (chainId: unknown) => {
const id = parseInt(chainId as string, 16);
this.store.setState({ chainId: id });
});
// 断开连接
provider.on('disconnect', (_error: unknown) => {
this.store.setState({
status: 'disconnected',
accounts: [],
chainId: null,
connectorId: null,
});
});
}
/** 清除事件监听 */
unbindEvents(): void {
this.provider = null;
}
}
/** 自动重连逻辑 */
class AutoReconnect {
private static STORAGE_KEY = 'wallet_last_connector';
/** 保存最后连接的钱包信息 */
static save(connectorId: string): void {
try {
localStorage.setItem(this.STORAGE_KEY, JSON.stringify({
connectorId,
timestamp: Date.now(),
}));
} catch {
// localStorage 不可用时静默失败
}
}
/** 尝试自动重连 */
static async tryReconnect(
connectors: Map<string, BaseConnector>
): Promise<ConnectResult | null> {
try {
const saved = localStorage.getItem(this.STORAGE_KEY);
if (!saved) return null;
const { connectorId, timestamp } = JSON.parse(saved);
// 超过 7 天不自动重连
if (Date.now() - timestamp > 7 * 24 * 60 * 60 * 1000) {
localStorage.removeItem(this.STORAGE_KEY);
return null;
}
const connector = connectors.get(connectorId);
if (!connector) return null;
return await connector.connect();
} catch {
localStorage.removeItem(this.STORAGE_KEY);
return null;
}
}
/** 清除重连信息 */
static clear(): void {
localStorage.removeItem(this.STORAGE_KEY);
}
}
八、主流方案对比
8.1 方案概览
- wagmi + viem
- ethers.js
- web3-react
wagmi 是目前最主流的 React Web3 库,底层使用 viem 作为以太坊交互层。
import { createConfig, http } from 'wagmi';
import { mainnet, polygon, bsc } from 'wagmi/chains';
import { injected, walletConnect } from 'wagmi/connectors';
const config = createConfig({
chains: [mainnet, polygon, bsc],
connectors: [
injected(),
walletConnect({ projectId: 'YOUR_PROJECT_ID' }),
],
transports: {
[mainnet.id]: http('https://eth.llamarpc.com'),
[polygon.id]: http('https://polygon-rpc.com'),
[bsc.id]: http('https://bsc-dataseed.binance.org'),
},
});
优势:类型安全、自动缓存、SSR 支持、React Query 集成
ethers.js 是老牌以太坊库,API 更传统,适合非 React 项目。
import { BrowserProvider, formatEther } from 'ethers';
async function connectWithEthers(): Promise<void> {
if (!window.ethereum) throw new Error('No wallet found');
const provider = new BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
const address = await signer.getAddress();
const balance = await provider.getBalance(address);
console.log('Address:', address);
console.log('Balance:', formatEther(balance), 'ETH');
}
优势:API 简洁、文档完善、不依赖框架
web3-react 是 Uniswap 团队开发的 React 库,基于 Connector 架构。
import { initializeConnector } from '@web3-react/core';
import { MetaMask } from '@web3-react/metamask';
const [metaMask, hooks] = initializeConnector<MetaMask>(
(actions) => new MetaMask({ actions })
);
const {
useChainId,
useAccounts,
useIsActivating,
useIsActive,
useProvider,
} = hooks;
优势:轻量、Connector 模式灵活、Uniswap 实战验证
8.2 方案对比表
| 特性 | wagmi + viem | ethers.js | web3-react |
|---|---|---|---|
| 框架依赖 | React | 无 | React |
| TypeScript | 原生支持,类型极强 | v6 原生支持 | 原生支持 |
| 多链支持 | 内置,配置声明式 | 手动管理 | 手动管理 |
| 自动重连 | 内置 | 需自行实现 | 需自行实现 |
| SSR 支持 | 内置 | 不适用 | 有限 |
| 缓存策略 | React Query 自动缓存 | 无 | 无 |
| WalletConnect | 内置 Connector | 需集成 | 需安装 Connector |
| 包大小 | wagmi ~30KB + viem ~30KB | ~120KB | ~15KB(核心) |
| 学习曲线 | 中等 | 低 | 中等 |
| 维护团队 | wevm (独立团队) | Ricmoo (个人) | Uniswap |
| 社区活跃度 | 非常活跃 | 活跃 | 一般 |
- React + 新项目:首选 wagmi + viem + RainbowKit/ConnectKit
- 非 React 项目:使用 viem(替代 ethers.js,性能更好、类型更强)
- 已有 ethers.js 项目:可继续使用,无需强制迁移
- 需要极致定制:使用 web3-react 或直接基于 EIP-1193 开发
8.3 wagmi React Hooks 用法
import { useAccount, useConnect, useDisconnect, useSwitchChain, useSignMessage, useSendTransaction } from 'wagmi';
/** 钱包连接组件示例 */
function WalletConnector() {
const { address, isConnected, chain } = useAccount();
const { connect, connectors, isPending } = useConnect();
const { disconnect } = useDisconnect();
const { switchChain } = useSwitchChain();
if (isConnected) {
return {
address,
chainId: chain?.id,
chainName: chain?.name,
disconnect: () => disconnect(),
switchChain: (chainId: number) => switchChain({ chainId }),
};
}
return {
connectors: connectors.map((connector) => ({
id: connector.id,
name: connector.name,
connect: () => connect({ connector }),
})),
isPending,
};
}
/** 消息签名 Hook */
function useWalletSign() {
const { signMessageAsync } = useSignMessage();
const signLogin = async (message: string): Promise<string> => {
const signature = await signMessageAsync({ message });
return signature;
};
return { signLogin };
}
/** 发送交易 Hook */
function useWalletTransaction() {
const { sendTransactionAsync } = useSendTransaction();
const sendETH = async (to: string, valueInEther: string): Promise<string> => {
const value = BigInt(Math.floor(parseFloat(valueInEther) * 1e18));
const hash = await sendTransactionAsync({ to: to as `0x${string}`, value });
return hash;
};
return { sendETH };
}
九、WalletConnect v2
9.1 核心架构
9.2 集成示例
import { WalletConnectConnector } from 'wagmi/connectors/walletConnect';
/** WalletConnect v2 配置 */
const walletConnectConfig = {
projectId: 'YOUR_WALLETCONNECT_PROJECT_ID', // 从 cloud.walletconnect.com 获取
metadata: {
name: 'My DApp',
description: 'A Web3 Application',
url: 'https://mydapp.com',
icons: ['https://mydapp.com/icon.png'],
},
// 请求的链命名空间
namespaces: {
eip155: {
chains: ['eip155:1', 'eip155:137'], // Ethereum + Polygon
methods: [
'eth_sendTransaction',
'personal_sign',
'eth_signTypedData_v4',
],
events: ['accountsChanged', 'chainChanged'],
},
},
// 可选配置
showQrModal: true, // 是否显示内置二维码弹窗
qrModalOptions: {
themeMode: 'dark' as const,
},
};
/** 移动端 Deep Link 处理 */
function openWalletApp(wcUri: string, walletScheme: string): void {
// 构造 Deep Link
const encodedUri = encodeURIComponent(wcUri);
const deepLink = `${walletScheme}://wc?uri=${encodedUri}`;
// 检测是否在移动端
const isMobile = /Android|iPhone|iPad/i.test(navigator.userAgent);
if (isMobile) {
window.location.href = deepLink;
}
}
/** 常见钱包的 Deep Link Scheme */
const walletSchemes: Record<string, string> = {
metamask: 'metamask',
trust: 'trust',
rainbow: 'rainbow',
imtoken: 'imtokenv2',
};
9.3 会话管理
/** WalletConnect 会话持久化 */
class WCSessionManager {
private static STORAGE_KEY = 'wc_session';
/** 保存会话 */
static saveSession(session: {
topic: string;
expiry: number;
accounts: string[];
chains: string[];
}): void {
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(session));
}
/** 恢复会话 */
static restoreSession(): {
topic: string;
expiry: number;
accounts: string[];
chains: string[];
} | null {
const saved = localStorage.getItem(this.STORAGE_KEY);
if (!saved) return null;
const session = JSON.parse(saved);
// 检查会话是否过期
if (session.expiry < Date.now() / 1000) {
this.clearSession();
return null;
}
return session;
}
/** 清除会话 */
static clearSession(): void {
localStorage.removeItem(this.STORAGE_KEY);
}
}
十、安全考虑
10.1 常见攻击方式
10.2 安全防护实现
/** 域名验证 - 防止钓鱼网站 */
function validateDomain(): { safe: boolean; warnings: string[] } {
const warnings: string[] = [];
const hostname = window.location.hostname;
// 1. 检查是否 HTTPS
if (window.location.protocol !== 'https:') {
warnings.push('Connection is not secure (HTTP)');
}
// 2. 检查同形异义字攻击(Punycode)
if (hostname !== hostname.normalize('NFKC')) {
warnings.push('Domain contains suspicious unicode characters');
}
// 3. 检查是否为已知钓鱼域名模式
const suspiciousPatterns = [
/metamask.*\.(com|io|org)$/i, // 仿冒 MetaMask
/uniswap.*\.(com|io|org)$/i, // 仿冒 Uniswap
];
for (const pattern of suspiciousPatterns) {
if (pattern.test(hostname) && !isOfficialDomain(hostname)) {
warnings.push(`Domain ${hostname} resembles a known protocol`);
}
}
return { safe: warnings.length === 0, warnings };
}
function isOfficialDomain(hostname: string): boolean {
const officialDomains = ['metamask.io', 'app.uniswap.org'];
return officialDomains.includes(hostname);
}
/** 交易预览 - 解析交易意图 */
interface TransactionPreview {
type: 'transfer' | 'approve' | 'swap' | 'mint' | 'unknown';
description: string;
risk: 'low' | 'medium' | 'high' | 'critical';
details: Record<string, string>;
}
function previewTransaction(tx: TransactionRequest): TransactionPreview {
const data = tx.data || '0x';
// 取函数选择器(前 4 字节,即 10 个字符:0x + 8 hex chars)
const selector = data.slice(0, 10);
// ERC-20 approve 函数选择器
if (selector === '0x095ea7b3') {
const spender = '0x' + data.slice(34, 74);
const amount = BigInt('0x' + data.slice(74, 138));
// 检查是否为无限授权
const isUnlimitedApproval = amount >= BigInt(2) ** BigInt(128);
return {
type: 'approve',
description: isUnlimitedApproval
? 'Unlimited token approval (HIGH RISK)'
: 'Token approval',
risk: isUnlimitedApproval ? 'high' : 'medium',
details: {
spender,
amount: isUnlimitedApproval ? 'Unlimited' : amount.toString(),
},
};
}
// ETH 转账(无 data)
if (data === '0x' && tx.value) {
return {
type: 'transfer',
description: 'ETH transfer',
risk: 'low',
details: {
to: tx.to,
value: tx.value,
},
};
}
return {
type: 'unknown',
description: 'Unknown contract interaction',
risk: 'high',
details: { data: data.slice(0, 100) + '...' },
};
}
/** 签名风险提示 */
function assessSignatureRisk(
method: string,
params: unknown[]
): { risk: 'low' | 'medium' | 'high'; message: string } {
switch (method) {
case 'personal_sign':
return { risk: 'low', message: 'Signing a plain text message. This cannot move funds.' };
case 'eth_signTypedData_v4': {
const typedData = JSON.parse(params[1] as string);
// Permit 签名可以授权代币转移,风险较高
if (typedData.primaryType === 'Permit') {
return {
risk: 'high',
message: 'This is a Permit signature that can authorize token transfers without a transaction.',
};
}
return { risk: 'medium', message: 'Signing structured data. Review the content carefully.' };
}
case 'eth_sendTransaction':
return { risk: 'high', message: 'This will send a transaction that may transfer funds.' };
default:
return { risk: 'medium', message: 'Unknown signing request.' };
}
}
以下签名操作需要特别警惕:
- Permit / Permit2 签名:链下签名即可授权代币转移,无需链上 approve 交易
- 无限授权 (Unlimited Approval):一旦合约被攻破,可转走用户所有授权代币
- eth_sign:对原始哈希签名,用户完全无法理解签名内容,应禁止使用
十一、账户抽象 (AA)
11.1 ERC-4337 架构
传统 EOA 钱包的局限性:必须持有 ETH 才能发交易、私钥丢失无法恢复、不支持批量交易。ERC-4337 引入了账户抽象,让智能合约账户拥有与 EOA 相同的一等公民地位。
| 组件 | 说明 |
|---|---|
| UserOperation | 用户操作对象(类似交易),包含 sender、callData、签名等字段 |
| Bundler | 收集多个 UserOp,打包成一笔链上交易提交给 EntryPoint |
| EntryPoint | 单例合约,验证 UserOp 并调用目标钱包合约执行操作 |
| Smart Contract Wallet | 用户的智能合约钱包,可自定义验证逻辑(多签、社交恢复等) |
| Paymaster | 代付 Gas 的合约,让用户无需持有 ETH 就能发交易 |
11.2 AA 钱包集成
/** UserOperation 结构 */
interface UserOperation {
sender: string; // 智能合约钱包地址
nonce: string; // 防重放
initCode: string; // 首次创建钱包的工厂代码
callData: string; // 要执行的操作
callGasLimit: string;
verificationGasLimit: string;
preVerificationGas: string;
maxFeePerGas: string;
maxPriorityFeePerGas: string;
paymasterAndData: string; // Paymaster 相关数据
signature: string; // 签名
}
/** 简化的 AA 钱包客户端 */
class SmartAccountClient {
private entryPointAddress: string;
private bundlerUrl: string;
private paymasterUrl: string;
constructor(config: {
entryPoint: string;
bundler: string;
paymaster: string;
}) {
this.entryPointAddress = config.entryPoint;
this.bundlerUrl = config.bundler;
this.paymasterUrl = config.paymaster;
}
/** 发送 UserOperation */
async sendUserOperation(
userOp: Partial<UserOperation>
): Promise<string> {
// 1. 估算 Gas
const gasEstimate = await this.estimateGas(userOp);
// 2. 请求 Paymaster 赞助(如果配置了)
const paymasterData = await this.sponsorUserOp(userOp);
// 3. 组装完整的 UserOp
const fullUserOp: UserOperation = {
...userOp,
...gasEstimate,
paymasterAndData: paymasterData,
} as UserOperation;
// 4. 签名
// (签名逻辑取决于 Signer 类型:EOA、Passkey、社交登录等)
// 5. 发送到 Bundler
const userOpHash = await this.submitToBundler(fullUserOp);
return userOpHash;
}
/** 批量交易(AA 的核心优势之一) */
async sendBatchTransactions(
calls: Array<{ to: string; value: string; data: string }>
): Promise<string> {
// 编码为 executeBatch 调用
const callData = this.encodeBatchCall(calls);
return this.sendUserOperation({
callData,
});
}
private async estimateGas(
_userOp: Partial<UserOperation>
): Promise<Partial<UserOperation>> {
const response = await fetch(this.bundlerUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0',
method: 'eth_estimateUserOperationGas',
params: [_userOp, this.entryPointAddress],
id: 1,
}),
});
return (await response.json()).result;
}
private async sponsorUserOp(
_userOp: Partial<UserOperation>
): Promise<string> {
const response = await fetch(this.paymasterUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0',
method: 'pm_sponsorUserOperation',
params: [_userOp, this.entryPointAddress],
id: 1,
}),
});
return (await response.json()).result.paymasterAndData;
}
private async submitToBundler(userOp: UserOperation): Promise<string> {
const response = await fetch(this.bundlerUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0',
method: 'eth_sendUserOperation',
params: [userOp, this.entryPointAddress],
id: 1,
}),
});
return (await response.json()).result;
}
private encodeBatchCall(
_calls: Array<{ to: string; value: string; data: string }>
): string {
// 实际实现需要 ABI 编码
return '0x';
}
}
11.3 社交登录集成
/** 社交登录类型 */
type SocialLoginProvider = 'google' | 'apple' | 'twitter' | 'email' | 'passkey';
/** 嵌入式钱包配置(以 Privy 为例) */
interface EmbeddedWalletConfig {
appId: string;
loginMethods: SocialLoginProvider[];
embeddedWallets: {
createOnLogin: 'all-users' | 'users-without-wallets';
};
chains: ChainConfig[];
}
/** 社交登录流程 */
async function socialLogin(
provider: SocialLoginProvider
): Promise<{ address: string; chainId: number }> {
// 1. 用户通过 OAuth 登录(Google、Apple 等)
// 2. SDK 在后台创建/恢复用户的密钥分片(MPC 或 SSS)
// 3. 用密钥分片生成智能合约钱包
// 4. 返回钱包地址
// 实际使用时通过 SDK 实现,如:
// const { user } = await privy.login({ loginMethod: provider });
// const wallet = user.wallet;
// return { address: wallet.address, chainId: wallet.chainId };
return { address: '0x...', chainId: 1 };
}
| 特性 | EOA 钱包 | AA 钱包 |
|---|---|---|
| Gas 支付 | 必须持有原生代币 | 可由 Paymaster 代付或用 ERC-20 支付 |
| 批量交易 | 每笔操作需单独交易 | 支持批量执行(一次签名多笔操作) |
| 账户恢复 | 私钥丢失即永久丢失 | 支持社交恢复、多签恢复 |
| 签名验证 | 仅支持 ECDSA | 可自定义(Passkey、多签、MPC 等) |
| 登录方式 | 必须安装钱包 | 支持邮箱、社交账号、Passkey |
| 用户门槛 | 高(需理解私钥、Gas 等) | 低(接近 Web2 体验) |
十二、性能优化
12.1 连接性能优化
| 优化策略 | 说明 | 效果 |
|---|---|---|
| EIP-6963 优先 | 优先使用事件驱动的钱包发现,避免轮询 window.ethereum | 发现速度 < 200ms |
| Provider 缓存 | 缓存已初始化的 Provider 实例,避免重复创建 | 二次连接 < 50ms |
| 自动重连 | 记住上次连接的钱包,页面刷新后自动重连 | 用户体验提升 |
| 懒加载 Connector | WalletConnect SDK 体积较大,按需加载 | 减少首屏 JS 约 80KB |
| 连接超时处理 | 设置合理的连接超时(如 30s),避免无限等待 | 降低用户流失 |
12.2 RPC 请求优化
/** RPC 请求批量处理 */
class RPCBatcher {
private queue: Array<{
method: string;
params: unknown[];
resolve: (value: unknown) => void;
reject: (error: Error) => void;
}> = [];
private timer: ReturnType<typeof setTimeout> | null = null;
private rpcUrl: string;
constructor(rpcUrl: string) {
this.rpcUrl = rpcUrl;
}
/** 添加请求到批量队列 */
request(method: string, params: unknown[] = []): Promise<unknown> {
return new Promise((resolve, reject) => {
this.queue.push({ method, params, resolve, reject });
// 微任务合并:同一事件循环内的请求合并为一次批量请求
if (!this.timer) {
this.timer = setTimeout(() => this.flush(), 0);
}
});
}
/** 执行批量请求 */
private async flush(): Promise<void> {
const batch = [...this.queue];
this.queue = [];
this.timer = null;
if (batch.length === 0) return;
// 构造 JSON-RPC 批量请求
const requests = batch.map((item, index) => ({
jsonrpc: '2.0' as const,
id: index + 1,
method: item.method,
params: item.params,
}));
try {
const response = await fetch(this.rpcUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(requests),
});
const results = (await response.json()) as Array<{
id: number;
result?: unknown;
error?: { code: number; message: string };
}>;
// 将结果分发给各个请求
for (const result of results) {
const item = batch[result.id - 1];
if (result.error) {
item.reject(new Error(result.error.message));
} else {
item.resolve(result.result);
}
}
} catch (error) {
batch.forEach((item) => item.reject(error as Error));
}
}
}
12.3 WalletConnect SDK 懒加载
/** 懒加载 WalletConnect Connector */
async function createWalletConnectConnector(
projectId: string
): Promise<BaseConnector> {
// 仅在用户选择 WalletConnect 时才加载 SDK(约 80KB gzip)
const { WalletConnectConnector } = await import(
/* webpackChunkName: "walletconnect" */
'@walletconnect/ethereum-provider'
);
// 初始化连接器
const connector = new WalletConnectConnector({
projectId,
chains: [1, 137, 56],
showQrModal: true,
});
return connector as unknown as BaseConnector;
}
/** Connector 注册表(支持懒加载) */
const connectorRegistry: Record<string, () => Promise<BaseConnector>> = {
injected: async () => new InjectedConnector([]),
walletconnect: () => createWalletConnectConnector('YOUR_PROJECT_ID'),
};
十三、扩展设计
13.1 插件化架构
/** 钱包插件接口 */
interface WalletPlugin {
name: string;
/** 连接前钩子 */
beforeConnect?(connector: BaseConnector): Promise<void>;
/** 连接后钩子 */
afterConnect?(result: ConnectResult): Promise<void>;
/** 交易前钩子(可用于交易预览、安全检查) */
beforeTransaction?(tx: TransactionRequest): Promise<TransactionRequest>;
/** 签名前钩子 */
beforeSign?(method: string, params: unknown[]): Promise<unknown[]>;
/** 错误处理钩子 */
onError?(error: Error): void;
}
/** 插件管理器 */
class PluginManager {
private plugins: WalletPlugin[] = [];
use(plugin: WalletPlugin): void {
this.plugins.push(plugin);
}
async runBeforeConnect(connector: BaseConnector): Promise<void> {
for (const plugin of this.plugins) {
await plugin.beforeConnect?.(connector);
}
}
async runBeforeTransaction(tx: TransactionRequest): Promise<TransactionRequest> {
let result = tx;
for (const plugin of this.plugins) {
if (plugin.beforeTransaction) {
result = await plugin.beforeTransaction(result);
}
}
return result;
}
}
/** 安全审计插件示例 */
const securityPlugin: WalletPlugin = {
name: 'security-audit',
async beforeTransaction(tx) {
const preview = previewTransaction(tx);
if (preview.risk === 'critical') {
throw new Error('Transaction blocked: critical risk detected');
}
return tx;
},
async beforeSign(method, params) {
const risk = assessSignatureRisk(method, params);
if (risk.risk === 'high') {
console.warn('High-risk signature:', risk.message);
}
return params;
},
};
/** 日志追踪插件示例 */
const analyticsPlugin: WalletPlugin = {
name: 'analytics',
async afterConnect(result) {
// 上报连接成功事件
console.log('Wallet connected', {
accounts: result.accounts.length,
chainId: result.chainId,
});
},
onError(error) {
console.error('Wallet error', { message: error.message });
},
};
13.2 多链 DApp 架构模式
常见面试问题
Q1: 请描述 Web3 DApp 中钱包连接的完整流程
答案:
Web3 钱包连接流程分为以下几个关键步骤:
1. 钱包检测:
- 优先使用 EIP-6963 事件机制发现已安装的钱包
- 降级检查
window.ethereum是否存在 - 如果未检测到钱包,引导用户安装或使用 WalletConnect
2. 连接请求:
- 用户选择钱包后,调用
eth_requestAccountsRPC 方法 - 钱包弹出授权窗口,用户确认连接
3. 获取信息:
- 连接成功后获取
accounts(账户地址数组)和chainId(当前链 ID) - 如果需要特定链,调用
wallet_switchEthereumChain切换
4. 事件监听:
- 监听
accountsChanged:用户在钱包中切换账户 - 监听
chainChanged:用户在钱包中切换网络 - 监听
disconnect:连接断开
5. 持久化与重连:
- 将连接信息存储到 localStorage
- 页面刷新时自动尝试重连
async function connectWallet(): Promise<void> {
// 1. 发现钱包
const wallets = await discoverWallets(); // EIP-6963
// 2. 用户选择钱包 + 连接
const provider = selectedWallet.provider;
const accounts = await provider.request({ method: 'eth_requestAccounts' });
const chainId = await provider.request({ method: 'eth_chainId' });
// 3. 切换到目标链(如果需要)
if (targetChainId !== parseInt(chainId, 16)) {
await provider.request({
method: 'wallet_switchEthereumChain',
params: [{ chainId: `0x${targetChainId.toString(16)}` }],
});
}
// 4. 注册事件监听
provider.on('accountsChanged', handleAccountsChanged);
provider.on('chainChanged', handleChainChanged);
// 5. 持久化
localStorage.setItem('lastConnector', selectedWallet.info.rdns);
}
Q2: 如何实现多链切换?EVM 链切换和非 EVM 链切换有什么区别?
答案:
EVM 链切换基于 EIP-3326 标准,通过 RPC 方法实现:
async function switchEVMChain(provider: EIP1193Provider, chainId: number): Promise<void> {
const hexChainId = `0x${chainId.toString(16)}`;
try {
// 尝试切换到已有的链
await provider.request({
method: 'wallet_switchEthereumChain',
params: [{ chainId: hexChainId }],
});
} catch (error) {
// 如果链不存在(错误码 4902),先添加再切换
if ((error as { code: number }).code === 4902) {
await provider.request({
method: 'wallet_addEthereumChain',
params: [{
chainId: hexChainId,
chainName: 'Polygon',
rpcUrls: ['https://polygon-rpc.com'],
nativeCurrency: { name: 'MATIC', symbol: 'MATIC', decimals: 18 },
}],
});
}
}
}
非 EVM 链切换无法通过 EIP 标准方法实现,因为 Solana、TON 等链使用完全不同的协议和钱包:
| 维度 | EVM 链切换 | 非 EVM 链切换 |
|---|---|---|
| 标准 | EIP-3326 统一方法 | 无统一标准 |
| Provider | 共享同一个 Provider | 需要不同的 Provider/Adapter |
| 地址格式 | 通用(0x 开头,20 字节) | 各链不同(Solana: Base58, TON: Base64) |
| 实现方式 | 单个 RPC 调用 | 需断开当前链,连接目标链 |
| 用户体验 | 钱包内弹窗确认 | 可能需要切换钱包 App |
最佳实践是在 Adapter 层做链抽象,对上层暴露统一的 switchChain(ecosystem, chainId) 接口。
Q3: EIP-712 签名的作用是什么?为什么比 personal_sign 更安全?
答案:
EIP-712 定义了结构化数据的签名标准,核心优势体现在三个方面:
1. 用户可读性:
personal_sign签名的是一段纯文本或十六进制字符串,用户很难理解内容- EIP-712 签名时,钱包会以结构化表格形式展示数据(如 "授权 100 USDC 给 Uniswap"),用户清楚知道在签什么
2. 防重放攻击:
const domain = {
name: 'MyProtocol', // 协议名称
version: '1', // 版本
chainId: 1, // 链 ID(防跨链重放)
verifyingContract: '0xContractAddr', // 合约地址(防跨合约重放)
};
3. 链上高效验证:
- EIP-712 签名可以在智能合约中通过
ecrecover高效验证 - 实现了链下签名 + 链上验证的模式,广泛用于:
- Permit(ERC-2612):无需 approve 交易即可授权代币
- 链下订单簿:OpenSea、Blur 等 NFT 市场
- 元交易(Meta Transaction):用户签名,Relayer 代付 Gas
| 对比 | personal_sign | EIP-712 |
|---|---|---|
| 签名内容 | 纯文本/十六进制 | 结构化 JSON |
| 用户可读性 | 差 | 好(钱包友好展示) |
| 重放防护 | 需自行在消息中包含 nonce | 内置 domain 分隔符 |
| 链上验证 | 可以但不推荐 | 原生支持 |
| 典型用途 | 登录签名(SIWE) | Permit、订单、授权 |
Q4: 如何防止 Web3 钓鱼攻击?
答案:
Web3 钓鱼攻击的防护需要从前端 DApp、钱包、用户教育三个层面入手:
1. 前端 DApp 层面:
// 1. 域名验证
function checkDomain(): boolean {
// 检查 HTTPS
if (location.protocol !== 'https:') return false;
// 检查 Punycode 同形异义字
if (location.hostname !== location.hostname.normalize('NFKC')) return false;
return true;
}
// 2. 交易预览 - 解析合约调用意图
function previewBeforeSign(tx: TransactionRequest): string {
const selector = tx.data?.slice(0, 10);
// 识别高危操作(如无限授权、setApprovalForAll)
if (selector === '0x095ea7b3') return 'ERC-20 Approve';
if (selector === '0xa22cb465') return 'NFT setApprovalForAll (HIGH RISK)';
return 'Unknown';
}
// 3. 合约地址校验
async function verifyContract(address: string): Promise<boolean> {
// 查询合约是否经过安全审计(通过安全 API 如 GoPlus)
const response = await fetch(
`https://api.gopluslabs.io/api/v1/approval_security/1?contract_addresses=${address}`
);
const data = await response.json();
return data.result[address]?.is_open_source === '1';
}
2. 钱包层面:
- 交易模拟:在用户确认前模拟执行交易,展示资产变动预览
- 域名黑名单:钱包内置钓鱼网站黑名单(如 MetaMask 的 PhishFort)
- 签名分类提醒:区分
personal_sign(安全)和eth_sign(危险)
3. 常见钓鱼手法与防护:
| 攻击手法 | 说明 | 防护措施 |
|---|---|---|
| 假冒网站 | 仿造知名 DApp 界面 | 域名验证、收藏官方链接 |
| 恶意授权 | 诱导用户签无限 Approve | 交易预览、限额授权 |
| 地址投毒 | 发送小额交易伪造相似地址 | 地址簿、完整地址校验 |
| Permit 钓鱼 | 诱导签 EIP-2612 Permit | 签名内容解析、风险提示 |
| NFT 空投钓鱼 | 发送含恶意合约交互的 NFT | 不与未知 NFT 交互 |
Q5: 什么是账户抽象(ERC-4337)?它解决了什么问题?
答案:
账户抽象(Account Abstraction, AA)是以太坊的重要升级方向,通过 ERC-4337 标准在协议层之上实现,无需修改以太坊共识层。
解决的核心问题:
- Gas 门槛:传统 EOA 必须持有 ETH 才能发交易。AA 通过 Paymaster 实现 Gas 代付,用户甚至可以用 USDC 支付 Gas
- 私钥风险:EOA 私钥丢失 = 资产永久丢失。AA 钱包支持社交恢复(如 3/5 多签恢复)
- 用户体验:安装钱包、备份助记词、理解 Gas 等门槛过高。AA 支持邮箱/社交账号登录,接近 Web2 体验
- 交易灵活性:EOA 每笔操作需单独交易。AA 支持批量交易(一次签名执行多笔操作)
ERC-4337 工作流程:
用户操作 → 构造 UserOperation → Bundler 打包 → EntryPoint 验证 → 智能合约钱包执行
↓
Paymaster 代付 Gas
主流 AA 方案对比:
| 方案 | 特点 | 适用场景 |
|---|---|---|
| Privy | 社交登录 + 嵌入式钱包 + AA | 面向 Web2 用户的 DApp |
| Safe (Gnosis) | 多签钱包,最成熟的合约钱包 | DAO 资金管理、团队钱包 |
| ZeroDev | Kernel 框架,模块化 AA | 需要高度定制的 DApp |
| Biconomy | SDK + Paymaster + Bundler 全套 | 快速集成 AA 功能 |
| Alchemy AA | 与 Alchemy 基础设施深度集成 | 已使用 Alchemy RPC 的项目 |
Q6: wagmi + viem 和 ethers.js 的主要区别是什么?如何选型?
答案:
| 维度 | wagmi + viem | ethers.js v6 |
|---|---|---|
| 架构 | React Hooks + 纯函数库 | 面向对象(Provider/Signer/Contract) |
| 类型安全 | 极强(ABI 级别类型推导) | 较好(v6 改进显著) |
| Tree Shaking | 优秀(viem 纯函数设计) | 一般(类实例不利于 shake) |
| 包大小 | wagmi ~30KB + viem ~30KB | ~120KB |
| 缓存 | 内置 React Query 缓存 | 无内置缓存 |
| 多链 | 声明式配置,内置多链支持 | 需手动管理多个 Provider |
| SSR | 原生支持 | 依赖 window,需额外处理 |
| Connector | 内置 Injected/WC/Coinbase 等 | 需自行集成 |
| 学习曲线 | 中等(需了解 Hooks 和 viem) | 低(API 直观) |
选型建议:
// 新 React 项目 → wagmi + viem + RainbowKit
// 非 React 项目 → viem(替代 ethers.js)
// 已有 ethers.js 项目 → 无需强制迁移
// 极致定制需求 → 直接使用 EIP-1193 + viem
// wagmi 示例:10 行代码完成钱包连接 + 链切换 + 签名
import { useAccount, useConnect, useSignMessage } from 'wagmi';
function App() {
const { address, isConnected } = useAccount();
const { connect, connectors } = useConnect();
const { signMessage } = useSignMessage();
// 完整的钱包功能开箱即用
}
相关链接
- EIP-1193: Ethereum Provider JavaScript API
- EIP-6963: Multi Injected Provider Discovery
- EIP-712: Typed Structured Data Hashing and Signing
- ERC-4337: Account Abstraction Using Alt Mempool
- wagmi 官方文档
- viem 官方文档
- WalletConnect 官方文档
- RainbowKit 官方文档
- ethers.js 官方文档
- Privy 官方文档
- MetaMask Developer Docs
- Sign-In with Ethereum (SIWE)