钱包集成与多链连接:MetaMask、WalletConnect 与 Wagmi

详解 Web3 DApp 的钱包连接方案:从 MetaMask 直连到 WalletConnect 通用协议,以及现代开发栈 Wagmi + Viem + RainbowKit 的高效集成模式。

钱包是用户与 Web3 世界交互的入口。一个优秀的 DApp 需要支持多种钱包、多链切换和流畅的用户体验。本文将涵盖原生连接方案、WalletConnect 协议以及现代 React 生态中最高效的 Wagmi + RainbowKit 组合。

一、以太坊浏览器钱包生态

主流钱包概览

钱包类型特点用户量
MetaMask浏览器扩展/移动端最早、生态最完善3000万+
Rabby浏览器扩展安全警报、多链体验优化增长最快
Trust Wallet移动端为主Binance 生态,内置 DEX大规模
Coinbase Wallet扩展 + 移动端Coinbase 生态集成大规模
Rainbow移动端/扩展设计优秀,NFT 友好中高端用户
Phantom扩展Solana 起家,已支持 EVM多链用户
OKX / Bitget交易所钱包内置交易功能交易所用户

EIP-1193:标准化的钱包接口

现代浏览器钱包都遵循 EIP-1193 标准,通过 window.ethereum 暴露统一 API:

interface EthereumProvider {
    request(args: { method: string; params?: unknown[] }): Promise<unknown>;
    on(event: string, listener: (...args: any[]) => void): void;
    removeListener(event: string, listener: (...args: any[]) => void): void;
}

二、原生连接方案(Ethers.js + MetaMask)

基础连接模式

import { ethers } from "ethers";

class WalletManager {
    private provider: ethers.BrowserProvider | null = null;
    private signer: ethers.JsonRpcSigner | null = null;
    private address: string | null = null;
    
    async connect(): Promise<string> {
        if (!window.ethereum) {
            throw new Error("请安装 MetaMask 或其他兼容钱包");
        }
        
        this.provider = new ethers.BrowserProvider(window.ethereum, {
            name: "mainnet",
            chainId: 1,
        });
        
        // 请求用户授权连接
        await this.provider.send("eth_requestAccounts", []);
        this.signer = await this.provider.getSigner();
        this.address = await this.signer.getAddress();
        
        this.setupListeners();
        return this.address;
    }
    
    private setupListeners() {
        window.ethereum.on("accountsChanged", (accounts: string[]) => {
            if (accounts.length === 0) {
                this.disconnect();
            } else {
                this.address = accounts[0];
            }
        });
        
        window.ethereum.on("chainChanged", (chainId: string) => {
            // chainId 是十六进制字符串,如 "0x1"
            window.location.reload();  // 推荐做法
        });
    }
    
    async disconnect() {
        this.provider = null;
        this.signer = null;
        this.address = null;
    }
    
    async switchNetwork(chainId: number): Promise<void> {
        const hexChainId = `0x${chainId.toString(16)}`;
        try {
            await window.ethereum.request({
                method: "wallet_switchEthereumChain",
                params: [{ chainId: hexChainId }],
            });
        } catch (switchError: any) {
            // 如果链未添加,需要先添加
            if (switchError.code === 4902) {
                await this.addNetwork(chainId);
            }
        }
    }
    
    async addNetwork(chainId: number): Promise<void> {
        const networks: Record<number, any> = {
            137: {
                chainId: "0x89",
                chainName: "Polygon Mainnet",
                nativeCurrency: { name: "MATIC", symbol: "MATIC", decimals: 18 },
                rpcUrls: ["https://polygon-rpc.com"],
                blockExplorerUrls: ["https://polygonscan.com"],
            },
            42161: {
                chainId: "0xa4b1",
                chainName: "Arbitrum One",
                nativeCurrency: { name: "ETH", symbol: "ETH", decimals: 18 },
                rpcUrls: ["https://arb1.arbitrum.io/rpc"],
                blockExplorerUrls: ["https://arbiscan.io"],
            },
        };
        
        await window.ethereum.request({
            method: "wallet_addEthereumChain",
            params: [networks[chainId]],
        });
    }
}

读取当前网络信息

async function getNetworkInfo() {
    const chainId = await window.ethereum.request({ method: "eth_chainId" });
    const accounts = await window.ethereum.request({ method: "eth_accounts" });
    
    return {
        chainId: parseInt(chainId, 16),
        isConnected: accounts.length > 0,
        address: accounts[0] || null,
    };
}

三、WalletConnect:通用多钱包连接器

为什么需要 WalletConnect?

原生 window.ethereum 只能连接浏览器扩展钱包,无法支持:

  • 移动端钱包(如 Trust Wallet、Rainbow)
  • 桌面端通过二维码扫描连接
  • 多钱包同时存在时的选择

WalletConnect v2 是一个通用协议,通过 WebSocket 中继 + 端到端加密实现钱包与 DApp 的安全通信。

使用 WalletConnect v2

import { EthereumProvider } from "@walletconnect/ethereum-provider";

const provider = await EthereumProvider.init({
    projectId: "YOUR_WALLETCONNECT_PROJECT_ID", // 从 cloud.walletconnect.com 获取
    chains: [1, 137, 42161],  // 支持的主链
    showQrModal: true,         // 显示二维码弹窗
    methods: ["eth_sendTransaction", "personal_sign"],
    events: ["chainChanged", "accountsChanged"],
});

// 启用连接
await provider.enable();

// 用法与 EIP-1193 完全一致
const ethersProvider = new ethers.BrowserProvider(provider);
const signer = await ethersProvider.getSigner();

Project ID 申请

  1. 访问 WalletConnect Cloud
  2. 注册账号并创建新项目
  3. 获取 projectId,免费额度通常足够中小型 DApp 使用

四、现代开发栈:Wagmi + Viem + RainbowKit

架构概览

┌─────────────────────────────────────┐
│           RainbowKit                 │  ← 连接按钮 + 钱包选择 UI
│      ( beautiful connect modal )     │
└────────────────┬────────────────────┘
                 │
┌────────────────┴────────────────────┐
│            Wagmi (React Hooks)       │  ← 状态管理 + 缓存
│   useAccount, useBalance, useWrite   │
└────────────────┬────────────────────┘
                 │
┌────────────────┴────────────────────┐
│           Viem (Core SDK)            │  ← 与链交互
│    Public Client + Wallet Client     │
└─────────────────────────────────────┘

安装与配置

npm install wagmi viem @rainbow-me/rainbowkit
// wagmi.ts
import { getDefaultConfig } from "@rainbow-me/rainbowkit";
import { mainnet, polygon, arbitrum, base } from "wagmi/chains";

export const config = getDefaultConfig({
    appName: "My DApp",
    projectId: "YOUR_WALLETCONNECT_PROJECT_ID",
    chains: [mainnet, polygon, arbitrum, base],
    ssr: true,  // 若使用 Next.js App Router
});

// providers.tsx (Next.js App Router)
import { WagmiProvider } from "wagmi";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { RainbowKitProvider } from "@rainbow-me/rainbowkit";
import { config } from "./wagmi";

const queryClient = new QueryClient();

export function Providers({ children }: { children: React.ReactNode }) {
    return (
        <WagmiProvider config={config}>
            <QueryClientProvider client={queryClient}>
                <RainbowKitProvider>{children}</RainbowKitProvider>
            </QueryClientProvider>
        </WagmiProvider>
    );
}

使用 Hook 进行交互

// components/WalletInfo.tsx
import { useAccount, useBalance, useChainId, useSwitchChain } from "wagmi";
import { ConnectButton } from "@rainbow-me/rainbowkit";

export function WalletInfo() {
    const { address, isConnected } = useAccount();
    const chainId = useChainId();
    const { switchChain } = useSwitchChain();
    
    // 自动缓存,自动刷新
    const { data: balance } = useBalance({ address });
    
    if (!isConnected) {
        return <ConnectButton />;
    }
    
    return (
        <div>
            <p>地址: {address}</p>
            <p>网络: {chainId}</p>
            <p>余额: {balance?.formatted} {balance?.symbol}</p>
            
            <button onClick={() => switchChain({ chainId: 137 })}>
                切换到 Polygon
            </button>
        </div>
    );
}

合约写入操作

// 准备写入操作
import { useWriteContract, useWaitForTransactionReceipt } from "wagmi";
import { parseUnits } from "viem";

const abi = [
    {
        name: "transfer",
        type: "function",
        inputs: [
            { name: "to", type: "address" },
            { name: "amount", type: "uint256" },
        ],
        outputs: [{ name: "", type: "bool" }],
    },
] as const;

export function TransferButton({ tokenAddress }: { tokenAddress: `0x${string}` }) {
    const { writeContract, data: hash } = useWriteContract();
    
    const { isLoading, isSuccess } = useWaitForTransactionReceipt({ hash });
    
    const handleTransfer = () => {
        writeContract({
            address: tokenAddress,
            abi,
            functionName: "transfer",
            args: ["0xRecipient...", parseUnits("100", 18)],
        });
    };
    
    return (
        <button onClick={handleTransfer} disabled={isLoading}>
            {isLoading ? "确认中..." : isSuccess ? "转账成功" : "转账"}
        </button>
    );
}

自定义 RainbowKit 主题

import { RainbowKitProvider, darkTheme } from "@rainbow-me/rainbowkit";

<RainbowKitProvider
    theme={darkTheme({
        accentColor: "#7b3fe4",
        accentColorForeground: "white",
        borderRadius: "medium",
    })}
    coolMode  // 连接时的酷炫粒子效果
>
    {children}
</RainbowKitProvider>

EIP-6963:多钱包发现协议

传统方案依赖 window.ethereum(单钱包注入),多钱包共存时会产生冲突。EIP-6963 定义了标准的事件接口,让多个钱包扩展能够同时声明自己的 Provider:

// 钱包扩展广播自身
window.dispatchEvent(new CustomEvent("eip6963:announceProvider", {
    detail: {
        info: { uuid: " abc123", name: "MetaMask", icon: "..." },
        provider: ethereumProvider,
    }
}));

// DApp 监听所有可用的钱包
window.addEventListener("eip6963:announceProvider", (event) => {
    const { info, provider } = event.detail;
    console.log(`发现钱包: ${info.name}`);
    // 用户可从列表中选择
});

// 请求钱包重新广播
dispatchEvent(new Event("eip6963:requestProvider"));

Wagmi v2 已内置 EIP-6963 支持,@rainbow-me/rainbowkit 自动聚合多钱包入口,无需手动处理事件监听。

智能合约交互安全

DApp 前端是与智能合约交互的第一道防线,安全疏忽可能导致用户资金损失:

Reentrancy 防护:

  • 前端不应依赖用户自行检查合约安全
  • 集成 OpenZeppelin Defender 或 Tenderly 进行合约审计标记
  • 对涉及外部调用的操作(如 transfer)添加二次确认弹窗

Token Approval 风险:

  • 无限授权(approve(spender, type(uint256).max))是常见安全隐患
  • 引导用户使用最小必要授权,或集成 Permit2(Uniswap)实现单次授权
  • 显示当前授权额度,提供一键撤销选项
// 检查授权额度
const allowance = await erc20Contract.read.allowiance([userAddress, spenderAddress]);
const decimals = await erc20Contract.read.decimals();
const formattedAllowance = formatUnits(allowance, decimals);

// 建议用户仅授权本次需要金额
const approvalAmount = parseUnits("100", decimals); // 而非 MaxUint256

Typed Data Signing(EIP-712)

传统 personal_sign 消息用户无法直观理解内容,EIP-712 引入结构化数据签名,提升可读性与安全性:

import { createWalletClient, custom } from "viem";

const message = {
    domain: {
        name: "My DApp",
        version: "1",
        chainId: 1,
        verifyingContract: "0x...",
    },
    types: {
        Order: [
            { name: "amount", type: "uint256" },
            { name: "recipient", type: "address" },
        ],
    },
    primaryType: "Order",
    message: {
        amount: 1000000000000000000n,
        recipient: "0x...",
    },
};

const signature = await walletClient.signTypedData(message);
// 用户签名时看到结构化人类可读内容,而非十六进制字符串

Sign-In with Ethereum(SIWE)

SIWE(EIP-4361)提供无需密码的去中心化身份认证:

import { SiweMessage } from "siwe";

const message = new SiweMessage({
    domain: window.location.host,
    address: address,
    statement: "Sign in to My DApp",
    uri: window.location.origin,
    version: "1",
    chainId: 1,
    nonce: generateNonce(),  // 服务端生成防重放
});

const signature = await walletClient.signMessage({
    message: message.prepareMessage(),
});

// 发送至服务端验证
const response = await fetch("/api/auth/verify", {
    method: "POST",
    body: JSON.stringify({ message: message.prepareMessage(), signature }),
});

SIWE 的优势:用户无需记忆密码、天然绑定 Web3 身份、可扩展为 DID(去中心化标识符)登录。

Gasless 交易与账户抽象

通过 ERC-4337(Account Abstraction),用户无需持有 ETH 支付 Gas,体验接近 Web2:

import { createSmartAccountClient } from "permissionless";

const smartAccountClient = createSmartAccountClient({
    account: simpleSmartAccount,
    chain: mainnet,
    bundlerTransport: http(bundlerRpc),
    middleware: {
        sponsorUserOperation: paymasterClient.sponsorUserOperation, // Paymaster 代付 Gas
    },
});

// 用户无需 ETH,Paymaster 代为支付 Gas
const txHash = await smartAccountClient.sendTransaction({
    to: "0x...",
    value: parseEther("0.1"),
});

Paymaster 模式适合:

  • 新用户零门槛体验( sponsor 首次交易 Gas)
  • 稳定币 Gas 支付(用户用 USDC 代替 ETH 付 Gas)
  • 订阅制产品(每月固定额度 sponsor)

前端安全审计清单

检查项严重级别验证方法
检测钓鱼域名🔴集成 ChainPatrol / Blowfish
合约地址白名单🔴前端硬编码校验 + 链上验证
交易模拟预览🟠Tenderly Simulation API
签名内容可读🟠强制使用 EIP-712 结构化数据
HTTPS + CSP🟠部署时验证响应头
依赖漏洞扫描🟡Dependabot / Snyk 自动化
第三方脚本完整性🟡Subresource Integrity (SRI)

五、多链策略设计

常见多链模式

模式描述适用场景
单链专注DApp 仅在一个链上运行低成本启动、特定生态
链选择器用户手动切换不同链的相同合约DeFi 协议多链部署
统一跨链使用 LayerZero / Axelar 抽象链差异跨链桥、全链应用
链抽象账户抽象 + 意图层,用户无感知未来方向(ERC-4337 + 意图)

推荐 RPC 提供商

提供商免费额度特色
Alchemygenerous开发者工具全面
Infura适中以太坊基金会背景
QuickNode适中全球节点多
Public Node免费公共节点,可靠性较低
Tenderly有限强大的模拟调试功能

六、本章小结

钱包连接是 DApp 用户体验的第一道门槛。从原生 window.ethereum 到 WalletConnect 多钱包协议,再到 Wagmi + RainbowKit 的现代 React 开发栈,工具链不断演进。对于新项目,强烈建议直接使用 Wagmi + Viem + RainbowKit 组合,可节省大量重复开发工作,且自动获得类型安全、缓存优化和优雅 UI。同时,要注意处理网络切换、错误边界和加载状态,提供流畅的用户体验。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「blockchain」更多文章

  1. DePIN 去中心化物理基础设施网络
  2. DeFi 衍生品:期权、永续合约与合成资产
  3. 智能合约形式化验证:Certora 与 K 框架