@onekeyfe/hd-core 1.2.2-alpha.12 → 1.2.2-alpha.120

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 (67) hide show
  1. package/README.md +1 -1
  2. package/__tests__/AllNetworkGetAddressBase.tracing.test.ts +452 -9
  3. package/__tests__/DeviceCommands.test.ts +254 -1
  4. package/__tests__/core-error-output.test.ts +92 -1
  5. package/__tests__/device-lifecycle-events.test.ts +379 -15
  6. package/__tests__/open-wallet-session-error-response.test.ts +2 -2
  7. package/__tests__/open-wallet-session.test.ts +8 -411
  8. package/__tests__/protocol-v2.test.ts +86 -0
  9. package/__tests__/public-device-state-api.test.ts +2 -7
  10. package/__tests__/sol-sign-offchain-message.test.ts +64 -0
  11. package/dist/api/GetFeatures.d.ts.map +1 -1
  12. package/dist/api/GetPassphraseState.d.ts.map +1 -1
  13. package/dist/api/OpenWalletSession.d.ts.map +1 -1
  14. package/dist/api/UploadPortfolio.d.ts +1 -0
  15. package/dist/api/UploadPortfolio.d.ts.map +1 -1
  16. package/dist/api/allnetwork/AllNetworkGetAddress.d.ts.map +1 -1
  17. package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts +6 -0
  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/api/solana/SolSignOffchainMessage.d.ts.map +1 -1
  22. package/dist/core/RequestQueue.d.ts +1 -0
  23. package/dist/core/RequestQueue.d.ts.map +1 -1
  24. package/dist/core/index.d.ts +3 -1
  25. package/dist/core/index.d.ts.map +1 -1
  26. package/dist/core/uiPromiseRegistry.d.ts +1 -1
  27. package/dist/device/Device.d.ts +2 -0
  28. package/dist/device/Device.d.ts.map +1 -1
  29. package/dist/device/DeviceCommands.d.ts +5 -4
  30. package/dist/device/DeviceCommands.d.ts.map +1 -1
  31. package/dist/index.d.ts +35 -30
  32. package/dist/index.js +402 -208
  33. package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
  34. package/dist/types/api/getFeatures.d.ts.map +1 -1
  35. package/dist/types/api/getPassphraseState.d.ts.map +1 -1
  36. package/dist/types/api/openWalletSession.d.ts +1 -10
  37. package/dist/types/api/openWalletSession.d.ts.map +1 -1
  38. package/dist/types/api/protocolV2.d.ts +3 -4
  39. package/dist/types/api/protocolV2.d.ts.map +1 -1
  40. package/dist/types/api/solSignOffchainMessage.d.ts +1 -0
  41. package/dist/types/api/solSignOffchainMessage.d.ts.map +1 -1
  42. package/dist/types/params.d.ts.map +1 -1
  43. package/dist/utils/patch.d.ts +1 -1
  44. package/dist/utils/patch.d.ts.map +1 -1
  45. package/package.json +4 -4
  46. package/src/api/GetFeatures.ts +1 -0
  47. package/src/api/GetPassphraseState.ts +1 -0
  48. package/src/api/OpenWalletSession.ts +7 -77
  49. package/src/api/UploadPortfolio.ts +5 -4
  50. package/src/api/allnetwork/AllNetworkGetAddress.ts +47 -20
  51. package/src/api/allnetwork/AllNetworkGetAddressBase.ts +88 -22
  52. package/src/api/allnetwork/AllNetworkGetAddressByLoop.ts +3 -0
  53. package/src/api/device/DeviceVerify.ts +8 -0
  54. package/src/api/solana/SolSignOffchainMessage.ts +44 -4
  55. package/src/core/RequestQueue.ts +20 -0
  56. package/src/core/index.ts +116 -49
  57. package/src/data/messages/messages-protocol-v2.json +49 -33
  58. package/src/data/messages/messages.json +8 -5
  59. package/src/device/Device.ts +51 -27
  60. package/src/device/DeviceCommands.ts +29 -2
  61. package/src/protocols/protocol-v2/walletSession.ts +7 -0
  62. package/src/types/api/getFeatures.ts +2 -1
  63. package/src/types/api/getPassphraseState.ts +2 -5
  64. package/src/types/api/openWalletSession.ts +7 -19
  65. package/src/types/api/protocolV2.ts +3 -4
  66. package/src/types/api/solSignOffchainMessage.ts +2 -0
  67. 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('Pro2 all-network loading lifecycle', () => {
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(() => EDeviceType.Pro2),
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', 'Neo'])('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 === 'Neo')
83
+ jest.spyOn(method.device, 'getCurrentDeviceType').mockReturnValue(EDeviceType.Neo);
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,257 @@ 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(method.postMessage).toHaveBeenCalledWith(
547
+ expect.objectContaining({ type: UI_REQUEST.DEVICE_PROGRESS, payload: { progress: 100 } })
548
+ );
549
+ expect(getActiveRequestsByDeviceInstance('device-instance')).toEqual([]);
550
+ }
551
+ );
552
+
553
+ test('does not retry a failed V2 link as individual address requests', async () => {
554
+ const { method, typedCall } = createGroupedAddressHarness(false);
555
+ const error = new Error('link disconnected');
556
+ typedCall.mockRejectedValueOnce(error);
557
+
558
+ await expect(method.getAllNetworkAddress(7)).rejects.toBe(error);
559
+
560
+ expect(typedCall).toHaveBeenCalledTimes(1);
561
+ });
562
+
563
+ test('does not retry a V2 wallet mismatch as individual address requests', async () => {
564
+ const { method, typedCall, checkPassphraseStateSafety } = createGroupedAddressHarness(false);
565
+ checkPassphraseStateSafety.mockResolvedValueOnce(false);
566
+
567
+ await expect(method.getAllNetworkAddress(7)).rejects.toMatchObject({
568
+ errorCode: HardwareErrorCode.DeviceCheckPassphraseStateError,
569
+ });
570
+
571
+ expect(checkPassphraseStateSafety).toHaveBeenCalledTimes(1);
572
+ expect(typedCall).not.toHaveBeenCalled();
573
+ });
574
+
575
+ test('does not start individual retries after cancellation', async () => {
576
+ const { method, typedCall } = createGroupedAddressHarness(false);
577
+ typedCall.mockImplementationOnce(() => {
578
+ method.abortController?.abort();
579
+ return Promise.reject(new Error('Forbidden key path'));
580
+ });
581
+
582
+ await expect(method.getAllNetworkAddress(7)).rejects.toThrow(
583
+ HardwareErrorCodeMessage[HardwareErrorCode.RepeatUnlocking]
584
+ );
585
+
586
+ expect(typedCall).toHaveBeenCalledTimes(1);
587
+ });
588
+
589
+ test('preserves Protocol V1 grouped error handling', async () => {
590
+ const { method, typedCall, checkPassphraseStateSafety } = createGroupedAddressHarness(false);
591
+ jest.spyOn(method.device, 'isProtocolV2').mockReturnValue(false);
592
+ jest.spyOn(method.device, 'getProtocol').mockReturnValue('V1');
593
+
594
+ const result = await method.getAllNetworkAddress(7);
595
+
596
+ expect(result.map(item => item.success)).toEqual([false, false, false]);
597
+ expect(typedCall).toHaveBeenCalledTimes(2);
598
+ expect(checkPassphraseStateSafety).not.toHaveBeenCalled();
599
+ });
600
+
601
+ test('reuses a Protocol V2 hidden-wallet session across later nested chain methods', async () => {
602
+ const { calls, checkPassphraseStateSafety, method } = createV2NestedHarness({
603
+ passphraseState: 'hidden-state',
604
+ });
605
+ (findMethod as jest.Mock)
606
+ .mockReturnValueOnce(mockInnerChainMethod('evmGetAddress', () => calls.push('run-evm')))
607
+ .mockReturnValueOnce(mockInnerChainMethod('solGetAddress', () => calls.push('run-sol')));
608
+
609
+ await method.callMethod(
610
+ 'evmGetAddress',
611
+ {
612
+ bundle: [{ _originRequestParams: { network: 'evm', path: "m/44'/60'/0'/0/0" } }],
613
+ },
614
+ 0
615
+ );
616
+ await method.callMethod(
617
+ 'solGetAddress',
618
+ {
619
+ bundle: [{ _originRequestParams: { network: 'sol', path: "m/44'/501'/0'" } }],
620
+ },
621
+ 0
622
+ );
623
+
624
+ expect(checkPassphraseStateSafety).toHaveBeenCalledTimes(1);
625
+ expect(calls).toEqual(['restore-wallet-session', 'run-evm', 'run-sol']);
626
+ });
627
+
628
+ test('resumes Cardano after a Protocol V2 standard-domain session, then reuses it', async () => {
629
+ const { calls, checkPassphraseStateSafety, method } = createV2NestedHarness({
630
+ passphraseState: 'hidden-state',
631
+ });
632
+ (findMethod as jest.Mock)
633
+ .mockReturnValueOnce(mockInnerChainMethod('evmGetAddress', () => calls.push('run-evm')))
634
+ .mockReturnValueOnce(
635
+ mockInnerChainMethod('cardanoGetAddress', () => calls.push('run-cardano'))
636
+ )
637
+ .mockReturnValueOnce(mockInnerChainMethod('solGetAddress', () => calls.push('run-sol')));
638
+
639
+ await method.callMethod(
640
+ 'evmGetAddress',
641
+ {
642
+ bundle: [{ _originRequestParams: { network: 'evm', path: "m/44'/60'/0'/0/0" } }],
643
+ },
644
+ 0
645
+ );
646
+ await method.callMethod(
647
+ 'cardanoGetAddress',
648
+ {
649
+ bundle: [{ _originRequestParams: { network: 'ada', path: "m/1852'/1815'/0'/0/0" } }],
650
+ },
651
+ 0
652
+ );
653
+ await method.callMethod(
654
+ 'solGetAddress',
655
+ {
656
+ bundle: [{ _originRequestParams: { network: 'sol', path: "m/44'/501'/0'" } }],
657
+ },
658
+ 0
659
+ );
660
+
661
+ expect(checkPassphraseStateSafety).toHaveBeenCalledTimes(2);
662
+ expect(checkPassphraseStateSafety).toHaveBeenNthCalledWith(
663
+ 2,
664
+ 'hidden-state',
665
+ false,
666
+ undefined,
667
+ true,
668
+ undefined
669
+ );
670
+ expect(calls).toEqual([
671
+ 'restore-wallet-session',
672
+ 'run-evm',
673
+ 'resume-cardano-session',
674
+ 'run-cardano',
675
+ 'run-sol',
676
+ ]);
677
+ });
678
+
679
+ test('batches Protocol V2 same-method addresses onto one nested chain call', async () => {
275
680
  const method = new AllNetworkGetAddress({
276
681
  id: 3,
277
682
  payload: {
@@ -280,8 +685,43 @@ describe('AllNetworkGetAddressBase tracing', () => {
280
685
  deviceId: 'device-id',
281
686
  useEmptyPassphrase: true,
282
687
  bundle: [
283
- { network: 'evm', path: "m/44'/60'/0'/0/0" },
284
- { network: 'evm', path: "m/44'/60'/0'/0/1" },
688
+ { network: 'evm', path: "m/44'/60'/0'/0/0", showOnOneKey: false },
689
+ { network: 'evm', path: "m/44'/60'/0'/0/1", showOnOneKey: false },
690
+ ],
691
+ },
692
+ });
693
+ method.device = {
694
+ isProtocolV2: jest.fn().mockReturnValue(true),
695
+ } as any;
696
+ method.postMessage = jest.fn();
697
+ const callMethod = jest.fn().mockResolvedValue([
698
+ { payload: { address: '0x1' }, success: true },
699
+ { payload: { address: '0x2' }, success: true },
700
+ ]);
701
+ method.callMethod = callMethod;
702
+
703
+ await method.getAllNetworkAddress(7);
704
+
705
+ expect(callMethod).toHaveBeenCalledTimes(1);
706
+ expect(callMethod).toHaveBeenCalledWith(
707
+ 'evmGetAddress',
708
+ expect.objectContaining({ bundle: [expect.any(Object), expect.any(Object)] }),
709
+ 7
710
+ );
711
+ });
712
+
713
+ test('batches Protocol V2 hidden-wallet same-method addresses onto one nested chain call', async () => {
714
+ const method = new AllNetworkGetAddress({
715
+ id: 6,
716
+ payload: {
717
+ method: 'allNetworkGetAddress',
718
+ connectId: 'connect-id',
719
+ deviceId: 'device-id',
720
+ passphraseState: 'hidden-state',
721
+ bundle: [
722
+ { network: 'evm', path: "m/44'/60'/0'/0/0", showOnOneKey: false },
723
+ { network: 'evm', path: "m/44'/60'/0'/0/1", showOnOneKey: false },
724
+ { network: 'sol', path: "m/44'/501'/0'", showOnOneKey: false },
285
725
  ],
286
726
  },
287
727
  });
@@ -291,8 +731,11 @@ describe('AllNetworkGetAddressBase tracing', () => {
291
731
  method.postMessage = jest.fn();
292
732
  const callMethod = jest
293
733
  .fn()
294
- .mockResolvedValueOnce([{ payload: { address: '0x1' }, success: true }])
295
- .mockResolvedValueOnce([{ payload: { address: '0x2' }, success: true }]);
734
+ .mockResolvedValueOnce([
735
+ { payload: { address: '0x1' }, success: true },
736
+ { payload: { address: '0x2' }, success: true },
737
+ ])
738
+ .mockResolvedValueOnce([{ payload: { address: 'sol1' }, success: true }]);
296
739
  method.callMethod = callMethod;
297
740
 
298
741
  await method.getAllNetworkAddress(7);
@@ -301,12 +744,12 @@ describe('AllNetworkGetAddressBase tracing', () => {
301
744
  expect(callMethod).toHaveBeenNthCalledWith(
302
745
  1,
303
746
  'evmGetAddress',
304
- expect.objectContaining({ bundle: [expect.any(Object)] }),
747
+ expect.objectContaining({ bundle: [expect.any(Object), expect.any(Object)] }),
305
748
  7
306
749
  );
307
750
  expect(callMethod).toHaveBeenNthCalledWith(
308
751
  2,
309
- 'evmGetAddress',
752
+ 'solGetAddress',
310
753
  expect.objectContaining({ bundle: [expect.any(Object)] }),
311
754
  7
312
755
  );