Skip to content

Commit 2f487f2

Browse files
feat: Use EIP-6963 for test-snaps (#3757)
Use EIP-6963 for the `test-snaps` page, preventing collisions with wallets that don't support Snaps. Mostly re-uses the logic from `snaps-directory`, adapting it to this repo. Fixes #3753 <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Adopts EIP-6963 to discover a MetaMask provider that supports Snaps and routes all RPC requests through it. > > - **Provider discovery and Snaps support**: > - Add `hasSnapsSupport`, `getMetaMaskEIP6963Provider`, and `getSnapsProvider` to find a MetaMask provider via EIP-6963 and verify `wallet_getSnaps` support. > - Update `request` base query to use the discovered provider and assert presence before RPC calls. > - **Type augmentations**: > - Extend `window.ethereum` to include `setProvider`, `detected`, and `providers`. > - Add `WindowEventMap` entries for `eip6963:requestProvider` and `eip6963:announceProvider`. > - **Imports**: > - Import EIP-6963 event types and `assert` from `@metamask/utils`. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 994719c. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 9392ef9 commit 2f487f2

File tree

1 file changed

+134
-3
lines changed

1 file changed

+134
-3
lines changed

packages/test-snaps/src/api.ts

Lines changed: 134 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,28 @@
11
import type {
2+
EIP6963AnnounceProviderEvent,
3+
EIP6963RequestProviderEvent,
24
MetaMaskInpageProvider,
35
RequestArguments,
46
} from '@metamask/providers';
57
import { logError } from '@metamask/snaps-utils';
6-
import type { JsonRpcError, JsonRpcParams } from '@metamask/utils';
8+
import { assert, type JsonRpcError, type JsonRpcParams } from '@metamask/utils';
79
import type { BaseQueryFn } from '@reduxjs/toolkit/query/react';
810
import { createApi } from '@reduxjs/toolkit/query/react';
911

1012
declare global {
1113
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
1214
interface Window {
13-
ethereum: MetaMaskInpageProvider;
15+
ethereum: MetaMaskInpageProvider & {
16+
setProvider?: (provider: MetaMaskInpageProvider) => void;
17+
detected?: MetaMaskInpageProvider[];
18+
providers?: MetaMaskInpageProvider[];
19+
};
20+
}
21+
22+
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
23+
interface WindowEventMap {
24+
'eip6963:requestProvider': EIP6963RequestProviderEvent;
25+
'eip6963:announceProvider': EIP6963AnnounceProviderEvent;
1426
}
1527
}
1628

@@ -22,6 +34,121 @@ export enum Tag {
2234
UnencryptedTestState = 'Unencrypted Test State',
2335
}
2436

37+
/**
38+
* Check if the current provider supports snaps by calling `wallet_getSnaps`.
39+
*
40+
* @param provider - The provider to use to check for snaps support. Defaults to
41+
* `window.ethereum`.
42+
* @returns True if the provider supports snaps, false otherwise.
43+
*/
44+
async function hasSnapsSupport(
45+
provider: MetaMaskInpageProvider = window.ethereum,
46+
) {
47+
try {
48+
await provider.request({
49+
method: 'wallet_getSnaps',
50+
});
51+
52+
return true;
53+
} catch {
54+
return false;
55+
}
56+
}
57+
58+
/**
59+
* Get a MetaMask provider using EIP6963. This will return the first provider
60+
* reporting as MetaMask. If no provider is found after 500ms, this will
61+
* return null instead.
62+
*
63+
* @returns A MetaMask provider if found, otherwise null.
64+
*/
65+
async function getMetaMaskEIP6963Provider() {
66+
return new Promise<MetaMaskInpageProvider | null>((resolve) => {
67+
// Timeout looking for providers after 500ms
68+
const timeout = setTimeout(() => {
69+
resolveWithCleanup(null);
70+
}, 500);
71+
72+
/**
73+
* Resolve the promise with a MetaMask provider and clean up.
74+
*
75+
* @param provider - A MetaMask provider if found, otherwise null.
76+
*/
77+
function resolveWithCleanup(provider: MetaMaskInpageProvider | null) {
78+
window.removeEventListener(
79+
'eip6963:announceProvider',
80+
onAnnounceProvider,
81+
);
82+
clearTimeout(timeout);
83+
resolve(provider);
84+
}
85+
86+
/**
87+
* Listener for the EIP6963 announceProvider event.
88+
*
89+
* Resolves the promise if a MetaMask provider is found.
90+
*
91+
* @param event - The EIP6963 announceProvider event.
92+
* @param event.detail - The details of the EIP6963 announceProvider event.
93+
*/
94+
function onAnnounceProvider({ detail }: EIP6963AnnounceProviderEvent) {
95+
if (!detail) {
96+
return;
97+
}
98+
99+
const { info, provider } = detail;
100+
101+
if (info.rdns.includes('io.metamask')) {
102+
resolveWithCleanup(provider);
103+
}
104+
}
105+
106+
window.addEventListener('eip6963:announceProvider', onAnnounceProvider);
107+
108+
window.dispatchEvent(new Event('eip6963:requestProvider'));
109+
});
110+
}
111+
112+
/**
113+
* Get a provider that supports snaps. This will loop through all the detected
114+
* providers and return the first one that supports snaps.
115+
*
116+
* @returns The provider, or `null` if no provider supports snaps.
117+
*/
118+
async function getSnapsProvider() {
119+
if (typeof window === 'undefined') {
120+
return null;
121+
}
122+
123+
const eip6963Provider = await getMetaMaskEIP6963Provider();
124+
125+
if (eip6963Provider && (await hasSnapsSupport(eip6963Provider))) {
126+
return eip6963Provider;
127+
}
128+
129+
if (await hasSnapsSupport()) {
130+
return window.ethereum;
131+
}
132+
133+
if (window.ethereum?.detected) {
134+
for (const provider of window.ethereum.detected) {
135+
if (await hasSnapsSupport(provider)) {
136+
return provider;
137+
}
138+
}
139+
}
140+
141+
if (window.ethereum?.providers) {
142+
for (const provider of window.ethereum.providers) {
143+
if (await hasSnapsSupport(provider)) {
144+
return provider;
145+
}
146+
}
147+
}
148+
149+
return null;
150+
}
151+
25152
/**
26153
* Base request function for all API calls.
27154
*
@@ -35,7 +162,11 @@ export const request: BaseQueryFn<RequestArguments> = async ({
35162
params,
36163
}) => {
37164
try {
38-
const data = await window.ethereum.request({ method, params });
165+
const provider = await getSnapsProvider();
166+
167+
assert(provider, 'No Ethereum provider found.');
168+
169+
const data = await provider.request({ method, params });
39170

40171
return { data };
41172
} catch (error: any) {

0 commit comments

Comments
 (0)