- 示例工程
- 区块链
【免费下载链接】Dapp-Learning
Dapp learning project for developers at all stages. Becoming and cultivating sovereign individuals. Nonprofit organization.
本篇技术指南以 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 聚焦一个最小但完整的闭环:
- 初始化 WalletConnect v2 客户端(SignClient);
- 发起连接提案,等待用户钱包扫码/授权;
- 会话建立后展示已连接账户;
- 向钱包发起
personal_sign签名请求并展示签名结果; - 支持主动断开与事件驱动的被动断开。
整个工程只有一个核心组件文件 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.2 | WalletConnect v2 协议客户端,负责会话建立、请求分发 |
@web3modal/standalone | ^2.4.3 | 独立二维码弹窗组件,用于展示连接 URI |
@material-ui/core | ^4.12.4 | UI 组件库(按钮) |
react/react-dom | ^18.2.0 | 前端框架 |
react-scripts | 5.0.1 | CRA 构建脚本 |
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_hereREACT_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 startpackage.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
client.connect(proposal)返回{ uri, approval }:uri是形如wc:开头的连接字符串,编码了对钱包的邀请信息;web3Modal.openModal({ uri })将 uri 渲染为二维码,用户使用支持 WalletConnect v2 的钱包扫码;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-example | basic-dapp-example | multichain-example |
|---|---|---|---|
| 核心 SDK | @walletconnect/sign-client+@web3modal/standalone | @web3modal/ethereum+@web3modal/react+wagmi | wagmihooks + ethers.js |
| 功能侧重 | 连接 +personal_sign签名 | 一键连接 + 网络切换 | 多链连接、余额查询、交易安全校验 |
| 链支持 | 单链(eip155:1) | mainnet / arbitrum / polygon / localhost | Ethereum / 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.
相关推荐
QKeyMapper:Windows平台终极输入设备映射解决方案,让游戏手柄秒变键鼠
QKeyMapper:Windows平台终极输入设备映射解决方案,让游戏手柄秒变键鼠 你是否曾为心爱的游戏手柄无法在PC游戏中正常使用而烦恼?是否想要为办公软件
示例工程区块链Dapp-Learning 实战:基于 Hardhat + Express + React 构建全栈 ERC20 DApp
Dapp Learning 实战:基于 Hardhat + Express + React 构建全栈 ERC20 DApp 导读 本教程以 Dapp Learn
示例工程区块链如何把QQ空间历史说说批量导出成Excel?GetQzonehistory 完整使用指南
如何把QQ空间历史说说批量导出成Excel?GetQzonehistory 完整使用指南 QQ 空间首页只推近几条动态,几年前写的内容想翻都翻不到。如果你希望
示例工程区块链
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考