2023年5月、Ethereumウォレット開発者のグループが集まり、dappとの接続に関する新しい業界標準を策定しました。それ以前は、ウォレットがウェブブラウザのwindow.ethereumグローバルオブジェクトを通じてEIP-1193プロバイダーを公開するのが一般的な慣行でした。この手法は、ユーザーが通常1つのウォレットを所有・接続していた初期の頃には十分に機能していました。
エコシステムが成長し、ユーザーが複数のウォレットを同時に利用するようになると、dapp開発者にとって、ユーザーがどのウォレットに接続しているかを適切に検出したり、多数のウォレットがインストールされている場合にどのウォレットを使用するかをユーザーが選択できるようにしたりするプロセスの管理が困難になりました。
2023年10月、EIP-6963(Multi-Wallet Injected Provider Discovery)として知られるこの標準が承認されました。それ以来、ウォレット開発者はEIP-6963をウォレットプロバイダーの検出におけるデフォルトの方法として導入し始めています。MetaMaskも例外ではなく、MetaMask Wallet APIおよびMetaMask SDKでこの標準がサポートされるよう対応しています。
また、window.ethereumに依存するdappについても、引き続きサポートが保証されています。ただし、推奨されるプロバイダー検出メカニズムとして、すべてのdappにEIP-6963サポートを実装することを推奨します。最も簡単な方法は、EIP-6963をサポートするサードパーティライブラリを統合することです。利用可能なライブラリの一覧(随時更新)については、MetaMaskドキュメントをご覧ください。
このチュートリアルでは、利用可能なサードパーティライブラリを使用しない場合に、dappにEIP-6963サポートを実装するための基本的な手順を説明します。ReactとTypeScriptを使用していますが、JavaScriptの開発者であれば、任意のフレームワークで実装できるはずです。
Web3ウォレット向けのサポートインターフェースと型の定義
dappでマルチウォレット検出をサポートするための最初のステップは、改善提案に概説されているすべてのサポートインターフェースと型を定義することです。これらのインターフェースと型は、Ethereumウォレットプロバイダーを扱うための構造化・標準化された方法を提供し、複数のEthereumウォレットの統合と検出を容易にします。
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);
}
}
上記のコードサンプルの詳細は以下のとおりです:
WindowEventMapを「eip6963:announceProvider」というカスタムイベントで拡張し、新しいEthereumウォレットプロバイダーの利用可能性を通知するグローバルイベント型を確立します。
検出されたすべてのウォレットプロバイダーを追跡するために使用される、名前付きプロバイダーの配列として表される外部ストアを初期化します。この配列は、ウォレットプロバイダーとそれに対応するEthereumプロバイダーに関する情報を含むEIP6963ProviderDetailインターフェースに準拠したオブジェクトを格納します。
ストアオブジェクト内のsubscribe関数を通じてサブスクリプションメカニズムを導入します。この関数は「eip6963:announceProvider」イベントをリッスンし、新たに通知されたプロバイダーの詳細で外部ストアを更新します。また、プロバイダーがストアに存在しない場合にのみ追加されるようにし、重複エントリーを防ぎます。
ストアオブジェクトは2つの主要な機能を提供します:1. value関数は外部ストアの現在の状態、つまり検出されたウォレットプロバイダーのリストを取得するために使用されます。
subscribe関数は、コンポーネントやアプリケーションの他の部分がストアの変更をサブスクライブできるようにします。新しいプロバイダーが通知されてストアに追加されると、サブスクライブしているエンティティはコールバックメカニズムを通じて通知を受け、更新されたプロバイダーリストに対応できます。
次に、上記のstore.tsxで定義された外部ストアとローカル状態を同期するためのuseSyncExternalStoreフックを定義しましょう:
import { useSyncExternalStore } from "react";
import { store } from "./store";
export const useSyncProviders = ()=> useSyncExternalStore(store.subscribe, store.value, store.value)
検出された各Web3ウォレットプロバイダーのボタンを動的に表示する
useSyncProvidersフックを使用して、検出された各ウォレットプロバイダーのボタンを動的にレンダリングするReactコンポーネントを定義しましょう:
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コンポーネントの詳細は以下のとおりです:
selectedWalletとuserAccountの2つのステートフックを使用して、現在選択されているEthereumウォレットプロバイダーとユーザーのアカウントアドレスをそれぞれ追跡します。
useSyncProvidersフックを使用して、利用可能な各EthereumウォレットプロバイダーをEIP6963ProviderDetailインターフェースに準拠した形で動的に検出します。このフックはプロバイダーの配列を返し、各プロバイダーにはプロバイダーの情報とEthereumプロバイダーオブジェクトが含まれます。
ウォレットプロバイダーボタンがクリックされたときに呼び出される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ウォレットの今後の展望
この標準がウォレット開発者やサードパーティの接続ライブラリによってより広く採用されていく中で、Ethereumコミュニティにとって開かれるさまざまなユースケース、特に新規ユーザーのオンボーディングにおけるUXの向上に期待しています。
MetaMaskは、EIP-6963のさらなる普及を確実にするために、他のウォレット開発者、dapp開発者、およびサードパーティの接続ライブラリと積極的に協力していきたいと考えています。この標準をサポートするための取り組みの詳細については、MetaMaskドキュメントを参照してください。