@onekeyfe/hd-core 1.2.8 → 1.2.9

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 (167) hide show
  1. package/README.md +27 -0
  2. package/__tests__/AllNetworkGetAddressBase.tracing.test.ts +13 -4
  3. package/__tests__/DeviceCommands.test.ts +91 -13
  4. package/__tests__/allNetworkGetAddress-privacy-networks.test.ts +77 -0
  5. package/__tests__/allNetworkGetAddress-skippable.test.ts +49 -0
  6. package/__tests__/btc-derive-context-hash.test.ts +200 -0
  7. package/__tests__/core-dispose.test.ts +114 -1
  8. package/__tests__/core-error-output.test.ts +55 -3
  9. package/__tests__/device-connector-protocol.test.ts +5 -2
  10. package/__tests__/device-lifecycle-events.test.ts +191 -27
  11. package/__tests__/device-pool-state.test.ts +50 -0
  12. package/__tests__/firmware-update/firmware-artifact-source.test.ts +11 -0
  13. package/__tests__/logBlockEvent.test.ts +331 -9
  14. package/__tests__/logger.test.ts +18 -0
  15. package/__tests__/monero-get-address.test.ts +147 -0
  16. package/__tests__/monero-key-image-sync.test.ts +272 -0
  17. package/__tests__/monero-sign-transaction.test.ts +436 -0
  18. package/__tests__/protocol-v2-bootloader-mode.test.ts +43 -2
  19. package/__tests__/protocol-v2-key-export-confirmation.test.ts +75 -0
  20. package/__tests__/protocol-v2-resources.test.ts +14 -0
  21. package/__tests__/protocol-v2.test.ts +841 -46
  22. package/__tests__/protocolV2FileWrite.test.ts +454 -7
  23. package/__tests__/refresh-device-state.test.ts +19 -17
  24. package/__tests__/search-devices.test.ts +36 -0
  25. package/__tests__/sol-sign-offchain-message.test.ts +8 -0
  26. package/__tests__/usb-identity-probe.test.ts +358 -0
  27. package/__tests__/zcash-protobuf.test.ts +152 -0
  28. package/__tests__/zcash-sign-pczt.test.ts +525 -0
  29. package/dist/api/BaseMethod.d.ts.map +1 -1
  30. package/dist/api/FileWrite.d.ts.map +1 -1
  31. package/dist/api/FirmwareUpdateV4.d.ts +5 -0
  32. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  33. package/dist/api/GetDeviceState.d.ts.map +1 -1
  34. package/dist/api/SearchDevices.d.ts +21 -1
  35. package/dist/api/SearchDevices.d.ts.map +1 -1
  36. package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts +3 -0
  37. package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts.map +1 -1
  38. package/dist/api/btc/BTCDeriveContextHash.d.ts +16 -0
  39. package/dist/api/btc/BTCDeriveContextHash.d.ts.map +1 -0
  40. package/dist/api/firmware/FirmwareArtifactSource.d.ts.map +1 -1
  41. package/dist/api/helpers/pathUtils.d.ts +1 -0
  42. package/dist/api/helpers/pathUtils.d.ts.map +1 -1
  43. package/dist/api/helpers/protocolV2FileWrite.d.ts +5 -1
  44. package/dist/api/helpers/protocolV2FileWrite.d.ts.map +1 -1
  45. package/dist/api/index.d.ts +8 -0
  46. package/dist/api/index.d.ts.map +1 -1
  47. package/dist/api/monero/MoneroGetAddress.d.ts +11 -0
  48. package/dist/api/monero/MoneroGetAddress.d.ts.map +1 -0
  49. package/dist/api/monero/MoneroGetWatchKey.d.ts +11 -0
  50. package/dist/api/monero/MoneroGetWatchKey.d.ts.map +1 -0
  51. package/dist/api/monero/MoneroKeyImageSync.d.ts +20 -0
  52. package/dist/api/monero/MoneroKeyImageSync.d.ts.map +1 -0
  53. package/dist/api/monero/MoneroSignTransaction.d.ts +17 -0
  54. package/dist/api/monero/MoneroSignTransaction.d.ts.map +1 -0
  55. package/dist/api/monero/helpers.d.ts +5 -0
  56. package/dist/api/monero/helpers.d.ts.map +1 -0
  57. package/dist/api/protocol-v2/DeviceReboot.d.ts.map +1 -1
  58. package/dist/api/protocol-v2/DeviceUploadNft.d.ts.map +1 -1
  59. package/dist/api/protocol-v2/DeviceUploadWallpaper.d.ts.map +1 -1
  60. package/dist/api/zcash/ZcashGetAddress.d.ts +12 -0
  61. package/dist/api/zcash/ZcashGetAddress.d.ts.map +1 -0
  62. package/dist/api/zcash/ZcashGetFullViewingKey.d.ts +12 -0
  63. package/dist/api/zcash/ZcashGetFullViewingKey.d.ts.map +1 -0
  64. package/dist/api/zcash/ZcashSignPczt.d.ts +16 -0
  65. package/dist/api/zcash/ZcashSignPczt.d.ts.map +1 -0
  66. package/dist/api/zcash/helpers.d.ts +2 -0
  67. package/dist/api/zcash/helpers.d.ts.map +1 -0
  68. package/dist/core/index.d.ts +3 -2
  69. package/dist/core/index.d.ts.map +1 -1
  70. package/dist/device/Device.d.ts +3 -0
  71. package/dist/device/Device.d.ts.map +1 -1
  72. package/dist/device/DeviceCommands.d.ts +4 -4
  73. package/dist/device/DeviceCommands.d.ts.map +1 -1
  74. package/dist/device/DevicePool.d.ts.map +1 -1
  75. package/dist/events/logBlockEvent.d.ts +10 -0
  76. package/dist/events/logBlockEvent.d.ts.map +1 -1
  77. package/dist/events/ui-request.d.ts +2 -0
  78. package/dist/events/ui-request.d.ts.map +1 -1
  79. package/dist/index.d.ts +206 -7
  80. package/dist/index.d.ts.map +1 -1
  81. package/dist/index.js +4456 -2801
  82. package/dist/inject.d.ts.map +1 -1
  83. package/dist/protocols/protocol-v2/resources.d.ts +1 -0
  84. package/dist/protocols/protocol-v2/resources.d.ts.map +1 -1
  85. package/dist/protocols/protocol-v2/uiInteraction.d.ts +1 -0
  86. package/dist/protocols/protocol-v2/uiInteraction.d.ts.map +1 -1
  87. package/dist/types/api/allNetworkGetAddress.d.ts +4 -1
  88. package/dist/types/api/allNetworkGetAddress.d.ts.map +1 -1
  89. package/dist/types/api/btcDeriveContextHash.d.ts +10 -0
  90. package/dist/types/api/btcDeriveContextHash.d.ts.map +1 -0
  91. package/dist/types/api/export.d.ts +8 -0
  92. package/dist/types/api/export.d.ts.map +1 -1
  93. package/dist/types/api/index.d.ts +16 -0
  94. package/dist/types/api/index.d.ts.map +1 -1
  95. package/dist/types/api/moneroGetAddress.d.ts +15 -0
  96. package/dist/types/api/moneroGetAddress.d.ts.map +1 -0
  97. package/dist/types/api/moneroGetWatchKey.d.ts +15 -0
  98. package/dist/types/api/moneroGetWatchKey.d.ts.map +1 -0
  99. package/dist/types/api/moneroKeyImageSync.d.ts +24 -0
  100. package/dist/types/api/moneroKeyImageSync.d.ts.map +1 -0
  101. package/dist/types/api/moneroSignTransaction.d.ts +32 -0
  102. package/dist/types/api/moneroSignTransaction.d.ts.map +1 -0
  103. package/dist/types/api/zcashGetAddress.d.ts +20 -0
  104. package/dist/types/api/zcashGetAddress.d.ts.map +1 -0
  105. package/dist/types/api/zcashGetFullViewingKey.d.ts +13 -0
  106. package/dist/types/api/zcashGetFullViewingKey.d.ts.map +1 -0
  107. package/dist/types/api/zcashSignPczt.d.ts +9 -0
  108. package/dist/types/api/zcashSignPczt.d.ts.map +1 -0
  109. package/dist/types/device.d.ts +10 -0
  110. package/dist/types/device.d.ts.map +1 -1
  111. package/dist/types/params.d.ts +1 -0
  112. package/dist/types/params.d.ts.map +1 -1
  113. package/dist/utils/logger.d.ts.map +1 -1
  114. package/dist/utils/patch.d.ts +1 -1
  115. package/dist/utils/patch.d.ts.map +1 -1
  116. package/package.json +4 -4
  117. package/src/api/BaseMethod.ts +2 -0
  118. package/src/api/FileWrite.ts +7 -1
  119. package/src/api/FirmwareUpdateV4.ts +366 -99
  120. package/src/api/GetDeviceState.ts +2 -5
  121. package/src/api/SearchDevices.ts +172 -7
  122. package/src/api/allnetwork/AllNetworkGetAddressBase.ts +55 -15
  123. package/src/api/btc/BTCDeriveContextHash.ts +90 -0
  124. package/src/api/firmware/FirmwareArtifactSource.ts +12 -3
  125. package/src/api/helpers/pathUtils.ts +23 -0
  126. package/src/api/helpers/protocolV2FileWrite.ts +382 -111
  127. package/src/api/index.ts +9 -0
  128. package/src/api/monero/MoneroGetAddress.ts +71 -0
  129. package/src/api/monero/MoneroGetWatchKey.ts +75 -0
  130. package/src/api/monero/MoneroKeyImageSync.ts +170 -0
  131. package/src/api/monero/MoneroSignTransaction.ts +319 -0
  132. package/src/api/monero/helpers.ts +28 -0
  133. package/src/api/protocol-v2/DeviceReboot.ts +2 -0
  134. package/src/api/protocol-v2/DeviceUploadNft.ts +6 -1
  135. package/src/api/protocol-v2/DeviceUploadWallpaper.ts +15 -1
  136. package/src/api/zcash/ZcashGetAddress.ts +108 -0
  137. package/src/api/zcash/ZcashGetFullViewingKey.ts +78 -0
  138. package/src/api/zcash/ZcashSignPczt.ts +152 -0
  139. package/src/api/zcash/helpers.ts +16 -0
  140. package/src/core/index.ts +52 -24
  141. package/src/data/messages/messages-protocol-v2.json +152 -0
  142. package/src/data/messages/messages.json +51 -0
  143. package/src/device/Device.ts +32 -4
  144. package/src/device/DeviceCommands.ts +32 -1
  145. package/src/device/DeviceConnector.ts +4 -4
  146. package/src/device/DevicePool.ts +12 -2
  147. package/src/events/core.ts +2 -2
  148. package/src/events/logBlockEvent.ts +58 -3
  149. package/src/events/ui-request.ts +2 -0
  150. package/src/index.ts +1 -0
  151. package/src/inject.ts +17 -0
  152. package/src/protocols/protocol-v2/resources.ts +6 -1
  153. package/src/protocols/protocol-v2/uiInteraction.ts +1 -1
  154. package/src/types/api/allNetworkGetAddress.ts +8 -1
  155. package/src/types/api/btcDeriveContextHash.ts +15 -0
  156. package/src/types/api/export.ts +17 -0
  157. package/src/types/api/index.ts +24 -0
  158. package/src/types/api/moneroGetAddress.ts +27 -0
  159. package/src/types/api/moneroGetWatchKey.ts +28 -0
  160. package/src/types/api/moneroKeyImageSync.ts +45 -0
  161. package/src/types/api/moneroSignTransaction.ts +54 -0
  162. package/src/types/api/zcashGetAddress.ts +32 -0
  163. package/src/types/api/zcashGetFullViewingKey.ts +24 -0
  164. package/src/types/api/zcashSignPczt.ts +18 -0
  165. package/src/types/device.ts +18 -0
  166. package/src/types/params.ts +1 -1
  167. package/src/utils/logger.ts +4 -0
@@ -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,40 @@ 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
+ test.each([
49
+ [HardwareErrorCode.TransportProtocolProbeFailed, { expectedProtocol: 'V1' }],
50
+ [
51
+ HardwareErrorCode.TransportProtocolMismatch,
52
+ { expectedProtocol: 'V2', detectedProtocol: 'V1' },
53
+ ],
54
+ ] as const)(
55
+ 'preserves protocol error %s and structured context across the public boundary',
56
+ (code, params) => {
57
+ const error = ERRORS.TypedError(code, undefined, params);
58
+ expect(JSON.parse(JSON.stringify(createErrorMessage(error)))).toEqual({
59
+ success: false,
60
+ payload: { code, error: error.message, params },
61
+ });
62
+ }
63
+ );
64
+
31
65
  test.each([
32
66
  ['webusb', false],
33
67
  ['desktop-webusb', false],
@@ -108,14 +142,32 @@ describe('Core 错误输出边界', () => {
108
142
  HardwareErrorCode.BleDeviceBondedCanceled,
109
143
  HardwareErrorCode.BleDeviceDisconnected,
110
144
  HardwareErrorCode.PollingTimeout,
145
+ HardwareErrorCode.BlePoweredOff,
146
+ HardwareErrorCode.BleUnsupported,
147
+ HardwareErrorCode.BridgeNeedsPermission,
148
+ HardwareErrorCode.WebUsbDeviceAccessError,
149
+ HardwareErrorCode.TransportProtocolProbeFailed,
150
+ HardwareErrorCode.TransportProtocolMismatch,
111
151
  ])(
112
- 'preserves terminal BLE error %s in the public response without polling again',
152
+ 'preserves terminal transport error %s in the public response without polling again',
113
153
  async errorCode => {
114
- jest.spyOn(DataManager, 'getSettings').mockReturnValue('react-native' as never);
154
+ jest
155
+ .spyOn(DataManager, 'getSettings')
156
+ .mockReturnValue(
157
+ [
158
+ HardwareErrorCode.BridgeNeedsPermission,
159
+ HardwareErrorCode.WebUsbDeviceAccessError,
160
+ HardwareErrorCode.TransportProtocolProbeFailed,
161
+ HardwareErrorCode.TransportProtocolMismatch,
162
+ ].includes(errorCode)
163
+ ? ('desktop-webusb' as never)
164
+ : ('react-native' as never)
165
+ );
115
166
  const error = ERRORS.TypedError(errorCode);
116
167
  const acquire = jest.fn().mockRejectedValue(error);
117
168
  jest.spyOn(TransportManager, 'getTransport').mockReturnValue({
118
169
  acquire,
170
+ enumerate: jest.fn().mockResolvedValue([{ path: 'ble-pairing-test' }]),
119
171
  release: jest.fn().mockResolvedValue(true),
120
172
  stop: jest.fn().mockResolvedValue(undefined),
121
173
  } as never);
@@ -75,8 +75,11 @@ describe('DeviceConnector protocol validation', () => {
75
75
  });
76
76
  const connector = new DeviceConnector();
77
77
 
78
- await expect(connector.acquire('classic-id', undefined, undefined, 'V2')).rejects.toThrow(
79
- 'Device protocol mismatch: expected V2, detected V1'
78
+ await expect(connector.acquire('classic-id', undefined, undefined, 'V2')).rejects.toMatchObject(
79
+ {
80
+ errorCode: HardwareErrorCode.TransportProtocolMismatch,
81
+ params: { expectedProtocol: 'V2', detectedProtocol: 'V1' },
82
+ }
80
83
  );
81
84
  });
82
85
 
@@ -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';
@@ -81,6 +80,102 @@ describe('public device lifecycle events', () => {
81
80
  jest.restoreAllMocks();
82
81
  });
83
82
 
83
+ test.each([
84
+ ['react-native', 'V2', EDeviceType.Pro2, true],
85
+ ['react-native', 'V2', EDeviceType.Neo, true],
86
+ ['desktop-web-ble', 'V2', EDeviceType.Pro2, true],
87
+ ['desktop-web-ble', 'V2', EDeviceType.Neo, true],
88
+ ['react-native', 'V1', EDeviceType.Pro, false],
89
+ ['desktop-web-ble', 'V1', EDeviceType.Touch, false],
90
+ ['react-native', 'V1', EDeviceType.Pro2, false],
91
+ ['react-native', 'V2', EDeviceType.Unknown, false],
92
+ ['desktop-webusb', 'V2', EDeviceType.Pro2, false],
93
+ ['lowlevel', 'V2', EDeviceType.Neo, false],
94
+ ] as const)(
95
+ 'releases the task and keeps only the intended BLE links: %s / %s / %s',
96
+ async (env, protocol, deviceType, keepConnection) => {
97
+ jest.spyOn(DataManager, 'getSettings').mockReturnValue(env as never);
98
+ const device = createInitializedDevice(protocol);
99
+ device.features = { ...device.features, deviceType } as never;
100
+ device.originalDescriptor.session = device.mainId;
101
+ (device as unknown as { deviceAcquired: boolean }).deviceAcquired = true;
102
+ const release = jest.fn().mockResolvedValue(true);
103
+ const disconnect = jest.fn().mockResolvedValue(undefined);
104
+ device.deviceConnector = { release, disconnect } as never;
105
+ const commands = {
106
+ disposed: false,
107
+ dispose: jest.fn(() => {
108
+ commands.disposed = true;
109
+ return Promise.resolve();
110
+ }),
111
+ cancel: jest.fn().mockResolvedValue(undefined),
112
+ };
113
+ device.commands = commands as never;
114
+
115
+ await device.release();
116
+
117
+ expect(release).toHaveBeenCalledWith(device.mainId, false, keepConnection);
118
+ expect(device.hasDeviceAcquire()).toBe(false);
119
+ expect(commands.disposed).toBe(true);
120
+ expect(device.keepSession).toBe(false);
121
+
122
+ await device.interruptionFromUser();
123
+ expect(disconnect).toHaveBeenCalledTimes(keepConnection ? 0 : 1);
124
+ }
125
+ );
126
+
127
+ test('still disconnects when setup is cancelled after a kept-alive release', async () => {
128
+ jest.spyOn(DataManager, 'getSettings').mockReturnValue('react-native' as never);
129
+ const device = createInitializedDevice('V2');
130
+ const pendingAcquire = createDeferred<{ uuid: string; protocolType: 'V2' }>();
131
+ const release = jest.fn().mockResolvedValue(true);
132
+ const disconnect = jest.fn().mockResolvedValue(undefined);
133
+ device.deviceConnector = {
134
+ release,
135
+ disconnect,
136
+ acquire: jest.fn(() => pendingAcquire.promise),
137
+ } as never;
138
+ device.commands = {
139
+ disposed: true,
140
+ dispose: jest.fn().mockResolvedValue(undefined),
141
+ cancel: jest.fn().mockResolvedValue(undefined),
142
+ } as never;
143
+ await device.release();
144
+ device.beginConnectionAttempt();
145
+ const acquiring = device.acquire('V2');
146
+
147
+ await device.interruptionFromUser();
148
+ expect(disconnect).toHaveBeenCalledWith(device.mainId);
149
+ pendingAcquire.resolve({ uuid: 'late-kept-alive-session', protocolType: 'V2' });
150
+ await expect(acquiring).rejects.toMatchObject({
151
+ errorCode: HardwareErrorCode.DeviceInterruptedFromUser,
152
+ });
153
+ expect(disconnect).toHaveBeenLastCalledWith('late-kept-alive-session');
154
+ });
155
+
156
+ test.each(['release failure', 'disconnect event'])(
157
+ 'does not treat %s as a kept-alive connection',
158
+ async reason => {
159
+ jest.spyOn(DataManager, 'getSettings').mockReturnValue('react-native' as never);
160
+ const device = createInitializedDevice('V2');
161
+ const release = jest.fn().mockResolvedValue(true);
162
+ if (reason === 'release failure') release.mockRejectedValueOnce(new Error('release failed'));
163
+ const disconnect = jest.fn().mockResolvedValue(undefined);
164
+ device.deviceConnector = { release, disconnect } as never;
165
+ device.commands = {
166
+ disposed: true,
167
+ dispose: jest.fn().mockResolvedValue(undefined),
168
+ cancel: jest.fn().mockResolvedValue(undefined),
169
+ } as never;
170
+ await device.release();
171
+ if (reason === 'disconnect event') device.markTransportDisconnected();
172
+
173
+ await device.interruptionFromUser();
174
+
175
+ expect(disconnect).toHaveBeenCalledWith(device.mainId);
176
+ }
177
+ );
178
+
84
179
  test.each(['V1', 'V2'] as const)(
85
180
  'preserves the %s acknowledged result and waits for release before the next acquire',
86
181
  async protocol => {
@@ -364,11 +459,11 @@ describe('public device lifecycle events', () => {
364
459
 
365
460
  test('clears the cleanup barrier when its deadline expires', async () => {
366
461
  const realSetTimeout = setTimeout;
367
- jest
368
- .spyOn(global, 'setTimeout')
369
- .mockImplementation((callback, delay, ...args) =>
370
- realSetTimeout(callback, delay === 5_000 ? 0 : delay, ...args)
371
- );
462
+ const cleanupDeadline = createDeferred<() => void>();
463
+ jest.spyOn(global, 'setTimeout').mockImplementation((callback, delay, ...args) => {
464
+ if (delay === 5_000) cleanupDeadline.resolve(() => callback(...args));
465
+ return realSetTimeout(callback, delay, ...args);
466
+ });
372
467
  {
373
468
  jest.spyOn(DataManager, 'getSettings').mockReturnValue('react-native' as never);
374
469
  const acquire = jest.spyOn(Device.prototype, 'acquire').mockResolvedValue(undefined);
@@ -382,13 +477,15 @@ describe('public device lifecycle events', () => {
382
477
  type: IFRAME.CALL,
383
478
  payload: { method: 'getDeviceState', connectId: 'draining-device', connectProtocol: 'V2' },
384
479
  } as CoreMessage);
385
- setImmediate(() => cancel(context, 'draining-device'));
480
+ const expireCleanupDeadline = await cleanupDeadline.promise;
481
+ cancel(context, 'draining-device');
482
+ expireCleanupDeadline();
386
483
  await expect(result).resolves.toBeDefined();
387
- gate.resolve();
388
484
  await new Promise(resolve => {
389
485
  setImmediate(resolve);
390
486
  });
391
487
  expect(context.getPrePendingCallPromise('draining-device')).toBeUndefined();
488
+ gate.resolve();
392
489
  }
393
490
  });
394
491
 
@@ -609,17 +706,18 @@ describe('public device lifecycle events', () => {
609
706
  );
610
707
 
611
708
  test.each([
612
- [HardwareErrorCode.RuntimeError, true],
709
+ [HardwareErrorCode.TransportProtocolProbeFailed, true],
710
+ [HardwareErrorCode.TransportProtocolMismatch, false],
711
+ [HardwareErrorCode.RuntimeError, false],
613
712
  [HardwareErrorCode.BleDeviceBondError, false],
614
- [HardwareErrorCode.BlePeerRemovedPairingInformation, false],
615
713
  ] as const)(
616
714
  'retries a Protocol V2 probe mismatch with error code %s: %s',
617
715
  (errorCode, expected) => {
618
716
  const method = { payload: { connectProtocol: 'V2' } } as never;
619
717
  const error = {
620
718
  errorCode,
621
- message:
622
- 'Device protocol mismatch: expected V2, but device did not respond to expected protocol',
719
+ message: 'diagnostic wording is not used for classification',
720
+ params: { expectedProtocol: 'V2' },
623
721
  };
624
722
 
625
723
  expect(isRetryableBleProtocolV2ProbeError(method, error)).toBe(expected);
@@ -629,10 +727,10 @@ describe('public device lifecycle events', () => {
629
727
  test.each([
630
728
  [HardwareErrorCode.BleConnectedError, true],
631
729
  [HardwareErrorCode.BleTimeoutError, true],
730
+ [HardwareErrorCode.TransportProtocolProbeFailed, true],
731
+ [HardwareErrorCode.TransportProtocolMismatch, false],
632
732
  [HardwareErrorCode.PollingTimeout, false],
633
733
  [HardwareErrorCode.BleDeviceBondError, false],
634
- [HardwareErrorCode.BlePeerRemovedPairingInformation, false],
635
- [HardwareErrorCode.BleBondInvalid, false],
636
734
  ] as const)('retries a BLE connection error with error code %s: %s', (errorCode, expected) => {
637
735
  const method = { payload: { connectProtocol: 'V2' } } as never;
638
736
  const error = {
@@ -643,6 +741,21 @@ describe('public device lifecycle events', () => {
643
741
  expect(isRetryableBleConnectionError(method, error)).toBe(expected);
644
742
  });
645
743
 
744
+ test('does not retry a failed strict V1 probe as a V2 probe or after user cancellation', () => {
745
+ const error = ERRORS.TypedError(HardwareErrorCode.TransportProtocolProbeFailed, undefined, {
746
+ expectedProtocol: 'V1',
747
+ });
748
+ const method = { payload: { connectProtocol: 'V2' } } as never;
749
+ expect(isRetryableBleProtocolV2ProbeError(method, error)).toBe(false);
750
+ expect(isRetryableBleConnectionError(method, error)).toBe(false);
751
+ expect(
752
+ isRetryableBleConnectionError(
753
+ { payload: {}, device: { wasInterruptedByUser: () => true } } as never,
754
+ ERRORS.TypedError(HardwareErrorCode.TransportProtocolProbeFailed)
755
+ )
756
+ ).toBe(false);
757
+ });
758
+
646
759
  test('does not retry the desktop acquire deadline fallback', () => {
647
760
  const method = { payload: { connectProtocol: 'V2' } } as never;
648
761
  const error = {
@@ -685,19 +798,6 @@ describe('public device lifecycle events', () => {
685
798
  }
686
799
  );
687
800
 
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
801
  test.each([
702
802
  [HardwareErrorCode.DeviceCheckDeviceIdError, true],
703
803
  [HardwareErrorCode.DeviceNotFound, false],
@@ -729,6 +829,48 @@ describe('public device lifecycle events', () => {
729
829
  expect(() => JSON.stringify(disconnectMessage?.payload.device)).not.toThrow();
730
830
  });
731
831
 
832
+ test.each(['V1', 'V2'] as const)(
833
+ 'preserves the %s connection identity for a transport disconnect after wipe',
834
+ protocol => {
835
+ jest.spyOn(DataManager, 'getSettings').mockReturnValue('webusb' as never);
836
+ core = initCore();
837
+ initConnector();
838
+ const messages: CoreMessage[] = [];
839
+ core.on(CORE_EVENT, message => messages.push(message));
840
+ const device = createInitializedDevice(protocol);
841
+ const connectId = device.getConnectId();
842
+ DevicePool.devicesCache[connectId] = device;
843
+
844
+ device.invalidateAfterWipe();
845
+ expect(device.toMessageObject()).toBeNull();
846
+ DevicePool.emitter.emit(DEVICE.CONNECT, device);
847
+ expect(messages.some(message => message.type === DEVICE.CONNECT)).toBe(false);
848
+ DevicePool.emitter.emit(TRANSPORT_EVENT.DEVICE_DISCONNECT, { connectId });
849
+
850
+ const disconnects = messages.filter(message => message.type === DEVICE.DISCONNECT);
851
+ expect(disconnects).toHaveLength(1);
852
+ expect(disconnects[0].payload.device).toMatchObject({
853
+ connectId,
854
+ serialNo: 'SERIAL-001',
855
+ deviceId: 'wallet-device-id',
856
+ });
857
+ expect(() => JSON.stringify(disconnects[0].payload.device)).not.toThrow();
858
+
859
+ device.features = {
860
+ ...createInitializedDevice(protocol).features,
861
+ deviceId: 'new-device-id',
862
+ initialized: false,
863
+ mode: 'notInitialized',
864
+ } as never;
865
+ DevicePool.emitter.emit(TRANSPORT_EVENT.DEVICE_DISCONNECT, { connectId });
866
+ const nextDisconnects = messages.filter(message => message.type === DEVICE.DISCONNECT);
867
+ expect(nextDisconnects[1].payload.device).toMatchObject({
868
+ deviceId: 'new-device-id',
869
+ mode: 'notInitialized',
870
+ });
871
+ }
872
+ );
873
+
732
874
  test('treats an acquired BLE device as reusable until the transport disconnects', async () => {
733
875
  jest.spyOn(DataManager, 'getSettings').mockReturnValue('desktop-web-ble' as never);
734
876
  const device = createInitializedDevice('V2');
@@ -1014,6 +1156,28 @@ describe('public device lifecycle events', () => {
1014
1156
  expect(device.wasInterruptedByUser()).toBe(false);
1015
1157
  });
1016
1158
 
1159
+ test.each(['V1', 'V2'] as const)(
1160
+ 'retires a failed USB %s session before the next user attempt',
1161
+ async protocol => {
1162
+ jest.spyOn(DataManager, 'getSettings').mockReturnValue('desktop-webusb' as never);
1163
+ const device = createInitializedDevice(protocol);
1164
+ device.originalDescriptor = { ...device.originalDescriptor, session: device.mainId };
1165
+ const disconnect = jest.fn().mockResolvedValue(undefined);
1166
+ const release = jest.fn().mockResolvedValue(undefined);
1167
+ device.deviceConnector = { disconnect, release } as never;
1168
+ device.commands = { disposed: false, dispose: jest.fn() } as never;
1169
+ (device as unknown as { deviceAcquired: boolean }).deviceAcquired = true;
1170
+ const error = ERRORS.TypedError(HardwareErrorCode.WebUsbDeviceAccessError);
1171
+ await expect(device.run(() => Promise.reject(error))).rejects.toBe(error);
1172
+ await new Promise(resolve => {
1173
+ setImmediate(resolve);
1174
+ });
1175
+ expect(disconnect).toHaveBeenCalledWith(device.mainId);
1176
+ expect(release).toHaveBeenCalledTimes(1);
1177
+ expect(device.hasDeviceAcquire()).toBe(false);
1178
+ }
1179
+ );
1180
+
1017
1181
  test('waits for the canceled run to finish releasing before cancellation completes', async () => {
1018
1182
  jest.spyOn(DataManager, 'getSettings').mockReturnValue('react-native' as never);
1019
1183
  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
@@ -31,6 +31,10 @@ const createReader = (bytes = artifactBytes) => {
31
31
  };
32
32
 
33
33
  describe('FirmwareArtifactSource', () => {
34
+ afterEach(() => {
35
+ jest.restoreAllMocks();
36
+ });
37
+
34
38
  it('materializes verified bytes before exposing the source', async () => {
35
39
  const backingBytes = Uint8Array.from(artifactBytes);
36
40
  const { reader, read, close } = createReader(backingBytes);
@@ -86,6 +90,10 @@ describe('FirmwareArtifactSource', () => {
86
90
  sha256: bytesToHex(sha256(bytes)),
87
91
  };
88
92
  const { reader, read } = createReader(bytes);
93
+ const hasher = sha256.create();
94
+ const update = jest.spyOn(hasher, 'update');
95
+ const digest = jest.spyOn(hasher, 'digest');
96
+ jest.spyOn(sha256, 'create').mockReturnValue(hasher);
89
97
 
90
98
  const source = await openFirmwareByteSource({ artifact: largeArtifact, reader });
91
99
  if (!source) {
@@ -103,6 +111,9 @@ describe('FirmwareArtifactSource', () => {
103
111
  length: 3,
104
112
  },
105
113
  ]);
114
+ expect(update).toHaveBeenCalledTimes(2);
115
+ expect(update.mock.invocationCallOrder[0]).toBeLessThan(read.mock.invocationCallOrder[1]);
116
+ expect(digest.mock.invocationCallOrder[0]).toBeGreaterThan(update.mock.invocationCallOrder[1]);
106
117
  await source.close();
107
118
  });
108
119