钱包集成与多链连接: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>

五、多链策略设计

常见多链模式

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

推荐 RPC 提供商

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

六、本章小结

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

继续阅读

探索更多技术文章

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

全部文章 返回首页

「区块链 Web3」更多文章

  1. Web3 全栈 DApp 开发实战:从前端到智能合约的完整链路
  2. 企业级区块链:Hyperledger Fabric 架构与链码开发
  3. 区块链安全:合约审计、攻击模式与防御体系