Skip to content

Commit 53e3f81

Browse files
authored
Implement Wallet Discovery For Multichain API #2970 (#395)
* feat: support CAIP294 (standardized messaging transport for browser extension wallets)
1 parent 157d24f commit 53e3f81

12 files changed

+633
-26
lines changed

.eslintrc.js

+6-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,12 @@ module.exports = {
4141
},
4242

4343
{
44-
files: ['EIP6963.test.ts', 'jest.setup.browser.js'],
44+
files: [
45+
'EIP6963.test.ts',
46+
'CAIP294.test.ts',
47+
'initializeInpageProvider.test.ts',
48+
'jest.setup.browser.js',
49+
],
4550
rules: {
4651
// We're mixing Node and browser environments in these files.
4752
'no-restricted-globals': 'off',

jest.config.js

+5-4
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,10 @@ const baseConfig = {
4545
// An object that configures minimum threshold enforcement for coverage results
4646
coverageThreshold: {
4747
global: {
48-
branches: 64.65,
49-
functions: 65.65,
50-
lines: 65.51,
51-
statements: 65.61,
48+
branches: 67.6,
49+
functions: 69.91,
50+
lines: 69.51,
51+
statements: 69.52,
5252
},
5353
},
5454

@@ -226,6 +226,7 @@ const browserConfig = {
226226
'**/*InpageProvider.test.ts',
227227
'**/*ExtensionProvider.test.ts',
228228
'**/EIP6963.test.ts',
229+
'**/CAIP294.test.ts',
229230
],
230231
setupFilesAfterEnv: ['./jest.setup.browser.js'],
231232
};

src/CAIP294.test.ts

+200
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
import {
2+
announceWallet,
3+
CAIP294EventNames,
4+
type CAIP294WalletData,
5+
requestWallet,
6+
} from './CAIP294';
7+
8+
const getWalletData = (): CAIP294WalletData => ({
9+
uuid: '350670db-19fa-4704-a166-e52e178b59d2',
10+
name: 'Example Wallet',
11+
icon: 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg"/>',
12+
rdns: 'com.example.wallet',
13+
extensionId: 'abcdefghijklmnopqrstuvwxyz',
14+
});
15+
16+
const walletDataValidationError = () =>
17+
new Error(
18+
`Invalid CAIP-294 WalletData object received from ${CAIP294EventNames.Prompt}. See https://github.com/ChainAgnostic/CAIPs/blob/bc4942857a8e04593ed92f7dc66653577a1c4435/CAIPs/caip-294.md for requirements.`,
19+
);
20+
21+
describe('CAIP-294', () => {
22+
describe('wallet data validation', () => {
23+
it('throws if the wallet data is not a plain object', () => {
24+
[null, undefined, Symbol('bar'), []].forEach((invalidInfo) => {
25+
expect(() => announceWallet(invalidInfo as any)).toThrow(
26+
walletDataValidationError(),
27+
);
28+
});
29+
});
30+
31+
it('throws if the `icon` field is invalid', () => {
32+
[
33+
null,
34+
undefined,
35+
'',
36+
'not-a-data-uri',
37+
'https://example.com/logo.png',
38+
'data:text/plain;blah',
39+
Symbol('bar'),
40+
].forEach((invalidIcon) => {
41+
const walletInfo = getWalletData();
42+
walletInfo.icon = invalidIcon as any;
43+
44+
expect(() => announceWallet(walletInfo)).toThrow(
45+
walletDataValidationError(),
46+
);
47+
});
48+
});
49+
50+
it('throws if the `name` field is invalid', () => {
51+
[null, undefined, '', {}, [], Symbol('bar')].forEach((invalidName) => {
52+
const walletInfo = getWalletData();
53+
walletInfo.name = invalidName as any;
54+
55+
expect(() => announceWallet(walletInfo)).toThrow(
56+
walletDataValidationError(),
57+
);
58+
});
59+
});
60+
61+
it('throws if the `uuid` field is invalid', () => {
62+
[null, undefined, '', 'foo', Symbol('bar')].forEach((invalidUuid) => {
63+
const walletInfo = getWalletData();
64+
walletInfo.uuid = invalidUuid as any;
65+
66+
expect(() => announceWallet(walletInfo)).toThrow(
67+
walletDataValidationError(),
68+
);
69+
});
70+
});
71+
72+
it('throws if the `rdns` field is invalid', () => {
73+
[
74+
null,
75+
undefined,
76+
'',
77+
'not-a-valid-domain',
78+
'..com',
79+
'com.',
80+
Symbol('bar'),
81+
].forEach((invalidRdns) => {
82+
const walletInfo = getWalletData();
83+
walletInfo.rdns = invalidRdns as any;
84+
85+
expect(() => announceWallet(walletInfo)).toThrow(
86+
walletDataValidationError(),
87+
);
88+
});
89+
});
90+
91+
it('allows `extensionId` to be undefined or a string', () => {
92+
const walletInfo = getWalletData();
93+
expect(() => announceWallet(walletInfo)).not.toThrow();
94+
95+
delete walletInfo.extensionId;
96+
97+
expect(() => announceWallet(walletInfo)).not.toThrow();
98+
99+
walletInfo.extensionId = 'valid-string';
100+
expect(() => announceWallet(walletInfo)).not.toThrow();
101+
});
102+
});
103+
104+
it('throws if the `extensionId` field is invalid', () => {
105+
[null, '', 42, Symbol('bar')].forEach((invalidExtensionId) => {
106+
const walletInfo = getWalletData();
107+
walletInfo.extensionId = invalidExtensionId as any;
108+
109+
expect(() => announceWallet(walletInfo)).toThrow(
110+
walletDataValidationError(),
111+
);
112+
});
113+
});
114+
115+
it('wallet is announced before dapp requests', async () => {
116+
const walletData = getWalletData();
117+
const handleWallet = jest.fn();
118+
const dispatchEvent = jest.spyOn(window, 'dispatchEvent');
119+
const addEventListener = jest.spyOn(window, 'addEventListener');
120+
121+
announceWallet(walletData);
122+
requestWallet(handleWallet);
123+
await delay();
124+
125+
expect(dispatchEvent).toHaveBeenCalledTimes(3);
126+
expect(dispatchEvent).toHaveBeenNthCalledWith(
127+
1,
128+
new CustomEvent(CAIP294EventNames.Announce, expect.any(Object)),
129+
);
130+
expect(dispatchEvent).toHaveBeenNthCalledWith(
131+
2,
132+
new CustomEvent(CAIP294EventNames.Prompt, expect.any(Object)),
133+
);
134+
expect(dispatchEvent).toHaveBeenNthCalledWith(
135+
3,
136+
new CustomEvent(CAIP294EventNames.Announce, expect.any(Object)),
137+
);
138+
139+
expect(addEventListener).toHaveBeenCalledTimes(2);
140+
expect(addEventListener).toHaveBeenCalledWith(
141+
CAIP294EventNames.Announce,
142+
expect.any(Function),
143+
);
144+
expect(addEventListener).toHaveBeenCalledWith(
145+
CAIP294EventNames.Prompt,
146+
expect.any(Function),
147+
);
148+
149+
expect(handleWallet).toHaveBeenCalledTimes(1);
150+
expect(handleWallet).toHaveBeenCalledWith(
151+
expect.objectContaining({ params: walletData }),
152+
);
153+
});
154+
155+
it('dapp requests before wallet is announced', async () => {
156+
const walletData = getWalletData();
157+
const handleWallet = jest.fn();
158+
const dispatchEvent = jest.spyOn(window, 'dispatchEvent');
159+
const addEventListener = jest.spyOn(window, 'addEventListener');
160+
161+
requestWallet(handleWallet);
162+
announceWallet(walletData);
163+
await delay();
164+
165+
expect(dispatchEvent).toHaveBeenCalledTimes(2);
166+
expect(dispatchEvent).toHaveBeenNthCalledWith(
167+
1,
168+
new CustomEvent(CAIP294EventNames.Prompt, expect.any(Object)),
169+
);
170+
expect(dispatchEvent).toHaveBeenNthCalledWith(
171+
2,
172+
new CustomEvent(CAIP294EventNames.Announce, expect.any(Object)),
173+
);
174+
175+
expect(addEventListener).toHaveBeenCalledTimes(2);
176+
expect(addEventListener).toHaveBeenCalledWith(
177+
CAIP294EventNames.Announce,
178+
expect.any(Function),
179+
);
180+
expect(addEventListener).toHaveBeenCalledWith(
181+
CAIP294EventNames.Prompt,
182+
expect.any(Function),
183+
);
184+
185+
expect(handleWallet).toHaveBeenCalledTimes(1);
186+
expect(handleWallet).toHaveBeenCalledWith(
187+
expect.objectContaining({ params: walletData }),
188+
);
189+
});
190+
});
191+
192+
/**
193+
* Delay for a number of milliseconds by awaiting a promise
194+
* resolved after the specified number of milliseconds.
195+
*
196+
* @param ms - The number of milliseconds to delay for.
197+
*/
198+
async function delay(ms = 1) {
199+
return new Promise((resolve) => setTimeout(resolve, ms));
200+
}

0 commit comments

Comments
 (0)