@onekeyfe/hd-core 1.2.0-alpha.99 → 1.2.0

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 (200) hide show
  1. package/__tests__/AllNetworkGetAddressBase.tracing.test.ts +20 -4
  2. package/__tests__/DeviceCommands.test.ts +203 -27
  3. package/__tests__/base64Data.test.ts +69 -0
  4. package/__tests__/check-all-firmware-release-protocol-v2.test.ts +48 -2
  5. package/__tests__/check-firmware-release-protocol-v2.test.ts +82 -13
  6. package/__tests__/device-connector-protocol.test.ts +21 -0
  7. package/__tests__/device-lifecycle-events.test.ts +427 -7
  8. package/__tests__/device-pool-state.test.ts +25 -0
  9. package/__tests__/device-settings.test.ts +237 -9
  10. package/__tests__/device-state-events.test.ts +2 -2
  11. package/__tests__/device-state-mapper.test.ts +61 -5
  12. package/__tests__/device-utils.test.ts +15 -1
  13. package/__tests__/device-wallet-session-store.test.ts +147 -21
  14. package/__tests__/deviceSettings.test.ts +7 -3
  15. package/__tests__/deviceUploadNft.test.ts +36 -4
  16. package/__tests__/firmware-update/device-update-bootloader.test.ts +13 -3
  17. package/__tests__/firmware-update/firmware-artifact-source.test.ts +61 -9
  18. package/__tests__/firmware-update/firmware-prepared-host-digest.test.ts +19 -9
  19. package/__tests__/firmware-update/firmware-prepared-resource-archive.test.ts +21 -0
  20. package/__tests__/firmware-update/firmware-update-bootloader-poll.test.ts +63 -0
  21. package/__tests__/firmware-update/firmware-update-plan.test.ts +59 -55
  22. package/__tests__/firmware-update/firmware-update-prepared-plan.test.ts +19 -5
  23. package/__tests__/firmware-update/firmware-update-v3-reconnect.test.ts +116 -0
  24. package/__tests__/firmware-update/firmware-update-v4-install-poll.test.ts +678 -0
  25. package/__tests__/firmware-update/firmware-update-v4-parameters.test.ts +85 -0
  26. package/__tests__/get-device-state.test.ts +60 -21
  27. package/__tests__/logBlockEvent.test.ts +13 -1
  28. package/__tests__/open-wallet-session.test.ts +127 -1
  29. package/__tests__/protocol-v2-firmware-targets.test.ts +23 -0
  30. package/__tests__/protocol-v2-resources.test.ts +185 -88
  31. package/__tests__/protocol-v2-unlock-policy.test.ts +235 -2
  32. package/__tests__/protocol-v2.test.ts +2198 -486
  33. package/__tests__/refresh-device-state.test.ts +3 -1
  34. package/__tests__/resourceBase64Boundary.test.ts +48 -0
  35. package/__tests__/search-devices.test.ts +79 -32
  36. package/__tests__/ton-sign-message.test.ts +84 -0
  37. package/dist/api/BaseMethod.d.ts +2 -1
  38. package/dist/api/BaseMethod.d.ts.map +1 -1
  39. package/dist/api/CheckAllFirmwareRelease.d.ts.map +1 -1
  40. package/dist/api/CheckBootloaderRelease.d.ts.map +1 -1
  41. package/dist/api/FirmwareUpdateV2.d.ts.map +1 -1
  42. package/dist/api/FirmwareUpdateV3.d.ts.map +1 -1
  43. package/dist/api/FirmwareUpdateV4.d.ts +19 -6
  44. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  45. package/dist/api/OpenWalletSession.d.ts.map +1 -1
  46. package/dist/api/SearchDevices.d.ts.map +1 -1
  47. package/dist/api/UploadPortfolio.d.ts +1 -1
  48. package/dist/api/UploadPortfolio.d.ts.map +1 -1
  49. package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts.map +1 -1
  50. package/dist/api/device/DeviceChangePin.d.ts.map +1 -1
  51. package/dist/api/device/DeviceRebootToBoardloader.d.ts.map +1 -1
  52. package/dist/api/device/DeviceRebootToBootloader.d.ts.map +1 -1
  53. package/dist/api/device/DeviceSettings.d.ts.map +1 -1
  54. package/dist/api/device/DeviceVerify.d.ts +1 -0
  55. package/dist/api/device/DeviceVerify.d.ts.map +1 -1
  56. package/dist/api/device/DeviceWipe.d.ts.map +1 -1
  57. package/dist/api/firmware/FirmwareArtifactSource.d.ts +5 -1
  58. package/dist/api/firmware/FirmwareArtifactSource.d.ts.map +1 -1
  59. package/dist/api/firmware/FirmwarePreparedResourceArchive.d.ts.map +1 -1
  60. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts +2 -2
  61. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts.map +1 -1
  62. package/dist/api/firmware/FirmwareUpdatePlan.d.ts +0 -15
  63. package/dist/api/firmware/FirmwareUpdatePlan.d.ts.map +1 -1
  64. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts.map +1 -1
  65. package/dist/api/firmware/getBinary.d.ts +3 -2
  66. package/dist/api/firmware/getBinary.d.ts.map +1 -1
  67. package/dist/api/firmware/protocolV2Release.d.ts.map +1 -1
  68. package/dist/api/helpers/base64Data.d.ts +16 -0
  69. package/dist/api/helpers/base64Data.d.ts.map +1 -0
  70. package/dist/api/protocol-v2/DeviceFirmwareUpdate.d.ts +3 -1
  71. package/dist/api/protocol-v2/DeviceFirmwareUpdate.d.ts.map +1 -1
  72. package/dist/api/protocol-v2/DeviceGetFirmwareUpdateStatus.d.ts.map +1 -1
  73. package/dist/api/protocol-v2/DeviceInfoGet.d.ts +1 -1
  74. package/dist/api/protocol-v2/DeviceInfoGet.d.ts.map +1 -1
  75. package/dist/api/protocol-v2/DeviceReboot.d.ts +1 -1
  76. package/dist/api/protocol-v2/DeviceReboot.d.ts.map +1 -1
  77. package/dist/api/protocol-v2/DeviceUploadNft.d.ts +2 -3
  78. package/dist/api/protocol-v2/DeviceUploadNft.d.ts.map +1 -1
  79. package/dist/api/protocol-v2/DeviceUploadWallpaper.d.ts +2 -3
  80. package/dist/api/protocol-v2/DeviceUploadWallpaper.d.ts.map +1 -1
  81. package/dist/api/protocol-v2/helpers.d.ts.map +1 -1
  82. package/dist/api/ton/TonSignMessage.d.ts.map +1 -1
  83. package/dist/core/index.d.ts +6 -1
  84. package/dist/core/index.d.ts.map +1 -1
  85. package/dist/data-manager/DataManager.d.ts +4 -3
  86. package/dist/data-manager/DataManager.d.ts.map +1 -1
  87. package/dist/data-manager/TransportManager.d.ts.map +1 -1
  88. package/dist/device/Device.d.ts +16 -3
  89. package/dist/device/Device.d.ts.map +1 -1
  90. package/dist/device/DeviceCommands.d.ts +4 -4
  91. package/dist/device/DeviceCommands.d.ts.map +1 -1
  92. package/dist/device/DeviceConnector.d.ts +1 -1
  93. package/dist/device/DeviceConnector.d.ts.map +1 -1
  94. package/dist/device/DevicePool.d.ts.map +1 -1
  95. package/dist/device/DeviceStateMapper.d.ts.map +1 -1
  96. package/dist/deviceProfile/buildDeviceFeatures.d.ts.map +1 -1
  97. package/dist/events/logBlockEvent.d.ts.map +1 -1
  98. package/dist/events/ui-request.d.ts +3 -0
  99. package/dist/events/ui-request.d.ts.map +1 -1
  100. package/dist/index.d.ts +54 -84
  101. package/dist/index.d.ts.map +1 -1
  102. package/dist/index.js +2135 -1470
  103. package/dist/protocols/protocol-v2/features.d.ts +13 -5
  104. package/dist/protocols/protocol-v2/features.d.ts.map +1 -1
  105. package/dist/protocols/protocol-v2/index.d.ts +2 -2
  106. package/dist/protocols/protocol-v2/index.d.ts.map +1 -1
  107. package/dist/protocols/protocol-v2/resources.d.ts +12 -7
  108. package/dist/protocols/protocol-v2/resources.d.ts.map +1 -1
  109. package/dist/protocols/protocol-v2/unlockPolicyRunner.d.ts +3 -1
  110. package/dist/protocols/protocol-v2/unlockPolicyRunner.d.ts.map +1 -1
  111. package/dist/protocols/protocol-v2/walletSession.d.ts +3 -0
  112. package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
  113. package/dist/types/api/firmwareUpdatePlan.d.ts +2 -2
  114. package/dist/types/api/firmwareUpdatePlan.d.ts.map +1 -1
  115. package/dist/types/api/protocolV2.d.ts +1 -1
  116. package/dist/types/api/protocolV2.d.ts.map +1 -1
  117. package/dist/types/device.d.ts.map +1 -1
  118. package/dist/types/settings.d.ts +3 -31
  119. package/dist/types/settings.d.ts.map +1 -1
  120. package/dist/utils/deviceFeaturesCompat.d.ts.map +1 -1
  121. package/dist/utils/deviceFeaturesUtils.d.ts +3 -0
  122. package/dist/utils/deviceFeaturesUtils.d.ts.map +1 -1
  123. package/dist/utils/deviceInfoUtils.d.ts.map +1 -1
  124. package/dist/utils/deviceSettings.d.ts +1 -0
  125. package/dist/utils/deviceSettings.d.ts.map +1 -1
  126. package/dist/utils/index.d.ts +1 -1
  127. package/dist/utils/index.d.ts.map +1 -1
  128. package/dist/utils/patch.d.ts +1 -1
  129. package/dist/utils/patch.d.ts.map +1 -1
  130. package/dist/utils/pro2Nft.d.ts +7 -0
  131. package/dist/utils/pro2Nft.d.ts.map +1 -1
  132. package/package.json +6 -4
  133. package/src/api/BaseMethod.ts +8 -1
  134. package/src/api/CheckAllFirmwareRelease.ts +2 -4
  135. package/src/api/CheckBLEFirmwareRelease.ts +2 -2
  136. package/src/api/CheckBootloaderRelease.ts +3 -2
  137. package/src/api/CheckFirmwareRelease.ts +2 -2
  138. package/src/api/FirmwareUpdateV2.ts +12 -9
  139. package/src/api/FirmwareUpdateV3.ts +29 -19
  140. package/src/api/FirmwareUpdateV4.ts +918 -741
  141. package/src/api/OpenWalletSession.ts +39 -9
  142. package/src/api/PromptWebDeviceAccess.ts +2 -2
  143. package/src/api/SearchDevices.ts +6 -3
  144. package/src/api/UploadPortfolio.ts +9 -2
  145. package/src/api/allnetwork/AllNetworkGetAddressBase.ts +2 -1
  146. package/src/api/device/DeviceChangePin.ts +4 -1
  147. package/src/api/device/DeviceRebootToBoardloader.ts +3 -1
  148. package/src/api/device/DeviceRebootToBootloader.ts +3 -1
  149. package/src/api/device/DeviceSettings.ts +34 -20
  150. package/src/api/device/DeviceVerify.ts +25 -0
  151. package/src/api/device/DeviceWipe.ts +4 -1
  152. package/src/api/firmware/FirmwareArtifactSource.ts +41 -14
  153. package/src/api/firmware/FirmwarePreparedResourceArchive.ts +16 -27
  154. package/src/api/firmware/FirmwareUpdateBaseMethod.ts +9 -3
  155. package/src/api/firmware/FirmwareUpdatePlan.ts +37 -77
  156. package/src/api/firmware/FirmwareUpdatePreparedPlan.ts +10 -6
  157. package/src/api/firmware/protocolV2Release.ts +7 -2
  158. package/src/api/helpers/base64Data.ts +85 -0
  159. package/src/api/protocol-v2/DeviceFactoryInfoSet.ts +1 -1
  160. package/src/api/protocol-v2/DeviceFirmwareUpdate.ts +6 -28
  161. package/src/api/protocol-v2/DeviceGetFirmwareUpdateStatus.ts +7 -1
  162. package/src/api/protocol-v2/DeviceInfoGet.ts +3 -3
  163. package/src/api/protocol-v2/DeviceReboot.ts +5 -0
  164. package/src/api/protocol-v2/DeviceUploadNft.ts +61 -9
  165. package/src/api/protocol-v2/DeviceUploadWallpaper.ts +53 -19
  166. package/src/api/protocol-v2/helpers.ts +8 -0
  167. package/src/api/ton/TonSignMessage.ts +5 -3
  168. package/src/core/index.ts +264 -55
  169. package/src/data/messages/messages-protocol-v2.json +140 -13
  170. package/src/data-manager/DataManager.ts +46 -39
  171. package/src/data-manager/TransportManager.ts +3 -1
  172. package/src/device/Device.ts +210 -44
  173. package/src/device/DeviceCommands.ts +27 -18
  174. package/src/device/DeviceConnector.ts +17 -3
  175. package/src/device/DevicePool.ts +9 -2
  176. package/src/device/DeviceStateMapper.ts +30 -24
  177. package/src/deviceProfile/buildDeviceFeatures.ts +10 -5
  178. package/src/events/logBlockEvent.ts +14 -1
  179. package/src/events/ui-request.ts +3 -0
  180. package/src/index.ts +0 -10
  181. package/src/protocols/protocol-v2/features.ts +22 -5
  182. package/src/protocols/protocol-v2/index.ts +2 -0
  183. package/src/protocols/protocol-v2/resources.ts +110 -147
  184. package/src/protocols/protocol-v2/unlockPolicyRunner.ts +13 -1
  185. package/src/protocols/protocol-v2/walletSession.ts +65 -22
  186. package/src/types/api/firmwareUpdate.ts +1 -1
  187. package/src/types/api/firmwareUpdatePlan.ts +2 -2
  188. package/src/types/api/protocolV2.ts +1 -1
  189. package/src/types/device.ts +2 -1
  190. package/src/types/settings.ts +6 -34
  191. package/src/utils/deviceFeaturesCompat.ts +9 -4
  192. package/src/utils/deviceFeaturesUtils.ts +4 -0
  193. package/src/utils/deviceInfoUtils.ts +9 -2
  194. package/src/utils/deviceSettings.ts +11 -4
  195. package/src/utils/index.ts +1 -0
  196. package/src/utils/pro2Nft.ts +31 -8
  197. package/__tests__/firmware-memory-host.test.ts +0 -126
  198. package/dist/api/firmware/FirmwareMemoryHost.d.ts +0 -22
  199. package/dist/api/firmware/FirmwareMemoryHost.d.ts.map +0 -1
  200. package/src/api/firmware/FirmwareMemoryHost.ts +0 -143
@@ -1,12 +1,21 @@
1
- import { HardwareErrorCode } from '@onekeyfe/hd-shared';
2
- import { TRANSPORT_EVENT } from '@onekeyfe/hd-transport';
3
-
4
- import { initConnector, initCore } from '../src/core';
1
+ import { EDeviceType, ERRORS, HardwareErrorCode, createDeferred } from '@onekeyfe/hd-shared';
2
+ import { DeviceType, TRANSPORT_EVENT } from '@onekeyfe/hd-transport';
3
+
4
+ import {
5
+ initConnector,
6
+ initCore,
7
+ isMissingDetectedProtocolV2Error,
8
+ isProtocolV2PeerRemovedPairingError,
9
+ isRetryableBleConnectionError,
10
+ isRetryableBleProtocolV2ProbeError,
11
+ } from '../src/core';
5
12
  import { DataManager } from '../src/data-manager';
6
13
  import TransportManager from '../src/data-manager/TransportManager';
7
14
  import { Device } from '../src/device/Device';
15
+ import { cancelDeviceInPrompt, cancelDeviceWithInitialize } from '../src/device/DeviceCommands';
8
16
  import { DevicePool } from '../src/device/DevicePool';
9
17
  import { CORE_EVENT, DEVICE, IFRAME } from '../src/events';
18
+ import { PROTOCOL_V2_DEVICE_STATUS_GET_MESSAGE_TYPE } from '../src/protocols/protocol-v2';
10
19
 
11
20
  import type Core from '../src/core';
12
21
  import type { CoreMessage } from '../src/events';
@@ -80,6 +89,23 @@ describe('public device lifecycle events', () => {
80
89
  expect(DevicePool.emitter.listenerCount(DEVICE.DISCONNECT)).toBe(1);
81
90
  });
82
91
 
92
+ test('isolates pending cancellation cleanup by connect id', () => {
93
+ core = initCore();
94
+ const context = (core as any).getCoreContext();
95
+ const firstCleanup = createDeferred<void>();
96
+ const replacementCleanup = createDeferred<void>();
97
+
98
+ context.setPrePendingCallPromise('device-a', firstCleanup.promise);
99
+
100
+ expect(context.getPrePendingCallPromise('device-a')).toBe(firstCleanup.promise);
101
+ expect(context.getPrePendingCallPromise('device-b')).toBeUndefined();
102
+
103
+ context.setPrePendingCallPromise('device-a', replacementCleanup.promise);
104
+ context.removePrePendingCallPromise('device-a', firstCleanup.promise);
105
+
106
+ expect(context.getPrePendingCallPromise('device-a')).toBe(replacementCleanup.promise);
107
+ });
108
+
83
109
  test('keeps shared device lifecycle listeners across a device cache reset', () => {
84
110
  jest.spyOn(DataManager, 'getSettings').mockReturnValue('react-native' as never);
85
111
  core = initCore();
@@ -109,7 +135,7 @@ describe('public device lifecycle events', () => {
109
135
 
110
136
  await device.acquire();
111
137
 
112
- expect(acquire).toHaveBeenCalledWith('ble-id', undefined, true, 'V1', undefined);
138
+ expect(acquire).toHaveBeenCalledWith('ble-id', undefined, true, 'V1', undefined, undefined);
113
139
  expect(device.getProtocol()).toBe('V1');
114
140
  expect(device.originalDescriptor.protocolType).toBe('V1');
115
141
  });
@@ -127,7 +153,9 @@ describe('public device lifecycle events', () => {
127
153
 
128
154
  await device.acquire(undefined, { forceProtocolDetection: true });
129
155
 
130
- expect(acquire).toHaveBeenCalledWith('ble-id', undefined, true, undefined, undefined);
156
+ // Explicit active detection forwards forceProtocolDetection so the
157
+ // transport bypasses its protocol cache.
158
+ expect(acquire).toHaveBeenCalledWith('ble-id', undefined, true, undefined, undefined, true);
131
159
  expect(device.getProtocol()).toBe('V2');
132
160
  expect(device.originalDescriptor.protocolType).toBe('V2');
133
161
  });
@@ -228,10 +256,76 @@ describe('public device lifecycle events', () => {
228
256
 
229
257
  await device.acquire('V2');
230
258
 
231
- expect(acquire).toHaveBeenCalledWith('ble-id', undefined, true, 'V2', undefined);
259
+ expect(acquire).toHaveBeenCalledWith('ble-id', undefined, true, 'V2', undefined, undefined);
232
260
  expect(device.getProtocol()).toBe('V2');
233
261
  });
234
262
 
263
+ test.each([
264
+ ['V2', true],
265
+ ['V1', false],
266
+ ] as const)(
267
+ 'retries a missing detected protocol only for an explicit Protocol %s connection',
268
+ (connectProtocol, expected) => {
269
+ const method = { payload: { connectProtocol } } as never;
270
+ const error = {
271
+ errorCode: HardwareErrorCode.RuntimeError,
272
+ message: 'Device protocol has not been detected for ble-id',
273
+ };
274
+
275
+ expect(isMissingDetectedProtocolV2Error(method, error)).toBe(expected);
276
+ }
277
+ );
278
+
279
+ test.each([
280
+ [HardwareErrorCode.RuntimeError, true],
281
+ [HardwareErrorCode.BleDeviceBondError, false],
282
+ [HardwareErrorCode.BlePeerRemovedPairingInformation, false],
283
+ ] as const)(
284
+ 'retries a Protocol V2 probe mismatch with error code %s: %s',
285
+ (errorCode, expected) => {
286
+ const method = { payload: { connectProtocol: 'V2' } } as never;
287
+ const error = {
288
+ errorCode,
289
+ message:
290
+ 'Device protocol mismatch: expected V2, but device did not respond to expected protocol',
291
+ };
292
+
293
+ expect(isRetryableBleProtocolV2ProbeError(method, error)).toBe(expected);
294
+ }
295
+ );
296
+
297
+ test.each([
298
+ [HardwareErrorCode.BleConnectedError, true],
299
+ [HardwareErrorCode.BleTimeoutError, true],
300
+ [HardwareErrorCode.PollingTimeout, false],
301
+ [HardwareErrorCode.BleDeviceBondError, false],
302
+ [HardwareErrorCode.BlePeerRemovedPairingInformation, false],
303
+ ] as const)('retries a BLE connection error with error code %s: %s', (errorCode, expected) => {
304
+ const method = { payload: { connectProtocol: 'V2' } } as never;
305
+ const error = {
306
+ errorCode,
307
+ message: 'BLE setup wedged repeatedly',
308
+ };
309
+
310
+ expect(isRetryableBleConnectionError(method, error)).toBe(expected);
311
+ });
312
+
313
+ test.each([
314
+ ['V2', true],
315
+ ['V1', false],
316
+ [undefined, false],
317
+ ] as const)(
318
+ 'treats peer-removed pairing as terminal only for Protocol %s: %s',
319
+ (connectProtocol, expected) => {
320
+ const method = { payload: { connectProtocol } } as never;
321
+ const error = {
322
+ errorCode: HardwareErrorCode.BlePeerRemovedPairingInformation,
323
+ };
324
+
325
+ expect(isProtocolV2PeerRemovedPairingError(method, error)).toBe(expected);
326
+ }
327
+ );
328
+
235
329
  test('converts an internal transport disconnect into a public KnownDevice snapshot', () => {
236
330
  jest.spyOn(DataManager, 'getSettings').mockReturnValue('react-native' as never);
237
331
  core = initCore();
@@ -271,6 +365,332 @@ describe('public device lifecycle events', () => {
271
365
  expect(device.isUsedHere()).toBe(false);
272
366
  });
273
367
 
368
+ test.each([
369
+ ['V2', 'Cancel', 'webusb'],
370
+ ['V2', 'Cancel', 'desktop-web-ble'],
371
+ ['V2', 'Cancel', 'react-native'],
372
+ ['V1', 'Initialize', 'webusb'],
373
+ ['V1', 'Cancel', 'desktop-web-ble'],
374
+ ] as const)(
375
+ 'sends Protocol %s %s without treating the acquired %s session as disconnected',
376
+ async (protocol, messageType, env) => {
377
+ jest.spyOn(DataManager, 'getSettings').mockReturnValue(env as never);
378
+ const device = createInitializedDevice(protocol);
379
+ const post = jest.fn().mockResolvedValue(undefined);
380
+ const call = jest.fn().mockResolvedValue(undefined);
381
+ const cancel = jest.fn().mockResolvedValue(undefined);
382
+ device.originalDescriptor.session = device.mainId;
383
+ (device as unknown as { deviceAcquired: boolean }).deviceAcquired = true;
384
+ (device as any).commands = { transport: { post, call }, cancel };
385
+ device.setCancelableAction(() =>
386
+ messageType === 'Cancel'
387
+ ? cancelDeviceInPrompt(device, false)
388
+ : cancelDeviceWithInitialize(device)
389
+ );
390
+
391
+ await device.interruptionFromUser();
392
+
393
+ if (messageType === 'Cancel') {
394
+ expect(post).toHaveBeenCalledWith(device.mainId, 'Cancel', {});
395
+ expect(call).not.toHaveBeenCalled();
396
+ } else {
397
+ expect(call).toHaveBeenCalledWith(device.mainId, 'Initialize', {});
398
+ expect(post).not.toHaveBeenCalled();
399
+ }
400
+ expect(cancel).toHaveBeenCalledTimes(1);
401
+ expect(device.hasDeviceAcquire()).toBe(true);
402
+ }
403
+ );
404
+
405
+ test.each(['react-native', 'webusb', 'desktop-webusb'] as const)(
406
+ 'sends a fallback Cancel for an acquired Protocol V2 %s call with an open UI',
407
+ async env => {
408
+ jest.spyOn(DataManager, 'getSettings').mockReturnValue(env as never);
409
+ const device = createInitializedDevice('V2');
410
+ const post = jest.fn().mockResolvedValue(undefined);
411
+ const cancelDevice = jest.fn(() => cancelDeviceInPrompt(device, false));
412
+ const cancel = jest.fn().mockResolvedValue(undefined);
413
+ device.originalDescriptor.session = device.mainId;
414
+ (device as unknown as { deviceAcquired: boolean }).deviceAcquired = true;
415
+ device.commands = {
416
+ transport: { post },
417
+ cancelDevice,
418
+ cancel,
419
+ } as never;
420
+ device.createProtocolV2UiPhaseMetadata('button', 'start');
421
+
422
+ await device.interruptionFromUser();
423
+
424
+ expect(cancelDevice).toHaveBeenCalledTimes(1);
425
+ expect(post).toHaveBeenCalledWith(device.mainId, 'Cancel', {});
426
+ expect(cancel).toHaveBeenCalledTimes(1);
427
+ }
428
+ );
429
+
430
+ test.each(['react-native', 'webusb', 'desktop-webusb'] as const)(
431
+ 'does not send Cancel for an acquired Protocol V2 %s connect without an open UI',
432
+ async env => {
433
+ jest.spyOn(DataManager, 'getSettings').mockReturnValue(env as never);
434
+ const device = createInitializedDevice('V2');
435
+ const post = jest.fn().mockResolvedValue(undefined);
436
+ const cancelDevice = jest.fn(() => cancelDeviceInPrompt(device, false));
437
+ const cancel = jest.fn().mockResolvedValue(undefined);
438
+ const disconnect = jest.fn().mockResolvedValue(undefined);
439
+ device.originalDescriptor.session = device.mainId;
440
+ (device as unknown as { deviceAcquired: boolean }).deviceAcquired = true;
441
+ device.deviceConnector = { disconnect } as never;
442
+ device.commands = {
443
+ transport: { post },
444
+ cancelDevice,
445
+ cancel,
446
+ } as never;
447
+
448
+ await device.interruptionFromUser();
449
+
450
+ expect(cancelDevice).not.toHaveBeenCalled();
451
+ expect(post).not.toHaveBeenCalled();
452
+ expect(disconnect).not.toHaveBeenCalled();
453
+ expect(cancel).toHaveBeenCalledTimes(1);
454
+ }
455
+ );
456
+
457
+ test('disconnects a BLE link without sending Cancel when the session is not acquired', async () => {
458
+ jest.spyOn(DataManager, 'getSettings').mockReturnValue('react-native' as never);
459
+ const device = createInitializedDevice('V2');
460
+ const post = jest.fn().mockResolvedValue(undefined);
461
+ const cancelDevice = jest.fn(() => cancelDeviceInPrompt(device, false));
462
+ const cancel = jest.fn().mockResolvedValue(undefined);
463
+ const disconnect = jest.fn().mockResolvedValue(undefined);
464
+ device.deviceConnector = { disconnect } as never;
465
+ device.commands = {
466
+ transport: { post },
467
+ cancelDevice,
468
+ cancel,
469
+ } as never;
470
+
471
+ await device.interruptionFromUser();
472
+
473
+ expect(cancelDevice).not.toHaveBeenCalled();
474
+ expect(post).not.toHaveBeenCalled();
475
+ expect(disconnect).toHaveBeenCalledWith(device.mainId);
476
+ expect(device.hasDeviceAcquire()).toBe(false);
477
+ expect(device.wasInterruptedByUser()).toBe(true);
478
+ expect(cancel).toHaveBeenCalledTimes(1);
479
+ });
480
+
481
+ test('does not finish acquire after the user already cancelled', async () => {
482
+ jest.spyOn(DataManager, 'getSettings').mockReturnValue('react-native' as never);
483
+ const device = createInitializedDevice('V2');
484
+ const disconnect = jest.fn().mockResolvedValue(undefined);
485
+ let resolveAcquire: ((value: { uuid: string; protocolType: 'V2' }) => void) | undefined;
486
+ const acquire = jest.fn(
487
+ () =>
488
+ new Promise<{ uuid: string; protocolType: 'V2' }>(resolve => {
489
+ resolveAcquire = resolve;
490
+ })
491
+ );
492
+ device.deviceConnector = { acquire, disconnect } as never;
493
+
494
+ const acquirePromise = device.acquire('V2');
495
+ await device.interruptionFromUser();
496
+ resolveAcquire?.({ uuid: 'late-session', protocolType: 'V2' });
497
+
498
+ await expect(acquirePromise).rejects.toMatchObject({
499
+ errorCode: HardwareErrorCode.DeviceInterruptedFromUser,
500
+ });
501
+ expect(disconnect).toHaveBeenCalledWith('late-session');
502
+ expect(device.hasDeviceAcquire()).toBe(false);
503
+ });
504
+
505
+ test('does not retry a BLE connection error after the user cancelled', () => {
506
+ const device = createInitializedDevice('V2');
507
+ device.beginConnectionAttempt();
508
+ (device as unknown as { interruptedAttempt: number }).interruptedAttempt = (
509
+ device as unknown as { connectionAttempt: number }
510
+ ).connectionAttempt;
511
+ const method = { payload: { connectProtocol: 'V2' }, device } as never;
512
+ const error = {
513
+ errorCode: HardwareErrorCode.BleConnectedError,
514
+ message: 'BLE setup wedged repeatedly',
515
+ };
516
+
517
+ expect(isRetryableBleConnectionError(method, error)).toBe(false);
518
+ });
519
+
520
+ test('keeps a user cancel across a later BLE poll iteration', async () => {
521
+ jest.spyOn(DataManager, 'getSettings').mockReturnValue('react-native' as never);
522
+ const device = createInitializedDevice('V2');
523
+ const disconnect = jest.fn().mockResolvedValue(undefined);
524
+ const acquire = jest.fn().mockResolvedValue({ uuid: 'poll-session', protocolType: 'V2' });
525
+ device.deviceConnector = { acquire, disconnect } as never;
526
+
527
+ // First ensureConnected poll iteration.
528
+ device.beginConnectionAttempt();
529
+ await device.interruptionFromUser();
530
+ expect(device.wasInterruptedByUser()).toBe(true);
531
+
532
+ // A later poll must not call beginConnectionAttempt() again.
533
+ await expect(device.acquire('V2')).rejects.toMatchObject({
534
+ errorCode: HardwareErrorCode.DeviceInterruptedFromUser,
535
+ });
536
+ expect(device.wasInterruptedByUser()).toBe(true);
537
+ expect(acquire).not.toHaveBeenCalled();
538
+ });
539
+
540
+ test('allows a new BLE connect after the previous attempt was cancelled', async () => {
541
+ jest.spyOn(DataManager, 'getSettings').mockReturnValue('react-native' as never);
542
+ const device = createInitializedDevice('V2');
543
+ const disconnect = jest.fn().mockResolvedValue(undefined);
544
+ const acquire = jest.fn().mockResolvedValue({ uuid: 'next-session', protocolType: 'V2' });
545
+ device.deviceConnector = { acquire, disconnect } as never;
546
+
547
+ device.beginConnectionAttempt();
548
+ await device.interruptionFromUser();
549
+ expect(device.wasInterruptedByUser()).toBe(true);
550
+
551
+ device.beginConnectionAttempt();
552
+ await expect(device.acquire('V2')).resolves.toBeUndefined();
553
+
554
+ expect(device.wasInterruptedByUser()).toBe(false);
555
+ expect(device.hasDeviceAcquire()).toBe(true);
556
+ expect(acquire).toHaveBeenCalledTimes(1);
557
+ });
558
+
559
+ test('discards a late cancelled acquire after a newer connect attempt starts', async () => {
560
+ jest.spyOn(DataManager, 'getSettings').mockReturnValue('react-native' as never);
561
+ const device = createInitializedDevice('V2');
562
+ const disconnect = jest.fn().mockResolvedValue(undefined);
563
+ let resolveAcquire: ((value: { uuid: string; protocolType: 'V2' }) => void) | undefined;
564
+ const acquire = jest.fn(
565
+ () =>
566
+ new Promise<{ uuid: string; protocolType: 'V2' }>(resolve => {
567
+ resolveAcquire = resolve;
568
+ })
569
+ );
570
+ device.deviceConnector = { acquire, disconnect } as never;
571
+
572
+ device.beginConnectionAttempt();
573
+ const cancelledAcquire = device.acquire('V2');
574
+ await device.interruptionFromUser();
575
+ device.beginConnectionAttempt();
576
+ resolveAcquire?.({ uuid: 'stale-session', protocolType: 'V2' });
577
+
578
+ await expect(cancelledAcquire).rejects.toMatchObject({
579
+ errorCode: HardwareErrorCode.DeviceInterruptedFromUser,
580
+ });
581
+ expect(disconnect).toHaveBeenCalledWith('stale-session');
582
+ expect(device.hasDeviceAcquire()).toBe(false);
583
+ expect(device.wasInterruptedByUser()).toBe(false);
584
+ });
585
+
586
+ test('waits for the canceled run to finish releasing before cancellation completes', async () => {
587
+ jest.spyOn(DataManager, 'getSettings').mockReturnValue('react-native' as never);
588
+ const device = createInitializedDevice('V2');
589
+ const operation = createDeferred<void>();
590
+ const releaseGate = createDeferred<void>();
591
+ const cancelError = ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromUser);
592
+ const release = jest.spyOn(device, 'release').mockImplementation(() => releaseGate.promise);
593
+ device.commands = {
594
+ disposed: false,
595
+ cancel: jest.fn(() => {
596
+ operation.reject(cancelError);
597
+ return Promise.resolve();
598
+ }),
599
+ } as never;
600
+ (device as unknown as { deviceAcquired: boolean }).deviceAcquired = true;
601
+
602
+ const runResult = device.run(() => operation.promise).catch(error => error);
603
+ const cancellation = device.interruptionFromUser();
604
+ let cancellationCompleted = false;
605
+ cancellation.then(() => {
606
+ cancellationCompleted = true;
607
+ });
608
+
609
+ await new Promise(resolve => {
610
+ setImmediate(resolve);
611
+ });
612
+ expect(release).toHaveBeenCalledTimes(1);
613
+ expect(cancellationCompleted).toBe(false);
614
+
615
+ releaseGate.resolve();
616
+ await cancellation;
617
+ await expect(runResult).resolves.toMatchObject({
618
+ errorCode: HardwareErrorCode.DeviceInterruptedFromUser,
619
+ });
620
+ });
621
+
622
+ test.each([
623
+ [EDeviceType.Pro2, 'webusb', false, DeviceType.PRO2],
624
+ [EDeviceType.Neo, 'webusb', false, DeviceType.NEO],
625
+ [EDeviceType.Pro2, 'desktop-web-ble', true, DeviceType.PRO2],
626
+ [EDeviceType.Neo, 'desktop-web-ble', true, DeviceType.NEO],
627
+ [EDeviceType.Pro2, 'react-native', true, DeviceType.PRO2],
628
+ [EDeviceType.Neo, 'react-native', true, DeviceType.NEO],
629
+ [EDeviceType.Pro2, 'lowlevel', false, DeviceType.PRO2],
630
+ ] as const)(
631
+ 'refreshes %s runtime state after acquiring a fresh %s session',
632
+ async (deviceType, env, isBle, protocolV2DeviceType) => {
633
+ jest.spyOn(DataManager, 'getSettings').mockReturnValue(env as never);
634
+ const device = createInitializedDevice('V2');
635
+ const staleProtocolInfo = {
636
+ version: 1,
637
+ build_fingerprint: 'bootloader__1.0.0__abcdef0__PROD__RELEASE',
638
+ supported_messages: [],
639
+ };
640
+ const freshProtocolInfo = {
641
+ version: 1,
642
+ build_fingerprint: 'application__5.0.0__abcdef0__PROD__RELEASE',
643
+ supported_messages: [PROTOCOL_V2_DEVICE_STATUS_GET_MESSAGE_TYPE],
644
+ };
645
+ device.updateState(
646
+ {
647
+ identity: { deviceType },
648
+ status: { mode: 'bootloader' },
649
+ raw: { protocolV2ProtocolInfo: staleProtocolInfo },
650
+ },
651
+ 'initialize'
652
+ );
653
+ const acquire = jest
654
+ .fn()
655
+ .mockResolvedValue(isBle ? { uuid: 'fresh-session', protocolType: 'V2' } : 'fresh-session');
656
+ device.deviceConnector = { acquire } as never;
657
+
658
+ await device.acquire('V2', { throwOnRunPromiseError: true });
659
+ const typedCall = jest.fn().mockImplementation((requestType: string) => {
660
+ if (requestType === 'DeviceInfoGet') {
661
+ return {
662
+ message: {
663
+ protocol_version: 2,
664
+ hw: { Device_type: protocolV2DeviceType, serial_no: 'SERIAL-001' },
665
+ main_mcu: { application: { version: '5.0.0' } },
666
+ },
667
+ };
668
+ }
669
+ if (requestType === 'ProtocolInfoRequest') {
670
+ return { message: freshProtocolInfo };
671
+ }
672
+ if (requestType === 'DeviceStatusGet') {
673
+ return {
674
+ message: { init_states: true, unlocked: true, device_id: 'wallet-device-id' },
675
+ };
676
+ }
677
+ throw new Error(`Unexpected request: ${requestType}`);
678
+ });
679
+ device.commands.typedCall = typedCall as never;
680
+
681
+ await device.initialize();
682
+
683
+ expect(typedCall.mock.calls.map(callArgs => callArgs[0])).toEqual([
684
+ 'DeviceInfoGet',
685
+ 'ProtocolInfoRequest',
686
+ 'DeviceStatusGet',
687
+ ]);
688
+ expect(device.state?.status.mode).toBe('normal');
689
+ expect(device.state?.identity.deviceType).toBe(deviceType);
690
+ expect(device.hasDeviceAcquire()).toBe(true);
691
+ }
692
+ );
693
+
274
694
  test('exposes the current transport usage state in public device snapshots', () => {
275
695
  const getSettings = jest
276
696
  .spyOn(DataManager, 'getSettings')
@@ -38,6 +38,31 @@ describe('DevicePool state lifecycle', () => {
38
38
  expect(getDeviceState).toHaveBeenNthCalledWith(2, { refreshSections: ['settings'] });
39
39
  });
40
40
 
41
+ test('refreshes Protocol V2 discovery state without legacy-specific options', async () => {
42
+ const getDeviceState = jest.fn().mockResolvedValue({ status: { mode: 'bootloader' } });
43
+ const run = jest.fn(async (callback: () => Promise<void>) => callback());
44
+ const device = {
45
+ isProtocolV2: () => true,
46
+ getDeviceState,
47
+ run,
48
+ } as any;
49
+
50
+ await (DevicePool as any)._refreshRuntimeState(device, {
51
+ refreshRuntimeState: true,
52
+ });
53
+
54
+ expect(run).toHaveBeenCalledWith(expect.any(Function), {
55
+ connectProtocol: undefined,
56
+ forceProtocolDetection: undefined,
57
+ });
58
+ expect(getDeviceState).toHaveBeenNthCalledWith(1, {
59
+ refreshSections: ['status'],
60
+ });
61
+ expect(getDeviceState).toHaveBeenNthCalledWith(2, {
62
+ refreshSections: ['settings'],
63
+ });
64
+ });
65
+
41
66
  test('actively re-detects a cached device when protocol detection is forced', async () => {
42
67
  const descriptor = { path: 'cached-path', protocolType: 'V1' } as any;
43
68
  const device = Device.fromDescriptor(descriptor);