用 wagmi v2 踩坑两天,我终于搞懂了多链钱包切换

用 wagmi v2 踩坑两天,我终于搞懂了多链钱包切换

摘要

跨链 DEX 项目里,用户切换以太坊主网到 BSC 后,RainbowKit 弹窗里的连接状态直接断了。我研究了 wagmi v2 的 useChain、useSwitchChain 和 createConfig 的配置细节,最终用 autoConnect + onStatus 回调实现无缝切换。这篇文章就是我当时解决问题的完整记录。

背景

今年初接了一个跨链聚合 DEX 的前端开发,需求是让用户可以用一个 MetaMask 钱包在 Ethereum、BSC、Polygon 三条链之间来回切换,每次换链后自动更新余额和交易对数据。我一开始想:RainbowKit 不是自带多链支持吗?直接照着文档配一下不就行了?结果一跑起来,用户从 Ethereum 切到 BSC,钱包弹窗里显示"Disconnected",再点连接又得重新授权,体验极其糟糕。我当时就踩了这个坑------RainbowKit 默认只管理钱包连接,但链切换后 wagmi 的 chainId 状态和钱包的 chainId 不一致,导致连接状态被重置。

问题分析

我最初的思路很简单:用 RainbowKit 的 chains 配置多链,然后在每个页面组件里调用 useAccount 获取连接状态,再用 useSwitchChain 来让用户手动切换。但测试时发现,用户切换到 BSC 后,useAccount 的 isConnected 变成了 false。我排查了半小时,打印了 useAccount 和 useNetwork 的所有字段,发现 chain 对象变了,但 connector 还在。这说明钱包本身没断开,但 wagmi 认为链不匹配,所以把连接状态清空了。

后来我翻 wagmi v2 的源码,看到 createConfig 里有个 multiInjectedProviderDiscovery 选项,默认是 true。这个选项会导致 wagmi 自动检测浏览器里所有的 injected provider(比如 MetaMask、Coinbase Wallet),然后在链切换时,它可能会把连接状态绑定到某个特定的 provider 实例上。如果用户切换链,provider 的 chainId 变了,wagmi 就认为连接失效。这就是问题的根源------wagmi v2 默认把链和连接强绑定,而 RainbowKit 没有自动处理这个绑定变化。

核心实现

1. 配置 wagmi 的 createConfig,避开自动检测的坑

第一步是修改 createConfig 的配置。这里有个关键点:multiInjectedProviderDiscovery 一定要设为 false,否则 wagmi 会反复创建多个 provider 实例,导致链切换时连接混乱。同时,connectors 里要显式指定 injected 作为默认连接器,而不是用 metaMask 这种特定钱包的连接器,因为 injected 会统一处理所有浏览器内置钱包。

typescript 复制代码
// wagmi.config.ts
import { createConfig, http } from 'wagmi'
import { mainnet, bsc, polygon } from 'wagmi/chains'
import { injected } from 'wagmi/connectors'

export const config = createConfig({
  chains: [mainnet, bsc, polygon],
  connectors: [
    injected({
      shimDisconnect: false, // 这个选项很重要,后面会讲
    }),
  ],
  transports: {
    [mainnet.id]: http(),
    [bsc.id]: http('https://bsc-dataseed.binance.org'),
    [polygon.id]: http('https://polygon-rpc.com'),
  },
  multiInjectedProviderDiscovery: false, // 关闭自动发现
})

这里有个坑:shimDisconnect 默认是 true,它会在 localStorage 里存一个标志,用来模拟断开连接。但多链场景下,这个标志会在链切换时被误触发,导致连接状态丢失。我一开始没关这个,结果用户切链后 isConnected 总是 false,排查了半天才发现是它搞的鬼。

2. 用 onStatus 回调监听连接状态变化,实现自动重连

光配置好还不行,还得在应用层面监听链切换事件。wagmi v2 的 createConfig 返回一个 store 对象,它有一个 subscribe 方法,可以监听状态变化。我写了一个自定义 hook useAutoReconnect,在链切换时自动调用 reconnect 方法。

typescript 复制代码
// hooks/useAutoReconnect.ts
import { useEffect } from 'react'
import { useConfig, useAccount } from 'wagmi'

export function useAutoReconnect() {
  const config = useConfig()
  const { chainId } = useAccount()

  useEffect(() => {
    // 监听 chainId 变化
    const unsubscribe = config.subscribe(
      (state) => state.chainId,
      async (newChainId, prevChainId) => {
        if (newChainId !== prevChainId && newChainId) {
          console.log(`链切换: ${prevChainId} -> ${newChainId}`)
          // 尝试重新连接,但不要弹窗
          try {
            await config.reconnect()
          } catch (error) {
            console.error('自动重连失败:', error)
          }
        }
      }
    )

    return () => unsubscribe()
  }, [config])
}

注意这个 config.subscribe 的用法:第一个参数是一个选择器函数,返回你要监听的状态片段;第二个参数是回调函数。这里我监听 state.chainId,一旦变化就执行重连。reconnect 方法会尝试用已有的连接器重新连接,不会弹出钱包授权窗口,用户体验很顺滑。

3. 在 RainbowKit 中集成自定义的链切换逻辑

RainbowKit 默认的链切换是通过 useSwitchChain 实现的,但它在多链场景下有个问题:它会让用户手动选择目标链,而不是自动跟随钱包的链切换。我需要让 RainbowKit 的链选择器与 wagmi 的链状态同步。

我重写了 RainbowKit 的 ChainSwitcher 组件,用 wagmi 的 useSwitchChain 替代默认实现,同时加上一个链切换确认弹窗,防止用户误操作。

typescript 复制代码
// components/CustomChainSwitcher.tsx
import { useSwitchChain, useChainId } from 'wagmi'
import { mainnet, bsc, polygon } from 'wagmi/chains'

const chainMap = {
  [mainnet.id]: { name: 'Ethereum', icon: '🟦' },
  [bsc.id]: { name: 'BSC', icon: '🟨' },
  [polygon.id]: { name: 'Polygon', icon: '🟪' },
}

export function CustomChainSwitcher() {
  const currentChainId = useChainId()
  const { switchChain } = useSwitchChain()

  const handleSwitch = (chainId: number) => {
    if (chainId === currentChainId) return
    // 这里可以加一个确认弹窗,防止误切换
    const confirm = window.confirm(`切换到 ${chainMap[chainId]?.name}?`)
    if (!confirm) return
    switchChain({ chainId })
  }

  return (
    <div style={{ display: 'flex', gap: '8px', padding: '8px' }}>
      {Object.entries(chainMap).map(([id, chain]) => (
        <button
          key={id}
          onClick={() => handleSwitch(Number(id))}
          style={{
            padding: '8px 16px',
            border: Number(id) === currentChainId ? '2px solid blue' : '1px solid gray',
            borderRadius: '8px',
            cursor: 'pointer',
          }}
        >
          {chain.icon} {chain.name}
        </button>
      ))}
    </div>
  )
}

这里有个细节:useSwitchChain 返回的 switchChain 函数需要传入 chainId,而且它会在底层调用钱包的 wallet_switchEthereumChain 方法。如果钱包不支持该链,它会自动尝试 wallet_addEthereumChain。但注意,有些钱包(比如 Trust Wallet)可能在切换 BSC 时失败,因为 BSC 的 chainId 不是标准的 EVM 链。我在测试时就遇到了这个情况,后面会讲怎么解决。

4. 处理钱包不支持目标链的 fallback

如果用户的钱包没有配置目标链,switchChain 会抛出一个错误。我需要捕获这个错误,并引导用户手动添加链。这里用了 useSwitchChain 的 onError 回调,或者直接在 switchChain 外面包一层 try-catch。

typescript 复制代码
// hooks/useSafeSwitchChain.ts
import { useSwitchChain } from 'wagmi'
import { useCallback } from 'react'

export function useSafeSwitchChain() {
  const { switchChain, isPending } = useSwitchChain({
    mutation: {
      onError: (error) => {
        // 检查错误是否是因为链未添加
        if (error.message?.includes('wallet_switchEthereumChain')) {
          alert('请手动在钱包中添加该链,或切换到支持的链')
          // 可以在这里打开一个帮助弹窗
        } else {
          console.error('链切换失败:', error)
        }
      },
    },
  })

  const safeSwitch = useCallback((chainId: number) => {
    try {
      switchChain({ chainId })
    } catch (error) {
      // 这里捕获同步错误,但 switchChain 是异步的,所以 onError 更可靠
    }
  }, [switchChain])

  return { safeSwitch, isPending }
}

注意:useSwitchChain 的 mutation 配置是在 wagmi v2 中新增的,它允许你传入 onSuccess、onError 等回调。这个比在外面用 try-catch 更优雅,因为 switchChain 本身是异步的,try-catch 只能捕获同步错误。

完整代码

下面是一个完整的可运行示例,包含所有关键部分:

typescript 复制代码
// App.tsx
import { WagmiProvider, createConfig, http } from 'wagmi'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { RainbowKitProvider, darkTheme } from '@rainbow-me/rainbowkit'
import { mainnet, bsc, polygon } from 'wagmi/chains'
import { injected } from 'wagmi/connectors'
import { CustomChainSwitcher } from './components/CustomChainSwitcher'
import { useAutoReconnect } from './hooks/useAutoReconnect'
import '@rainbow-me/rainbowkit/styles.css'

const queryClient = new QueryClient()

const config = createConfig({
  chains: [mainnet, bsc, polygon],
  connectors: [
    injected({
      shimDisconnect: false,
    }),
  ],
  transports: {
    [mainnet.id]: http(),
    [bsc.id]: http('https://bsc-dataseed.binance.org'),
    [polygon.id]: http('https://polygon-rpc.com'),
  },
  multiInjectedProviderDiscovery: false,
})

function AppContent() {
  useAutoReconnect() // 启用自动重连

  return (
    <div>
      <CustomChainSwitcher />
      {/* 其他组件 */}
    </div>
  )
}

export default function App() {
  return (
    <WagmiProvider config={config}>
      <QueryClientProvider client={queryClient}>
        <RainbowKitProvider theme={darkTheme()}>
          <AppContent />
        </RainbowKitProvider>
      </QueryClientProvider>
    </WagmiProvider>
  )
}
typescript 复制代码
// hooks/useAutoReconnect.ts
import { useEffect } from 'react'
import { useConfig, useAccount } from 'wagmi'

export function useAutoReconnect() {
  const config = useConfig()
  const { chainId } = useAccount()

  useEffect(() => {
    const unsubscribe = config.subscribe(
      (state) => state.chainId,
      async (newChainId, prevChainId) => {
        if (newChainId !== prevChainId && newChainId) {
          console.log(`链切换: ${prevChainId} -> ${newChainId}`)
          try {
            await config.reconnect()
          } catch (error) {
            console.error('自动重连失败:', error)
          }
        }
      }
    )

    return () => unsubscribe()
  }, [config])
}

踩坑记录

  1. shimDisconnect 导致连接状态丢失 :一开始没关这个选项,用户切链后 isConnected 总是 false。排查了一上午,最后在 wagmi 的 GitHub issue 里看到有人说这个选项会在 localStorage 里存一个 wagmi.disconnect 标志,链切换时这个标志被误触发。关掉后就正常了。

  2. BSC 链切换失败 :用 MetaMask 测试时,从 Ethereum 切到 BSC 没问题,但用 Trust Wallet 时,switchChain 直接抛错。原因是 Trust Wallet 默认没有添加 BSC 链,需要先调用 wallet_addEthereumChain。我后来在 onError 回调里加了一个处理:如果错误是 wallet_switchEthereumChain 导致的,就弹窗提示用户手动添加链,或者自动调用 wallet_addEthereumChain(但不同钱包的 RPC URL 可能不同,所以我选择了弹窗提示)。

  3. config.subscribe 在组件卸载后仍执行 :第一次写 useAutoReconnect 时,我没有在 useEffect 的清理函数里取消订阅,结果组件卸载后订阅还在运行,导致内存泄漏。后来加了 return () => unsubscribe() 才解决。这个坑在 wagmi 文档里有提到,但我一开始没仔细看。

  4. RainbowKit 的 ChainSwitcher 与自定义组件冲突 :我替换了 RainbowKit 默认的链选择器后,发现 RainbowKit 的弹窗里还有一个链选择器,两个同时存在,用户会迷惑。后来我在 RainbowKitProvider 里设置了 showRecentTransactions={true} 但隐藏了链选择器,只保留自定义的 CustomChainSwitcher。具体做法是在 RainbowKitProvider 的 chains 配置里,把链列表设为只读。

小结

核心收获:wagmi v2 的多链切换本质是 chainId 和连接器实例的绑定问题,关闭 shimDisconnect 和 multiInjectedProviderDiscovery,再配合 config.subscribe 监听链变化自动重连,就能实现无缝切换。可以继续深挖的方向:用 viem 的 createPublicClient 替代 wagmi 的 http 传输层,实现更精细的链状态同步。

相关推荐
huakoh9 分钟前
MCP 调用超时后别急着重试:三步读回分清执行状态
前端
打工仔折腾 AI10 分钟前
把模型切换交给平台:用蓝耘智能路由搭建商品评论分析工具
java·服务器·前端·后端·python·性能优化·ai agent 实战
不可能片场16 分钟前
Electron 发布标题的冒号 击穿命令行解析
前端·electron
reeswell24 分钟前
我开源了 inspect-devtools —— 让 AI 终于能"看见"你屏幕上的组件
前端·人工智能
Yuhano35 分钟前
W3. 实现Agent工具调用引擎
前端·aigc·ai编程
flash俊杰35 分钟前
第三节 基于 Vue 3 的领域模型架构——Schema 驱动的低代码后台实战
前端
热爱23339 分钟前
dsh里使用chatgpt plus或pro会员而不是apikey,其实很简单。
前端·openai
linux_cfan1 小时前
videojs v10 源代码系列解读:32 · Reactor 模型:信号驱动的状态机
前端·javascript·音视频
羲云1 小时前
给页面加一个 JSON 编辑器:jsoneditor 的功能、配置和接入注意事项
javascript
福兮说1 小时前
Base64 遇上中文和 emoji:btoa 报错、解出一串 %E7、URL 里加号变空格,六个坑一次说清
前端·javascript·base64·编码