@meshconnect/uwc-bridge-parent 1.6.0 → 1.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/BaseWalletAdapter.d.ts +1 -1
- package/dist/BaseWalletAdapter.d.ts.map +1 -1
- package/dist/BaseWalletAdapter.js.map +1 -1
- package/dist/BridgeParent.d.ts.map +1 -1
- package/dist/BridgeParent.js +1 -4
- package/dist/BridgeParent.js.map +1 -1
- package/package.json +4 -3
- package/src/BaseWalletAdapter.test.ts +9 -6
- package/src/BridgeParent.test.ts +12 -8
- package/src/BridgeParent.ts +1 -4
- package/dist/BaseWalletAdapter.test.d.ts +0 -2
- package/dist/BaseWalletAdapter.test.d.ts.map +0 -1
- package/dist/BaseWalletAdapter.test.js +0 -299
- package/dist/BaseWalletAdapter.test.js.map +0 -1
- package/dist/BridgeParent.test.d.ts +0 -2
- package/dist/BridgeParent.test.d.ts.map +0 -1
- package/dist/BridgeParent.test.js +0 -1524
- package/dist/BridgeParent.test.js.map +0 -1
- package/dist/signSolanaBytes.test.d.ts +0 -2
- package/dist/signSolanaBytes.test.d.ts.map +0 -1
- package/dist/signSolanaBytes.test.js +0 -194
- package/dist/signSolanaBytes.test.js.map +0 -1
|
@@ -1,1524 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
-
import * as Comlink from 'comlink';
|
|
3
|
-
// Mock Comlink. windowEndpoint mirrors the real one (comlink 4.4.2): its
|
|
4
|
-
// add/removeEventListener are window's own methods bound to window. This lets the
|
|
5
|
-
// destroy tests assert real attachment/detachment on window via dispatchEvent,
|
|
6
|
-
// rather than against a decoupled spy.
|
|
7
|
-
vi.mock('comlink', () => ({
|
|
8
|
-
expose: vi.fn(),
|
|
9
|
-
windowEndpoint: vi.fn(() => ({
|
|
10
|
-
addEventListener: window.addEventListener.bind(window),
|
|
11
|
-
removeEventListener: window.removeEventListener.bind(window),
|
|
12
|
-
postMessage: vi.fn()
|
|
13
|
-
})),
|
|
14
|
-
proxy: vi.fn((obj) => obj)
|
|
15
|
-
}));
|
|
16
|
-
// Mock @wallet-standard/app
|
|
17
|
-
vi.mock('@wallet-standard/app', () => ({
|
|
18
|
-
getWallets: vi.fn().mockReturnValue({ get: () => [] })
|
|
19
|
-
}));
|
|
20
|
-
// Mock @solana/wallet-adapter-base
|
|
21
|
-
vi.mock('@solana/wallet-adapter-base', () => ({
|
|
22
|
-
isWalletAdapterCompatibleStandardWallet: vi.fn().mockReturnValue(false),
|
|
23
|
-
WalletReadyState: {
|
|
24
|
-
Installed: 'Installed',
|
|
25
|
-
NotDetected: 'NotDetected',
|
|
26
|
-
Loadable: 'Loadable',
|
|
27
|
-
Unsupported: 'Unsupported'
|
|
28
|
-
}
|
|
29
|
-
}));
|
|
30
|
-
// Mock @solana/wallet-standard-wallet-adapter-base
|
|
31
|
-
vi.mock('@solana/wallet-standard-wallet-adapter-base', () => ({
|
|
32
|
-
StandardWalletAdapter: vi.fn()
|
|
33
|
-
}));
|
|
34
|
-
// Mock @solana/web3.js
|
|
35
|
-
vi.mock('@solana/web3.js', () => ({
|
|
36
|
-
Connection: vi.fn(),
|
|
37
|
-
Transaction: { from: vi.fn() },
|
|
38
|
-
VersionedTransaction: { deserialize: vi.fn() }
|
|
39
|
-
}));
|
|
40
|
-
// Hoisted mock for BaseWalletAdapter - direct reference avoids restoreAllMocks issues
|
|
41
|
-
const baseAdapterMock = vi.hoisted(() => ({
|
|
42
|
-
readyState: 'NotDetected',
|
|
43
|
-
shouldThrow: false,
|
|
44
|
-
fn: null
|
|
45
|
-
}));
|
|
46
|
-
baseAdapterMock.fn = vi.fn();
|
|
47
|
-
// Mock ./BaseWalletAdapter
|
|
48
|
-
vi.mock('./BaseWalletAdapter', () => ({
|
|
49
|
-
BaseWalletAdapter: baseAdapterMock.fn
|
|
50
|
-
}));
|
|
51
|
-
describe('BridgeParent', () => {
|
|
52
|
-
let mockIframe;
|
|
53
|
-
beforeEach(() => {
|
|
54
|
-
vi.clearAllMocks();
|
|
55
|
-
vi.useFakeTimers();
|
|
56
|
-
mockIframe = {
|
|
57
|
-
contentWindow: {},
|
|
58
|
-
src: 'https://child.example.com/page'
|
|
59
|
-
};
|
|
60
|
-
});
|
|
61
|
-
afterEach(() => {
|
|
62
|
-
vi.useRealTimers();
|
|
63
|
-
vi.restoreAllMocks();
|
|
64
|
-
});
|
|
65
|
-
describe('constructor', () => {
|
|
66
|
-
it('should throw if no iframe provided', async () => {
|
|
67
|
-
const { BridgeParent } = await import('./BridgeParent');
|
|
68
|
-
expect(() => new BridgeParent(null)).toThrow('BridgeParent requires an iframe element');
|
|
69
|
-
});
|
|
70
|
-
it('should accept a valid iframe', async () => {
|
|
71
|
-
const { BridgeParent } = await import('./BridgeParent');
|
|
72
|
-
const bridge = new BridgeParent(mockIframe);
|
|
73
|
-
expect(bridge).toBeDefined();
|
|
74
|
-
});
|
|
75
|
-
it('should call Comlink.expose after initialization', async () => {
|
|
76
|
-
const Comlink = await import('comlink');
|
|
77
|
-
const { BridgeParent } = await import('./BridgeParent');
|
|
78
|
-
new BridgeParent(mockIframe);
|
|
79
|
-
// Allow the async initializeConnection to run
|
|
80
|
-
await vi.advanceTimersByTimeAsync(400);
|
|
81
|
-
expect(Comlink.expose).toHaveBeenCalled();
|
|
82
|
-
});
|
|
83
|
-
it('should call Comlink.windowEndpoint with iframe contentWindow', async () => {
|
|
84
|
-
const Comlink = await import('comlink');
|
|
85
|
-
const { BridgeParent } = await import('./BridgeParent');
|
|
86
|
-
new BridgeParent(mockIframe);
|
|
87
|
-
await vi.advanceTimersByTimeAsync(400);
|
|
88
|
-
expect(Comlink.windowEndpoint).toHaveBeenCalledWith(mockIframe.contentWindow, window, 'https://child.example.com');
|
|
89
|
-
});
|
|
90
|
-
it('should not initialize if iframe has no contentWindow', async () => {
|
|
91
|
-
const Comlink = await import('comlink');
|
|
92
|
-
const { BridgeParent } = await import('./BridgeParent');
|
|
93
|
-
const iframeNoContent = { src: 'https://example.com' };
|
|
94
|
-
new BridgeParent(iframeNoContent);
|
|
95
|
-
await vi.advanceTimersByTimeAsync(400);
|
|
96
|
-
expect(Comlink.expose).not.toHaveBeenCalled();
|
|
97
|
-
});
|
|
98
|
-
});
|
|
99
|
-
describe('inbound origin/source hardening', () => {
|
|
100
|
-
it('restricts comlink expose() to the iframe origin (not the "*" default)', async () => {
|
|
101
|
-
const Comlink = await import('comlink');
|
|
102
|
-
const { BridgeParent } = await import('./BridgeParent');
|
|
103
|
-
new BridgeParent(mockIframe);
|
|
104
|
-
await vi.advanceTimersByTimeAsync(200);
|
|
105
|
-
// mockIframe.src is https://child.example.com/page → origin only.
|
|
106
|
-
expect(Comlink.expose).toHaveBeenCalledWith(expect.anything(), expect.anything(), ['https://child.example.com']);
|
|
107
|
-
});
|
|
108
|
-
it('drops a message whose source is not the iframe window', async () => {
|
|
109
|
-
const Comlink = await import('comlink');
|
|
110
|
-
const { BridgeParent } = await import('./BridgeParent');
|
|
111
|
-
// Mirror how real comlink registers its endpoint listener.
|
|
112
|
-
const exposedListener = vi.fn();
|
|
113
|
-
vi.mocked(Comlink.expose).mockImplementationOnce((_api, endpoint) => {
|
|
114
|
-
const ep = endpoint;
|
|
115
|
-
ep.addEventListener('message', exposedListener);
|
|
116
|
-
});
|
|
117
|
-
new BridgeParent(mockIframe);
|
|
118
|
-
await vi.advanceTimersByTimeAsync(200);
|
|
119
|
-
// A foreign window (different reference than mockIframe.contentWindow).
|
|
120
|
-
const foreignWindow = {};
|
|
121
|
-
const evt = new MessageEvent('message', { data: { id: 1 } });
|
|
122
|
-
Object.defineProperty(evt, 'source', { value: foreignWindow });
|
|
123
|
-
window.dispatchEvent(evt);
|
|
124
|
-
expect(exposedListener).not.toHaveBeenCalled();
|
|
125
|
-
});
|
|
126
|
-
it('delivers a message that comes from the iframe window', async () => {
|
|
127
|
-
const Comlink = await import('comlink');
|
|
128
|
-
const { BridgeParent } = await import('./BridgeParent');
|
|
129
|
-
const exposedListener = vi.fn();
|
|
130
|
-
vi.mocked(Comlink.expose).mockImplementationOnce((_api, endpoint) => {
|
|
131
|
-
const ep = endpoint;
|
|
132
|
-
ep.addEventListener('message', exposedListener);
|
|
133
|
-
});
|
|
134
|
-
new BridgeParent(mockIframe);
|
|
135
|
-
await vi.advanceTimersByTimeAsync(200);
|
|
136
|
-
const evt = new MessageEvent('message', { data: { id: 1 } });
|
|
137
|
-
Object.defineProperty(evt, 'source', {
|
|
138
|
-
value: mockIframe.contentWindow
|
|
139
|
-
});
|
|
140
|
-
window.dispatchEvent(evt);
|
|
141
|
-
expect(exposedListener).toHaveBeenCalledTimes(1);
|
|
142
|
-
});
|
|
143
|
-
});
|
|
144
|
-
describe('destroy', () => {
|
|
145
|
-
it('detaches from window the listener expose() registered (no zombie across sessions)', async () => {
|
|
146
|
-
const Comlink = await import('comlink');
|
|
147
|
-
const { BridgeParent } = await import('./BridgeParent');
|
|
148
|
-
// Real Comlink registers its endpoint listener via
|
|
149
|
-
// ep.addEventListener('message', cb); simulate that so we can prove destroy()
|
|
150
|
-
// detaches it. windowEndpoint's add/removeEventListener are window's own
|
|
151
|
-
// methods (see the comlink mock), so this exercises real window attachment.
|
|
152
|
-
const exposedListener = vi.fn();
|
|
153
|
-
vi.mocked(Comlink.expose).mockImplementationOnce((_api, endpoint) => {
|
|
154
|
-
const ep = endpoint;
|
|
155
|
-
ep.addEventListener('message', exposedListener);
|
|
156
|
-
});
|
|
157
|
-
const bridge = new BridgeParent(mockIframe);
|
|
158
|
-
await vi.advanceTimersByTimeAsync(400);
|
|
159
|
-
// Live before teardown: a window 'message' from the iframe reaches the
|
|
160
|
-
// listener (source must match — the guard drops anything else).
|
|
161
|
-
const liveEvt = new MessageEvent('message', { data: { id: 1 } });
|
|
162
|
-
Object.defineProperty(liveEvt, 'source', {
|
|
163
|
-
value: mockIframe.contentWindow
|
|
164
|
-
});
|
|
165
|
-
window.dispatchEvent(liveEvt);
|
|
166
|
-
expect(exposedListener).toHaveBeenCalledTimes(1);
|
|
167
|
-
bridge.destroy();
|
|
168
|
-
// Gone after teardown: the zombie no longer answers.
|
|
169
|
-
const deadEvt = new MessageEvent('message', { data: { id: 2 } });
|
|
170
|
-
Object.defineProperty(deadEvt, 'source', {
|
|
171
|
-
value: mockIframe.contentWindow
|
|
172
|
-
});
|
|
173
|
-
window.dispatchEvent(deadEvt);
|
|
174
|
-
expect(exposedListener).toHaveBeenCalledTimes(1);
|
|
175
|
-
});
|
|
176
|
-
it('detaches the guarded wrapper when comlink removes its ORIGINAL listener (RELEASE teardown)', async () => {
|
|
177
|
-
const Comlink = await import('comlink');
|
|
178
|
-
const { BridgeParent } = await import('./BridgeParent');
|
|
179
|
-
// We register a source-guarded wrapper, not comlink's own callback. On a
|
|
180
|
-
// child RELEASE comlink calls removeEventListener('message', <original>),
|
|
181
|
-
// so removeEventListener must translate the original back to the wrapper —
|
|
182
|
-
// otherwise the wrapper stays attached as a zombie.
|
|
183
|
-
const exposedListener = vi.fn();
|
|
184
|
-
let ep;
|
|
185
|
-
vi.mocked(Comlink.expose).mockImplementationOnce((_api, endpoint) => {
|
|
186
|
-
ep = endpoint;
|
|
187
|
-
ep.addEventListener('message', exposedListener);
|
|
188
|
-
});
|
|
189
|
-
new BridgeParent(mockIframe);
|
|
190
|
-
await vi.advanceTimersByTimeAsync(200);
|
|
191
|
-
// comlink's RELEASE removal uses its ORIGINAL callback reference.
|
|
192
|
-
ep.removeEventListener('message', exposedListener);
|
|
193
|
-
const evt = new MessageEvent('message', { data: { id: 1 } });
|
|
194
|
-
Object.defineProperty(evt, 'source', { value: mockIframe.contentWindow });
|
|
195
|
-
window.dispatchEvent(evt);
|
|
196
|
-
expect(exposedListener).not.toHaveBeenCalled();
|
|
197
|
-
});
|
|
198
|
-
it('resets readiness flags so a stale read cannot report ready', async () => {
|
|
199
|
-
const Comlink = await import('comlink');
|
|
200
|
-
const { BridgeParent } = await import('./BridgeParent');
|
|
201
|
-
const bridge = new BridgeParent(mockIframe);
|
|
202
|
-
await vi.advanceTimersByTimeAsync(400);
|
|
203
|
-
const exposedAPI = vi.mocked(Comlink.expose).mock.calls[0]?.[0];
|
|
204
|
-
// Solana/Bitcoin readiness is true right after init — a real
|
|
205
|
-
// precondition, not a setup.
|
|
206
|
-
expect(exposedAPI.walletStandardWalletsReady).toBe(true);
|
|
207
|
-
expect(exposedAPI.bip122WalletsReady).toBe(true);
|
|
208
|
-
exposedAPI.tronWalletsReady = true;
|
|
209
|
-
exposedAPI.tonWalletsReady = true;
|
|
210
|
-
exposedAPI.eip6963WalletsReady = true;
|
|
211
|
-
bridge.destroy();
|
|
212
|
-
expect(exposedAPI.tronWalletsReady).toBe(false);
|
|
213
|
-
expect(exposedAPI.tonWalletsReady).toBe(false);
|
|
214
|
-
expect(exposedAPI.eip6963WalletsReady).toBe(false);
|
|
215
|
-
expect(exposedAPI.walletStandardWalletsReady).toBe(false);
|
|
216
|
-
expect(exposedAPI.bip122WalletsReady).toBe(false);
|
|
217
|
-
});
|
|
218
|
-
it('resets bip122Wallets to an empty array on destroy', async () => {
|
|
219
|
-
const { getWallets } = await import('@wallet-standard/app');
|
|
220
|
-
const Comlink = await import('comlink');
|
|
221
|
-
const { BridgeParent } = await import('./BridgeParent');
|
|
222
|
-
vi.mocked(getWallets).mockReturnValue({
|
|
223
|
-
get: () => [
|
|
224
|
-
{
|
|
225
|
-
name: 'MetaMask',
|
|
226
|
-
chains: ['bitcoin:mainnet'],
|
|
227
|
-
features: { 'bitcoin:connect': { connect: vi.fn() } },
|
|
228
|
-
accounts: []
|
|
229
|
-
}
|
|
230
|
-
],
|
|
231
|
-
on: vi.fn(),
|
|
232
|
-
register: vi.fn()
|
|
233
|
-
});
|
|
234
|
-
const bridge = new BridgeParent(mockIframe);
|
|
235
|
-
await vi.advanceTimersByTimeAsync(200);
|
|
236
|
-
const exposedAPI = vi.mocked(Comlink.expose).mock.calls[0]?.[0];
|
|
237
|
-
expect(exposedAPI.bip122Wallets).toHaveLength(1);
|
|
238
|
-
bridge.destroy();
|
|
239
|
-
expect(exposedAPI.bip122Wallets).toEqual([]);
|
|
240
|
-
});
|
|
241
|
-
it('is a safe no-op when the iframe never had a contentWindow', async () => {
|
|
242
|
-
const { BridgeParent } = await import('./BridgeParent');
|
|
243
|
-
const iframeNoContent = { src: 'https://child.example.com' };
|
|
244
|
-
const bridge = new BridgeParent(iframeNoContent);
|
|
245
|
-
await vi.advanceTimersByTimeAsync(400);
|
|
246
|
-
expect(() => bridge.destroy()).not.toThrow();
|
|
247
|
-
});
|
|
248
|
-
it('clears state and tolerates a second destroy()', async () => {
|
|
249
|
-
const { BridgeParent } = await import('./BridgeParent');
|
|
250
|
-
const bridge = new BridgeParent(mockIframe);
|
|
251
|
-
await vi.advanceTimersByTimeAsync(400);
|
|
252
|
-
bridge.destroy();
|
|
253
|
-
expect(() => bridge.destroy()).not.toThrow();
|
|
254
|
-
});
|
|
255
|
-
});
|
|
256
|
-
describe('EIP6963 wallet discovery', () => {
|
|
257
|
-
it('should dispatch eip6963:requestProvider event', async () => {
|
|
258
|
-
const dispatchSpy = vi.spyOn(window, 'dispatchEvent');
|
|
259
|
-
const { BridgeParent } = await import('./BridgeParent');
|
|
260
|
-
new BridgeParent(mockIframe);
|
|
261
|
-
await vi.advanceTimersByTimeAsync(400);
|
|
262
|
-
expect(dispatchSpy).toHaveBeenCalledWith(expect.objectContaining({ type: 'eip6963:requestProvider' }));
|
|
263
|
-
});
|
|
264
|
-
it('should collect announced wallets', async () => {
|
|
265
|
-
const Comlink = await import('comlink');
|
|
266
|
-
const { BridgeParent } = await import('./BridgeParent');
|
|
267
|
-
// Set up a listener that announces a wallet when requestProvider fires
|
|
268
|
-
const mockProvider = { request: vi.fn() };
|
|
269
|
-
window.addEventListener('eip6963:requestProvider', () => {
|
|
270
|
-
window.dispatchEvent(new CustomEvent('eip6963:announceProvider', {
|
|
271
|
-
detail: {
|
|
272
|
-
info: {
|
|
273
|
-
uuid: 'wallet-1',
|
|
274
|
-
name: 'TestWallet',
|
|
275
|
-
icon: 'test-icon',
|
|
276
|
-
rdns: 'com.test.wallet'
|
|
277
|
-
},
|
|
278
|
-
provider: mockProvider
|
|
279
|
-
}
|
|
280
|
-
}));
|
|
281
|
-
});
|
|
282
|
-
new BridgeParent(mockIframe);
|
|
283
|
-
await vi.advanceTimersByTimeAsync(400);
|
|
284
|
-
// The exposed parentAPI should contain the discovered wallet
|
|
285
|
-
const exposedAPI = vi.mocked(Comlink.expose).mock.calls[0]?.[0];
|
|
286
|
-
expect(exposedAPI).toBeDefined();
|
|
287
|
-
expect(exposedAPI.eip6963Wallets).toHaveLength(1);
|
|
288
|
-
expect(exposedAPI.eip6963Wallets[0].uuid).toBe('wallet-1');
|
|
289
|
-
expect(exposedAPI.eip6963Wallets[0].name).toBe('TestWallet');
|
|
290
|
-
expect(exposedAPI.eip6963WalletsReady).toBe(true);
|
|
291
|
-
});
|
|
292
|
-
it('should deduplicate wallets by uuid', async () => {
|
|
293
|
-
const Comlink = await import('comlink');
|
|
294
|
-
const { BridgeParent } = await import('./BridgeParent');
|
|
295
|
-
const mockProvider = { request: vi.fn() };
|
|
296
|
-
window.addEventListener('eip6963:requestProvider', () => {
|
|
297
|
-
// Announce same wallet twice
|
|
298
|
-
const event = new CustomEvent('eip6963:announceProvider', {
|
|
299
|
-
detail: {
|
|
300
|
-
info: {
|
|
301
|
-
uuid: 'wallet-1',
|
|
302
|
-
name: 'TestWallet',
|
|
303
|
-
icon: 'icon',
|
|
304
|
-
rdns: 'com.test'
|
|
305
|
-
},
|
|
306
|
-
provider: mockProvider
|
|
307
|
-
}
|
|
308
|
-
});
|
|
309
|
-
window.dispatchEvent(event);
|
|
310
|
-
window.dispatchEvent(event);
|
|
311
|
-
});
|
|
312
|
-
new BridgeParent(mockIframe);
|
|
313
|
-
await vi.advanceTimersByTimeAsync(400);
|
|
314
|
-
const exposedAPI = vi.mocked(Comlink.expose).mock.calls[0]?.[0];
|
|
315
|
-
expect(exposedAPI.eip6963Wallets).toHaveLength(1);
|
|
316
|
-
});
|
|
317
|
-
});
|
|
318
|
-
describe('Solana wallet discovery', () => {
|
|
319
|
-
it('should call getWallets and filter Solana wallets', async () => {
|
|
320
|
-
const { getWallets } = await import('@wallet-standard/app');
|
|
321
|
-
const Comlink = await import('comlink');
|
|
322
|
-
const { BridgeParent } = await import('./BridgeParent');
|
|
323
|
-
const mockSolanaWallet = {
|
|
324
|
-
name: 'Phantom',
|
|
325
|
-
chains: ['solana:mainnet'],
|
|
326
|
-
features: { 'standard:connect': {} },
|
|
327
|
-
accounts: []
|
|
328
|
-
};
|
|
329
|
-
const mockNonSolanaWallet = {
|
|
330
|
-
name: 'SomeWallet',
|
|
331
|
-
chains: ['ethereum:1'],
|
|
332
|
-
features: {},
|
|
333
|
-
accounts: []
|
|
334
|
-
};
|
|
335
|
-
vi.mocked(getWallets).mockReturnValue({
|
|
336
|
-
get: () => [mockSolanaWallet, mockNonSolanaWallet],
|
|
337
|
-
on: vi.fn(),
|
|
338
|
-
register: vi.fn()
|
|
339
|
-
});
|
|
340
|
-
new BridgeParent(mockIframe);
|
|
341
|
-
await vi.advanceTimersByTimeAsync(400);
|
|
342
|
-
const exposedAPI = vi.mocked(Comlink.expose).mock.calls[0]?.[0];
|
|
343
|
-
expect(exposedAPI.walletStandardWallets).toHaveLength(1);
|
|
344
|
-
expect(exposedAPI.walletStandardWallets[0].name).toBe('Phantom');
|
|
345
|
-
expect(exposedAPI.walletStandardWalletsReady).toBe(true);
|
|
346
|
-
});
|
|
347
|
-
it('should return empty array when no Solana wallets found', async () => {
|
|
348
|
-
const { getWallets } = await import('@wallet-standard/app');
|
|
349
|
-
const Comlink = await import('comlink');
|
|
350
|
-
const { BridgeParent } = await import('./BridgeParent');
|
|
351
|
-
vi.mocked(getWallets).mockReturnValue({
|
|
352
|
-
get: () => [],
|
|
353
|
-
on: vi.fn(),
|
|
354
|
-
register: vi.fn()
|
|
355
|
-
});
|
|
356
|
-
new BridgeParent(mockIframe);
|
|
357
|
-
await vi.advanceTimersByTimeAsync(400);
|
|
358
|
-
const exposedAPI = vi.mocked(Comlink.expose).mock.calls[0]?.[0];
|
|
359
|
-
expect(exposedAPI.walletStandardWallets).toEqual([]);
|
|
360
|
-
});
|
|
361
|
-
});
|
|
362
|
-
describe('Bitcoin wallet discovery', () => {
|
|
363
|
-
it('should call getWallets and filter bitcoin:* wallets', async () => {
|
|
364
|
-
const { getWallets } = await import('@wallet-standard/app');
|
|
365
|
-
const Comlink = await import('comlink');
|
|
366
|
-
const { BridgeParent } = await import('./BridgeParent');
|
|
367
|
-
const mockBitcoinWallet = {
|
|
368
|
-
name: 'MetaMask',
|
|
369
|
-
chains: ['bitcoin:mainnet'],
|
|
370
|
-
features: {
|
|
371
|
-
'bitcoin:connect': { connect: vi.fn() },
|
|
372
|
-
'bitcoin:signMessage': { signMessage: vi.fn() },
|
|
373
|
-
'bitcoin:signTransaction': { signTransaction: vi.fn() }
|
|
374
|
-
},
|
|
375
|
-
accounts: []
|
|
376
|
-
};
|
|
377
|
-
const mockNonBitcoinWallet = {
|
|
378
|
-
name: 'SomeWallet',
|
|
379
|
-
chains: ['ethereum:1'],
|
|
380
|
-
features: {},
|
|
381
|
-
accounts: []
|
|
382
|
-
};
|
|
383
|
-
vi.mocked(getWallets).mockReturnValue({
|
|
384
|
-
get: () => [mockBitcoinWallet, mockNonBitcoinWallet],
|
|
385
|
-
on: vi.fn(),
|
|
386
|
-
register: vi.fn()
|
|
387
|
-
});
|
|
388
|
-
new BridgeParent(mockIframe);
|
|
389
|
-
await vi.advanceTimersByTimeAsync(200);
|
|
390
|
-
const exposedAPI = vi.mocked(Comlink.expose).mock.calls[0]?.[0];
|
|
391
|
-
expect(exposedAPI.bip122Wallets).toHaveLength(1);
|
|
392
|
-
expect(exposedAPI.bip122Wallets[0].name).toBe('MetaMask');
|
|
393
|
-
expect(exposedAPI.bip122Wallets[0].features).toEqual([
|
|
394
|
-
'bitcoin:connect',
|
|
395
|
-
'bitcoin:signMessage',
|
|
396
|
-
'bitcoin:signTransaction'
|
|
397
|
-
]);
|
|
398
|
-
expect(exposedAPI.bip122Wallets[0].wallet.accounts).toEqual([]);
|
|
399
|
-
expect(exposedAPI.bip122Wallets[0].wallet.features['bitcoin:connect']).toBeDefined();
|
|
400
|
-
expect(exposedAPI.bip122WalletsReady).toBe(true);
|
|
401
|
-
});
|
|
402
|
-
it('keys the uuid on the bitcoin: chain, not chains[0], for a multi-chain wallet', async () => {
|
|
403
|
-
const { getWallets } = await import('@wallet-standard/app');
|
|
404
|
-
const Comlink = await import('comlink');
|
|
405
|
-
const { BridgeParent } = await import('./BridgeParent');
|
|
406
|
-
// MetaMask-shaped: eip155 chains registered first, bitcoin: chain last.
|
|
407
|
-
const mockMultiChainWallet = {
|
|
408
|
-
name: 'MetaMask',
|
|
409
|
-
chains: ['eip155:1', 'eip155:137', 'bitcoin:mainnet'],
|
|
410
|
-
features: {
|
|
411
|
-
'bitcoin:connect': { connect: vi.fn() },
|
|
412
|
-
'bitcoin:signMessage': { signMessage: vi.fn() },
|
|
413
|
-
'bitcoin:signTransaction': { signTransaction: vi.fn() }
|
|
414
|
-
},
|
|
415
|
-
accounts: []
|
|
416
|
-
};
|
|
417
|
-
vi.mocked(getWallets).mockReturnValue({
|
|
418
|
-
get: () => [mockMultiChainWallet],
|
|
419
|
-
on: vi.fn(),
|
|
420
|
-
register: vi.fn()
|
|
421
|
-
});
|
|
422
|
-
new BridgeParent(mockIframe);
|
|
423
|
-
await vi.advanceTimersByTimeAsync(200);
|
|
424
|
-
const exposedAPI = vi.mocked(Comlink.expose).mock.calls[0]?.[0];
|
|
425
|
-
expect(exposedAPI.bip122Wallets).toHaveLength(1);
|
|
426
|
-
// Must key on 'bitcoin:mainnet' (bitcoinChain), not 'eip155:1'
|
|
427
|
-
// (chains[0]) — otherwise this uuid disagrees with the local
|
|
428
|
-
// (unframed) discovery path's uuid for the exact same wallet.
|
|
429
|
-
expect(exposedAPI.bip122Wallets[0].uuid).toBe('metamask-bitcoin:mainnet');
|
|
430
|
-
});
|
|
431
|
-
it('should return empty array when no bitcoin wallets found', async () => {
|
|
432
|
-
const { getWallets } = await import('@wallet-standard/app');
|
|
433
|
-
const Comlink = await import('comlink');
|
|
434
|
-
const { BridgeParent } = await import('./BridgeParent');
|
|
435
|
-
vi.mocked(getWallets).mockReturnValue({
|
|
436
|
-
get: () => [],
|
|
437
|
-
on: vi.fn(),
|
|
438
|
-
register: vi.fn()
|
|
439
|
-
});
|
|
440
|
-
new BridgeParent(mockIframe);
|
|
441
|
-
await vi.advanceTimersByTimeAsync(200);
|
|
442
|
-
const exposedAPI = vi.mocked(Comlink.expose).mock.calls[0]?.[0];
|
|
443
|
-
expect(exposedAPI.bip122Wallets).toEqual([]);
|
|
444
|
-
});
|
|
445
|
-
it('omits a feature entry the wallet does not declare', async () => {
|
|
446
|
-
const { getWallets } = await import('@wallet-standard/app');
|
|
447
|
-
const Comlink = await import('comlink');
|
|
448
|
-
const { BridgeParent } = await import('./BridgeParent');
|
|
449
|
-
const mockWallet = {
|
|
450
|
-
name: 'PartialWallet',
|
|
451
|
-
chains: ['bitcoin:mainnet'],
|
|
452
|
-
features: { 'bitcoin:connect': { connect: vi.fn() } },
|
|
453
|
-
accounts: []
|
|
454
|
-
};
|
|
455
|
-
vi.mocked(getWallets).mockReturnValue({
|
|
456
|
-
get: () => [mockWallet],
|
|
457
|
-
on: vi.fn(),
|
|
458
|
-
register: vi.fn()
|
|
459
|
-
});
|
|
460
|
-
new BridgeParent(mockIframe);
|
|
461
|
-
await vi.advanceTimersByTimeAsync(200);
|
|
462
|
-
const exposedAPI = vi.mocked(Comlink.expose).mock.calls[0]?.[0];
|
|
463
|
-
const wallet = exposedAPI.bip122Wallets[0].wallet;
|
|
464
|
-
expect(wallet.features['bitcoin:connect']).toBeDefined();
|
|
465
|
-
expect(wallet.features['bitcoin:signMessage']).toBeUndefined();
|
|
466
|
-
expect(wallet.features['bitcoin:signTransaction']).toBeUndefined();
|
|
467
|
-
});
|
|
468
|
-
});
|
|
469
|
-
describe('subscribeBip122WalletEvent', () => {
|
|
470
|
-
// Bitcoin has no adapter class to construct (unlike Solana's
|
|
471
|
-
// StandardWalletAdapter) — `getBitcoinWallets()` reshapes the raw
|
|
472
|
-
// registry wallet directly, so the resolver just re-scans the registry
|
|
473
|
-
// for a `bitcoin:*` wallet whose uuid matches. `standard:events` is the
|
|
474
|
-
// ONLY path (no connect/disconnect fallback), since there is no adapter
|
|
475
|
-
// to re-emit them.
|
|
476
|
-
async function armStandardEventsBitcoinWallet(initialAccounts, chains = ['bitcoin:mainnet']) {
|
|
477
|
-
const { getWallets } = await import('@wallet-standard/app');
|
|
478
|
-
let changeListener;
|
|
479
|
-
const off = vi.fn();
|
|
480
|
-
const wallet = {
|
|
481
|
-
name: 'MetaMask',
|
|
482
|
-
chains,
|
|
483
|
-
accounts: initialAccounts,
|
|
484
|
-
features: {
|
|
485
|
-
'bitcoin:connect': { connect: vi.fn() },
|
|
486
|
-
'standard:events': {
|
|
487
|
-
on: vi.fn((event, listener) => {
|
|
488
|
-
if (event === 'change')
|
|
489
|
-
changeListener = listener;
|
|
490
|
-
return off;
|
|
491
|
-
})
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
};
|
|
495
|
-
vi.mocked(getWallets).mockReturnValue({
|
|
496
|
-
get: () => [wallet],
|
|
497
|
-
on: vi.fn(),
|
|
498
|
-
register: vi.fn()
|
|
499
|
-
});
|
|
500
|
-
const { BridgeParent } = await import('./BridgeParent');
|
|
501
|
-
const bridgeParent = new BridgeParent(mockIframe);
|
|
502
|
-
await vi.advanceTimersByTimeAsync(400);
|
|
503
|
-
const callback = vi.fn();
|
|
504
|
-
const walletId = `metamask-${chains.find(c => c.startsWith('bitcoin:')) ?? chains[0]}`;
|
|
505
|
-
const teardown = bridgeParent.parentAPI.subscribeBip122WalletEvent(walletId, callback);
|
|
506
|
-
return {
|
|
507
|
-
wallet,
|
|
508
|
-
off,
|
|
509
|
-
callback,
|
|
510
|
-
teardown,
|
|
511
|
-
fire: (props) => changeListener?.(props)
|
|
512
|
-
};
|
|
513
|
-
}
|
|
514
|
-
it('throws when no wallet matches the given walletId', async () => {
|
|
515
|
-
const { getWallets } = await import('@wallet-standard/app');
|
|
516
|
-
vi.mocked(getWallets).mockReturnValue({
|
|
517
|
-
get: () => [],
|
|
518
|
-
on: vi.fn(),
|
|
519
|
-
register: vi.fn()
|
|
520
|
-
});
|
|
521
|
-
const { BridgeParent } = await import('./BridgeParent');
|
|
522
|
-
const bridgeParent = new BridgeParent(mockIframe);
|
|
523
|
-
await vi.advanceTimersByTimeAsync(400);
|
|
524
|
-
expect(() => bridgeParent.parentAPI.subscribeBip122WalletEvent('unknown-wallet-id', vi.fn())).toThrow(/no wallet found for walletId/);
|
|
525
|
-
});
|
|
526
|
-
it('returns undefined when the resolved wallet exposes no standard:events', async () => {
|
|
527
|
-
const { getWallets } = await import('@wallet-standard/app');
|
|
528
|
-
vi.mocked(getWallets).mockReturnValue({
|
|
529
|
-
get: () => [
|
|
530
|
-
{
|
|
531
|
-
name: 'MetaMask',
|
|
532
|
-
chains: ['bitcoin:mainnet'],
|
|
533
|
-
accounts: [],
|
|
534
|
-
features: { 'bitcoin:connect': { connect: vi.fn() } }
|
|
535
|
-
}
|
|
536
|
-
],
|
|
537
|
-
on: vi.fn(),
|
|
538
|
-
register: vi.fn()
|
|
539
|
-
});
|
|
540
|
-
const { BridgeParent } = await import('./BridgeParent');
|
|
541
|
-
const bridgeParent = new BridgeParent(mockIframe);
|
|
542
|
-
await vi.advanceTimersByTimeAsync(400);
|
|
543
|
-
const teardown = bridgeParent.parentAPI.subscribeBip122WalletEvent('metamask-bitcoin:mainnet', vi.fn());
|
|
544
|
-
expect(teardown).toBeUndefined();
|
|
545
|
-
});
|
|
546
|
-
it('resolves a multi-chain wallet (eip155 first) by its bitcoin:-keyed walletId, not chains[0]', async () => {
|
|
547
|
-
const { teardown } = await armStandardEventsBitcoinWallet([{ address: 'bc1qexisting' }], ['eip155:1', 'eip155:137', 'bitcoin:mainnet']);
|
|
548
|
-
expect(typeof teardown).toBe('function');
|
|
549
|
-
});
|
|
550
|
-
it('subscribes to standard:events change and forwards the active address as a plain string', async () => {
|
|
551
|
-
const { wallet, teardown, off } = await armStandardEventsBitcoinWallet([
|
|
552
|
-
{ address: 'bc1qoriginal' }
|
|
553
|
-
]);
|
|
554
|
-
expect(wallet.features['standard:events'].on).toHaveBeenCalledWith('change', expect.any(Function));
|
|
555
|
-
expect(typeof teardown).toBe('function');
|
|
556
|
-
expect(Comlink.proxy).toHaveBeenCalledWith(expect.any(Function));
|
|
557
|
-
expect(off).not.toHaveBeenCalled();
|
|
558
|
-
});
|
|
559
|
-
it('propagates a switch, then null on de-auth, and ignores non-account changes', async () => {
|
|
560
|
-
const { wallet, callback, off, teardown, fire } = await armStandardEventsBitcoinWallet([{ address: 'bc1qACC1' }]);
|
|
561
|
-
wallet.accounts = [{ address: 'bc1qACC2' }];
|
|
562
|
-
fire({ accounts: wallet.accounts });
|
|
563
|
-
expect(callback).toHaveBeenNthCalledWith(1, 'bc1qACC2');
|
|
564
|
-
// A pure chain/feature change (no `accounts` key) must be ignored.
|
|
565
|
-
fire({ chains: ['bitcoin:mainnet'] });
|
|
566
|
-
expect(callback).toHaveBeenCalledTimes(1);
|
|
567
|
-
wallet.accounts = [];
|
|
568
|
-
fire({ accounts: [] });
|
|
569
|
-
expect(callback).toHaveBeenNthCalledWith(2, null);
|
|
570
|
-
teardown?.();
|
|
571
|
-
expect(off).toHaveBeenCalledTimes(1);
|
|
572
|
-
});
|
|
573
|
-
it('never lets the wallet-standard unsubscribe return value escape the teardown (unserializable-return-value guard)', async () => {
|
|
574
|
-
const { off, teardown } = await armStandardEventsBitcoinWallet([
|
|
575
|
-
{ address: 'bc1qACC1' }
|
|
576
|
-
]);
|
|
577
|
-
off.mockReturnValue({ some: 'non-cloneable-emitter-return' });
|
|
578
|
-
expect(() => teardown?.()).not.toThrow();
|
|
579
|
-
});
|
|
580
|
-
});
|
|
581
|
-
describe('Tron wallet discovery', () => {
|
|
582
|
-
it('should start with empty tron wallets and not ready', async () => {
|
|
583
|
-
const Comlink = await import('comlink');
|
|
584
|
-
const { BridgeParent } = await import('./BridgeParent');
|
|
585
|
-
new BridgeParent(mockIframe);
|
|
586
|
-
await vi.advanceTimersByTimeAsync(400);
|
|
587
|
-
const exposedAPI = vi.mocked(Comlink.expose).mock.calls[0]?.[0];
|
|
588
|
-
expect(exposedAPI.tronWallets).toEqual([]);
|
|
589
|
-
expect(exposedAPI.tronWalletsReady).toBe(false);
|
|
590
|
-
});
|
|
591
|
-
it('should discover tron wallets when discoverTronWallets is called', async () => {
|
|
592
|
-
const Comlink = await import('comlink');
|
|
593
|
-
const { BridgeParent } = await import('./BridgeParent');
|
|
594
|
-
window.tronLink = {
|
|
595
|
-
ready: true,
|
|
596
|
-
request: vi.fn(),
|
|
597
|
-
tronWeb: {
|
|
598
|
-
defaultAddress: { base58: 'TAddr1', hex: '0x1' },
|
|
599
|
-
trx: {
|
|
600
|
-
sign: vi.fn(),
|
|
601
|
-
signMessageV2: vi.fn(),
|
|
602
|
-
sendRawTransaction: vi.fn()
|
|
603
|
-
},
|
|
604
|
-
transactionBuilder: {
|
|
605
|
-
sendTrx: vi.fn(),
|
|
606
|
-
triggerSmartContract: vi.fn()
|
|
607
|
-
},
|
|
608
|
-
toHex: vi.fn()
|
|
609
|
-
}
|
|
610
|
-
};
|
|
611
|
-
new BridgeParent(mockIframe);
|
|
612
|
-
await vi.advanceTimersByTimeAsync(400);
|
|
613
|
-
const exposedAPI = vi.mocked(Comlink.expose).mock.calls[0]?.[0];
|
|
614
|
-
// Simulate child calling discoverTronWallets
|
|
615
|
-
exposedAPI.discoverTronWallets(['tronLink']);
|
|
616
|
-
expect(exposedAPI.tronWalletsReady).toBe(true);
|
|
617
|
-
expect(exposedAPI.tronWallets).toHaveLength(1);
|
|
618
|
-
expect(exposedAPI.tronWallets[0].uuid).toBe('tron-tronlink');
|
|
619
|
-
expect(exposedAPI.tronWallets[0].name).toBe('tronLink');
|
|
620
|
-
expect(exposedAPI.tronWallets[0].injectedId).toBe('tronLink');
|
|
621
|
-
// Cleanup
|
|
622
|
-
delete window.tronLink;
|
|
623
|
-
});
|
|
624
|
-
it('should handle nested tron provider paths', async () => {
|
|
625
|
-
const Comlink = await import('comlink');
|
|
626
|
-
const { BridgeParent } = await import('./BridgeParent');
|
|
627
|
-
window.tokenpocket = {
|
|
628
|
-
tron: {
|
|
629
|
-
ready: true,
|
|
630
|
-
request: vi.fn(),
|
|
631
|
-
tronWeb: {
|
|
632
|
-
defaultAddress: { base58: 'TAddr2' },
|
|
633
|
-
trx: {
|
|
634
|
-
sign: vi.fn(),
|
|
635
|
-
signMessageV2: vi.fn(),
|
|
636
|
-
sendRawTransaction: vi.fn()
|
|
637
|
-
},
|
|
638
|
-
transactionBuilder: {
|
|
639
|
-
sendTrx: vi.fn(),
|
|
640
|
-
triggerSmartContract: vi.fn()
|
|
641
|
-
},
|
|
642
|
-
toHex: vi.fn()
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
};
|
|
646
|
-
new BridgeParent(mockIframe);
|
|
647
|
-
await vi.advanceTimersByTimeAsync(400);
|
|
648
|
-
const exposedAPI = vi.mocked(Comlink.expose).mock.calls[0]?.[0];
|
|
649
|
-
exposedAPI.discoverTronWallets(['tokenpocket.tron']);
|
|
650
|
-
expect(exposedAPI.tronWallets).toHaveLength(1);
|
|
651
|
-
expect(exposedAPI.tronWallets[0].uuid).toBe('tron-tokenpocket-tron');
|
|
652
|
-
delete window.tokenpocket;
|
|
653
|
-
});
|
|
654
|
-
it('should return empty when injectedIds is empty', async () => {
|
|
655
|
-
const Comlink = await import('comlink');
|
|
656
|
-
const { BridgeParent } = await import('./BridgeParent');
|
|
657
|
-
new BridgeParent(mockIframe);
|
|
658
|
-
await vi.advanceTimersByTimeAsync(400);
|
|
659
|
-
const exposedAPI = vi.mocked(Comlink.expose).mock.calls[0]?.[0];
|
|
660
|
-
exposedAPI.discoverTronWallets([]);
|
|
661
|
-
expect(exposedAPI.tronWallets).toEqual([]);
|
|
662
|
-
expect(exposedAPI.tronWalletsReady).toBe(true);
|
|
663
|
-
});
|
|
664
|
-
it('should skip providers without valid tronWeb.trx', async () => {
|
|
665
|
-
const Comlink = await import('comlink');
|
|
666
|
-
const { BridgeParent } = await import('./BridgeParent');
|
|
667
|
-
window.badWallet = {
|
|
668
|
-
ready: true,
|
|
669
|
-
tronWeb: { notTrx: true }
|
|
670
|
-
};
|
|
671
|
-
new BridgeParent(mockIframe);
|
|
672
|
-
await vi.advanceTimersByTimeAsync(400);
|
|
673
|
-
const exposedAPI = vi.mocked(Comlink.expose).mock.calls[0]?.[0];
|
|
674
|
-
exposedAPI.discoverTronWallets(['badWallet']);
|
|
675
|
-
expect(exposedAPI.tronWallets).toEqual([]);
|
|
676
|
-
delete window.badWallet;
|
|
677
|
-
});
|
|
678
|
-
it('should deduplicate tron wallets by injectedId', async () => {
|
|
679
|
-
const Comlink = await import('comlink');
|
|
680
|
-
const { BridgeParent } = await import('./BridgeParent');
|
|
681
|
-
window.tronLink = {
|
|
682
|
-
ready: true,
|
|
683
|
-
request: vi.fn(),
|
|
684
|
-
tronWeb: {
|
|
685
|
-
defaultAddress: { base58: 'TAddr1' },
|
|
686
|
-
trx: {
|
|
687
|
-
sign: vi.fn(),
|
|
688
|
-
signMessageV2: vi.fn(),
|
|
689
|
-
sendRawTransaction: vi.fn()
|
|
690
|
-
},
|
|
691
|
-
transactionBuilder: {
|
|
692
|
-
sendTrx: vi.fn(),
|
|
693
|
-
triggerSmartContract: vi.fn()
|
|
694
|
-
},
|
|
695
|
-
toHex: vi.fn()
|
|
696
|
-
}
|
|
697
|
-
};
|
|
698
|
-
new BridgeParent(mockIframe);
|
|
699
|
-
await vi.advanceTimersByTimeAsync(400);
|
|
700
|
-
const exposedAPI = vi.mocked(Comlink.expose).mock.calls[0]?.[0];
|
|
701
|
-
exposedAPI.discoverTronWallets(['tronLink', 'tronLink']);
|
|
702
|
-
expect(exposedAPI.tronWallets).toHaveLength(1);
|
|
703
|
-
delete window.tronLink;
|
|
704
|
-
});
|
|
705
|
-
});
|
|
706
|
-
describe('TON wallet discovery', () => {
|
|
707
|
-
it('should start with empty ton wallets and not ready', async () => {
|
|
708
|
-
const Comlink = await import('comlink');
|
|
709
|
-
const { BridgeParent } = await import('./BridgeParent');
|
|
710
|
-
new BridgeParent(mockIframe);
|
|
711
|
-
await vi.advanceTimersByTimeAsync(400);
|
|
712
|
-
const exposedAPI = vi.mocked(Comlink.expose).mock.calls[0]?.[0];
|
|
713
|
-
expect(exposedAPI.tonWallets).toEqual([]);
|
|
714
|
-
expect(exposedAPI.tonWalletsReady).toBe(false);
|
|
715
|
-
});
|
|
716
|
-
it('should discover TON wallets when discoverTonWallets is called', async () => {
|
|
717
|
-
const Comlink = await import('comlink');
|
|
718
|
-
const { BridgeParent } = await import('./BridgeParent');
|
|
719
|
-
window.trustwalletTon = {
|
|
720
|
-
tonconnect: {
|
|
721
|
-
deviceInfo: {
|
|
722
|
-
platform: 'chrome',
|
|
723
|
-
appName: 'Trust Wallet',
|
|
724
|
-
appVersion: '1.0',
|
|
725
|
-
maxProtocolVersion: 2,
|
|
726
|
-
features: []
|
|
727
|
-
},
|
|
728
|
-
walletInfo: {
|
|
729
|
-
name: 'Trust Wallet',
|
|
730
|
-
app_name: 'trustwallet',
|
|
731
|
-
image: 'https://example.com/icon.png',
|
|
732
|
-
about_url: 'https://example.com',
|
|
733
|
-
tondns: '',
|
|
734
|
-
platforms: ['chrome'],
|
|
735
|
-
features: []
|
|
736
|
-
},
|
|
737
|
-
protocolVersion: 2,
|
|
738
|
-
isWalletBrowser: false,
|
|
739
|
-
connect: vi.fn(),
|
|
740
|
-
restoreConnection: vi.fn(),
|
|
741
|
-
send: vi.fn(),
|
|
742
|
-
listen: vi.fn(() => vi.fn()),
|
|
743
|
-
disconnect: vi.fn()
|
|
744
|
-
}
|
|
745
|
-
};
|
|
746
|
-
new BridgeParent(mockIframe);
|
|
747
|
-
await vi.advanceTimersByTimeAsync(400);
|
|
748
|
-
const exposedAPI = vi.mocked(Comlink.expose).mock.calls[0]?.[0];
|
|
749
|
-
exposedAPI.discoverTonWallets(['trustwalletTon']);
|
|
750
|
-
expect(exposedAPI.tonWalletsReady).toBe(true);
|
|
751
|
-
expect(exposedAPI.tonWallets).toHaveLength(1);
|
|
752
|
-
expect(exposedAPI.tonWallets[0].jsBridgeKey).toBe('trustwalletTon');
|
|
753
|
-
expect(exposedAPI.tonWallets[0].uuid).toBe('ton-trustwalletton');
|
|
754
|
-
delete window.trustwalletTon;
|
|
755
|
-
});
|
|
756
|
-
it('should proxy walletInfo for isJSBridgeWithMetadata compatibility', async () => {
|
|
757
|
-
const Comlink = await import('comlink');
|
|
758
|
-
const { BridgeParent } = await import('./BridgeParent');
|
|
759
|
-
window.trustwalletTon = {
|
|
760
|
-
tonconnect: {
|
|
761
|
-
deviceInfo: {
|
|
762
|
-
platform: 'chrome',
|
|
763
|
-
appName: 'Trust Wallet',
|
|
764
|
-
appVersion: '1.0',
|
|
765
|
-
maxProtocolVersion: 2,
|
|
766
|
-
features: []
|
|
767
|
-
},
|
|
768
|
-
walletInfo: {
|
|
769
|
-
name: 'Trust Wallet',
|
|
770
|
-
app_name: 'trustwallet',
|
|
771
|
-
image: 'https://icon.png',
|
|
772
|
-
about_url: 'https://tw.com',
|
|
773
|
-
tondns: '',
|
|
774
|
-
platforms: ['chrome'],
|
|
775
|
-
features: []
|
|
776
|
-
},
|
|
777
|
-
protocolVersion: 2,
|
|
778
|
-
isWalletBrowser: false,
|
|
779
|
-
connect: vi.fn(),
|
|
780
|
-
restoreConnection: vi.fn(),
|
|
781
|
-
send: vi.fn(),
|
|
782
|
-
listen: vi.fn(() => vi.fn()),
|
|
783
|
-
disconnect: vi.fn()
|
|
784
|
-
}
|
|
785
|
-
};
|
|
786
|
-
new BridgeParent(mockIframe);
|
|
787
|
-
await vi.advanceTimersByTimeAsync(400);
|
|
788
|
-
const exposedAPI = vi.mocked(Comlink.expose).mock.calls[0]?.[0];
|
|
789
|
-
exposedAPI.discoverTonWallets(['trustwalletTon']);
|
|
790
|
-
const provider = exposedAPI.tonWallets[0].provider;
|
|
791
|
-
// Comlink.proxy mock returns the object as-is
|
|
792
|
-
expect(provider.walletInfo).toBeDefined();
|
|
793
|
-
expect(provider.walletInfo.name).toBe('Trust Wallet');
|
|
794
|
-
expect(provider.walletInfo.app_name).toBe('trustwallet');
|
|
795
|
-
expect(provider.walletInfo.platforms).toEqual(['chrome']);
|
|
796
|
-
expect(provider.protocolVersion).toBe(2);
|
|
797
|
-
expect(provider.isWalletBrowser).toBe(false);
|
|
798
|
-
expect(typeof provider.restoreConnection).toBe('function');
|
|
799
|
-
delete window.trustwalletTon;
|
|
800
|
-
});
|
|
801
|
-
it('should skip wallets without valid tonconnect bridge', async () => {
|
|
802
|
-
const Comlink = await import('comlink');
|
|
803
|
-
const { BridgeParent } = await import('./BridgeParent');
|
|
804
|
-
window.fakeTon = { tonconnect: { notABridge: true } };
|
|
805
|
-
new BridgeParent(mockIframe);
|
|
806
|
-
await vi.advanceTimersByTimeAsync(400);
|
|
807
|
-
const exposedAPI = vi.mocked(Comlink.expose).mock.calls[0]?.[0];
|
|
808
|
-
exposedAPI.discoverTonWallets(['fakeTon']);
|
|
809
|
-
expect(exposedAPI.tonWallets).toEqual([]);
|
|
810
|
-
delete window.fakeTon;
|
|
811
|
-
});
|
|
812
|
-
it('should deduplicate TON wallets by jsBridgeKey', async () => {
|
|
813
|
-
const Comlink = await import('comlink');
|
|
814
|
-
const { BridgeParent } = await import('./BridgeParent');
|
|
815
|
-
window.trustwalletTon = {
|
|
816
|
-
tonconnect: {
|
|
817
|
-
connect: vi.fn(),
|
|
818
|
-
send: vi.fn(),
|
|
819
|
-
listen: vi.fn(() => vi.fn()),
|
|
820
|
-
deviceInfo: { appName: 'Trust Wallet' }
|
|
821
|
-
}
|
|
822
|
-
};
|
|
823
|
-
new BridgeParent(mockIframe);
|
|
824
|
-
await vi.advanceTimersByTimeAsync(400);
|
|
825
|
-
const exposedAPI = vi.mocked(Comlink.expose).mock.calls[0]?.[0];
|
|
826
|
-
exposedAPI.discoverTonWallets(['trustwalletTon', 'trustwalletTon']);
|
|
827
|
-
expect(exposedAPI.tonWallets).toHaveLength(1);
|
|
828
|
-
delete window.trustwalletTon;
|
|
829
|
-
});
|
|
830
|
-
});
|
|
831
|
-
describe('Base Wallet detection', () => {
|
|
832
|
-
beforeEach(async () => {
|
|
833
|
-
baseAdapterMock.readyState = 'NotDetected';
|
|
834
|
-
baseAdapterMock.shouldThrow = false;
|
|
835
|
-
// Re-set mocks that vi.restoreAllMocks() may have cleared
|
|
836
|
-
const { getWallets } = await import('@wallet-standard/app');
|
|
837
|
-
vi.mocked(getWallets).mockReturnValue({
|
|
838
|
-
get: () => [],
|
|
839
|
-
on: vi.fn(),
|
|
840
|
-
register: vi.fn()
|
|
841
|
-
});
|
|
842
|
-
// Re-set BaseWalletAdapter mock - must use 'function' (not arrow) for 'new' compatibility
|
|
843
|
-
baseAdapterMock.fn.mockImplementation(function () {
|
|
844
|
-
if (baseAdapterMock.shouldThrow)
|
|
845
|
-
throw new Error('init failed');
|
|
846
|
-
return {
|
|
847
|
-
readyState: baseAdapterMock.readyState,
|
|
848
|
-
name: 'Base Wallet',
|
|
849
|
-
icon: 'data:image/svg+xml;base64,test',
|
|
850
|
-
url: 'https://example.com',
|
|
851
|
-
publicKey: null,
|
|
852
|
-
connecting: false,
|
|
853
|
-
connected: false,
|
|
854
|
-
supportedTransactionVersions: new Set(['legacy', 0]),
|
|
855
|
-
connect: vi.fn(),
|
|
856
|
-
disconnect: vi.fn(),
|
|
857
|
-
sendTransaction: vi.fn(),
|
|
858
|
-
signTransaction: vi.fn(),
|
|
859
|
-
signAllTransactions: vi.fn(),
|
|
860
|
-
signMessage: vi.fn()
|
|
861
|
-
};
|
|
862
|
-
});
|
|
863
|
-
});
|
|
864
|
-
it('should include Base Wallet when detected as Installed', async () => {
|
|
865
|
-
const Comlink = await import('comlink');
|
|
866
|
-
const { BridgeParent } = await import('./BridgeParent');
|
|
867
|
-
baseAdapterMock.readyState = 'Installed';
|
|
868
|
-
new BridgeParent(mockIframe);
|
|
869
|
-
await vi.advanceTimersByTimeAsync(400);
|
|
870
|
-
const exposedAPI = vi.mocked(Comlink.expose).mock.calls[0]?.[0];
|
|
871
|
-
const baseWallet = exposedAPI.walletStandardWallets.find((w) => w.name === 'Base Wallet');
|
|
872
|
-
expect(baseWallet).toBeDefined();
|
|
873
|
-
expect(baseWallet.uuid).toBe('base-wallet-traditional');
|
|
874
|
-
expect(baseWallet.chains).toEqual(['solana:mainnet']);
|
|
875
|
-
expect(baseWallet.features).toEqual(['traditional-adapter']);
|
|
876
|
-
expect(baseWallet.adapter).toBeDefined();
|
|
877
|
-
});
|
|
878
|
-
it('should include Base Wallet when detected as Loadable', async () => {
|
|
879
|
-
const Comlink = await import('comlink');
|
|
880
|
-
const { BridgeParent } = await import('./BridgeParent');
|
|
881
|
-
baseAdapterMock.readyState = 'Loadable';
|
|
882
|
-
new BridgeParent(mockIframe);
|
|
883
|
-
await vi.advanceTimersByTimeAsync(400);
|
|
884
|
-
const exposedAPI = vi.mocked(Comlink.expose).mock.calls[0]?.[0];
|
|
885
|
-
const baseWallet = exposedAPI.walletStandardWallets.find((w) => w.name === 'Base Wallet');
|
|
886
|
-
expect(baseWallet).toBeDefined();
|
|
887
|
-
});
|
|
888
|
-
it('should not include Base Wallet when not detected', async () => {
|
|
889
|
-
const Comlink = await import('comlink');
|
|
890
|
-
const { BridgeParent } = await import('./BridgeParent');
|
|
891
|
-
new BridgeParent(mockIframe);
|
|
892
|
-
await vi.advanceTimersByTimeAsync(400);
|
|
893
|
-
const exposedAPI = vi.mocked(Comlink.expose).mock.calls[0]?.[0];
|
|
894
|
-
const baseWallet = exposedAPI.walletStandardWallets.find((w) => w.name === 'Base Wallet');
|
|
895
|
-
expect(baseWallet).toBeUndefined();
|
|
896
|
-
});
|
|
897
|
-
it('should skip Base Wallet if name already in Wallet Standard wallets', async () => {
|
|
898
|
-
const { getWallets } = await import('@wallet-standard/app');
|
|
899
|
-
const Comlink = await import('comlink');
|
|
900
|
-
const { BridgeParent } = await import('./BridgeParent');
|
|
901
|
-
const existingWallet = {
|
|
902
|
-
name: 'Base Wallet',
|
|
903
|
-
chains: ['solana:mainnet'],
|
|
904
|
-
features: { 'standard:connect': {} },
|
|
905
|
-
accounts: []
|
|
906
|
-
};
|
|
907
|
-
vi.mocked(getWallets).mockReturnValue({
|
|
908
|
-
get: () => [existingWallet],
|
|
909
|
-
on: vi.fn(),
|
|
910
|
-
register: vi.fn()
|
|
911
|
-
});
|
|
912
|
-
baseAdapterMock.readyState = 'Installed';
|
|
913
|
-
new BridgeParent(mockIframe);
|
|
914
|
-
await vi.advanceTimersByTimeAsync(400);
|
|
915
|
-
const exposedAPI = vi.mocked(Comlink.expose).mock.calls[0]?.[0];
|
|
916
|
-
const baseWallets = exposedAPI.walletStandardWallets.filter((w) => w.name === 'Base Wallet');
|
|
917
|
-
expect(baseWallets).toHaveLength(1);
|
|
918
|
-
expect(baseWallets[0].features).not.toContain('traditional-adapter');
|
|
919
|
-
});
|
|
920
|
-
it('should handle BaseWalletAdapter constructor errors gracefully', async () => {
|
|
921
|
-
const Comlink = await import('comlink');
|
|
922
|
-
const { BridgeParent } = await import('./BridgeParent');
|
|
923
|
-
baseAdapterMock.shouldThrow = true;
|
|
924
|
-
new BridgeParent(mockIframe);
|
|
925
|
-
await vi.advanceTimersByTimeAsync(400);
|
|
926
|
-
const exposedAPI = vi.mocked(Comlink.expose).mock.calls[0]?.[0];
|
|
927
|
-
expect(exposedAPI).toBeDefined();
|
|
928
|
-
expect(exposedAPI.walletStandardWallets).toEqual([]);
|
|
929
|
-
});
|
|
930
|
-
});
|
|
931
|
-
describe('subscribeEip6963ProviderEvent', () => {
|
|
932
|
-
function makeMockProvider() {
|
|
933
|
-
return {
|
|
934
|
-
request: vi.fn(),
|
|
935
|
-
on: vi.fn(),
|
|
936
|
-
removeListener: vi.fn()
|
|
937
|
-
};
|
|
938
|
-
}
|
|
939
|
-
// Lets the real one-shot discovery window (300ms) resolve first, THEN
|
|
940
|
-
// overwrites eip6963Wallets with the test's fixture — otherwise the
|
|
941
|
-
// discovery's own `.then()` (see BridgeParent.ts's initializeConnection)
|
|
942
|
-
// would overwrite the fixture with `[]` out from under the test the
|
|
943
|
-
// moment `subscribeEip6963ProviderEvent`'s internal await lets it run.
|
|
944
|
-
async function readyBridgeParent(mockIframe) {
|
|
945
|
-
const { BridgeParent } = await import('./BridgeParent');
|
|
946
|
-
const bridgeParent = new BridgeParent(mockIframe);
|
|
947
|
-
await vi.advanceTimersByTimeAsync(400);
|
|
948
|
-
return bridgeParent;
|
|
949
|
-
}
|
|
950
|
-
it('attaches a real listener to the detected provider via a LOCAL wrapper, not the raw callback', async () => {
|
|
951
|
-
const bridgeParent = await readyBridgeParent(mockIframe);
|
|
952
|
-
const provider = makeMockProvider();
|
|
953
|
-
bridgeParent.parentAPI.eip6963Wallets = [
|
|
954
|
-
{
|
|
955
|
-
uuid: 'uuid-1',
|
|
956
|
-
name: 'MetaMask',
|
|
957
|
-
icon: '',
|
|
958
|
-
rdns: 'io.metamask',
|
|
959
|
-
provider
|
|
960
|
-
}
|
|
961
|
-
];
|
|
962
|
-
const callback = vi.fn();
|
|
963
|
-
const teardown = await bridgeParent.parentAPI.subscribeEip6963ProviderEvent('uuid-1', 'accountsChanged', callback);
|
|
964
|
-
expect(provider.on).toHaveBeenCalledWith('accountsChanged', expect.any(Function));
|
|
965
|
-
// Regression guard: `callback` here IS the Comlink proxy the child
|
|
966
|
-
// passed. Registering it directly on the real provider means Comlink
|
|
967
|
-
// must serialize EVERY argument the wallet's own dispatch calls it
|
|
968
|
-
// with — some wallets' internal EventEmitter pass extra, non-EIP-1193
|
|
969
|
-
// arguments (observed in production: a wallet's own internal handler
|
|
970
|
-
// function) alongside the real payload, and Comlink can't clone a
|
|
971
|
-
// function, throwing "DataCloneError" the moment the wallet fires the
|
|
972
|
-
// event. The registered listener must NOT be `callback` itself.
|
|
973
|
-
expect(provider.on).not.toHaveBeenCalledWith('accountsChanged', callback);
|
|
974
|
-
expect(typeof teardown).toBe('function');
|
|
975
|
-
// Regression guard: the teardown crosses back to the child as a
|
|
976
|
-
// Comlink return value — an un-proxied function throws "Unserializable
|
|
977
|
-
// return value" the moment a real postMessage transport (not this
|
|
978
|
-
// identity-mocked one) tries to deserialize it, exactly like TON's
|
|
979
|
-
// `listen()` teardown above must be (and is) proxied.
|
|
980
|
-
expect(Comlink.proxy).toHaveBeenCalledWith(expect.any(Function));
|
|
981
|
-
});
|
|
982
|
-
it('the local wrapper forwards ONLY the first argument to callback, discarding any extra ones the wallet passes', async () => {
|
|
983
|
-
const bridgeParent = await readyBridgeParent(mockIframe);
|
|
984
|
-
const provider = makeMockProvider();
|
|
985
|
-
bridgeParent.parentAPI.eip6963Wallets = [
|
|
986
|
-
{
|
|
987
|
-
uuid: 'uuid-1',
|
|
988
|
-
name: 'MetaMask',
|
|
989
|
-
icon: '',
|
|
990
|
-
rdns: 'io.metamask',
|
|
991
|
-
provider
|
|
992
|
-
}
|
|
993
|
-
];
|
|
994
|
-
const callback = vi.fn();
|
|
995
|
-
await bridgeParent.parentAPI.subscribeEip6963ProviderEvent('uuid-1', 'accountsChanged', callback);
|
|
996
|
-
const registeredWrapper = provider.on.mock.calls[0]?.[1];
|
|
997
|
-
const extraNonCloneableArg = () => { };
|
|
998
|
-
registeredWrapper(['0xNEW'], extraNonCloneableArg, { some: 'extra' });
|
|
999
|
-
expect(callback).toHaveBeenCalledTimes(1);
|
|
1000
|
-
expect(callback).toHaveBeenCalledWith(['0xNEW']);
|
|
1001
|
-
});
|
|
1002
|
-
it("teardown returns undefined even when the real EventEmitter's removeListener returns `this` (the whole provider)", async () => {
|
|
1003
|
-
// By EventEmitter convention (Node's `events` module, and wallet
|
|
1004
|
-
// providers built on it, e.g. Trust Wallet's inpage.js), .removeListener
|
|
1005
|
-
// returns `this` for chaining. An expression-bodied teardown
|
|
1006
|
-
// (`() => provider.removeListener?.(...)`) would implicitly return that
|
|
1007
|
-
// — the whole provider object — across the Comlink boundary, which is
|
|
1008
|
-
// itself unserializable and reproduces the exact same failure one level
|
|
1009
|
-
// down, the moment a caller actually invokes the teardown.
|
|
1010
|
-
const bridgeParent = await readyBridgeParent(mockIframe);
|
|
1011
|
-
const provider = {
|
|
1012
|
-
request: vi.fn(),
|
|
1013
|
-
on: vi.fn(),
|
|
1014
|
-
removeListener: vi.fn(function () {
|
|
1015
|
-
return this;
|
|
1016
|
-
})
|
|
1017
|
-
};
|
|
1018
|
-
bridgeParent.parentAPI.eip6963Wallets = [
|
|
1019
|
-
{
|
|
1020
|
-
uuid: 'uuid-1',
|
|
1021
|
-
name: 'Trust Wallet',
|
|
1022
|
-
icon: '',
|
|
1023
|
-
rdns: 'com.trustwallet.app',
|
|
1024
|
-
provider
|
|
1025
|
-
}
|
|
1026
|
-
];
|
|
1027
|
-
const teardown = await bridgeParent.parentAPI.subscribeEip6963ProviderEvent('uuid-1', 'accountsChanged', vi.fn());
|
|
1028
|
-
expect(teardown()).toBeUndefined();
|
|
1029
|
-
});
|
|
1030
|
-
it('teardown calls removeListener with the SAME wrapper reference registered via .on', async () => {
|
|
1031
|
-
const bridgeParent = await readyBridgeParent(mockIframe);
|
|
1032
|
-
const provider = makeMockProvider();
|
|
1033
|
-
bridgeParent.parentAPI.eip6963Wallets = [
|
|
1034
|
-
{
|
|
1035
|
-
uuid: 'uuid-1',
|
|
1036
|
-
name: 'MetaMask',
|
|
1037
|
-
icon: '',
|
|
1038
|
-
rdns: 'io.metamask',
|
|
1039
|
-
provider
|
|
1040
|
-
}
|
|
1041
|
-
];
|
|
1042
|
-
const callback = vi.fn();
|
|
1043
|
-
const teardown = await bridgeParent.parentAPI.subscribeEip6963ProviderEvent('uuid-1', 'accountsChanged', callback);
|
|
1044
|
-
const registeredWrapper = provider.on.mock.calls[0]?.[1];
|
|
1045
|
-
teardown?.();
|
|
1046
|
-
expect(provider.removeListener).toHaveBeenCalledWith('accountsChanged', registeredWrapper);
|
|
1047
|
-
});
|
|
1048
|
-
it('throws for an unknown uuid (wallet not detected)', async () => {
|
|
1049
|
-
const bridgeParent = await readyBridgeParent(mockIframe);
|
|
1050
|
-
bridgeParent.parentAPI.eip6963Wallets = [];
|
|
1051
|
-
await expect(bridgeParent.parentAPI.subscribeEip6963ProviderEvent('unknown-uuid', 'accountsChanged', vi.fn())).rejects.toThrow(/no wallet found for uuid/);
|
|
1052
|
-
});
|
|
1053
|
-
it('throws when no wallet matches the given uuid (the uuid-mismatch bug this guards against)', async () => {
|
|
1054
|
-
const bridgeParent = await readyBridgeParent(mockIframe);
|
|
1055
|
-
bridgeParent.parentAPI.eip6963Wallets = [];
|
|
1056
|
-
await expect(bridgeParent.parentAPI.subscribeEip6963ProviderEvent('uuid-not-discovered-by-this-parent', 'accountsChanged', vi.fn())).rejects.toThrow(/no wallet found for uuid/);
|
|
1057
|
-
});
|
|
1058
|
-
it('falls back to matching by rdns when the uuid does not match (the uuid-mismatch bug this closes structurally)', async () => {
|
|
1059
|
-
const bridgeParent = await readyBridgeParent(mockIframe);
|
|
1060
|
-
const provider = makeMockProvider();
|
|
1061
|
-
bridgeParent.parentAPI.eip6963Wallets = [
|
|
1062
|
-
{
|
|
1063
|
-
uuid: 'uuid-from-top-frame',
|
|
1064
|
-
name: 'MetaMask',
|
|
1065
|
-
icon: '',
|
|
1066
|
-
rdns: 'io.metamask',
|
|
1067
|
-
provider
|
|
1068
|
-
}
|
|
1069
|
-
];
|
|
1070
|
-
const callback = vi.fn();
|
|
1071
|
-
const teardown = await bridgeParent.parentAPI.subscribeEip6963ProviderEvent('uuid-from-local-iframe-injection', 'accountsChanged', callback, 'io.metamask');
|
|
1072
|
-
expect(typeof teardown).toBe('function');
|
|
1073
|
-
expect(provider.on).toHaveBeenCalledWith('accountsChanged', expect.any(Function));
|
|
1074
|
-
});
|
|
1075
|
-
it('still throws when neither uuid nor rdns match anything', async () => {
|
|
1076
|
-
const bridgeParent = await readyBridgeParent(mockIframe);
|
|
1077
|
-
bridgeParent.parentAPI.eip6963Wallets = [];
|
|
1078
|
-
await expect(bridgeParent.parentAPI.subscribeEip6963ProviderEvent('uuid-not-discovered-by-this-parent', 'accountsChanged', vi.fn(), 'com.nonexistent.wallet')).rejects.toThrow(/no wallet found for uuid/);
|
|
1079
|
-
});
|
|
1080
|
-
it('awaits the in-flight discovery window instead of failing permanently when subscribe is called BEFORE it resolves', async () => {
|
|
1081
|
-
// Regression for the startup race: a subscribe call arriving while the
|
|
1082
|
-
// one-shot 300ms discovery window is still open (e.g. immediately on
|
|
1083
|
-
// page load, during a session restore) used to look up against a
|
|
1084
|
-
// still-empty eip6963Wallets, miss, and throw — permanently, since the
|
|
1085
|
-
// child never retries a rejected subscribe (see BridgeChild.ts's
|
|
1086
|
-
// window.__uwcSubscribeEip6963ProviderEvent .catch()).
|
|
1087
|
-
const provider = makeMockProvider();
|
|
1088
|
-
window.addEventListener('eip6963:requestProvider', () => {
|
|
1089
|
-
window.dispatchEvent(new CustomEvent('eip6963:announceProvider', {
|
|
1090
|
-
detail: {
|
|
1091
|
-
info: {
|
|
1092
|
-
uuid: 'uuid-1',
|
|
1093
|
-
name: 'MetaMask',
|
|
1094
|
-
icon: '',
|
|
1095
|
-
rdns: 'io.metamask'
|
|
1096
|
-
},
|
|
1097
|
-
provider
|
|
1098
|
-
}
|
|
1099
|
-
}));
|
|
1100
|
-
});
|
|
1101
|
-
const { BridgeParent } = await import('./BridgeParent');
|
|
1102
|
-
const bridgeParent = new BridgeParent(mockIframe);
|
|
1103
|
-
// Call subscribe IMMEDIATELY — well before the 300ms discovery window
|
|
1104
|
-
// (dispatched synchronously above, so the wallet IS in the discovery
|
|
1105
|
-
// listener's closure, but the parent hasn't resolved/flipped
|
|
1106
|
-
// eip6963WalletsReady yet).
|
|
1107
|
-
const subscribePromise = bridgeParent.parentAPI.subscribeEip6963ProviderEvent('uuid-1', 'accountsChanged', vi.fn());
|
|
1108
|
-
// Let the 300ms discovery window elapse.
|
|
1109
|
-
await vi.advanceTimersByTimeAsync(400);
|
|
1110
|
-
const teardown = await subscribePromise;
|
|
1111
|
-
expect(typeof teardown).toBe('function');
|
|
1112
|
-
expect(provider.on).toHaveBeenCalledWith('accountsChanged', expect.any(Function));
|
|
1113
|
-
});
|
|
1114
|
-
});
|
|
1115
|
-
describe('subscribeTronProviderEvent', () => {
|
|
1116
|
-
function makeMockTronProvider() {
|
|
1117
|
-
return {
|
|
1118
|
-
tronWeb: {},
|
|
1119
|
-
on: vi.fn(),
|
|
1120
|
-
removeListener: vi.fn()
|
|
1121
|
-
};
|
|
1122
|
-
}
|
|
1123
|
-
it('resolves the provider by injectedId window path and attaches a real listener via a LOCAL wrapper', async () => {
|
|
1124
|
-
const { BridgeParent } = await import('./BridgeParent');
|
|
1125
|
-
const bridgeParent = new BridgeParent(mockIframe);
|
|
1126
|
-
const provider = makeMockTronProvider();
|
|
1127
|
-
window.tronLink = provider;
|
|
1128
|
-
const callback = vi.fn();
|
|
1129
|
-
const teardown = bridgeParent.parentAPI.subscribeTronProviderEvent('tronLink', 'accountsChanged', callback);
|
|
1130
|
-
expect(provider.on).toHaveBeenCalledWith('accountsChanged', expect.any(Function));
|
|
1131
|
-
// Regression guard: same "the registered listener must not be the raw
|
|
1132
|
-
// Comlink-proxied callback" requirement as the EIP-6963 method above —
|
|
1133
|
-
// a wallet's own dispatch calling the listener with an extra,
|
|
1134
|
-
// non-serializable argument (e.g. an internal handler function)
|
|
1135
|
-
// otherwise throws DataCloneError the moment the wallet fires the
|
|
1136
|
-
// event.
|
|
1137
|
-
expect(provider.on).not.toHaveBeenCalledWith('accountsChanged', callback);
|
|
1138
|
-
expect(typeof teardown).toBe('function');
|
|
1139
|
-
// Regression guard: same "must cross the bridge as a Comlink return
|
|
1140
|
-
// value" requirement as the EIP-6963 subscribe method above.
|
|
1141
|
-
expect(Comlink.proxy).toHaveBeenCalledWith(expect.any(Function));
|
|
1142
|
-
const registeredWrapper = provider.on.mock.calls[0]?.[1];
|
|
1143
|
-
teardown?.();
|
|
1144
|
-
expect(provider.removeListener).toHaveBeenCalledWith('accountsChanged', registeredWrapper);
|
|
1145
|
-
delete window.tronLink;
|
|
1146
|
-
});
|
|
1147
|
-
it('the local wrapper forwards ONLY the first argument to callback, discarding any extra ones the wallet passes', async () => {
|
|
1148
|
-
const { BridgeParent } = await import('./BridgeParent');
|
|
1149
|
-
const bridgeParent = new BridgeParent(mockIframe);
|
|
1150
|
-
const provider = makeMockTronProvider();
|
|
1151
|
-
window.tronLink = provider;
|
|
1152
|
-
const callback = vi.fn();
|
|
1153
|
-
bridgeParent.parentAPI.subscribeTronProviderEvent('tronLink', 'accountsChanged', callback);
|
|
1154
|
-
const registeredWrapper = provider.on.mock.calls[0]?.[1];
|
|
1155
|
-
const extraNonCloneableArg = () => { };
|
|
1156
|
-
registeredWrapper('TSwitchedAddress', extraNonCloneableArg);
|
|
1157
|
-
expect(callback).toHaveBeenCalledTimes(1);
|
|
1158
|
-
expect(callback).toHaveBeenCalledWith('TSwitchedAddress');
|
|
1159
|
-
delete window.tronLink;
|
|
1160
|
-
});
|
|
1161
|
-
it("teardown returns undefined even when the real provider's removeListener returns `this`", async () => {
|
|
1162
|
-
const { BridgeParent } = await import('./BridgeParent');
|
|
1163
|
-
const bridgeParent = new BridgeParent(mockIframe);
|
|
1164
|
-
const provider = {
|
|
1165
|
-
tronWeb: {},
|
|
1166
|
-
on: vi.fn(),
|
|
1167
|
-
removeListener: vi.fn(function () {
|
|
1168
|
-
return this;
|
|
1169
|
-
})
|
|
1170
|
-
};
|
|
1171
|
-
window.tronLink = provider;
|
|
1172
|
-
const teardown = bridgeParent.parentAPI.subscribeTronProviderEvent('tronLink', 'accountsChanged', vi.fn());
|
|
1173
|
-
expect(teardown()).toBeUndefined();
|
|
1174
|
-
delete window.tronLink;
|
|
1175
|
-
});
|
|
1176
|
-
it('throws when the injectedId window path resolves to nothing', async () => {
|
|
1177
|
-
const { BridgeParent } = await import('./BridgeParent');
|
|
1178
|
-
const bridgeParent = new BridgeParent(mockIframe);
|
|
1179
|
-
expect(() => bridgeParent.parentAPI.subscribeTronProviderEvent('nonexistent.path', 'accountsChanged', vi.fn())).toThrow(/no provider found at injectedId/);
|
|
1180
|
-
});
|
|
1181
|
-
it('throws when no provider resolves at the given injectedId path', async () => {
|
|
1182
|
-
const { BridgeParent } = await import('./BridgeParent');
|
|
1183
|
-
const bridgeParent = new BridgeParent(mockIframe);
|
|
1184
|
-
expect(() => bridgeParent.parentAPI.subscribeTronProviderEvent('nonExistentWallet', 'accountsChanged', vi.fn())).toThrow(/no provider found at injectedId/);
|
|
1185
|
-
});
|
|
1186
|
-
// MFS-778: TronLink resolves `window.tronLink` (request+tronWeb, no `.on`)
|
|
1187
|
-
// for discovery/signing, but emits account/disconnect events on its
|
|
1188
|
-
// separate TIP-1193 `window.tron`. Binding on the resolved provider alone
|
|
1189
|
-
// silently dropped every account switch.
|
|
1190
|
-
it('subscribes on the TIP-1193 window.tron when the resolved provider has no .on', async () => {
|
|
1191
|
-
const { BridgeParent } = await import('./BridgeParent');
|
|
1192
|
-
const bridgeParent = new BridgeParent(mockIframe);
|
|
1193
|
-
window.tronLink = { tronWeb: {}, request: vi.fn() };
|
|
1194
|
-
// TIP-1193 emitter TronLink actually fires events on.
|
|
1195
|
-
const emitter = {
|
|
1196
|
-
tronWeb: {},
|
|
1197
|
-
request: vi.fn(),
|
|
1198
|
-
on: vi.fn(),
|
|
1199
|
-
removeListener: vi.fn()
|
|
1200
|
-
};
|
|
1201
|
-
window.tron = emitter;
|
|
1202
|
-
const callback = vi.fn();
|
|
1203
|
-
const teardown = bridgeParent.parentAPI.subscribeTronProviderEvent('tronLink', 'accountsChanged', callback);
|
|
1204
|
-
expect(emitter.on).toHaveBeenCalledWith('accountsChanged', expect.any(Function));
|
|
1205
|
-
// Same regression guard as the eip6963/direct paths: the raw
|
|
1206
|
-
// Comlink-proxied callback is never registered directly.
|
|
1207
|
-
expect(emitter.on).not.toHaveBeenCalledWith('accountsChanged', callback);
|
|
1208
|
-
const registeredWrapper = emitter.on.mock.calls[0]?.[1];
|
|
1209
|
-
registeredWrapper(['TSwitched'], () => { });
|
|
1210
|
-
expect(callback).toHaveBeenCalledTimes(1);
|
|
1211
|
-
expect(callback).toHaveBeenCalledWith(['TSwitched']);
|
|
1212
|
-
teardown?.();
|
|
1213
|
-
expect(emitter.removeListener).toHaveBeenCalledWith('accountsChanged', registeredWrapper);
|
|
1214
|
-
delete window.tronLink;
|
|
1215
|
-
delete window.tron;
|
|
1216
|
-
});
|
|
1217
|
-
// A non-TronLink wallet whose own Tron provider lacks `.on` (confirmed live
|
|
1218
|
-
// for OKX's `okxwallet.tronLink`) must NOT inherit TronLink's `window.tron`.
|
|
1219
|
-
// OKX broadcasts account switches on the window `message` channel; binding
|
|
1220
|
-
// its `accountsChanged` on TronLink's `window.tron` means OKX's switches
|
|
1221
|
-
// fire on the wrong wallet's provider and are silently dropped (the OKX+Tron
|
|
1222
|
-
// account-switch bug). It must fall through to the `message` channel.
|
|
1223
|
-
it('does NOT inherit TronLink window.tron for OKX (emitter-less, non-TronLink) — falls back to window `message`', async () => {
|
|
1224
|
-
const { BridgeParent } = await import('./BridgeParent');
|
|
1225
|
-
const bridgeParent = new BridgeParent(mockIframe);
|
|
1226
|
-
window.okxwallet = {
|
|
1227
|
-
tronLink: { tronWeb: {}, request: vi.fn() }
|
|
1228
|
-
};
|
|
1229
|
-
// TronLink's TIP-1193 emitter IS present (both wallets installed) and is a
|
|
1230
|
-
// valid Tron provider — so ONLY the identity gate keeps OKX off it.
|
|
1231
|
-
const tronLinkEmitter = {
|
|
1232
|
-
tronWeb: {},
|
|
1233
|
-
request: vi.fn(),
|
|
1234
|
-
on: vi.fn(),
|
|
1235
|
-
removeListener: vi.fn()
|
|
1236
|
-
};
|
|
1237
|
-
window.tron = tronLinkEmitter;
|
|
1238
|
-
const addSpy = vi.spyOn(window, 'addEventListener');
|
|
1239
|
-
const callback = vi.fn();
|
|
1240
|
-
const teardown = bridgeParent.parentAPI.subscribeTronProviderEvent('okxwallet.tronLink', 'accountsChanged', callback);
|
|
1241
|
-
// Must NOT bind on TronLink's window.tron.
|
|
1242
|
-
expect(tronLinkEmitter.on).not.toHaveBeenCalled();
|
|
1243
|
-
// Instead subscribes to the window `message` channel — OKX's real channel.
|
|
1244
|
-
expect(addSpy).toHaveBeenCalledWith('message', expect.any(Function));
|
|
1245
|
-
window.dispatchEvent(new MessageEvent('message', {
|
|
1246
|
-
data: {
|
|
1247
|
-
message: {
|
|
1248
|
-
action: 'accountsChanged',
|
|
1249
|
-
data: { address: 'TOkxSwitched' }
|
|
1250
|
-
}
|
|
1251
|
-
},
|
|
1252
|
-
source: window,
|
|
1253
|
-
origin: window.location.origin
|
|
1254
|
-
}));
|
|
1255
|
-
expect(callback).toHaveBeenCalledWith(['TOkxSwitched']);
|
|
1256
|
-
teardown?.();
|
|
1257
|
-
delete window.okxwallet;
|
|
1258
|
-
delete window.tron;
|
|
1259
|
-
});
|
|
1260
|
-
it('ignores a forged Tron `setAccount` from a foreign source/origin (runs in the top frame)', async () => {
|
|
1261
|
-
// This handler runs in the client's TOP frame and feeds the live session
|
|
1262
|
-
// address, so a same-origin sibling frame or window.opener must not be
|
|
1263
|
-
// able to post `{ message: { action: 'setAccount' } }` and move the account.
|
|
1264
|
-
const { BridgeParent } = await import('./BridgeParent');
|
|
1265
|
-
const bridgeParent = new BridgeParent(mockIframe);
|
|
1266
|
-
window.tronLink = { tronWeb: {}, request: vi.fn() };
|
|
1267
|
-
const callback = vi.fn();
|
|
1268
|
-
const teardown = bridgeParent.parentAPI.subscribeTronProviderEvent('tronLink', 'accountsChanged', callback);
|
|
1269
|
-
// Foreign source.
|
|
1270
|
-
window.dispatchEvent(new MessageEvent('message', {
|
|
1271
|
-
data: {
|
|
1272
|
-
message: { action: 'setAccount', data: { address: 'TAttacker' } }
|
|
1273
|
-
},
|
|
1274
|
-
source: {},
|
|
1275
|
-
origin: window.location.origin
|
|
1276
|
-
}));
|
|
1277
|
-
// Right source, wrong origin.
|
|
1278
|
-
window.dispatchEvent(new MessageEvent('message', {
|
|
1279
|
-
data: {
|
|
1280
|
-
message: { action: 'setAccount', data: { address: 'TAttacker' } }
|
|
1281
|
-
},
|
|
1282
|
-
source: window,
|
|
1283
|
-
origin: 'https://evil.example'
|
|
1284
|
-
}));
|
|
1285
|
-
expect(callback).not.toHaveBeenCalled();
|
|
1286
|
-
teardown?.();
|
|
1287
|
-
delete window.tronLink;
|
|
1288
|
-
});
|
|
1289
|
-
it('falls back to the window `message` channel when neither the resolved provider nor window.tron has .on', async () => {
|
|
1290
|
-
const { BridgeParent } = await import('./BridgeParent');
|
|
1291
|
-
const bridgeParent = new BridgeParent(mockIframe);
|
|
1292
|
-
window.tronLink = { tronWeb: {}, request: vi.fn() };
|
|
1293
|
-
// No window.tron emitter at all.
|
|
1294
|
-
const addSpy = vi.spyOn(window, 'addEventListener');
|
|
1295
|
-
const removeSpy = vi.spyOn(window, 'removeEventListener');
|
|
1296
|
-
const callback = vi.fn();
|
|
1297
|
-
const teardown = bridgeParent.parentAPI.subscribeTronProviderEvent('tronLink', 'accountsChanged', callback);
|
|
1298
|
-
expect(addSpy).toHaveBeenCalledWith('message', expect.any(Function));
|
|
1299
|
-
window.dispatchEvent(new MessageEvent('message', {
|
|
1300
|
-
data: {
|
|
1301
|
-
message: { action: 'setAccount', data: { address: 'TSwitched' } }
|
|
1302
|
-
},
|
|
1303
|
-
source: window,
|
|
1304
|
-
origin: window.location.origin
|
|
1305
|
-
}));
|
|
1306
|
-
expect(callback).toHaveBeenCalledWith(['TSwitched']);
|
|
1307
|
-
teardown?.();
|
|
1308
|
-
expect(removeSpy).toHaveBeenCalledWith('message', expect.any(Function));
|
|
1309
|
-
delete window.tronLink;
|
|
1310
|
-
});
|
|
1311
|
-
});
|
|
1312
|
-
describe('subscribeSolanaWalletEvent', () => {
|
|
1313
|
-
beforeEach(async () => {
|
|
1314
|
-
// Re-set mocks that vi.restoreAllMocks() may have cleared.
|
|
1315
|
-
const { getWallets } = await import('@wallet-standard/app');
|
|
1316
|
-
vi.mocked(getWallets).mockReturnValue({
|
|
1317
|
-
get: () => [],
|
|
1318
|
-
on: vi.fn(),
|
|
1319
|
-
register: vi.fn()
|
|
1320
|
-
});
|
|
1321
|
-
const { isWalletAdapterCompatibleStandardWallet } = await import('@solana/wallet-adapter-base');
|
|
1322
|
-
vi.mocked(isWalletAdapterCompatibleStandardWallet).mockReturnValue(false);
|
|
1323
|
-
baseAdapterMock.readyState = 'NotDetected';
|
|
1324
|
-
baseAdapterMock.shouldThrow = false;
|
|
1325
|
-
baseAdapterMock.fn.mockImplementation(function () {
|
|
1326
|
-
if (baseAdapterMock.shouldThrow)
|
|
1327
|
-
throw new Error('init failed');
|
|
1328
|
-
return {
|
|
1329
|
-
readyState: baseAdapterMock.readyState,
|
|
1330
|
-
name: 'Base Wallet',
|
|
1331
|
-
on: vi.fn(),
|
|
1332
|
-
off: vi.fn()
|
|
1333
|
-
};
|
|
1334
|
-
});
|
|
1335
|
-
});
|
|
1336
|
-
function stubStandardWalletAdapter(adapter) {
|
|
1337
|
-
return import('@solana/wallet-standard-wallet-adapter-base').then(({ StandardWalletAdapter }) => {
|
|
1338
|
-
vi.mocked(StandardWalletAdapter).mockImplementation(function () {
|
|
1339
|
-
return adapter;
|
|
1340
|
-
});
|
|
1341
|
-
});
|
|
1342
|
-
}
|
|
1343
|
-
it('resolves the connected wallet-standard adapter by walletId and attaches connect/disconnect listeners via a LOCAL wrapper', async () => {
|
|
1344
|
-
const { getWallets } = await import('@wallet-standard/app');
|
|
1345
|
-
const { isWalletAdapterCompatibleStandardWallet } = await import('@solana/wallet-adapter-base');
|
|
1346
|
-
const { BridgeParent } = await import('./BridgeParent');
|
|
1347
|
-
vi.mocked(getWallets).mockReturnValue({
|
|
1348
|
-
get: () => [
|
|
1349
|
-
{
|
|
1350
|
-
name: 'Phantom',
|
|
1351
|
-
chains: ['solana:mainnet'],
|
|
1352
|
-
features: {},
|
|
1353
|
-
accounts: []
|
|
1354
|
-
}
|
|
1355
|
-
],
|
|
1356
|
-
on: vi.fn(),
|
|
1357
|
-
register: vi.fn()
|
|
1358
|
-
});
|
|
1359
|
-
vi.mocked(isWalletAdapterCompatibleStandardWallet).mockReturnValue(true);
|
|
1360
|
-
const adapterOn = vi.fn();
|
|
1361
|
-
const adapterOff = vi.fn();
|
|
1362
|
-
await stubStandardWalletAdapter({ on: adapterOn, off: adapterOff });
|
|
1363
|
-
const bridgeParent = new BridgeParent(mockIframe);
|
|
1364
|
-
await vi.advanceTimersByTimeAsync(400);
|
|
1365
|
-
const callback = vi.fn();
|
|
1366
|
-
const teardown = bridgeParent.parentAPI.subscribeSolanaWalletEvent('phantom-solana:mainnet', callback);
|
|
1367
|
-
expect(adapterOn).toHaveBeenCalledWith('connect', expect.any(Function));
|
|
1368
|
-
expect(adapterOn).toHaveBeenCalledWith('disconnect', expect.any(Function));
|
|
1369
|
-
expect(typeof teardown).toBe('function');
|
|
1370
|
-
// Regression guard: the teardown crosses back to the child as a
|
|
1371
|
-
// Comlink return value, same requirement as the eip155/tron teardowns.
|
|
1372
|
-
expect(Comlink.proxy).toHaveBeenCalledWith(expect.any(Function));
|
|
1373
|
-
teardown?.();
|
|
1374
|
-
expect(adapterOff).toHaveBeenCalledWith('connect', expect.any(Function));
|
|
1375
|
-
expect(adapterOff).toHaveBeenCalledWith('disconnect', expect.any(Function));
|
|
1376
|
-
});
|
|
1377
|
-
it('reduces the PublicKey to a plain base58 string BEFORE forwarding to callback (never the object itself)', async () => {
|
|
1378
|
-
const { getWallets } = await import('@wallet-standard/app');
|
|
1379
|
-
const { isWalletAdapterCompatibleStandardWallet } = await import('@solana/wallet-adapter-base');
|
|
1380
|
-
const { BridgeParent } = await import('./BridgeParent');
|
|
1381
|
-
vi.mocked(getWallets).mockReturnValue({
|
|
1382
|
-
get: () => [
|
|
1383
|
-
{
|
|
1384
|
-
name: 'Phantom',
|
|
1385
|
-
chains: ['solana:mainnet'],
|
|
1386
|
-
features: {},
|
|
1387
|
-
accounts: []
|
|
1388
|
-
}
|
|
1389
|
-
],
|
|
1390
|
-
on: vi.fn(),
|
|
1391
|
-
register: vi.fn()
|
|
1392
|
-
});
|
|
1393
|
-
vi.mocked(isWalletAdapterCompatibleStandardWallet).mockReturnValue(true);
|
|
1394
|
-
const adapterOn = vi.fn();
|
|
1395
|
-
const adapterOff = vi.fn();
|
|
1396
|
-
await stubStandardWalletAdapter({ on: adapterOn, off: adapterOff });
|
|
1397
|
-
const bridgeParent = new BridgeParent(mockIframe);
|
|
1398
|
-
await vi.advanceTimersByTimeAsync(400);
|
|
1399
|
-
const callback = vi.fn();
|
|
1400
|
-
bridgeParent.parentAPI.subscribeSolanaWalletEvent('phantom-solana:mainnet', callback);
|
|
1401
|
-
const onConnectWrapper = adapterOn.mock.calls.find(call => call[0] === 'connect')?.[1];
|
|
1402
|
-
const onDisconnectWrapper = adapterOn.mock.calls.find(call => call[0] === 'disconnect')?.[1];
|
|
1403
|
-
const fakePublicKey = { toBase58: () => 'SoLaNaAddr_NEW' };
|
|
1404
|
-
onConnectWrapper(fakePublicKey);
|
|
1405
|
-
expect(callback).toHaveBeenCalledWith('SoLaNaAddr_NEW');
|
|
1406
|
-
expect(callback).not.toHaveBeenCalledWith(fakePublicKey);
|
|
1407
|
-
onDisconnectWrapper();
|
|
1408
|
-
expect(callback).toHaveBeenCalledWith(null);
|
|
1409
|
-
});
|
|
1410
|
-
it('throws when no wallet matches the given walletId', async () => {
|
|
1411
|
-
const { BridgeParent } = await import('./BridgeParent');
|
|
1412
|
-
const bridgeParent = new BridgeParent(mockIframe);
|
|
1413
|
-
await vi.advanceTimersByTimeAsync(400);
|
|
1414
|
-
expect(() => bridgeParent.parentAPI.subscribeSolanaWalletEvent('unknown-wallet-id', vi.fn())).toThrow(/no wallet found for walletId/);
|
|
1415
|
-
});
|
|
1416
|
-
it('resolves the traditional Base Wallet adapter by its "-traditional" walletId', async () => {
|
|
1417
|
-
const { BridgeParent } = await import('./BridgeParent');
|
|
1418
|
-
baseAdapterMock.readyState = 'Installed';
|
|
1419
|
-
const bridgeParent = new BridgeParent(mockIframe);
|
|
1420
|
-
await vi.advanceTimersByTimeAsync(400);
|
|
1421
|
-
const callback = vi.fn();
|
|
1422
|
-
const teardown = bridgeParent.parentAPI.subscribeSolanaWalletEvent('base-wallet-traditional', callback);
|
|
1423
|
-
expect(typeof teardown).toBe('function');
|
|
1424
|
-
});
|
|
1425
|
-
// --- Phantom multi-switch regression (MFS-778) ------------------------
|
|
1426
|
-
// The bridge resolves a FRESH, never-connected StandardWalletAdapter, whose
|
|
1427
|
-
// connect/disconnect derivation ignores account-to-account switches — so it
|
|
1428
|
-
// must observe the wallet's `standard:events` change stream directly.
|
|
1429
|
-
function stubAdapterInstance(adapter) {
|
|
1430
|
-
return import('@solana/wallet-standard-wallet-adapter-base').then(({ StandardWalletAdapter }) => {
|
|
1431
|
-
// Must be a regular function (not an arrow): the SUT calls
|
|
1432
|
-
// `new StandardWalletAdapter(...)`, and arrows can't be constructed.
|
|
1433
|
-
vi.mocked(StandardWalletAdapter).mockImplementation(function () {
|
|
1434
|
-
return adapter;
|
|
1435
|
-
});
|
|
1436
|
-
});
|
|
1437
|
-
}
|
|
1438
|
-
async function armStandardEventsWallet(initialAccounts) {
|
|
1439
|
-
const { getWallets } = await import('@wallet-standard/app');
|
|
1440
|
-
const { isWalletAdapterCompatibleStandardWallet } = await import('@solana/wallet-adapter-base');
|
|
1441
|
-
vi.mocked(getWallets).mockReturnValue({
|
|
1442
|
-
get: () => [
|
|
1443
|
-
{
|
|
1444
|
-
name: 'Phantom',
|
|
1445
|
-
chains: ['solana:mainnet'],
|
|
1446
|
-
features: {},
|
|
1447
|
-
accounts: []
|
|
1448
|
-
}
|
|
1449
|
-
],
|
|
1450
|
-
on: vi.fn(),
|
|
1451
|
-
register: vi.fn()
|
|
1452
|
-
});
|
|
1453
|
-
vi.mocked(isWalletAdapterCompatibleStandardWallet).mockReturnValue(true);
|
|
1454
|
-
let changeListener;
|
|
1455
|
-
const off = vi.fn();
|
|
1456
|
-
// `accounts` is mutable: the wallet updates it as the user switches, and
|
|
1457
|
-
// the handler must read the CURRENT active account on each change.
|
|
1458
|
-
const wallet = {
|
|
1459
|
-
accounts: initialAccounts,
|
|
1460
|
-
features: {
|
|
1461
|
-
'standard:events': {
|
|
1462
|
-
on: vi.fn((event, listener) => {
|
|
1463
|
-
if (event === 'change')
|
|
1464
|
-
changeListener = listener;
|
|
1465
|
-
return off;
|
|
1466
|
-
})
|
|
1467
|
-
}
|
|
1468
|
-
}
|
|
1469
|
-
};
|
|
1470
|
-
const adapterOn = vi.fn();
|
|
1471
|
-
const adapterOff = vi.fn();
|
|
1472
|
-
await stubAdapterInstance({ on: adapterOn, off: adapterOff, wallet });
|
|
1473
|
-
const { BridgeParent } = await import('./BridgeParent');
|
|
1474
|
-
const bridgeParent = new BridgeParent(mockIframe);
|
|
1475
|
-
await vi.advanceTimersByTimeAsync(400);
|
|
1476
|
-
const callback = vi.fn();
|
|
1477
|
-
const teardown = bridgeParent.parentAPI.subscribeSolanaWalletEvent('phantom-solana:mainnet', callback);
|
|
1478
|
-
return {
|
|
1479
|
-
wallet,
|
|
1480
|
-
off,
|
|
1481
|
-
adapterOn,
|
|
1482
|
-
callback,
|
|
1483
|
-
teardown,
|
|
1484
|
-
fire: (props) => changeListener?.(props)
|
|
1485
|
-
};
|
|
1486
|
-
}
|
|
1487
|
-
it('subscribes to the wallet `standard:events` change stream and NOT the fresh adapter connect/disconnect (the multi-switch fix)', async () => {
|
|
1488
|
-
const { wallet, adapterOn, teardown, off } = await armStandardEventsWallet([{ address: 'ACC_1' }]);
|
|
1489
|
-
// Subscribed to the change stream — the signal that fires on every switch.
|
|
1490
|
-
expect(wallet.features['standard:events'].on).toHaveBeenCalledWith('change', expect.any(Function));
|
|
1491
|
-
// NOT the never-connected adapter's connect/disconnect, which silently
|
|
1492
|
-
// dropped account-to-account switches on the bridge.
|
|
1493
|
-
expect(adapterOn).not.toHaveBeenCalledWith('connect', expect.any(Function));
|
|
1494
|
-
expect(adapterOn).not.toHaveBeenCalledWith('disconnect', expect.any(Function));
|
|
1495
|
-
// Teardown crosses back to the child as a Comlink return value.
|
|
1496
|
-
expect(typeof teardown).toBe('function');
|
|
1497
|
-
expect(Comlink.proxy).toHaveBeenCalledWith(expect.any(Function));
|
|
1498
|
-
expect(off).not.toHaveBeenCalled();
|
|
1499
|
-
});
|
|
1500
|
-
it('propagates each of multiple switches, then null on de-auth, and ignores non-account changes', async () => {
|
|
1501
|
-
const { wallet, callback, off, teardown, fire } = await armStandardEventsWallet([{ address: 'ACC_1' }]);
|
|
1502
|
-
// Switch #1
|
|
1503
|
-
wallet.accounts = [{ address: 'ACC_2' }];
|
|
1504
|
-
fire({ accounts: wallet.accounts });
|
|
1505
|
-
expect(callback).toHaveBeenNthCalledWith(1, 'ACC_2');
|
|
1506
|
-
// Switch #2 (the intermediate switch that used to be dropped)
|
|
1507
|
-
wallet.accounts = [{ address: 'ACC_3' }];
|
|
1508
|
-
fire({ accounts: wallet.accounts });
|
|
1509
|
-
expect(callback).toHaveBeenNthCalledWith(2, 'ACC_3');
|
|
1510
|
-
// A pure chain/feature change (no `accounts` key) must be ignored.
|
|
1511
|
-
fire({ chains: ['solana:devnet'] });
|
|
1512
|
-
expect(callback).toHaveBeenCalledTimes(2);
|
|
1513
|
-
// Switch to an account not authorized for the dapp → empty accounts → null,
|
|
1514
|
-
// which the child maps to [] (disconnect).
|
|
1515
|
-
wallet.accounts = [];
|
|
1516
|
-
fire({ accounts: [] });
|
|
1517
|
-
expect(callback).toHaveBeenNthCalledWith(3, null);
|
|
1518
|
-
// Teardown unsubscribes the change stream.
|
|
1519
|
-
teardown?.();
|
|
1520
|
-
expect(off).toHaveBeenCalledTimes(1);
|
|
1521
|
-
});
|
|
1522
|
-
});
|
|
1523
|
-
});
|
|
1524
|
-
//# sourceMappingURL=BridgeParent.test.js.map
|