@mictonode/widget 0.3.22 → 0.3.23

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.
Files changed (89) hide show
  1. package/README.md +2 -2
  2. package/dist/{ConstantsUtil-df193f67.js → ConstantsUtil-f9dc60b6.js} +3 -3
  3. package/dist/{ConstantsUtil-1b0c0436.js → ConstantsUtil-fb0ff2e5.js} +1 -1
  4. package/dist/{ExchangeController-953f14bc.js → ExchangeController-173fd1dc.js} +2 -2
  5. package/dist/{HelpersUtil-3810bb58.js → HelpersUtil-4891ff5f.js} +4 -4
  6. package/dist/{ReownAuthentication-7c5b6e79.js → ReownAuthentication-bba89dc5.js} +2 -2
  7. package/dist/{SwapController-e1a7b5f8.js → SwapController-e8b097ca.js} +3 -3
  8. package/dist/{abis-61cccfbf.js → abis-96fed285.js} +1 -1
  9. package/dist/{assertCurrentChain-0c3d6c93.js → assertCurrentChain-b60aa59f.js} +1 -1
  10. package/dist/{ccip-80fe88fb.js → ccip-8f82ae2a.js} +6 -6
  11. package/dist/{data-capture-7edeb077.js → data-capture-7009ffa0.js} +8 -8
  12. package/dist/{email-40537ec5.js → email-d6e2da58.js} +12 -12
  13. package/dist/{embedded-wallet-f71f6169.js → embedded-wallet-48284179.js} +14 -14
  14. package/dist/{encodeFunctionData-91e7c0f6.js → encodeFunctionData-b24a0b2b.js} +2 -2
  15. package/dist/{fallback-1ab5a270.js → fallback-ae0315d4.js} +2 -2
  16. package/dist/features-82db0d52.js +8 -0
  17. package/dist/{hashTypedData-1e059dc4.js → hashTypedData-c13ba550.js} +2 -2
  18. package/dist/{http-d9e0a50a.js → http-90ef23f3.js} +2 -2
  19. package/dist/{index-d3f36c31.js → index-0ac134af.js} +34 -34
  20. package/dist/{index-2cd6a2b2.js → index-19f302c4.js} +3 -3
  21. package/dist/{index-78c5b9e2.js → index-22727038.js} +3 -3
  22. package/dist/{index-144edd3f.js → index-270ff8a2.js} +11 -11
  23. package/dist/{index-abb0ff46.js → index-29eb95bd.js} +3 -3
  24. package/dist/{index-9cc1116b.js → index-2a90711e.js} +2 -2
  25. package/dist/{index-289a865f.js → index-2de6c5bf.js} +4 -4
  26. package/dist/{index-fbced89b.js → index-3de7fa96.js} +3 -3
  27. package/dist/{index-26b53c8a.js → index-42a8a5ec.js} +15 -15
  28. package/dist/{index-58ec291c.js → index-4bd03aed.js} +7 -7
  29. package/dist/{index-85bc86d7.js → index-52edc95a.js} +2 -2
  30. package/dist/{index-04dd1ab3.js → index-577d2709.js} +4 -4
  31. package/dist/{index-4a4ee1e7.js → index-582e4199.js} +7 -7
  32. package/dist/{index-72706598.js → index-617b2147.js} +6 -6
  33. package/dist/{index-0b6166c5.js → index-6a0902fc.js} +2 -2
  34. package/dist/{index-a37d4c03.js → index-6d51f24f.js} +2 -2
  35. package/dist/{index-759b09b0.js → index-80954332.js} +3 -3
  36. package/dist/{index-0e2c49ad.js → index-95777860.js} +7 -7
  37. package/dist/{index-d9effd37.js → index-96729706.js} +13 -13
  38. package/dist/{index-39f31546.js → index-997bc756.js} +2 -2
  39. package/dist/{index-cb4d3345.js → index-a41841e6.js} +3 -3
  40. package/dist/{index-c10f5e72.js → index-adaa2ea4.js} +3 -3
  41. package/dist/{index-97b58f89.js → index-b37b670f.js} +11 -11
  42. package/dist/{index-0763de43.js → index-b8e2c282.js} +3 -3
  43. package/dist/{index-1f02e7d0.js → index-bb5e4071.js} +1 -1
  44. package/dist/{index-3678e7d8.js → index-bf40b971.js} +2 -2
  45. package/dist/{index-a4f054e9.js → index-cb69565e.js} +2 -2
  46. package/dist/{index-197dfd69.js → index-cbb6096a.js} +3 -3
  47. package/dist/{index-119c358f.js → index-ce6854ef.js} +14 -14
  48. package/dist/{index-4fdc7a25.js → index-d7e2a4a3.js} +2 -2
  49. package/dist/{index-0176180b.js → index-e7fbb597.js} +4 -4
  50. package/dist/{index-d5e3e4ba.js → index-f4cd8dd1.js} +3 -3
  51. package/dist/{index-a0c590f7.js → index-f886cf92.js} +2 -2
  52. package/dist/{index-95e1a01b.js → index-faade258.js} +3 -3
  53. package/dist/{localBatchGatewayRequest-8fd1b198.js → localBatchGatewayRequest-ade4aa2f.js} +3 -3
  54. package/dist/{main-7ddb238e.js → main-3b573cfc.js} +38694 -38472
  55. package/dist/{networks-d2ae1da7.js → networks-446d3512.js} +10 -10
  56. package/dist/{onramp-2966f1b1.js → onramp-57d19c5d.js} +19 -19
  57. package/dist/{parseSignature-fca372a1.js → parseSignature-783d626a.js} +8 -8
  58. package/dist/{pay-with-exchange-b0198b3f.js → pay-with-exchange-c59a5773.js} +22 -22
  59. package/dist/ping-widget.js +1 -1
  60. package/dist/ping-widget.umd.cjs +680 -680
  61. package/dist/{query.lcd-873499ca.js → query.lcd-447fb91a.js} +1 -1
  62. package/dist/{query.rpc.Query-d2bcf632.js → query.rpc.Query-97503d87.js} +1 -1
  63. package/dist/{receive-6fbb6662.js → receive-3073a49d.js} +6 -6
  64. package/dist/{ref-2c86b029.js → ref-6c3aabc7.js} +2 -2
  65. package/dist/{send-630ed203.js → send-42403797.js} +22 -22
  66. package/dist/{serializeAuthorizationList-e95a85d8.js → serializeAuthorizationList-70ec977c.js} +4 -4
  67. package/dist/{sha2-c04ffd62.js → sha2-249ae7eb.js} +2 -2
  68. package/dist/{socials-73a5efb0.js → socials-69b0cf18.js} +18 -18
  69. package/dist/{swaps-7b57265f.js → swaps-4e99bb0f.js} +18 -18
  70. package/dist/{transactions-876c066c.js → transactions-1d0c2669.js} +9 -9
  71. package/dist/{tx.rpc.msg-4d8bd1a4.js → tx.rpc.msg-8aa727ea.js} +1 -1
  72. package/dist/{tx.rpc.msg-d08b8ebe.js → tx.rpc.msg-a26b3557.js} +1 -1
  73. package/dist/{verifyHash-ee28e3d7.js → verifyHash-c8962067.js} +5 -5
  74. package/dist/{vue-bea1a08b.js → vue-9dc93a14.js} +10 -10
  75. package/dist/{w3m-modal-7d61c210.js → w3m-modal-291afec4.js} +25 -25
  76. package/dist/{zora-1091e7bb.js → zora-061565b0.js} +4 -4
  77. package/lib/components/ConnectWallet/index.vue +113 -31
  78. package/lib/wallet/KeplrSessionManager.ts +142 -0
  79. package/lib/wallet/WalletSessionManager.ts +111 -25
  80. package/lib/wallet/reown/EvmNetworkProvisioner.ts +97 -0
  81. package/lib/wallet/reown/ReownGateway.ts +22 -3
  82. package/package.json +1 -1
  83. package/test/components/ConnectWallet.test.ts +640 -2
  84. package/test/components/ConnectWalletStorage.test.ts +113 -3
  85. package/test/wallet/KeplrSessionManager.test.ts +269 -0
  86. package/test/wallet/WalletSessionManager.test.ts +901 -2
  87. package/test/wallet/reown/EvmNetworkProvisioner.test.ts +264 -0
  88. package/test/wallet/reown/ReownGateway.test.ts +66 -1
  89. package/dist/features-a2f2ff4f.js +0 -8
@@ -1,7 +1,9 @@
1
- import { mount } from '@vue/test-utils';
1
+ import { flushPromises, mount } from '@vue/test-utils';
2
2
  import { beforeEach, describe, expect, it, vi } from 'vitest';
3
3
  import ConnectWallet from '../../lib/components/ConnectWallet/index.vue';
4
4
  import {
5
+ createWallet,
6
+ type ConnectedWallet,
5
7
  WalletName,
6
8
  readWallet,
7
9
  removeWallet,
@@ -15,6 +17,12 @@ const sessionMocks = vi.hoisted(() => ({
15
17
  start: vi.fn(async () => undefined),
16
18
  }));
17
19
 
20
+ const keplrSessionMocks = vi.hoisted(() => ({
21
+ paths: [] as string[],
22
+ dispose: vi.fn(),
23
+ start: vi.fn(async () => undefined),
24
+ }));
25
+
18
26
  vi.mock('../../lib/wallet/WalletSessionManager', () => ({
19
27
  WalletSessionManager: class WalletSessionManager {
20
28
  constructor(hdPath: string) {
@@ -27,6 +35,16 @@ vi.mock('../../lib/wallet/WalletSessionManager', () => ({
27
35
  },
28
36
  }));
29
37
 
38
+ vi.mock('../../lib/wallet/KeplrSessionManager', () => ({
39
+ KeplrSessionManager: class KeplrSessionManager {
40
+ constructor(hdPath: string) {
41
+ keplrSessionMocks.paths.push(hdPath);
42
+ }
43
+ dispose = keplrSessionMocks.dispose;
44
+ start = keplrSessionMocks.start;
45
+ },
46
+ }));
47
+
30
48
  vi.mock('../../lib/wallet/Wallet', () => ({
31
49
  WalletName: {
32
50
  Keplr: 'Keplr',
@@ -51,6 +69,7 @@ interface MountWalletOptions {
51
69
  hdPath?: string;
52
70
  evmConfig?: string;
53
71
  params?: string;
72
+ onDisconnect?: (payload: unknown) => void;
54
73
  }
55
74
 
56
75
  const evmConfig = JSON.stringify({
@@ -72,6 +91,16 @@ const evmConfig = JSON.stringify({
72
91
  featuredWalletIds: [],
73
92
  });
74
93
 
94
+ function deferred<T>() {
95
+ let resolve!: (value: T | PromiseLike<T>) => void;
96
+ let reject!: (reason?: unknown) => void;
97
+ const promise = new Promise<T>((resolvePromise, rejectPromise) => {
98
+ resolve = resolvePromise;
99
+ reject = rejectPromise;
100
+ });
101
+ return { promise, resolve, reject };
102
+ }
103
+
75
104
  function mountWallet(options: MountWalletOptions = {}) {
76
105
  return mount(ConnectWallet, {
77
106
  props: {
@@ -81,6 +110,9 @@ function mountWallet(options: MountWalletOptions = {}) {
81
110
  addrPrefix: 'warden',
82
111
  evmConfig: options.evmConfig,
83
112
  },
113
+ attrs: {
114
+ onDisconnect: options.onDisconnect,
115
+ },
84
116
  global: {
85
117
  stubs: {
86
118
  Icon: true,
@@ -93,6 +125,7 @@ describe('ConnectWallet', () => {
93
125
  beforeEach(() => {
94
126
  vi.clearAllMocks();
95
127
  sessionMocks.paths.length = 0;
128
+ keplrSessionMocks.paths.length = 0;
96
129
  vi.mocked(readWallet).mockReturnValue({} as ReturnType<typeof readWallet>);
97
130
  });
98
131
 
@@ -163,6 +196,211 @@ describe('ConnectWallet', () => {
163
196
  );
164
197
  });
165
198
 
199
+ it('revalidates a persisted Keplr session on mount', async () => {
200
+ const persisted = {
201
+ wallet: WalletName.Keplr,
202
+ cosmosAddress: 'cosmos1stored',
203
+ hdPath: "m/44'/118/0'/0/0",
204
+ };
205
+ vi.mocked(readWallet).mockReturnValue(persisted);
206
+ keplrSessionMocks.start.mockImplementationOnce(
207
+ async (_argument, onChange) => {
208
+ onChange(undefined);
209
+ onChange(persisted);
210
+ },
211
+ );
212
+
213
+ const wrapper = mountWallet();
214
+
215
+ await vi.waitFor(() =>
216
+ expect(keplrSessionMocks.start).toHaveBeenCalledWith(
217
+ expect.objectContaining({
218
+ chainId: 'barra_9191-1',
219
+ hdPath: persisted.hdPath,
220
+ prefix: 'warden',
221
+ }),
222
+ expect.any(Function),
223
+ expect.any(Function),
224
+ ),
225
+ );
226
+ expect(wrapper.emitted('disconnect')).toEqual([
227
+ [{ value: undefined }],
228
+ ]);
229
+ expect(wrapper.emitted('connect')).toEqual([
230
+ [{ value: persisted }],
231
+ ]);
232
+ });
233
+
234
+ it('routes manual Keplr connection through KeplrSessionManager', async () => {
235
+ const wrapper = mountWallet();
236
+ const option = wrapper
237
+ .findAll('li')
238
+ .find((item) => item.text().includes(WalletName.Keplr));
239
+
240
+ await option?.trigger('click');
241
+ await wrapper.get('.ping-connect-confirm').trigger('click');
242
+
243
+ expect(keplrSessionMocks.start).toHaveBeenCalledWith(
244
+ expect.objectContaining({
245
+ chainId: 'barra_9191-1',
246
+ hdPath: "m/44'/118/0'/0/0",
247
+ prefix: 'warden',
248
+ }),
249
+ expect.any(Function),
250
+ expect.any(Function),
251
+ );
252
+ });
253
+
254
+ it('keeps the Keplr dialog open when manual validation fails', async () => {
255
+ keplrSessionMocks.start.mockImplementationOnce(
256
+ async (_argument, _onChange, onError) => {
257
+ onError({ message: 'Keplr unavailable' });
258
+ },
259
+ );
260
+ const wrapper = mountWallet();
261
+
262
+ await wrapper.get('#PingConnectWallet').setValue(true);
263
+ await wrapper.get('.ping-connect-confirm').trigger('click');
264
+
265
+ expect(
266
+ (wrapper.get('#PingConnectWallet').element as HTMLInputElement).checked,
267
+ ).toBe(true);
268
+ expect(wrapper.text()).toContain('Keplr unavailable');
269
+ });
270
+
271
+ it('does not reopen the Keplr dialog for a later refresh failure', async () => {
272
+ let reportRefreshFailure!: (error: { message: string }) => void;
273
+ keplrSessionMocks.start.mockImplementationOnce(
274
+ async (_argument, _onChange, onError) => {
275
+ reportRefreshFailure = onError;
276
+ },
277
+ );
278
+ const wrapper = mountWallet();
279
+
280
+ await wrapper.get('#PingConnectWallet').setValue(true);
281
+ await wrapper.get('.ping-connect-confirm').trigger('click');
282
+ expect(
283
+ (wrapper.get('#PingConnectWallet').element as HTMLInputElement).checked,
284
+ ).toBe(false);
285
+
286
+ reportRefreshFailure({ message: 'Keplr refresh unavailable' });
287
+ await flushPromises();
288
+
289
+ expect(
290
+ (wrapper.get('#PingConnectWallet').element as HTMLInputElement).checked,
291
+ ).toBe(false);
292
+ expect(wrapper.text()).toContain('Keplr refresh unavailable');
293
+ });
294
+
295
+ it('isolates a replacement wallet from stale Keplr callbacks', async () => {
296
+ const wrapper = mountWallet();
297
+ await flushPromises();
298
+ sessionMocks.dispose.mockClear();
299
+ keplrSessionMocks.dispose.mockClear();
300
+ let staleChange!: (value: ConnectedWallet | undefined) => void;
301
+ let staleError!: (error: { message: string }) => void;
302
+ keplrSessionMocks.start.mockImplementationOnce(
303
+ async (_argument, onChange, onError) => {
304
+ staleChange = onChange;
305
+ staleError = onError;
306
+ onError({ message: 'Initial Keplr failure' });
307
+ },
308
+ );
309
+ vi.mocked(createWallet).mockReturnValue({
310
+ getAccounts: vi.fn(async () => [{
311
+ address: 'cosmos1ledger',
312
+ algo: 'secp256k1',
313
+ pubkey: new Uint8Array([1]),
314
+ }]),
315
+ } as unknown as ReturnType<typeof createWallet>);
316
+
317
+ await wrapper.get('.ping-connect-confirm').trigger('click');
318
+ const ledgerOption = wrapper
319
+ .findAll('li')
320
+ .find((item) => item.text().includes(WalletName.Ledger));
321
+ await ledgerOption?.trigger('click');
322
+ await wrapper.get('.ping-connect-confirm').trigger('click');
323
+ await vi.waitFor(() =>
324
+ expect(wrapper.text()).toContain('cosmos1ledger'),
325
+ );
326
+
327
+ staleChange({
328
+ wallet: WalletName.Keplr,
329
+ cosmosAddress: 'cosmos1stale',
330
+ hdPath: "m/44'/118/0'/0/0",
331
+ });
332
+ staleError({ message: 'Stale Keplr refresh' });
333
+ await flushPromises();
334
+
335
+ expect(keplrSessionMocks.dispose).toHaveBeenCalledTimes(2);
336
+ expect(sessionMocks.dispose).toHaveBeenCalledTimes(2);
337
+ expect(wrapper.text()).toContain('cosmos1ledger');
338
+ expect(wrapper.text()).not.toContain('cosmos1stale');
339
+ expect(wrapper.text()).not.toContain('Stale Keplr refresh');
340
+ });
341
+
342
+ it('isolates a replacement wallet from a pending Reown attempt', async () => {
343
+ const wrapper = mountWallet({
344
+ hdPath: "m/44'/60/0'/0/0",
345
+ evmConfig,
346
+ });
347
+ await flushPromises();
348
+ sessionMocks.dispose.mockClear();
349
+ keplrSessionMocks.dispose.mockClear();
350
+ const pending = deferred<void>();
351
+ let staleChange!: (value: ConnectedWallet | undefined) => void;
352
+ let staleError!: (error: { message: string }) => void;
353
+ sessionMocks.connect.mockImplementationOnce(
354
+ async (_config, _argument, onChange, onError) => {
355
+ staleChange = onChange;
356
+ staleError = onError;
357
+ await pending.promise;
358
+ },
359
+ );
360
+ vi.mocked(createWallet).mockReturnValue({
361
+ getAccounts: vi.fn(async () => [{
362
+ address: 'cosmos1ledger',
363
+ algo: 'secp256k1',
364
+ pubkey: new Uint8Array([1]),
365
+ }]),
366
+ } as unknown as ReturnType<typeof createWallet>);
367
+ const evmOption = wrapper
368
+ .findAll('li')
369
+ .find((item) => item.text().includes(WalletName.Evm));
370
+ await evmOption?.trigger('click');
371
+ await wrapper.get('.ping-connect-confirm').trigger('click');
372
+ await vi.waitFor(() => expect(sessionMocks.connect).toHaveBeenCalledOnce());
373
+
374
+ const ledgerOption = wrapper
375
+ .findAll('li')
376
+ .find((item) => item.text().includes(WalletName.Ledger));
377
+ await ledgerOption?.trigger('click');
378
+ await wrapper.get('.ping-connect-confirm').trigger('click');
379
+ await vi.waitFor(() =>
380
+ expect(wrapper.text()).toContain('cosmos1ledger'),
381
+ );
382
+
383
+ staleChange({
384
+ version: 2,
385
+ wallet: WalletName.Evm,
386
+ cosmosAddress: 'warden1stale',
387
+ hdPath: "m/44'/60/0'/0/0",
388
+ connector: {
389
+ source: 'reown',
390
+ name: 'Stale Wallet',
391
+ },
392
+ });
393
+ staleError({ message: 'Stale Reown callback' });
394
+ pending.reject(new Error('Stale Reown rejection'));
395
+ await flushPromises();
396
+
397
+ expect(keplrSessionMocks.dispose).toHaveBeenCalledTimes(2);
398
+ expect(sessionMocks.dispose).toHaveBeenCalledTimes(2);
399
+ expect(wrapper.text()).toContain('cosmos1ledger');
400
+ expect(wrapper.text()).not.toContain('warden1stale');
401
+ expect(wrapper.text()).not.toContain('Stale Reown');
402
+ });
403
+
166
404
  it('clears storage and emits the previous connection on disconnect', async () => {
167
405
  const connected = {
168
406
  wallet: WalletName.Keplr,
@@ -170,13 +408,172 @@ describe('ConnectWallet', () => {
170
408
  hdPath: "m/44'/118/0'/0/0",
171
409
  };
172
410
  vi.mocked(readWallet).mockReturnValue(connected);
411
+ keplrSessionMocks.start.mockImplementationOnce(
412
+ async (_argument, onChange) => {
413
+ onChange(undefined);
414
+ onChange(connected);
415
+ },
416
+ );
173
417
  const wrapper = mountWallet();
174
418
 
419
+ await vi.waitFor(() =>
420
+ expect(wrapper.find('[data-testid="wallet-disconnect"]').exists()).toBe(true),
421
+ );
175
422
  await wrapper.get('[data-testid="wallet-disconnect"]').trigger('click');
176
423
 
177
424
  expect(removeWallet).toHaveBeenCalledWith(connected.hdPath);
178
- expect(wrapper.emitted('disconnect')).toEqual([[{ value: connected }]]);
425
+ const disconnectEvents = wrapper.emitted('disconnect')!;
426
+ expect(disconnectEvents[0]).toEqual([{ value: undefined }]);
427
+ expect(disconnectEvents[disconnectEvents.length - 1]).toEqual([
428
+ { value: connected },
429
+ ]);
430
+ expect(wrapper.text()).toContain('Connect Wallet');
431
+ });
432
+
433
+ it('fails closed once when a direct EVM disconnect rejects', async () => {
434
+ const hdPath = "m/44'/60/0'/0/0";
435
+ const connected = {
436
+ version: 2 as const,
437
+ wallet: WalletName.Evm,
438
+ cosmosAddress: 'warden1connected',
439
+ hdPath,
440
+ connector: {
441
+ source: 'reown' as const,
442
+ name: 'Rabby Wallet',
443
+ },
444
+ };
445
+ vi.mocked(readWallet).mockReturnValue(connected);
446
+ sessionMocks.start.mockImplementationOnce(
447
+ async (_config, _argument, onChange) => onChange(connected),
448
+ );
449
+ const pending = deferred<void>();
450
+ sessionMocks.disconnect.mockImplementationOnce(async () => {
451
+ await pending.promise;
452
+ });
453
+ const onDisconnect = vi.fn();
454
+ const wrapper = mountWallet({ hdPath, evmConfig, onDisconnect });
455
+ await vi.waitFor(() =>
456
+ expect(wrapper.find('[data-testid="wallet-disconnect"]').exists()).toBe(true),
457
+ );
458
+ const disconnectCount = onDisconnect.mock.calls.length;
459
+
460
+ await wrapper.get('[data-testid="wallet-disconnect"]').trigger('click');
461
+ await vi.waitFor(() =>
462
+ expect(sessionMocks.disconnect).toHaveBeenCalledOnce(),
463
+ );
464
+ pending.reject(new Error('provider failure'));
465
+ await flushPromises();
466
+
467
+ expect(onDisconnect).toHaveBeenCalledTimes(disconnectCount + 1);
468
+ expect(onDisconnect).toHaveBeenLastCalledWith({ value: connected });
179
469
  expect(wrapper.text()).toContain('Connect Wallet');
470
+ expect(wrapper.text()).toContain('Wallet request failed');
471
+ wrapper.unmount();
472
+ });
473
+
474
+ it('ignores a stale direct disconnect after a replacement wallet connects', async () => {
475
+ const hdPath = "m/44'/60/0'/0/0";
476
+ const connected = {
477
+ version: 2 as const,
478
+ wallet: WalletName.Evm,
479
+ cosmosAddress: 'warden1connected',
480
+ hdPath,
481
+ connector: {
482
+ source: 'reown' as const,
483
+ name: 'Rabby Wallet',
484
+ },
485
+ };
486
+ vi.mocked(readWallet).mockReturnValue(connected);
487
+ sessionMocks.start.mockImplementationOnce(
488
+ async (_config, _argument, onChange) => onChange(connected),
489
+ );
490
+ const pending = deferred<void>();
491
+ sessionMocks.disconnect.mockImplementationOnce(async () => {
492
+ await pending.promise;
493
+ });
494
+ vi.mocked(createWallet).mockReturnValue({
495
+ getAccounts: vi.fn(async () => [{
496
+ address: 'cosmos1ledger',
497
+ algo: 'secp256k1',
498
+ pubkey: new Uint8Array([1]),
499
+ }]),
500
+ } as unknown as ReturnType<typeof createWallet>);
501
+ const onDisconnect = vi.fn();
502
+ const wrapper = mountWallet({ hdPath, evmConfig, onDisconnect });
503
+ await vi.waitFor(() =>
504
+ expect(wrapper.find('[data-testid="wallet-disconnect"]').exists()).toBe(true),
505
+ );
506
+
507
+ await wrapper.get('[data-testid="wallet-disconnect"]').trigger('click');
508
+ await vi.waitFor(() =>
509
+ expect(sessionMocks.disconnect).toHaveBeenCalledOnce(),
510
+ );
511
+ const ledgerOption = wrapper
512
+ .findAll('li')
513
+ .find((item) => item.text().includes(WalletName.Ledger));
514
+ await ledgerOption?.trigger('click');
515
+ await wrapper.get('.ping-connect-confirm').trigger('click');
516
+ await vi.waitFor(() =>
517
+ expect(wrapper.text()).toContain('cosmos1ledger'),
518
+ );
519
+ const disconnectCount = onDisconnect.mock.calls.length;
520
+
521
+ pending.reject(new Error('stale provider failure'));
522
+ await flushPromises();
523
+
524
+ expect(onDisconnect).toHaveBeenCalledTimes(disconnectCount);
525
+ expect(wrapper.text()).toContain('cosmos1ledger');
526
+ expect(wrapper.text()).not.toContain('Wallet request failed');
527
+ wrapper.unmount();
528
+ });
529
+
530
+ it('ignores a stale direct disconnect completion after unmount', async () => {
531
+ const hdPath = "m/44'/60/0'/0/0";
532
+ const connected = {
533
+ version: 2 as const,
534
+ wallet: WalletName.Evm,
535
+ cosmosAddress: 'warden1connected',
536
+ hdPath,
537
+ connector: {
538
+ source: 'reown' as const,
539
+ name: 'Rabby Wallet',
540
+ },
541
+ };
542
+ vi.mocked(readWallet).mockReturnValue(connected);
543
+ sessionMocks.start.mockImplementationOnce(
544
+ async (_config, _argument, onChange) => onChange(connected),
545
+ );
546
+ const pending = deferred<void>();
547
+ sessionMocks.disconnect.mockImplementationOnce(async () => {
548
+ await pending.promise;
549
+ });
550
+ const onDisconnect = vi.fn();
551
+ const wrapper = mountWallet({ hdPath, evmConfig, onDisconnect });
552
+ await vi.waitFor(() =>
553
+ expect(wrapper.find('[data-testid="wallet-disconnect"]').exists()).toBe(true),
554
+ );
555
+
556
+ await wrapper.get('[data-testid="wallet-disconnect"]').trigger('click');
557
+ await vi.waitFor(() =>
558
+ expect(sessionMocks.disconnect).toHaveBeenCalledOnce(),
559
+ );
560
+ const setupState = (wrapper.vm as unknown as {
561
+ $: {
562
+ setupState: {
563
+ connected: ConnectedWallet;
564
+ error: string;
565
+ };
566
+ };
567
+ }).$.setupState;
568
+ const disconnectCount = onDisconnect.mock.calls.length;
569
+ wrapper.unmount();
570
+
571
+ pending.reject(new Error('stale provider failure'));
572
+ await flushPromises();
573
+
574
+ expect(onDisconnect).toHaveBeenCalledTimes(disconnectCount);
575
+ expect(setupState.error).toBe('');
576
+ expect(setupState.connected.cosmosAddress).toBe('warden1connected');
180
577
  });
181
578
 
182
579
  it('disconnects a persisted EVM session after the explorer event', async () => {
@@ -209,6 +606,162 @@ describe('ConnectWallet', () => {
209
606
  wrapper.unmount();
210
607
  });
211
608
 
609
+ it('fails closed when external EVM disconnect rejects', async () => {
610
+ const hdPath = "m/44'/60/0'/0/0";
611
+ const connected = {
612
+ version: 2 as const,
613
+ wallet: WalletName.Evm,
614
+ cosmosAddress: 'warden1connected',
615
+ hdPath,
616
+ connector: {
617
+ source: 'reown' as const,
618
+ name: 'Rabby Wallet',
619
+ },
620
+ };
621
+ vi.mocked(readWallet).mockReturnValue(connected);
622
+ sessionMocks.start.mockImplementationOnce(
623
+ async (_config, _argument, onChange) => onChange(connected),
624
+ );
625
+ sessionMocks.disconnect.mockRejectedValueOnce(
626
+ new Error('provider failure'),
627
+ );
628
+ const onDisconnect = vi.fn();
629
+ const wrapper = mountWallet({ hdPath, evmConfig, onDisconnect });
630
+ await vi.waitFor(() =>
631
+ expect(wrapper.find('[data-testid="wallet-disconnect"]').exists()).toBe(true),
632
+ );
633
+ const disconnectCount = onDisconnect.mock.calls.length;
634
+
635
+ window.dispatchEvent(
636
+ new CustomEvent('mictonode:wallet-disconnect', {
637
+ detail: { hdPath },
638
+ }),
639
+ );
640
+ await vi.waitFor(() =>
641
+ expect(sessionMocks.disconnect).toHaveBeenCalledOnce(),
642
+ );
643
+ await flushPromises();
644
+
645
+ expect(onDisconnect).toHaveBeenCalledTimes(disconnectCount + 1);
646
+ expect(onDisconnect).toHaveBeenLastCalledWith({ value: undefined });
647
+ expect(wrapper.text()).toContain('Connect Wallet');
648
+ expect(wrapper.text()).toContain('Wallet request failed');
649
+ wrapper.unmount();
650
+ });
651
+
652
+ it('ignores a stale external disconnect after a replacement wallet connects', async () => {
653
+ const hdPath = "m/44'/60/0'/0/0";
654
+ const connected = {
655
+ version: 2 as const,
656
+ wallet: WalletName.Evm,
657
+ cosmosAddress: 'warden1connected',
658
+ hdPath,
659
+ connector: {
660
+ source: 'reown' as const,
661
+ name: 'Rabby Wallet',
662
+ },
663
+ };
664
+ vi.mocked(readWallet).mockReturnValue(connected);
665
+ sessionMocks.start.mockImplementationOnce(
666
+ async (_config, _argument, onChange) => onChange(connected),
667
+ );
668
+ const pending = deferred<void>();
669
+ sessionMocks.disconnect.mockImplementationOnce(async () => {
670
+ await pending.promise;
671
+ });
672
+ vi.mocked(createWallet).mockReturnValue({
673
+ getAccounts: vi.fn(async () => [{
674
+ address: 'cosmos1ledger',
675
+ algo: 'secp256k1',
676
+ pubkey: new Uint8Array([1]),
677
+ }]),
678
+ } as unknown as ReturnType<typeof createWallet>);
679
+ const onDisconnect = vi.fn();
680
+ const wrapper = mountWallet({ hdPath, evmConfig, onDisconnect });
681
+ await vi.waitFor(() =>
682
+ expect(wrapper.find('[data-testid="wallet-disconnect"]').exists()).toBe(true),
683
+ );
684
+
685
+ window.dispatchEvent(
686
+ new CustomEvent('mictonode:wallet-disconnect', {
687
+ detail: { hdPath },
688
+ }),
689
+ );
690
+ await vi.waitFor(() =>
691
+ expect(sessionMocks.disconnect).toHaveBeenCalledOnce(),
692
+ );
693
+ const ledgerOption = wrapper
694
+ .findAll('li')
695
+ .find((item) => item.text().includes(WalletName.Ledger));
696
+ await ledgerOption?.trigger('click');
697
+ await wrapper.get('.ping-connect-confirm').trigger('click');
698
+ await vi.waitFor(() =>
699
+ expect(wrapper.text()).toContain('cosmos1ledger'),
700
+ );
701
+ const disconnectCount = onDisconnect.mock.calls.length;
702
+
703
+ pending.reject(new Error('stale provider failure'));
704
+ await flushPromises();
705
+
706
+ expect(onDisconnect).toHaveBeenCalledTimes(disconnectCount);
707
+ expect(wrapper.text()).toContain('cosmos1ledger');
708
+ expect(wrapper.text()).not.toContain('Wallet request failed');
709
+ wrapper.unmount();
710
+ });
711
+
712
+ it('ignores a stale external disconnect completion after unmount', async () => {
713
+ const hdPath = "m/44'/60/0'/0/0";
714
+ const connected = {
715
+ version: 2 as const,
716
+ wallet: WalletName.Evm,
717
+ cosmosAddress: 'warden1connected',
718
+ hdPath,
719
+ connector: {
720
+ source: 'reown' as const,
721
+ name: 'Rabby Wallet',
722
+ },
723
+ };
724
+ vi.mocked(readWallet).mockReturnValue(connected);
725
+ sessionMocks.start.mockImplementationOnce(
726
+ async (_config, _argument, onChange) => onChange(connected),
727
+ );
728
+ const pending = deferred<void>();
729
+ sessionMocks.disconnect.mockImplementationOnce(async () => {
730
+ await pending.promise;
731
+ });
732
+ const onDisconnect = vi.fn();
733
+ const wrapper = mountWallet({ hdPath, evmConfig, onDisconnect });
734
+ await vi.waitFor(() =>
735
+ expect(wrapper.find('[data-testid="wallet-disconnect"]').exists()).toBe(true),
736
+ );
737
+
738
+ window.dispatchEvent(
739
+ new CustomEvent('mictonode:wallet-disconnect', {
740
+ detail: { hdPath },
741
+ }),
742
+ );
743
+ await vi.waitFor(() =>
744
+ expect(sessionMocks.disconnect).toHaveBeenCalledOnce(),
745
+ );
746
+ const setupState = (wrapper.vm as unknown as {
747
+ $: {
748
+ setupState: {
749
+ connected: ConnectedWallet;
750
+ error: string;
751
+ };
752
+ };
753
+ }).$.setupState;
754
+ const disconnectCount = onDisconnect.mock.calls.length;
755
+ wrapper.unmount();
756
+
757
+ pending.reject(new Error('stale provider failure'));
758
+ await flushPromises();
759
+
760
+ expect(onDisconnect).toHaveBeenCalledTimes(disconnectCount);
761
+ expect(setupState.error).toBe('');
762
+ expect(setupState.connected.cosmosAddress).toBe('warden1connected');
763
+ });
764
+
212
765
  it('rebinds the session manager when the active chain HD path changes', async () => {
213
766
  const evmHdPath = "m/44'/60/0'/0/0";
214
767
  const evmWallet = {
@@ -248,4 +801,89 @@ describe('ConnectWallet', () => {
248
801
  );
249
802
  expect(sessionMocks.dispose).toHaveBeenCalled();
250
803
  });
804
+
805
+ it('disposes both manager types when wallet context changes', async () => {
806
+ const wrapper = mountWallet();
807
+
808
+ await wrapper.setProps({
809
+ chainId: 'cosmoshub-4',
810
+ addrPrefix: 'cosmos',
811
+ });
812
+ await vi.waitFor(() => {
813
+ expect(keplrSessionMocks.dispose).toHaveBeenCalled();
814
+ expect(sessionMocks.dispose).toHaveBeenCalled();
815
+ });
816
+ });
817
+
818
+ it('ignores a stale Reown restore rejection after context replacement', async () => {
819
+ const oldHdPath = "m/44'/60/0'/0/0";
820
+ const newHdPath = "m/44'/118/0'/0/0";
821
+ const oldConnection = {
822
+ version: 2 as const,
823
+ wallet: WalletName.Evm,
824
+ cosmosAddress: 'warden1old',
825
+ hdPath: oldHdPath,
826
+ connector: {
827
+ source: 'reown' as const,
828
+ name: 'Rabby Wallet',
829
+ },
830
+ };
831
+ const replacement = {
832
+ wallet: WalletName.Ledger,
833
+ cosmosAddress: 'cosmos1replacement',
834
+ hdPath: newHdPath,
835
+ };
836
+ vi.mocked(readWallet).mockImplementation((path) =>
837
+ path === oldHdPath ? oldConnection : replacement,
838
+ );
839
+ const pending = deferred<void>();
840
+ sessionMocks.start.mockImplementationOnce(() => pending.promise);
841
+ const wrapper = mountWallet({ hdPath: oldHdPath, evmConfig });
842
+ await vi.waitFor(() => expect(sessionMocks.start).toHaveBeenCalledOnce());
843
+
844
+ await wrapper.setProps({
845
+ hdPath: newHdPath,
846
+ chainId: 'cosmoshub-4',
847
+ addrPrefix: 'cosmos',
848
+ evmConfig: undefined,
849
+ });
850
+ await vi.waitFor(() =>
851
+ expect(wrapper.text()).toContain('cosmos1replacement'),
852
+ );
853
+ vi.mocked(removeWallet).mockClear();
854
+
855
+ pending.reject(new Error('stale Reown restore'));
856
+ await flushPromises();
857
+
858
+ expect(removeWallet).not.toHaveBeenCalled();
859
+ expect(wrapper.text()).toContain('cosmos1replacement');
860
+ });
861
+
862
+ it('ignores a stale Reown restore rejection after unmount', async () => {
863
+ const hdPath = "m/44'/60/0'/0/0";
864
+ vi.mocked(readWallet).mockReturnValue({
865
+ version: 2,
866
+ wallet: WalletName.Evm,
867
+ cosmosAddress: 'warden1old',
868
+ hdPath,
869
+ connector: {
870
+ source: 'reown',
871
+ name: 'Rabby Wallet',
872
+ },
873
+ });
874
+ const pending = deferred<void>();
875
+ sessionMocks.start.mockImplementationOnce(() => pending.promise);
876
+ const onDisconnect = vi.fn();
877
+ const wrapper = mountWallet({ hdPath, evmConfig, onDisconnect });
878
+ await vi.waitFor(() => expect(sessionMocks.start).toHaveBeenCalledOnce());
879
+ const disconnectCount = onDisconnect.mock.calls.length;
880
+ vi.mocked(removeWallet).mockClear();
881
+
882
+ wrapper.unmount();
883
+ pending.reject(new Error('stale Reown restore'));
884
+ await flushPromises();
885
+
886
+ expect(removeWallet).not.toHaveBeenCalled();
887
+ expect(onDisconnect).toHaveBeenCalledTimes(disconnectCount);
888
+ });
251
889
  });