什么是 EIP-6963 协议?
EIP-6963 是以太坊改进提案(Ethereum Improvement Proposal, EIP)中的一项提案,旨在定义与以太坊节点(Ethereum nodes)进行交互的标准协议。它为开发者和用户提供了一种标准化的方法,来发现和连接以太坊节点,从而简化了以太坊客户端和应用程序之间的互操作性。
EIP-6963的主要目标
- 节点发现:提供一种标准化的方法,帮助客户端应用程序发现可用的以太坊节点。
- 节点连接:定义一种协议,使得客户端应用程序能够轻松地连接到发现的节点,并开始与其进行通信。
- 互操作性:确保不同的客户端和节点实现能够互操作,减少开发者在实现节点发现和连接功能时的复杂性。
EIP-1193和EIP-6963的区别是什么
其实两者的区别还是蛮大的。业务应用场景基本是满足不同的需求。
- EIP-1193 只是定义了一个标准的 JavaScript API,用于与以太坊提供者(钱包或节点)进行交互。其主要目标是提供一个统一的接口,使得 DApp 开发者能够以一致的方式与各种以太坊提供者进行通信。
- EIP-6963 提出了一个标准协议,用于发现和连接以太坊节点。其主要目标是提供一种标准化的方法,使得客户端应用程序能够更容易地发现和连接到以太坊节点。
但老实说只要开发过钱包的人都会知道,不同浏览器的注入对象冲突是非常常见的事情。往往需要在业务逻辑中加入判断代码或者做区分,这对于一个dapp开发来显然是不现实的。因为通过EIP-1193标准本身是无法区分钱包环境的。所以说其实 EIP-6963 也可以在某方面解决这个问题
如何在前端集成EIP-6963
1. 设定Web3钱包支持的接口与类型定义
首先定义接口类型,为了让你的dapp支持多种钱包,首先需要根据相关改进提案来定义所有支持的接口和类型。这提供了一种组织化的方式来与以太坊钱包提供者交互,使得集成和识别多个钱包变得简单。
// EthereumProviderTypes.d.ts
// Interface for provider information following EIP-6963.
interface EIP6963ProviderInfo {
walletId: string; // Unique identifier for the wallet e.g io.metamask, io.metamask.flask
uuid: string; // Globally unique ID to differentiate between provider sessions for the lifetime of the page
name: string; // Human-readable name of the wallet
icon: string; // URL to the wallet's icon
}
// Interface for Ethereum providers based on the EIP-1193 standard.
interface EIP1193Provider {
isStatus?: boolean; // Optional: Indicates the status of the provider
host?: string; // Optional: Host URL of the Ethereum node
path?: string; // Optional: Path to a specific endpoint or service on the host
sendAsync?: (request: { method: string, params?: Array<unknown> }, callback: (error: Error | null, response: unknown) => void) => void; // For sending asynchronous requests
send?: (request: { method: string, params?: Array<unknown> }, callback: (error: Error | null, response: unknown) => void) => void; // For sending synchronous requests
request: (request: { method: string, params?: Array<unknown> }) => Promise<unknown>; // Standard method for sending requests per EIP-1193
}
// Interface detailing the structure of provider information and its Ethereum provider.
interface EIP6963ProviderDetail {
info: EIP6963ProviderInfo; // The provider's info
provider: EIP1193Provider; // The EIP-1193 compatible provider
}
// Type representing the event structure for announcing a provider based on EIP-6963.
type EIP6963AnnounceProviderEvent = {
detail: {
info: EIP6963ProviderInfo; // The provider's info
provider: EIP1193Provider; // The EIP-1193 compatible provider
}
}
2. 监听事件并接收新钱包连接的通知
接下来,你需要建立一个机制,以便当新的钱包连接被检测到时,你的dapp能够得到通知。EIP-6963的目标是实现多个钱包之间的无缝连接与切换。在React环境中,我们可以通过以下代码示例来实现这一点:
The next step is to establish a way to alert your dapp when new wallet connections are detected. Remember, the purpose of EIP-6963 is to enable seamless connection and switching between multiple wallets. In React, we can achieve this with the sample code below.
// store.tsx
declare global {
interface WindowEventMap {
"eip6963:announceProvider": CustomEvent<EIP6963AnnounceProviderEvent>;
}
}
let providers: EIP6963ProviderDetail[] = [];
export const store = {
value: () => providers,
subscribe: (callback: () => void) => {
function onAnnouncement(event: EIP6963AnnounceProviderEvent) {
// Prevent adding a provider if it already exists in the list based on its uuid.
if (providers.some(p => p.info.uuid === event.detail.info.uuid)) return;
// Add the new provider to the list and call the provided callback function.
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);
}
}
以下是上述代码示例的详细说明:
- 我们通过扩展
WindowEventMap
,添加了一个名为 "eip6963:announceProvider" 的自定义事件,从而建立了一个用于宣布新的以太坊钱包提供者可用性的全局事件类型。 - 我们初始化了一个外部存储,表示为一个命名提供者的数组,用于跟踪所有检测到的钱包提供者。这个数组存储符合
EIP6963ProviderDetail
接口的对象,其中包括关于钱包提供者及其相应的以太坊提供者的信息。 - 我们通过
store
对象内的subscribe
函数引入了一个订阅机制。该函数监听 "eip6963:announceProvider" 事件,并更新外部存储中的任何新宣布的提供者的详细信息。它确保只有不在存储中的提供者才会被添加,避免了重复条目的出现。 store
对象提供了两个关键功能:value
函数允许检索外部存储的当前状态,即检测到的钱包提供者列表。subscribe
函数使组件或应用程序的其他部分能够订阅存储中的更改。当一个新的提供者被宣布并添加到存储中时,订阅的实体通过回调机制收到通知,从而能够对提供者列表的更新做出反应。
接下来,我们定义一个 useSyncExternalStore
钩子,以便将本地状态与上面 store.tsx
中定义的外部存储同步:
// useSyncProviders.tsx
import { useSyncExternalStore } from "react";
import { store } from "./store";
export const useSyncProviders = ()=> useSyncExternalStore(store.subscribe, store.value, store.value)
3. 定义一个React组件,该组件使用上述的钩子动态渲染每个检测到的钱包提供者的按钮:
Let’s define a React component that uses the useSyncProviders hook to render a button for each detected wallet provider dynamically:
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 组件的详细说明:
- 我们使用两个状态钩子,
selectedWallet
和userAccount
,分别跟踪当前选择的以太坊钱包提供者和用户的账户地址。 - 我们使用
useSyncProviders
钩子来动态检测每个可用的以太坊钱包提供者。该钩子返回一个提供者数组,每个提供者都符合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了。