跳至內容

如何在您的 Web3 DApp 中實作 EIP-6963 支援

了解如何在您的 Web3 dApp 中實作 EIP-6963,以獲得更流暢的錢包連接體驗

6 分鐘閱讀
如何在您的 Web3 DApp 中實作 EIP-6963 支援

2023 年 5 月,一群以太坊錢包開發者齊聚一堂,共同制定了一項連接 dapp 的新行業標準。在此之前,常見的做法是讓錢包透過網頁瀏覽器中的 window.ethereum 全域物件公開 EIP-1193 provider。這項技術在早期運作良好,因為當時使用者通常只擁有並連接一個錢包。

隨著生態系統的成長,使用者開始同時使用多個錢包,dapp 開發者便難以妥善管理偵測使用者所連接錢包的流程,也難以在安裝多個錢包時提供使用者選擇要使用哪個錢包的選項。

2023 年 10 月,這項標準正式通過,現稱為 EIP-6963 或多錢包注入式 Provider 探索機制。自此,錢包開發者開始將 EIP-6963 作為探索錢包 provider 的預設方式。MetaMask 也不例外,我們已確保 MetaMask 錢包 API 與 MetaMask SDK 均支援此標準。

此外,依賴 window.ethereum 的 dapp 仍可獲得持續支援。不過,我們鼓勵所有 dapp 將 EIP-6963 支援作為首選的 provider 探索機制加以實作。最簡便的方式是整合支援 EIP-6963 的第三方函式庫。請造訪 MetaMask 文件,查看持續更新的可用函式庫清單。

本教學將引導您完成在 dapp 中實作 EIP-6963 支援的基本步驟,適用於決定不使用現有第三方函式庫的開發者。範例使用 React 與 TypeScript,但任何 JavaScript 開發者都應能跟著操作,並在自己偏好的框架中加以實作。

為 Web3 錢包定義支援的介面與型別

在 dapp 中支援多錢包探索的第一步,是依照改進提案的說明,定義所有支援的介面與型別。這些介面與型別提供了一種結構化且標準化的方式來處理以太坊錢包 provider,有助於整合與探索多個以太坊錢包。

// 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,藉此建立一個全域事件型別,用於宣告新以太坊錢包 provider 的可用性。

  • 我們初始化一個外部 store,以命名 provider 的陣列表示,用於追蹤所有已偵測到的錢包 provider。此陣列儲存符合 EIP6963ProviderDetail 介面的物件,其中包含錢包 provider 及其對應以太坊 provider 的相關資訊。

  • 我們透過 store 物件中的 subscribe 函式引入訂閱機制。此函式監聽「eip6963:announceProvider」事件,並以任何新宣告 provider 的詳細資訊更新外部 store。它確保只有在 store 中不存在的 provider 才會被加入,以避免重複項目。

  • store 物件提供兩項主要功能:1. value 函式可用於擷取外部 store 的目前狀態,即已偵測到的錢包 provider 清單。

  1. subscribe 函式讓元件或應用程式的其他部分能夠訂閱 store 的變更。當新 provider 被宣告並加入 store 時,已訂閱的實體會透過回呼機制收到通知,使其能夠回應更新後的 provider 清單。

接下來,讓我們定義一個 useSyncExternalStore hook,以將本地狀態與上方 store.tsx 中定義的外部 store 同步:

// useSyncProviders.tsx

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

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

為每個偵測到的 Web3 錢包 Provider 動態顯示按鈕

讓我們定義一個 React 元件,使用 useSyncProviders hook 為每個偵測到的錢包 provider 動態渲染按鈕:

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,分別用於追蹤目前選取的以太坊錢包 provider 及使用者的帳戶地址。

  • 我們使用 useSyncProviders hook 動態偵測每個可用的以太坊錢包 provider。此 hook 回傳一個 provider 陣列,每個 provider 均符合 EIP6963ProviderDetail 介面,其中包含 provider 的資訊與以太坊 provider 物件。

  • 我們定義 handleConnect 函式,當錢包 provider 按鈕被點擊時觸發。此函式使用 provider 的 request 方法搭配 eth_requestAccounts 方法,提示使用者授予帳戶存取權限。若存取獲得授權,第一個帳戶地址將儲存於 userAccount,provider 的詳細資訊則儲存於 selectedWallet。

  • 最後,我們為每個偵測到的錢包 provider 渲染一個按鈕清單,顯示 provider 的名稱與圖示。

元件定義完成後,最後一步是渲染此元件。

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。他目前定居於杜拜,閒暇時喜歡打籃球和踢足球。

    閱讀所有文章