集成 EIP 6963 - 给前端开发者的指南

EIP-6963 解决同时多个钱包提供者的烦恼,本文介绍如在在前端 React应用中集成 EIP-6963 。

本文将快速帮助你理解 EIP-6963(多钱包注入提供者发现),并回答如何在你的 React dApp 中实现它的问题。

如果只想看代码? 这个 ViteJS(React + TypeScript)的示例Github 代码:vite-react-ts-eip-6963

这个示例代码实现了 EIP-6963,用来检测多个注入的提供者(浏览器安装的钱包(外部拥有账户))。

跟随我们一起探索 EIP 中概述的接口,了解如何监听 eip6963:announceProvider 事件,并存储和区分返回的各种提供者,以便在我们的 dApp 中使用。

如果你了解 EIP-6963 背景,可以直接跳到“开发者需要知道的内容”

我们将构建的内容预览:

EIP-6963 React 演示

EIP-6963 摘要

EIP-6963 是一种替代检查 window.ethereum 的机制,它是 EIP-1193 定义的提供者,支持使用 JavaScript 的 window 事件在网页中发现多个注入的钱包提供者

如果不了解 EIP-6963,你可以阅读 EIP-6963 的摘要动机

如果文档中有你不理解任何部分,可以通过像 Ethereum Magicians 这样的站点获取更多信息(EIP-6963 MIPD),在那里你可以从作者和社区获取大多数 EIP 的额外背景信息。熟悉 EIP-1193 也会有所帮助。

EIP-6963 摘要 中最重要的一句话是:

"一种替代 window.ethereum 的发现机制,其作为 EIP-1193 提供者"

这告诉我们,EIP-6963 的多注入提供者发现提案引入了一种不同的方法来发现和与 EIP-1193 提供者交互,与现有方法依赖 window.ethereum 对象形成对比。

作为一个在 dapps 中到处使用 window.ethereum 的开发者,以及旧方法带来的用户体验问题,他们引起了我的注意。

希望这些信息能帮助我们在阅读 EIP 并开始实现这种改进的用户钱包检测方法时,框定我们的思维。

EIP-6963 之前的问题

在以太坊 dApps 中,钱包传统上使用一个称为“提供者”的 JavaScript 对象来暴露它们的 API。最初的 EIP 是为了标准化这个接口,即 EIP-1193,但不同钱包实现之间出现了冲突。虽然 EIP-1193 旨在建立一个通用约定,但由于钱包注入它们的提供者(覆盖)window(以太坊对象)导致的竞争条件,用户体验受到了影响。这是对 Web3 用户体验及其在钱包发现、入门和连接方面缺陷的巨大批评。这些竞争条件导致在同一浏览器中启用多个钱包扩展时发生冲突,最后一个注入的提供者优先。

EIP-5749 尝试通过 window.evmprovider 解决这个问题,这是朝着正确方向迈出的一步,但最终没有被足够多的钱包采用来解决我们的用户体验问题。这就是我们最终得到 EIP 6963 的地方,它提出了一种进行多注入提供者发现的标准,钱包们正在注意并大多数已经实现了这个新标准。

支持 EIP-6963 的钱包列表

开发者需要知道的内容

作为开发者,熟悉 EIP-6963 的第一件事是了解最初的动机、基本描述,更重要的是实现这种新方法所需的类型、接口和逻辑。

在 ViteJS React + TS 应用中实现 EIP-6963

提供者信息

每个钱包提供者将使用以下接口公告(announce)自己:

/**
 * 表示显示钱包所需的资源
 */
interface EIP6963ProviderInfo {
  uuid: string;
  name: string;
  icon: string;
  rdns: string;
}

提供者详情

用作组合接口来公告钱包提供者及其相关元数据

interface EIP6963ProviderDetail {
  info: EIP6963ProviderInfo;
  provider: EIP1193Provider;
}

公告

EIP6963AnnounceProviderEvent 接口必须是一个 CustomEvent 对象,其 type 属性包含一个字符串值 eip6963:announceProvider,并且其 detail 属性包含一个类型为 EIP6963ProviderDetail 的对象值。

// 钱包派发的公告事件
interface EIP6963AnnounceProviderEvent extends CustomEvent {
  type: "eip6963:announceProvider";
  detail: EIP6963ProviderDetail;
}

请求事件

EIP6963RequestProviderEvent 接口必须是一个 Event 对象,其 type 属性包含一个字符串值 eip6963:requestProvider

// DApp 派发的请求事件
interface EIP6963RequestProviderEvent extends Event {
  type: "eip6963:requestProvider";
}

理解了这些接口后,我们可以启动一个 ViteJS React + TypeScript 应用,并在 src/vite-env.d.ts 中更新这些接口,并添加一个 EIP1193Provider

vite-env.d.ts

/// <reference types="vite/client" />
interface EIP6963ProviderInfo {
  walletId: string;
  uuid: string;
  name: string;
  icon: string;
}

// Represents the structure of an Ethereum provider based on the EIP-1193 standard.
interface EIP1193Provider {
  isStatus?: boolean;
  host?: string;
  path?: string;
  sendAsync?: (request: { method: string, params?: Array<unknown> }, callback: (error: Error | null, response: unknown) => void) => void
  send?: (request: { method: string, params?: Array<unknown> }, callback: (error: Error | null, response: unknown) => void) => void
  request: (request: { method: string, params?: Array<unknown> }) => Promise<unknown>
}

interface EIP6963ProviderDetail {
  info: EIP6963ProviderInfo;
  provider: EIP1193Provider;
}

// This type represents the structure of an event dispatched by a wallet to announce its presence based on EIP-6963.
type EIP6963AnnounceProviderEvent = {
  detail:{
    info: EIP6963ProviderInfo,
    provider: EIP1193Provider
  }
}

这些接口和类型提供了一种结构化和标准化的方式来处理以太坊钱包提供者,促进了 EIP-6963 的集成。定义为钱包提供者提供了明确的规范,促进了一致性和互操作性。

然后我们可以创建一个 hooks 目录并添加以下两个文件:

store.tsx

declare global {
  interface WindowEventMap {
    "eip6963:announceProvider": CustomEvent
  }
}

let providers: EIP6963ProviderDetail[] = []

export const store = {
  value: ()=>providers,
  subscribe: (callback: ()=>void)=>{
    function onAnnouncement(event: EIP6963AnnounceProviderEvent){
      if(providers.map(p => p.info.uuid).includes(event.detail.info.uuid)) return
      providers = [...providers, event.detail]
      callback()
    }
    window.addEventListener("eip6963:announceProvider", onAnnouncement);
    window.dispatchEvent(new Event("eip6963:requestProvider"));

    return ()=>window.removeEventListener("eip6963:announceProvider", onAnnouncement)
  }
}

关于这个 store:

  • 定义了一个全局事件类型,用于 EIP-6963 提供者公告。
  • 管理一个外部 store(providers),保存检测到的钱包提供者的状态。
  • 订阅 "eip6963:announceProvider" 事件,并在宣布新提供者时更新外部 store。
  • 提供 value 函数以访问当前状态,并提供 subscribe 函数以订阅外部 store 的更改。

useSyncProviders.tsx

import { useSyncExternalStore } from "react";
import { store } from "./store";

export const useSyncProviders = ()=> useSyncExternalStore(store.subscribe, store.value, store.value)

利用 useSyncExternalStore 钩子将本地状态与在 store.tsx 中定义的外部 store 同步。

在下一个组件 DiscoverWalletProviders 中,调用 useSyncProviders 钩子以获取同步状态。providers 变量现在保存了外部 store store.value 的当前状态。DiscoverWalletProviders 组件使用此状态动态渲染每个检测到的钱包提供者的按钮。

接下来我们将在 src/components 目录中添加一个组件。

DiscoverWalletProviders.tsx

import { useState } from 'react'
import { useSyncProviders } from '../hooks/useSyncProviders'
import { formatAddress } from '~/utils'

export const  DiscoverWalletProviders = () => {
  const [selectedWallet, setSelectedWallet] = useState<EIP6963ProviderDetail>()
  const [userAccount, setUserAccount] = useState<string>('')
  const providers = useSyncProviders()

  const handleConnect = async(providerWithInfo: EIP6963ProviderDetail)=> {
    const accounts = await providerWithInfo.provider
      .request({method:'eth_requestAccounts'})
      .catch(console.error)

    if(accounts?.[0]){
      setSelectedWallet(providerWithInfo)
      setUserAccount(accounts?.[0])
    }
  }

  return (
    <>
      <h2>Wallets Detected:</h2>
      <div>
        {
          providers.length > 0 ? providers?.map((provider: EIP6963ProviderDetail)=>(
            <button key={provider.info.uuid} onClick={()=>handleConnect(provider)} >
              <img src={provider.info.icon} alt={provider.info.name} />
              <div>{provider.info.name}</div>
            </button>
          )) :
          <div>
            there are no Announced Providers
          </div>
        }
      </div>
      <hr />
      <h2>{ userAccount ? "" : "No " }Wallet Selected</h2>
      { userAccount &&
        <div>
          <div>
            <img src={selectedWallet.info.icon} alt={selectedWallet.info.name} />
            <div>{selectedWallet.info.name}</div>
            <div>({formatAddress(userAccount)})</div>
          </div>
        </div>
      }
    </>
  )
}

在上面的代码中,一旦页面渲染,我们就会记录检测到的提供者。见第 11 行。我们可以遍历这些提供者并为每个提供者创建一个按钮,该按钮将用于调用 eth_requestAccounts

最后我们可以在 src/App.tsx 中渲染这个组件

App.tsx

import './App.css'
import { DiscoverWalletProviders } from './components/DiscoverWalletProviders'

function App() {

  return (
    <>
      <DiscoverWalletProviders/>
    </>
  )
}

export default App

总之,这些文件在 ViteJS(React + TypeScript)应用程序中实现了 EIP-6963 的集成。定义了接口和类型,通过 useSyncProviders 钩子处理组件之间的同步,并且外部 store store.tsx 管理检测到的钱包提供者的状态。

通过这些简单的步骤,我们在使用 TypeScript 接口的 React 应用程序中实现了 EIP-6963。基本上就是这样。你可以在这里看到源代码:vite-react-ts-eip-6963

第三方库的支持

对于构建 Web3 的开发人员来说,开始使用 EIP-6963(多注入提供者发现)的最简单方法是利用已经支持它的第三方连接库(便利库)。在撰写本文时,以下是列表:

如果你正在构建钱包连接库,你会很高兴知道,集成 MetaMask 的 SDK 将确保连接到 MetaMask 支持 EIP-6963,以检测 MetaMask 扩展和 MetaMask Flask 钱包。你可以使用 Wagmi 的 MIPD Store 获取注入的提供者,包含 Vanilla JS 和 React 示例。

MetaMask SDK 不仅支持在 MetaMask 上检测 EIP-6963,还集成到 Web3 Onboard 和 WAGMI v2.0 中。

SDK 对 EIP-6963 的集成旨在高效发现和连接 MetaMask 扩展。这一增强对于简化用户体验和促进与以太坊区块链的无缝交互至关重要。

MetaMask SDK 自动检测

MetaMask JS SDK 现在会自动检查支持 EIP-6963 的 MetaMask 扩展的存在。这消除了手动配置或检测方法的需要,从而简化了开发人员和用户的初始设置过程。
冲突解决:通过遵循 EIP-6963 设定的标准,SDK 明确识别并连接到 MetaMask 扩展。这种方法有效解决了可能与其他钱包扩展产生的潜在冲突,确保用户更稳定和可靠的交互。

向后兼容性

不支持 EIP-6963 的 Dapps 仍然可以使用 window.ethereum 提供者检测 MetaMask。
然而,我们建议添加支持以改善多钱包安装的用户体验。
阅读更多关于 EIP-6963 向后兼容性

EIP-6963 阅读资源

Wallet Connect 团队的这个应用的 NextJS 版本
EIP-6963 概述:解决多钱包冲突的可能方案
EIP-6963
EIP-6963 标准化你的浏览器钱包体验

额外的演示和信息

我是 AI 翻译官,为大家转译优秀英文文章,如有翻译不通的地方,在这里修改,还请包涵~

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

1 条评论

请先 登录 后评论
Eric Bishard
Eric Bishard
江湖只有他的大名,没有他的介绍。