@onekeyfe/hd-core 1.2.2-alpha.0 → 1.2.2-alpha.10

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 (38) hide show
  1. package/__tests__/AllNetworkGetAddressBase.tracing.test.ts +2 -2
  2. package/__tests__/device-lifecycle-events.test.ts +67 -0
  3. package/__tests__/deviceUploadNft.test.ts +27 -0
  4. package/__tests__/firmware-update/firmware-update-v4-install-poll.test.ts +322 -21
  5. package/__tests__/open-wallet-session.test.ts +613 -80
  6. package/__tests__/pro2HostAssetPackage.test.ts +108 -1
  7. package/__tests__/protocol-v2.test.ts +231 -49
  8. package/__tests__/protocolV2FileWrite.test.ts +40 -0
  9. package/dist/api/FirmwareUpdateV4.d.ts +2 -0
  10. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  11. package/dist/api/OpenWalletSession.d.ts.map +1 -1
  12. package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts.map +1 -1
  13. package/dist/api/helpers/protocolV2FileWrite.d.ts +1 -0
  14. package/dist/api/helpers/protocolV2FileWrite.d.ts.map +1 -1
  15. package/dist/api/protocol-v2/DeviceUploadNft.d.ts.map +1 -1
  16. package/dist/api/protocol-v2/DeviceUploadWallpaper.d.ts.map +1 -1
  17. package/dist/core/RequestQueue.d.ts +2 -0
  18. package/dist/core/RequestQueue.d.ts.map +1 -1
  19. package/dist/core/index.d.ts +2 -1
  20. package/dist/core/index.d.ts.map +1 -1
  21. package/dist/index.d.ts +12 -1
  22. package/dist/index.js +1454 -1144
  23. package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
  24. package/dist/utils/patch.d.ts +1 -1
  25. package/dist/utils/patch.d.ts.map +1 -1
  26. package/dist/utils/pro2HostAssetPackage.d.ts.map +1 -1
  27. package/package.json +4 -4
  28. package/src/api/FirmwareUpdateV4.ts +73 -23
  29. package/src/api/OpenWalletSession.ts +0 -3
  30. package/src/api/allnetwork/AllNetworkGetAddressBase.ts +3 -1
  31. package/src/api/helpers/protocolV2FileWrite.ts +11 -5
  32. package/src/api/protocol-v2/DeviceUploadNft.ts +4 -1
  33. package/src/api/protocol-v2/DeviceUploadWallpaper.ts +17 -3
  34. package/src/core/RequestQueue.ts +16 -1
  35. package/src/core/index.ts +48 -21
  36. package/src/data/messages/messages-protocol-v2.json +1471 -1367
  37. package/src/protocols/protocol-v2/walletSession.ts +157 -38
  38. package/src/utils/pro2HostAssetPackage.ts +143 -58
@@ -374,6 +374,22 @@ const isProtocolV2FirmwareStatusEndpointUnavailable = (error: unknown) => {
374
374
  );
375
375
  };
376
376
 
377
+ const isProtocolV2InstallResponseTimeout = (error: unknown) => {
378
+ if (!error || typeof error !== 'object') return false;
379
+ const { code } = error as { code?: number | string };
380
+ return (
381
+ code === 'response-timeout' ||
382
+ /\bresponse timeout\b/i.test(getProtocolV2UnknownErrorText(error))
383
+ );
384
+ };
385
+
386
+ const getProtocolV2ErrorCode = (error: unknown): number | string | undefined => {
387
+ if (!error || typeof error !== 'object') return undefined;
388
+ const { errorCode, code } = error as { errorCode?: unknown; code?: unknown };
389
+ const causeCode = errorCode ?? code;
390
+ return typeof causeCode === 'number' || typeof causeCode === 'string' ? causeCode : undefined;
391
+ };
392
+
377
393
  const isProtocolV2TerminalInstallStatusError = (error: unknown) =>
378
394
  isBleStaleBondHardwareError(error) ||
379
395
  (error instanceof HardwareError &&
@@ -565,6 +581,8 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
565
581
 
566
582
  private protocolV2InstallNeedsReconnect = false;
567
583
 
584
+ private protocolV2InstallDisconnectObserved = false;
585
+
568
586
  private protocolV2InstallTerminalSuccessObserved = false;
569
587
 
570
588
  private protocolV2LastRuntimeProbeFeatures?: Features;
@@ -573,6 +591,8 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
573
591
 
574
592
  private protocolV2LastTransferProgressAt = 0;
575
593
 
594
+ private protocolV2LastTransferredBytes = 0;
595
+
576
596
  init() {
577
597
  this.allowDeviceMode = [UI_REQUEST.BOOTLOADER, UI_REQUEST.NOT_INITIALIZE];
578
598
  this.requireDeviceMode = [];
@@ -2103,6 +2123,8 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2103
2123
  this.postTipMessage(FirmwareUpdateTipMessage.StartTransferData);
2104
2124
  this.protocolV2LastTransferProgress = undefined;
2105
2125
  this.protocolV2LastTransferProgressAt = 0;
2126
+ this.protocolV2LastTransferredBytes = 0;
2127
+ const transferStartedAt = Date.now();
2106
2128
  let processedSize = 0;
2107
2129
  for (const resource of resourcesToSync) {
2108
2130
  // The bootloader keeps its live resource package mounted. FatFs rejects
@@ -2113,6 +2135,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2113
2135
  filePath: writePath,
2114
2136
  processedSize,
2115
2137
  totalSize,
2138
+ transferStartedAt,
2116
2139
  });
2117
2140
  await this.verifyProtocolV2StagedFile(writePath, resource.source.size);
2118
2141
  if (isProtocolV2BootResourcePackagePath(resource.devicePath)) {
@@ -2128,6 +2151,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2128
2151
  filePath,
2129
2152
  processedSize,
2130
2153
  totalSize,
2154
+ transferStartedAt,
2131
2155
  });
2132
2156
  await this.verifyProtocolV2StagedFile(filePath, item.source.size);
2133
2157
  stagedInstallTargets.push({
@@ -2137,7 +2161,13 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2137
2161
  }
2138
2162
 
2139
2163
  if (totalSize > 0) {
2140
- this.postProgressMessage(100, 'transferData');
2164
+ const elapsedMs = Math.max(Date.now() - transferStartedAt, 0);
2165
+ this.postProgressMessage(100, 'transferData', {
2166
+ transferredBytes: totalSize,
2167
+ totalBytes: totalSize,
2168
+ rateBytesPerSecond: elapsedMs > 0 ? Math.round((totalSize / elapsedMs) * 1000) : undefined,
2169
+ elapsedMs,
2170
+ });
2141
2171
  }
2142
2172
  if (stagedInstallTargets.length === 0) {
2143
2173
  return;
@@ -2162,20 +2192,23 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2162
2192
  filePath,
2163
2193
  processedSize,
2164
2194
  totalSize,
2195
+ transferStartedAt = Date.now(),
2165
2196
  }: {
2166
2197
  source: FirmwareByteSource;
2167
2198
  filePath: string;
2168
2199
  processedSize: number;
2169
2200
  totalSize: number;
2201
+ transferStartedAt?: number;
2170
2202
  }) {
2171
2203
  let lastError: unknown;
2172
2204
  for (let attempt = 1; attempt <= PROTOCOL_V2_FILE_TRANSFER_RETRY_COUNT; attempt += 1) {
2205
+ this.throwIfAborted();
2173
2206
  try {
2174
- const transferStartedAt = Date.now();
2175
2207
  await writeFirmwareByteSource({
2176
2208
  source,
2177
2209
  chunkSize: this.getProtocolV2FirmwareChunkSize('write', filePath),
2178
2210
  write: async ({ data, sourceOffset, length, first }) => {
2211
+ this.throwIfAborted();
2179
2212
  const chunkEnd = sourceOffset + length;
2180
2213
  const deviceProgress = getProtocolV2DeviceTransferProgress(
2181
2214
  processedSize + sourceOffset,
@@ -2203,18 +2236,20 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2203
2236
  const elapsedMs = Math.max(now - transferStartedAt, 0);
2204
2237
  const progress = Math.min(Math.ceil((transferredBytes / totalSize) * 100), 99);
2205
2238
  const shouldPostProgress =
2206
- progress !== this.protocolV2LastTransferProgress ||
2207
- now - this.protocolV2LastTransferProgressAt >=
2208
- PROTOCOL_V2_TRANSFER_PROGRESS_HEARTBEAT_MS ||
2209
- chunkEnd === source.size;
2239
+ transferredBytes >= this.protocolV2LastTransferredBytes &&
2240
+ (progress !== this.protocolV2LastTransferProgress ||
2241
+ now - this.protocolV2LastTransferProgressAt >=
2242
+ PROTOCOL_V2_TRANSFER_PROGRESS_HEARTBEAT_MS ||
2243
+ chunkEnd === source.size);
2210
2244
  if (shouldPostProgress) {
2211
2245
  this.protocolV2LastTransferProgress = progress;
2212
2246
  this.protocolV2LastTransferProgressAt = now;
2247
+ this.protocolV2LastTransferredBytes = transferredBytes;
2213
2248
  this.postProgressMessage(progress, 'transferData', {
2214
2249
  transferredBytes,
2215
2250
  totalBytes: totalSize,
2216
2251
  rateBytesPerSecond:
2217
- elapsedMs > 0 ? Math.round((chunkEnd / elapsedMs) * 1000) : undefined,
2252
+ elapsedMs > 0 ? Math.round((transferredBytes / elapsedMs) * 1000) : undefined,
2218
2253
  elapsedMs,
2219
2254
  });
2220
2255
  }
@@ -2223,18 +2258,22 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2223
2258
  });
2224
2259
  return processedSize + source.size;
2225
2260
  } catch (error) {
2261
+ this.throwIfAborted();
2226
2262
  if (isBleStaleBondHardwareError(error)) {
2227
2263
  throw error;
2228
2264
  }
2229
2265
  lastError = error;
2230
2266
  if (attempt < PROTOCOL_V2_FILE_TRANSFER_RETRY_COUNT) {
2231
2267
  await this.recoverProtocolV2FileTransfer();
2268
+ this.throwIfAborted();
2232
2269
  }
2233
2270
  }
2234
2271
  }
2272
+ const causeCode = getProtocolV2ErrorCode(lastError);
2235
2273
  throw ERRORS.TypedError(
2236
2274
  HardwareErrorCode.EmmcFileWriteFirmwareError,
2237
- `transfer data error: ${getProtocolV2UnknownErrorText(lastError)}`
2275
+ `transfer data error: ${getProtocolV2UnknownErrorText(lastError)}`,
2276
+ causeCode === undefined ? undefined : { causeCode }
2238
2277
  );
2239
2278
  }
2240
2279
 
@@ -2517,9 +2556,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2517
2556
  ) {
2518
2557
  const expectedTargetIds = new Set(targets.map(target => target.target_id));
2519
2558
  const expectedPaths = new Map(targets.map(target => [target.target_id, target.path]));
2520
- const isBleCoprocessorInstall =
2521
- this.isBleReconnect() &&
2522
- expectedTargetIds.has(ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_COPROCESSOR);
2559
+ const isBleInstall = this.isBleReconnect();
2523
2560
  const startTime = Date.now();
2524
2561
  let lastError: unknown;
2525
2562
  // The loader may release either USB or BLE as installation starts. Recovery reconnects
@@ -2528,7 +2565,8 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2528
2565
  this.protocolV2InstallNeedsReconnect = false;
2529
2566
  let deviceInfo: ProtocolV2DeviceInfo | undefined;
2530
2567
  let bleInstallLinkReady = false;
2531
- let installStatusDisconnectObserved = false;
2568
+ let installStatusDisconnectObserved = isBleInstall && this.protocolV2InstallDisconnectObserved;
2569
+ this.protocolV2InstallDisconnectObserved = false;
2532
2570
  let installReconnectObserved = false;
2533
2571
  let finishedStatusSnapshotKey: string | undefined;
2534
2572
  let finishedStatusSnapshotPolls = 0;
@@ -2536,6 +2574,19 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2536
2574
  let installEvidenceObserved = this.protocolV2InstallTerminalSuccessObserved;
2537
2575
  let currentInstallStatusObserved = false;
2538
2576
  const liveTargetIds = new Set<number>();
2577
+ const recordBleInstallDisconnect = (error: unknown) => {
2578
+ if (
2579
+ !isBleInstall ||
2580
+ !isProtocolV2DeviceDisconnectedError(error) ||
2581
+ isProtocolV2InstallResponseTimeout(error)
2582
+ ) {
2583
+ return;
2584
+ }
2585
+ installStatusDisconnectObserved = true;
2586
+ installReconnectObserved = false;
2587
+ finishedStatusSnapshotKey = undefined;
2588
+ finishedStatusSnapshotPolls = 0;
2589
+ };
2539
2590
 
2540
2591
  while (Date.now() - startTime < PROTOCOL_V2_INSTALL_TIMEOUT) {
2541
2592
  // A transport release caused by an explicit workflow cancellation must not
@@ -2682,7 +2733,9 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2682
2733
  if (!hasCurrentInstallEvidence) {
2683
2734
  // A successful status poll clears DeviceCommands' cancel action. Keep cancellation
2684
2735
  // available while UpdateRequest is still waiting for confirmation on the device.
2685
- this.device.setCancelableAction(() => this.device.getCommands().cancelDevice());
2736
+ // Bootloader does not register the protocol Cancel message. Keep the
2737
+ // operation locally cancellable; interruptionFromUser() disposes the link.
2738
+ this.device.setCancelableAction(() => Promise.resolve());
2686
2739
  }
2687
2740
 
2688
2741
  if (
@@ -2787,15 +2840,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2787
2840
  );
2788
2841
  }
2789
2842
  } else {
2790
- if (
2791
- isBleCoprocessorInstall &&
2792
- isProtocolV2DeviceDisconnectedError(error) &&
2793
- !isProtocolV2ResponseTimeout(error)
2794
- ) {
2795
- installStatusDisconnectObserved = true;
2796
- finishedStatusSnapshotKey = undefined;
2797
- finishedStatusSnapshotPolls = 0;
2798
- }
2843
+ recordBleInstallDisconnect(error);
2799
2844
  shouldReconnect = true;
2800
2845
  deviceInfo = undefined;
2801
2846
  bleInstallLinkReady = false;
@@ -2820,6 +2865,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2820
2865
  ) {
2821
2866
  throw error;
2822
2867
  }
2868
+ recordBleInstallDisconnect(error);
2823
2869
  shouldReconnect = true;
2824
2870
  deviceInfo = undefined;
2825
2871
  bleInstallLinkReady = false;
@@ -3154,10 +3200,13 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
3154
3200
  }) {
3155
3201
  this.protocolV2LastRuntimeProbeFeatures = undefined;
3156
3202
  this.protocolV2InstallNeedsReconnect = false;
3203
+ this.protocolV2InstallDisconnectObserved = false;
3157
3204
  this.protocolV2InstallTerminalSuccessObserved = false;
3158
3205
  const commands = this.device.getCommands();
3159
3206
  await commands.typedCall('DeviceFirmwareUpdateStage', 'Success', { targets });
3160
- this.device.setCancelableAction(() => this.device.getCommands().cancelDevice());
3207
+ // Bootloader does not register the protocol Cancel message. Keep the
3208
+ // operation locally cancellable; interruptionFromUser() disposes the link.
3209
+ this.device.setCancelableAction(() => Promise.resolve());
3161
3210
  const interaction = this.device.createProtocolV2UiPhaseMetadata('button', 'start');
3162
3211
  this.postMessage(
3163
3212
  createUiMessage(UI_REQUEST.REQUEST_BUTTON, {
@@ -3196,6 +3245,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
3196
3245
  '[FirmwareUpdateV4] transport released while writing install request; continue status polling'
3197
3246
  );
3198
3247
  this.protocolV2InstallNeedsReconnect = true;
3248
+ this.protocolV2InstallDisconnectObserved = !isProtocolV2InstallResponseTimeout(error);
3199
3249
  }
3200
3250
  }
3201
3251
 
@@ -160,7 +160,6 @@ export default class OpenWalletSession extends BaseMethod<OpenWalletSessionParam
160
160
  const session = isProtocolV2
161
161
  ? await getProtocolV2WalletSession(this.device, {
162
162
  onlyMainPin: true,
163
- deriveCardano: this.payload.deriveCardano,
164
163
  selectMainWalletBeforeRestore:
165
164
  !hasAuthoritativeProtocolV2WalletStatus(state) ||
166
165
  state.status.unlockedAttachPin === true,
@@ -225,7 +224,6 @@ export default class OpenWalletSession extends BaseMethod<OpenWalletSessionParam
225
224
  const session = isProtocolV2
226
225
  ? await getProtocolV2WalletSession(this.device, {
227
226
  expectedPassphraseState: this.params.passphraseState,
228
- deriveCardano: this.payload.deriveCardano,
229
227
  })
230
228
  : await getPassphraseStateWithRefreshDeviceInfo(this.device, {
231
229
  expectPassphraseState: this.params.passphraseState,
@@ -257,7 +255,6 @@ export default class OpenWalletSession extends BaseMethod<OpenWalletSessionParam
257
255
  ...(readCurrentAttachPinSession
258
256
  ? { readCurrentAttachPinSession: true }
259
257
  : { forceWalletSelection: true }),
260
- deriveCardano: this.payload.deriveCardano,
261
258
  })
262
259
  : await getPassphraseStateWithRefreshDeviceInfo(this.device, { initSession: true });
263
260
  const refreshedState = isProtocolV2
@@ -393,7 +393,9 @@ export default abstract class AllNetworkGetAddressBase extends BaseMethod<
393
393
  // the root fingerprint, so each nested chain method must resume the
394
394
  // requested standard or hidden wallet before sending its device command.
395
395
  const useEmptyPassphrase = this.payload.useEmptyPassphrase === true;
396
- const deriveCardano = method.name.startsWith('cardano');
396
+ // Nested Cardano methods opt in to [Standard, Cardano] if Ask rebuilds.
397
+ // Other chains stay Standard-only.
398
+ const deriveCardano = method.name.startsWith('cardano') ? true : undefined;
397
399
  const shouldResumeWalletSession = useEmptyPassphrase || !!this.payload.passphraseState;
398
400
  if (this.device.isProtocolV2() && shouldResumeWalletSession) {
399
401
  const passphraseStateSafety = await this.device.checkPassphraseStateSafety(
@@ -36,6 +36,8 @@ export type ProtocolV2FileWriteOptions = {
36
36
  chunkSize?: number;
37
37
  chunkLen?: number;
38
38
  chunkSizeLimit?: number;
39
+ /** BLE-only limit for a caller whose fixed short path has a verified larger frame budget. */
40
+ bleChunkSizeLimit?: number;
39
41
  overwrite?: boolean;
40
42
  append?: boolean;
41
43
  uiPercentage?: number;
@@ -114,11 +116,15 @@ export function isProtocolV2ResponseTimeout(error: unknown) {
114
116
  );
115
117
  }
116
118
 
117
- function getProtocolV2FileChunkLimit() {
119
+ function getProtocolV2FileChunkLimit(bleChunkSizeLimit?: number) {
118
120
  const env = DataManager.getSettings('env');
119
- return env && DataManager.isBleConnect(env)
120
- ? PROTOCOL_V2_BLE_FILE_CHUNK_SIZE
121
- : PROTOCOL_V2_WEBUSB_FILE_CHUNK_SIZE;
121
+ if (env && DataManager.isBleConnect(env)) {
122
+ const configuredLimit = Number(bleChunkSizeLimit);
123
+ return Number.isFinite(configuredLimit) && configuredLimit > 0
124
+ ? Math.floor(configuredLimit)
125
+ : PROTOCOL_V2_BLE_FILE_CHUNK_SIZE;
126
+ }
127
+ return PROTOCOL_V2_WEBUSB_FILE_CHUNK_SIZE;
122
128
  }
123
129
 
124
130
  async function dataToUint8Array(data: ProtocolV2FileWriteData): Promise<Uint8Array> {
@@ -178,7 +184,7 @@ export async function writeProtocolV2File(options: ProtocolV2FileWriteOptions) {
178
184
  );
179
185
  }
180
186
 
181
- const defaultChunkSizeLimit = getProtocolV2FileChunkLimit();
187
+ const defaultChunkSizeLimit = getProtocolV2FileChunkLimit(options.bleChunkSizeLimit);
182
188
  const configuredChunkSizeLimit = Number(options.chunkSizeLimit);
183
189
  const chunkSizeLimit =
184
190
  Number.isFinite(configuredChunkSizeLimit) && configuredChunkSizeLimit > 0
@@ -42,8 +42,11 @@ export type DeviceUploadNftParams = {
42
42
 
43
43
  export type DeviceUploadNftResponse = {
44
44
  basename: string;
45
+ /** Final image path after firmware extracts a host-asset package. */
45
46
  imagePath: string;
47
+ /** Final thumbnail path after firmware extracts a host-asset package. */
46
48
  thumbnailPath: string;
49
+ /** Final metadata path after firmware extracts a host-asset package. */
47
50
  metadataPath: string;
48
51
  totalSize: number;
49
52
  nftUpdated: true;
@@ -197,7 +200,7 @@ export default class DeviceUploadNft extends BaseMethod<DeviceUploadNftParams> {
197
200
  if (!bundle) throw invalidParameter('NFT data has not been initialized.');
198
201
 
199
202
  const useHostAssetPackage = supportsPro2HostAssetPackage(
200
- this.device.getCurrentFirmwareVersionString()
203
+ this.device.state?.versions.firmware ?? undefined
201
204
  );
202
205
  await this.assertCapabilities(useHostAssetPackage);
203
206
  this.throwIfAborted();
@@ -23,11 +23,19 @@ import {
23
23
 
24
24
  export type DeviceUploadWallpaperParams = {
25
25
  jpegBase64: string;
26
+ /**
27
+ * Legacy firmware uses this name for the uploaded `.bin` file. Firmware
28
+ * 1.0.1+ always consumes the fixed `wallpaper.okpkg` package path.
29
+ */
26
30
  fileName?: string;
27
31
  chunkSize?: number;
28
32
  };
29
33
 
30
34
  export type DeviceUploadWallpaperResponse = {
35
+ /**
36
+ * Filesystem path sent to `DeviceSettingsSet`. On firmware 1.0.1+ this is
37
+ * the temporary package path; firmware extracts and persists wallpaper.bin.
38
+ */
31
39
  path: string;
32
40
  size: number;
33
41
  colorFormat: Pro2WallpaperColorFormat;
@@ -41,6 +49,7 @@ const SAFE_FILE_NAME = /^[A-Za-z0-9_-]+(?:\.bin)?$/;
41
49
  const DEVICE_SETTINGS_SET_MESSAGE_TYPE = 60412;
42
50
  const FILESYSTEM_FILE_WRITE_MESSAGE_TYPE = 60805;
43
51
  const FILESYSTEM_DIR_MAKE_MESSAGE_TYPE = 60809;
52
+ const WALLPAPER_PACKAGE_BLE_CHUNK_SIZE = 1960;
44
53
  const Log = getLogger(LoggerNames.Method);
45
54
 
46
55
  function normalizeFileName(fileName: string | undefined, data: Uint8Array): string {
@@ -122,7 +131,7 @@ export default class DeviceUploadWallpaper extends BaseMethod<DeviceUploadWallpa
122
131
  this.directoryReady = true;
123
132
  }
124
133
 
125
- private async upload(path: string, data: Uint8Array) {
134
+ private async upload(path: string, data: Uint8Array, bleChunkSizeLimit?: number) {
126
135
  if (this.uploaded) return;
127
136
 
128
137
  await writeProtocolV2File({
@@ -131,6 +140,7 @@ export default class DeviceUploadWallpaper extends BaseMethod<DeviceUploadWallpa
131
140
  data,
132
141
  totalSize: data.byteLength,
133
142
  chunkSize: this.params.chunkSize,
143
+ bleChunkSizeLimit,
134
144
  maxChunkRetries: 3,
135
145
  overwrite: true,
136
146
  append: false,
@@ -150,13 +160,17 @@ export default class DeviceUploadWallpaper extends BaseMethod<DeviceUploadWallpa
150
160
  await this.assertCapabilities();
151
161
  await this.ensureDirectory();
152
162
  const useHostAssetPackage = supportsPro2HostAssetPackage(
153
- this.device.getCurrentFirmwareVersionString()
163
+ this.device.state?.versions.firmware ?? undefined
154
164
  );
155
165
  const data = useHostAssetPackage
156
166
  ? buildPro2HostAssetPackage([{ name: WALLPAPER_PACKAGE_ENTRY, data: encoded.data }])
157
167
  : encoded.data;
158
168
  if (useHostAssetPackage) this.path = WALLPAPER_PACKAGE_PATH;
159
- await this.upload(this.path, data);
169
+ await this.upload(
170
+ this.path,
171
+ data,
172
+ useHostAssetPackage ? WALLPAPER_PACKAGE_BLE_CHUNK_SIZE : undefined
173
+ );
160
174
  const response = await this.device.commands.typedCall('DeviceSettingsSet', 'Success', {
161
175
  settings: { wallpaper_path: this.path },
162
176
  });
@@ -56,11 +56,20 @@ export default class RequestQueue {
56
56
  return false;
57
57
  }
58
58
 
59
+ private isRequestForConnectId(request: RequestTask, connectId: string) {
60
+ const { method } = request;
61
+ return (
62
+ method.connectId === connectId ||
63
+ method.device?.mainId === connectId ||
64
+ method.device?.getConnectId() === connectId
65
+ );
66
+ }
67
+
59
68
  // 取消与指定connectId相关的所有请求
60
69
  public abortRequestsByConnectId(connectId: string) {
61
70
  let count = 0;
62
71
  this.requestQueue.forEach((request, _) => {
63
- if (request.abortController && request.method.connectId === connectId) {
72
+ if (request.abortController && this.isRequestForConnectId(request, connectId)) {
64
73
  request.abortController.abort();
65
74
  request.abortController = undefined;
66
75
  count++;
@@ -69,6 +78,12 @@ export default class RequestQueue {
69
78
  return count;
70
79
  }
71
80
 
81
+ public getRequestTasksIdByConnectId(connectId: string) {
82
+ return Array.from(this.requestQueue.values())
83
+ .filter(request => this.isRequestForConnectId(request, connectId))
84
+ .map(request => request.id);
85
+ }
86
+
72
87
  // 取消所有请求
73
88
  public abortAllRequests() {
74
89
  let count = 0;
package/src/core/index.ts CHANGED
@@ -2,7 +2,11 @@ import semver from 'semver';
2
2
  import EventEmitter from 'events';
3
3
  import {
4
4
  DeviceSessionPinType,
5
+ type LowlevelTransportSharedPlugin,
6
+ type OneKeyDeviceInfo,
7
+ type ProtocolType,
5
8
  TRANSPORT_EVENT,
9
+ type TransportDeviceDisconnectEvent,
6
10
  isProtocolV2LinkDisabledError,
7
11
  } from '@onekeyfe/hd-transport';
8
12
  import {
@@ -74,11 +78,6 @@ import type { CoreMessage, IFrameCallMessage, UiPromise, UiPromiseResponse } fro
74
78
  import type { DeviceEvents, InitOptions, RunOptions } from '../device/Device';
75
79
  import type { SdkTracingContext } from '../utils/tracing';
76
80
  import type { Deferred } from '@onekeyfe/hd-shared';
77
- import type {
78
- LowlevelTransportSharedPlugin,
79
- OneKeyDeviceInfo,
80
- TransportDeviceDisconnectEvent,
81
- } from '@onekeyfe/hd-transport';
82
81
  import type { BaseMethod } from '../api/BaseMethod';
83
82
 
84
83
  const Log = getLogger(LoggerNames.Core);
@@ -92,7 +91,7 @@ const preWarmDoneAt = new Map<string, number>();
92
91
 
93
92
  export type CoreContext = ReturnType<Core['getCoreContext']>;
94
93
 
95
- function hasDeriveCardano(method: BaseMethod): boolean {
94
+ function resolveDeriveCardano(method: BaseMethod): boolean | undefined {
96
95
  if (
97
96
  method.name.startsWith('allNetworkGetAddress') &&
98
97
  method.payload &&
@@ -102,15 +101,22 @@ function hasDeriveCardano(method: BaseMethod): boolean {
102
101
  ) {
103
102
  return true;
104
103
  }
105
-
106
- return method.name.startsWith('cardano') || method.payload?.deriveCardano;
104
+ if (method.name.startsWith('cardano')) {
105
+ return true;
106
+ }
107
+ // V1 Initialize only sends derive_cardano on an explicit true.
108
+ // V2 AskPassphrase maps true to [Standard, Cardano] and anything else to [Standard].
109
+ if (method.payload?.deriveCardano === true) {
110
+ return true;
111
+ }
112
+ return undefined;
107
113
  }
108
114
 
109
115
  const parseInitOptions = (method?: BaseMethod): InitOptions => ({
110
116
  initSession: method?.payload.initSession,
111
117
  passphraseState: method?.payload.useEmptyPassphrase ? undefined : method?.payload.passphraseState,
112
118
  deviceId: method?.payload.deviceId,
113
- deriveCardano: method && hasDeriveCardano(method),
119
+ deriveCardano: method ? resolveDeriveCardano(method) : undefined,
114
120
  connectProtocol: method?.payload.connectProtocol,
115
121
  forceProtocolDetection: method?.payload.forceProtocolDetection,
116
122
  protocolV2DeviceInfoTimeoutMs: method?.payload.protocolV2DeviceInfoTimeoutMs,
@@ -651,7 +657,7 @@ const onCallDevice = async (
651
657
  method.payload?.passphraseState,
652
658
  method.payload?.useEmptyPassphrase,
653
659
  method.payload?.skipPassphraseCheck,
654
- hasDeriveCardano(method),
660
+ resolveDeriveCardano(method),
655
661
  method.protocolV2UnlockContext?.preflightMainPinSelected
656
662
  );
657
663
 
@@ -1037,6 +1043,7 @@ async function connectDeviceForBle(
1037
1043
  !device.commands ||
1038
1044
  device.commands.disposed;
1039
1045
  if (shouldAcquire) {
1046
+ const connectProtocol = resolveBleConnectProtocol(method);
1040
1047
  // The deadline/abort guards are scoped to the desktop electron
1041
1048
  // transport: its IPC acquire is the only path with a proven
1042
1049
  // never-settling failure mode, while react-native/lowlevel acquire may
@@ -1048,13 +1055,13 @@ async function connectDeviceForBle(
1048
1055
  throw ERRORS.TypedError(HardwareErrorCode.CallQueueActionCancelled);
1049
1056
  }
1050
1057
  if (!useAcquireGuards) {
1051
- await device.acquire(method.payload.connectProtocol, {
1058
+ await device.acquire(connectProtocol, {
1052
1059
  forceProtocolDetection: method.payload.forceProtocolDetection,
1053
1060
  });
1054
1061
  } else {
1055
1062
  try {
1056
1063
  await raceBleAcquire(
1057
- device.acquire(method.payload.connectProtocol, {
1064
+ device.acquire(connectProtocol, {
1058
1065
  forceProtocolDetection: method.payload.forceProtocolDetection,
1059
1066
  }),
1060
1067
  abortSignal
@@ -1120,6 +1127,14 @@ async function connectDeviceForBle(
1120
1127
  }
1121
1128
  }
1122
1129
 
1130
+ export function resolveBleConnectProtocol(method: BaseMethod): ProtocolType | undefined {
1131
+ if (method.payload.connectProtocol === 'V1' || method.payload.connectProtocol === 'V2') {
1132
+ return method.payload.connectProtocol;
1133
+ }
1134
+ const supportedProtocols = method.getSupportedProtocols();
1135
+ return supportedProtocols.length === 1 && supportedProtocols[0] === 'V2' ? 'V2' : undefined;
1136
+ }
1137
+
1123
1138
  type IPollFn<T> = (time?: number) => T;
1124
1139
  // eslint-disable-next-line @typescript-eslint/require-await
1125
1140
  const ensureConnected = async (
@@ -1318,7 +1333,7 @@ export const cancel = (context: CoreContext, connectId?: string) => {
1318
1333
  // cancel callback tasks
1319
1334
  requestQueue.cancelCallbackTasks(connectId);
1320
1335
 
1321
- const requestIds = requestQueue.getRequestTasksId();
1336
+ const requestIds = requestQueue.getRequestTasksIdByConnectId(connectId);
1322
1337
  Log.debug(
1323
1338
  `Cancel Api connect requestQueues: length:${requestIds.length} requestIds:${requestIds.join(
1324
1339
  ','
@@ -1326,6 +1341,8 @@ export const cancel = (context: CoreContext, connectId?: string) => {
1326
1341
  );
1327
1342
  // Abort before rejecting: rejectRequest releases the task and would make
1328
1343
  // its AbortController unreachable to an in-flight method loop.
1344
+ // Match both the requested connectId and a device selected internally by the
1345
+ // method, such as Desktop WebUSB firmwareUpdateV4.
1329
1346
  requestQueue.abortRequestsByConnectId(connectId);
1330
1347
  const canceledDevices: Device[] = [];
1331
1348
  const interruptDevice = (device: Device | undefined, deviceConnectId: string) => {
@@ -1342,7 +1359,7 @@ export const cancel = (context: CoreContext, connectId?: string) => {
1342
1359
  // During ensureConnected the method has a connectId but device is
1343
1360
  // assigned only after the poll succeeds. Interrupt the cached BLE
1344
1361
  // Device so an in-flight acquire/initialize cannot finish.
1345
- interruptDevice(task.method?.device, task.method.connectId ?? connectId);
1362
+ interruptDevice(task.method?.device, connectId);
1346
1363
  interruptDevice(deviceCacheMap.get(connectId), connectId);
1347
1364
  requestQueue.rejectRequest(
1348
1365
  requestId,
@@ -1357,10 +1374,11 @@ export const cancel = (context: CoreContext, connectId?: string) => {
1357
1374
  }
1358
1375
  } else {
1359
1376
  const env = DataManager.getSettings('env');
1377
+ // Abort every method before rejecting its queue task. Non-BLE methods also
1378
+ // use the signal to stop recovery loops after the public promise is rejected.
1379
+ requestQueue.abortAllRequests();
1360
1380
  if (DataManager.isBleConnect(env)) {
1361
1381
  Log.debug('Cancel Api all _deviceList: ');
1362
- // Keep method abort signals observable until every active task is rejected.
1363
- requestQueue.abortAllRequests();
1364
1382
  const canceledDevices: Device[] = [];
1365
1383
  const interruptDevice = (device?: Device) => {
1366
1384
  if (!device || canceledDevices.includes(device)) {
@@ -1403,8 +1421,10 @@ export const cancel = (context: CoreContext, connectId?: string) => {
1403
1421
  }
1404
1422
  }
1405
1423
 
1406
- cleanup();
1407
- closePopup();
1424
+ cleanup(connectId);
1425
+ if (!connectId || _uiPromises.length === 0) {
1426
+ closePopup();
1427
+ }
1408
1428
  };
1409
1429
 
1410
1430
  const checkPassphraseEnableState = (method: BaseMethod, features?: Features) => {
@@ -1442,9 +1462,16 @@ const shouldCheckPassphraseState = (method: BaseMethod, device: Device) => {
1442
1462
  return device.hasUsePassphrase();
1443
1463
  };
1444
1464
 
1445
- const cleanup = () => {
1446
- const pendingUiPromises = _uiPromises;
1447
- _uiPromises = [];
1465
+ const cleanup = (connectId?: string) => {
1466
+ const pendingUiPromises = connectId
1467
+ ? _uiPromises.filter(
1468
+ uiPromise =>
1469
+ uiPromise.data?.mainId === connectId || uiPromise.data?.getConnectId() === connectId
1470
+ )
1471
+ : _uiPromises;
1472
+ _uiPromises = connectId
1473
+ ? _uiPromises.filter(uiPromise => !pendingUiPromises.includes(uiPromise))
1474
+ : [];
1448
1475
  rejectUiPromises(
1449
1476
  pendingUiPromises,
1450
1477
  ERRORS.TypedError(HardwareErrorCode.ActionCancelled, 'UI request was cancelled')