@onekeyfe/hd-core 1.2.8-alpha.4 → 1.2.8-alpha.50

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 (36) hide show
  1. package/__tests__/DeviceCommands.test.ts +66 -13
  2. package/__tests__/btc-derive-context-hash.test.ts +28 -3
  3. package/__tests__/core-error-output.test.ts +34 -3
  4. package/__tests__/device-lifecycle-events.test.ts +73 -24
  5. package/__tests__/device-pool-state.test.ts +50 -0
  6. package/__tests__/protocol-v2-bootloader-mode.test.ts +43 -2
  7. package/__tests__/protocol-v2.test.ts +0 -47
  8. package/__tests__/refresh-device-state.test.ts +19 -17
  9. package/__tests__/sol-sign-offchain-message.test.ts +8 -0
  10. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  11. package/dist/api/GetDeviceState.d.ts.map +1 -1
  12. package/dist/api/btc/BTCDeriveContextHash.d.ts +3 -0
  13. package/dist/api/btc/BTCDeriveContextHash.d.ts.map +1 -1
  14. package/dist/api/protocol-v2/DeviceReboot.d.ts.map +1 -1
  15. package/dist/core/index.d.ts +0 -1
  16. package/dist/core/index.d.ts.map +1 -1
  17. package/dist/device/Device.d.ts +2 -0
  18. package/dist/device/Device.d.ts.map +1 -1
  19. package/dist/device/DeviceCommands.d.ts +4 -4
  20. package/dist/device/DeviceCommands.d.ts.map +1 -1
  21. package/dist/device/DevicePool.d.ts.map +1 -1
  22. package/dist/index.d.ts +6 -2
  23. package/dist/index.js +43 -29
  24. package/dist/types/params.d.ts +1 -0
  25. package/dist/types/params.d.ts.map +1 -1
  26. package/package.json +4 -4
  27. package/src/api/FirmwareUpdateV4.ts +8 -25
  28. package/src/api/GetDeviceState.ts +2 -5
  29. package/src/api/btc/BTCDeriveContextHash.ts +4 -1
  30. package/src/api/protocol-v2/DeviceReboot.ts +2 -0
  31. package/src/core/index.ts +3 -7
  32. package/src/device/Device.ts +11 -1
  33. package/src/device/DeviceCommands.ts +15 -1
  34. package/src/device/DevicePool.ts +12 -2
  35. package/src/events/core.ts +2 -2
  36. package/src/types/params.ts +1 -1
@@ -130,19 +130,6 @@ describe('DeviceCommands failure mapping', () => {
130
130
  );
131
131
  });
132
132
 
133
- it('preserves the Protocol V2 peer-removed pairing error code', async () => {
134
- const commands = createCommands();
135
- const transportError = Object.assign(new Error('Peer removed pairing information'), {
136
- errorCode: HardwareErrorCode.BlePeerRemovedPairingInformation,
137
- });
138
- commands.mainId = 'main-id';
139
- commands.transport = {
140
- call: jest.fn().mockRejectedValue(transportError),
141
- } as any;
142
-
143
- await expect(commands._commonCall('DeviceInfoGet', {})).rejects.toBe(transportError);
144
- });
145
-
146
133
  it('logs canonical DeviceStatus response fields without exposing the device ID', async () => {
147
134
  const commands = createCommands();
148
135
  const log = getLogger(LoggerNames.DeviceCommands);
@@ -414,6 +401,27 @@ describe('DeviceCommands failure mapping', () => {
414
401
  ).rejects.toMatchObject({ errorCode: HardwareErrorCode.DeviceBusy });
415
402
  });
416
403
 
404
+ it('maps commands rejected during a Protocol V2 firmware update to DeviceBusy', async () => {
405
+ const commands = createCommands();
406
+ const message = 'Requested command not allowed while a firmware update is in progress';
407
+
408
+ await expect(
409
+ commands._filterCommonTypes(
410
+ {
411
+ type: 'Failure',
412
+ message: { code: 'Failure_ProcessError', message },
413
+ } as any,
414
+ 'ProtocolInfoRequest'
415
+ )
416
+ ).rejects.toMatchObject({
417
+ errorCode: HardwareErrorCode.DeviceBusy,
418
+ params: {
419
+ failureCode: 'Failure_ProcessError',
420
+ firmwareMessage: message,
421
+ },
422
+ });
423
+ });
424
+
417
425
  it('maps the current AskPin passphrase-disabled response without relying on a subcode', async () => {
418
426
  const commands = createCommands();
419
427
 
@@ -551,6 +559,51 @@ describe('DeviceCommands failure mapping', () => {
551
559
  }
552
560
  );
553
561
 
562
+ it('maps the Protocol V2 seed-session busy response to DeviceBusy', async () => {
563
+ const commands = createCommands();
564
+
565
+ await expect(
566
+ commands._filterCommonTypes(
567
+ {
568
+ type: 'Failure',
569
+ message: {
570
+ code: 'Failure_ProcessError',
571
+ subcode: DeviceSessionErrorCode.DeviceSessionError_UserCancelled,
572
+ message: 'PIN cancelled',
573
+ },
574
+ } as any,
575
+ 'SignTx'
576
+ )
577
+ ).rejects.toMatchObject({
578
+ errorCode: HardwareErrorCode.DeviceBusy,
579
+ params: {
580
+ failureCode: 'Failure_ProcessError',
581
+ subcode: DeviceSessionErrorCode.DeviceSessionError_UserCancelled,
582
+ firmwareMessage: 'PIN cancelled',
583
+ },
584
+ });
585
+ });
586
+
587
+ it('keeps the same Protocol V2 response as ActionCancelled for DeviceSession calls', async () => {
588
+ const commands = createCommands();
589
+
590
+ await expect(
591
+ commands._filterCommonTypes(
592
+ {
593
+ type: 'Failure',
594
+ message: {
595
+ code: 'Failure_ProcessError',
596
+ subcode: DeviceSessionErrorCode.DeviceSessionError_UserCancelled,
597
+ message: 'PIN cancelled',
598
+ },
599
+ } as any,
600
+ 'DeviceSessionAskPin'
601
+ )
602
+ ).rejects.toMatchObject({
603
+ errorCode: HardwareErrorCode.ActionCancelled,
604
+ });
605
+ });
606
+
554
607
  it('does not use the legacy cancellation-message fallback for Protocol V1', async () => {
555
608
  const commands = createCommands();
556
609
  (commands.device.isProtocolV2 as jest.Mock).mockReturnValue(false);
@@ -1,11 +1,12 @@
1
- import { EFirmwareType, HardwareErrorCode } from '@onekeyfe/hd-shared';
1
+ import { EDeviceType, EFirmwareType, HardwareErrorCode } from '@onekeyfe/hd-shared';
2
2
 
3
3
  import BTCDeriveContextHash from '../src/api/btc/BTCDeriveContextHash';
4
4
  import { findMethod } from '../src/api/utils';
5
5
  import { getLogBlockLabel, getSafeLogPayload } from '../src/events/logBlockEvent';
6
6
  import { createCoreApi } from '../src/inject';
7
+ import { Device } from '../src/device/Device';
7
8
 
8
- import type { Device } from '../src/device/Device';
9
+ import type { DeviceFirmwareRange } from '../src/types';
9
10
 
10
11
  jest.mock('../src/data/config', () => ({
11
12
  getSDKVersion: jest.fn(() => '1.0.0'),
@@ -133,12 +134,36 @@ describe('btcDeriveContextHash', () => {
133
134
  method.init();
134
135
  expect(method).toBeInstanceOf(BTCDeriveContextHash);
135
136
  expect(method.strictCheckDeviceSupport).toBe(true);
136
- expect(method.getVersionRange()).toEqual({ classic1s: { min: '3.21.0' } });
137
137
  expect(() => method.assertProtocolSupported('V2', EFirmwareType.Universal)).toThrow(
138
138
  expect.objectContaining({ errorCode: HardwareErrorCode.DeviceNotSupportMethod })
139
139
  );
140
140
  });
141
141
 
142
+ test.each([EDeviceType.Classic1s, EDeviceType.ClassicPure])(
143
+ 'resolves the firmware requirement for %s through the device support check',
144
+ deviceType => {
145
+ const method = createMethod();
146
+ const device = Object.create(Device.prototype) as Device;
147
+ device.getCurrentDeviceType = jest.fn(() => deviceType);
148
+ const range: DeviceFirmwareRange = method.getVersionRange();
149
+
150
+ expect(device.getCurrentMethodVersionRange(type => range[type])).toEqual({ min: '3.21.0' });
151
+ expect(() => method.assertProtocolSupported('V1', EFirmwareType.Universal)).not.toThrow();
152
+ }
153
+ );
154
+
155
+ test.each([EDeviceType.Classic, EDeviceType.Mini, EDeviceType.Touch, EDeviceType.Pro])(
156
+ 'keeps %s outside the supported firmware range',
157
+ deviceType => {
158
+ const method = createMethod();
159
+ const device = Object.create(Device.prototype) as Device;
160
+ device.getCurrentDeviceType = jest.fn(() => deviceType);
161
+ const range: DeviceFirmwareRange = method.getVersionRange();
162
+
163
+ expect(device.getCurrentMethodVersionRange(type => range[type])).toBeUndefined();
164
+ }
165
+ );
166
+
142
167
  test('propagates device rejection without returning a secret', async () => {
143
168
  const method = createMethod();
144
169
  method.init();
@@ -3,7 +3,7 @@ import { ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared';
3
3
  import { initConnector, initCore } from '../src/core';
4
4
  import { DataManager } from '../src/data-manager';
5
5
  import TransportManager from '../src/data-manager/TransportManager';
6
- import { IFRAME } from '../src/events';
6
+ import { IFRAME, createErrorMessage } from '../src/events';
7
7
  import SearchDevices from '../src/api/SearchDevices';
8
8
  import { DeviceList } from '../src/device/DeviceList';
9
9
  import { Device } from '../src/device/Device';
@@ -28,6 +28,23 @@ describe('Core 错误输出边界', () => {
28
28
  jest.restoreAllMocks();
29
29
  });
30
30
 
31
+ test('entry-point errors preserve USB recovery context across JSON serialization', () => {
32
+ const params = {
33
+ operation: 'open',
34
+ nativeErrorName: 'SecurityError',
35
+ nativeErrorMessage: 'Access denied',
36
+ };
37
+ const error = ERRORS.TypedError(
38
+ HardwareErrorCode.BridgeNeedsPermission,
39
+ 'Access denied',
40
+ params
41
+ );
42
+ expect(JSON.parse(JSON.stringify(createErrorMessage(error)))).toEqual({
43
+ success: false,
44
+ payload: { code: HardwareErrorCode.BridgeNeedsPermission, error: 'Access denied', params },
45
+ });
46
+ });
47
+
31
48
  test.each([
32
49
  ['webusb', false],
33
50
  ['desktop-webusb', false],
@@ -108,14 +125,28 @@ describe('Core 错误输出边界', () => {
108
125
  HardwareErrorCode.BleDeviceBondedCanceled,
109
126
  HardwareErrorCode.BleDeviceDisconnected,
110
127
  HardwareErrorCode.PollingTimeout,
128
+ HardwareErrorCode.BlePoweredOff,
129
+ HardwareErrorCode.BleUnsupported,
130
+ HardwareErrorCode.BridgeNeedsPermission,
131
+ HardwareErrorCode.WebUsbDeviceAccessError,
111
132
  ])(
112
- 'preserves terminal BLE error %s in the public response without polling again',
133
+ 'preserves terminal transport error %s in the public response without polling again',
113
134
  async errorCode => {
114
- jest.spyOn(DataManager, 'getSettings').mockReturnValue('react-native' as never);
135
+ jest
136
+ .spyOn(DataManager, 'getSettings')
137
+ .mockReturnValue(
138
+ [
139
+ HardwareErrorCode.BridgeNeedsPermission,
140
+ HardwareErrorCode.WebUsbDeviceAccessError,
141
+ ].includes(errorCode)
142
+ ? ('desktop-webusb' as never)
143
+ : ('react-native' as never)
144
+ );
115
145
  const error = ERRORS.TypedError(errorCode);
116
146
  const acquire = jest.fn().mockRejectedValue(error);
117
147
  jest.spyOn(TransportManager, 'getTransport').mockReturnValue({
118
148
  acquire,
149
+ enumerate: jest.fn().mockResolvedValue([{ path: 'ble-pairing-test' }]),
119
150
  release: jest.fn().mockResolvedValue(true),
120
151
  stop: jest.fn().mockResolvedValue(undefined),
121
152
  } as never);
@@ -10,7 +10,6 @@ import {
10
10
  isMissingDetectedProtocolV2Error,
11
11
  isRetryableBleConnectionError,
12
12
  isRetryableBleProtocolV2ProbeError,
13
- isTerminalBleStaleBondError,
14
13
  resolveBleConnectProtocol,
15
14
  } from '../src/core';
16
15
  import { DataManager } from '../src/data-manager';
@@ -364,11 +363,11 @@ describe('public device lifecycle events', () => {
364
363
 
365
364
  test('clears the cleanup barrier when its deadline expires', async () => {
366
365
  const realSetTimeout = setTimeout;
367
- jest
368
- .spyOn(global, 'setTimeout')
369
- .mockImplementation((callback, delay, ...args) =>
370
- realSetTimeout(callback, delay === 5_000 ? 0 : delay, ...args)
371
- );
366
+ const cleanupDeadline = createDeferred<() => void>();
367
+ jest.spyOn(global, 'setTimeout').mockImplementation((callback, delay, ...args) => {
368
+ if (delay === 5_000) cleanupDeadline.resolve(() => callback(...args));
369
+ return realSetTimeout(callback, delay, ...args);
370
+ });
372
371
  {
373
372
  jest.spyOn(DataManager, 'getSettings').mockReturnValue('react-native' as never);
374
373
  const acquire = jest.spyOn(Device.prototype, 'acquire').mockResolvedValue(undefined);
@@ -382,13 +381,15 @@ describe('public device lifecycle events', () => {
382
381
  type: IFRAME.CALL,
383
382
  payload: { method: 'getDeviceState', connectId: 'draining-device', connectProtocol: 'V2' },
384
383
  } as CoreMessage);
385
- setImmediate(() => cancel(context, 'draining-device'));
384
+ const expireCleanupDeadline = await cleanupDeadline.promise;
385
+ cancel(context, 'draining-device');
386
+ expireCleanupDeadline();
386
387
  await expect(result).resolves.toBeDefined();
387
- gate.resolve();
388
388
  await new Promise(resolve => {
389
389
  setImmediate(resolve);
390
390
  });
391
391
  expect(context.getPrePendingCallPromise('draining-device')).toBeUndefined();
392
+ gate.resolve();
392
393
  }
393
394
  });
394
395
 
@@ -611,7 +612,6 @@ describe('public device lifecycle events', () => {
611
612
  test.each([
612
613
  [HardwareErrorCode.RuntimeError, true],
613
614
  [HardwareErrorCode.BleDeviceBondError, false],
614
- [HardwareErrorCode.BlePeerRemovedPairingInformation, false],
615
615
  ] as const)(
616
616
  'retries a Protocol V2 probe mismatch with error code %s: %s',
617
617
  (errorCode, expected) => {
@@ -631,8 +631,6 @@ describe('public device lifecycle events', () => {
631
631
  [HardwareErrorCode.BleTimeoutError, true],
632
632
  [HardwareErrorCode.PollingTimeout, false],
633
633
  [HardwareErrorCode.BleDeviceBondError, false],
634
- [HardwareErrorCode.BlePeerRemovedPairingInformation, false],
635
- [HardwareErrorCode.BleBondInvalid, false],
636
634
  ] as const)('retries a BLE connection error with error code %s: %s', (errorCode, expected) => {
637
635
  const method = { payload: { connectProtocol: 'V2' } } as never;
638
636
  const error = {
@@ -685,19 +683,6 @@ describe('public device lifecycle events', () => {
685
683
  }
686
684
  );
687
685
 
688
- test.each([
689
- [HardwareErrorCode.BleDeviceBondError, true],
690
- [HardwareErrorCode.BlePeerRemovedPairingInformation, true],
691
- [HardwareErrorCode.BleBondInvalid, true],
692
- [HardwareErrorCode.DeviceNotFound, false],
693
- ] as const)('treats BLE stale-bond error code %s as terminal: %s', (errorCode, expected) => {
694
- const error = {
695
- errorCode,
696
- };
697
-
698
- expect(isTerminalBleStaleBondError(error)).toBe(expected);
699
- });
700
-
701
686
  test.each([
702
687
  [HardwareErrorCode.DeviceCheckDeviceIdError, true],
703
688
  [HardwareErrorCode.DeviceNotFound, false],
@@ -729,6 +714,48 @@ describe('public device lifecycle events', () => {
729
714
  expect(() => JSON.stringify(disconnectMessage?.payload.device)).not.toThrow();
730
715
  });
731
716
 
717
+ test.each(['V1', 'V2'] as const)(
718
+ 'preserves the %s connection identity for a transport disconnect after wipe',
719
+ protocol => {
720
+ jest.spyOn(DataManager, 'getSettings').mockReturnValue('webusb' as never);
721
+ core = initCore();
722
+ initConnector();
723
+ const messages: CoreMessage[] = [];
724
+ core.on(CORE_EVENT, message => messages.push(message));
725
+ const device = createInitializedDevice(protocol);
726
+ const connectId = device.getConnectId();
727
+ DevicePool.devicesCache[connectId] = device;
728
+
729
+ device.invalidateAfterWipe();
730
+ expect(device.toMessageObject()).toBeNull();
731
+ DevicePool.emitter.emit(DEVICE.CONNECT, device);
732
+ expect(messages.some(message => message.type === DEVICE.CONNECT)).toBe(false);
733
+ DevicePool.emitter.emit(TRANSPORT_EVENT.DEVICE_DISCONNECT, { connectId });
734
+
735
+ const disconnects = messages.filter(message => message.type === DEVICE.DISCONNECT);
736
+ expect(disconnects).toHaveLength(1);
737
+ expect(disconnects[0].payload.device).toMatchObject({
738
+ connectId,
739
+ serialNo: 'SERIAL-001',
740
+ deviceId: 'wallet-device-id',
741
+ });
742
+ expect(() => JSON.stringify(disconnects[0].payload.device)).not.toThrow();
743
+
744
+ device.features = {
745
+ ...createInitializedDevice(protocol).features,
746
+ deviceId: 'new-device-id',
747
+ initialized: false,
748
+ mode: 'notInitialized',
749
+ } as never;
750
+ DevicePool.emitter.emit(TRANSPORT_EVENT.DEVICE_DISCONNECT, { connectId });
751
+ const nextDisconnects = messages.filter(message => message.type === DEVICE.DISCONNECT);
752
+ expect(nextDisconnects[1].payload.device).toMatchObject({
753
+ deviceId: 'new-device-id',
754
+ mode: 'notInitialized',
755
+ });
756
+ }
757
+ );
758
+
732
759
  test('treats an acquired BLE device as reusable until the transport disconnects', async () => {
733
760
  jest.spyOn(DataManager, 'getSettings').mockReturnValue('desktop-web-ble' as never);
734
761
  const device = createInitializedDevice('V2');
@@ -1014,6 +1041,28 @@ describe('public device lifecycle events', () => {
1014
1041
  expect(device.wasInterruptedByUser()).toBe(false);
1015
1042
  });
1016
1043
 
1044
+ test.each(['V1', 'V2'] as const)(
1045
+ 'retires a failed USB %s session before the next user attempt',
1046
+ async protocol => {
1047
+ jest.spyOn(DataManager, 'getSettings').mockReturnValue('desktop-webusb' as never);
1048
+ const device = createInitializedDevice(protocol);
1049
+ device.originalDescriptor = { ...device.originalDescriptor, session: device.mainId };
1050
+ const disconnect = jest.fn().mockResolvedValue(undefined);
1051
+ const release = jest.fn().mockResolvedValue(undefined);
1052
+ device.deviceConnector = { disconnect, release } as never;
1053
+ device.commands = { disposed: false, dispose: jest.fn() } as never;
1054
+ (device as unknown as { deviceAcquired: boolean }).deviceAcquired = true;
1055
+ const error = ERRORS.TypedError(HardwareErrorCode.WebUsbDeviceAccessError);
1056
+ await expect(device.run(() => Promise.reject(error))).rejects.toBe(error);
1057
+ await new Promise(resolve => {
1058
+ setImmediate(resolve);
1059
+ });
1060
+ expect(disconnect).toHaveBeenCalledWith(device.mainId);
1061
+ expect(release).toHaveBeenCalledTimes(1);
1062
+ expect(device.hasDeviceAcquire()).toBe(false);
1063
+ }
1064
+ );
1065
+
1017
1066
  test('waits for the canceled run to finish releasing before cancellation completes', async () => {
1018
1067
  jest.spyOn(DataManager, 'getSettings').mockReturnValue('react-native' as never);
1019
1068
  const device = createInitializedDevice('V2');
@@ -85,6 +85,56 @@ describe('DevicePool state lifecycle', () => {
85
85
  expect(result.deviceList).toEqual([device]);
86
86
  });
87
87
 
88
+ test.each([
89
+ ['V1', undefined],
90
+ ['V1', 'wipe-path'],
91
+ ['V2', undefined],
92
+ ['V2', 'wipe-path'],
93
+ ] as const)('rediscovers a wiped %s device with connect id %s', async (protocol, connectId) => {
94
+ const descriptor = { path: 'wipe-path', protocolType: protocol } as never;
95
+ const device = Device.fromDescriptor(descriptor);
96
+ device.features = {
97
+ protocol,
98
+ deviceType: protocol === 'V2' ? 'pro2' : 'classic',
99
+ deviceId: 'old-device-id',
100
+ serialNo: 'wipe-path',
101
+ initialized: true,
102
+ mode: 'normal',
103
+ } as never;
104
+ DevicePool.devicesCache = { 'wipe-path': device };
105
+ device.invalidateAfterWipe();
106
+ expect(device.toMessageObject()).toBeNull();
107
+ const connect = jest.spyOn(device, 'connect').mockResolvedValue(true);
108
+ const initialize = jest.spyOn(device, 'initialize').mockImplementation(() => {
109
+ device.features = {
110
+ protocol,
111
+ deviceType: protocol === 'V2' ? 'pro2' : 'classic',
112
+ deviceId: 'new-device-id',
113
+ serialNo: 'wipe-path',
114
+ initialized: false,
115
+ mode: 'notInitialized',
116
+ } as never;
117
+ return Promise.resolve();
118
+ });
119
+ const release = jest.spyOn(device, 'release').mockResolvedValue(undefined);
120
+
121
+ try {
122
+ const result = await DevicePool.getDevices([descriptor], connectId);
123
+ expect(initialize).toHaveBeenCalledTimes(1);
124
+ expect(connect).toHaveBeenCalledTimes(1);
125
+ expect(release).toHaveBeenCalledTimes(1);
126
+ expect(result.deviceList[0].toMessageObject()).toMatchObject({
127
+ deviceId: 'new-device-id',
128
+ mode: 'notInitialized',
129
+ features: { initialized: false },
130
+ });
131
+ await DevicePool.getDevices([descriptor], connectId);
132
+ expect(initialize).toHaveBeenCalledTimes(1);
133
+ } finally {
134
+ jest.restoreAllMocks();
135
+ }
136
+ });
137
+
88
138
  test('keeps a discovered device when its Protocol V2 label cannot be read', async () => {
89
139
  const labelError = new Error('settings unavailable');
90
140
  const getDeviceState = jest
@@ -1,3 +1,6 @@
1
+ import { EDeviceType } from '@onekeyfe/hd-shared';
2
+
3
+ import DeviceReboot from '../src/api/protocol-v2/DeviceReboot';
1
4
  import { Device } from '../src/device/Device';
2
5
  import { UI_REQUEST } from '../src/events/ui-request';
3
6
 
@@ -23,12 +26,50 @@ const createBootloaderDevice = (protocolType: 'V1' | 'V2') => {
23
26
  };
24
27
 
25
28
  describe('Pro2 bootloader mode', () => {
26
- test('does not block Protocol V2 methods in bootloader mode', () => {
29
+ test('allows deviceReboot to return a Protocol V2 bootloader to normal mode', () => {
30
+ const device = createBootloaderDevice('V2');
31
+ const method = new DeviceReboot({
32
+ id: 1,
33
+ payload: { method: 'deviceReboot', rebootType: 'normal' },
34
+ });
35
+ method.init();
36
+
37
+ expect(device.hasUnexpectedMode(method.allowDeviceMode, method.requireDeviceMode)).toBeNull();
38
+ });
39
+
40
+ test('blocks Protocol V2 methods that are not allowed in bootloader mode', () => {
41
+ const device = createBootloaderDevice('V2');
42
+
43
+ expect(device.hasUnexpectedMode([], [])).toBe(UI_REQUEST.BOOTLOADER);
44
+ });
45
+
46
+ test('allows Protocol V2 methods that explicitly support bootloader mode', () => {
27
47
  const device = createBootloaderDevice('V2');
28
48
 
29
- expect(device.hasUnexpectedMode([], [])).toBeNull();
49
+ expect(device.hasUnexpectedMode([UI_REQUEST.BOOTLOADER], [])).toBeNull();
30
50
  });
31
51
 
52
+ test.each([EDeviceType.Pro2, EDeviceType.Neo])(
53
+ 'blocks unsupported Protocol V2 methods in %s romloader mode',
54
+ deviceType => {
55
+ const device = createBootloaderDevice('V2');
56
+ device.updateState(
57
+ {
58
+ protocol: 'V2',
59
+ identity: { deviceType },
60
+ status: { mode: 'romloader', initialized: null },
61
+ },
62
+ 'initialize'
63
+ );
64
+
65
+ expect(device.hasUnexpectedMode([], [])).toBe(UI_REQUEST.BOOTLOADER);
66
+ expect(device.hasUnexpectedMode([UI_REQUEST.NOT_INITIALIZE], [])).toBe(UI_REQUEST.BOOTLOADER);
67
+ expect(
68
+ device.hasUnexpectedMode([UI_REQUEST.BOOTLOADER, UI_REQUEST.NOT_INITIALIZE], [])
69
+ ).toBeNull();
70
+ }
71
+ );
72
+
32
73
  test('keeps the Protocol V1 bootloader restriction', () => {
33
74
  const device = createBootloaderDevice('V1');
34
75
 
@@ -4991,23 +4991,6 @@ describe('Protocol V2 firmware update targets', () => {
4991
4991
  expect(typedCall.mock.calls.map(call => call[0])).toEqual(['DeviceInfoGet']);
4992
4992
  });
4993
4993
 
4994
- test('stops V4 bootloader reconnect polling on a stale BLE bond', async () => {
4995
- const method = new FirmwareUpdateV4({
4996
- id: 1,
4997
- payload: {
4998
- method: 'firmwareUpdateV4',
4999
- },
5000
- });
5001
- const staleBondError = ERRORS.TypedError(HardwareErrorCode.BlePeerRemovedPairingInformation);
5002
- const reconnectProtocolV2Device = jest.fn().mockRejectedValue(staleBondError);
5003
- (method as any).reconnectProtocolV2Device = reconnectProtocolV2Device;
5004
-
5005
- await expect((method as any).waitForProtocolV2BootloaderMode(60_000, 0)).rejects.toBe(
5006
- staleBondError
5007
- );
5008
- expect(reconnectProtocolV2Device).toHaveBeenCalledTimes(1);
5009
- });
5010
-
5011
4994
  test('polls again when Protocol V2 bootloader serial is temporarily unavailable', async () => {
5012
4995
  const method = new FirmwareUpdateV4({
5013
4996
  id: 1,
@@ -9009,36 +8992,6 @@ describe('Protocol V2 firmware update targets', () => {
9009
8992
  );
9010
8993
  });
9011
8994
 
9012
- test('does not retry or wrap a stale BLE bond during a V4 file transfer', async () => {
9013
- const method = new FirmwareUpdateV4({
9014
- id: 1,
9015
- payload: {
9016
- method: 'firmwareUpdateV4',
9017
- },
9018
- });
9019
- const staleBondError = ERRORS.TypedError(HardwareErrorCode.BlePeerRemovedPairingInformation);
9020
- (method as any).fileWriteChunk = jest.fn().mockRejectedValue(staleBondError);
9021
- const recoverProtocolV2FileTransfer = jest.fn();
9022
- (method as any).recoverProtocolV2FileTransfer = recoverProtocolV2FileTransfer;
9023
- const source = await openFirmwareByteSource({
9024
- binary: new Uint8Array([1]).buffer,
9025
- });
9026
-
9027
- try {
9028
- await expect(
9029
- (method as any).protocolV2SourceUpdateProcess({
9030
- source,
9031
- filePath: 'vol1:firmware.bin',
9032
- processedSize: 0,
9033
- totalSize: 1,
9034
- })
9035
- ).rejects.toBe(staleBondError);
9036
- } finally {
9037
- await source?.close();
9038
- }
9039
- expect(recoverProtocolV2FileTransfer).not.toHaveBeenCalled();
9040
- });
9041
-
9042
8995
  test('does not recover or wrap a cancelled V4 file transfer', async () => {
9043
8996
  const method = new FirmwareUpdateV4({
9044
8997
  id: 1,
@@ -73,24 +73,26 @@ describe('live device state reads', () => {
73
73
  }
74
74
  );
75
75
 
76
- test('rejects settings scope in loader mode after minimal mode detection', async () => {
77
- const getDeviceState = jest.fn().mockResolvedValue({ status: { mode: 'bootloader' } });
78
- const method = new GetDeviceState({
79
- id: 1,
80
- payload: { method: 'getDeviceState', scope: 'settings' },
81
- });
82
- method.init();
83
- (method as any).device = {
84
- isProtocolV2: () => true,
85
- getCurrentFirmwareType: () => 'universal',
86
- getDeviceState,
87
- };
76
+ test.each(['bootloader', 'romloader'] as const)(
77
+ 'reports %s mode for settings scope after minimal mode detection',
78
+ async mode => {
79
+ const getDeviceState = jest.fn().mockResolvedValue({ status: { mode } });
80
+ const method = new GetDeviceState({
81
+ id: 1,
82
+ payload: { method: 'getDeviceState', scope: 'settings' },
83
+ });
84
+ method.init();
85
+ (method as any).device = {
86
+ isProtocolV2: () => true,
87
+ getDeviceState,
88
+ };
88
89
 
89
- await expect(method.run()).rejects.toMatchObject({
90
- errorCode: HardwareErrorCode.DeviceNotSupportMethod,
91
- });
92
- expect(getDeviceState).toHaveBeenCalledWith({ refreshSections: ['status', 'settings'] });
93
- });
90
+ await expect(method.run()).rejects.toMatchObject({
91
+ errorCode: HardwareErrorCode.NotAllowInBootloaderMode,
92
+ });
93
+ expect(getDeviceState).toHaveBeenCalledWith({ refreshSections: ['status', 'settings'] });
94
+ }
95
+ );
94
96
 
95
97
  test('rejects an unknown scope', () => {
96
98
  const method = new GetDeviceState({
@@ -54,6 +54,14 @@ describe('SolSignOffchainMessage', () => {
54
54
  });
55
55
  });
56
56
 
57
+ test('maps omitted required signers to an empty repeated field', () => {
58
+ const method = createMethod({ requiredSigners: undefined });
59
+
60
+ method.init();
61
+
62
+ expect(method.params.required_signers).toEqual([]);
63
+ });
64
+
57
65
  test.each([
58
66
  { requiredSigners: ['11'], error: '32-byte hex public key' },
59
67
  { requiredSigners: [signerB, signerA], error: 'strictly sorted and unique' },
@@ -1 +1 @@
1
- {"version":3,"file":"FirmwareUpdateV4.d.ts","sourceRoot":"","sources":["../../src/api/FirmwareUpdateV4.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"FirmwareUpdateV4.d.ts","sourceRoot":"","sources":["../../src/api/FirmwareUpdateV4.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"GetDeviceState.d.ts","sourceRoot":"","sources":["../../src/api/GetDeviceState.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAG1C,OAAO,KAAK,EAAoB,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AAU1F,MAAM,CAAC,OAAO,OAAO,cAAe,SAAQ,UAAU,CACpD,QAAQ,CAAC,IAAI,CAAC,oBAAoB,EAAE,OAAO,CAAC,CAAC,CAC9C;IACC,qBAAqB;IAIrB,IAAI;IAmBE,GAAG;CAgBV"}
1
+ {"version":3,"file":"GetDeviceState.d.ts","sourceRoot":"","sources":["../../src/api/GetDeviceState.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAG1C,OAAO,KAAK,EAAoB,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AAU1F,MAAM,CAAC,OAAO,OAAO,cAAe,SAAQ,UAAU,CACpD,QAAQ,CAAC,IAAI,CAAC,oBAAoB,EAAE,OAAO,CAAC,CAAC,CAC9C;IACC,qBAAqB;IAIrB,IAAI;IAmBE,GAAG;CAaV"}
@@ -7,6 +7,9 @@ export default class BTCDeriveContextHash extends BaseMethod<BabylonDeriveContex
7
7
  classic1s: {
8
8
  min: string;
9
9
  };
10
+ classicpure: {
11
+ min: string;
12
+ };
10
13
  };
11
14
  run(): Promise<import("@onekeyfe/hd-transport").BabylonDerivedContextHash>;
12
15
  }
@@ -1 +1 @@
1
- {"version":3,"file":"BTCDeriveContextHash.d.ts","sourceRoot":"","sources":["../../../src/api/btc/BTCDeriveContextHash.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAI3C,OAAO,KAAK,EAAE,wBAAwB,EAA2B,MAAM,wBAAwB,CAAC;AAUhG,MAAM,CAAC,OAAO,OAAO,oBAAqB,SAAQ,UAAU,CAAC,wBAAwB,CAAC;IACpF,wBAAwB,UAAQ;IAEhC,IAAI;IAgDJ,eAAe;;;;;IAIT,GAAG;CAcV"}
1
+ {"version":3,"file":"BTCDeriveContextHash.d.ts","sourceRoot":"","sources":["../../../src/api/btc/BTCDeriveContextHash.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAI3C,OAAO,KAAK,EAAE,wBAAwB,EAA2B,MAAM,wBAAwB,CAAC;AAUhG,MAAM,CAAC,OAAO,OAAO,oBAAqB,SAAQ,UAAU,CAAC,wBAAwB,CAAC;IACpF,wBAAwB,UAAQ;IAEhC,IAAI;IAgDJ,eAAe;;;;;;;;IAOT,GAAG;CAcV"}
@@ -1 +1 @@
1
- {"version":3,"file":"DeviceReboot.d.ts","sourceRoot":"","sources":["../../../src/api/protocol-v2/DeviceReboot.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAG3C,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAEpD,MAAM,CAAC,OAAO,OAAO,YAAa,SAAQ,UAAU,CAAC,kBAAkB,CAAC;IACtE,qBAAqB;IAIrB,IAAI;IAYE,GAAG;CAQV"}
1
+ {"version":3,"file":"DeviceReboot.d.ts","sourceRoot":"","sources":["../../../src/api/protocol-v2/DeviceReboot.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAG3C,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAEpD,MAAM,CAAC,OAAO,OAAO,YAAa,SAAQ,UAAU,CAAC,kBAAkB,CAAC;IACtE,qBAAqB;IAIrB,IAAI;IAaE,GAAG;CAQV"}
@@ -12,7 +12,6 @@ export declare function getPostCallPendingPromise(method: BaseMethod, device: De
12
12
  export declare function isRetryableBleProtocolV2ProbeError(method: BaseMethod, error: unknown): boolean;
13
13
  export declare function isRetryableBleConnectionError(method: BaseMethod, error: unknown): boolean;
14
14
  export declare function isMissingDetectedProtocolV2Error(method: BaseMethod, error: unknown): boolean;
15
- export declare function isTerminalBleStaleBondError(error: unknown): boolean;
16
15
  export declare function isDeviceIdentityMismatchError(error: unknown): boolean;
17
16
  export declare function resolveBleConnectProtocol(method: BaseMethod): ProtocolType | undefined;
18
17
  export declare const cancel: (context: CoreContext, connectId?: string) => void;