En mayo de 2023, un grupo de desarrolladores de wallets de Ethereum se reunió para establecer un nuevo estándar de la industria para conectarse con dapps. Antes de eso, la práctica habitual era que las wallets expusieran un proveedor EIP-1193 a través del objeto global window.ethereum en los navegadores web. Esta técnica funcionaba bien en los primeros tiempos, cuando los usuarios generalmente poseían y se conectaban a una sola wallet.
A medida que el ecosistema creció y los usuarios comenzaron a utilizar múltiples wallets al mismo tiempo, se volvió difícil para los desarrolladores de dapps gestionar correctamente el proceso de detectar a qué wallet estaba conectado un usuario, o darles la opción de seleccionar qué wallet usar cuando tenían varias instaladas.
En octubre de 2023, este estándar, ahora conocido como EIP-6963 o Multi-Wallet Injected Provider Discovery, fue aceptado. Desde entonces, los desarrolladores de wallets han comenzado a introducir EIP-6963 como la forma predeterminada de descubrir proveedores de wallets. MetaMask no es la excepción, ya que nos hemos asegurado de que este estándar sea compatible con la API de wallet de MetaMask y el SDK de MetaMask.
Además, las dapps que dependen de window.ethereum tienen garantizada la continuidad del soporte. Sin embargo, recomendamos implementar la compatibilidad con EIP-6963 en todas las dapps como el mecanismo de descubrimiento de proveedores preferido. La forma más sencilla de hacerlo es integrándose con bibliotecas de terceros que soporten EIP-6963. Visita la documentación de MetaMask para ver una lista en crecimiento de las bibliotecas disponibles.
Este tutorial te guiará a través de los pasos básicos para implementar la compatibilidad con EIP-6963 en tu dapp si decides no utilizar las bibliotecas de terceros disponibles. Utiliza React y TypeScript; sin embargo, cualquier desarrollador de JavaScript debería poder seguirlo e implementarlo en el framework de su elección.
Define las Interfaces y Tipos Compatibles para Wallets Web3
El primer paso para soportar el descubrimiento de múltiples wallets en tu dapp es definir todas las interfaces y tipos compatibles tal como se describen en la propuesta de mejora. Estas interfaces y tipos proporcionan una forma estructurada y estandarizada de gestionar los proveedores de wallets de Ethereum, facilitando la integración y el descubrimiento de múltiples wallets de 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;
}
}
He añadido comentarios para ayudarte a entender qué representa cada interfaz y tipo. Consulta la propuesta de mejora para obtener una explicación más detallada.
Suscríbete a Eventos y Recibe Notificaciones de Nuevas Conexiones
El siguiente paso es establecer una forma de alertar a tu dapp cuando se detecten nuevas conexiones de wallets. Recuerda que el propósito de EIP-6963 es permitir la conexión y el cambio fluido entre múltiples wallets. En React, podemos lograrlo con el código de ejemplo que se muestra a continuación.
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);
}
}
A continuación, se explica el código de ejemplo anterior:
Extendemos el WindowEventMap con un evento personalizado llamado "eip6963:announceProvider", estableciendo así un tipo de evento global para anunciar la disponibilidad de nuevos proveedores de wallets de Ethereum.
Inicializamos un almacén externo, representado por un array de proveedores con nombre, que se utiliza para llevar un registro de todos los proveedores de wallets detectados. Este array almacena objetos que se ajustan a la interfaz EIP6963ProviderDetail, que incluye información sobre el proveedor de la wallet y su proveedor de Ethereum correspondiente.
Introducimos un mecanismo de suscripción a través de la función subscribe dentro del objeto store. Esta función escucha el evento "eip6963:announceProvider" y actualiza el almacén externo con los detalles de cualquier nuevo proveedor anunciado. Garantiza que los proveedores solo se añadan si no están en el almacén, evitando entradas duplicadas.
El objeto store proporciona dos funcionalidades clave: 1. La función value permite recuperar el estado actual del almacén externo, es decir, la lista de proveedores de wallets detectados.
La función subscribe permite que los componentes u otras partes de la aplicación se suscriban a los cambios en el almacén. Cuando se anuncia un nuevo proveedor y se añade al almacén, las entidades suscritas reciben una notificación a través de un mecanismo de callback, lo que les permite reaccionar ante la lista actualizada de proveedores.
A continuación, definamos un hook useSyncExternalStore para sincronizar el estado local con el almacén externo definido en store.tsx anteriormente:
import { useSyncExternalStore } from "react";
import { store } from "./store";
export const useSyncProviders = ()=> useSyncExternalStore(store.subscribe, store.value, store.value)
Muestra Dinámicamente Botones para cada Proveedor de Wallet Web3 Detectado
Definamos un componente de React que utilice el hook useSyncProviders para renderizar dinámicamente un botón por cada proveedor de wallet detectado:
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>
)}
</>
);
};
A continuación, se explica el componente de React anterior:
Utilizamos dos hooks de estado, selectedWallet y userAccount, para llevar un registro del proveedor de wallet de Ethereum actualmente seleccionado y la dirección de cuenta del usuario, respectivamente.
Utilizamos el hook useSyncProviders para detectar dinámicamente cada proveedor de wallet de Ethereum disponible. Este hook devuelve un array de proveedores, cada uno conforme a la interfaz EIP6963ProviderDetail, que incluye la información del proveedor y el objeto proveedor de Ethereum.
Definimos la función handleConnect, que se invoca cuando se hace clic en el botón de un proveedor de wallet. Esta función utiliza el método request del proveedor con el método eth_requestAccounts para solicitar al usuario acceso a su cuenta. Si se concede el acceso, la primera dirección de cuenta se almacena en userAccount y los detalles del proveedor se almacenan en selectedWallet.
Por último, renderizamos una lista de botones para cada proveedor de wallet detectado, mostrando el nombre e ícono del proveedor.
Con nuestro componente definido, el último paso es renderizar este componente.
import './App.css'
import { DiscoverWalletProviders } from './components/DiscoverWalletProviders'
function App() {
return (
<>
<DiscoverWalletProviders/>
</>
)
}
export default App
Con estos pocos pasos, hemos implementado exitosamente la compatibilidad con EIP-6963 en nuestra dapp a un nivel fundamental. A continuación, encontrarás una representación visual de la funcionalidad que acabamos de construir. El código completo de la dapp capturado en el visual se puede encontrar en GitHub.
EIP-6963 y el Camino por Delante para las Wallets Web3
A medida que seguimos viendo una adopción más amplia de este estándar por parte de los desarrolladores de wallets y las bibliotecas de conexión de terceros, estamos entusiasmados con los diversos casos de uso que abre para la comunidad de Ethereum, especialmente en la mejora de la experiencia de usuario para la incorporación de nuevos usuarios.
En MetaMask estamos ansiosos por colaborar con otros desarrolladores de wallets, desarrolladores de dapps y bibliotecas de conexión de terceros para garantizar una adopción aún mayor de EIP-6963. Para obtener más información sobre nuestro trabajo en apoyo de este estándar, consulta la documentación de MetaMask.