@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
@@ -9,6 +9,7 @@ import {
9
9
  ERROR_CODES_REQUIRE_RELEASE,
10
10
  HardwareError,
11
11
  HardwareErrorCode,
12
+ canonicalizePro2BleAdvertisementName,
12
13
  createDeferred,
13
14
  createDeviceNotSupportMethodError,
14
15
  } from '@onekeyfe/hd-shared';
@@ -61,6 +62,7 @@ import {
61
62
  PROTOCOL_V2_VERSIONS_DEVICE_INFO_REQUEST,
62
63
  type ProtocolV2RuntimeMode,
63
64
  getProtocolV2RuntimeMode,
65
+ isLegacyProtocolV2ProtocolInfo,
64
66
  requestProtocolV2DeviceInfo,
65
67
  requestProtocolV2DeviceStatus,
66
68
  requestProtocolV2ProtocolInfo,
@@ -234,6 +236,15 @@ export class Device extends EventEmitter {
234
236
  */
235
237
  private deviceAcquired = false;
236
238
 
239
+ /**
240
+ * Connection-attempt generation. interruptionFromUser() pins the current
241
+ * attempt so a late acquire cannot finish, while the next public connect
242
+ * increments this and is allowed to proceed.
243
+ */
244
+ private connectionAttempt = 0;
245
+
246
+ private interruptedAttempt: number | null = null;
247
+
237
248
  /** Canonical device-state cache; legacy Features is a compatibility projection. */
238
249
  private stateStore = new DeviceStateStore();
239
250
 
@@ -283,6 +294,9 @@ export class Device extends EventEmitter {
283
294
 
284
295
  runPromise?: Deferred<void> | null;
285
296
 
297
+ /** Resolves only after the active run has completed its release path. */
298
+ private runCleanupPromise?: Promise<void>;
299
+
286
300
  externalState: string[] = [];
287
301
 
288
302
  unavailableCapabilities: UnavailableCapabilities = {};
@@ -425,6 +439,8 @@ export class Device extends EventEmitter {
425
439
  expectedProtocol?: HardwareConnectProtocol,
426
440
  options?: { throwOnRunPromiseError?: boolean; forceProtocolDetection?: boolean }
427
441
  ) {
442
+ const attempt = this.connectionAttempt;
443
+ this.throwIfInterruptedByUser();
428
444
  const env = DataManager.getSettings('env');
429
445
  const mainIdKey = DataManager.isBleConnect(env) ? 'id' : 'session';
430
446
  const previousProtocol = this.originalDescriptor.protocolType;
@@ -445,7 +461,8 @@ export class Device extends EventEmitter {
445
461
  undefined,
446
462
  true,
447
463
  strictProtocol,
448
- undefined
464
+ undefined,
465
+ options?.forceProtocolDetection
449
466
  );
450
467
  this.mainId = (acquireResult as any)?.uuid ?? '';
451
468
  Log.debug('Expected uuid:', this.mainId);
@@ -455,7 +472,8 @@ export class Device extends EventEmitter {
455
472
  this.originalDescriptor.session,
456
473
  undefined,
457
474
  strictProtocol,
458
- undefined
475
+ undefined,
476
+ options?.forceProtocolDetection
459
477
  );
460
478
  this.mainId = acquireResult as string | undefined;
461
479
  Log.debug('Expected session id:', this.mainId);
@@ -477,6 +495,15 @@ export class Device extends EventEmitter {
477
495
  if (detectedProtocol) {
478
496
  this.originalDescriptor.protocolType = detectedProtocol;
479
497
  }
498
+ if (this.interruptedAttempt === attempt || this.connectionAttempt !== attempt) {
499
+ const session = this.mainId;
500
+ if (session && this.deviceConnector?.disconnect) {
501
+ await this.deviceConnector.disconnect(session).catch(disconnectError => {
502
+ Log.debug('Ignored disconnect after user cancel during acquire', disconnectError);
503
+ });
504
+ }
505
+ throw ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromUser);
506
+ }
480
507
  this.deviceAcquired = true;
481
508
  this.updateDescriptor({ [mainIdKey]: this.mainId } as unknown as DeviceDescriptor);
482
509
 
@@ -485,6 +512,10 @@ export class Device extends EventEmitter {
485
512
  }
486
513
 
487
514
  this.commands = new DeviceCommands(this, this.mainId ?? '');
515
+ // Protocol V2 runtime metadata belongs to one active transport link. A
516
+ // successful acquire creates a fresh link/session, so never carry cached
517
+ // ProtocolInfo or pre-initialize state across that boundary.
518
+ this.invalidateProtocolV2RuntimeState();
488
519
  } catch (error) {
489
520
  if (options?.forceProtocolDetection) {
490
521
  this.originalDescriptor.protocolType = previousProtocol;
@@ -642,7 +673,8 @@ export class Device extends EventEmitter {
642
673
  }
643
674
 
644
675
  getCurrentBleName() {
645
- return this.state?.identity.bleName ?? null;
676
+ const bleName = this.state?.identity.bleName ?? null;
677
+ return bleName ? canonicalizePro2BleAdvertisementName(bleName) : null;
646
678
  }
647
679
 
648
680
  getCurrentLabel() {
@@ -893,6 +925,7 @@ export class Device extends EventEmitter {
893
925
  }
894
926
 
895
927
  async initialize(options?: InitOptions) {
928
+ this.throwIfInterruptedByUser();
896
929
  // Protocol V2 does not support legacy Initialize; use its dedicated flow.
897
930
  if (this.isProtocolV2()) {
898
931
  this.passphraseState = options?.passphraseState;
@@ -922,13 +955,18 @@ export class Device extends EventEmitter {
922
955
  };
923
956
 
924
957
  const expectedDeviceId = options?.deviceId;
925
- if (expectedDeviceId) {
926
- // 先只读校验物理设备身份;钱包上下文仍由下方携带完整参数的
927
- // Initialize 选择,避免标准钱包请求复用此前的隐藏钱包上下文。
958
+
959
+ if (expectedDeviceId && !(this.features && this.checkDeviceId(expectedDeviceId))) {
960
+ // No locally-cached evidence that the device at this path is the
961
+ // expected one (first contact, or the cached features already disagree
962
+ // with the caller). Establish identity with a context-free Initialize
963
+ // BEFORE any wallet context (session_id / passphrase_state) goes on
964
+ // the wire. In normal flows features are always fresh — enumerate /
965
+ // getFeatures run first and every call response refreshes them — so
966
+ // this extra round trip is confined to the ambiguous cases where the
967
+ // disclosure risk actually lives.
928
968
  this.passphraseState = undefined;
929
- const { message } = await this.commands.typedCall('GetFeatures', 'Features', {});
930
- this._updateFeatures(message);
931
- await TransportManager.reconfigure(this.features);
969
+ await callInitialize({ is_contains_attach: true });
932
970
  if (!this.checkDeviceId(expectedDeviceId)) {
933
971
  throw ERRORS.TypedError(HardwareErrorCode.DeviceCheckDeviceIdError);
934
972
  }
@@ -952,7 +990,46 @@ export class Device extends EventEmitter {
952
990
  payload.derive_cardano = true;
953
991
  }
954
992
 
955
- await callInitialize(payload, options?.initSession);
993
+ if (this.features) {
994
+ // Re-sync the V1 message schema for THIS device before encoding
995
+ // Initialize: the process-global schema may still reflect another
996
+ // device (e.g. legacy-firmware Touch/Mini) on multi-device setups, and
997
+ // a stale legacy schema would silently strip passphrase_state /
998
+ // is_contains_attach from the wire message. Local operation, no wire
999
+ // I/O; a no-op when the schema is unchanged.
1000
+ await TransportManager.reconfigure(this.features);
1001
+ }
1002
+
1003
+ // Initialize's own Features response carries device_id, so the physical
1004
+ // device identity is validated on the same round trip instead of via a
1005
+ // separate read-only GetFeatures preflight (which doubled the wire cost
1006
+ // of every deviceId-carrying call). The method fn has not run yet, so a
1007
+ // mismatch still fails before any wallet data can be derived, with the
1008
+ // same DeviceCheckDeviceIdError. Wallet-context selection is unchanged:
1009
+ // it is decided by the Initialize payload above either way.
1010
+ const assertExpectedDeviceIdentity = () => {
1011
+ if (expectedDeviceId && !this.checkDeviceId(expectedDeviceId)) {
1012
+ // The mismatched Initialize may have cached a session under the wrong
1013
+ // device's identity; drop it so no wallet context survives from it.
1014
+ // (This also evicts any session the wrong device legitimately cached
1015
+ // under the same passphraseState — a deliberate conservative purge
1016
+ // after a physical-swap event, consistent with
1017
+ // reconcileDeviceIdentity purging the previous device's sessions.)
1018
+ this.clearInternalState();
1019
+ throw ERRORS.TypedError(HardwareErrorCode.DeviceCheckDeviceIdError);
1020
+ }
1021
+ };
1022
+
1023
+ try {
1024
+ await callInitialize(payload, options?.initSession);
1025
+ } catch (error) {
1026
+ // callInitialize can fail AFTER the wire call cached the session (e.g.
1027
+ // TransportManager.reconfigure rejecting); the identity check must
1028
+ // still run so a wrong-device session never survives the error path.
1029
+ assertExpectedDeviceIdentity();
1030
+ throw error;
1031
+ }
1032
+ assertExpectedDeviceIdentity();
956
1033
  } catch (error) {
957
1034
  Log.error('Initialization failed:', error);
958
1035
  throw error;
@@ -977,11 +1054,7 @@ export class Device extends EventEmitter {
977
1054
  });
978
1055
  // The default request excludes SE/hash data and therefore uses basic scope.
979
1056
  // Full version and verification data require getDeviceState({ scope: 'firmware' }).
980
- const features = await this.probeProtocolV2RuntimeState(
981
- deviceInfo,
982
- options?.protocolV2DeviceInfoTimeoutMs
983
- );
984
- Log.debug('Protocol V2 features:', features);
1057
+ await this.probeProtocolV2RuntimeState(deviceInfo, options?.protocolV2DeviceInfoTimeoutMs);
985
1058
  } catch (error) {
986
1059
  Log.error('Protocol V2 initialization failed:', error);
987
1060
  throw error;
@@ -1053,7 +1126,13 @@ export class Device extends EventEmitter {
1053
1126
  }
1054
1127
 
1055
1128
  if (refresh.has('status') && !initializedWithDeviceInfo) {
1056
- await this.probeProtocolV2RuntimeState(refreshedDeviceInfo);
1129
+ const cachedMode = this.state?.status.mode;
1130
+ await this.probeProtocolV2RuntimeState(refreshedDeviceInfo, undefined, {
1131
+ // Loader firmware does not support DeviceStatusGet. Renegotiate ProtocolInfo
1132
+ // during an explicit refresh so a device rebooted into application firmware
1133
+ // can leave the cached loader state.
1134
+ forceRuntimeContextRefresh: cachedMode === 'bootloader' || cachedMode === 'romloader',
1135
+ });
1057
1136
  }
1058
1137
 
1059
1138
  if (refresh.has('settings') && this.state?.status.mode === 'normal') {
@@ -1072,6 +1151,10 @@ export class Device extends EventEmitter {
1072
1151
  return params.includeRaw ? cloneDeviceState(this.state) : createPublicDeviceState(this.state);
1073
1152
  }
1074
1153
 
1154
+ async refreshProtocolV2SettingsAfterMutation() {
1155
+ return this.getDeviceState({ refreshSections: ['status', 'settings'] });
1156
+ }
1157
+
1075
1158
  _updateFeatures(protoFeatures: PROTO.Features | Features, initSession?: boolean) {
1076
1159
  const previousDeviceId = this.getCurrentDeviceId();
1077
1160
  let feat =
@@ -1101,9 +1184,10 @@ export class Device extends EventEmitter {
1101
1184
  source,
1102
1185
  changedKeys: result.changedKeys,
1103
1186
  };
1104
- Log.debug('Device state patch committed', {
1187
+ Log.debug('Device state updated', {
1105
1188
  source,
1106
- keys: result.changedKeys,
1189
+ revision: result.revision,
1190
+ changedKeyCount: result.changedKeys.length,
1107
1191
  });
1108
1192
  this.emit(DEVICE.STATE, this, event);
1109
1193
  if (result.state.protocol === 'V1') {
@@ -1124,12 +1208,17 @@ export class Device extends EventEmitter {
1124
1208
  return this.features;
1125
1209
  }
1126
1210
 
1127
- async ensureProtocolV2RuntimeContext(timeoutMs?: number): Promise<ProtocolInfo> {
1211
+ async ensureProtocolV2RuntimeContext(
1212
+ timeoutMs?: number,
1213
+ options?: { forceRefresh?: boolean }
1214
+ ): Promise<ProtocolInfo> {
1128
1215
  const cachedProtocolInfo =
1129
- this.protocolV2RuntimeContext ??
1130
- (!this.protocolV2StateNeedsReload
1131
- ? this.state?.raw?.protocolV2ProtocolInfo ?? undefined
1132
- : undefined);
1216
+ options?.forceRefresh === true
1217
+ ? undefined
1218
+ : this.protocolV2RuntimeContext ??
1219
+ (!this.protocolV2StateNeedsReload
1220
+ ? this.state?.raw?.protocolV2ProtocolInfo ?? undefined
1221
+ : undefined);
1133
1222
  if (cachedProtocolInfo) {
1134
1223
  this.protocolV2RuntimeContext = cachedProtocolInfo;
1135
1224
  return cachedProtocolInfo;
@@ -1169,10 +1258,19 @@ export class Device extends EventEmitter {
1169
1258
  }
1170
1259
  }
1171
1260
 
1172
- async probeProtocolV2RuntimeState(deviceInfo?: ProtocolV2DeviceInfo, timeoutMs?: number) {
1173
- const protocolInfo = await this.ensureProtocolV2RuntimeContext(timeoutMs);
1174
- const runtimeMode = getProtocolV2RuntimeMode(protocolInfo);
1261
+ async probeProtocolV2RuntimeState(
1262
+ deviceInfo?: ProtocolV2DeviceInfo,
1263
+ timeoutMs?: number,
1264
+ options?: {
1265
+ forceRuntimeContextRefresh?: boolean;
1266
+ }
1267
+ ) {
1268
+ const protocolInfo = await this.ensureProtocolV2RuntimeContext(timeoutMs, {
1269
+ forceRefresh: options?.forceRuntimeContextRefresh,
1270
+ });
1175
1271
  const runtimeDeviceInfo = deviceInfo ?? this.state?.raw?.protocolV2DeviceInfo;
1272
+ const runtimeMode = getProtocolV2RuntimeMode(protocolInfo, runtimeDeviceInfo);
1273
+ const legacyProtocolInfo = isLegacyProtocolV2ProtocolInfo(protocolInfo);
1176
1274
  const protocolV2DeviceType = runtimeDeviceInfo
1177
1275
  ? resolveProtocolV2DeviceIdentity(runtimeDeviceInfo.hw?.Device_type).deviceType
1178
1276
  : this.getCurrentDeviceType();
@@ -1186,10 +1284,9 @@ export class Device extends EventEmitter {
1186
1284
  'Protocol V2 romloader mode is only supported for Pro2 and Neo.'
1187
1285
  );
1188
1286
  }
1189
- const deviceStatusSupported = supportsProtocolV2Message(
1190
- protocolInfo,
1191
- PROTOCOL_V2_DEVICE_STATUS_GET_MESSAGE_TYPE
1192
- );
1287
+ const deviceStatusSupported =
1288
+ legacyProtocolInfo ||
1289
+ supportsProtocolV2Message(protocolInfo, PROTOCOL_V2_DEVICE_STATUS_GET_MESSAGE_TYPE);
1193
1290
 
1194
1291
  if (runtimeMode === 'bootloader' || runtimeMode === 'romloader') {
1195
1292
  return this.updateProtocolV2Features(deviceInfo, null, runtimeMode, protocolInfo);
@@ -1250,8 +1347,7 @@ export class Device extends EventEmitter {
1250
1347
  });
1251
1348
  }
1252
1349
 
1253
- markTransportDisconnected() {
1254
- this.deviceAcquired = false;
1350
+ private invalidateProtocolV2RuntimeState() {
1255
1351
  if (!this.isProtocolV2()) return;
1256
1352
  this.protocolV2StateNeedsReload = true;
1257
1353
  this.protocolV2RuntimeContext = undefined;
@@ -1260,6 +1356,11 @@ export class Device extends EventEmitter {
1260
1356
  this.clearPreInitialized();
1261
1357
  }
1262
1358
 
1359
+ markTransportDisconnected() {
1360
+ this.deviceAcquired = false;
1361
+ this.invalidateProtocolV2RuntimeState();
1362
+ }
1363
+
1263
1364
  invalidateAfterWipe() {
1264
1365
  const deviceId = this.getCurrentDeviceId();
1265
1366
  if (deviceId) {
@@ -1269,10 +1370,7 @@ export class Device extends EventEmitter {
1269
1370
  if (this.originalDescriptor.path !== deviceId) {
1270
1371
  deviceWalletSessionStore.deleteDevice(this.originalDescriptor.path);
1271
1372
  }
1272
- this.protocolV2StateNeedsReload = true;
1273
- this.protocolV2RuntimeContext = undefined;
1274
- this.protocolV2RuntimeContextPromise = undefined;
1275
- this.protocolV2RuntimeContextRequestToken = undefined;
1373
+ this.invalidateProtocolV2RuntimeState();
1276
1374
  }
1277
1375
 
1278
1376
  this.passphraseState = undefined;
@@ -1284,11 +1382,7 @@ export class Device extends EventEmitter {
1284
1382
  markProtocolV2Reboot(rebootType: DeviceRebootType) {
1285
1383
  if (!this.isProtocolV2()) return;
1286
1384
 
1287
- this.protocolV2StateNeedsReload = true;
1288
- this.protocolV2RuntimeContext = undefined;
1289
- this.protocolV2RuntimeContextPromise = undefined;
1290
- this.protocolV2RuntimeContextRequestToken = undefined;
1291
- this.clearPreInitialized();
1385
+ this.invalidateProtocolV2RuntimeState();
1292
1386
  let loaderMode: 'bootloader' | 'romloader' | undefined;
1293
1387
  if (rebootType === DeviceRebootType.Bootloader) {
1294
1388
  loaderMode = 'bootloader';
@@ -1362,6 +1456,10 @@ export class Device extends EventEmitter {
1362
1456
  if (device.features) {
1363
1457
  this._updateFeatures(device.features);
1364
1458
  }
1459
+ // Adopting another Device instance's command channel also crosses an active
1460
+ // link boundary. Renegotiate runtime metadata on that channel instead of
1461
+ // retaining ProtocolInfo from the previous instance/session.
1462
+ this.invalidateProtocolV2RuntimeState();
1365
1463
  }
1366
1464
 
1367
1465
  async run(fn?: () => Promise<void>, options?: RunOptions) {
@@ -1370,16 +1468,25 @@ export class Device extends EventEmitter {
1370
1468
  Log.debug('[Device] run error:', 'Device is running, but will cancel previous operate');
1371
1469
  }
1372
1470
 
1471
+ this.beginConnectionAttempt();
1373
1472
  options = parseRunOptions(options);
1374
1473
 
1375
1474
  const runPromise = createDeferred<void>();
1376
1475
  this.runPromise = runPromise;
1377
- this._runInner(fn, options, runPromise).catch(error => {
1476
+ const cleanupPromise = this._runInner(fn, options, runPromise).catch(error => {
1378
1477
  if (this.runPromise === runPromise) {
1379
1478
  this.runPromise = null;
1380
1479
  }
1381
1480
  runPromise.reject(error);
1382
1481
  });
1482
+ this.runCleanupPromise = cleanupPromise;
1483
+ cleanupPromise
1484
+ .finally(() => {
1485
+ if (this.runCleanupPromise === cleanupPromise) {
1486
+ this.runCleanupPromise = undefined;
1487
+ }
1488
+ })
1489
+ .catch(() => undefined);
1383
1490
  return runPromise.promise;
1384
1491
  }
1385
1492
 
@@ -1491,13 +1598,32 @@ export class Device extends EventEmitter {
1491
1598
 
1492
1599
  async interruptionFromUser() {
1493
1600
  const error = ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromUser);
1494
- await this.cancelableAction?.(error);
1601
+ this.interruptedAttempt = this.connectionAttempt;
1602
+ const cleanupPromise = this.runCleanupPromise;
1603
+ const { cancelableAction } = this;
1604
+ if (cancelableAction) {
1605
+ await cancelableAction(error);
1606
+ } else if (this.shouldSendFallbackProtocolCancel()) {
1607
+ await this.commands?.cancelDevice?.().catch(cancelError => {
1608
+ Log.debug('Protocol V2 fallback cancel error', cancelError);
1609
+ });
1610
+ } else if (!this.hasDeviceAcquire()) {
1611
+ // Pairing / connect-native / probe: drop the physical link only.
1612
+ // Never acquire or send protocol Cancel just to abort setup.
1613
+ if (this.mainId && this.deviceConnector?.disconnect) {
1614
+ await this.deviceConnector.disconnect(this.mainId).catch(disconnectError => {
1615
+ Log.debug('Ignored disconnect during user cancel without acquire', disconnectError);
1616
+ });
1617
+ }
1618
+ this.markTransportDisconnected();
1619
+ }
1495
1620
  await this.commands?.cancel();
1496
1621
 
1497
1622
  if (this.runPromise) {
1498
1623
  this.runPromise.reject(error);
1499
1624
  this.runPromise = null;
1500
1625
  }
1626
+ await cleanupPromise?.catch(() => undefined);
1501
1627
  }
1502
1628
 
1503
1629
  setCancelableAction(callback: (err?: Error) => Promise<unknown>) {
@@ -1553,6 +1679,44 @@ export class Device extends EventEmitter {
1553
1679
  return typeof this.originalDescriptor.session === 'string';
1554
1680
  }
1555
1681
 
1682
+ beginConnectionAttempt() {
1683
+ this.connectionAttempt += 1;
1684
+ return this.connectionAttempt;
1685
+ }
1686
+
1687
+ wasInterruptedByUser() {
1688
+ return (
1689
+ typeof this.interruptedAttempt === 'number' &&
1690
+ this.interruptedAttempt === this.connectionAttempt
1691
+ );
1692
+ }
1693
+
1694
+ private throwIfInterruptedByUser() {
1695
+ if (this.wasInterruptedByUser()) {
1696
+ throw ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromUser);
1697
+ }
1698
+ }
1699
+
1700
+ /**
1701
+ * Protocol Cancel is only for an acquired session that is already in a
1702
+ * user-facing prompt. Connect, probe and initialize must not send Cancel
1703
+ * and must not re-acquire just to deliver one.
1704
+ */
1705
+ private shouldSendFallbackProtocolCancel() {
1706
+ if (!this.hasDeviceAcquire() || !this.isProtocolV2()) {
1707
+ return false;
1708
+ }
1709
+ if (!this.hasOpenProtocolV2UiInteraction()) {
1710
+ return false;
1711
+ }
1712
+ const env = DataManager.getSettings('env');
1713
+ return (
1714
+ DataManager.isBleConnect(env) ||
1715
+ DataManager.isBrowserWebUsb(env) ||
1716
+ DataManager.isDesktopWebUsb(env)
1717
+ );
1718
+ }
1719
+
1556
1720
  hasDeviceAcquire() {
1557
1721
  const env = DataManager.getSettings('env');
1558
1722
  if (DataManager.isBleConnect(env)) {
@@ -1841,7 +2005,8 @@ export class Device extends EventEmitter {
1841
2005
  passphraseState?: string,
1842
2006
  useEmptyPassphrase?: boolean,
1843
2007
  skipPassphraseCheck?: boolean,
1844
- deriveCardano?: boolean
2008
+ deriveCardano?: boolean,
2009
+ mainPinSelected?: boolean
1845
2010
  ) {
1846
2011
  if (this.isUnacquired()) return false;
1847
2012
 
@@ -1852,6 +2017,7 @@ export class Device extends EventEmitter {
1852
2017
  onlyMainPin: useEmptyPassphrase,
1853
2018
  deriveCardano,
1854
2019
  rejectAttachPinForMainWallet: useEmptyPassphrase === true,
2020
+ mainPinSelected,
1855
2021
  });
1856
2022
 
1857
2023
  // Main wallet and unlock Attach Pin, throw safe error
@@ -7,6 +7,7 @@ import {
7
7
  type Transport,
8
8
  type TransportCallOptions,
9
9
  getSafeTransportLogPayload,
10
+ isProtocolV2LinkDisabledFailure,
10
11
  } from '@onekeyfe/hd-transport';
11
12
 
12
13
  import TransportManager from '../data-manager/TransportManager';
@@ -68,7 +69,7 @@ const DEVICE_CONTROL_CALLS = new Set([
68
69
  // Older Protocol V2 firmware may omit the cancellation subcode, so retain a
69
70
  // narrow message fallback for compatibility.
70
71
  const isProtocolV2ActionCancelledMessage = (message: string) =>
71
- /^(?:cancel(?:led|ed)(?: on device)?|confirm dismissed|user cancel(?:led|ed)(?:\s+.*)?)$/i.test(
72
+ /^(?:cancel(?:led|ed)(?: on device)?|confirm dismissed|update cancel(?:led|ed)|user cancel(?:led|ed)(?:\s+.*)?)$/i.test(
72
73
  message
73
74
  );
74
75
 
@@ -263,6 +264,7 @@ export class DeviceCommands {
263
264
  }
264
265
  const activeCall = this.callPromise;
265
266
  let timer: ReturnType<typeof setTimeout> | undefined;
267
+ let timedOut = false;
266
268
  const cancellation = (async () => {
267
269
  await this.dispose(true);
268
270
  await activeCall?.catch(() => undefined);
@@ -272,11 +274,23 @@ export class DeviceCommands {
272
274
  await Promise.race([
273
275
  cancellation,
274
276
  new Promise<void>(resolve => {
275
- timer = setTimeout(resolve, 10 * 1000);
277
+ timer = setTimeout(() => {
278
+ timedOut = true;
279
+ resolve();
280
+ }, 10 * 1000);
276
281
  }),
277
282
  ]);
278
283
  } finally {
279
284
  if (timer) clearTimeout(timer);
285
+ if (
286
+ timedOut &&
287
+ this.transport.name === 'ReactNativeBleTransport' &&
288
+ this.transport.disconnect
289
+ ) {
290
+ await this.transport.disconnect(this.mainId).catch(error => {
291
+ Log.debug('BLE cancellation timeout disconnect error (ignored)', error);
292
+ });
293
+ }
280
294
  if (this.callPromise === activeCall) {
281
295
  this.callPromise = undefined;
282
296
  }
@@ -304,13 +318,6 @@ export class DeviceCommands {
304
318
  const promise = this.transport.call(this.mainId, type, msg ?? {}, options) as any;
305
319
  this.callPromise = promise;
306
320
  const res = await promise;
307
- if (!shouldReduceDebug) {
308
- LogCore.debug(
309
- '[DeviceCommands] [call] Received',
310
- res.type,
311
- getSafeTransportLogPayload(res.message, res.type)
312
- );
313
- }
314
321
  return res;
315
322
  } catch (error) {
316
323
  LogCore.debug('[DeviceCommands] [call] Received error', {
@@ -445,13 +452,10 @@ export class DeviceCommands {
445
452
  if (!shouldReduceDebugForCall(callType)) {
446
453
  Log.debug('_filterCommonTypes: ', {
447
454
  request: callType,
448
- response:
449
- callType === 'DeviceFirmwareUpdateStatusGet'
450
- ? {
451
- type: res.type,
452
- message: getSafeTransportLogPayload(res.message, res.type),
453
- }
454
- : res.type,
455
+ response: {
456
+ type: res.type,
457
+ message: getSafeTransportLogPayload(res.message, res.type),
458
+ },
455
459
  });
456
460
  }
457
461
  } catch (error) {
@@ -491,7 +495,7 @@ export class DeviceCommands {
491
495
  if (message === 'Please confirm the BlindSign enabled') {
492
496
  error = ERRORS.TypedError(HardwareErrorCode.BlindSignDisabled);
493
497
  }
494
- if (message === 'File already exists') {
498
+ if (message === 'File already exists' || message === 'NFT already exists') {
495
499
  error = ERRORS.TypedError(HardwareErrorCode.FileAlreadyExists);
496
500
  }
497
501
  if (message?.includes('bytes overflow')) {
@@ -505,7 +509,12 @@ export class DeviceCommands {
505
509
  }
506
510
  }
507
511
 
508
- if (code === 'Failure_ProcessError') {
512
+ if (isProtocolV2LinkDisabledFailure(code, message)) {
513
+ error = ERRORS.TypedError(HardwareErrorCode.BleUnavailableWhileUsbConnected, undefined, {
514
+ failureCode: code,
515
+ firmwareMessage: message,
516
+ });
517
+ } else if (code === 'Failure_ProcessError') {
509
518
  const normalizedMessage = message?.trim() ?? '';
510
519
  const isProtocolV2ActionCancelledFailure =
511
520
  this.device.isProtocolV2() && isProtocolV2ActionCancelledMessage(normalizedMessage);
@@ -1,3 +1,4 @@
1
+ import { isProtocolV2LinkDisabledError } from '@onekeyfe/hd-transport';
1
2
  import { ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared';
2
3
 
3
4
  import { safeThrowError } from '../constants';
@@ -94,7 +95,9 @@ export default class DeviceConnector {
94
95
  session?: string | null,
95
96
  forceCleanRunPromise?: boolean,
96
97
  expectedProtocol?: HardwareConnectProtocol,
97
- protocolHint?: HardwareConnectProtocol
98
+ protocolHint?: HardwareConnectProtocol,
99
+ forceProtocolDetection?: boolean,
100
+ skipProtocolProbe?: boolean
98
101
  ) {
99
102
  Log.debug('acquire', path, session, expectedProtocol, protocolHint);
100
103
  const env = DataManager.getSettings('env');
@@ -102,18 +105,23 @@ export default class DeviceConnector {
102
105
  const transport = this.getActiveTransport();
103
106
  let res;
104
107
  if (DataManager.isBleConnect(env)) {
105
- res = await transport.acquire({
108
+ const acquireInput: Parameters<Transport['acquire']>[0] = {
106
109
  uuid: path,
107
110
  forceCleanRunPromise,
108
111
  expectedProtocol,
109
112
  protocolHint,
110
- });
113
+ forceProtocolDetection,
114
+ skipProtocolProbe,
115
+ };
116
+ res = await transport.acquire(acquireInput);
111
117
  } else {
112
118
  res = await transport.acquire({
113
119
  path,
114
120
  previous: session ?? null,
115
121
  expectedProtocol,
116
122
  protocolHint,
123
+ forceProtocolDetection,
124
+ skipProtocolProbe,
117
125
  });
118
126
  }
119
127
  if (expectedProtocol) {
@@ -134,6 +142,12 @@ export default class DeviceConnector {
134
142
  return res;
135
143
  } catch (error) {
136
144
  Log.error('acquire error: ', error.message);
145
+ if (isProtocolV2LinkDisabledError(error)) {
146
+ throw ERRORS.TypedError(HardwareErrorCode.BleUnavailableWhileUsbConnected, undefined, {
147
+ failureCode: error.failureCode,
148
+ firmwareMessage: error.firmwareMessage,
149
+ });
150
+ }
137
151
  safeThrowError(error);
138
152
  }
139
153
  }
@@ -172,6 +172,9 @@ export class DevicePool extends EventEmitter {
172
172
  const device = cachedDevice ?? Device.fromDescriptor(descriptor);
173
173
  if (isNewDevice || forceProtocolDetection) {
174
174
  device.deviceConnector = this.connector;
175
+ // Search/discovery acquires outside Device.run(). Start a new attempt so
176
+ // a previous user cancel does not hide this device from later searches.
177
+ device.beginConnectionAttempt?.();
175
178
  if (forceProtocolDetection && !isNewDevice) {
176
179
  await device.acquire(initOptions?.connectProtocol, { forceProtocolDetection: true });
177
180
  } else {
@@ -218,9 +221,13 @@ export class DevicePool extends EventEmitter {
218
221
  * Device.getDeviceState skips unsupported status/settings calls in loader mode.
219
222
  */
220
223
  static async _refreshProtocolV2DiscoveryState(device: Device) {
221
- await device.getDeviceState({ refreshSections: ['status'] });
224
+ await device.getDeviceState({
225
+ refreshSections: ['status'],
226
+ });
222
227
  try {
223
- await device.getDeviceState({ refreshSections: ['settings'] });
228
+ await device.getDeviceState({
229
+ refreshSections: ['settings'],
230
+ });
224
231
  } catch (error) {
225
232
  Log.debug('Unable to refresh Protocol V2 device label during discovery', error);
226
233
  }