EIP-6963: 多注入 Provider 发现
使用 window 事件来宣告注入的钱包 Provider
Authors | Pedro Gomes (@pedrouid), Kosala Hemachandra (@kvhnuke), Richard Moore (@ricmoo), Gregory Markou (@GregTheGreek), Kyle Den Hartog (@kdenhartog), Glitch (@glitch-txs), Jake Moxey (@jxom), Pierre Bertet (@bpierre), Darryl Yeo (@darrylyeo), Yaroslav Sergievsky (@everdimension) |
---|---|
Created | 2023-05-01 |
Requires | EIP-1193 |
Table of Contents
摘要
一个替代的发现机制,用于 EIP-1193 Provider 的 window.ethereum
,它支持使用 Javascript 的 window
事件在网页中发现多个注入的钱包 Provider。
动机
目前,提供浏览器扩展的钱包 Provider 必须将其 Ethereum Provider (EIP-1193) 注入到同一个 window 对象 window.ethereum
中;然而,这会为安装了多个浏览器扩展的用户带来冲突。
浏览器扩展以不可预测和不稳定的顺序加载到网页中,导致竞争条件,用户无法控制选择哪个钱包 Provider 在 window.ethereum
对象下公开 Ethereum 接口。相反,通常是最后加载的钱包获胜。
这不仅导致用户体验下降,而且增加了新浏览器扩展的进入门槛,因为用户被迫一次只安装一个浏览器扩展。
一些浏览器扩展试图通过延迟注入来抵消这个问题,以覆盖同一个 window.ethereum
对象,这为钱包 Provider 带来了不公平的竞争和缺乏互操作性。
在本提案中,我们提出了一种解决方案,该方案侧重于优化多个钱包 Provider 的互操作性。该解决方案旨在通过减少新钱包 Provider 的进入门槛来促进更公平的竞争,同时增强 Ethereum 网络上的用户体验。
这是通过引入一组 window 事件来实现的,这些事件提供 Ethereum 库和浏览器扩展提供的注入脚本之间的双向通信协议,从而使用户可以选择他们选择的钱包。
规范
本文档中的关键词“必须”,“不得”,“必需”,“应该”,“不应该”,“建议”,“可以”和“可选”应按照 RFC-2119 中所述进行解释。
定义
钱包 Provider:一种管理密钥并促进与 Ethereum 进行交易的用户代理。
去中心化应用程序 (DApp):一个依赖于一个或多个 Web3 平台 API 的网页,这些 API 通过钱包暴露给网页。
Provider 发现库:一个协助 DApp 与钱包交互的库或软件。
Provider 信息
每个钱包 Provider 都将通过以下接口 EIP6963ProviderInfo
进行宣告。EIP6963ProviderInfo
中的值必须包含在 EIP6963ProviderInfo
对象中。EIP6963ProviderInfo
还可以包含对象中的额外可扩展属性。如果 DApp 无法识别其他属性,则应忽略它们。
uuid
- 钱包 Provider 的全局唯一标识符,必须符合 (UUIDv4),以便在页面生命周期内唯一区分具有以下匹配属性的不同 EIP-1193 provider 会话。UUIDv4 提供的加密唯一性保证了可以分别识别两个独立的EIP6963ProviderInfo
对象。name
- 钱包 Provider 的人类可读的本地别名,将在 DApp 上向用户显示。(例如Example Wallet Extension
或Awesome Example Wallet
)icon
- 指向图像的 URI。图像应为正方形,最小分辨率为 96x96px。有关此属性的进一步要求,请参见下面的 图像/图标。rdns
- 钱包必须提供rdns
属性,该属性旨在成为来自域名系统的反向语法排序的域名,例如com.example.subdomain
。由钱包来确定他们希望使用的域名,但通常期望标识符在钱包的整个开发过程中保持不变。还值得注意的是,与浏览器中的用户代理字符串类似,有时提供的价值可能未知、无效、不正确或试图模仿不同的钱包。因此,DApp 应该能够处理这些故障情况,并且对 DApp 功能的降级程度最小。
/**
* 表示显示钱包所需的资产
*/
interface EIP6963ProviderInfo {
uuid: string;
name: string;
icon: string;
rdns: string;
}
图像/图标
选择 URI 编码的图像是为了能够灵活地使用多种协议来获取和渲染图标,例如:
# svg (data uri)
data:image/svg+xml,<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32px" height="32px" viewBox="0 0 32 32"><circle fill="red" cx="16" cy="16" r="12"/></svg>
# png (data uri)
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==
icon
字符串必须是 RFC-2397 中定义的数据 URI。图像应为正方形,最小分辨率为 96x96px。建议图像格式为无损或基于矢量的格式,例如 PNG、WebP 或 SVG,以使图像易于在 DApp 上渲染。由于 SVG 图像可以执行 Javascript,因此应用程序和库必须使用 <img>
标记渲染 SVG 图像,以确保不会发生不受信任的 Javascript 执行。
RDNS
rdns
(反向 DNS) 属性用于提供 DApp 可以依赖的在会话之间保持稳定的标识符。选择反向域名表示法是为了防止命名空间冲突。
反向 DNS 约定意味着该值应以 Provider 控制的反向 DNS 域名开头。域名后应跟一个子域名或产品名称。示例:com.example.MyBrowserWallet
。
rdns
值必须是有效的 RFC-1034 域名;rdns
值的 DNS 部分应该是 Provider 控制的活动域名;- DApp 可以拒绝未正确遵循反向 DNS 约定的 Provider;
- DApp 不应将
rnds
值用于功能检测,因为这些值是自我证明的,并且容易受到模仿或不良激励的影响,而没有额外的验证机制;功能发现和验证都不在此接口规范的范围之内。
Provider 详细信息
EIP6963ProviderDetail
用作组合接口,用于宣告钱包 Provider 和有关钱包 Provider 的相关元数据。EIP6963ProviderDetail
必须包含类型为 EIP6963ProviderInfo
的 info
属性和 EIP-1193 定义的类型为 EIP1193Provider
的 provider
属性。
interface EIP6963ProviderDetail {
info: EIP6963ProviderInfo;
provider: EIP1193Provider;
}
Window 事件
为了防止 provider 冲突,DApp 和钱包应发出一个事件并实例化一个 eventListener 来发现各种钱包。这形成了一个事件并发循环。
由于 DApp 代码和钱包代码不能保证以特定的顺序运行,因此这些事件旨在处理此类竞争条件。
为了发出事件,DApp 和钱包必须使用 window.dispatchEvent
函数来发出事件,并且必须使用 window.addEventListener
函数来观察事件。DApp 和钱包之间使用两个 Event 接口来相互发现。
宣告和请求事件
EIP6963AnnounceProviderEvent
接口必须是一个 CustomEvent
对象,其 type
属性包含一个字符串值 eip6963:announceProvider
,其 detail
属性包含一个类型为 EIP6963ProviderDetail
的对象值。应该通过在 detail
属性的值上调用 Object.freeze()
来冻结 EIP6963ProviderDetail
对象。
// 由钱包分发的宣告事件
interface EIP6963AnnounceProviderEvent extends CustomEvent {
type: "eip6963:announceProvider";
detail: EIP6963ProviderDetail;
}
EIP6963RequestProviderEvent
接口必须是一个 Event
对象,其 type
属性包含一个字符串值 eip6963:requestProvider
。
// 由 DApp 分发的请求事件
interface EIP6963RequestProviderEvent extends Event {
type: "eip6963:requestProvider";
}
钱包必须通过 window.dispatchEvent()
函数调用向 DApp 宣告 EIP6963AnnounceProviderEvent
。钱包必须添加一个 EventListener 以捕获从 DApp 分发的 EIP6963RequestProviderEvent
。此 EventListener 必须使用一个将重新分发 EIP6963AnnounceProviderEvent
的处理程序。当钱包的初始事件宣告可能被延迟或在 DApp 初始化其 EventListener 之前触发时,钱包的重新宣告很有用。这允许各种钱包 Provider 对 DApp 做出反应,而无需污染 window.ethereum
命名空间,这可能会产生不确定的钱包行为,例如每次连接不同的钱包。
钱包分发带有不可变内容的 "eip6963:announceProvider"
事件,并侦听 "eip6963:requestProvider"
事件:
let info: EIP6963ProviderInfo;
let provider: EIP1193Provider;
const announceEvent: EIP6963AnnounceProviderEvent = new CustomEvent(
"eip6963:announceProvider",
{ detail: Object.freeze({ info, provider }) }
);
// 钱包分发一个宣告事件,该事件被
// 之前运行的 DApp 代码听到
window.dispatchEvent(announceEvent);
// 钱包侦听稍后可能分发的请求事件,并重新分发 `EIP6963AnnounceProviderEvent`
window.addEventListener("eip6963:requestProvider", () => {
window.dispatchEvent(announceEvent);
});
DApp 必须通过 window.addEventListener()
方法侦听钱包分发的 EIP6963AnnounceProviderEvent
,并且不得在页面的生命周期内删除 Event Listener,以便 DApp 可以继续处理超出初始页面加载交互的事件。DApp 必须在 EIP6963AnnounceProviderEvent
处理程序初始化后,通过 window.dispatchEvent()
函数调用分发 EIP6963RequestProviderEvent
。
// DApp 侦听宣告的 provider
window.addEventListener(
"eip6963:announceProvider",
(event: EIP6963AnnounceProviderEvent) => {}
);
// DApp 分发一个请求事件,该事件将被
// 之前运行的钱包代码听到
window.dispatchEvent(new Event("eip6963:requestProvider"));
DApp 可以选择持久化多个钱包发送的宣告事件中包含的各种 EIP6963ProviderDetail
对象。因此,如果用户希望随着时间的推移使用不同的钱包,则用户可以在 DApp 的界面中表达这一点,并且 DApp 可以立即选择将交易发送到该新钱包。否则,DApp 可以通过分发新的 EIP6963RequestProviderEvent
来重新启动钱包发现流程,从而有可能发现一组不同的钱包。
所描述的事件编排保证了 DApp 能够发现钱包,而不管哪个代码先执行,是钱包代码还是 DApp 代码。
理由
之前的提案引入了依赖于单个、可变 window 对象的机制,该对象可以被多方覆盖。我们选择了一种基于事件的方法,以避免竞争条件、命名空间冲突以及对共享可变对象的潜在“污染”攻击;基于事件的编排在钱包和 dapp 之间创建了一个可以随时间重新编排的双向通信通道。
为了遵循 Javascript 事件名称约定,这些名称以现在时态书写,并以本文档的编号 (EIP6963
) 为前缀。
接口
标准化 provider 信息接口 (EIP6963ProviderInfo
) 允许 DApp 确定填充用户友好的钱包选择模式所需的所有信息。这对于依赖于 Web3Modal、RainbowKit、Web3-Onboard 或 ConnectKit 等库以编程方式生成此类选择模式的 DApp 尤其有用。
关于宣告的 provider 接口 (EIP6963ProviderDetail
),重要的是保持 EIP-1193 provider 接口不变以实现向后兼容性;这允许符合规范的 DApp 与符合任一规范的钱包进行交互,并允许符合此规范的钱包仍然为旧版 DApp 注入 EIP-1193 provider。请注意,如果存在多个旧版 dapp 或符合此规范的 DApp 连接到旧版钱包,则不能保证会选择正确的钱包。
向后兼容性
此 EIP 不需要取代 window.ethereum
,因此它不会直接破坏无法更新到此钱包发现方法的现有应用程序。但是,建议 DApp 实现此 EIP 以确保发现多个钱包 Provider,并且应该禁用 window.ethereum
用法,除非在发现失败时作为故障转移。同样,钱包应该保持 window.ethereum
的兼容性,以确保尚未实现此 EIP 的 DApp 的向后兼容性。为了防止先前出现的命名空间冲突问题,还建议钱包在钱包特定的命名空间下注入其 provider 对象,然后将该对象代理到 window.ethereum
命名空间中。
参考实现
钱包 Provider
这是钱包 Provider 的注入脚本的参考实现,用于并行支持此新接口和现有模式。
function onPageLoad() {
let provider: EIP1193Provider;
window.ethereum = provider;
function announceProvider() {
const info: EIP6963ProviderInfo = {
uuid: "350670db-19fa-4704-a166-e52e178b59d2",
name: "Example Wallet",
icon: "data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg'/>",
rdns: "com.example.wallet"
};
window.dispatchEvent(
new CustomEvent("eip6963:announceProvider", {
detail: Object.freeze({ info, provider }),
})
);
}
window.addEventListener(
"eip6963:requestProvider",
(event: EIP6963RequestProviderEvent) => {
announceProvider();
}
);
announceProvider();
}
DApp 实现
这是 DApp 的参考实现,用于显示和跟踪由浏览器扩展注入的多个钱包 Provider。
const providers: EIP6963ProviderDetail[];
function onPageLoad() {
window.addEventListener(
"eip6963:announceProvider",
(event: EIP6963AnnounceProviderEvent) => {
providers.push(event.detail);
}
);
window.dispatchEvent(new Event("eip6963:requestProvider"));
}
安全注意事项
EIP-1193 安全注意事项
EIP-1193 的安全注意事项适用于此 EIP。期望实施者考虑并遵循他们正在使用的 provider 的指导。
钱包 Provider 对象的原型污染
浏览器扩展,以及因此钱包扩展,能够通过设计来修改页面的内容和 Provider 对象。各种钱包的 provider 对象被认为是传达交易数据的高度信任的接口。为了防止页面或各种其他扩展以意外的方式修改 DApp 和钱包之间的交互,最佳实践是在钱包在 eip6963:announceProvider
事件中分发 EIP1193Provider
对象之前,通过在 EIP1193Provider
对象上使用 object.freeze()
来“冻结” provider 发现对象。但是,在页面需要对对象进行猴子补丁的情况下,可能会出现一些与 Web 兼容性相关的问题。在这种情况下,需要在安全性和 Web 兼容性之间进行权衡,期望钱包实施者对此进行考虑。
钱包模仿和操纵
同样,期望 DApp 主动检测属性或函数被修改以篡改或修改其他钱包的异常行为。一种可以轻松实现此目的的方法是查找两个 EIP6963ProviderInfo
对象中的 uuid
属性何时匹配。期望 DApp 和 DApp 发现库考虑 EIP6963ProviderInfo
对象被篡改的其他潜在方法,并考虑额外的缓解技术来防止这种情况,以保护用户。
阻止 SVG Javascript 执行
SVG 图像的使用引入了跨站脚本风险,因为它们可以包含 JavaScript 代码。此 Javascript 在页面的上下文中执行,因此可以修改页面或页面的内容。因此,在考虑渲染图标的体验时,DApp 需要考虑如何处理这些问题,以防止图像被用作混淆技术来隐藏对页面或其他钱包的恶意修改。
阻止钱包指纹识别
此设计使用的并发事件循环的一个优点是,DApp 或钱包都可以启动流程来宣告 provider。因此,钱包实施者现在可以考虑是否希望将自己宣告给所有页面,还是尝试其他方法以减少用户通过注入 window.ethereum
对象进行指纹识别的能力。一些可以考虑的替代流程示例是等待注入 provider 对象,直到 DApp 宣告了 eip6963:requestProvider
。此时,钱包可以启动 UI 同意流程,以询问用户是否愿意共享其钱包地址。这允许钱包启用“私有连接”功能选项。但是,如果采用这种方法,钱包还必须考虑如何支持与不支持此 EIP 的 DApp 的向后兼容性。
版权
版权及相关权利通过 CC0 放弃。
Citation
Please cite this document as:
Pedro Gomes (@pedrouid), Kosala Hemachandra (@kvhnuke), Richard Moore (@ricmoo), Gregory Markou (@GregTheGreek), Kyle Den Hartog (@kdenhartog), Glitch (@glitch-txs), Jake Moxey (@jxom), Pierre Bertet (@bpierre), Darryl Yeo (@darrylyeo), Yaroslav Sergievsky (@everdimension), "EIP-6963: 多注入 Provider 发现," Ethereum Improvement Proposals, no. 6963, May 2023. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-6963.