跳到内容

如何在 Web3 Dapp 中实现 EIP-6963 支持

了解如何在您的 Web3 DApp 中实现 EIP-6963,以获得更流畅的钱包连接体验

6 分钟阅读
如何在 Web3 Dapp 中实现 EIP-6963 支持

2023年5月,一群以太坊钱包开发者齐聚一堂,共同制定了一项用于连接dapp的新行业标准。在此之前,钱包通常通过网络浏览器中的 window.ethereum 全局对象暴露EIP-1193提供者,这一做法已成为惯例。在早期,用户通常只拥有并连接一个钱包,这种方式运行良好。

随着生态系统的发展,用户开始同时使用多个钱包,dapp开发者在管理钱包检测流程方面愈发困难——无论是识别用户当前连接的是哪个钱包,还是在安装了多个钱包时为用户提供选择,都面临挑战。

2023年10月,这一标准正式获得通过,即现在所称的EIP-6963,又名多钱包注入提供者发现协议。此后,钱包开发者开始将EIP-6963作为发现钱包提供者的默认方式。MetaMask也不例外,我们已确保MetaMask钱包API和MetaMask SDK均支持该标准。

此外,依赖 window.ethereum 的dapp将继续获得支持。不过,我们鼓励所有dapp将EIP-6963支持作为首选的提供者发现机制加以实现。最简便的方式是集成支持EIP-6963的第三方库。请访问MetaMask文档,查看持续更新的可用库列表。

本教程将引导您完成在dapp中实现EIP-6963支持的基本步骤,适用于不希望使用现有第三方库的开发者。教程采用React和TypeScript编写,但任何JavaScript开发者都应能够参照实现,并将其应用于自己偏好的框架中。

为Web3钱包定义支持的接口和类型

在dapp中支持多钱包发现的第一步,是按照改进提案的规范定义所有支持的接口和类型。这些接口和类型提供了一种结构化、标准化的方式来处理以太坊钱包提供者,从而便于集成和发现多个以太坊钱包。

// EthereumProviderTypes.d.ts

// Interface for provider information following EIP-6963.
interface EIP6963ProviderInfo {
  walletId: string; // Unique identifier for the wallet e.g io.metamask, io.metamask.flask 
  uuid: string; // Globally unique ID to differentiate between provider sessions for the lifetime of the page
  name: string; // Human-readable name of the wallet
  icon: string; // URL to the wallet's icon
}

// Interface for Ethereum providers based on the EIP-1193 standard.
interface EIP1193Provider {
  isStatus?: boolean; // Optional: Indicates the status of the provider
  host?: string; // Optional: Host URL of the Ethereum node
  path?: string; // Optional: Path to a specific endpoint or service on the host
  sendAsync?: (request: { method: string, params?: Array<unknown> }, callback: (error: Error | null, response: unknown) => void) => void; // For sending asynchronous requests
  send?: (request: { method: string, params?: Array<unknown> }, callback: (error: Error | null, response: unknown) => void) => void; // For sending synchronous requests
  request: (request: { method: string, params?: Array<unknown> }) => Promise<unknown>; // Standard method for sending requests per EIP-1193
}

// Interface detailing the structure of provider information and its Ethereum provider.
interface EIP6963ProviderDetail {
  info: EIP6963ProviderInfo; // The provider's info
  provider: EIP1193Provider; // The EIP-1193 compatible provider
}

// Type representing the event structure for announcing a provider based on EIP-6963.
type EIP6963AnnounceProviderEvent = {
  detail: {
    info: EIP6963ProviderInfo; // The provider's info
    provider: EIP1193Provider; // The EIP-1193 compatible provider
  }
}
复制

我已添加注释,帮助您理解每个接口和类型的含义。如需更详细的说明,请参阅改进提案。

订阅事件并获取新连接通知

下一步是建立一种机制,在检测到新的钱包连接时通知您的dapp。请记住,EIP-6963的目的是实现多个钱包之间的无缝连接与切换。在React中,我们可以通过以下示例代码实现这一目标。

// store.tsx

declare global {
  interface WindowEventMap {
    "eip6963:announceProvider": CustomEvent<EIP6963AnnounceProviderEvent>;
  }
}

let providers: EIP6963ProviderDetail[] = [];

export const store = {
  value: () => providers,

  subscribe: (callback: () => void) => {
    function onAnnouncement(event: EIP6963AnnounceProviderEvent) {
      // Prevent adding a provider if it already exists in the list based on its uuid.
      if (providers.some(p => p.info.uuid === event.detail.info.uuid)) return;

      // Add the new provider to the list and call the provided callback function.
      providers = [...providers, event.detail];
      callback();
    }

    window.addEventListener("eip6963:announceProvider", onAnnouncement as EventListener);
    window.dispatchEvent(new Event("eip6963:requestProvider"));

    return () => window.removeEventListener("eip6963:announceProvider", onAnnouncement as EventListener);
  }
}
复制

以下是上述代码示例的详细说明:

  • 我们通过自定义事件"eip6963:announceProvider"扩展了 WindowEventMap,从而建立了一个全局事件类型,用于宣告新以太坊钱包提供者的可用性。

  • 我们初始化了一个外部存储,以命名提供者数组的形式表示,用于追踪所有已检测到的钱包提供者。该数组存储符合 EIP6963ProviderDetail 接口的对象,其中包含钱包提供者及其对应以太坊提供者的相关信息。

  • 我们通过存储对象中的 subscribe 函数引入了订阅机制。该函数监听"eip6963:announceProvider"事件,并将任何新宣告的提供者详情更新至外部存储。同时,它确保仅在提供者尚未存在于存储中时才将其添加,从而避免重复条目。

  • 存储对象提供两项核心功能:1. value 函数用于获取外部存储的当前状态,即已检测到的钱包提供者列表。

  1. subscribe 函数使组件或应用程序的其他部分能够订阅存储中的变更。当新提供者被宣告并添加至存储时,已订阅的实体将通过回调机制收到通知,从而对更新后的提供者列表作出响应。

接下来,让我们定义一个 useSyncExternalStore hook,以将本地状态与上方store.tsx中定义的外部存储同步:

// useSyncProviders.tsx

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

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

为每个检测到的Web3钱包提供者动态显示按钮

让我们定义一个React组件,使用 useSyncProviders hook为每个检测到的钱包提供者动态渲染一个按钮:

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

export const DiscoverWalletProviders = () => {
  const [selectedWallet, setSelectedWallet] = useState<EIP6963ProviderDetail | undefined>();
  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 && accounts[0]) {
      setSelectedWallet(providerWithInfo);
      setUserAccount(accounts[0]);
    }
  };

  return (
    <>
      <h2>Wallets Detected:</h2>
      <div>
        {providers.length > 0 ? (
          providers.map((provider) => (
            <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 ? 'Wallet Selected' : 'No Wallet Selected'}</h2>
      {userAccount && (
        <div>
          <img src={selectedWallet!.info.icon} alt={selectedWallet!.info.name} />
          <div>{selectedWallet!.info.name}</div>
          <div>({formatAddress(userAccount)})</div>
        </div>
      )}
    </>
  );
};
复制

以下是上述React组件的详细说明:

  • 我们使用两个状态hook——selectedWallet 和 userAccount,分别追踪当前选定的以太坊钱包提供者和用户的账户地址。

  • 我们使用 useSyncProviders hook动态检测每个可用的以太坊钱包提供者。该hook返回一个提供者数组,每个提供者均符合EIP6963ProviderDetail接口,包含提供者信息和以太坊提供者对象。

  • 我们定义了 handleConnect 函数,该函数在点击钱包提供者按钮时被调用。此函数通过提供者的 request 方法配合 eth_requestAccounts 方法,提示用户授权账户访问。若授权成功,第一个账户地址将存储至 userAccount,提供者详情将存储至 selectedWallet。

  • 最后,我们为每个检测到的钱包提供者渲染一个按钮列表,显示提供者的名称和图标。

组件定义完成后,最后一步是渲染该组件。

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

function App() {

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

export default App
复制

通过以上几个步骤,我们已在基础层面成功为dapp实现了EIP-6963支持。请参阅下方的可视化演示,了解我们刚刚构建的功能效果。该演示中dapp的完整代码可在GitHub上获取。

EIP-6963与Web3钱包的未来之路

随着越来越多的钱包开发者和第三方连接库采用这一标准,我们对其为以太坊社区带来的各种应用场景充满期待,尤其是在改善新用户引导体验方面。

MetaMask团队热切期待与其他钱包开发者、dapp开发者以及第三方连接库携手合作,推动EIP-6963的更广泛采用。如需了解我们在支持该标准方面所做工作的更多信息,请参阅MetaMask文档。

AI 翻译。可能包含错误。请务必核实信息。

评价此翻译

本文作者:

  • Kingsley Okonkwo

    Kingsley 是一位经认证的以太坊区块链开发者,现任 Consensys 技术写作工程师,专注于开发者工具领域。加入 Consensys 之前,Kingsley 曾在 Gigster 和 Braintrust 等多个平台担任自由职业后端开发者。他拥有 5 年软件开发经验,熟练掌握 JavaScript(Node.js 和 React)、GoLang、SQL 及 Docker 等技术工具。Kingsley 擅长撰写分步教程和操作指南,善用最新 Web3 技术帮助开发者构建更优质的 dapp。他目前定居迪拜,业余时间喜欢打篮球和踢足球。

    阅读所有文章