跳到主要内容

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

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 是唯一的请求方法(取代了旧版的 sendsendAsync
  3. 事件系统支持 accountsChangedchainChangedconnectdisconnect
  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 中的 chainIdverifyingContract 防止签名被跨链/跨合约使用
  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
框架依赖ReactReact
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();
// 完整的钱包功能开箱即用
}

相关链接