ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Dapp-Learning 实战:基于 WalletConnect v2 的 React 签名示例(sign-example)深度解析

Dapp-Learning 实战:基于 WalletConnect v2 的 React 签名示例(sign-example)深度解析
  • 示例工程
  • 区块链

【免费下载链接】Dapp-Learning

Dapp learning project for developers at all stages. Becoming and cultivating sovereign individuals. Nonprofit organization.

项目地址:https://gitcode.com/gh_mirrors/da/Dapp-Learning
点击查看免费下载

本篇技术指南以 Dapp-Learning 仓库中 basic/78-wallet-connect/v2/sign-example/README.md 为核心,围绕其配套的 React 示例工程,完整讲解如何基于 WalletConnect v2 协议在 DApp 中实现"连接钱包 + 消息签名"两大核心能力。读完本文,你将掌握@walletconnect/sign-client与@web3modal/standalone的初始化方式、提案命名空间(proposal namespace)的配置方法、会话建立与断开流程、会话事件订阅机制,以及通过personal_sign请求钱包签名的完整调用链,可直接复用到自己的 Web3 项目中。

一、示例项目定位:连接 + 签名的最小闭环

sign-example是 Dapp-Learning 中 WalletConnect v2 系列的三个示例之一(其余两个为 basic-dapp-example 与 multichain-example)。与 basic-dapp-example 侧重"展示 wagmi + web3modal 一键连接"、multichain-example 侧重"多链资产与切换"不同,sign-example 聚焦一个最小但完整的闭环:

  1. 初始化 WalletConnect v2 客户端(SignClient);
  2. 发起连接提案,等待用户钱包扫码/授权;
  3. 会话建立后展示已连接账户;
  4. 向钱包发起personal_sign签名请求并展示签名结果;
  5. 支持主动断开与事件驱动的被动断开。

整个工程只有一个核心组件文件 src/App.js,状态管理全部基于 ReactuseState/useEffect,非常适合作为学习 WalletConnect v2 协议流程的入门范本。需要特别说明的是:README 开篇提到使用wagmi和web3modal,但从实际源码看,sign-example 走的是更底层的两条路径——@walletconnect/sign-client(协议客户端)与@web3modal/standalone(独立二维码弹窗),并未直接使用 wagmi hooks,这一点在阅读时需以源码为准。

二、环境准备:依赖安装与 Project ID 配置

2.1 安装依赖

进入示例目录后执行:

yarn

该工程的依赖清单见 package.json,核心依赖及版本如下:

依赖包版本作用
@walletconnect/sign-client^2.9.2WalletConnect v2 协议客户端,负责会话建立、请求分发
@web3modal/standalone^2.4.3独立二维码弹窗组件,用于展示连接 URI
@material-ui/core^4.12.4UI 组件库(按钮)
react/react-dom^18.2.0前端框架
react-scripts5.0.1CRA 构建脚本
viem^1.6.0现代以太坊工具库(wagmi 底层依赖)
wagmi^1.3.10以太坊 React Hooks 库(本示例未直接使用)

2.2 配置 Project ID

在项目根目录创建.env文件,并填入从 WalletConnect Cloud 获取的 Project ID:

REACT_APP_PROJECT_ID=your_project_id_here

REACT_APP_前缀是 Create React App 约定,保证该变量能被注入到浏览器端代码中。源码中读取方式如下(src/App.js):

const projectId = process.env.REACT_APP_PROJECT_ID || "453f2a8e1d89bc35b8bc49eb781167b9"; const web3Modal = new Web3Modal({ projectId, walletConnectVersion: 2 });

注意两点:

  • 仓库中内置了一个兜底 Project ID,便于未配置环境变量时直接运行;但生产环境强烈建议通过.env配置自己的 Project ID,避免共享配额与统计污染;
  • Project ID 同时用于Web3Modal初始化与Client.init,是 WalletConnect v2 的必填凭证。

2.3 启动

yarn start

package.json中start脚本为GENERATE_SOURCEMAP=false react-scripts start,关闭 sourcemap 以加快开发启动速度。浏览器打开后即可看到 "WalletConnect Sign Demo" 页面。

三、双初始化:Web3Modal 与 SignClient

sign-example 采用"双实例"架构,两者职责分离:

import Client from "@walletconnect/sign-client"; import { Web3Modal } from "@web3modal/standalone"; // 二维码弹窗实例(全局单例,模块顶层创建) const web3Modal = new Web3Modal({ projectId, walletConnectVersion: 2 }); // SignClient 实例(异步创建,保存在 state 中) const createClient = async () => { const newClient = await Client.init({ projectId, relayUrl: "wss://relay.walletconnect.com", }); setClient(newClient); subscribeToEvents(newClient); };

关键点解析:

  • Client.init是 v2 客户端的入口,relayUrl指向官方中继服务器wss://relay.walletconnect.com,负责 DApp 与钱包之间的消息转发(加密中继,中继节点无法解密消息内容);
  • Web3Modal的walletConnectVersion: 2显式声明使用 v2 协议,这是与 v1(bridge 模式)区分的关键参数;
  • createClient在useEffect中于首次渲染时被调用一次(if (!client) createClient()),并随即注册事件监听,保证后续任何交互前客户端已就绪。

四、建立会话:连接钱包的完整流程

handleConnect是核心的连接逻辑,完整代码如下(src/App.js):

const handleConnect = async () => { if (!client) { console.error("Client not initialized"); return; } try { const proposalNamespace = { eip155: { methods: ["eth_sendTransaction", "personal_sign"], chains: ["eip155:1"], events: ["chainChanged", "accountsChanged"], }, }; const { uri, approval } = await client.connect({ requiredNamespaces: proposalNamespace, }); if (uri) { web3Modal.openModal({ uri }); const sessionNamespace = await approval(); onSessionConnected(sessionNamespace); web3Modal.closeModal(); } } catch (error) { console.error("Failed to connect:", error); } };

4.1 提案命名空间(proposalNamespace)

requiredNamespaces是 v2 协议的核心概念,它声明了 DApp 需要钱包授权的"能力清单",逐字段说明:

字段值含义
methods["eth_sendTransaction", "personal_sign"]允许 DApp 向钱包请求的方法。本示例声明了发交易与签名两种能力,但实际只调用了personal_sign
chains["eip155:1"]命名空间标识符,eip155:1即以太坊主网(Chain ID 1)
events["chainChanged", "accountsChanged"]钱包需向 DApp 推送的事件类型,链切换与账户切换是钱包侧的标准事件

钱包用户在确认授权时会看到这份清单,因此只声明实际需要的方法与链,是良好的最小权限实践。

4.2 连接三步曲:uri → openModal → approval

  1. client.connect(proposal)返回{ uri, approval }:uri是形如wc:开头的连接字符串,编码了对钱包的邀请信息;
  2. web3Modal.openModal({ uri })将 uri 渲染为二维码,用户使用支持 WalletConnect v2 的钱包扫码;
  3. await approval()是一个 Promise,挂起等待用户在钱包端确认授权。只有用户批准后才会 resolve,返回完整的会话命名空间(session namespace),随后关闭弹窗并进入已连接状态。

这段代码将"发起请求"与"等待批准"分离,是 v2 会话建立的经典模式。

五、会话生命周期与事件订阅

客户端初始化后立即注册了四类事件监听(src/App.js):

const subscribeToEvents = (client) => { client.on("session_ping", () => alert("Ping received")); client.on("session_event", (args) => console.log("Event received:", args)); client.on("session_update", ({ topic, params }) => { console.log("Session updated:", params); const updatedSession = { ...client.session.get(topic), namespaces: params.namespaces }; onSessionConnected(updatedSession); }); client.on("session_delete", () => { console.log("Session deleted"); reset(); }); };

各事件语义如下:

事件触发时机示例处理方式
session_ping对端心跳探测alert提示收到 Ping
session_event钱包推送链上事件(如交易回执)打印事件负载
session_update会话命名空间变化(如账户/链切换)通过client.session.get(topic)重新拉取会话,合并新的 namespaces 后刷新 UI
session_delete会话被删除(钱包端主动断开等)调用reset()清空账户与会话状态

其中session_update的处理非常关键:钱包端切换账户或链后,DApp 需要同步刷新界面。源码通过client.session.get(topic)读取最新会话,再更新 UI 状态,实现"被动刷新"。

六、发起 personal_sign 签名请求

连接建立后,点击 "Personal Sign" 按钮触发签名(src/App.js):

const handleSend = async () => { if (!account || !session) { console.error("No account or session found"); return; } try { const message = "My email is john@doe.com - 1691813801271"; const result = await client.request({ topic: session.topic, chainId: "eip155:1", request: { method: "personal_sign", params: [message, account], }, }); alert("Signature: " + result); } catch (error) { console.error("Failed to sign:", error); } };

这段代码演示了 v2 协议中所有 RPC 请求的统一入口client.request,参数逐项说明:

  • topic:当前会话的唯一标识,路由请求到对应钱包;
  • chainId:"eip155:1",指明请求作用于哪条链,必须与提案中的chains匹配;
  • request.method:personal_sign,EIP-191 前缀签名(\x19Ethereum Signed Message:\n),用于"证明持有某地址"或"登录鉴权"场景;
  • request.params:[message, account],消息原文与签名账户地址。示例中的消息"My email is john@doe.com - 1691813801271"是典型的"邮箱 + 时间戳"防重放登录消息模式;
  • 返回值result:钱包私钥签出的十六进制签名串,可直接用于链下验证(如恢复签名者地址)。

在 Dapp-Learning 仓库的 multichain-example/src/utils/securityUtils.js 中可以看到对应的验证侧实现:通过ethers.utils.verifyMessage(message, signature)恢复签名者地址并与期望地址比对,构成"签名—验证"的完整闭环。

七、主动断开会话

点击 "Disconnect" 按钮调用handleDisconnect(src/App.js):

const handleDisconnect = async () => { if (!client || !session) return; try { await client.disconnect({ topic: session.topic, message: "User disconnected", code: 6000, }); reset(); } catch (error) { console.error("Failed to disconnect:", error); } };

disconnect需要三个参数:会话topic、面向对端的message与 JSON-RPC 错误码code。示例使用6000(自定义应用错误码区间),通知钱包端会话已被 DApp 关闭;成功后调用reset()清空本地状态,UI 回到待连接界面。此后再收到session_delete事件时也会走同样的reset()路径。

八、UI 状态机与组件结构

App 组件的渲染逻辑以account是否存在为分界(src/App.js):

{account ? ( <> <p>Connected Account: {account}</p> <Button variant="contained" onClick={handleSend}>Personal Sign</Button> <Button variant="contained" color="secondary" onClick={handleDisconnect}>Disconnect</Button> </> ) : ( <Button variant="contained" onClick={handleConnect} disabled={!client}>Connect</Button> )}
  • 未连接态:仅显示 "Connect" 按钮,且disabled={!client},防止 SignClient 尚未初始化完成时被点击;
  • 已连接态:显示账户地址、签名按钮与断开按钮;
  • 账户地址的提取:sessionNamespace.namespaces.eip155.accounts[0].slice(9)中,accounts[0]形如"eip155:1:0xAbC...",slice(9)恰好剥离"eip155:1:"前缀(9 个字符)得到纯地址。

reset()统一重置账户与会话状态,被主动断开、被动session_delete两条路径复用,避免状态残留。

九、与同仓库其他 v2 示例的横向对比

Dapp-Learning 的 WalletConnect v2 目录下还有两个姊妹示例,三者对比有助于理解技术选型梯度:

维度sign-examplebasic-dapp-examplemultichain-example
核心 SDK@walletconnect/sign-client+@web3modal/standalone@web3modal/ethereum+@web3modal/react+wagmiwagmihooks + ethers.js
功能侧重连接 +personal_sign签名一键连接 + 网络切换多链连接、余额查询、交易安全校验
链支持单链(eip155:1)mainnet / arbitrum / polygon / localhostEthereum / Polygon / Arbitrum / Optimism / BSC

其中 basic-dapp-example 的 src/App.js 展示了更上层的封装:通过configureChains+w3mConnectors+createConfig构建 wagmi 配置,再渲染<Web3Button>/<Web3NetworkSwitch>组件即可完成连接;而 multichain-example 的 chainUtils.js 与 securityUtils.js 则展示了生产级的多链 RPC 配置与交易风险校验(如isHighRiskTransaction对approve/transferFrom等敏感方法的识别)。sign-example 介于两者之间——比 wagmi 封装更贴近协议层,比多链示例更聚焦单链签名,是理解 v2 协议内部机制的最佳切入点。

十、常见问题与排查建议

  • 二维码未弹出:确认Client.init已成功(client非空,Connect 按钮可点击),并检查.env中REACT_APP_PROJECT_ID是否正确读取——可临时在控制台打印projectId验证;
  • 钱包扫码后无响应:确认钱包支持 WalletConnect v2(部分老版本钱包仅支持 v1),并检查relayUrl中继连接是否通畅(浏览器控制台的 WebSocket 日志);
  • personal_sign报错:确认请求chainId与提案chains一致、params中账户地址与已连接账户一致;若钱包拒绝,错误会体现在catch中,可按错误信息区分"用户拒绝"与"参数非法";
  • 签名后账户显示异常:检查slice(9)的前提是账户格式为eip155:1:0x...;若使用了其他链的命名空间(如eip155:137),前缀长度相同(9 字符)仍可正常工作,但建议改为正则解析以更稳健;
  • 会话不同步:钱包端切换账户/链后 DApp 未刷新,通常需要确认session_update监听已注册(subscribeToEvents在createClient后立即调用)。

参考与延伸阅读

  • 示例说明文档:basic/78-wallet-connect/v2/sign-example/README.md
  • 核心源码:src/App.js
  • 依赖清单:package.json
  • 同目录基础连接示例:basic-dapp-example/README.md
  • 同目录多链示例:multichain-example/README.md
  • WalletConnect 协议概述与安装指南:basic/78-wallet-connect/docs/introduction/overview.md、basic/78-wallet-connect/docs/guides/installation.md
  • WalletConnect v1/v2 对比与整体介绍:basic/78-wallet-connect/README.md
  • 示例工程
  • 区块链

【免费下载链接】Dapp-Learning

Dapp learning project for developers at all stages. Becoming and cultivating sovereign individuals. Nonprofit organization.

项目地址:https://gitcode.com/gh_mirrors/da/Dapp-Learning
点击查看免费下载

相关推荐

上一篇:gRPC-Web 完整实战指南:在浏览器中调用 gRPC 服务
下一篇:Data-Juicer 后微调对话数据格式转换指南:打通 Swift 与 LLaMA-Factory 的 8 个内置工具

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进