Em maio de 2023, um grupo de desenvolvedores de carteiras Ethereum se reuniu para estabelecer um novo padrão da indústria para conexão com dapps. Antes disso, a prática comum era que as carteiras expusessem um provedor EIP-1193 por meio do objeto global window.ethereum nos navegadores web. Essa técnica funcionava bem nos primeiros tempos, quando os usuários normalmente possuíam e se conectavam a apenas uma carteira.
À medida que o ecossistema cresceu e os usuários passaram a utilizar múltiplas carteiras ao mesmo tempo, ficou difícil para os desenvolvedores de dapps gerenciar adequadamente o processo de detectar a qual carteira um usuário estava conectado ou oferecer aos usuários a opção de selecionar qual carteira usar quando várias estavam instaladas.
Em outubro de 2023, esse padrão, agora conhecido como EIP-6963 ou Multi-Wallet Injected Provider Discovery, foi aceito. Desde então, os desenvolvedores de carteiras começaram a adotar o EIP-6963 como a forma padrão de descoberta de provedores de carteiras. A MetaMask não é exceção, pois garantimos que esse padrão seja suportado pela API de carteira da MetaMask e pelo MetaMask SDK.
Além disso, os dapps que dependem do window.ethereum têm suporte contínuo garantido. No entanto, incentivamos a implementação do suporte ao EIP-6963 em todos os dapps como o mecanismo preferencial de descoberta de provedores. A maneira mais fácil de fazer isso é integrando bibliotecas de terceiros com suporte ao EIP-6963. Acesse a documentação da MetaMask para uma lista crescente de bibliotecas disponíveis.
Este tutorial irá guiá-lo pelos passos básicos para implementar o suporte ao EIP-6963 no seu dapp, caso você decida não utilizar as bibliotecas de terceiros disponíveis. Ele usa React e TypeScript; no entanto, qualquer desenvolvedor JavaScript deve conseguir acompanhar e implementar isso no framework de sua preferência.
Defina as Interfaces e Tipos Suportados para Carteiras Web3
O primeiro passo para suportar a descoberta de múltiplas carteiras no seu dapp é definir todas as interfaces e tipos suportados, conforme descrito na proposta de melhoria. Essas interfaces e tipos fornecem uma maneira estruturada e padronizada de lidar com provedores de carteiras Ethereum, facilitando a integração e a descoberta de múltiplas carteiras 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;
}
}
Adicionei comentários para ajudá-lo a entender o que cada interface e tipo representa. Consulte a proposta de melhoria para uma explicação mais detalhada.
Inscreva-se em Eventos e Seja Notificado sobre Novas Conexões
O próximo passo é estabelecer uma forma de alertar seu dapp quando novas conexões de carteiras forem detectadas. Lembre-se: o objetivo do EIP-6963 é permitir a conexão e a alternância fluidas entre múltiplas carteiras. No React, podemos alcançar isso com o código de exemplo abaixo.
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);
}
}
Veja a seguir uma análise do código de exemplo acima:
Estendemos o WindowEventMap com um evento personalizado chamado "eip6963:announceProvider", estabelecendo assim um tipo de evento global para anunciar a disponibilidade de novos provedores de carteiras Ethereum.
Inicializamos um store externo, representado por um array de provedores nomeados, que é usado para rastrear todos os provedores de carteiras detectados. Esse array armazena objetos em conformidade com a interface EIP6963ProviderDetail, que inclui informações sobre o provedor de carteira e seu provedor Ethereum correspondente.
Introduzimos um mecanismo de inscrição por meio da função subscribe dentro do objeto store. Essa função escuta o evento "eip6963:announceProvider" e atualiza o store externo com os detalhes de quaisquer novos provedores anunciados. Ela garante que os provedores sejam adicionados apenas se ainda não estiverem no store, evitando entradas duplicadas.
O objeto store oferece duas funcionalidades principais: 1. A função value permite recuperar o estado atual do store externo, ou seja, a lista de provedores de carteiras detectados.
A função subscribe permite que componentes ou outras partes da aplicação se inscrevam para receber atualizações do store. Quando um novo provedor é anunciado e adicionado ao store, as entidades inscritas são notificadas por meio de um mecanismo de callback, permitindo que reajam à lista atualizada de provedores.
A seguir, vamos definir um hook useSyncExternalStore para sincronizar o estado local com o store externo definido no store.tsx acima:
import { useSyncExternalStore } from "react";
import { store } from "./store";
export const useSyncProviders = ()=> useSyncExternalStore(store.subscribe, store.value, store.value)
Exiba Botões Dinamicamente para cada Provedor de Carteira Web3 Detectado
Vamos definir um componente React que usa o hook useSyncProviders para renderizar dinamicamente um botão para cada provedor de carteira 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>
)}
</>
);
};
Veja a seguir uma análise do componente React acima:
Usamos dois hooks de estado, selectedWallet e userAccount, para rastrear o provedor de carteira Ethereum atualmente selecionado e o endereço da conta do usuário, respectivamente.
Usamos o hook useSyncProviders para detectar dinamicamente cada provedor de carteira Ethereum disponível. Esse hook retorna um array de provedores, cada um em conformidade com a interface EIP6963ProviderDetail, que inclui as informações do provedor e o objeto provedor Ethereum.
Definimos a função handleConnect, que é invocada quando o botão de um provedor de carteira é clicado. Essa função usa o método request do provedor com o método eth_requestAccounts para solicitar ao usuário o acesso à conta. Se o acesso for concedido, o primeiro endereço de conta é armazenado em userAccount, e os detalhes do provedor são armazenados em selectedWallet.
Por fim, renderizamos uma lista de botões para cada provedor de carteira detectado, exibindo o nome e o ícone do provedor.
Com nosso componente definido, o último passo é renderizá-lo.
import './App.css'
import { DiscoverWalletProviders } from './components/DiscoverWalletProviders'
function App() {
return (
<>
<DiscoverWalletProviders/>
</>
)
}
export default App
Com esses poucos passos, implementamos com sucesso o suporte ao EIP-6963 no nosso dapp em um nível fundamental. Veja abaixo uma representação visual da funcionalidade que acabamos de construir. O código completo do dapp capturado no visual pode ser encontrado no GitHub.
EIP-6963 e o Caminho à Frente para as Carteiras Web3
À medida que continuamos a ver uma adoção mais ampla desse padrão por desenvolvedores de carteiras e bibliotecas de conexão de terceiros, estamos animados com os diversos casos de uso que ele abre para a comunidade Ethereum, especialmente na melhoria da UX para a integração de novos usuários.
Nós, da MetaMask, estamos ansiosos para colaborar com outros desenvolvedores de carteiras, desenvolvedores de dapps e bibliotecas de conexão de terceiros para garantir uma adoção ainda maior do EIP-6963. Para mais informações sobre nosso trabalho de suporte a esse padrão, consulte a documentação da MetaMask.