Skip to content

Commit 66cdaf0

Browse files
feat: Use EIP-6963 for test-snaps
1 parent 9392ef9 commit 66cdaf0

File tree

1 file changed

+129
-3
lines changed

1 file changed

+129
-3
lines changed

packages/test-snaps/src/api.ts

Lines changed: 129 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,116 @@ 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+
*/
93+
function onAnnounceProvider(event: EIP6963AnnounceProviderEvent) {
94+
const { info, provider } = event.detail;
95+
96+
if (info.rdns.includes('io.metamask')) {
97+
resolveWithCleanup(provider);
98+
}
99+
}
100+
101+
window.addEventListener('eip6963:announceProvider', onAnnounceProvider);
102+
103+
window.dispatchEvent(new Event('eip6963:requestProvider'));
104+
});
105+
}
106+
107+
/**
108+
* Get a provider that supports snaps. This will loop through all the detected
109+
* providers and return the first one that supports snaps.
110+
*
111+
* @returns The provider, or `null` if no provider supports snaps.
112+
*/
113+
async function getSnapsProvider() {
114+
if (typeof window === 'undefined') {
115+
return null;
116+
}
117+
118+
const eip6963Provider = await getMetaMaskEIP6963Provider();
119+
120+
if (eip6963Provider && (await hasSnapsSupport(eip6963Provider))) {
121+
return eip6963Provider;
122+
}
123+
124+
if (await hasSnapsSupport()) {
125+
return window.ethereum;
126+
}
127+
128+
if (window.ethereum?.detected) {
129+
for (const provider of window.ethereum.detected) {
130+
if (await hasSnapsSupport(provider)) {
131+
return provider;
132+
}
133+
}
134+
}
135+
136+
if (window.ethereum?.providers) {
137+
for (const provider of window.ethereum.providers) {
138+
if (await hasSnapsSupport(provider)) {
139+
return provider;
140+
}
141+
}
142+
}
143+
144+
return null;
145+
}
146+
25147
/**
26148
* Base request function for all API calls.
27149
*
@@ -35,7 +157,11 @@ export const request: BaseQueryFn<RequestArguments> = async ({
35157
params,
36158
}) => {
37159
try {
38-
const data = await window.ethereum.request({ method, params });
160+
const provider = await getSnapsProvider();
161+
162+
assert(provider, 'No Ethereum provider found.');
163+
164+
const data = await provider.request({ method, params });
39165

40166
return { data };
41167
} catch (error: any) {

0 commit comments

Comments
 (0)