@onekeyfe/hd-core 1.2.8-alpha.2 → 1.2.8-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 (48) hide show
  1. package/README.md +27 -0
  2. package/__tests__/DeviceCommands.test.ts +13 -66
  3. package/__tests__/btc-derive-context-hash.test.ts +175 -0
  4. package/__tests__/core-error-output.test.ts +3 -34
  5. package/__tests__/device-lifecycle-events.test.ts +17 -22
  6. package/__tests__/protocol-v2-bootloader-mode.test.ts +2 -31
  7. package/__tests__/protocol-v2.test.ts +47 -0
  8. package/__tests__/refresh-device-state.test.ts +17 -19
  9. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  10. package/dist/api/GetDeviceState.d.ts.map +1 -1
  11. package/dist/api/btc/BTCDeriveContextHash.d.ts +13 -0
  12. package/dist/api/btc/BTCDeriveContextHash.d.ts.map +1 -0
  13. package/dist/api/index.d.ts +1 -0
  14. package/dist/api/index.d.ts.map +1 -1
  15. package/dist/core/index.d.ts +1 -0
  16. package/dist/core/index.d.ts.map +1 -1
  17. package/dist/device/DeviceCommands.d.ts +4 -4
  18. package/dist/device/DeviceCommands.d.ts.map +1 -1
  19. package/dist/events/logBlockEvent.d.ts.map +1 -1
  20. package/dist/index.d.ts +14 -6
  21. package/dist/index.js +142 -21
  22. package/dist/inject.d.ts.map +1 -1
  23. package/dist/types/api/btcDeriveContextHash.d.ts +10 -0
  24. package/dist/types/api/btcDeriveContextHash.d.ts.map +1 -0
  25. package/dist/types/api/export.d.ts +1 -0
  26. package/dist/types/api/export.d.ts.map +1 -1
  27. package/dist/types/api/index.d.ts +2 -0
  28. package/dist/types/api/index.d.ts.map +1 -1
  29. package/dist/types/params.d.ts +0 -1
  30. package/dist/types/params.d.ts.map +1 -1
  31. package/dist/utils/patch.d.ts +1 -1
  32. package/dist/utils/patch.d.ts.map +1 -1
  33. package/package.json +4 -4
  34. package/src/api/FirmwareUpdateV4.ts +25 -8
  35. package/src/api/GetDeviceState.ts +5 -2
  36. package/src/api/btc/BTCDeriveContextHash.ts +87 -0
  37. package/src/api/index.ts +1 -0
  38. package/src/core/index.ts +6 -2
  39. package/src/data/messages/messages.json +51 -0
  40. package/src/device/Device.ts +1 -1
  41. package/src/device/DeviceCommands.ts +1 -15
  42. package/src/events/core.ts +2 -2
  43. package/src/events/logBlockEvent.ts +1 -0
  44. package/src/inject.ts +2 -0
  45. package/src/types/api/btcDeriveContextHash.ts +15 -0
  46. package/src/types/api/export.ts +1 -0
  47. package/src/types/api/index.ts +2 -0
  48. package/src/types/params.ts +1 -1
package/README.md CHANGED
@@ -29,4 +29,31 @@ import Core from '@onekeyfe/hd-core';
29
29
  Documentation is available [Hardware SDK Getting Started](https://developer.onekey.so/en/hardware-sdk/getting-started)
30
30
 
31
31
  ## Examples
32
+
33
+ ### Babylon context hash
34
+
35
+ `btcDeriveContextHash(connectId, deviceId, params)` requires Classic 1s firmware
36
+ 3.21.0 or later and Protocol V1. It uses the existing wallet session and device
37
+ confirmation flow. Other device families and Protocol V2 are not supported.
38
+
39
+ ```typescript
40
+ const result = await HardwareSDK.btcDeriveContextHash(connectId, deviceId, {
41
+ path: "m/84'/0'/0'/0/0", // The currently selected Bitcoin key, not the IKM path.
42
+ appName: 'babylon-btc-vault',
43
+ context: 'deadbeef',
44
+ network: 'bitcoin-mainnet',
45
+ });
46
+ // result.payload.secret is a 32-byte value encoded as 64 lowercase hex characters.
47
+ ```
48
+
49
+ `appName` must contain 1–64 ASCII lowercase letters, digits or hyphens. `context`
50
+ must be lowercase hex without `0x`, containing 1–1024 decoded bytes. `network` is
51
+ required: `bitcoin-mainnet`, `bitcoin-testnet` (testnet3/testnet4), `bitcoin-signet`,
52
+ or `bitcoin-regtest`. The SDK infers the Bitcoin input script type from `path` and
53
+ encodes `appName` as ASCII bytes; the device performs the derivation internally.
54
+ Device rejection and unsupported firmware errors propagate through the normal
55
+ SDK response. Treat the returned secret as sensitive and do not log or persist it.
56
+
57
+ See the [Babylon specification](https://github.com/babylonlabs-io/babylon-toolkit/blob/main/docs/specs/derive-context-hash.md).
58
+
32
59
  // TODO: add example url
@@ -130,6 +130,19 @@ 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
+
133
146
  it('logs canonical DeviceStatus response fields without exposing the device ID', async () => {
134
147
  const commands = createCommands();
135
148
  const log = getLogger(LoggerNames.DeviceCommands);
@@ -401,27 +414,6 @@ describe('DeviceCommands failure mapping', () => {
401
414
  ).rejects.toMatchObject({ errorCode: HardwareErrorCode.DeviceBusy });
402
415
  });
403
416
 
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
-
425
417
  it('maps the current AskPin passphrase-disabled response without relying on a subcode', async () => {
426
418
  const commands = createCommands();
427
419
 
@@ -559,51 +551,6 @@ describe('DeviceCommands failure mapping', () => {
559
551
  }
560
552
  );
561
553
 
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
-
607
554
  it('does not use the legacy cancellation-message fallback for Protocol V1', async () => {
608
555
  const commands = createCommands();
609
556
  (commands.device.isProtocolV2 as jest.Mock).mockReturnValue(false);
@@ -0,0 +1,175 @@
1
+ import { EFirmwareType, HardwareErrorCode } from '@onekeyfe/hd-shared';
2
+
3
+ import BTCDeriveContextHash from '../src/api/btc/BTCDeriveContextHash';
4
+ import { findMethod } from '../src/api/utils';
5
+ import { getLogBlockLabel, getSafeLogPayload } from '../src/events/logBlockEvent';
6
+ import { createCoreApi } from '../src/inject';
7
+
8
+ import type { Device } from '../src/device/Device';
9
+
10
+ jest.mock('../src/data/config', () => ({
11
+ getSDKVersion: jest.fn(() => '1.0.0'),
12
+ DEFAULT_DOMAIN: 'https://jssdk.onekey.so/1.0.0/',
13
+ }));
14
+
15
+ const createMethod = (overrides: Record<string, unknown> = {}) =>
16
+ new BTCDeriveContextHash({
17
+ id: 1,
18
+ payload: {
19
+ method: 'btcDeriveContextHash',
20
+ path: "m/84'/0'/0'/0/0",
21
+ appName: 'test-app',
22
+ context: 'deadbeef',
23
+ network: 'bitcoin-mainnet',
24
+ ...overrides,
25
+ },
26
+ });
27
+
28
+ describe('btcDeriveContextHash', () => {
29
+ test('forwards the public API call through the existing dispatcher', async () => {
30
+ const call = jest
31
+ .fn()
32
+ .mockResolvedValue({ success: true, payload: { secret: '00'.repeat(32) } });
33
+ const api = createCoreApi(call);
34
+ const params = {
35
+ path: "m/84'/0'/0'/0/0",
36
+ appName: 'test-app',
37
+ context: 'deadbeef',
38
+ network: 'bitcoin-mainnet' as const,
39
+ };
40
+ await api.btcDeriveContextHash('connect-id', 'device-id', params);
41
+ expect(call).toHaveBeenCalledWith({
42
+ ...params,
43
+ connectId: 'connect-id',
44
+ deviceId: 'device-id',
45
+ method: 'btcDeriveContextHash',
46
+ });
47
+ });
48
+
49
+ test.each([
50
+ ['bitcoin-mainnet', 'BITCOIN_MAINNET'],
51
+ ['bitcoin-testnet', 'BITCOIN_TESTNET'],
52
+ ['bitcoin-signet', 'BITCOIN_SIGNET'],
53
+ ['bitcoin-regtest', 'BITCOIN_REGTEST'],
54
+ ])('maps %s and returns the device result', async (network, wireNetwork) => {
55
+ const method = createMethod({ network });
56
+ method.init();
57
+ const response = { secret: '00'.repeat(32) };
58
+ const typedCall = jest.fn().mockResolvedValue({ message: response });
59
+ method.device = { commands: { typedCall } } as unknown as Device;
60
+
61
+ await expect(method.run()).resolves.toBe(response);
62
+ expect(typedCall).toHaveBeenCalledWith(
63
+ 'BabylonDeriveContextHash',
64
+ 'BabylonDerivedContextHash',
65
+ {
66
+ address_n: [2147483732, 2147483648, 2147483648, 0, 0],
67
+ script_type: 'SPENDWITNESS',
68
+ app_name: '746573742d617070',
69
+ context: 'deadbeef',
70
+ network: wireNetwork,
71
+ }
72
+ );
73
+ });
74
+
75
+ test.each([
76
+ ["m/44'/0'/0'/0/0", 'SPENDADDRESS'],
77
+ ["m/49'/0'/0'/0/0", 'SPENDP2SHWITNESS'],
78
+ [[2147483734, 2147483648, 2147483648, 0, 0], 'SPENDTAPROOT'],
79
+ ])('uses the selected path script type for %s', (path, scriptType) => {
80
+ const method = createMethod({ path });
81
+ method.init();
82
+ expect(method.params.script_type).toBe(scriptType);
83
+ });
84
+
85
+ test('accepts the maximum app name and context sizes', () => {
86
+ const method = createMethod({ appName: 'a'.repeat(64), context: 'ab'.repeat(1024) });
87
+ expect(() => method.init()).not.toThrow();
88
+ expect(method.params.context).toHaveLength(2048);
89
+ });
90
+
91
+ test.each([
92
+ { appName: '' },
93
+ { appName: 'a'.repeat(65) },
94
+ { appName: 'Test-App' },
95
+ { appName: 'test_app' },
96
+ { appName: '你好' },
97
+ { appName: null },
98
+ { context: '' },
99
+ { context: 'abc' },
100
+ { context: 'DEADBEEF' },
101
+ { context: '0xdeadbeef' },
102
+ { context: 'gg' },
103
+ { context: 'ab'.repeat(1025) },
104
+ { context: null },
105
+ { network: '' },
106
+ { network: 'litecoin' },
107
+ { network: 'toString' },
108
+ { network: 1 },
109
+ { path: 'm' },
110
+ { path: "m/84x'/0'/0'/0/0" },
111
+ { path: "m/2147483648'/0'/0'/0/0" },
112
+ { path: [2147483732, 2147483648, 2147483648, 0, 0.5] },
113
+ { path: [2147483732, 2147483648, 2147483648, 0, 4294967296] },
114
+ ])('rejects invalid inputs before a device call: %j', overrides => {
115
+ expect(() => createMethod(overrides).init()).toThrow(
116
+ expect.objectContaining({ errorCode: HardwareErrorCode.CallMethodInvalidParameter })
117
+ );
118
+ });
119
+
120
+ test('rejects sparse numeric paths before a device call', () => {
121
+ const sparsePath = [2147483732, 2147483648, 2147483648, 0, 5];
122
+ delete sparsePath[3];
123
+
124
+ [Array<number>(3), sparsePath].forEach(path => {
125
+ expect(() => createMethod({ path }).init()).toThrow(
126
+ expect.objectContaining({ errorCode: HardwareErrorCode.CallMethodInvalidParameter })
127
+ );
128
+ });
129
+ });
130
+
131
+ test('registers the public method and restricts it to supported firmware and protocol', () => {
132
+ const method = findMethod({ id: 1, payload: { ...createMethod().payload } });
133
+ method.init();
134
+ expect(method).toBeInstanceOf(BTCDeriveContextHash);
135
+ expect(method.strictCheckDeviceSupport).toBe(true);
136
+ expect(method.getVersionRange()).toEqual({ classic1s: { min: '3.21.0' } });
137
+ expect(() => method.assertProtocolSupported('V2', EFirmwareType.Universal)).toThrow(
138
+ expect.objectContaining({ errorCode: HardwareErrorCode.DeviceNotSupportMethod })
139
+ );
140
+ });
141
+
142
+ test('propagates device rejection without returning a secret', async () => {
143
+ const method = createMethod();
144
+ method.init();
145
+ const error = new Error('User rejected');
146
+ method.device = {
147
+ commands: { typedCall: jest.fn().mockRejectedValue(error) },
148
+ } as unknown as Device;
149
+ await expect(method.run()).rejects.toBe(error);
150
+ });
151
+
152
+ test.each([undefined, '00', '00'.repeat(33)])(
153
+ 'rejects malformed device responses',
154
+ async secret => {
155
+ const method = createMethod();
156
+ method.init();
157
+ method.device = {
158
+ commands: { typedCall: jest.fn().mockResolvedValue({ message: { secret } }) },
159
+ } as unknown as Device;
160
+ await expect(method.run()).rejects.toMatchObject({
161
+ errorCode: HardwareErrorCode.CallMethodError,
162
+ });
163
+ }
164
+ );
165
+
166
+ test('blocks API request and response payloads in logs', () => {
167
+ const request = { payload: { method: 'btcDeriveContextHash' } };
168
+ const label = getLogBlockLabel(request);
169
+ expect(label).toBe('btcDeriveContextHash');
170
+ expect(getSafeLogPayload({ payload: { secret: '00'.repeat(32) } }, label)).toEqual({
171
+ method: 'btcDeriveContextHash',
172
+ payload: '[REDACTED]',
173
+ });
174
+ });
175
+ });
@@ -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, createErrorMessage } from '../src/events';
6
+ import { IFRAME } 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,23 +28,6 @@ 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
-
48
31
  test.each([
49
32
  ['webusb', false],
50
33
  ['desktop-webusb', false],
@@ -125,28 +108,14 @@ describe('Core 错误输出边界', () => {
125
108
  HardwareErrorCode.BleDeviceBondedCanceled,
126
109
  HardwareErrorCode.BleDeviceDisconnected,
127
110
  HardwareErrorCode.PollingTimeout,
128
- HardwareErrorCode.BlePoweredOff,
129
- HardwareErrorCode.BleUnsupported,
130
- HardwareErrorCode.BridgeNeedsPermission,
131
- HardwareErrorCode.WebUsbDeviceAccessError,
132
111
  ])(
133
- 'preserves terminal transport error %s in the public response without polling again',
112
+ 'preserves terminal BLE error %s in the public response without polling again',
134
113
  async errorCode => {
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
- );
114
+ jest.spyOn(DataManager, 'getSettings').mockReturnValue('react-native' as never);
145
115
  const error = ERRORS.TypedError(errorCode);
146
116
  const acquire = jest.fn().mockRejectedValue(error);
147
117
  jest.spyOn(TransportManager, 'getTransport').mockReturnValue({
148
118
  acquire,
149
- enumerate: jest.fn().mockResolvedValue([{ path: 'ble-pairing-test' }]),
150
119
  release: jest.fn().mockResolvedValue(true),
151
120
  stop: jest.fn().mockResolvedValue(undefined),
152
121
  } as never);
@@ -10,6 +10,7 @@ import {
10
10
  isMissingDetectedProtocolV2Error,
11
11
  isRetryableBleConnectionError,
12
12
  isRetryableBleProtocolV2ProbeError,
13
+ isTerminalBleStaleBondError,
13
14
  resolveBleConnectProtocol,
14
15
  } from '../src/core';
15
16
  import { DataManager } from '../src/data-manager';
@@ -610,6 +611,7 @@ describe('public device lifecycle events', () => {
610
611
  test.each([
611
612
  [HardwareErrorCode.RuntimeError, true],
612
613
  [HardwareErrorCode.BleDeviceBondError, false],
614
+ [HardwareErrorCode.BlePeerRemovedPairingInformation, false],
613
615
  ] as const)(
614
616
  'retries a Protocol V2 probe mismatch with error code %s: %s',
615
617
  (errorCode, expected) => {
@@ -629,6 +631,8 @@ describe('public device lifecycle events', () => {
629
631
  [HardwareErrorCode.BleTimeoutError, true],
630
632
  [HardwareErrorCode.PollingTimeout, false],
631
633
  [HardwareErrorCode.BleDeviceBondError, false],
634
+ [HardwareErrorCode.BlePeerRemovedPairingInformation, false],
635
+ [HardwareErrorCode.BleBondInvalid, false],
632
636
  ] as const)('retries a BLE connection error with error code %s: %s', (errorCode, expected) => {
633
637
  const method = { payload: { connectProtocol: 'V2' } } as never;
634
638
  const error = {
@@ -681,6 +685,19 @@ describe('public device lifecycle events', () => {
681
685
  }
682
686
  );
683
687
 
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
+
684
701
  test.each([
685
702
  [HardwareErrorCode.DeviceCheckDeviceIdError, true],
686
703
  [HardwareErrorCode.DeviceNotFound, false],
@@ -997,28 +1014,6 @@ describe('public device lifecycle events', () => {
997
1014
  expect(device.wasInterruptedByUser()).toBe(false);
998
1015
  });
999
1016
 
1000
- test.each(['V1', 'V2'] as const)(
1001
- 'retires a failed USB %s session before the next user attempt',
1002
- async protocol => {
1003
- jest.spyOn(DataManager, 'getSettings').mockReturnValue('desktop-webusb' as never);
1004
- const device = createInitializedDevice(protocol);
1005
- device.originalDescriptor = { ...device.originalDescriptor, session: device.mainId };
1006
- const disconnect = jest.fn().mockResolvedValue(undefined);
1007
- const release = jest.fn().mockResolvedValue(undefined);
1008
- device.deviceConnector = { disconnect, release } as never;
1009
- device.commands = { disposed: false, dispose: jest.fn() } as never;
1010
- (device as unknown as { deviceAcquired: boolean }).deviceAcquired = true;
1011
- const error = ERRORS.TypedError(HardwareErrorCode.WebUsbDeviceAccessError);
1012
- await expect(device.run(() => Promise.reject(error))).rejects.toBe(error);
1013
- await new Promise(resolve => {
1014
- setImmediate(resolve);
1015
- });
1016
- expect(disconnect).toHaveBeenCalledWith(device.mainId);
1017
- expect(release).toHaveBeenCalledTimes(1);
1018
- expect(device.hasDeviceAcquire()).toBe(false);
1019
- }
1020
- );
1021
-
1022
1017
  test('waits for the canceled run to finish releasing before cancellation completes', async () => {
1023
1018
  jest.spyOn(DataManager, 'getSettings').mockReturnValue('react-native' as never);
1024
1019
  const device = createInitializedDevice('V2');
@@ -1,5 +1,3 @@
1
- import { EDeviceType } from '@onekeyfe/hd-shared';
2
-
3
1
  import { Device } from '../src/device/Device';
4
2
  import { UI_REQUEST } from '../src/events/ui-request';
5
3
 
@@ -25,39 +23,12 @@ const createBootloaderDevice = (protocolType: 'V1' | 'V2') => {
25
23
  };
26
24
 
27
25
  describe('Pro2 bootloader mode', () => {
28
- test('blocks Protocol V2 methods that are not allowed in bootloader mode', () => {
29
- const device = createBootloaderDevice('V2');
30
-
31
- expect(device.hasUnexpectedMode([], [])).toBe(UI_REQUEST.BOOTLOADER);
32
- });
33
-
34
- test('allows Protocol V2 methods that explicitly support bootloader mode', () => {
26
+ test('does not block Protocol V2 methods in bootloader mode', () => {
35
27
  const device = createBootloaderDevice('V2');
36
28
 
37
- expect(device.hasUnexpectedMode([UI_REQUEST.BOOTLOADER], [])).toBeNull();
29
+ expect(device.hasUnexpectedMode([], [])).toBeNull();
38
30
  });
39
31
 
40
- test.each([EDeviceType.Pro2, EDeviceType.Neo])(
41
- 'blocks unsupported Protocol V2 methods in %s romloader mode',
42
- deviceType => {
43
- const device = createBootloaderDevice('V2');
44
- device.updateState(
45
- {
46
- protocol: 'V2',
47
- identity: { deviceType },
48
- status: { mode: 'romloader', initialized: null },
49
- },
50
- 'initialize'
51
- );
52
-
53
- expect(device.hasUnexpectedMode([], [])).toBe(UI_REQUEST.BOOTLOADER);
54
- expect(device.hasUnexpectedMode([UI_REQUEST.NOT_INITIALIZE], [])).toBe(UI_REQUEST.BOOTLOADER);
55
- expect(
56
- device.hasUnexpectedMode([UI_REQUEST.BOOTLOADER, UI_REQUEST.NOT_INITIALIZE], [])
57
- ).toBeNull();
58
- }
59
- );
60
-
61
32
  test('keeps the Protocol V1 bootloader restriction', () => {
62
33
  const device = createBootloaderDevice('V1');
63
34
 
@@ -4991,6 +4991,23 @@ 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
+
4994
5011
  test('polls again when Protocol V2 bootloader serial is temporarily unavailable', async () => {
4995
5012
  const method = new FirmwareUpdateV4({
4996
5013
  id: 1,
@@ -8992,6 +9009,36 @@ describe('Protocol V2 firmware update targets', () => {
8992
9009
  );
8993
9010
  });
8994
9011
 
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
+
8995
9042
  test('does not recover or wrap a cancelled V4 file transfer', async () => {
8996
9043
  const method = new FirmwareUpdateV4({
8997
9044
  id: 1,
@@ -73,26 +73,24 @@ describe('live device state reads', () => {
73
73
  }
74
74
  );
75
75
 
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
- };
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
+ };
89
88
 
90
- await expect(method.run()).rejects.toMatchObject({
91
- errorCode: HardwareErrorCode.NotAllowInBootloaderMode,
92
- });
93
- expect(getDeviceState).toHaveBeenCalledWith({ refreshSections: ['status', 'settings'] });
94
- }
95
- );
89
+ await expect(method.run()).rejects.toMatchObject({
90
+ errorCode: HardwareErrorCode.DeviceNotSupportMethod,
91
+ });
92
+ expect(getDeviceState).toHaveBeenCalledWith({ refreshSections: ['status', 'settings'] });
93
+ });
96
94
 
97
95
  test('rejects an unknown scope', () => {
98
96
  const method = new GetDeviceState({
@@ -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;CAaV"}
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"}
@@ -0,0 +1,13 @@
1
+ import { BaseMethod } from '../BaseMethod';
2
+ import type { BabylonDeriveContextHash } from '@onekeyfe/hd-transport';
3
+ export default class BTCDeriveContextHash extends BaseMethod<BabylonDeriveContextHash> {
4
+ strictCheckDeviceSupport: boolean;
5
+ init(): void;
6
+ getVersionRange(): {
7
+ classic1s: {
8
+ min: string;
9
+ };
10
+ };
11
+ run(): Promise<import("@onekeyfe/hd-transport").BabylonDerivedContextHash>;
12
+ }
13
+ //# sourceMappingURL=BTCDeriveContextHash.d.ts.map
@@ -0,0 +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"}
@@ -52,6 +52,7 @@ export { default as allNetworkGetAddress } from './allnetwork/AllNetworkGetAddre
52
52
  export { default as allNetworkGetAddressByLoop } from './allnetwork/AllNetworkGetAddressByLoop';
53
53
  export { default as btcGetAddress } from './btc/BTCGetAddress';
54
54
  export { default as btcGetPublicKey } from './btc/BTCGetPublicKey';
55
+ export { default as btcDeriveContextHash } from './btc/BTCDeriveContextHash';
55
56
  export { default as btcSignMessage } from './btc/BTCSignMessage';
56
57
  export { default as btcSignPsbt } from './btc/BTCSignPsbt';
57
58
  export { default as btcSignTransaction } from './btc/BTCSignTransaction';