3、WalletConnect集成:协议原理、QR Code配对、Session建立、发送交易、断开连接、多链支持

WalletConnect,说白了就是让DApp和手机钱包「隔空握手」的协议。我最早接触它是在2020年,那时候大家还在用MetaMask浏览器插件,移动端体验简直灾难。后来WalletConnect出现,才真正打通了桌面端和移动端的壁垒。

嗯,咱们直接进入正题。这一章我会把WalletConnect的集成拆成六个核心环节来讲:协议原理、QR Code配对、Session建立、发送交易、断开连接、多链支持。每个环节我都会结合自己踩过的坑来聊。

3.1 协议原理:它到底是怎么工作的?

WalletConnect不是点对点直连,而是通过一个「中继桥」来转发消息。你想想看,手机和电脑不在同一个局域网,怎么通信?靠的就是这个中继服务器。

核心流程其实很简单:

  • DApp端:生成一个配对URI,包含一个唯一的Session ID
  • 钱包端:扫描二维码,解析出URI,然后通过中继桥建立连接
  • 中继桥:负责转发加密后的JSON-RPC消息

这里有个关键点——所有消息都是端到端加密的。中继服务器只能看到「谁在跟谁说话」,但看不到「说了什么」。我在项目中遇到过有人担心隐私问题,其实大可不必,协议层已经帮你兜底了。

协议栈速览:

  • 传输层:WebSocket(中继桥)
  • 加密层:对称加密(Session Key)
  • 消息层:JSON-RPC 2.0
  • 应用层:EIP-155(交易格式)

3.2 QR Code配对:从生成到扫描

配对的第一步,是DApp生成一个二维码。这个二维码里藏的是什么?是一个标准的WalletConnect URI,格式长这样:

wc:<topic>@<version>?bridge=<bridge_url>&key=<symmetric_key>

我来拆解一下:

  • topic:随机生成的Session ID,用于标识这次连接
  • version:协议版本,目前主流是2.0
  • bridge:中继服务器地址,默认是官方提供的
  • key:对称加密密钥,用于后续消息加密

生成二维码的代码,我个人习惯用qrcode.react这个库:

import QRCode from 'qrcode.react';

function WalletConnectQR({ uri }) {
  return (
    <div className="qr-container">
      <QRCode value={uri} size={256} />
      <p>请用钱包扫描此二维码</p>
    </div>
  );
}

小技巧:二维码尺寸建议不小于200px,否则手机摄像头可能识别不了。我曾经因为二维码太小,被测试同事吐槽了整整一下午。

3.3 Session建立:握手与确认

二维码扫完之后,钱包和DApp之间会进行一轮「握手」。这个过程大概分三步:

  1. 钱包发送Session Proposal:钱包告诉DApp「我支持这些链、这些方法」
  2. DApp响应Session Approval:DApp确认「好的,我们开始吧」
  3. 双方交换Session Key:后续所有消息都用这个Key加密

代码层面,用@walletconnect/web3-provider这个库可以快速搞定:

import WalletConnectProvider from '@walletconnect/web3-provider';

const provider = new WalletConnectProvider({
  infuraId: '你的INFURA_ID',  // 用于RPC调用
  qrcode: false,  // 我们自己生成二维码,所以关掉内置的
});

// 建立连接
await provider.enable();
// 此时Session已经建立,可以拿到accounts和chainId

嗯,这里要注意:provider.enable()会弹出一个二维码窗口(如果你没关掉内置的)。我建议把qrcode设为false,然后自己控制UI展示,体验会好很多。

避坑指南:我曾经遇到过Session建立后突然断开的情况,排查了半天发现是中继服务器超时。解决方案是自定义bridge地址,用自己部署的中继服务,稳定性会高很多。

3.4 发送交易:签名与广播

Session建立之后,就可以发送交易了。流程其实跟MetaMask差不多:

  1. DApp构造交易对象
  2. 通过Provider发送eth_sendTransaction
  3. 钱包弹出确认界面,用户签名
  4. 签名后的交易广播到链上

代码示例:

const tx = {
  from: accounts[0],
  to: '0x...',
  value: ethers.utils.parseEther('0.1'),
  gasLimit: 21000,
};

// 发送交易
const txHash = await provider.send('eth_sendTransaction', [tx]);
console.log('交易哈希:', txHash);

你想想看,这里有个细节——gasLimitgasPrice要不要自己算?我个人建议让钱包端来处理,因为不同钱包的gas策略不一样。你硬塞一个值过去,反而可能出问题。

签名类型:

  • eth_sendTransaction:发送交易(需要gas)
  • eth_sign:普通签名(不消耗gas)
  • personal_sign:带消息前缀的签名(推荐)
  • eth_signTypedData:结构化数据签名(EIP-712)

3.5 断开连接:优雅地结束会话

断开连接看似简单,但处理不好会留下「僵尸Session」。我见过不少DApp,用户断开后重新连接,结果因为旧Session没清理,导致各种奇怪错误。

正确的断开方式:

// 主动断开
await provider.disconnect();

// 监听断开事件
provider.on('disconnect', (code, reason) => {
  console.log('连接已断开:', code, reason);
  // 清理本地状态
  setAccounts([]);
  setConnected(false);
});

为什么要监听disconnect事件?因为用户可能在钱包端主动断开,这时候DApp需要同步更新UI。我曾经没加这个监听,用户断开后界面还显示「已连接」,被用户骂了一顿。

注意:断开后一定要清除本地存储的Session数据,否则下次连接时可能会复用旧的Session,导致配对失败。

3.6 多链支持:一个Session,多条链

WalletConnect 2.0最大的改进之一,就是原生支持多链。一个Session可以同时连接以太坊、Polygon、BNB Chain等。

配置方式很简单,在初始化Provider时指定支持的链:

const provider = new WalletConnectProvider({
  rpc: {
    1: 'https://mainnet.infura.io/v3/你的ID',   // 以太坊
    137: 'https://polygon-rpc.com',              // Polygon
    56: 'https://bsc-dataseed.binance.org',      // BSC
  },
  chainId: 1,  // 默认链
});

切换链的时候,调用wallet_switchEthereumChain

await provider.send('wallet_switchEthereumChain', [
  { chainId: '0x89' }  // 137的十六进制
]);

这里有个坑——有些钱包不支持动态切换链。我遇到过用户反馈「点了切换没反应」,后来发现是钱包版本太旧。解决方案是在UI上加个提示,让用户手动在钱包里切换。

多链最佳实践:

  • 初始化时列出所有支持的链,不要只列一条
  • 切换链前先检查钱包是否支持wallet_switchEthereumChain
  • 如果不支持,回退到wallet_addEthereumChain添加链

总结

WalletConnect的集成,说白了就是「配对-握手-通信-断开」这四个步骤。我个人觉得最难的部分不是写代码,而是处理各种边界情况——用户扫码后取消、网络断开、Session过期……这些都需要你提前想好。

嗯,这一章的内容就到这里。下一章我们会聊硬件钱包的集成,那又是另一番天地了。


公众号:蓝海资料掘金营,微信deep3321