@onekeyfe/hd-core 1.2.0-alpha.98 → 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 (204) 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 +89 -38
  5. package/__tests__/check-firmware-release-protocol-v2.test.ts +83 -14
  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 +14 -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 +128 -2
  21. package/__tests__/firmware-update/firmware-update-plan-bootloader-identity.test.ts +80 -0
  22. package/__tests__/firmware-update/firmware-update-plan.test.ts +114 -50
  23. package/__tests__/firmware-update/firmware-update-prepared-plan.test.ts +19 -5
  24. package/__tests__/firmware-update/firmware-update-v3-reconnect.test.ts +116 -0
  25. package/__tests__/firmware-update/firmware-update-v4-install-poll.test.ts +678 -0
  26. package/__tests__/firmware-update/firmware-update-v4-parameters.test.ts +85 -0
  27. package/__tests__/get-device-state.test.ts +60 -21
  28. package/__tests__/logBlockEvent.test.ts +13 -1
  29. package/__tests__/open-wallet-session.test.ts +127 -1
  30. package/__tests__/protocol-v2-firmware-targets.test.ts +39 -0
  31. package/__tests__/protocol-v2-resources.test.ts +185 -88
  32. package/__tests__/protocol-v2-unlock-policy.test.ts +235 -2
  33. package/__tests__/protocol-v2.test.ts +2274 -466
  34. package/__tests__/refresh-device-state.test.ts +3 -1
  35. package/__tests__/resourceBase64Boundary.test.ts +48 -0
  36. package/__tests__/search-devices.test.ts +79 -32
  37. package/__tests__/ton-sign-message.test.ts +84 -0
  38. package/dist/api/BaseMethod.d.ts +2 -1
  39. package/dist/api/BaseMethod.d.ts.map +1 -1
  40. package/dist/api/CheckAllFirmwareRelease.d.ts.map +1 -1
  41. package/dist/api/CheckBootloaderRelease.d.ts.map +1 -1
  42. package/dist/api/FirmwareUpdateV2.d.ts.map +1 -1
  43. package/dist/api/FirmwareUpdateV3.d.ts.map +1 -1
  44. package/dist/api/FirmwareUpdateV4.d.ts +22 -7
  45. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  46. package/dist/api/OpenWalletSession.d.ts.map +1 -1
  47. package/dist/api/SearchDevices.d.ts.map +1 -1
  48. package/dist/api/UploadPortfolio.d.ts +1 -1
  49. package/dist/api/UploadPortfolio.d.ts.map +1 -1
  50. package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts.map +1 -1
  51. package/dist/api/device/DeviceChangePin.d.ts.map +1 -1
  52. package/dist/api/device/DeviceRebootToBoardloader.d.ts.map +1 -1
  53. package/dist/api/device/DeviceRebootToBootloader.d.ts.map +1 -1
  54. package/dist/api/device/DeviceSettings.d.ts.map +1 -1
  55. package/dist/api/device/DeviceVerify.d.ts +1 -0
  56. package/dist/api/device/DeviceVerify.d.ts.map +1 -1
  57. package/dist/api/device/DeviceWipe.d.ts.map +1 -1
  58. package/dist/api/firmware/FirmwareArtifactSource.d.ts +5 -1
  59. package/dist/api/firmware/FirmwareArtifactSource.d.ts.map +1 -1
  60. package/dist/api/firmware/FirmwareHostBinding.d.ts.map +1 -1
  61. package/dist/api/firmware/FirmwarePreparedResourceArchive.d.ts.map +1 -1
  62. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts +2 -2
  63. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts.map +1 -1
  64. package/dist/api/firmware/FirmwareUpdatePlan.d.ts +0 -15
  65. package/dist/api/firmware/FirmwareUpdatePlan.d.ts.map +1 -1
  66. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts +1 -0
  67. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts.map +1 -1
  68. package/dist/api/firmware/getBinary.d.ts +3 -2
  69. package/dist/api/firmware/getBinary.d.ts.map +1 -1
  70. package/dist/api/firmware/protocolV2Release.d.ts.map +1 -1
  71. package/dist/api/helpers/base64Data.d.ts +16 -0
  72. package/dist/api/helpers/base64Data.d.ts.map +1 -0
  73. package/dist/api/protocol-v2/DeviceFirmwareUpdate.d.ts +3 -1
  74. package/dist/api/protocol-v2/DeviceFirmwareUpdate.d.ts.map +1 -1
  75. package/dist/api/protocol-v2/DeviceGetFirmwareUpdateStatus.d.ts.map +1 -1
  76. package/dist/api/protocol-v2/DeviceInfoGet.d.ts +1 -1
  77. package/dist/api/protocol-v2/DeviceInfoGet.d.ts.map +1 -1
  78. package/dist/api/protocol-v2/DeviceReboot.d.ts +1 -1
  79. package/dist/api/protocol-v2/DeviceReboot.d.ts.map +1 -1
  80. package/dist/api/protocol-v2/DeviceUploadNft.d.ts +2 -3
  81. package/dist/api/protocol-v2/DeviceUploadNft.d.ts.map +1 -1
  82. package/dist/api/protocol-v2/DeviceUploadWallpaper.d.ts +2 -3
  83. package/dist/api/protocol-v2/DeviceUploadWallpaper.d.ts.map +1 -1
  84. package/dist/api/protocol-v2/helpers.d.ts.map +1 -1
  85. package/dist/api/ton/TonSignMessage.d.ts.map +1 -1
  86. package/dist/core/index.d.ts +6 -1
  87. package/dist/core/index.d.ts.map +1 -1
  88. package/dist/data-manager/DataManager.d.ts +4 -3
  89. package/dist/data-manager/DataManager.d.ts.map +1 -1
  90. package/dist/data-manager/TransportManager.d.ts.map +1 -1
  91. package/dist/device/Device.d.ts +16 -3
  92. package/dist/device/Device.d.ts.map +1 -1
  93. package/dist/device/DeviceCommands.d.ts +4 -4
  94. package/dist/device/DeviceCommands.d.ts.map +1 -1
  95. package/dist/device/DeviceConnector.d.ts +1 -1
  96. package/dist/device/DeviceConnector.d.ts.map +1 -1
  97. package/dist/device/DevicePool.d.ts.map +1 -1
  98. package/dist/device/DeviceStateMapper.d.ts.map +1 -1
  99. package/dist/deviceProfile/buildDeviceFeatures.d.ts.map +1 -1
  100. package/dist/events/logBlockEvent.d.ts.map +1 -1
  101. package/dist/events/ui-request.d.ts +3 -0
  102. package/dist/events/ui-request.d.ts.map +1 -1
  103. package/dist/index.d.ts +54 -84
  104. package/dist/index.d.ts.map +1 -1
  105. package/dist/index.js +2333 -1515
  106. package/dist/protocols/protocol-v2/features.d.ts +13 -5
  107. package/dist/protocols/protocol-v2/features.d.ts.map +1 -1
  108. package/dist/protocols/protocol-v2/index.d.ts +2 -2
  109. package/dist/protocols/protocol-v2/index.d.ts.map +1 -1
  110. package/dist/protocols/protocol-v2/resources.d.ts +12 -7
  111. package/dist/protocols/protocol-v2/resources.d.ts.map +1 -1
  112. package/dist/protocols/protocol-v2/unlockPolicyRunner.d.ts +3 -1
  113. package/dist/protocols/protocol-v2/unlockPolicyRunner.d.ts.map +1 -1
  114. package/dist/protocols/protocol-v2/walletSession.d.ts +3 -0
  115. package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
  116. package/dist/types/api/firmwareUpdatePlan.d.ts +2 -2
  117. package/dist/types/api/firmwareUpdatePlan.d.ts.map +1 -1
  118. package/dist/types/api/protocolV2.d.ts +1 -1
  119. package/dist/types/api/protocolV2.d.ts.map +1 -1
  120. package/dist/types/device.d.ts.map +1 -1
  121. package/dist/types/settings.d.ts +3 -31
  122. package/dist/types/settings.d.ts.map +1 -1
  123. package/dist/utils/deviceFeaturesCompat.d.ts.map +1 -1
  124. package/dist/utils/deviceFeaturesUtils.d.ts +3 -0
  125. package/dist/utils/deviceFeaturesUtils.d.ts.map +1 -1
  126. package/dist/utils/deviceInfoUtils.d.ts.map +1 -1
  127. package/dist/utils/deviceSettings.d.ts +1 -0
  128. package/dist/utils/deviceSettings.d.ts.map +1 -1
  129. package/dist/utils/index.d.ts +1 -1
  130. package/dist/utils/index.d.ts.map +1 -1
  131. package/dist/utils/patch.d.ts +1 -1
  132. package/dist/utils/patch.d.ts.map +1 -1
  133. package/dist/utils/pro2Nft.d.ts +7 -0
  134. package/dist/utils/pro2Nft.d.ts.map +1 -1
  135. package/package.json +6 -4
  136. package/src/api/BaseMethod.ts +8 -1
  137. package/src/api/CheckAllFirmwareRelease.ts +8 -5
  138. package/src/api/CheckBLEFirmwareRelease.ts +2 -2
  139. package/src/api/CheckBootloaderRelease.ts +3 -2
  140. package/src/api/CheckFirmwareRelease.ts +2 -2
  141. package/src/api/FirmwareUpdateV2.ts +130 -69
  142. package/src/api/FirmwareUpdateV3.ts +29 -19
  143. package/src/api/FirmwareUpdateV4.ts +966 -743
  144. package/src/api/OpenWalletSession.ts +39 -9
  145. package/src/api/PromptWebDeviceAccess.ts +2 -2
  146. package/src/api/SearchDevices.ts +6 -3
  147. package/src/api/UploadPortfolio.ts +9 -2
  148. package/src/api/allnetwork/AllNetworkGetAddressBase.ts +2 -1
  149. package/src/api/device/DeviceChangePin.ts +4 -1
  150. package/src/api/device/DeviceRebootToBoardloader.ts +3 -1
  151. package/src/api/device/DeviceRebootToBootloader.ts +3 -1
  152. package/src/api/device/DeviceSettings.ts +34 -20
  153. package/src/api/device/DeviceVerify.ts +25 -0
  154. package/src/api/device/DeviceWipe.ts +4 -1
  155. package/src/api/firmware/FirmwareArtifactSource.ts +41 -14
  156. package/src/api/firmware/FirmwareHostBinding.ts +10 -0
  157. package/src/api/firmware/FirmwarePreparedResourceArchive.ts +16 -27
  158. package/src/api/firmware/FirmwareUpdateBaseMethod.ts +131 -76
  159. package/src/api/firmware/FirmwareUpdatePlan.ts +55 -82
  160. package/src/api/firmware/FirmwareUpdatePreparedPlan.ts +32 -15
  161. package/src/api/firmware/protocolV2Release.ts +7 -2
  162. package/src/api/helpers/base64Data.ts +85 -0
  163. package/src/api/protocol-v2/DeviceFactoryInfoSet.ts +1 -1
  164. package/src/api/protocol-v2/DeviceFirmwareUpdate.ts +6 -28
  165. package/src/api/protocol-v2/DeviceGetFirmwareUpdateStatus.ts +7 -1
  166. package/src/api/protocol-v2/DeviceInfoGet.ts +3 -3
  167. package/src/api/protocol-v2/DeviceReboot.ts +5 -0
  168. package/src/api/protocol-v2/DeviceUploadNft.ts +61 -9
  169. package/src/api/protocol-v2/DeviceUploadWallpaper.ts +53 -19
  170. package/src/api/protocol-v2/helpers.ts +8 -0
  171. package/src/api/ton/TonSignMessage.ts +5 -3
  172. package/src/core/index.ts +264 -55
  173. package/src/data/messages/messages-protocol-v2.json +140 -13
  174. package/src/data-manager/DataManager.ts +46 -39
  175. package/src/data-manager/TransportManager.ts +3 -1
  176. package/src/device/Device.ts +210 -44
  177. package/src/device/DeviceCommands.ts +27 -18
  178. package/src/device/DeviceConnector.ts +17 -3
  179. package/src/device/DevicePool.ts +9 -2
  180. package/src/device/DeviceStateMapper.ts +30 -24
  181. package/src/deviceProfile/buildDeviceFeatures.ts +10 -5
  182. package/src/events/logBlockEvent.ts +14 -1
  183. package/src/events/ui-request.ts +3 -0
  184. package/src/index.ts +0 -10
  185. package/src/protocols/protocol-v2/features.ts +22 -5
  186. package/src/protocols/protocol-v2/index.ts +2 -0
  187. package/src/protocols/protocol-v2/resources.ts +110 -147
  188. package/src/protocols/protocol-v2/unlockPolicyRunner.ts +13 -1
  189. package/src/protocols/protocol-v2/walletSession.ts +65 -22
  190. package/src/types/api/firmwareUpdate.ts +1 -1
  191. package/src/types/api/firmwareUpdatePlan.ts +2 -2
  192. package/src/types/api/protocolV2.ts +1 -1
  193. package/src/types/device.ts +2 -1
  194. package/src/types/settings.ts +6 -34
  195. package/src/utils/deviceFeaturesCompat.ts +9 -4
  196. package/src/utils/deviceFeaturesUtils.ts +4 -0
  197. package/src/utils/deviceInfoUtils.ts +9 -2
  198. package/src/utils/deviceSettings.ts +11 -4
  199. package/src/utils/index.ts +1 -0
  200. package/src/utils/pro2Nft.ts +31 -8
  201. package/__tests__/firmware-memory-host.test.ts +0 -112
  202. package/dist/api/firmware/FirmwareMemoryHost.d.ts +0 -22
  203. package/dist/api/firmware/FirmwareMemoryHost.d.ts.map +0 -1
  204. package/src/api/firmware/FirmwareMemoryHost.ts +0 -142
@@ -16,8 +16,6 @@ jest.mock('../src/data/config', () => ({
16
16
  const release: IFirmwareReleaseInfo = {
17
17
  required: false,
18
18
  version: [2, 0, 0],
19
- url: 'https://example.com/application-p1.okpkg',
20
- fingerprint: 'release-fingerprint',
21
19
  changelog: {
22
20
  'zh-CN': '更新',
23
21
  'en-US': 'Update',
@@ -68,6 +66,37 @@ const createDevice = (deviceType: 'pro2' | 'neo') => ({
68
66
  }),
69
67
  });
70
68
 
69
+ const createFeaturelessDevice = (protocolV2: boolean) => {
70
+ const device = {
71
+ isProtocolV2: jest.fn(() => protocolV2),
72
+ features: undefined,
73
+ getDeviceState: jest.fn(),
74
+ };
75
+ device.getDeviceState.mockImplementation(() => {
76
+ Object.assign(device, {
77
+ features: {
78
+ deviceType: 'pro2',
79
+ firmwareVersion: '1.0.0',
80
+ },
81
+ });
82
+ return Promise.resolve({
83
+ identity: {
84
+ deviceType: 'pro2',
85
+ firmwareType: EFirmwareType.Universal,
86
+ },
87
+ status: { mode: 'bootloader' },
88
+ versions: {
89
+ firmware: '1.0.0',
90
+ applicationP1: '1.0.0',
91
+ bootloader: '1.0.0',
92
+ board: '1.0.0',
93
+ ble: '1.0.0',
94
+ },
95
+ });
96
+ });
97
+ return device;
98
+ };
99
+
71
100
  describe.each(['pro2', 'neo'] as const)('Protocol V2 release checks for %s', deviceType => {
72
101
  afterEach(() => {
73
102
  jest.restoreAllMocks();
@@ -147,7 +176,7 @@ describe.each(['pro2', 'neo'] as const)('Protocol V2 release checks for %s', dev
147
176
  deviceType,
148
177
  status: 'outdated',
149
178
  hasUpgrade: true,
150
- targetsToUpdate: ['boot', 'app_v1', 'coprocessor'],
179
+ targetsToUpdate: ['boot', 'app_v1', 'coprocessor', 'resource'],
151
180
  firmware: { status: 'outdated', shouldUpdate: true },
152
181
  ble: {
153
182
  status: 'outdated',
@@ -168,12 +197,50 @@ describe.each(['pro2', 'neo'] as const)('Protocol V2 release checks for %s', dev
168
197
  });
169
198
  });
170
199
 
171
- test('returns null from optional release probes when device features are unavailable', async () => {
172
- const deviceWithoutFeatures = {
173
- isProtocolV2: jest.fn(() => true),
174
- features: undefined,
175
- getDeviceState: jest.fn(),
176
- };
200
+ test('probes Protocol V2 loader state before requiring initialized features', async () => {
201
+ jest.spyOn(DataManager, 'getFirmwareLatestRelease').mockReturnValue(release);
202
+ const firmwareDevice = createFeaturelessDevice(true);
203
+ const bleDevice = createFeaturelessDevice(true);
204
+ const bootloaderDevice = createFeaturelessDevice(true);
205
+ const firmwareMethod = new CheckFirmwareRelease({
206
+ id: 1,
207
+ payload: { method: 'checkFirmwareRelease' },
208
+ });
209
+ const bleMethod = new CheckBLEFirmwareRelease({
210
+ id: 2,
211
+ payload: { method: 'checkBLEFirmwareRelease' },
212
+ });
213
+ const bootloaderMethod = new CheckBootloaderRelease({
214
+ id: 3,
215
+ payload: { method: 'checkBootloaderRelease' },
216
+ });
217
+
218
+ firmwareMethod.device = firmwareDevice as unknown as CheckFirmwareRelease['device'];
219
+ bleMethod.device = bleDevice as unknown as CheckBLEFirmwareRelease['device'];
220
+ bootloaderMethod.device = bootloaderDevice as unknown as CheckBootloaderRelease['device'];
221
+
222
+ await expect(firmwareMethod.run()).resolves.toMatchObject({
223
+ status: 'outdated',
224
+ shouldUpdate: true,
225
+ });
226
+ await expect(bleMethod.run()).resolves.toMatchObject({
227
+ status: 'outdated',
228
+ shouldUpdate: true,
229
+ });
230
+ await expect(bootloaderMethod.run()).resolves.toMatchObject({
231
+ status: 'outdated',
232
+ shouldUpdate: true,
233
+ });
234
+ for (const device of [firmwareDevice, bleDevice, bootloaderDevice]) {
235
+ expect(device.isProtocolV2).toHaveBeenCalled();
236
+ expect(device.getDeviceState).toHaveBeenCalledTimes(1);
237
+ }
238
+ });
239
+
240
+ test('keeps featureless Protocol V1 release probes optional', async () => {
241
+ const firmwareDevice = createFeaturelessDevice(false);
242
+ const bleDevice = createFeaturelessDevice(false);
243
+ const bootloaderDevice = createFeaturelessDevice(false);
177
244
  const firmwareMethod = new CheckFirmwareRelease({
178
245
  id: 1,
179
246
  payload: { method: 'checkFirmwareRelease' },
@@ -187,13 +254,15 @@ test('returns null from optional release probes when device features are unavail
187
254
  payload: { method: 'checkBootloaderRelease' },
188
255
  });
189
256
 
190
- firmwareMethod.device = deviceWithoutFeatures as unknown as CheckFirmwareRelease['device'];
191
- bleMethod.device = deviceWithoutFeatures as unknown as CheckBLEFirmwareRelease['device'];
192
- bootloaderMethod.device = deviceWithoutFeatures as unknown as CheckBootloaderRelease['device'];
257
+ firmwareMethod.device = firmwareDevice as unknown as CheckFirmwareRelease['device'];
258
+ bleMethod.device = bleDevice as unknown as CheckBLEFirmwareRelease['device'];
259
+ bootloaderMethod.device = bootloaderDevice as unknown as CheckBootloaderRelease['device'];
193
260
 
194
261
  await expect(firmwareMethod.run()).resolves.toBeNull();
195
262
  await expect(bleMethod.run()).resolves.toBeNull();
196
263
  await expect(bootloaderMethod.run()).resolves.toBeNull();
197
- expect(deviceWithoutFeatures.isProtocolV2).not.toHaveBeenCalled();
198
- expect(deviceWithoutFeatures.getDeviceState).not.toHaveBeenCalled();
264
+ for (const device of [firmwareDevice, bleDevice, bootloaderDevice]) {
265
+ expect(device.isProtocolV2).toHaveBeenCalled();
266
+ expect(device.getDeviceState).not.toHaveBeenCalled();
267
+ }
199
268
  });
@@ -1,4 +1,5 @@
1
1
  import { HardwareErrorCode } from '@onekeyfe/hd-shared';
2
+ import { createProtocolV2LinkDisabledError } from '@onekeyfe/hd-transport';
2
3
 
3
4
  import DeviceConnector from '../src/device/DeviceConnector';
4
5
 
@@ -78,4 +79,24 @@ describe('DeviceConnector protocol validation', () => {
78
79
  'Device protocol mismatch: expected V2, detected V1'
79
80
  );
80
81
  });
82
+
83
+ it('preserves the Protocol V2 USB-priority failure as public BLE error 723', async () => {
84
+ transportManagerMock.default.getTransport.mockReturnValue({
85
+ acquire: jest
86
+ .fn()
87
+ .mockRejectedValue(
88
+ createProtocolV2LinkDisabledError('Failure_ProcessError', 'link disabled')
89
+ ),
90
+ getProtocolType: jest.fn(() => undefined),
91
+ });
92
+ const connector = new DeviceConnector();
93
+
94
+ await expect(connector.acquire('pro2-id', undefined, undefined, 'V2')).rejects.toMatchObject({
95
+ errorCode: HardwareErrorCode.BleUnavailableWhileUsbConnected,
96
+ params: {
97
+ failureCode: 'Failure_ProcessError',
98
+ firmwareMessage: 'link disabled',
99
+ },
100
+ });
101
+ });
81
102
  });
@@ -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);