EIP-1193 & EIP-6963
USB 标准化之前,每台设备都有自己的接口:打印机有打印机口,鼠标有 PS/2 口,调制解调器有串口。 USB 做的事很简单:定义一个通用语言,让所有外设能和主机说同一种话。 但 USB 只解决了"怎么说话",没解决"怎么找到设备"——那是 USB 设备枚举协议做的事。
Web3 浏览器钱包面临的,是同一个两阶段问题: 先要有人定义 dApp 和钱包"说同一种语言"的规范(EIP-1193), 再要有人定义多个钱包同时存在时 dApp "如何找到并选择它们"(EIP-6963)。 这两个 EIP 不是竞争关系,是同一个问题的两个层次。
EIP-1193 是"通信协议",EIP-6963 是"发现协议"。标准化让它们能交流;发现机制让它们知道对方的存在。
背景:混乱的 window.ethereum 时代
↑ top
Web3 的浏览器插件钱包最早没有任何标准。MetaMask 在 2016 年率先将一个名为 web3 的对象注入到页面的全局作用域,
dApp 通过 window.web3.eth.sendTransaction() 发送交易。其他钱包——Brave Wallet、Coinbase Wallet、imToken——
纷纷效仿,但每家的 API 形状都略有不同。
很多开发者以为这个问题后来被统一了。其实只是暂时被压制了。
EIP-1193 在 2019 年标准化了 window.ethereum 这个 Provider 接口,但它没有规定"谁有权利把自己放进 window.ethereum"。
结果是:当用户同时安装了 MetaMask 和 Phantom,最后加载的那个插件会悄悄覆盖前一个写入的值。
dApp 读到的是哪个钱包,取决于插件的加载顺序——这是一个竞争条件,不是设计。
| 时代 | 方式 | 问题 |
|---|---|---|
| 2016–2018 | window.web3,各家自定义 API |
没有标准,dApp 要对每个钱包单独适配 |
| 2019–2022 | EIP-1193 标准化 window.ethereum |
接口统一了,但多钱包冲突未解决:最后注入者覆盖前者 |
| 2023–今 | EIP-6963 引入事件式多钱包发现 | 多钱包共存,dApp 主动枚举,用户自由选择 |
window.ethereum 是单例变量,天然不支持多值。EIP-6963 的核心思路是:放弃争夺同一个变量,改用事件总线让所有钱包各自报到。
EIP-1193:Provider 接口标准
↑ topEIP-1193(由 Ryan Ghods 等人提出,2019 年 Final)定义了一个最小化的 JavaScript 接口, 让任何符合标准的钱包都能被任何符合标准的 dApp 使用,无需了解彼此的内部实现。
接口极简:一个方法(request)+ 三个事件监听方法(on / removeListener / once)。
这不是偶然的——越小的接口越难被打破,越容易被正确实现。
request() 方法 — 唯一的入口
整个 EIP-1193 最核心的约定只有一个方法签名:
request() 是一个 Promise 化的 JSON-RPC 调用。method 是任何合法的 JSON-RPC 方法名,
params 是对应的参数。如果请求成功,Promise resolve 为结果;如果失败,reject 为一个 ProviderRpcError。
常用的 JSON-RPC 方法:
| 方法 | 说明 | 典型返回值 |
|---|---|---|
| eth_requestAccounts | 请求连接钱包,弹出授权弹窗 | string[] — 账户地址数组 |
| eth_accounts | 获取当前已授权账户(不弹窗) | string[] |
| eth_chainId | 获取当前链 ID | "0xc4" — 十六进制字符串 |
| eth_sendTransaction | 发送交易(需用户确认) | string — 交易哈希 |
| eth_sign / personal_sign | 签名消息 | string — 签名结果 |
| wallet_switchEthereumChain | 请求切换到指定链 | null |
| wallet_addEthereumChain | 请求添加自定义链 | null |
Provider 事件
Provider 还需要支持标准事件,让 dApp 响应钱包状态变化,而不是主动轮询。
| 事件 | 触发时机 | 回调参数 |
|---|---|---|
| connect | Provider 连接到链时(首次或重连) | { chainId: string } |
| disconnect | Provider 与链断开连接 | ProviderRpcError |
| chainChanged | 用户在钱包中切换了网络 | string — 新 chainId(十六进制) |
| accountsChanged | 用户切换账户或撤销授权 | string[] — 新账户列表 |
| message | 收到订阅推送(如 eth_subscribe) | { type: string, data: unknown } |
EIP-1193 规范建议在收到 chainChanged 事件后刷新页面,以避免因链切换导致的状态不一致。大多数生产级 dApp 都遵循这个建议。
错误码
当 request() 失败时,会抛出一个实现了以下接口的 ProviderRpcError:
| Code | 含义 | 常见触发场景 |
|---|---|---|
| 4001 | User Rejected Request — 用户主动拒绝 | 用户点击取消连接弹窗、拒绝交易确认 |
| 4100 | Unauthorized — 未获授权 | dApp 未调用 eth_requestAccounts 就直接调用需要授权的方法 |
| 4200 | Unsupported Method — 不支持该方法 | 钱包未实现该 JSON-RPC 方法 |
| 4900 | Disconnected — Provider 未连接任何链 | 钱包离线、网络错误 |
| 4901 | Chain Disconnected — 未连接到请求的链 | 当前连接链与请求操作的目标链不符 |
交互演示:构造 EIP-1193 请求
EIP-6963:多钱包发现机制
↑ topEIP-6963(由 Pedro Gomes 等人提出,2023 年 Final)解决了一个在 EIP-1193 标准化之后才变得更严重的问题: 当多个 EIP-1193 兼容钱包同时存在时,dApp 如何感知它们的存在,用户如何做出选择。
问题:最后写入者胜出
理解 EIP-6963 的前提,是理解它想解决的问题有多荒谬。
假设用户同时安装了 MetaMask 和 Phantom。两个插件都会在页面加载时执行
window.ethereum = myProvider。
执行顺序取决于浏览器插件的加载顺序——而这个顺序取决于用户的安装顺序、插件的启动速度,甚至当天浏览器的心情。
dApp 看到的 window.ethereum 永远只有一个,另一个钱包对它来说根本不存在。
EIP-1193 假设"有且仅有一个 Provider"。但现实是用户可能同时安装多个钱包。window.ethereum 是单例,承载不了多值。
解决方案:事件总线式广播
EIP-6963 的解决思路借鉴了广播协议(如 mDNS/Bonjour): 不要争夺一个全局变量,改用事件系统让所有参与者主动声明自己的存在。
协议定义了两个自定义事件:
| 事件名 | 发出方 | 时机 | 作用 |
|---|---|---|---|
| eip6963:announceProvider | 钱包 | 页面加载时主动广播;收到 request 事件时重新广播 | 声明自己存在,携带 Provider 详情 |
| eip6963:requestProvider | dApp | 需要枚举可用钱包时 | 触发所有钱包重新广播(用于晚启动的 dApp) |
EIP6963ProviderInfo 结构
每次 announceProvider 事件携带一个 EIP6963ProviderDetail 对象:
rdns 是关键字段:它使用反向域名格式(类似 Android App ID),在所有支持 EIP-6963 的钱包中全局唯一,
可用于精确标识某个特定钱包(例如,dApp 可以通过 rdns === "io.metamask" 筛选 MetaMask)。
完整发现流程
如果 dApp 比钱包插件晚加载(例如懒加载场景),它可能错过初始的 announceProvider 广播。
发送 requestProvider 事件会触发所有钱包重新广播,确保 dApp 不会遗漏任何已安装的钱包。
协议如何配合
↑ top
EIP-1193 和 EIP-6963 分工明确,层次清晰:
EIP-6963 负责发现("这里有哪些钱包"),EIP-1193 负责通信("怎么和选中的钱包交互")。
发现完成后,拿到的 EIP6963ProviderDetail.provider 就是一个标准的 EIP-1193 Provider,直接使用。
钱包侧:如何正确实现 EIP-6963
EIP-6963 要求 detail 是 frozen 的 — 防止 dApp 或恶意脚本篡改 Provider 对象。这是协议安全模型的一部分。
dApp 侧:枚举并展示钱包选择器
从旧模式迁移:window.ethereum 还能用吗
很多人问:实现了 EIP-6963 之后,是否还需要 window.ethereum?
答案是:暂时两者并行。window.ethereum 仍然是大量 dApp 和钱包的既有接口,短期内不会消失。
推荐做法是:优先使用 EIP-6963,回退到 window.ethereum。
安全考量
↑ topEIP-1193 的安全边界
| 风险 | 说明 | 缓解措施 |
|---|---|---|
| 钓鱼 Provider | 恶意页面注入伪造的 window.ethereum,劫持 request() 调用 |
用户应通过浏览器扩展安装钱包,不信任页面注入的 Provider;钱包应在安全上下文(content script)中注入 |
| 过度权限请求 | dApp 在未告知用户的情况下调用高权限方法(如 eth_sign) |
钱包应对每个高危方法显示清晰的用户确认弹窗 |
| 事件监听泄漏 | 忘记移除事件监听器,导致内存泄漏和重复触发 | 使用 removeListener 及时清理;只在需要时注册监听器 |
EIP-6963 的安全考量
| 风险 | 说明 | 缓解措施 |
|---|---|---|
| 恶意 announceProvider | 恶意脚本派发伪造的 announceProvider 事件,冒充合法钱包 |
协议本身无法完全防止;用户应辨别钱包名称和图标,dApp 可通过 rdns 做已知钱包白名单校验 |
| Provider 对象篡改 | 事件 detail 被中间代码修改,Provider 行为被劫持 | 规范要求 Object.freeze(detail),使 detail 不可变 |
| UUID 碰撞 | 两个钱包使用相同的 UUID,导致 dApp 错误去重 | UUID v4 碰撞概率极低;dApp 可同时使用 rdns 辅助去重 |
EIP-6963 的安全模型依赖于"合法钱包在受信任的浏览器扩展上下文中运行"这一前提。 它无法防止运行在同一页面上下文中的恶意脚本伪造钱包事件。 最终的安全保障来自浏览器的扩展隔离机制,而不是协议本身。
Quick Reference
↑ top| EIP-1193 — Provider 接口 | ||
|---|---|---|
| 项目 | 值 | 说明 |
| 核心方法 | provider.request({ method, params }) |
所有 JSON-RPC 调用的统一入口,返回 Promise |
| 事件订阅 | provider.on(event, listener) |
监听 connect / disconnect / chainChanged / accountsChanged |
| 错误基类 | ProviderRpcError |
扩展自 Error,附带 code 字段(4001=拒绝,4100=未授权…) |
| 连接账户 | eth_requestAccounts |
弹出授权弹窗,返回账户地址数组 |
| 获取链 | eth_chainId |
返回十六进制字符串,如 "0x1"(主网)、"0xc4"(X Layer) |
| EIP-6963 — 多钱包发现 | ||
|---|---|---|
| 项目 | 值 | 说明 |
| 钱包广播事件 | eip6963:announceProvider |
钱包主动派发,携带 frozen EIP6963ProviderDetail |
| dApp 请求事件 | eip6963:requestProvider |
dApp 派发,触发所有钱包重新广播 |
| 身份字段 | rdns |
反向域名,全局唯一,用于精确识别钱包(如 "io.metamask") |
| 去重字段 | uuid |
UUID v4,每个钱包实例唯一,用于 Map 去重 |
| 不可变要求 | Object.freeze(detail) |
防止 Provider 对象被篡改,协议安全基础 |
理解测验
↑ topQ1. EIP-1193 定义的 request() 方法的正确返回类型是?
Q2. 当用户在 MetaMask 中切换网络时,EIP-1193 规范要求 Provider 派发哪个事件?
Q3. EIP-1193 错误码 4001 代表什么?
Q4. EIP-6963 中 rdns 字段的主要用途是?
Q5. 为什么 EIP-6963 要求钱包广播时使用 Object.freeze(detail)?
Q6. dApp 派发 eip6963:requestProvider 事件的主要目的是?
Summary
↑ top| 项目 | EIP-1193 | EIP-6963 |
|---|---|---|
| 解决的问题 | dApp 与钱包如何通信(接口标准) | 多钱包共存时 dApp 如何发现它们(发现协议) |
| 核心机制 | provider.request() + 事件系统 |
announceProvider / requestProvider 双向事件 |
| 关键接口 | EIP1193Provider:request + on + removeListener |
EIP6963ProviderDetail:info(uuid/name/icon/rdns)+ provider |
| 错误处理 | ProviderRpcError,code 4001/4100/4200/4900/4901 |
—(使用底层 EIP-1193 错误) |
| 向后兼容 | 沿用 window.ethereum 单例 |
推荐优先使用,回退到 window.ethereum |
| 状态 | Final(2019) | Final(2023) |
EIP-1193 是通信层,EIP-6963 是发现层。它们不是替代关系,而是叠加关系:
先用 EIP-6963 发现所有可用钱包,用户选择后,用 EIP-1193 和所选钱包进行通信。
EIP-6963 的 detail.provider 直接就是一个 EIP-1193 Provider,无需任何适配。