@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
package/src/core/index.ts CHANGED
@@ -1,6 +1,10 @@
1
1
  import semver from 'semver';
2
2
  import EventEmitter from 'events';
3
- import { DeviceSessionPinType, TRANSPORT_EVENT } from '@onekeyfe/hd-transport';
3
+ import {
4
+ DeviceSessionPinType,
5
+ TRANSPORT_EVENT,
6
+ isProtocolV2LinkDisabledError,
7
+ } from '@onekeyfe/hd-transport';
4
8
  import {
5
9
  ERRORS,
6
10
  ERROR_CODES_REQUIRE_DISCONNECT,
@@ -79,6 +83,7 @@ import type { BaseMethod } from '../api/BaseMethod';
79
83
 
80
84
  const Log = getLogger(LoggerNames.Core);
81
85
  const PRE_INITIALIZE_TTL_MS = 60 * 1000;
86
+ const PRE_PENDING_CALL_TIMEOUT_MS = 15 * 1000;
82
87
 
83
88
  // Dedup/coalesce state for "pre-warm signal" methods (isPreWarmSignal),
84
89
  // keyed by getPreWarmKey(): coalesce in-flight, skip if warmed within TTL.
@@ -136,6 +141,9 @@ const toError = (error: unknown): Error | undefined => {
136
141
  }
137
142
  };
138
143
 
144
+ const isExpectedCompatibilityError = (error: unknown) =>
145
+ error instanceof HardwareError && error.errorCode === HardwareErrorCode.DeviceNotSupportMethod;
146
+
139
147
  const updateMethodRequestContext = (method: BaseMethod, updates: any) => {
140
148
  if (method.requestContext) {
141
149
  updateRequestContext(method.requestContext.responseID, updates);
@@ -289,26 +297,35 @@ const handlePreWarmSignal = async (
289
297
  }
290
298
  };
291
299
 
292
- const waitWithTimeout = async (promise: Promise<any>, timeout: number) => {
293
- const timeoutPromise = new Promise((_, reject) => {
294
- setTimeout(() => reject(new Error('Request timeout')), timeout);
295
- });
296
- return Promise.race([promise, timeoutPromise]);
297
- };
298
-
299
300
  const waitForPendingPromise = async (
300
- getPrePendingCallPromise: () => Promise<void> | undefined,
301
- removePrePendingCallPromise?: (promise: Promise<void> | undefined) => void
301
+ connectId: string,
302
+ getPrePendingCallPromise: (connectId: string) => Promise<void> | undefined,
303
+ removePrePendingCallPromise?: (connectId: string, promise: Promise<void>) => void
302
304
  ) => {
303
- const pendingPromise = getPrePendingCallPromise();
305
+ const pendingPromise = getPrePendingCallPromise(connectId);
304
306
  if (pendingPromise) {
305
307
  Log.debug('pre pending call promise before call method, wait for it');
308
+ let timer: ReturnType<typeof setTimeout> | undefined;
309
+ let timedOut = false;
306
310
  try {
307
- await waitWithTimeout(pendingPromise, 5 * 1000);
311
+ await Promise.race([
312
+ pendingPromise,
313
+ new Promise<void>(resolve => {
314
+ timer = setTimeout(() => {
315
+ timedOut = true;
316
+ resolve();
317
+ }, PRE_PENDING_CALL_TIMEOUT_MS);
318
+ }),
319
+ ]);
308
320
  } catch (error) {
309
- // ignore timeout error
321
+ // Cancellation is best-effort; the transport teardown owns recovery.
322
+ } finally {
323
+ if (timer) clearTimeout(timer);
324
+ removePrePendingCallPromise?.(connectId, pendingPromise);
325
+ }
326
+ if (timedOut) {
327
+ Log.warn('pre pending call promise timed out before call method', { connectId });
310
328
  }
311
- removePrePendingCallPromise?.(pendingPromise);
312
329
  Log.debug('pre pending call promise before call method done');
313
330
  }
314
331
  };
@@ -320,7 +337,7 @@ const onCallDevice = async (
320
337
  ): Promise<any> => {
321
338
  let messageResponse: any;
322
339
 
323
- const { requestQueue, getPrePendingCallPromise, setPrePendingCallPromise } = context;
340
+ const { requestQueue, getPrePendingCallPromise, removePrePendingCallPromise } = context;
324
341
 
325
342
  updateMethodRequestContext(method, { status: 'running' });
326
343
 
@@ -348,7 +365,11 @@ const onCallDevice = async (
348
365
  await context.waitForCallbackTasks(method.connectId);
349
366
  }
350
367
 
351
- await waitForPendingPromise(getPrePendingCallPromise, setPrePendingCallPromise);
368
+ await waitForPendingPromise(
369
+ method.connectId ?? '',
370
+ getPrePendingCallPromise,
371
+ removePrePendingCallPromise
372
+ );
352
373
 
353
374
  const task = requestQueue.createTask(method);
354
375
 
@@ -394,6 +415,13 @@ const onCallDevice = async (
394
415
  if (method.payload?.onlyConnectBleDevice) {
395
416
  preWarmCallbackTask?.resolve();
396
417
  Log.debug('Call API - only connect ble device: ', device?.mainId);
418
+ // This early return bypasses the normal-path bookkeeping at the end of the
419
+ // call. Without it the task leaks and haunts every later queue snapshot
420
+ // and cancel sweep (field log: a completed task lingered for 6 minutes),
421
+ // and the request stays in the active maps, so repeated preconnects pile
422
+ // up phantom work in diagnostics.
423
+ completeMethodRequestContext(method);
424
+ requestQueue.releaseTask(method.responseID);
397
425
  return createResponseMessage(method.responseID, true, null);
398
426
  }
399
427
 
@@ -447,7 +475,11 @@ const onCallDevice = async (
447
475
  await context.waitForCallbackTasks(method.connectId, preWarmCallbackTask);
448
476
  }
449
477
 
450
- await waitForPendingPromise(getPrePendingCallPromise, setPrePendingCallPromise);
478
+ await waitForPendingPromise(
479
+ method.connectId ?? '',
480
+ getPrePendingCallPromise,
481
+ removePrePendingCallPromise
482
+ );
451
483
 
452
484
  const inner = async (): Promise<void> => {
453
485
  // Protocol is established from an active device response during acquire/initialize.
@@ -619,7 +651,8 @@ const onCallDevice = async (
619
651
  method.payload?.passphraseState,
620
652
  method.payload?.useEmptyPassphrase,
621
653
  method.payload?.skipPassphraseCheck,
622
- hasDeriveCardano(method)
654
+ hasDeriveCardano(method),
655
+ method.protocolV2UnlockContext?.preflightMainPinSelected
623
656
  );
624
657
 
625
658
  // Double check, handles the special case of Touch/Pro
@@ -693,7 +726,9 @@ const onCallDevice = async (
693
726
  try {
694
727
  return await task.callPromise.promise;
695
728
  } catch (e) {
696
- Log.debug('Device Run Error: ', e);
729
+ if (!isExpectedCompatibilityError(e)) {
730
+ Log.debug('Device Run Error: ', e);
731
+ }
697
732
  completeMethodRequestContext(method, e);
698
733
  return createResponseMessage(method.responseID, false, { error: e });
699
734
  }
@@ -887,22 +922,112 @@ function canSkipInitialize(method: BaseMethod, device: Device): boolean {
887
922
  return true;
888
923
  }
889
924
 
890
- function isRetryableBleProtocolV2ProbeError(method: BaseMethod, error: unknown) {
891
- const message = error instanceof Error ? error.message : String(error ?? '');
925
+ export function isRetryableBleProtocolV2ProbeError(method: BaseMethod, error: unknown) {
926
+ const typedError = error as { errorCode?: unknown; message?: unknown };
927
+ const message =
928
+ typeof typedError?.message === 'string' ? typedError.message : String(error ?? '');
892
929
  return (
893
930
  method.payload.connectProtocol === 'V2' &&
931
+ typedError?.errorCode === HardwareErrorCode.RuntimeError &&
894
932
  message.includes('Device protocol mismatch') &&
895
933
  message.includes('expected V2') &&
896
934
  message.includes('did not respond to expected protocol')
897
935
  );
898
936
  }
899
937
 
938
+ export function isRetryableBleConnectionError(method: BaseMethod, error: unknown) {
939
+ if (method.device?.wasInterruptedByUser()) {
940
+ return false;
941
+ }
942
+ const typedError = error as { errorCode?: unknown };
943
+ return (
944
+ typedError?.errorCode === HardwareErrorCode.BleTimeoutError ||
945
+ typedError?.errorCode === HardwareErrorCode.BleConnectedError ||
946
+ isRetryableBleProtocolV2ProbeError(method, error) ||
947
+ isMissingDetectedProtocolV2Error(method, error)
948
+ );
949
+ }
950
+
951
+ export function isMissingDetectedProtocolV2Error(method: BaseMethod, error: unknown) {
952
+ const typedError = error as { errorCode?: unknown; message?: unknown };
953
+ return (
954
+ method.payload.connectProtocol === 'V2' &&
955
+ typedError?.errorCode === HardwareErrorCode.RuntimeError &&
956
+ typeof typedError.message === 'string' &&
957
+ typedError.message.includes('Device protocol has not been detected')
958
+ );
959
+ }
960
+
961
+ export function isProtocolV2PeerRemovedPairingError(method: BaseMethod, error: unknown) {
962
+ return (
963
+ method.payload.connectProtocol === 'V2' &&
964
+ (error as { errorCode?: unknown })?.errorCode ===
965
+ HardwareErrorCode.BlePeerRemovedPairingInformation
966
+ );
967
+ }
968
+
900
969
  /**
901
970
  * If the Bluetooth connection times out, retry up to 6 times
902
971
  * @param retryCount - Current retry count (default 0)
903
972
  */
904
- async function connectDeviceForBle(method: BaseMethod, device: Device, retryCount = 0) {
973
+ // device.acquire awaits a transport reply with no deadline of its own; a
974
+ // transport that never settles (field case: Electron main lost an IPC reply,
975
+ // "reply was never sent" after 5 minutes) hangs the call forever and cancel()
976
+ // only takes effect at poll checkpoints. Race acquire against a deadline and
977
+ // the caller's abort signal so the hang is bounded and cancel is immediate.
978
+ const BLE_ACQUIRE_DEADLINE_MS = 60 * 1000;
979
+
980
+ function raceBleAcquire<T>(acquirePromise: Promise<T>, abortSignal?: AbortSignal): Promise<T> {
981
+ return new Promise<T>((resolve, reject) => {
982
+ let settled = false;
983
+ const settle = (fn: () => void) => {
984
+ if (settled) return;
985
+ settled = true;
986
+ clearTimeout(deadline);
987
+ abortSignal?.removeEventListener('abort', onAbort);
988
+ fn();
989
+ };
990
+ const onAbort = () =>
991
+ settle(() => reject(ERRORS.TypedError(HardwareErrorCode.CallQueueActionCancelled)));
992
+ const deadline = setTimeout(
993
+ () =>
994
+ settle(() =>
995
+ reject(
996
+ ERRORS.TypedError(
997
+ HardwareErrorCode.BleTimeoutError,
998
+ `BLE acquire exceeded ${BLE_ACQUIRE_DEADLINE_MS}ms deadline`
999
+ )
1000
+ )
1001
+ ),
1002
+ BLE_ACQUIRE_DEADLINE_MS
1003
+ );
1004
+ // Attach before any early return so a late settlement of acquirePromise
1005
+ // is always consumed — an abort or deadline must never leave the acquire
1006
+ // rejection unhandled.
1007
+ acquirePromise.then(
1008
+ value => settle(() => resolve(value)),
1009
+ error => settle(() => reject(error))
1010
+ );
1011
+ if (abortSignal) {
1012
+ if (abortSignal.aborted) {
1013
+ onAbort();
1014
+ return;
1015
+ }
1016
+ abortSignal.addEventListener('abort', onAbort);
1017
+ }
1018
+ });
1019
+ }
1020
+
1021
+ async function connectDeviceForBle(
1022
+ method: BaseMethod,
1023
+ device: Device,
1024
+ abortSignal?: AbortSignal,
1025
+ retryCount = 0
1026
+ ) {
905
1027
  try {
1028
+ if (device.wasInterruptedByUser()) {
1029
+ throw ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromUser);
1030
+ }
906
1031
  if (method.payload.forceProtocolDetection && device.hasDeviceAcquire()) {
907
1032
  await device.release();
908
1033
  }
@@ -912,9 +1037,43 @@ async function connectDeviceForBle(method: BaseMethod, device: Device, retryCoun
912
1037
  !device.commands ||
913
1038
  device.commands.disposed;
914
1039
  if (shouldAcquire) {
915
- await device.acquire(method.payload.connectProtocol, {
916
- forceProtocolDetection: method.payload.forceProtocolDetection,
917
- });
1040
+ // The deadline/abort guards are scoped to the desktop electron
1041
+ // transport: its IPC acquire is the only path with a proven
1042
+ // never-settling failure mode, while react-native/lowlevel acquire may
1043
+ // legitimately block on a user-driven system bonding prompt for longer
1044
+ // than any sane deadline. Other envs keep the plain acquire unchanged.
1045
+ const useAcquireGuards = DataManager.getSettings('env') === 'desktop-web-ble';
1046
+ // A cancel landing during the retry backoff must not start a new acquire.
1047
+ if (useAcquireGuards && abortSignal?.aborted) {
1048
+ throw ERRORS.TypedError(HardwareErrorCode.CallQueueActionCancelled);
1049
+ }
1050
+ if (!useAcquireGuards) {
1051
+ await device.acquire(method.payload.connectProtocol, {
1052
+ forceProtocolDetection: method.payload.forceProtocolDetection,
1053
+ });
1054
+ } else {
1055
+ try {
1056
+ await raceBleAcquire(
1057
+ device.acquire(method.payload.connectProtocol, {
1058
+ forceProtocolDetection: method.payload.forceProtocolDetection,
1059
+ }),
1060
+ abortSignal
1061
+ );
1062
+ } catch (err) {
1063
+ // A deadline hit means the transport is wedged mid-acquire; drop the
1064
+ // link before the retry so it cold-connects instead of stacking a
1065
+ // second connect onto the half-open one.
1066
+ if (
1067
+ err.errorCode === HardwareErrorCode.BleTimeoutError &&
1068
+ device.mainId &&
1069
+ device.deviceConnector
1070
+ ) {
1071
+ await device.deviceConnector.disconnect(device.mainId).catch(() => undefined);
1072
+ device.markTransportDisconnected();
1073
+ }
1074
+ throw err;
1075
+ }
1076
+ }
918
1077
  }
919
1078
  if (method.payload?.onlyConnectBleDevice) {
920
1079
  if (shouldAcquire) {
@@ -934,12 +1093,13 @@ async function connectDeviceForBle(method: BaseMethod, device: Device, retryCoun
934
1093
  DevicePool.emitter.emit(DEVICE.CONNECT, device);
935
1094
  }
936
1095
  } catch (err) {
1096
+ const requiresColdReconnect = isMissingDetectedProtocolV2Error(method, err);
937
1097
  // Device.run()'s REQUIRE_DISCONNECT handling never sees acquire/initialize
938
1098
  // failures, so with keep-alive a wedged link would be reused by every retry
939
1099
  // (field case: Initialize timing out at 25s per attempt, forever). Drop it
940
1100
  // here so the next retry cold-connects.
941
1101
  if (
942
- ERROR_CODES_REQUIRE_DISCONNECT.includes(err.errorCode) &&
1102
+ (ERROR_CODES_REQUIRE_DISCONNECT.includes(err.errorCode) || requiresColdReconnect) &&
943
1103
  device.mainId &&
944
1104
  device.deviceConnector
945
1105
  ) {
@@ -949,16 +1109,11 @@ async function connectDeviceForBle(method: BaseMethod, device: Device, retryCoun
949
1109
  // next attempt skip acquire and initialize onto the link we just cut.
950
1110
  device.markTransportDisconnected();
951
1111
  }
952
- if (
953
- (err.errorCode === HardwareErrorCode.BleTimeoutError ||
954
- err.errorCode === HardwareErrorCode.BleConnectedError ||
955
- isRetryableBleProtocolV2ProbeError(method, err)) &&
956
- retryCount < 6
957
- ) {
1112
+ if (isRetryableBleConnectionError(method, err) && retryCount < 6) {
958
1113
  const nextRetry = retryCount + 1;
959
- Log.debug(`Bluetooth connect timeout and will retry, retry count: ${nextRetry}`);
1114
+ Log.debug(`Bluetooth connection will retry, retry count: ${nextRetry}`);
960
1115
  await wait(3000);
961
- await connectDeviceForBle(method, device, nextRetry);
1116
+ await connectDeviceForBle(method, device, abortSignal, nextRetry);
962
1117
  } else {
963
1118
  throw err;
964
1119
  }
@@ -1063,13 +1218,31 @@ const ensureConnected = async (
1063
1218
  if (abort()) {
1064
1219
  return;
1065
1220
  }
1066
- await connectDeviceForBle(method, device);
1221
+ // One generation per public ensureConnected() call, not per poll
1222
+ // iteration. A later poll must not clear a user cancel.
1223
+ if (tryCount === 1) {
1224
+ device.beginConnectionAttempt();
1225
+ }
1226
+ await connectDeviceForBle(method, device, abortSignal);
1067
1227
  }
1068
1228
  resolve(device);
1069
1229
  return;
1070
1230
  }
1071
1231
  } catch (error) {
1072
1232
  Log.debug('device error: ', error);
1233
+ if (error.errorCode === HardwareErrorCode.BleUnavailableWhileUsbConnected) {
1234
+ reject(error);
1235
+ return;
1236
+ }
1237
+ if (isProtocolV2LinkDisabledError(error)) {
1238
+ reject(
1239
+ ERRORS.TypedError(HardwareErrorCode.BleUnavailableWhileUsbConnected, undefined, {
1240
+ failureCode: error.failureCode,
1241
+ firmwareMessage: error.firmwareMessage,
1242
+ })
1243
+ );
1244
+ return;
1245
+ }
1073
1246
  if ([HardwareErrorCode.BleCharacteristicNotifyChangeFailure].includes(error.errorCode)) {
1074
1247
  postMessage(createUiMessage(UI_REQUEST.BLUETOOTH_CHARACTERISTIC_NOTIFY_CHANGE_FAILURE));
1075
1248
  }
@@ -1092,7 +1265,10 @@ const ensureConnected = async (
1092
1265
  HardwareErrorCode.DeviceDetectInBootloaderMode,
1093
1266
  HardwareErrorCode.BleCharacteristicNotifyChangeFailure,
1094
1267
  HardwareErrorCode.BridgeNeedsPermission,
1095
- ].includes(error.errorCode)
1268
+ HardwareErrorCode.DeviceInterruptedFromUser,
1269
+ HardwareErrorCode.CallQueueActionCancelled,
1270
+ ].includes(error.errorCode) ||
1271
+ isProtocolV2PeerRemovedPairingError(method, error)
1096
1272
  ) {
1097
1273
  reject(error);
1098
1274
  return;
@@ -1148,23 +1324,34 @@ export const cancel = (context: CoreContext, connectId?: string) => {
1148
1324
  ','
1149
1325
  )}`
1150
1326
  );
1327
+ // Abort before rejecting: rejectRequest releases the task and would make
1328
+ // its AbortController unreachable to an in-flight method loop.
1329
+ requestQueue.abortRequestsByConnectId(connectId);
1151
1330
  const canceledDevices: Device[] = [];
1331
+ const interruptDevice = (device: Device | undefined, deviceConnectId: string) => {
1332
+ if (!device || canceledDevices.includes(device)) {
1333
+ return;
1334
+ }
1335
+ setPrePendingCallPromise(deviceConnectId, device.interruptionFromUser());
1336
+ canceledDevices.push(device);
1337
+ };
1152
1338
  for (const requestId of requestIds) {
1153
1339
  const task = requestQueue.getTask(requestId);
1154
1340
  Log.debug('Cancel Api connect task: ', task);
1155
- if (task && task.method?.device) {
1156
- if (!canceledDevices.includes(task.method.device)) {
1157
- const { device } = task.method;
1158
- setPrePendingCallPromise(device?.interruptionFromUser());
1159
- canceledDevices.push(device);
1160
- }
1341
+ if (task) {
1342
+ // During ensureConnected the method has a connectId but device is
1343
+ // assigned only after the poll succeeds. Interrupt the cached BLE
1344
+ // Device so an in-flight acquire/initialize cannot finish.
1345
+ interruptDevice(task.method?.device, task.method.connectId ?? connectId);
1346
+ interruptDevice(deviceCacheMap.get(connectId), connectId);
1161
1347
  requestQueue.rejectRequest(
1162
1348
  requestId,
1163
1349
  ERRORS.TypedError(HardwareErrorCode.CallQueueActionCancelled)
1164
1350
  );
1165
1351
  }
1166
1352
  }
1167
- requestQueue.abortRequestsByConnectId(connectId);
1353
+ interruptDevice(deviceCacheMap.get(connectId), connectId);
1354
+ pollingManager.stop(connectId);
1168
1355
  } catch (e) {
1169
1356
  Log.error('Cancel API Error: ', e);
1170
1357
  }
@@ -1172,23 +1359,33 @@ export const cancel = (context: CoreContext, connectId?: string) => {
1172
1359
  const env = DataManager.getSettings('env');
1173
1360
  if (DataManager.isBleConnect(env)) {
1174
1361
  Log.debug('Cancel Api all _deviceList: ');
1362
+ // Keep method abort signals observable until every active task is rejected.
1363
+ requestQueue.abortAllRequests();
1175
1364
  const canceledDevices: Device[] = [];
1365
+ const interruptDevice = (device?: Device) => {
1366
+ if (!device || canceledDevices.includes(device)) {
1367
+ return;
1368
+ }
1369
+ device.interruptionFromUser().catch(() => undefined);
1370
+ canceledDevices.push(device);
1371
+ };
1176
1372
  for (const requestId of requestQueue.getRequestTasksId()) {
1177
1373
  const task = requestQueue.getTask(requestId);
1178
1374
  Log.debug('Cancel Api connect task: ', task);
1179
- if (task && task.method?.device) {
1180
- if (!canceledDevices.includes(task.method.device)) {
1181
- const { device } = task.method;
1182
- device?.interruptionFromUser();
1183
- canceledDevices.push(device);
1375
+ if (task) {
1376
+ interruptDevice(task.method?.device);
1377
+ if (task.method.connectId) {
1378
+ interruptDevice(deviceCacheMap.get(task.method.connectId));
1379
+ pollingManager.stop(task.method.connectId);
1184
1380
  }
1185
-
1186
1381
  requestQueue.rejectRequest(
1187
1382
  requestId,
1188
1383
  ERRORS.TypedError(HardwareErrorCode.CallQueueActionCancelled)
1189
1384
  );
1190
1385
  }
1191
1386
  }
1387
+ deviceCacheMap.forEach(interruptDevice);
1388
+ pollingManager.stopAll();
1192
1389
  } else {
1193
1390
  _deviceList?.allDevices().forEach(device => {
1194
1391
  Log.debug('device: ', device, ' device.hasDeviceAcquire: ', device.hasDeviceAcquire());
@@ -1238,6 +1435,10 @@ const checkPassphraseEnableState = (method: BaseMethod, features?: Features) =>
1238
1435
  const shouldCheckPassphraseState = (method: BaseMethod, device: Device) => {
1239
1436
  if (!method.useDevicePassphraseState) return false;
1240
1437
 
1438
+ if (device.isProtocolV2() && method.payload?.useEmptyPassphrase === true) {
1439
+ return true;
1440
+ }
1441
+
1241
1442
  return device.hasUsePassphrase();
1242
1443
  };
1243
1444
 
@@ -1248,7 +1449,6 @@ const cleanup = () => {
1248
1449
  pendingUiPromises,
1249
1450
  ERRORS.TypedError(HardwareErrorCode.ActionCancelled, 'UI request was cancelled')
1250
1451
  );
1251
- Log.debug('Cleanup...');
1252
1452
  };
1253
1453
 
1254
1454
  const removeDeviceListener = (device: Device) => {
@@ -1510,7 +1710,7 @@ export default class Core extends EventEmitter {
1510
1710
  private disposePromise?: Promise<void>;
1511
1711
 
1512
1712
  // background task
1513
- private prePendingCallPromise: Promise<void> | undefined;
1713
+ private prePendingCallPromises = new Map<string, Promise<void>>();
1514
1714
 
1515
1715
  private methodSynchronize = getSynchronize();
1516
1716
 
@@ -1527,9 +1727,18 @@ export default class Core extends EventEmitter {
1527
1727
  tracingContext: this.tracingContext,
1528
1728
  requestQueue: this.requestQueue,
1529
1729
  methodSynchronize: this.methodSynchronize,
1530
- getPrePendingCallPromise: () => this.prePendingCallPromise,
1531
- setPrePendingCallPromise: (promise: Promise<void> | undefined) => {
1532
- this.prePendingCallPromise = promise;
1730
+ getPrePendingCallPromise: (connectId: string) => this.prePendingCallPromises.get(connectId),
1731
+ setPrePendingCallPromise: (connectId: string, promise?: Promise<void>) => {
1732
+ if (!promise) {
1733
+ this.prePendingCallPromises.delete(connectId);
1734
+ return;
1735
+ }
1736
+ this.prePendingCallPromises.set(connectId, promise);
1737
+ },
1738
+ removePrePendingCallPromise: (connectId: string, promise: Promise<void>) => {
1739
+ if (this.prePendingCallPromises.get(connectId) === promise) {
1740
+ this.prePendingCallPromises.delete(connectId);
1741
+ }
1533
1742
  },
1534
1743
  // callback 任务管理
1535
1744
  registerCallbackTask: (connectId: string, callbackPromise: Deferred<any>) => {
@@ -1644,7 +1853,7 @@ export default class Core extends EventEmitter {
1644
1853
  preWarmInflight.clear();
1645
1854
  preWarmDoneAt.clear();
1646
1855
  preConnectCache = { passphraseState: undefined };
1647
- this.prePendingCallPromise = undefined;
1856
+ this.prePendingCallPromises.clear();
1648
1857
  this.removeAllListeners();
1649
1858
  cleanupSdkInstance(this.sdkInstanceId);
1650
1859
  if (_core === this) _core = undefined;