Im Mai 2023 schlossen sich eine Gruppe von Ethereum-Wallet-Entwicklern zusammen, um einen neuen Industriestandard für die Verbindung mit Dapps zu etablieren. Zuvor war es gängige Praxis, dass Wallets einen EIP-1193-Provider über das globale window.ethereum-Objekt in Webbrowsern bereitstellten. Diese Methode funktionierte in den Anfangstagen gut, als Nutzer typischerweise nur eine Wallet besaßen und nutzten.
Als das Ökosystem wuchs und Nutzer begannen, mehrere Wallets gleichzeitig zu verwenden, wurde es für Dapp-Entwickler schwierig, den Prozess der Erkennung der verbundenen Wallet eines Nutzers ordnungsgemäß zu verwalten oder Nutzern die Möglichkeit zu geben, bei mehreren installierten Wallets die gewünschte auszuwählen.
Im Oktober 2023 wurde dieser Standard, heute bekannt als EIP-6963 oder Multi-Wallet Injected Provider Discovery, akzeptiert. Seitdem haben Wallet-Entwickler damit begonnen, EIP-6963 als Standardmethode zur Erkennung von Wallet-Providern einzuführen. MetaMask bildet dabei keine Ausnahme, da wir sichergestellt haben, dass dieser Standard von der MetaMask Wallet API und dem MetaMask SDK unterstützt wird.
Darüber hinaus wird die weitere Unterstützung von Dapps, die auf window.ethereum angewiesen sind, gewährleistet. Wir empfehlen jedoch, die EIP-6963-Unterstützung als bevorzugten Provider-Erkennungsmechanismus in alle Dapps zu implementieren. Am einfachsten gelingt dies durch die Integration von Drittanbieter-Bibliotheken, die EIP-6963 unterstützen. Eine wachsende Liste verfügbarer Bibliotheken finden Sie in der MetaMask-Dokumentation.
Dieses Tutorial führt Sie durch die grundlegenden Schritte zur Implementierung der EIP-6963-Unterstützung in Ihrer Dapp, falls Sie sich entscheiden, die verfügbaren Drittanbieter-Bibliotheken nicht zu verwenden. Es verwendet React und TypeScript, jedoch sollte jeder JavaScript-Entwickler in der Lage sein, dies nachzuvollziehen und in seinem bevorzugten Framework umzusetzen.
Unterstützte Interfaces und Typen für Web3-Wallets definieren
Der erste Schritt zur Unterstützung der Multi-Wallet-Erkennung in Ihrer Dapp besteht darin, alle unterstützten Interfaces und Typen zu definieren, wie im Verbesserungsvorschlag beschrieben. Diese Interfaces und Typen bieten eine strukturierte und standardisierte Möglichkeit, Ethereum-Wallet-Provider zu verwalten und erleichtern die Integration und Erkennung mehrerer Ethereum-Wallets.
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;
}
}
Ich habe Kommentare hinzugefügt, die Ihnen helfen zu verstehen, was jedes Interface und jeder Typ repräsentiert. Eine ausführlichere Erklärung finden Sie im Verbesserungsvorschlag.
Events abonnieren und über neue Verbindungen benachrichtigt werden
Der nächste Schritt besteht darin, eine Möglichkeit zu schaffen, Ihre Dapp zu benachrichtigen, wenn neue Wallet-Verbindungen erkannt werden. Denken Sie daran: Der Zweck von EIP-6963 ist es, eine nahtlose Verbindung und den Wechsel zwischen mehreren Wallets zu ermöglichen. In React können wir dies mit dem folgenden Beispielcode erreichen.
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);
}
}
Hier ist eine Erläuterung des obigen Code-Beispiels:
Wir erweitern die WindowEventMap um ein benutzerdefiniertes Event namens „eip6963:announceProvider" und etablieren damit einen globalen Event-Typ zur Ankündigung der Verfügbarkeit neuer Ethereum-Wallet-Provider.
Wir initialisieren einen externen Store, dargestellt durch ein Array benannter Provider, der verwendet wird, um alle erkannten Wallet-Provider zu verfolgen. Dieses Array speichert Objekte, die dem EIP6963ProviderDetail-Interface entsprechen, welches Informationen über den Wallet-Provider und den zugehörigen Ethereum-Provider enthält.
Wir führen einen Abonnementmechanismus über die subscribe-Funktion innerhalb des Store-Objekts ein. Diese Funktion lauscht auf das „eip6963:announceProvider"-Event und aktualisiert den externen Store mit den Details neu angekündigter Provider. Sie stellt sicher, dass Provider nur hinzugefügt werden, wenn sie noch nicht im Store vorhanden sind, um doppelte Einträge zu vermeiden.
Das Store-Objekt bietet zwei wesentliche Funktionalitäten: 1. Die value-Funktion ermöglicht das Abrufen des aktuellen Zustands des externen Stores, d. h. der Liste der erkannten Wallet-Provider. 2. Die subscribe-Funktion ermöglicht es Komponenten oder anderen Teilen der Anwendung, Änderungen im Store zu abonnieren. Wenn ein neuer Provider angekündigt und dem Store hinzugefügt wird, werden abonnierte Entitäten über einen Callback-Mechanismus benachrichtigt, sodass sie auf die aktualisierte Provider-Liste reagieren können.
Als Nächstes definieren wir einen useSyncExternalStore-Hook, um den lokalen Zustand mit dem oben in store.tsx definierten externen Store zu synchronisieren:
import { useSyncExternalStore } from "react";
import { store } from "./store";
export const useSyncProviders = ()=> useSyncExternalStore(store.subscribe, store.value, store.value)
Buttons für jeden erkannten Web3-Wallet-Provider dynamisch anzeigen
Definieren wir eine React-Komponente, die den useSyncProviders-Hook verwendet, um für jeden erkannten Wallet-Provider dynamisch einen Button zu rendern:
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>
)}
</>
);
};
Hier ist eine Erläuterung der obigen React-Komponente:
Wir verwenden zwei State-Hooks, selectedWallet und userAccount, um den aktuell ausgewählten Ethereum-Wallet-Provider bzw. die Kontoadresse des Nutzers zu verfolgen.
Wir verwenden den useSyncProviders-Hook, um jeden verfügbaren Ethereum-Wallet-Provider dynamisch zu erkennen. Dieser Hook gibt ein Array von Providern zurück, die jeweils dem EIP6963ProviderDetail-Interface entsprechen, das die Informationen des Providers und das Ethereum-Provider-Objekt enthält.
Wir definieren die handleConnect-Funktion, die aufgerufen wird, wenn ein Wallet-Provider-Button angeklickt wird. Diese Funktion verwendet die request-Methode des Providers mit der eth_requestAccounts-Methode, um den Nutzer zur Kontozugriffserteilung aufzufordern. Wird der Zugriff gewährt, wird die erste Kontoadresse in userAccount gespeichert und die Details des Providers in selectedWallet.
Abschließend rendern wir eine Liste von Buttons für jeden erkannten Wallet-Provider, die den Namen und das Symbol des Providers anzeigen.
Nachdem unsere Komponente definiert ist, besteht der letzte Schritt darin, diese Komponente zu rendern.
import './App.css'
import { DiscoverWalletProviders } from './components/DiscoverWalletProviders'
function App() {
return (
<>
<DiscoverWalletProviders/>
</>
)
}
export default App
Mit diesen wenigen Schritten haben wir die EIP-6963-Unterstützung in unserer Dapp auf grundlegender Ebene erfolgreich implementiert. Nachfolgend finden Sie eine visuelle Darstellung der soeben erstellten Funktionalität. Den vollständigen Code für die in der Visualisierung dargestellte Dapp finden Sie auf GitHub.
EIP-6963 und der Weg nach vorne für Web3-Wallets
Da wir eine zunehmend breitere Akzeptanz dieses Standards durch Wallet-Entwickler und Drittanbieter-Verbindungsbibliotheken beobachten, freuen wir uns über die vielfältigen Anwendungsfälle, die sich dadurch für die Ethereum-Community eröffnen – insbesondere im Hinblick auf die Verbesserung der UX beim Onboarding neuer Nutzer.
Wir bei MetaMask sind bestrebt, mit anderen Wallet-Entwicklern, Dapp-Entwicklern und Drittanbieter-Verbindungsbibliotheken zusammenzuarbeiten, um eine noch breitere Akzeptanz von EIP-6963 zu gewährleisten. Weitere Informationen zu unserer Arbeit zur Unterstützung dieses Standards finden Sie in der MetaMask-Dokumentation.