跳到主要内容

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 行代码接入

面试速答版

怎么设计一套 Web3 钱包连接方案? 核心是「协议层标准化 + Connector 适配 + Hooks 上层封装」三层架构:

  • 协议层:EVM 钱包统一走 EIP-1193(provider.request({ method, params }) 的 JSON-RPC 接口);多钱包共存用 EIP-6963 解决「window.ethereum 互相覆盖」的问题;非 EVM 链(Solana / TON)走各自的 Wallet Adapter。
  • Connector 层:每种钱包/连接方式封装成一个 Connector(InjectedConnector 浏览器扩展、WalletConnectConnector 移动 App 扫码、CoinbaseConnector、EmbeddedConnector 嵌入式社交登录)。
  • Hooks 层:基于 wagmi / RainbowKit 提供 useConnect / useAccount / useSignMessage / useSendTransaction / useSwitchChain,业务层一行代码搞定连接、签名、发交易。
  • 关键事件:监听 accountsChanged(用户切账户)、chainChanged(用户换链)、disconnect,本地状态及时更新。

EIP-6963 解决了什么问题? 旧方案下钱包都往 window.ethereum 上塞 Provider,用户装了 MetaMask + OKX + Coinbase 三个扩展时,后加载的会覆盖前面的,DApp 只能看到一个钱包:

  • EIP-6963 改成事件机制:DApp 派发 eip6963:requestProvider 事件,所有已安装钱包用 eip6963:announceProvider 回应自己的 info(name/icon/uuid/rdns)+ provider。
  • DApp 拿到完整的钱包列表渲染选择 UI,不再有覆盖问题。

怎么处理「连完钱包但用户切账户/切链」? 钱包是用户的「控制台」,账户和链随时可能变,DApp 必须监听并响应:

  • provider.on('accountsChanged', accounts => ...):账户变了要重新拉用户数据,可能要重新签名登录。
  • provider.on('chainChanged', chainId => ...):链变了要切换 RPC、重新读合约、提示「当前网络不支持」。
  • wallet_switchEthereumChain 主动让用户换链,钱包没该链就 wallet_addEthereumChain 引导添加。

二、整体架构​

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 钱包都应实现这个协议。

types/eip1193.ts
/** 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'; // 添加代币到钱包
EIP-1193 核心要点
  1. 所有通信基于 JSON-RPC 2.0 协议
  2. request 是唯一的请求方法(取代了旧版的 send、sendAsync)
  3. 事件系统支持 accountsChanged、chainChanged、connect、disconnect
  4. Provider 通过 window.ethereum 注入到页面(旧方式,已被 EIP-6963 改进)

3.2 EIP-6963: 多钱包发现协议​

当用户安装了多个钱包扩展时(如 MetaMask + OKX Wallet),它们会争夺 window.ethereum,导致只有一个钱包可用("钱包覆盖问题")。EIP-6963 通过事件机制解决了这个问题。

eip6963/discovery.ts
/** 钱包信息 */
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 v1WalletConnect v2
中继架构单一 Bridge Server分布式 Relay Network
协议WebSocket + 自定义协议WebSocket + JSON-RPC
多链支持仅 EVMEVM + Solana + Cosmos + 更多
会话管理一个 Session 绑定一条链一个 Session 支持多链 (Namespace)
配对方式二维码 / Deep Link二维码 / Deep Link / URI
加密对称加密 (AES-256-CBC)X25519 + ChaCha20-Poly1305
状态已废弃当前版本

四、连接流程详解​

4.1 完整连接流程​

4.2 Connector 抽象设计​

connectors/base.ts
/** 钱包连接状态 */
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 实现​

connectors/injected.ts
/** 注入式钱包连接器(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链下订单、授权、PermitEIP-712
交易签名eth_sendTransaction转账、合约调用-

5.2 personal_sign(登录签名)​

Web3 中最常见的登录方式是 Sign-In with Ethereum (SIWE),通过让用户签名一段消息来证明地址所有权。

sign/personal-sign.ts
/** 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 定义了结构化数据的签名标准,让用户在签名时能看到可读的结构化数据,而非一串难以理解的十六进制字符串。

sign/eip712.ts
/** 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;
}
EIP-712 的优势
  1. 用户可读:钱包会以结构化方式展示签名内容,用户能看懂在签什么
  2. 防重放:通过 domain 中的 chainId 和 verifyingContract 防止签名被跨链/跨合约使用
  3. 链下可验证:签名结果可在智能合约中用 ecrecover 验证,也可在服务端验证
  4. Gas 优化:链下签名 + 链上验证的模式可节省大量 Gas(如 Permit2)

5.4 发送交易​

sign/send-transaction.ts
/** 交易参数 */
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 抽象层​

chains/config.ts
/** 链配置定义 */
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 管理​

chains/multi-chain-manager.ts
/** 链生态类型 */
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 钱包状态设计​

store/wallet-store.ts
/** 钱包全局状态 */
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 事件监听与自动重连​

store/auto-reconnect.ts
/** 事件监听管理 */
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 是目前最主流的 React Web3 库,底层使用 viem 作为以太坊交互层。

wagmi-example.ts
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 集成

8.2 方案对比表​

特性wagmi + viemethers.jsweb3-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 用法​

hooks/use-wallet.tsx
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 集成示例​

walletconnect/setup.ts
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/session.ts
/** 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 安全防护实现​

security/protection.ts
/** 域名验证 - 防止钓鱼网站 */
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.' };
}
}
高危操作提醒

以下签名操作需要特别警惕:

  1. Permit / Permit2 签名:链下签名即可授权代币转移,无需链上 approve 交易
  2. 无限授权 (Unlimited Approval):一旦合约被攻破,可转走用户所有授权代币
  3. 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 钱包集成​

aa/smart-account.ts
/** 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 社交登录集成​

aa/social-login.ts
/** 社交登录类型 */
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 };
}
AA 钱包的优势
特性EOA 钱包AA 钱包
Gas 支付必须持有原生代币可由 Paymaster 代付或用 ERC-20 支付
批量交易每笔操作需单独交易支持批量执行(一次签名多笔操作)
账户恢复私钥丢失即永久丢失支持社交恢复、多签恢复
签名验证仅支持 ECDSA可自定义(Passkey、多签、MPC 等)
登录方式必须安装钱包支持邮箱、社交账号、Passkey
用户门槛高(需理解私钥、Gas 等)低(接近 Web2 体验)

十二、性能优化​

12.1 连接性能优化​

优化策略说明效果
EIP-6963 优先优先使用事件驱动的钱包发现,避免轮询 window.ethereum发现速度 < 200ms
Provider 缓存缓存已初始化的 Provider 实例,避免重复创建二次连接 < 50ms
自动重连记住上次连接的钱包,页面刷新后自动重连用户体验提升
懒加载 ConnectorWalletConnect SDK 体积较大,按需加载减少首屏 JS 约 80KB
连接超时处理设置合理的连接超时(如 30s),避免无限等待降低用户流失

12.2 RPC 请求优化​

optimization/rpc-batch.ts
/** 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 懒加载​

optimization/lazy-connector.ts
/** 懒加载 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 插件化架构​

plugins/plugin-system.ts
/** 钱包插件接口 */
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_requestAccounts RPC 方法
  • 钱包弹出授权窗口,用户确认连接

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 方法实现:

EVM 链切换
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. 防重放攻击:

EIP-712 Domain 防重放
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_signEIP-712
签名内容纯文本/十六进制结构化 JSON
用户可读性差好(钱包友好展示)
重放防护需自行在消息中包含 nonce内置 domain 分隔符
链上验证可以但不推荐原生支持
典型用途登录签名(SIWE)Permit、订单、授权

Q4: 如何防止 Web3 钓鱼攻击?​

答案:

Web3 钓鱼攻击的防护需要从前端 DApp、钱包、用户教育三个层面入手:

1. 前端 DApp 层面:

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 标准在协议层之上实现,无需修改以太坊共识层。

解决的核心问题:

  1. Gas 门槛:传统 EOA 必须持有 ETH 才能发交易。AA 通过 Paymaster 实现 Gas 代付,用户甚至可以用 USDC 支付 Gas
  2. 私钥风险:EOA 私钥丢失 = 资产永久丢失。AA 钱包支持社交恢复(如 3/5 多签恢复)
  3. 用户体验:安装钱包、备份助记词、理解 Gas 等门槛过高。AA 支持邮箱/社交账号登录,接近 Web2 体验
  4. 交易灵活性:EOA 每笔操作需单独交易。AA 支持批量交易(一次签名执行多笔操作)

ERC-4337 工作流程:

用户操作 → 构造 UserOperation → Bundler 打包 → EntryPoint 验证 → 智能合约钱包执行
↓
Paymaster 代付 Gas

主流 AA 方案对比:

方案特点适用场景
Privy社交登录 + 嵌入式钱包 + AA面向 Web2 用户的 DApp
Safe (Gnosis)多签钱包,最成熟的合约钱包DAO 资金管理、团队钱包
ZeroDevKernel 框架,模块化 AA需要高度定制的 DApp
BiconomySDK + Paymaster + Bundler 全套快速集成 AA 功能
Alchemy AA与 Alchemy 基础设施深度集成已使用 Alchemy RPC 的项目

Q6: wagmi + viem 和 ethers.js 的主要区别是什么?如何选型?​

答案:

维度wagmi + viemethers.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();
// 完整的钱包功能开箱即用
}

相关链接​