2023年5月,一群以太坊钱包开发者齐聚一堂,共同制定了一项用于连接dapp的新行业标准。在此之前,钱包通常通过网络浏览器中的 window.ethereum 全局对象暴露EIP-1193提供者,这一做法已成为惯例。在早期,用户通常只拥有并连接一个钱包,这种方式运行良好。
随着生态系统的发展,用户开始同时使用多个钱包,dapp开发者在管理钱包检测流程方面愈发困难——无论是识别用户当前连接的是哪个钱包,还是在安装了多个钱包时为用户提供选择,都面临挑战。
2023年10月,这一标准正式获得通过,即现在所称的EIP-6963,又名多钱包注入提供者发现协议。此后,钱包开发者开始将EIP-6963作为发现钱包提供者的默认方式。MetaMask也不例外,我们已确保MetaMask钱包API和MetaMask SDK均支持该标准。
此外,依赖 window.ethereum 的dapp将继续获得支持。不过,我们鼓励所有dapp将EIP-6963支持作为首选的提供者发现机制加以实现。最简便的方式是集成支持EIP-6963的第三方库。请访问MetaMask文档,查看持续更新的可用库列表。
本教程将引导您完成在dapp中实现EIP-6963支持的基本步骤,适用于不希望使用现有第三方库的开发者。教程采用React和TypeScript编写,但任何JavaScript开发者都应能够参照实现,并将其应用于自己偏好的框架中。
为Web3钱包定义支持的接口和类型
在dapp中支持多钱包发现的第一步,是按照改进提案的规范定义所有支持的接口和类型。这些接口和类型提供了一种结构化、标准化的方式来处理以太坊钱包提供者,从而便于集成和发现多个以太坊钱包。
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);
}
}
以下是上述代码示例的详细说明:
我们通过自定义事件"eip6963:announceProvider"扩展了 WindowEventMap,从而建立了一个全局事件类型,用于宣告新以太坊钱包提供者的可用性。
我们初始化了一个外部存储,以命名提供者数组的形式表示,用于追踪所有已检测到的钱包提供者。该数组存储符合 EIP6963ProviderDetail 接口的对象,其中包含钱包提供者及其对应以太坊提供者的相关信息。
我们通过存储对象中的 subscribe 函数引入了订阅机制。该函数监听"eip6963:announceProvider"事件,并将任何新宣告的提供者详情更新至外部存储。同时,它确保仅在提供者尚未存在于存储中时才将其添加,从而避免重复条目。
存储对象提供两项核心功能:1. value 函数用于获取外部存储的当前状态,即已检测到的钱包提供者列表。
subscribe 函数使组件或应用程序的其他部分能够订阅存储中的变更。当新提供者被宣告并添加至存储时,已订阅的实体将通过回调机制收到通知,从而对更新后的提供者列表作出响应。
接下来,让我们定义一个 useSyncExternalStore hook,以将本地状态与上方store.tsx中定义的外部存储同步:
import { useSyncExternalStore } from "react";
import { store } from "./store";
export const useSyncProviders = ()=> useSyncExternalStore(store.subscribe, store.value, store.value)
为每个检测到的Web3钱包提供者动态显示按钮
让我们定义一个React组件,使用 useSyncProviders hook为每个检测到的钱包提供者动态渲染一个按钮:
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组件的详细说明:
我们使用两个状态hook——selectedWallet 和 userAccount,分别追踪当前选定的以太坊钱包提供者和用户的账户地址。
我们使用 useSyncProviders hook动态检测每个可用的以太坊钱包提供者。该hook返回一个提供者数组,每个提供者均符合EIP6963ProviderDetail接口,包含提供者信息和以太坊提供者对象。
我们定义了 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钱包的未来之路
随着越来越多的钱包开发者和第三方连接库采用这一标准,我们对其为以太坊社区带来的各种应用场景充满期待,尤其是在改善新用户引导体验方面。
MetaMask团队热切期待与其他钱包开发者、dapp开发者以及第三方连接库携手合作,推动EIP-6963的更广泛采用。如需了解我们在支持该标准方面所做工作的更多信息,请参阅MetaMask文档。