En mai 2023, un groupe de développeurs de portefeuilles Ethereum s'est réuni pour établir un nouveau standard industriel permettant la connexion aux dapps. Auparavant, la pratique courante consistait pour les portefeuilles à exposer un fournisseur EIP-1193 via l'objet global window.ethereum dans les navigateurs web. Cette technique fonctionnait bien à ses débuts, lorsque les utilisateurs possédaient et se connectaient généralement à un seul portefeuille.
À mesure que l'écosystème s'est développé et que les utilisateurs ont commencé à utiliser plusieurs portefeuilles simultanément, il est devenu difficile pour les développeurs de dapps de gérer correctement le processus de détection du portefeuille auquel un utilisateur était connecté, ou de donner aux utilisateurs la possibilité de choisir quel portefeuille utiliser lorsque plusieurs étaient installés.
En octobre 2023, ce standard, désormais connu sous le nom d'EIP-6963 ou Multi-Wallet Injected Provider Discovery, a été adopté. Depuis lors, les développeurs de portefeuilles ont commencé à introduire EIP-6963 comme méthode par défaut pour découvrir les fournisseurs de portefeuilles. MetaMask ne fait pas exception, car nous avons veillé à ce que ce standard soit pris en charge par l'API du portefeuille MetaMask et le SDK MetaMask.
Par ailleurs, les dapps qui s'appuient sur window.ethereum bénéficient d'une compatibilité continue garantie. Nous encourageons néanmoins l'implémentation du support EIP-6963 dans toutes les dapps en tant que mécanisme de découverte de fournisseurs privilégié. La façon la plus simple d'y parvenir est d'intégrer des bibliothèques tierces prenant en charge EIP-6963. Consultez la documentation MetaMask pour accéder à une liste croissante de bibliothèques disponibles.
Ce tutoriel vous guidera à travers les étapes fondamentales de l'implémentation du support EIP-6963 dans votre dapp si vous décidez de ne pas utiliser les bibliothèques tierces disponibles. Il utilise React et TypeScript ; toutefois, tout développeur JavaScript devrait être en mesure de le suivre et de l'implémenter dans le framework de son choix.
Définir les interfaces et types pris en charge pour les portefeuilles Web3
La première étape pour prendre en charge la découverte multi-portefeuilles dans votre dapp consiste à définir toutes les interfaces et types pris en charge, tels que décrits dans la proposition d'amélioration. Ces interfaces et types fournissent une méthode structurée et standardisée pour gérer les fournisseurs de portefeuilles Ethereum, facilitant ainsi l'intégration et la découverte de plusieurs portefeuilles 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;
}
}
J'ai ajouté des commentaires pour vous aider à comprendre ce que représente chaque interface et type. Veuillez vous référer à la proposition d'amélioration pour une explication plus détaillée.
S'abonner aux événements et être notifié des nouvelles connexions
L'étape suivante consiste à mettre en place un mécanisme permettant d'alerter votre dapp lorsque de nouvelles connexions de portefeuilles sont détectées. Rappelons que l'objectif d'EIP-6963 est de permettre une connexion et un basculement fluides entre plusieurs portefeuilles. Avec React, nous pouvons y parvenir grâce à l'exemple de code ci-dessous.
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);
}
}
Voici une présentation détaillée de l'exemple de code ci-dessus :
Nous étendons WindowEventMap avec un événement personnalisé nommé « eip6963:announceProvider », établissant ainsi un type d'événement global pour annoncer la disponibilité de nouveaux fournisseurs de portefeuilles Ethereum.
Nous initialisons un store externe, représenté par un tableau de fournisseurs nommés, utilisé pour suivre tous les fournisseurs de portefeuilles détectés. Ce tableau stocke des objets conformes à l'interface EIP6963ProviderDetail, qui inclut des informations sur le fournisseur de portefeuille et son fournisseur Ethereum correspondant.
Nous introduisons un mécanisme d'abonnement via la fonction subscribe au sein de l'objet store. Cette fonction écoute l'événement « eip6963:announceProvider » et met à jour le store externe avec les détails de tout nouveau fournisseur annoncé. Elle garantit que les fournisseurs ne sont ajoutés que s'ils ne figurent pas déjà dans le store, évitant ainsi les doublons.
L'objet store offre deux fonctionnalités clés : 1. La fonction value permet de récupérer l'état actuel du store externe, c'est-à-dire la liste des fournisseurs de portefeuilles détectés.
La fonction subscribe permet aux composants ou à d'autres parties de l'application de s'abonner aux modifications du store. Lorsqu'un nouveau fournisseur est annoncé et ajouté au store, les entités abonnées en sont notifiées via un mécanisme de callback, leur permettant de réagir à la liste mise à jour des fournisseurs.
Définissons ensuite un hook useSyncExternalStore pour synchroniser l'état local avec le store externe défini dans store.tsx ci-dessus :
import { useSyncExternalStore } from "react";
import { store } from "./store";
export const useSyncProviders = ()=> useSyncExternalStore(store.subscribe, store.value, store.value)
Afficher dynamiquement des boutons pour chaque fournisseur de portefeuille Web3 détecté
Définissons un composant React qui utilise le hook useSyncProviders pour afficher dynamiquement un bouton pour chaque fournisseur de portefeuille détecté :
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>
)}
</>
);
};
Voici une présentation détaillée du composant React ci-dessus :
Nous utilisons deux hooks d'état, selectedWallet et userAccount, pour suivre respectivement le fournisseur de portefeuille Ethereum actuellement sélectionné et l'adresse du compte de l'utilisateur.
Nous utilisons le hook useSyncProviders pour détecter dynamiquement chaque fournisseur de portefeuille Ethereum disponible. Ce hook retourne un tableau de fournisseurs, chacun conforme à l'interface EIP6963ProviderDetail, qui inclut les informations du fournisseur et l'objet fournisseur Ethereum.
Nous définissons la fonction handleConnect, invoquée lorsqu'un bouton de fournisseur de portefeuille est cliqué. Cette fonction utilise la méthode request du fournisseur avec la méthode eth_requestAccounts pour demander à l'utilisateur l'accès à son compte. Si l'accès est accordé, la première adresse de compte est stockée dans userAccount, et les détails du fournisseur sont stockés dans selectedWallet.
Enfin, nous affichons une liste de boutons pour chaque fournisseur de portefeuille détecté, affichant le nom et l'icône du fournisseur.
Une fois notre composant défini, la dernière étape consiste à l'afficher.
import './App.css'
import { DiscoverWalletProviders } from './components/DiscoverWalletProviders'
function App() {
return (
<>
<DiscoverWalletProviders/>
</>
)
}
export default App
En quelques étapes, nous avons réussi à implémenter le support EIP-6963 dans notre dapp à un niveau fondamental. Vous trouverez ci-dessous une représentation visuelle des fonctionnalités que nous venons de créer. Le code complet de la dapp illustrée dans la démonstration visuelle est disponible sur GitHub.
EIP-6963 et la voie à suivre pour les portefeuilles Web3
Alors que nous continuons à observer une adoption plus large de ce standard par les développeurs de portefeuilles et les bibliothèques de connexion tierces, nous sommes enthousiastes quant aux nombreux cas d'usage qu'il ouvre pour la communauté Ethereum, notamment en améliorant l'expérience utilisateur pour l'intégration de nouveaux utilisateurs.
Chez MetaMask, nous sommes impatients de collaborer avec d'autres développeurs de portefeuilles, développeurs de dapps et bibliothèques de connexion tierces pour assurer une adoption encore plus large d'EIP-6963. Pour plus d'informations sur nos travaux visant à soutenir ce standard, veuillez consulter la documentation MetaMask.