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,有助於整合與探索多個以太坊錢包。
interface EIP6963ProviderInfo {
walletId: string;
uuid: string;
name: string;
icon: string;
}
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;
}
type EIP6963AnnounceProviderEvent = {
detail: {
info: EIP6963ProviderInfo;
provider: EIP1193Provider;
}
}
我已加入註解,協助您了解每個介面與型別所代表的意義。如需更詳細的說明,請參閱改進提案。
訂閱事件並接收新連接通知
下一步是建立一種機制,在偵測到新的錢包連接時通知您的 dapp。請記住,EIP-6963 的目的是實現多個錢包之間的無縫連接與切換。在 React 中,我們可以使用以下範例程式碼來達成此目的。
declare global {
interface WindowEventMap {
"eip6963:announceProvider": CustomEvent<EIP6963AnnounceProviderEvent>;
}
}
let providers: EIP6963ProviderDetail[] = [];
export const store = {
value: () => providers,
subscribe: (callback: () => void) => {
function onAnnouncement(event: EIP6963AnnounceProviderEvent) {
if (providers.some(p => p.info.uuid === event.detail.info.uuid)) return;
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 清單。
subscribe 函式讓元件或應用程式的其他部分能夠訂閱 store 的變更。當新 provider 被宣告並加入 store 時,已訂閱的實體會透過回呼機制收到通知,使其能夠回應更新後的 provider 清單。
接下來,讓我們定義一個 useSyncExternalStore hook,以將本地狀態與上方 store.tsx 中定義的外部 store 同步:
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 文件。