@onekeyfe/hd-core 1.2.2-alpha.119 → 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.
@@ -1,8 +1,10 @@
1
- import { EDeviceType } from '@onekeyfe/hd-shared';
1
+ import { EDeviceType, HardwareErrorCode, HardwareErrorCodeMessage } from '@onekeyfe/hd-shared';
2
2
 
3
3
  import AllNetworkGetAddressBase from '../src/api/allnetwork/AllNetworkGetAddressBase';
4
4
  import AllNetworkGetAddress from '../src/api/allnetwork/AllNetworkGetAddress';
5
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';
6
8
  import { findMethod } from '../src/api/utils';
7
9
  import { getActiveRequestsByDeviceInstance } from '../src/utils/tracing';
8
10
 
@@ -424,7 +426,257 @@ describe('AllNetworkGetAddressBase tracing', () => {
424
426
  expect(typedCall).not.toHaveBeenCalled();
425
427
  });
426
428
 
427
- 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 () => {
428
680
  const method = new AllNetworkGetAddress({
429
681
  id: 3,
430
682
  payload: {
@@ -433,8 +685,43 @@ describe('AllNetworkGetAddressBase tracing', () => {
433
685
  deviceId: 'device-id',
434
686
  useEmptyPassphrase: true,
435
687
  bundle: [
436
- { network: 'evm', path: "m/44'/60'/0'/0/0" },
437
- { 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 },
438
725
  ],
439
726
  },
440
727
  });
@@ -444,8 +731,11 @@ describe('AllNetworkGetAddressBase tracing', () => {
444
731
  method.postMessage = jest.fn();
445
732
  const callMethod = jest
446
733
  .fn()
447
- .mockResolvedValueOnce([{ payload: { address: '0x1' }, success: true }])
448
- .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 }]);
449
739
  method.callMethod = callMethod;
450
740
 
451
741
  await method.getAllNetworkAddress(7);
@@ -454,12 +744,12 @@ describe('AllNetworkGetAddressBase tracing', () => {
454
744
  expect(callMethod).toHaveBeenNthCalledWith(
455
745
  1,
456
746
  'evmGetAddress',
457
- expect.objectContaining({ bundle: [expect.any(Object)] }),
747
+ expect.objectContaining({ bundle: [expect.any(Object), expect.any(Object)] }),
458
748
  7
459
749
  );
460
750
  expect(callMethod).toHaveBeenNthCalledWith(
461
751
  2,
462
- 'evmGetAddress',
752
+ 'solGetAddress',
463
753
  expect.objectContaining({ bundle: [expect.any(Object)] }),
464
754
  7
465
755
  );
@@ -4,8 +4,6 @@ import { initConnector, initCore } from '../src/core';
4
4
  import { DataManager } from '../src/data-manager';
5
5
  import TransportManager from '../src/data-manager/TransportManager';
6
6
  import { IFRAME } from '../src/events';
7
- import SearchDevices from '../src/api/SearchDevices';
8
- import { DeviceList } from '../src/device/DeviceList';
9
7
 
10
8
  jest.mock('../src/data/config', () => ({
11
9
  getSDKVersion: jest.fn(() => '1.0.0-test'),
@@ -26,81 +24,6 @@ describe('Core 错误输出边界', () => {
26
24
  jest.restoreAllMocks();
27
25
  });
28
26
 
29
- test.each([
30
- ['webusb', false],
31
- ['desktop-webusb', false],
32
- ['desktop-webusb', true],
33
- ] as const)(
34
- '%s waits for discovery without masking initialization or cancellation (cancel=%s)',
35
- async (env, shouldCancel) => {
36
- jest.spyOn(DataManager, 'getSettings').mockReturnValue(env as never);
37
- const error = ERRORS.TypedError(HardwareErrorCode.DeviceInitializeFailed, 'probe failed');
38
- let finishSearch!: () => void;
39
- let searchStarted!: () => void;
40
- const started = new Promise<void>(resolve => {
41
- searchStarted = resolve;
42
- });
43
- const search = jest.spyOn(SearchDevices.prototype, 'run').mockImplementation(async () => {
44
- searchStarted();
45
- await new Promise<void>(resolve => {
46
- finishSearch = resolve;
47
- });
48
- return [];
49
- });
50
- const initialize = jest
51
- .spyOn(DeviceList.prototype, 'getDeviceLists')
52
- .mockRejectedValue(error);
53
- const core = initCore();
54
- initConnector();
55
- try {
56
- const discovery = core.handleMessage({
57
- id: 10,
58
- type: IFRAME.CALL,
59
- payload: { method: 'searchDevices' },
60
- } as never);
61
- await started;
62
- const request = core.handleMessage({
63
- id: 11,
64
- type: IFRAME.CALL,
65
- payload: {
66
- method: 'getDeviceState',
67
- connectId: 'serial-V2',
68
- retryCount: 1,
69
- pollIntervalTime: 1,
70
- timeout: 1000,
71
- },
72
- } as never);
73
- await new Promise(resolve => {
74
- setTimeout(resolve, 0);
75
- });
76
- expect(initialize).not.toHaveBeenCalled();
77
- if (shouldCancel) {
78
- await core.handleMessage({
79
- type: IFRAME.CANCEL,
80
- payload: { connectId: 'serial-V2' },
81
- } as never);
82
- }
83
- finishSearch();
84
- await expect(discovery).resolves.toMatchObject({ success: true });
85
- const expectedError = shouldCancel
86
- ? ERRORS.TypedError(HardwareErrorCode.CallQueueActionCancelled)
87
- : error;
88
- await expect(request).resolves.toMatchObject({
89
- success: false,
90
- payload: { code: expectedError.errorCode, error: expectedError.message },
91
- });
92
- await new Promise(resolve => {
93
- setTimeout(resolve, 0);
94
- });
95
- expect(search).toHaveBeenCalledTimes(1);
96
- expect(initialize).toHaveBeenCalledTimes(shouldCancel ? 0 : 2);
97
- } finally {
98
- finishSearch?.();
99
- await core.dispose();
100
- }
101
- }
102
- );
103
-
104
27
  test.each([
105
28
  HardwareErrorCode.BleDeviceNotBonded,
106
29
  HardwareErrorCode.BleDeviceBondedCanceled,
@@ -362,12 +362,12 @@ describe('public device lifecycle events', () => {
362
362
  }
363
363
  );
364
364
 
365
- test('clears the cleanup barrier when its deadline expires', async () => {
365
+ test('keeps the cleanup barrier when its deadline expires', async () => {
366
366
  const realSetTimeout = setTimeout;
367
367
  jest
368
368
  .spyOn(global, 'setTimeout')
369
369
  .mockImplementation((callback, delay, ...args) =>
370
- realSetTimeout(callback, delay === 5_000 ? 0 : delay, ...args)
370
+ realSetTimeout(callback, delay === 15_000 ? 0 : delay, ...args)
371
371
  );
372
372
  {
373
373
  jest.spyOn(DataManager, 'getSettings').mockReturnValue('react-native' as never);
@@ -382,13 +382,13 @@ describe('public device lifecycle events', () => {
382
382
  type: IFRAME.CALL,
383
383
  payload: { method: 'getDeviceState', connectId: 'draining-device', connectProtocol: 'V2' },
384
384
  } as CoreMessage);
385
- setImmediate(() => cancel(context, 'draining-device'));
386
- await expect(result).resolves.toBeDefined();
387
- gate.resolve();
388
- await new Promise(resolve => {
389
- setImmediate(resolve);
385
+ await expect(result).resolves.toMatchObject({
386
+ success: false,
387
+ payload: { code: HardwareErrorCode.DeviceBusy },
390
388
  });
391
- expect(context.getPrePendingCallPromise('draining-device')).toBeUndefined();
389
+ expect(acquire).not.toHaveBeenCalled();
390
+ expect(context.getPrePendingCallPromise('draining-device')).toBe(gate.promise);
391
+ gate.resolve();
392
392
  }
393
393
  });
394
394