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

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

摘要

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

背景

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

问题分析

我最初的思路很简单:用 RainbowKit 的 chains 配置多链,然后在每个页面组件里调用 useAccount 获取连接状态,再用 useSwitchChain 来让用户手动切换。但测试时发现,用户切换到 BSC 后,useAccountisConnected 变成了 false。我排查了半小时,打印了 useAccountuseNetwork 的所有字段,发现 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 会抛出一个错误。我需要捕获这个错误,并引导用户手动添加链。这里用了 useSwitchChainonError 回调,或者直接在 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 }
}

注意:useSwitchChainmutation 配置是在 wagmi v2 中新增的,它允许你传入 onSuccessonError 等回调。这个比在外面用 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。具体做法是在 RainbowKitProviderchains 配置里,把链列表设为只读。

小结

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

相关推荐
吃乔巴的糖4 小时前
Vue 3 打印模板设计器 (print-canvas-designer)
前端·vue.js
名字都不重要何况昵称5 小时前
canvas 分层渲染思路和脏矩形处理
前端·canvas
布列瑟农的星空5 小时前
前端是否需要架构
前端
子云zy5 小时前
JS 对象与包装类:new 做了什么?字符串为什么有 length?
前端·javascript
还有多久拿退休金5 小时前
LLM应用开发二:让AI学会"翻书"——RAG检索增强从踩坑到跑通
前端·llm
weiggle5 小时前
第二篇:搭建你的第一个 Compose 项目——开发环境与项目结构
android·前端
Simon523146 小时前
Spring AOP 五大通知类型
java·前端·spring
Asmewill6 小时前
LangGraph学习笔记八(SubGraph)
前端
叶落阁主6 小时前
AntV npm 投毒复盘:一次公司私服缓存恶意包引发的账号封禁事件
前端·安全·npm