@onekeyfe/hd-core 1.2.3-alpha.2 → 1.2.3-alpha.4

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 (57) hide show
  1. package/README.md +1 -1
  2. package/__tests__/AllNetworkGetAddressBase.tracing.test.ts +503 -10
  3. package/__tests__/core-error-output.test.ts +169 -1
  4. package/__tests__/device-lifecycle-events.test.ts +350 -2
  5. package/__tests__/open-wallet-session-error-response.test.ts +2 -2
  6. package/__tests__/open-wallet-session.test.ts +8 -411
  7. package/__tests__/protocol-v2.test.ts +51 -0
  8. package/__tests__/public-device-state-api.test.ts +2 -7
  9. package/__tests__/search-devices.test.ts +196 -8
  10. package/dist/api/GetFeatures.d.ts.map +1 -1
  11. package/dist/api/GetPassphraseState.d.ts.map +1 -1
  12. package/dist/api/OpenWalletSession.d.ts.map +1 -1
  13. package/dist/api/SearchDevices.d.ts +2 -15
  14. package/dist/api/SearchDevices.d.ts.map +1 -1
  15. package/dist/api/allnetwork/AllNetworkGetAddress.d.ts +2 -0
  16. package/dist/api/allnetwork/AllNetworkGetAddress.d.ts.map +1 -1
  17. package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts +7 -1
  18. package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts.map +1 -1
  19. package/dist/api/allnetwork/AllNetworkGetAddressByLoop.d.ts.map +1 -1
  20. package/dist/api/device/DeviceVerify.d.ts.map +1 -1
  21. package/dist/core/RequestQueue.d.ts +1 -0
  22. package/dist/core/RequestQueue.d.ts.map +1 -1
  23. package/dist/core/index.d.ts +1 -0
  24. package/dist/core/index.d.ts.map +1 -1
  25. package/dist/data-manager/TransportManager.d.ts +2 -0
  26. package/dist/data-manager/TransportManager.d.ts.map +1 -1
  27. package/dist/device/Device.d.ts +2 -0
  28. package/dist/device/Device.d.ts.map +1 -1
  29. package/dist/index.d.ts +17 -19
  30. package/dist/index.js +449 -184
  31. package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
  32. package/dist/types/api/getFeatures.d.ts.map +1 -1
  33. package/dist/types/api/getPassphraseState.d.ts.map +1 -1
  34. package/dist/types/api/openWalletSession.d.ts +1 -10
  35. package/dist/types/api/openWalletSession.d.ts.map +1 -1
  36. package/dist/types/params.d.ts.map +1 -1
  37. package/dist/utils/patch.d.ts +1 -1
  38. package/dist/utils/patch.d.ts.map +1 -1
  39. package/package.json +4 -4
  40. package/src/api/GetFeatures.ts +1 -0
  41. package/src/api/GetPassphraseState.ts +1 -0
  42. package/src/api/OpenWalletSession.ts +7 -77
  43. package/src/api/SearchDevices.ts +121 -27
  44. package/src/api/allnetwork/AllNetworkGetAddress.ts +79 -45
  45. package/src/api/allnetwork/AllNetworkGetAddressBase.ts +95 -24
  46. package/src/api/allnetwork/AllNetworkGetAddressByLoop.ts +3 -0
  47. package/src/api/device/DeviceVerify.ts +8 -0
  48. package/src/core/RequestQueue.ts +20 -0
  49. package/src/core/index.ts +135 -43
  50. package/src/data/messages/messages-protocol-v2.json +21 -0
  51. package/src/data-manager/TransportManager.ts +14 -3
  52. package/src/device/Device.ts +58 -27
  53. package/src/protocols/protocol-v2/walletSession.ts +7 -0
  54. package/src/types/api/getFeatures.ts +2 -1
  55. package/src/types/api/getPassphraseState.ts +2 -5
  56. package/src/types/api/openWalletSession.ts +7 -19
  57. package/src/types/params.ts +7 -0
package/README.md CHANGED
@@ -26,7 +26,7 @@ import Core from '@onekeyfe/hd-core';
26
26
 
27
27
  ## Docs
28
28
 
29
- Documentation is available [hardware-js-sdk](https://developer.onekey.so/connect-to-hardware/hardware-sdk/start)
29
+ Documentation is available [Hardware SDK Getting Started](https://developer.onekey.so/en/hardware-sdk/getting-started)
30
30
 
31
31
  ## Examples
32
32
  // TODO: add example url
@@ -1,5 +1,10 @@
1
+ import { EDeviceType, HardwareErrorCode, HardwareErrorCodeMessage } from '@onekeyfe/hd-shared';
2
+
1
3
  import AllNetworkGetAddressBase from '../src/api/allnetwork/AllNetworkGetAddressBase';
2
4
  import AllNetworkGetAddress from '../src/api/allnetwork/AllNetworkGetAddress';
5
+ import AllNetworkGetAddressByLoop from '../src/api/allnetwork/AllNetworkGetAddressByLoop';
6
+ import EvmGetAddress from '../src/api/evm/EVMGetAddress';
7
+ import { UI_REQUEST } from '../src/constants/ui-request';
3
8
  import { findMethod } from '../src/api/utils';
4
9
  import { getActiveRequestsByDeviceInstance } from '../src/utils/tracing';
5
10
 
@@ -18,11 +23,161 @@ class TestAllNetworkMethod extends AllNetworkGetAddressBase {
18
23
  }
19
24
  }
20
25
 
26
+ describe.each([EDeviceType.Pro2, EDeviceType.Neo])('%s loading lifecycle', deviceType => {
27
+ function setup(callback = false) {
28
+ const Method = callback ? AllNetworkGetAddressByLoop : AllNetworkGetAddress;
29
+ const method = new Method({
30
+ id: 1,
31
+ payload: {
32
+ method: callback ? 'allNetworkGetAddressByLoop' : 'allNetworkGetAddress',
33
+ connectId: 'test-device',
34
+ callbackId: 'item',
35
+ callbackIdFinish: 'finish',
36
+ bundle: [
37
+ { network: 'evm', path: "m/44'/60'/0'/0/0" },
38
+ { network: 'sol', path: "m/44'/501'/0'" },
39
+ ],
40
+ },
41
+ });
42
+ const calls: string[] = [];
43
+ const typedCall = jest.fn((type: string, _response: string, message: { action?: number }) => {
44
+ calls.push(type === 'DeviceAnimationControl' ? `animation:${message.action}` : type);
45
+ return Promise.resolve({ message: { root_fingerprint: 0 } });
46
+ });
47
+ method.device = {
48
+ commands: { typedCall, disposed: false },
49
+ isProtocolV2: jest.fn(() => true),
50
+ getCurrentDeviceType: jest.fn(() => deviceType),
51
+ ensureProtocolV2RuntimeContext: jest.fn(() =>
52
+ Promise.resolve({ supported_messages: [60461] })
53
+ ),
54
+ } as any;
55
+ method.postMessage = jest.fn();
56
+ method.context = {
57
+ registerCallbackTask: jest.fn(),
58
+ cancelCallbackTasks: jest.fn(() => method.device.pendingCallbackPromise?.resolve()),
59
+ } as any;
60
+ const callMethod = jest.spyOn(method, 'callMethod').mockImplementation(name => {
61
+ calls.push(name);
62
+ return Promise.resolve([{ success: true, payload: {} }] as any);
63
+ });
64
+ return { method, calls, typedCall, callMethod };
65
+ }
66
+
67
+ test('keeps one session across fingerprint and all chain requests', async () => {
68
+ const { method, calls } = setup();
69
+ await method.run();
70
+ expect(calls).toEqual([
71
+ 'animation:1',
72
+ 'GetPublicKey',
73
+ 'evmGetAddress',
74
+ 'solGetAddress',
75
+ 'animation:2',
76
+ ]);
77
+ });
78
+
79
+ test.each(['V1', 'unsupported', 'Pro'])('does not change %s device behavior', async variant => {
80
+ const { method, calls } = setup();
81
+ if (variant === 'V1') jest.spyOn(method.device, 'isProtocolV2').mockReturnValue(false);
82
+ if (variant === 'Pro')
83
+ jest.spyOn(method.device, 'getCurrentDeviceType').mockReturnValue(EDeviceType.Pro);
84
+ if (variant === 'unsupported') {
85
+ jest
86
+ .spyOn(method.device, 'ensureProtocolV2RuntimeContext')
87
+ .mockResolvedValue({ supported_messages: [] } as any);
88
+ }
89
+ await method.run();
90
+ expect(calls).toEqual(['GetPublicKey', 'evmGetAddress', 'solGetAddress']);
91
+ });
92
+
93
+ test('does not reconnect for cleanup after chain failure', async () => {
94
+ const { method, calls, callMethod } = setup();
95
+ const error = new Error('chain failed');
96
+ callMethod.mockRejectedValue(error);
97
+ await expect(method.run()).rejects.toBe(error);
98
+ expect(calls).toEqual(['animation:1', 'GetPublicKey']);
99
+ });
100
+
101
+ test('does not replace a successful result when Stop fails', async () => {
102
+ const { method, calls, typedCall } = setup();
103
+ typedCall.mockResolvedValueOnce({ message: { root_fingerprint: 0 } });
104
+ typedCall.mockResolvedValueOnce({ message: { root_fingerprint: 0 } });
105
+ typedCall.mockImplementationOnce(() => {
106
+ calls.push('stop-failed');
107
+ return Promise.reject(new Error('cleanup failed'));
108
+ });
109
+ await expect(method.run()).resolves.toHaveLength(2);
110
+ expect(calls).toEqual(['evmGetAddress', 'solGetAddress', 'stop-failed']);
111
+ });
112
+
113
+ test('does not continue wallet commands when starting the session fails', async () => {
114
+ const { method, typedCall, callMethod } = setup();
115
+ const error = new Error('link disconnected');
116
+ typedCall.mockRejectedValueOnce(error);
117
+ await expect(method.run()).rejects.toBe(error);
118
+ expect(typedCall).toHaveBeenCalledTimes(1);
119
+ expect(callMethod).not.toHaveBeenCalled();
120
+ });
121
+
122
+ test.each(['success', 'failure', 'cancel'])(
123
+ 'keeps callback loading until background %s',
124
+ async outcome => {
125
+ const { method, calls, callMethod } = setup(true);
126
+ let finishChain: () => void = () => {};
127
+ const chainPending = new Promise<void>(resolve => {
128
+ finishChain = resolve;
129
+ });
130
+ callMethod.mockImplementationOnce(async () => {
131
+ calls.push('chain-pending');
132
+ await chainPending;
133
+ if (outcome === 'failure') throw new Error('chain failed');
134
+ if (outcome === 'cancel') method.abortController?.abort();
135
+ return [{ success: true, payload: {} }] as any;
136
+ });
137
+ await expect(method.run()).resolves.toEqual([]);
138
+ expect(calls).toEqual(['animation:1', 'GetPublicKey', 'chain-pending']);
139
+ finishChain();
140
+ await method.device.pendingCallbackPromise?.promise;
141
+ expect(calls.filter(call => call === 'animation:2')).toHaveLength(
142
+ outcome === 'success' ? 1 : 0
143
+ );
144
+ expect(method.postMessage).toHaveBeenCalledWith(
145
+ expect.objectContaining({
146
+ payload: expect.objectContaining({ callbackId: 'finish' }),
147
+ })
148
+ );
149
+ }
150
+ );
151
+
152
+ test('does not send further commands when the fingerprint response is invalid', async () => {
153
+ const { method, typedCall, callMethod } = setup();
154
+ typedCall.mockResolvedValueOnce({ message: { root_fingerprint: 0 } });
155
+ typedCall.mockResolvedValueOnce({ message: {} } as any);
156
+ await expect(method.run()).rejects.toThrow();
157
+ expect(callMethod).not.toHaveBeenCalled();
158
+ expect(typedCall).toHaveBeenCalledTimes(2);
159
+ });
160
+
161
+ test.each(['disposed', 'replaced'])(
162
+ 'does not send cleanup through a %s connection',
163
+ async state => {
164
+ const { method, calls, callMethod } = setup();
165
+ callMethod.mockImplementation(() => {
166
+ if (state === 'disposed') method.device.commands.disposed = true;
167
+ else method.device.commands = { typedCall: jest.fn() } as any;
168
+ return Promise.resolve([{ success: true, payload: {} }] as any);
169
+ });
170
+ await method.run();
171
+ expect(calls).toEqual(['animation:1', 'GetPublicKey']);
172
+ }
173
+ );
174
+ });
175
+
21
176
  describe('AllNetworkGetAddressBase tracing', () => {
22
177
  test('resumes a Protocol V2 hidden wallet before running a nested chain method', async () => {
23
178
  const calls: string[] = [];
24
179
  const checkPassphraseStateSafety = jest.fn().mockImplementation(() => {
25
- calls.push('resume-hidden-session');
180
+ calls.push('restore-wallet-session');
26
181
  return Promise.resolve(true);
27
182
  });
28
183
  const innerMethod = {
@@ -98,7 +253,7 @@ describe('AllNetworkGetAddressBase tracing', () => {
98
253
  undefined,
99
254
  undefined
100
255
  );
101
- expect(calls).toEqual(['resume-hidden-session', 'run-chain-method']);
256
+ expect(calls).toEqual(['restore-wallet-session', 'run-chain-method']);
102
257
  expect(typedCall).not.toHaveBeenCalled();
103
258
  });
104
259
 
@@ -271,7 +426,305 @@ describe('AllNetworkGetAddressBase tracing', () => {
271
426
  expect(typedCall).not.toHaveBeenCalled();
272
427
  });
273
428
 
274
- test('runs Protocol V2 addresses one at a time so each command receives a wallet session', async () => {
429
+ function createV2NestedHarness(payload: Record<string, unknown>) {
430
+ const calls: string[] = [];
431
+ const checkPassphraseStateSafety = jest
432
+ .fn()
433
+ .mockImplementation((_state, _empty, _skip, deriveCardano) => {
434
+ calls.push(deriveCardano ? 'resume-cardano-session' : 'restore-wallet-session');
435
+ return Promise.resolve(true);
436
+ });
437
+ const method = new TestAllNetworkMethod({
438
+ id: 10,
439
+ payload: {
440
+ method: 'allNetworkGetAddress',
441
+ connectId: 'connect-id',
442
+ deviceId: 'device-id',
443
+ bundle: [],
444
+ ...payload,
445
+ },
446
+ });
447
+ method.protocolV2UnlockContext = { preflightCompleted: true };
448
+ method.device = {
449
+ checkPassphraseStateSafety,
450
+ commands: {
451
+ typedCall: jest.fn(),
452
+ },
453
+ getCurrentFirmwareType: jest.fn(),
454
+ getProtocol: jest.fn().mockReturnValue('V2'),
455
+ getCurrentFirmwareVersionString: jest.fn().mockReturnValue('1.0.0'),
456
+ getCurrentMethodVersionRange: jest
457
+ .fn()
458
+ .mockImplementation((getRange: (type: string) => unknown) => getRange('pro2')),
459
+ instanceId: 'device-instance',
460
+ isProtocolV2: jest.fn().mockReturnValue(true),
461
+ isBootloader: jest.fn().mockReturnValue(false),
462
+ isRomloader: jest.fn().mockReturnValue(false),
463
+ off: jest.fn(),
464
+ on: jest.fn(),
465
+ state: { status: { unlocked: true } },
466
+ updateProtocolV2Status: jest.fn(),
467
+ } as any;
468
+ return { calls, checkPassphraseStateSafety, method };
469
+ }
470
+
471
+ function mockInnerChainMethod(name: string, onRun: () => void) {
472
+ return {
473
+ checkSafetyLevelOnTestNet: jest.fn().mockResolvedValue(false),
474
+ connectId: 'connect-id',
475
+ deviceId: 'device-id',
476
+ getVersionRange: jest.fn().mockReturnValue({}),
477
+ assertProtocolSupported: jest.fn(),
478
+ init: jest.fn(),
479
+ name,
480
+ responseID: 50,
481
+ unlockPolicy: 'unlock-before-run',
482
+ run: jest.fn().mockImplementation(() => {
483
+ onRun();
484
+ return Promise.resolve([{ address: `${name}-address` }]);
485
+ }),
486
+ setDevice: jest.fn(),
487
+ strictCheckDeviceSupport: false,
488
+ };
489
+ }
490
+
491
+ function createGroupedAddressHarness(showOnOneKey?: boolean) {
492
+ const { method: nestedHarness, checkPassphraseStateSafety } = createV2NestedHarness({});
493
+ const bundle = [0, 1, 2].map(index => ({
494
+ network: 'evm',
495
+ path: `m/44'/60'/${index}'/0/0`,
496
+ showOnOneKey,
497
+ }));
498
+ const method = new AllNetworkGetAddress({
499
+ id: 11,
500
+ payload: {
501
+ method: 'allNetworkGetAddress',
502
+ connectId: 'connect-id',
503
+ deviceId: 'device-id',
504
+ useEmptyPassphrase: true,
505
+ bundle,
506
+ },
507
+ });
508
+ method.protocolV2UnlockContext = nestedHarness.protocolV2UnlockContext;
509
+ method.abortController = new AbortController();
510
+ method.device = nestedHarness.device;
511
+ method.device.getCurrentDeviceType = jest.fn().mockReturnValue(EDeviceType.Pro2);
512
+ method.device.toMessageObject = jest.fn().mockReturnValue({});
513
+ method.postMessage = jest.fn();
514
+ const typedCall = jest
515
+ .fn()
516
+ .mockImplementation((_type: string, _response: string, params: { address_n: number[] }) => {
517
+ const index = params.address_n[2] - 0x80000000;
518
+ if (index === 1) return Promise.reject(new Error('Forbidden key path'));
519
+ return Promise.resolve({ message: { address: `address-${index}` } });
520
+ });
521
+ method.device.commands.typedCall = typedCall;
522
+ (findMethod as jest.Mock).mockImplementation(message => new EvmGetAddress(message));
523
+ method.init();
524
+ return { method, typedCall, checkPassphraseStateSafety, bundle };
525
+ }
526
+
527
+ test.each([false, true, undefined])(
528
+ 'isolates a V2 address failure without repeating device confirmations (showOnOneKey=%s)',
529
+ async showOnOneKey => {
530
+ const { method, typedCall, checkPassphraseStateSafety, bundle } =
531
+ createGroupedAddressHarness(showOnOneKey);
532
+
533
+ const result = await method.getAllNetworkAddress(7);
534
+
535
+ expect(result.map(item => item.success)).toEqual([true, false, true]);
536
+ expect(result.map(item => item.path)).toEqual(bundle.map(item => item.path));
537
+ expect(result[0].payload).toMatchObject({ address: 'address-0', rootFingerprint: 7 });
538
+ expect(result[1].payload).toMatchObject({
539
+ code: HardwareErrorCode.CallMethodInvalidParameter,
540
+ });
541
+ expect(result[2].payload).toMatchObject({ address: 'address-2', rootFingerprint: 7 });
542
+ expect(typedCall.mock.calls.map(([, , params]) => params.address_n[2] - 0x80000000)).toEqual(
543
+ showOnOneKey === false ? [0, 1, 0, 1, 2] : [0, 1, 2]
544
+ );
545
+ expect(checkPassphraseStateSafety).toHaveBeenCalledTimes(1);
546
+ expect(
547
+ jest
548
+ .mocked(method.postMessage)
549
+ .mock.calls.flatMap(([message]) =>
550
+ message.type === UI_REQUEST.PREVIOUS_ADDRESS_RESULT ? [message.payload.data.path] : []
551
+ )
552
+ ).toEqual([bundle[0].path, bundle[2].path]);
553
+ expect(method.postMessage).toHaveBeenCalledWith(
554
+ expect.objectContaining({ type: UI_REQUEST.DEVICE_PROGRESS, payload: { progress: 100 } })
555
+ );
556
+ expect(getActiveRequestsByDeviceInstance('device-instance')).toEqual([]);
557
+ }
558
+ );
559
+
560
+ test('preserves repeated input addresses when suppressing V2 retry notifications', async () => {
561
+ const { method, bundle } = createGroupedAddressHarness(false);
562
+ method.payload.bundle = [bundle[0], bundle[0], bundle[1], bundle[0], bundle[2]];
563
+
564
+ const result = await method.getAllNetworkAddress(7);
565
+
566
+ expect(result.map(item => item.success)).toEqual([true, true, false, true, true]);
567
+ expect(
568
+ jest
569
+ .mocked(method.postMessage)
570
+ .mock.calls.flatMap(([message]) =>
571
+ message.type === UI_REQUEST.PREVIOUS_ADDRESS_RESULT ? [message.payload.data.path] : []
572
+ )
573
+ ).toEqual([bundle[0].path, bundle[0].path, bundle[0].path, bundle[2].path]);
574
+ });
575
+
576
+ test('forwards successful V2 address notifications before the batch finishes', async () => {
577
+ const { method, typedCall, bundle } = createGroupedAddressHarness(false);
578
+ typedCall.mockImplementation((_type, _response, params) => {
579
+ const index = params.address_n[2] - 0x80000000;
580
+ expect(
581
+ jest
582
+ .mocked(method.postMessage)
583
+ .mock.calls.filter(([message]) => message.type === UI_REQUEST.PREVIOUS_ADDRESS_RESULT)
584
+ ).toHaveLength(index);
585
+ return Promise.resolve({ message: { address: `address-${index}` } });
586
+ });
587
+
588
+ const result = await method.getAllNetworkAddress(7);
589
+
590
+ expect(result.map(item => item.success)).toEqual([true, true, true]);
591
+ expect(typedCall).toHaveBeenCalledTimes(3);
592
+ expect(
593
+ jest
594
+ .mocked(method.postMessage)
595
+ .mock.calls.flatMap(([message]) =>
596
+ message.type === UI_REQUEST.PREVIOUS_ADDRESS_RESULT ? [message.payload.data.path] : []
597
+ )
598
+ ).toEqual(bundle.map(item => item.path));
599
+ });
600
+
601
+ test('does not retry a failed V2 link as individual address requests', async () => {
602
+ const { method, typedCall } = createGroupedAddressHarness(false);
603
+ const error = new Error('link disconnected');
604
+ typedCall.mockRejectedValueOnce(error);
605
+
606
+ await expect(method.getAllNetworkAddress(7)).rejects.toBe(error);
607
+
608
+ expect(typedCall).toHaveBeenCalledTimes(1);
609
+ });
610
+
611
+ test('does not retry a V2 wallet mismatch as individual address requests', async () => {
612
+ const { method, typedCall, checkPassphraseStateSafety } = createGroupedAddressHarness(false);
613
+ checkPassphraseStateSafety.mockResolvedValueOnce(false);
614
+
615
+ await expect(method.getAllNetworkAddress(7)).rejects.toMatchObject({
616
+ errorCode: HardwareErrorCode.DeviceCheckPassphraseStateError,
617
+ });
618
+
619
+ expect(checkPassphraseStateSafety).toHaveBeenCalledTimes(1);
620
+ expect(typedCall).not.toHaveBeenCalled();
621
+ });
622
+
623
+ test('does not start individual retries after cancellation', async () => {
624
+ const { method, typedCall } = createGroupedAddressHarness(false);
625
+ typedCall.mockImplementationOnce(() => {
626
+ method.abortController?.abort();
627
+ return Promise.reject(new Error('Forbidden key path'));
628
+ });
629
+
630
+ await expect(method.getAllNetworkAddress(7)).rejects.toThrow(
631
+ HardwareErrorCodeMessage[HardwareErrorCode.RepeatUnlocking]
632
+ );
633
+
634
+ expect(typedCall).toHaveBeenCalledTimes(1);
635
+ });
636
+
637
+ test('preserves Protocol V1 grouped error handling', async () => {
638
+ const { method, typedCall, checkPassphraseStateSafety } = createGroupedAddressHarness(false);
639
+ jest.spyOn(method.device, 'isProtocolV2').mockReturnValue(false);
640
+ jest.spyOn(method.device, 'getProtocol').mockReturnValue('V1');
641
+
642
+ const result = await method.getAllNetworkAddress(7);
643
+
644
+ expect(result.map(item => item.success)).toEqual([false, false, false]);
645
+ expect(typedCall).toHaveBeenCalledTimes(2);
646
+ expect(checkPassphraseStateSafety).not.toHaveBeenCalled();
647
+ });
648
+
649
+ test('reuses a Protocol V2 hidden-wallet session across later nested chain methods', async () => {
650
+ const { calls, checkPassphraseStateSafety, method } = createV2NestedHarness({
651
+ passphraseState: 'hidden-state',
652
+ });
653
+ (findMethod as jest.Mock)
654
+ .mockReturnValueOnce(mockInnerChainMethod('evmGetAddress', () => calls.push('run-evm')))
655
+ .mockReturnValueOnce(mockInnerChainMethod('solGetAddress', () => calls.push('run-sol')));
656
+
657
+ await method.callMethod(
658
+ 'evmGetAddress',
659
+ {
660
+ bundle: [{ _originRequestParams: { network: 'evm', path: "m/44'/60'/0'/0/0" } }],
661
+ },
662
+ 0
663
+ );
664
+ await method.callMethod(
665
+ 'solGetAddress',
666
+ {
667
+ bundle: [{ _originRequestParams: { network: 'sol', path: "m/44'/501'/0'" } }],
668
+ },
669
+ 0
670
+ );
671
+
672
+ expect(checkPassphraseStateSafety).toHaveBeenCalledTimes(1);
673
+ expect(calls).toEqual(['restore-wallet-session', 'run-evm', 'run-sol']);
674
+ });
675
+
676
+ test('resumes Cardano after a Protocol V2 standard-domain session, then reuses it', async () => {
677
+ const { calls, checkPassphraseStateSafety, method } = createV2NestedHarness({
678
+ passphraseState: 'hidden-state',
679
+ });
680
+ (findMethod as jest.Mock)
681
+ .mockReturnValueOnce(mockInnerChainMethod('evmGetAddress', () => calls.push('run-evm')))
682
+ .mockReturnValueOnce(
683
+ mockInnerChainMethod('cardanoGetAddress', () => calls.push('run-cardano'))
684
+ )
685
+ .mockReturnValueOnce(mockInnerChainMethod('solGetAddress', () => calls.push('run-sol')));
686
+
687
+ await method.callMethod(
688
+ 'evmGetAddress',
689
+ {
690
+ bundle: [{ _originRequestParams: { network: 'evm', path: "m/44'/60'/0'/0/0" } }],
691
+ },
692
+ 0
693
+ );
694
+ await method.callMethod(
695
+ 'cardanoGetAddress',
696
+ {
697
+ bundle: [{ _originRequestParams: { network: 'ada', path: "m/1852'/1815'/0'/0/0" } }],
698
+ },
699
+ 0
700
+ );
701
+ await method.callMethod(
702
+ 'solGetAddress',
703
+ {
704
+ bundle: [{ _originRequestParams: { network: 'sol', path: "m/44'/501'/0'" } }],
705
+ },
706
+ 0
707
+ );
708
+
709
+ expect(checkPassphraseStateSafety).toHaveBeenCalledTimes(2);
710
+ expect(checkPassphraseStateSafety).toHaveBeenNthCalledWith(
711
+ 2,
712
+ 'hidden-state',
713
+ false,
714
+ undefined,
715
+ true,
716
+ undefined
717
+ );
718
+ expect(calls).toEqual([
719
+ 'restore-wallet-session',
720
+ 'run-evm',
721
+ 'resume-cardano-session',
722
+ 'run-cardano',
723
+ 'run-sol',
724
+ ]);
725
+ });
726
+
727
+ test('batches Protocol V2 same-method addresses onto one nested chain call', async () => {
275
728
  const method = new AllNetworkGetAddress({
276
729
  id: 3,
277
730
  payload: {
@@ -280,8 +733,44 @@ describe('AllNetworkGetAddressBase tracing', () => {
280
733
  deviceId: 'device-id',
281
734
  useEmptyPassphrase: true,
282
735
  bundle: [
283
- { network: 'evm', path: "m/44'/60'/0'/0/0" },
284
- { network: 'evm', path: "m/44'/60'/0'/0/1" },
736
+ { network: 'evm', path: "m/44'/60'/0'/0/0", showOnOneKey: false },
737
+ { network: 'evm', path: "m/44'/60'/0'/0/1", showOnOneKey: false },
738
+ ],
739
+ },
740
+ });
741
+ method.device = {
742
+ isProtocolV2: jest.fn().mockReturnValue(true),
743
+ } as any;
744
+ method.postMessage = jest.fn();
745
+ const callMethod = jest.fn().mockResolvedValue([
746
+ { payload: { address: '0x1' }, success: true },
747
+ { payload: { address: '0x2' }, success: true },
748
+ ]);
749
+ method.callMethod = callMethod;
750
+
751
+ await method.getAllNetworkAddress(7);
752
+
753
+ expect(callMethod).toHaveBeenCalledTimes(1);
754
+ expect(callMethod).toHaveBeenCalledWith(
755
+ 'evmGetAddress',
756
+ expect.objectContaining({ bundle: [expect.any(Object), expect.any(Object)] }),
757
+ 7,
758
+ expect.any(Function)
759
+ );
760
+ });
761
+
762
+ test('batches Protocol V2 hidden-wallet same-method addresses onto one nested chain call', async () => {
763
+ const method = new AllNetworkGetAddress({
764
+ id: 6,
765
+ payload: {
766
+ method: 'allNetworkGetAddress',
767
+ connectId: 'connect-id',
768
+ deviceId: 'device-id',
769
+ passphraseState: 'hidden-state',
770
+ bundle: [
771
+ { network: 'evm', path: "m/44'/60'/0'/0/0", showOnOneKey: false },
772
+ { network: 'evm', path: "m/44'/60'/0'/0/1", showOnOneKey: false },
773
+ { network: 'sol', path: "m/44'/501'/0'", showOnOneKey: false },
285
774
  ],
286
775
  },
287
776
  });
@@ -291,8 +780,11 @@ describe('AllNetworkGetAddressBase tracing', () => {
291
780
  method.postMessage = jest.fn();
292
781
  const callMethod = jest
293
782
  .fn()
294
- .mockResolvedValueOnce([{ payload: { address: '0x1' }, success: true }])
295
- .mockResolvedValueOnce([{ payload: { address: '0x2' }, success: true }]);
783
+ .mockResolvedValueOnce([
784
+ { payload: { address: '0x1' }, success: true },
785
+ { payload: { address: '0x2' }, success: true },
786
+ ])
787
+ .mockResolvedValueOnce([{ payload: { address: 'sol1' }, success: true }]);
296
788
  method.callMethod = callMethod;
297
789
 
298
790
  await method.getAllNetworkAddress(7);
@@ -301,12 +793,13 @@ describe('AllNetworkGetAddressBase tracing', () => {
301
793
  expect(callMethod).toHaveBeenNthCalledWith(
302
794
  1,
303
795
  'evmGetAddress',
304
- expect.objectContaining({ bundle: [expect.any(Object)] }),
305
- 7
796
+ expect.objectContaining({ bundle: [expect.any(Object), expect.any(Object)] }),
797
+ 7,
798
+ expect.any(Function)
306
799
  );
307
800
  expect(callMethod).toHaveBeenNthCalledWith(
308
801
  2,
309
- 'evmGetAddress',
802
+ 'solGetAddress',
310
803
  expect.objectContaining({ bundle: [expect.any(Object)] }),
311
804
  7
312
805
  );