@onekeyfe/hd-core 1.2.0-alpha.140 → 1.2.0-alpha.142

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.
@@ -105,6 +105,7 @@ const PROTOCOL_V2_CONNECT_POLL_INTERVAL = 500;
105
105
  const PROTOCOL_V2_CONNECT_SINGLE_TIMEOUT = 75 * 1000;
106
106
  const PROTOCOL_V2_DEVICE_INFO_READY_TIMEOUT = 30 * 1000;
107
107
  const PROTOCOL_V2_FILE_TRANSFER_RETRY_COUNT = 3;
108
+ const PROTOCOL_V2_TRANSFER_PROGRESS_HEARTBEAT_MS = 1000;
108
109
  const PROTOCOL_V2_INSTALL_STATUS_CONFLICT_CODE = 'FirmwareInstallStatusConflict';
109
110
  const PROTOCOL_V2_INSTALL_FAILED_CODE = 'FirmwareInstallFailed';
110
111
  const PROTOCOL_V2_INSTALL_TIMEOUT_CODE = 'FirmwareInstallTimeout';
@@ -594,10 +595,16 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
594
595
 
595
596
  private protocolV2LatestFinalFeatures?: Features;
596
597
 
598
+ private protocolV2LatestFinalDeviceInfo?: ProtocolV2DeviceInfo;
599
+
597
600
  private protocolV2InstallBaselineVersions = new Map<number, string>();
598
601
 
599
602
  private protocolV2LastRuntimeProbeFeatures?: Features;
600
603
 
604
+ private protocolV2LastTransferProgress?: number;
605
+
606
+ private protocolV2LastTransferProgressAt = 0;
607
+
601
608
  init() {
602
609
  this.allowDeviceMode = [UI_REQUEST.BOOTLOADER, UI_REQUEST.NOT_INITIALIZE];
603
610
  this.requireDeviceMode = [];
@@ -1598,21 +1605,20 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1598
1605
  const expected = this.params?.expectedTargetVersions;
1599
1606
  if (!expected) return;
1600
1607
  const features = this.protocolV2LatestFinalFeatures;
1601
- const requestedTargets = this.getProtocolV2RequestedTargets();
1602
- const applicationTargets = requestedTargets.filter(
1603
- target => target === 'app_v1' || target === 'app_v2'
1604
- );
1608
+ const deviceInfo = this.protocolV2LatestFinalDeviceInfo;
1605
1609
  const visibleVersions: Partial<Record<FirmwareUpdateV4Target, string>> = {
1606
1610
  boot: features ? getDeviceBootloaderVersion(features).join('.') : undefined,
1611
+ // Protocol V2 firmware exposes P1 as the device firmware version after reboot.
1612
+ app_v1: features
1613
+ ? getDeviceFirmwareVersion(features).join('.')
1614
+ : deviceInfo?.main_mcu?.application?.version,
1615
+ app_v2: deviceInfo?.main_mcu?.application_data?.version,
1607
1616
  coprocessor: features ? getDeviceBLEFirmwareVersion(features).join('.') : undefined,
1608
1617
  se01: features?.se01Version ?? undefined,
1609
1618
  se02: features?.se02Version ?? undefined,
1610
1619
  se03: features?.se03Version ?? undefined,
1611
1620
  se04: features?.se04Version ?? undefined,
1612
1621
  };
1613
- if (features && applicationTargets.length === 1) {
1614
- visibleVersions[applicationTargets[0]] = getDeviceFirmwareVersion(features).join('.');
1615
- }
1616
1622
  const expectedEntries = (
1617
1623
  Object.entries(expected) as Array<[FirmwareUpdateV4Target, string]>
1618
1624
  ).filter(([target]) => !targets || targets.includes(target));
@@ -1629,14 +1635,8 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1629
1635
  Math.floor(statusVersion / 0x100) % 0x100
1630
1636
  }.${statusVersion % 0x100}`;
1631
1637
  if (!observedVersion) {
1632
- const requestedTargetIds = requestedTargets.flatMap(requestedTarget => {
1633
- if (requestedTarget === 'resource' || requestedTarget === 'boot_resources') return [];
1634
- const installTarget = PROTOCOL_V2_INSTALL_TARGET_BY_UPDATE_TARGET.get(requestedTarget);
1635
- return installTarget ? [installTarget.targetId] : [];
1636
- });
1637
1638
  const hasCompleteTargetEvidence =
1638
- requestedTargetIds.length > 0 &&
1639
- requestedTargetIds.every(targetId => this.protocolV2CompletedTargetIds.has(targetId));
1639
+ targetId !== undefined && this.protocolV2CompletedTargetIds.has(targetId);
1640
1640
  if (hasCompleteTargetEvidence) {
1641
1641
  Log.warn(
1642
1642
  `Protocol V2 target ${target} has no observable final version; install completion is authoritative`
@@ -1656,28 +1656,6 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1656
1656
  }
1657
1657
  }
1658
1658
 
1659
- private getProtocolV2RequestedTargets(): FirmwareUpdateV4Target[] {
1660
- if (this.params.targetsToUpdate?.length) {
1661
- return [...new Set(this.params.targetsToUpdate)];
1662
- }
1663
- if (this.params.preparedPlan?.targetsToUpdate.length) {
1664
- return [...new Set(this.params.preparedPlan.targetsToUpdate)] as FirmwareUpdateV4Target[];
1665
- }
1666
- const targets = new Set<FirmwareUpdateV4Target>();
1667
- Object.keys(this.params.componentArtifacts ?? {}).forEach(target =>
1668
- targets.add(target as FirmwareUpdateV4Target)
1669
- );
1670
- if (this.params.bootloaderBinary) targets.add('boot');
1671
- if (this.params.applicationP1Binary) targets.add('app_v1');
1672
- if (this.params.applicationP2Binary) targets.add('app_v2');
1673
- if (this.params.coprocessorBinary) targets.add('coprocessor');
1674
- if (this.params.se01Binary) targets.add('se01');
1675
- if (this.params.se02Binary) targets.add('se02');
1676
- if (this.params.se03Binary) targets.add('se03');
1677
- if (this.params.se04Binary) targets.add('se04');
1678
- return Array.from(targets);
1679
- }
1680
-
1681
1659
  private async getProtocolV2DeviceFeatures(): Promise<Features> {
1682
1660
  let { features } = this.device;
1683
1661
  if (typeof this.device.getFeatures === 'function') {
@@ -1987,13 +1965,15 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1987
1965
  ? PROTOCOL_V2_UPDATE_TARGET_BY_TARGET_ID.get(target.targetId)
1988
1966
  : undefined;
1989
1967
  if (updateTarget && targetsToUpdate.has(updateTarget)) {
1990
- if (component.version) {
1991
- this.params.expectedTargetVersions ??= {};
1992
- this.params.expectedTargetVersions[updateTarget] = component.version.join('.');
1993
- }
1994
1968
  const explicitInstallItem = explicitInstallItemByTargetId.get(target.targetId);
1995
- const installItem =
1996
- explicitInstallItem ?? (await this.downloadRemoteProtocolV2Component(key, component));
1969
+ let installItem = explicitInstallItem;
1970
+ if (!installItem) {
1971
+ installItem = await this.downloadRemoteProtocolV2Component(key, component);
1972
+ if (component.version) {
1973
+ this.params.expectedTargetVersions ??= {};
1974
+ this.params.expectedTargetVersions[updateTarget] = component.version.join('.');
1975
+ }
1976
+ }
1997
1977
  if (installItem.kind === 'bootloader') {
1998
1978
  bootloaderBinary = installItem.binary;
1999
1979
  } else {
@@ -2362,6 +2342,8 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2362
2342
  }
2363
2343
 
2364
2344
  this.postTipMessage(FirmwareUpdateTipMessage.StartTransferData);
2345
+ this.protocolV2LastTransferProgress = undefined;
2346
+ this.protocolV2LastTransferProgressAt = 0;
2365
2347
  let processedSize = 0;
2366
2348
  for (const resource of resourcesToSync) {
2367
2349
  // The bootloader keeps its live resource package mounted. FatFs rejects
@@ -2458,18 +2440,25 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2458
2440
  );
2459
2441
  }
2460
2442
  const transferredBytes = processedSize + chunkEnd;
2461
- const elapsedMs = Math.max(Date.now() - transferStartedAt, 0);
2462
- this.postProgressMessage(
2463
- Math.min(Math.ceil((transferredBytes / totalSize) * 100), 99),
2464
- 'transferData',
2465
- {
2443
+ const now = Date.now();
2444
+ const elapsedMs = Math.max(now - transferStartedAt, 0);
2445
+ const progress = Math.min(Math.ceil((transferredBytes / totalSize) * 100), 99);
2446
+ const shouldPostProgress =
2447
+ progress !== this.protocolV2LastTransferProgress ||
2448
+ now - this.protocolV2LastTransferProgressAt >=
2449
+ PROTOCOL_V2_TRANSFER_PROGRESS_HEARTBEAT_MS ||
2450
+ chunkEnd === source.size;
2451
+ if (shouldPostProgress) {
2452
+ this.protocolV2LastTransferProgress = progress;
2453
+ this.protocolV2LastTransferProgressAt = now;
2454
+ this.postProgressMessage(progress, 'transferData', {
2466
2455
  transferredBytes,
2467
2456
  totalBytes: totalSize,
2468
2457
  rateBytesPerSecond:
2469
2458
  elapsedMs > 0 ? Math.round((chunkEnd / elapsedMs) * 1000) : undefined,
2470
2459
  elapsedMs,
2471
- }
2472
- );
2460
+ });
2461
+ }
2473
2462
  return length;
2474
2463
  },
2475
2464
  });
@@ -2525,9 +2514,13 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2525
2514
  path: failedTarget.path,
2526
2515
  });
2527
2516
  Log.error(`[FirmwareUpdateV4] firmware install failed target=${failedTargetDetails}`);
2528
- throw ERRORS.TypedError(HardwareErrorCode.FirmwareError, undefined, {
2529
- firmwareUpdateCode: PROTOCOL_V2_INSTALL_FAILED_CODE,
2530
- });
2517
+ throw ERRORS.TypedError(
2518
+ HardwareErrorCode.FirmwareError,
2519
+ 'Protocol V2 firmware install failed',
2520
+ {
2521
+ firmwareUpdateCode: PROTOCOL_V2_INSTALL_FAILED_CODE,
2522
+ }
2523
+ );
2531
2524
  }
2532
2525
 
2533
2526
  const matchingTargets = statusTargets.filter(target =>
@@ -2542,9 +2535,13 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2542
2535
  (target.path && expectedPaths.get(targetId) && target.path !== expectedPaths.get(targetId))
2543
2536
  ) {
2544
2537
  Log.error(`[FirmwareUpdateV4] install status conflicts with target=${target.target_id}`);
2545
- throw ERRORS.TypedError(HardwareErrorCode.FirmwareError, undefined, {
2546
- firmwareUpdateCode: PROTOCOL_V2_INSTALL_STATUS_CONFLICT_CODE,
2547
- });
2538
+ throw ERRORS.TypedError(
2539
+ HardwareErrorCode.FirmwareError,
2540
+ 'Protocol V2 firmware install status conflict',
2541
+ {
2542
+ firmwareUpdateCode: PROTOCOL_V2_INSTALL_STATUS_CONFLICT_CODE,
2543
+ }
2544
+ );
2548
2545
  }
2549
2546
  seenTargetIds.add(targetId);
2550
2547
  }
@@ -2657,6 +2654,10 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2657
2654
  });
2658
2655
  }
2659
2656
 
2657
+ private recordProtocolV2AuthoritativeInstallCompletion(expectedTargetIds: Set<number>) {
2658
+ expectedTargetIds.forEach(targetId => this.protocolV2CompletedTargetIds.add(targetId));
2659
+ }
2660
+
2660
2661
  private async waitForProtocolV2FirmwareUpdateComplete(
2661
2662
  targets: Array<{ target_id: number; path: string }>,
2662
2663
  requireCurrentInstallStatus = false
@@ -2760,6 +2761,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2760
2761
  Log.log(
2761
2762
  '[FirmwareUpdateV4] empty firmware status after confirmed App reboot; update complete'
2762
2763
  );
2764
+ this.recordProtocolV2AuthoritativeInstallCompletion(expectedTargetIds);
2763
2765
  this.postProgressMessage(100, 'installingFirmware');
2764
2766
  return;
2765
2767
  }
@@ -2807,6 +2809,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2807
2809
  Log.log(
2808
2810
  '[FirmwareUpdateV4] firmware status endpoint unavailable after confirmed App reboot'
2809
2811
  );
2812
+ this.recordProtocolV2AuthoritativeInstallCompletion(expectedTargetIds);
2810
2813
  this.postProgressMessage(100, 'installingFirmware');
2811
2814
  return;
2812
2815
  }
@@ -2855,9 +2858,13 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2855
2858
  PROTOCOL_V2_INSTALL_TIMEOUT / 1000
2856
2859
  }s: ${this.normalizeErrorMessage(lastError)}`
2857
2860
  );
2858
- throw ERRORS.TypedError(HardwareErrorCode.FirmwareError, undefined, {
2859
- firmwareUpdateCode: PROTOCOL_V2_INSTALL_TIMEOUT_CODE,
2860
- });
2861
+ throw ERRORS.TypedError(
2862
+ HardwareErrorCode.FirmwareError,
2863
+ 'Protocol V2 firmware install timed out',
2864
+ {
2865
+ firmwareUpdateCode: PROTOCOL_V2_INSTALL_TIMEOUT_CODE,
2866
+ }
2867
+ );
2861
2868
  }
2862
2869
 
2863
2870
  private async exitProtocolV2BootloaderToNormal() {
@@ -2931,6 +2938,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2931
2938
  const startTime = Date.now();
2932
2939
  let lastError: unknown;
2933
2940
  let shouldReconnect = true;
2941
+ this.protocolV2LatestFinalDeviceInfo = undefined;
2934
2942
 
2935
2943
  while (Date.now() - startTime < timeout) {
2936
2944
  try {
@@ -2951,6 +2959,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2951
2959
  { forceRuntimeContextRefresh: true }
2952
2960
  );
2953
2961
  if (this.isProtocolV2ApplicationMode(features)) {
2962
+ this.protocolV2LatestFinalDeviceInfo = deviceInfo;
2954
2963
  return features;
2955
2964
  }
2956
2965
  lastError = ERRORS.TypedError(
@@ -449,7 +449,8 @@ export class Device extends EventEmitter {
449
449
  undefined,
450
450
  true,
451
451
  strictProtocol,
452
- undefined
452
+ undefined,
453
+ options?.forceProtocolDetection
453
454
  );
454
455
  this.mainId = (acquireResult as any)?.uuid ?? '';
455
456
  Log.debug('Expected uuid:', this.mainId);
@@ -459,7 +460,8 @@ export class Device extends EventEmitter {
459
460
  this.originalDescriptor.session,
460
461
  undefined,
461
462
  strictProtocol,
462
- undefined
463
+ undefined,
464
+ options?.forceProtocolDetection
463
465
  );
464
466
  this.mainId = acquireResult as string | undefined;
465
467
  Log.debug('Expected session id:', this.mainId);
@@ -930,13 +932,18 @@ export class Device extends EventEmitter {
930
932
  };
931
933
 
932
934
  const expectedDeviceId = options?.deviceId;
933
- if (expectedDeviceId) {
934
- // 先只读校验物理设备身份;钱包上下文仍由下方携带完整参数的
935
- // Initialize 选择,避免标准钱包请求复用此前的隐藏钱包上下文。
935
+
936
+ if (expectedDeviceId && !(this.features && this.checkDeviceId(expectedDeviceId))) {
937
+ // No locally-cached evidence that the device at this path is the
938
+ // expected one (first contact, or the cached features already disagree
939
+ // with the caller). Establish identity with a context-free Initialize
940
+ // BEFORE any wallet context (session_id / passphrase_state) goes on
941
+ // the wire. In normal flows features are always fresh — enumerate /
942
+ // getFeatures run first and every call response refreshes them — so
943
+ // this extra round trip is confined to the ambiguous cases where the
944
+ // disclosure risk actually lives.
936
945
  this.passphraseState = undefined;
937
- const { message } = await this.commands.typedCall('GetFeatures', 'Features', {});
938
- this._updateFeatures(message);
939
- await TransportManager.reconfigure(this.features);
946
+ await callInitialize({ is_contains_attach: true });
940
947
  if (!this.checkDeviceId(expectedDeviceId)) {
941
948
  throw ERRORS.TypedError(HardwareErrorCode.DeviceCheckDeviceIdError);
942
949
  }
@@ -960,7 +967,46 @@ export class Device extends EventEmitter {
960
967
  payload.derive_cardano = true;
961
968
  }
962
969
 
963
- await callInitialize(payload, options?.initSession);
970
+ if (this.features) {
971
+ // Re-sync the V1 message schema for THIS device before encoding
972
+ // Initialize: the process-global schema may still reflect another
973
+ // device (e.g. legacy-firmware Touch/Mini) on multi-device setups, and
974
+ // a stale legacy schema would silently strip passphrase_state /
975
+ // is_contains_attach from the wire message. Local operation, no wire
976
+ // I/O; a no-op when the schema is unchanged.
977
+ await TransportManager.reconfigure(this.features);
978
+ }
979
+
980
+ // Initialize's own Features response carries device_id, so the physical
981
+ // device identity is validated on the same round trip instead of via a
982
+ // separate read-only GetFeatures preflight (which doubled the wire cost
983
+ // of every deviceId-carrying call). The method fn has not run yet, so a
984
+ // mismatch still fails before any wallet data can be derived, with the
985
+ // same DeviceCheckDeviceIdError. Wallet-context selection is unchanged:
986
+ // it is decided by the Initialize payload above either way.
987
+ const assertExpectedDeviceIdentity = () => {
988
+ if (expectedDeviceId && !this.checkDeviceId(expectedDeviceId)) {
989
+ // The mismatched Initialize may have cached a session under the wrong
990
+ // device's identity; drop it so no wallet context survives from it.
991
+ // (This also evicts any session the wrong device legitimately cached
992
+ // under the same passphraseState — a deliberate conservative purge
993
+ // after a physical-swap event, consistent with
994
+ // reconcileDeviceIdentity purging the previous device's sessions.)
995
+ this.clearInternalState();
996
+ throw ERRORS.TypedError(HardwareErrorCode.DeviceCheckDeviceIdError);
997
+ }
998
+ };
999
+
1000
+ try {
1001
+ await callInitialize(payload, options?.initSession);
1002
+ } catch (error) {
1003
+ // callInitialize can fail AFTER the wire call cached the session (e.g.
1004
+ // TransportManager.reconfigure rejecting); the identity check must
1005
+ // still run so a wrong-device session never survives the error path.
1006
+ assertExpectedDeviceIdentity();
1007
+ throw error;
1008
+ }
1009
+ assertExpectedDeviceIdentity();
964
1010
  } catch (error) {
965
1011
  Log.error('Initialization failed:', error);
966
1012
  throw error;
@@ -95,7 +95,8 @@ export default class DeviceConnector {
95
95
  session?: string | null,
96
96
  forceCleanRunPromise?: boolean,
97
97
  expectedProtocol?: HardwareConnectProtocol,
98
- protocolHint?: HardwareConnectProtocol
98
+ protocolHint?: HardwareConnectProtocol,
99
+ forceProtocolDetection?: boolean
99
100
  ) {
100
101
  Log.debug('acquire', path, session, expectedProtocol, protocolHint);
101
102
  const env = DataManager.getSettings('env');
@@ -108,6 +109,7 @@ export default class DeviceConnector {
108
109
  forceCleanRunPromise,
109
110
  expectedProtocol,
110
111
  protocolHint,
112
+ forceProtocolDetection,
111
113
  });
112
114
  } else {
113
115
  res = await transport.acquire({
@@ -115,6 +117,7 @@ export default class DeviceConnector {
115
117
  previous: session ?? null,
116
118
  expectedProtocol,
117
119
  protocolHint,
120
+ forceProtocolDetection,
118
121
  });
119
122
  }
120
123
  if (expectedProtocol) {