@onekeyfe/hd-core 1.2.2-alpha.8 → 1.2.2

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 (81) hide show
  1. package/README.md +1 -1
  2. package/__tests__/AllNetworkGetAddressBase.tracing.test.ts +503 -10
  3. package/__tests__/DeviceCommands.test.ts +254 -1
  4. package/__tests__/core-error-output.test.ts +169 -1
  5. package/__tests__/device-lifecycle-events.test.ts +432 -15
  6. package/__tests__/logBlockEvent.test.ts +45 -122
  7. package/__tests__/open-wallet-session-error-response.test.ts +2 -2
  8. package/__tests__/open-wallet-session.test.ts +94 -363
  9. package/__tests__/protocol-v2.test.ts +132 -2
  10. package/__tests__/public-device-state-api.test.ts +2 -7
  11. package/__tests__/search-devices.test.ts +196 -8
  12. package/__tests__/sol-sign-offchain-message.test.ts +64 -0
  13. package/dist/api/FirmwareUpdateV4.d.ts +1 -0
  14. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  15. package/dist/api/GetFeatures.d.ts.map +1 -1
  16. package/dist/api/GetPassphraseState.d.ts.map +1 -1
  17. package/dist/api/OpenWalletSession.d.ts.map +1 -1
  18. package/dist/api/SearchDevices.d.ts +2 -15
  19. package/dist/api/SearchDevices.d.ts.map +1 -1
  20. package/dist/api/UploadPortfolio.d.ts +1 -0
  21. package/dist/api/UploadPortfolio.d.ts.map +1 -1
  22. package/dist/api/allnetwork/AllNetworkGetAddress.d.ts +2 -0
  23. package/dist/api/allnetwork/AllNetworkGetAddress.d.ts.map +1 -1
  24. package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts +7 -1
  25. package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts.map +1 -1
  26. package/dist/api/allnetwork/AllNetworkGetAddressByLoop.d.ts.map +1 -1
  27. package/dist/api/device/DeviceVerify.d.ts.map +1 -1
  28. package/dist/api/solana/SolSignOffchainMessage.d.ts.map +1 -1
  29. package/dist/core/RequestQueue.d.ts +3 -0
  30. package/dist/core/RequestQueue.d.ts.map +1 -1
  31. package/dist/core/index.d.ts +3 -1
  32. package/dist/core/index.d.ts.map +1 -1
  33. package/dist/core/uiPromiseRegistry.d.ts +1 -1
  34. package/dist/data-manager/TransportManager.d.ts +2 -0
  35. package/dist/data-manager/TransportManager.d.ts.map +1 -1
  36. package/dist/device/Device.d.ts +2 -0
  37. package/dist/device/Device.d.ts.map +1 -1
  38. package/dist/device/DeviceCommands.d.ts +5 -4
  39. package/dist/device/DeviceCommands.d.ts.map +1 -1
  40. package/dist/events/logBlockEvent.d.ts.map +1 -1
  41. package/dist/index.d.ts +35 -30
  42. package/dist/index.js +620 -325
  43. package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
  44. package/dist/types/api/getFeatures.d.ts.map +1 -1
  45. package/dist/types/api/getPassphraseState.d.ts.map +1 -1
  46. package/dist/types/api/openWalletSession.d.ts +1 -10
  47. package/dist/types/api/openWalletSession.d.ts.map +1 -1
  48. package/dist/types/api/protocolV2.d.ts +3 -4
  49. package/dist/types/api/protocolV2.d.ts.map +1 -1
  50. package/dist/types/api/solSignOffchainMessage.d.ts +1 -0
  51. package/dist/types/api/solSignOffchainMessage.d.ts.map +1 -1
  52. package/dist/types/params.d.ts.map +1 -1
  53. package/dist/utils/patch.d.ts +1 -1
  54. package/dist/utils/patch.d.ts.map +1 -1
  55. package/package.json +4 -4
  56. package/src/api/FirmwareUpdateV4.ts +9 -4
  57. package/src/api/GetFeatures.ts +1 -0
  58. package/src/api/GetPassphraseState.ts +1 -0
  59. package/src/api/OpenWalletSession.ts +7 -77
  60. package/src/api/SearchDevices.ts +121 -27
  61. package/src/api/UploadPortfolio.ts +5 -4
  62. package/src/api/allnetwork/AllNetworkGetAddress.ts +79 -45
  63. package/src/api/allnetwork/AllNetworkGetAddressBase.ts +95 -24
  64. package/src/api/allnetwork/AllNetworkGetAddressByLoop.ts +3 -0
  65. package/src/api/device/DeviceVerify.ts +8 -0
  66. package/src/api/solana/SolSignOffchainMessage.ts +44 -4
  67. package/src/core/RequestQueue.ts +36 -1
  68. package/src/core/index.ts +178 -63
  69. package/src/data/messages/messages-protocol-v2.json +49 -33
  70. package/src/data/messages/messages.json +8 -5
  71. package/src/data-manager/TransportManager.ts +14 -3
  72. package/src/device/Device.ts +58 -27
  73. package/src/device/DeviceCommands.ts +29 -2
  74. package/src/events/logBlockEvent.ts +6 -75
  75. package/src/protocols/protocol-v2/walletSession.ts +18 -2
  76. package/src/types/api/getFeatures.ts +2 -1
  77. package/src/types/api/getPassphraseState.ts +2 -5
  78. package/src/types/api/openWalletSession.ts +7 -19
  79. package/src/types/api/protocolV2.ts +3 -4
  80. package/src/types/api/solSignOffchainMessage.ts +2 -0
  81. package/src/types/params.ts +7 -0
package/src/core/index.ts CHANGED
@@ -10,6 +10,7 @@ import {
10
10
  isProtocolV2LinkDisabledError,
11
11
  } from '@onekeyfe/hd-transport';
12
12
  import {
13
+ EDeviceType,
13
14
  ERRORS,
14
15
  ERROR_CODES_REQUIRE_DISCONNECT,
15
16
  ERROR_CODES_REQUIRE_RELEASE,
@@ -22,6 +23,7 @@ import {
22
23
  createNeedUpgradeFirmwareHardwareError,
23
24
  createNewFirmwareForceUpdateHardwareError,
24
25
  createNewFirmwareUnReleaseHardwareError,
26
+ isBleStaleBondHardwareError,
25
27
  } from '@onekeyfe/hd-shared';
26
28
 
27
29
  import { LoggerNames, enableLog, getLogger, setLoggerPostMessage, wait } from '../utils';
@@ -82,7 +84,8 @@ import type { BaseMethod } from '../api/BaseMethod';
82
84
 
83
85
  const Log = getLogger(LoggerNames.Core);
84
86
  const PRE_INITIALIZE_TTL_MS = 60 * 1000;
85
- const PRE_PENDING_CALL_TIMEOUT_MS = 15 * 1000;
87
+ const PRE_PENDING_CALL_TIMEOUT_MS = 5 * 1000;
88
+ const PRO2_USB_SIGNING_COOLDOWN_MS = 1000;
86
89
 
87
90
  // Dedup/coalesce state for "pre-warm signal" methods (isPreWarmSignal),
88
91
  // keyed by getPreWarmKey(): coalesce in-flight, skip if warmed within TTL.
@@ -180,6 +183,7 @@ export const callAPI = async (context: CoreContext, message: CoreMessage) => {
180
183
  }
181
184
  };
182
185
  method.setContext?.(context);
186
+ method.context = context;
183
187
 
184
188
  method.requestContext = createRequestContext(method.responseID, method.name, {
185
189
  sdkInstanceId: context.sdkInstanceId,
@@ -200,7 +204,12 @@ export const callAPI = async (context: CoreContext, message: CoreMessage) => {
200
204
  if (!method.useDevice) {
201
205
  updateMethodRequestContext(method, { status: 'running' });
202
206
  try {
203
- const response = await method.run();
207
+ const env = DataManager.getSettings('env');
208
+ const response =
209
+ method.name === 'searchDevices' &&
210
+ (DataManager.isBrowserWebUsb(env) || DataManager.isDesktopWebUsb(env))
211
+ ? await context.methodSynchronize(() => method.run(), 'webusb-discovery')
212
+ : await method.run();
204
213
  completeMethodRequestContext(method);
205
214
  return createResponseMessage(method.responseID, true, response);
206
215
  } catch (error) {
@@ -313,6 +322,7 @@ const waitForPendingPromise = async (
313
322
  Log.debug('pre pending call promise before call method, wait for it');
314
323
  let timer: ReturnType<typeof setTimeout> | undefined;
315
324
  let timedOut = false;
325
+ let completed = false;
316
326
  try {
317
327
  await Promise.race([
318
328
  pendingPromise,
@@ -323,11 +333,12 @@ const waitForPendingPromise = async (
323
333
  }, PRE_PENDING_CALL_TIMEOUT_MS);
324
334
  }),
325
335
  ]);
326
- } catch (error) {
327
- // Cancellation is best-effort; the transport teardown owns recovery.
336
+ completed = !timedOut;
328
337
  } finally {
329
338
  if (timer) clearTimeout(timer);
330
- removePrePendingCallPromise?.(connectId, pendingPromise);
339
+ // Keep a rejected cleanup barrier for the next caller's safety check;
340
+ // only a completed cleanup or a timeout may clear it.
341
+ if (timedOut || completed) removePrePendingCallPromise?.(connectId, pendingPromise);
331
342
  }
332
343
  if (timedOut) {
333
344
  Log.warn('pre pending call promise timed out before call method', { connectId });
@@ -336,6 +347,20 @@ const waitForPendingPromise = async (
336
347
  }
337
348
  };
338
349
 
350
+ export function getPostCallPendingPromise(method: BaseMethod, device: Device): Promise<void> {
351
+ const cleanupPromise = device.waitForRunCleanup();
352
+ const env = DataManager.getSettings('env');
353
+ const deviceType = device.getCurrentDeviceType();
354
+ const requiresSigningCooldown =
355
+ method.name.includes('Sign') &&
356
+ (deviceType === EDeviceType.Pro2 || deviceType === EDeviceType.Neo) &&
357
+ (DataManager.isBrowserWebUsb(env) || DataManager.isDesktopWebUsb(env) || env === 'node-usb');
358
+
359
+ return requiresSigningCooldown
360
+ ? cleanupPromise.then(() => wait(PRO2_USB_SIGNING_COOLDOWN_MS)).then(() => undefined)
361
+ : cleanupPromise;
362
+ }
363
+
339
364
  const onCallDevice = async (
340
365
  context: CoreContext,
341
366
  message: CoreMessage,
@@ -366,42 +391,57 @@ const onCallDevice = async (
366
391
  DevicePool.clearDeviceCache(method.payload.connectId);
367
392
  }
368
393
 
369
- // wait for previous callback tasks to complete (ensure device does not call concurrently)
370
- if (method.connectId) {
371
- await context.waitForCallbackTasks(method.connectId);
372
- }
373
-
374
- await waitForPendingPromise(
375
- method.connectId ?? '',
376
- getPrePendingCallPromise,
377
- removePrePendingCallPromise
378
- );
379
-
394
+ // Register before waiting so cancellation also covers queued requests.
380
395
  const task = requestQueue.createTask(method);
381
396
 
382
397
  // Pre-warm holds the device as a per-connectId callback task so a concurrent
383
398
  // real call waits (before ensureConnected) instead of racing its Initialize.
384
399
  // Only covers pre-warm -> real-call ordering; the reverse is fail-closed.
385
400
  let preWarmCallbackTask: Deferred<void> | undefined;
386
- if (method.isPreWarmSignal && method.connectId) {
387
- preWarmCallbackTask = createDeferred<void>();
388
- context.registerCallbackTask(method.connectId, preWarmCallbackTask);
389
- }
390
-
391
401
  let device: Device;
392
402
  try {
403
+ const connectId = method.connectId ?? '';
404
+ if (connectId) {
405
+ await requestQueue.waitForTask(task, () => context.waitForCallbackTasks(connectId));
406
+ }
407
+ await requestQueue.waitForTask(task, () =>
408
+ waitForPendingPromise(
409
+ method.connectId ?? '',
410
+ getPrePendingCallPromise,
411
+ removePrePendingCallPromise
412
+ )
413
+ );
414
+ if (method.isPreWarmSignal && method.connectId) {
415
+ preWarmCallbackTask = createDeferred<void>();
416
+ context.registerCallbackTask(method.connectId, preWarmCallbackTask);
417
+ }
393
418
  /**
394
419
  * Polling to ensure successful connection
395
420
  */
396
- const connectId = method.connectId ?? '';
397
421
  const pollingId = pollingManager.start(connectId);
398
- device = await ensureConnected(
399
- context,
400
- method,
401
- connectId,
402
- pollingId,
403
- task.abortController?.signal
404
- );
422
+ const env = DataManager.getSettings('env');
423
+ const connect = () =>
424
+ ensureConnected(context, method, connectId, pollingId, method.abortSignal);
425
+ // Discovery may acquire USB endpoints. Finish it before initializing a public
426
+ // request; once registered, that request makes discovery use cached state only.
427
+ if (DataManager.isBrowserWebUsb(env) || DataManager.isDesktopWebUsb(env)) {
428
+ // Synchronization can keep the connect action queued after the caller is
429
+ // cancelled. Observe that promise so a late device-not-found error does
430
+ // not become an unhandled rejection.
431
+ const connectPromise = context.methodSynchronize(async () => {
432
+ if (method.abortSignal?.aborted) {
433
+ throw ERRORS.TypedError(HardwareErrorCode.CallQueueActionCancelled);
434
+ }
435
+ return connect();
436
+ }, 'webusb-discovery');
437
+ connectPromise.catch(() => undefined);
438
+ device = await requestQueue.waitForTask(task, () => connectPromise);
439
+ } else {
440
+ device = await connect();
441
+ }
442
+ if (method.abortSignal?.aborted) {
443
+ throw ERRORS.TypedError(HardwareErrorCode.CallQueueActionCancelled);
444
+ }
405
445
  } catch (e) {
406
446
  preWarmCallbackTask?.resolve();
407
447
  Log.debug('ensureConnected error: ', e);
@@ -477,14 +517,19 @@ const onCallDevice = async (
477
517
 
478
518
  try {
479
519
  // Wait for any pending task except our own (self-wait would deadlock).
480
- if (method.connectId) {
481
- await context.waitForCallbackTasks(method.connectId, preWarmCallbackTask);
520
+ const { connectId } = method;
521
+ if (connectId) {
522
+ await requestQueue.waitForTask(task, () =>
523
+ context.waitForCallbackTasks(connectId, preWarmCallbackTask)
524
+ );
482
525
  }
483
526
 
484
- await waitForPendingPromise(
485
- method.connectId ?? '',
486
- getPrePendingCallPromise,
487
- removePrePendingCallPromise
527
+ await requestQueue.waitForTask(task, () =>
528
+ waitForPendingPromise(
529
+ method.connectId ?? '',
530
+ getPrePendingCallPromise,
531
+ removePrePendingCallPromise
532
+ )
488
533
  );
489
534
 
490
535
  const inner = async (): Promise<void> => {
@@ -689,6 +734,13 @@ const onCallDevice = async (
689
734
  },
690
735
  });
691
736
  messageResponse = createResponseMessage(method.responseID, true, response);
737
+ // Preserve the acknowledged result while the next call waits for cleanup and cooldown.
738
+ if (method.connectId) {
739
+ context.setPrePendingCallPromise(
740
+ method.connectId,
741
+ getPostCallPendingPromise(method, device)
742
+ );
743
+ }
692
744
  requestQueue.resolveRequest(method.responseID, messageResponse);
693
745
  completeMethodRequestContext(method);
694
746
  } catch (error) {
@@ -700,6 +752,12 @@ const onCallDevice = async (
700
752
  }
701
753
  Log.debug(`Call API - Inner Method Run Error`, error);
702
754
  messageResponse = createResponseMessage(method.responseID, false, { error });
755
+ if (method.connectId) {
756
+ context.setPrePendingCallPromise(
757
+ method.connectId,
758
+ getPostCallPendingPromise(method, device)
759
+ );
760
+ }
703
761
  requestQueue.resolveRequest(method.responseID, messageResponse);
704
762
  completeMethodRequestContext(method, error);
705
763
 
@@ -726,6 +784,9 @@ const onCallDevice = async (
726
784
  skipInitialize: canSkipInitialize(method, device),
727
785
  ...parseInitOptions(method),
728
786
  };
787
+ if (method.abortSignal?.aborted) {
788
+ throw ERRORS.TypedError(HardwareErrorCode.CallQueueActionCancelled);
789
+ }
729
790
  const deviceRun = () => device.run(inner, runOptions);
730
791
  task.callPromise = createDeferred<any>(deviceRun);
731
792
 
@@ -746,6 +807,7 @@ const onCallDevice = async (
746
807
  );
747
808
  Log.debug('Call API - Run Error: ', error);
748
809
  completeMethodRequestContext(method, error);
810
+ return messageResponse;
749
811
  } finally {
750
812
  // Release the pre-warm callback task so the next real call can proceed.
751
813
  preWarmCallbackTask?.resolve();
@@ -945,7 +1007,16 @@ export function isRetryableBleConnectionError(method: BaseMethod, error: unknown
945
1007
  if (method.device?.wasInterruptedByUser()) {
946
1008
  return false;
947
1009
  }
948
- const typedError = error as { errorCode?: unknown };
1010
+ const typedError = error as {
1011
+ errorCode?: unknown;
1012
+ params?: { acquireDeadlineExceeded?: unknown };
1013
+ };
1014
+ if (
1015
+ typedError?.errorCode === HardwareErrorCode.BleTimeoutError &&
1016
+ typedError.params?.acquireDeadlineExceeded === true
1017
+ ) {
1018
+ return false;
1019
+ }
949
1020
  return (
950
1021
  typedError?.errorCode === HardwareErrorCode.BleTimeoutError ||
951
1022
  typedError?.errorCode === HardwareErrorCode.BleConnectedError ||
@@ -964,11 +1035,13 @@ export function isMissingDetectedProtocolV2Error(method: BaseMethod, error: unkn
964
1035
  );
965
1036
  }
966
1037
 
967
- export function isProtocolV2PeerRemovedPairingError(method: BaseMethod, error: unknown) {
1038
+ export function isTerminalBleStaleBondError(error: unknown) {
1039
+ return isBleStaleBondHardwareError(error);
1040
+ }
1041
+
1042
+ export function isDeviceIdentityMismatchError(error: unknown) {
968
1043
  return (
969
- method.payload.connectProtocol === 'V2' &&
970
- (error as { errorCode?: unknown })?.errorCode ===
971
- HardwareErrorCode.BlePeerRemovedPairingInformation
1044
+ (error as { errorCode?: unknown })?.errorCode === HardwareErrorCode.DeviceCheckDeviceIdError
972
1045
  );
973
1046
  }
974
1047
 
@@ -1001,7 +1074,8 @@ function raceBleAcquire<T>(acquirePromise: Promise<T>, abortSignal?: AbortSignal
1001
1074
  reject(
1002
1075
  ERRORS.TypedError(
1003
1076
  HardwareErrorCode.BleTimeoutError,
1004
- `BLE acquire exceeded ${BLE_ACQUIRE_DEADLINE_MS}ms deadline`
1077
+ `BLE acquire exceeded ${BLE_ACQUIRE_DEADLINE_MS}ms deadline`,
1078
+ { acquireDeadlineExceeded: true }
1005
1079
  )
1006
1080
  )
1007
1081
  ),
@@ -1031,7 +1105,7 @@ async function connectDeviceForBle(
1031
1105
  retryCount = 0
1032
1106
  ) {
1033
1107
  try {
1034
- if (device.wasInterruptedByUser()) {
1108
+ if (abortSignal?.aborted || device.wasInterruptedByUser()) {
1035
1109
  throw ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromUser);
1036
1110
  }
1037
1111
  if (method.payload.forceProtocolDetection && device.hasDeviceAcquire()) {
@@ -1100,6 +1174,9 @@ async function connectDeviceForBle(
1100
1174
  DevicePool.emitter.emit(DEVICE.CONNECT, device);
1101
1175
  }
1102
1176
  } catch (err) {
1177
+ if (abortSignal?.aborted || device.wasInterruptedByUser()) {
1178
+ throw ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromUser);
1179
+ }
1103
1180
  const requiresColdReconnect = isMissingDetectedProtocolV2Error(method, err);
1104
1181
  // Device.run()'s REQUIRE_DISCONNECT handling never sees acquire/initialize
1105
1182
  // failures, so with keep-alive a wedged link would be reused by every retry
@@ -1150,6 +1227,7 @@ const ensureConnected = async (
1150
1227
  const POLL_INTERVAL_TIME = (method.payload && method.payload.pollIntervalTime) || 1000;
1151
1228
  const TIME_OUT = (method.payload && method.payload.timeout) || 10000;
1152
1229
  let timer: ReturnType<typeof setTimeout> | null = null;
1230
+ let lastInitializeError: any;
1153
1231
  Log.debug(
1154
1232
  `EnsureConnected function start, MAX_RETRY_COUNT=${MAX_RETRY_COUNT}, POLL_INTERVAL_TIME=${POLL_INTERVAL_TIME} `
1155
1233
  );
@@ -1190,7 +1268,9 @@ const ensureConnected = async (
1190
1268
  Log.debug('EnsureConnected function try count: ', tryCount, ' poll interval time: ', time);
1191
1269
  try {
1192
1270
  await initDeviceList(method);
1271
+ lastInitializeError = undefined;
1193
1272
  } catch (error) {
1273
+ lastInitializeError = error;
1194
1274
  Log.debug('device list error: ', error);
1195
1275
  if (
1196
1276
  [
@@ -1205,6 +1285,7 @@ const ensureConnected = async (
1205
1285
  }
1206
1286
  if (error.errorCode === HardwareErrorCode.TransportNotConfigured) {
1207
1287
  await TransportManager.configure();
1288
+ lastInitializeError = undefined;
1208
1289
  }
1209
1290
  }
1210
1291
 
@@ -1273,6 +1354,8 @@ const ensureConnected = async (
1273
1354
  HardwareErrorCode.BleDeviceBondedCanceled,
1274
1355
  HardwareErrorCode.BleCharacteristicNotifyError,
1275
1356
  HardwareErrorCode.BleTimeoutError,
1357
+ // A transport setup reset must also stop this outer poll.
1358
+ HardwareErrorCode.PollingTimeout,
1276
1359
  HardwareErrorCode.BleWriteCharacteristicError,
1277
1360
  HardwareErrorCode.BleAlreadyConnected,
1278
1361
  HardwareErrorCode.FirmwareUpdateLimitOneDevice,
@@ -1283,7 +1366,12 @@ const ensureConnected = async (
1283
1366
  HardwareErrorCode.DeviceInterruptedFromUser,
1284
1367
  HardwareErrorCode.CallQueueActionCancelled,
1285
1368
  ].includes(error.errorCode) ||
1286
- isProtocolV2PeerRemovedPairingError(method, error)
1369
+ (env === 'react-native' &&
1370
+ [HardwareErrorCode.BleDeviceDisconnected, HardwareErrorCode.PollingTimeout].includes(
1371
+ error.errorCode
1372
+ )) ||
1373
+ isTerminalBleStaleBondError(error) ||
1374
+ isDeviceIdentityMismatchError(error)
1287
1375
  ) {
1288
1376
  reject(error);
1289
1377
  return;
@@ -1295,14 +1383,22 @@ const ensureConnected = async (
1295
1383
  clearTimeout(timer);
1296
1384
  }
1297
1385
  Log.debug('EnsureConnected get to max try count, will return: ', tryCount);
1298
- // Browser WebUSB needs permission prompt, desktop WebUSB doesn't
1299
- // skipWebDevicePrompt can override this behavior for special cases
1300
- if (DataManager.isBrowserWebUsb(env) && !method.payload?.skipWebDevicePrompt) {
1386
+ const preserveWebUsbInitError =
1387
+ DataManager.isBrowserWebUsb(env) || DataManager.isDesktopWebUsb(env);
1388
+ const needsPermissionPrompt =
1389
+ DataManager.isBrowserWebUsb(env) && !method.payload?.skipWebDevicePrompt;
1390
+ const fallbackError = needsPermissionPrompt
1391
+ ? ERRORS.TypedError(HardwareErrorCode.WebDeviceNotFoundOrNeedsPermission)
1392
+ : ERRORS.TypedError(HardwareErrorCode.DeviceNotFound);
1393
+ const errorToReject =
1394
+ preserveWebUsbInitError && lastInitializeError ? lastInitializeError : fallbackError;
1395
+ // Only ask the host for a WebUSB grant when the failure is actually
1396
+ // "not found / needs permission". A preserved initialize error must
1397
+ // not fire that prompt with a different public code.
1398
+ if (needsPermissionPrompt && errorToReject === fallbackError) {
1301
1399
  postMessage(createUiMessage(UI_REQUEST.WEB_DEVICE_PROMPT_ACCESS_PERMISSION));
1302
- reject(ERRORS.TypedError(HardwareErrorCode.WebDeviceNotFoundOrNeedsPermission));
1303
- } else {
1304
- reject(ERRORS.TypedError(HardwareErrorCode.DeviceNotFound));
1305
1400
  }
1401
+ reject(errorToReject);
1306
1402
  return;
1307
1403
  }
1308
1404
 
@@ -1333,7 +1429,7 @@ export const cancel = (context: CoreContext, connectId?: string) => {
1333
1429
  // cancel callback tasks
1334
1430
  requestQueue.cancelCallbackTasks(connectId);
1335
1431
 
1336
- const requestIds = requestQueue.getRequestTasksId();
1432
+ const requestIds = requestQueue.getRequestTasksIdByConnectId(connectId);
1337
1433
  Log.debug(
1338
1434
  `Cancel Api connect requestQueues: length:${requestIds.length} requestIds:${requestIds.join(
1339
1435
  ','
@@ -1341,10 +1437,9 @@ export const cancel = (context: CoreContext, connectId?: string) => {
1341
1437
  );
1342
1438
  // Abort before rejecting: rejectRequest releases the task and would make
1343
1439
  // its AbortController unreachable to an in-flight method loop.
1344
- // This branch rejects every queued request below. Abort the same set first so
1345
- // methods whose physical connectId is selected internally (for example
1346
- // Desktop WebUSB firmwareUpdateV4) cannot keep retrying after rejection.
1347
- requestQueue.abortAllRequests();
1440
+ // Match both the requested connectId and a device selected internally by the
1441
+ // method, such as Desktop WebUSB firmwareUpdateV4.
1442
+ requestQueue.abortRequestsByConnectId(connectId);
1348
1443
  const canceledDevices: Device[] = [];
1349
1444
  const interruptDevice = (device: Device | undefined, deviceConnectId: string) => {
1350
1445
  if (!device || canceledDevices.includes(device)) {
@@ -1360,7 +1455,7 @@ export const cancel = (context: CoreContext, connectId?: string) => {
1360
1455
  // During ensureConnected the method has a connectId but device is
1361
1456
  // assigned only after the poll succeeds. Interrupt the cached BLE
1362
1457
  // Device so an in-flight acquire/initialize cannot finish.
1363
- interruptDevice(task.method?.device, task.method.connectId ?? connectId);
1458
+ interruptDevice(task.method?.device, connectId);
1364
1459
  interruptDevice(deviceCacheMap.get(connectId), connectId);
1365
1460
  requestQueue.rejectRequest(
1366
1461
  requestId,
@@ -1375,10 +1470,11 @@ export const cancel = (context: CoreContext, connectId?: string) => {
1375
1470
  }
1376
1471
  } else {
1377
1472
  const env = DataManager.getSettings('env');
1473
+ // Abort every method before rejecting its queue task. Non-BLE methods also
1474
+ // use the signal to stop recovery loops after the public promise is rejected.
1475
+ requestQueue.abortAllRequests();
1378
1476
  if (DataManager.isBleConnect(env)) {
1379
1477
  Log.debug('Cancel Api all _deviceList: ');
1380
- // Keep method abort signals observable until every active task is rejected.
1381
- requestQueue.abortAllRequests();
1382
1478
  const canceledDevices: Device[] = [];
1383
1479
  const interruptDevice = (device?: Device) => {
1384
1480
  if (!device || canceledDevices.includes(device)) {
@@ -1421,8 +1517,10 @@ export const cancel = (context: CoreContext, connectId?: string) => {
1421
1517
  }
1422
1518
  }
1423
1519
 
1424
- cleanup();
1425
- closePopup();
1520
+ cleanup(connectId);
1521
+ if (!connectId || _uiPromises.length === 0) {
1522
+ closePopup();
1523
+ }
1426
1524
  };
1427
1525
 
1428
1526
  const checkPassphraseEnableState = (method: BaseMethod, features?: Features) => {
@@ -1460,9 +1558,16 @@ const shouldCheckPassphraseState = (method: BaseMethod, device: Device) => {
1460
1558
  return device.hasUsePassphrase();
1461
1559
  };
1462
1560
 
1463
- const cleanup = () => {
1464
- const pendingUiPromises = _uiPromises;
1465
- _uiPromises = [];
1561
+ const cleanup = (connectId?: string) => {
1562
+ const pendingUiPromises = connectId
1563
+ ? _uiPromises.filter(
1564
+ uiPromise =>
1565
+ uiPromise.data?.mainId === connectId || uiPromise.data?.getConnectId() === connectId
1566
+ )
1567
+ : _uiPromises;
1568
+ _uiPromises = connectId
1569
+ ? _uiPromises.filter(uiPromise => !pendingUiPromises.includes(uiPromise))
1570
+ : [];
1466
1571
  rejectUiPromises(
1467
1572
  pendingUiPromises,
1468
1573
  ERRORS.TypedError(HardwareErrorCode.ActionCancelled, 'UI request was cancelled')
@@ -1751,7 +1856,17 @@ export default class Core extends EventEmitter {
1751
1856
  this.prePendingCallPromises.delete(connectId);
1752
1857
  return;
1753
1858
  }
1754
- this.prePendingCallPromises.set(connectId, promise);
1859
+ const previous = this.prePendingCallPromises.get(connectId);
1860
+ const cleanupPromise =
1861
+ previous && previous !== promise
1862
+ ? Promise.all([previous, promise]).then(() => undefined)
1863
+ : promise;
1864
+ this.prePendingCallPromises.set(connectId, cleanupPromise);
1865
+ // cancel() is fire-and-forget. Observe failures immediately, while
1866
+ // preserving the rejected barrier for the next caller's safety check.
1867
+ cleanupPromise.catch(error => {
1868
+ Log.warn('Device cancellation cleanup failed', { errorCode: error?.errorCode });
1869
+ });
1755
1870
  },
1756
1871
  removePrePendingCallPromise: (connectId: string, promise: Promise<void>) => {
1757
1872
  if (this.prePendingCallPromises.get(connectId) === promise) {
@@ -4463,10 +4463,6 @@
4463
4463
  "request_id": {
4464
4464
  "type": "bytes",
4465
4465
  "id": 5
4466
- },
4467
- "source_fingerprint": {
4468
- "type": "uint32",
4469
- "id": 6
4470
4466
  }
4471
4467
  }
4472
4468
  },
@@ -4722,10 +4718,6 @@
4722
4718
  "expected_address": {
4723
4719
  "type": "bytes",
4724
4720
  "id": 12
4725
- },
4726
- "source_fingerprint": {
4727
- "type": "uint32",
4728
- "id": 13
4729
4721
  }
4730
4722
  }
4731
4723
  },
@@ -4810,10 +4802,6 @@
4810
4802
  "expected_address": {
4811
4803
  "type": "bytes",
4812
4804
  "id": 12
4813
- },
4814
- "source_fingerprint": {
4815
- "type": "uint32",
4816
- "id": 13
4817
4805
  }
4818
4806
  }
4819
4807
  },
@@ -4961,7 +4949,14 @@
4961
4949
  "type": "EthereumAuthorizationSignature",
4962
4950
  "id": 10
4963
4951
  }
4964
- }
4952
+ },
4953
+ "reserved": [
4954
+ [5, 5],
4955
+ [6, 6],
4956
+ [7, 7],
4957
+ [8, 8],
4958
+ [9, 9]
4959
+ ]
4965
4960
  },
4966
4961
  "EthereumTxAckOneKey": {
4967
4962
  "fields": {
@@ -4990,10 +4985,6 @@
4990
4985
  "chain_id": {
4991
4986
  "type": "uint64",
4992
4987
  "id": 3
4993
- },
4994
- "source_fingerprint": {
4995
- "type": "uint32",
4996
- "id": 4
4997
4988
  }
4998
4989
  }
4999
4990
  },
@@ -8968,7 +8959,8 @@
8968
8959
  },
8969
8960
  "SolanaOffChainMessageVersion": {
8970
8961
  "values": {
8971
- "MESSAGE_VERSION_0": 0
8962
+ "MESSAGE_VERSION_0": 0,
8963
+ "MESSAGE_VERSION_1": 1
8972
8964
  }
8973
8965
  },
8974
8966
  "SolanaOffChainMessageFormat": {
@@ -9016,10 +9008,6 @@
9016
9008
  "rule": "required",
9017
9009
  "type": "bytes",
9018
9010
  "id": 2
9019
- },
9020
- "source_fingerprint": {
9021
- "type": "uint32",
9022
- "id": 3
9023
9011
  }
9024
9012
  }
9025
9013
  },
@@ -9056,17 +9044,15 @@
9056
9044
  },
9057
9045
  "message_format": {
9058
9046
  "type": "SolanaOffChainMessageFormat",
9059
- "id": 4,
9060
- "options": {
9061
- "default": "V0_RESTRICTED_ASCII"
9062
- }
9047
+ "id": 4
9063
9048
  },
9064
9049
  "application_domain": {
9065
9050
  "type": "bytes",
9066
9051
  "id": 5
9067
9052
  },
9068
- "source_fingerprint": {
9069
- "type": "uint32",
9053
+ "required_signers": {
9054
+ "rule": "repeated",
9055
+ "type": "bytes",
9070
9056
  "id": 6
9071
9057
  }
9072
9058
  }
@@ -9085,10 +9071,6 @@
9085
9071
  "rule": "required",
9086
9072
  "type": "bytes",
9087
9073
  "id": 2
9088
- },
9089
- "source_fingerprint": {
9090
- "type": "uint32",
9091
- "id": 3
9092
9074
  }
9093
9075
  }
9094
9076
  },
@@ -11501,6 +11483,7 @@
11501
11483
  "MessageType_DeviceFindMyTokenState": 60450,
11502
11484
  "MessageType_DeviceFindMyTokenUpdate": 60451,
11503
11485
  "MessageType_DeviceFindMyTokenStateGet": 60452,
11486
+ "MessageType_DeviceAnimationControl": 60461,
11504
11487
  "MessageType_DeviceInfoGet": 60600,
11505
11488
  "MessageType_DeviceInfo": 60601,
11506
11489
  "MessageType_DeviceStatusGet": 60602,
@@ -11824,6 +11807,26 @@
11824
11807
  }
11825
11808
  }
11826
11809
  },
11810
+ "DeviceAnimationAction": {
11811
+ "values": {
11812
+ "AnimationAction_Unknown": 0,
11813
+ "AnimationAction_Start": 1,
11814
+ "AnimationAction_Stop": 2
11815
+ }
11816
+ },
11817
+ "DeviceAnimationControl": {
11818
+ "fields": {
11819
+ "action": {
11820
+ "rule": "required",
11821
+ "type": "DeviceAnimationAction",
11822
+ "id": 1
11823
+ },
11824
+ "timeout_ms": {
11825
+ "type": "uint32",
11826
+ "id": 2
11827
+ }
11828
+ }
11829
+ },
11827
11830
  "DeviceFactoryAck": {
11828
11831
  "values": {
11829
11832
  "FACTORY_ACK_SUCCESS": 0,
@@ -11885,6 +11888,10 @@
11885
11888
  "manufacture_time": {
11886
11889
  "type": "DeviceFactoryInfoManufactureTime",
11887
11890
  "id": 5
11891
+ },
11892
+ "data": {
11893
+ "type": "bytes",
11894
+ "id": 100
11888
11895
  }
11889
11896
  }
11890
11897
  },
@@ -11894,11 +11901,20 @@
11894
11901
  "rule": "required",
11895
11902
  "type": "DeviceFactoryInfo",
11896
11903
  "id": 1
11904
+ },
11905
+ "full_data": {
11906
+ "type": "bool",
11907
+ "id": 2
11897
11908
  }
11898
11909
  }
11899
11910
  },
11900
11911
  "DeviceFactoryInfoGet": {
11901
- "fields": {}
11912
+ "fields": {
11913
+ "full_data": {
11914
+ "type": "bool",
11915
+ "id": 1
11916
+ }
11917
+ }
11902
11918
  },
11903
11919
  "DeviceFactoryPermanentLock": {
11904
11920
  "fields": {
@@ -10243,7 +10243,8 @@
10243
10243
  },
10244
10244
  "SolanaOffChainMessageVersion": {
10245
10245
  "values": {
10246
- "MESSAGE_VERSION_0": 0
10246
+ "MESSAGE_VERSION_0": 0,
10247
+ "MESSAGE_VERSION_1": 1
10247
10248
  }
10248
10249
  },
10249
10250
  "SolanaOffChainMessageFormat": {
@@ -10276,14 +10277,16 @@
10276
10277
  },
10277
10278
  "message_format": {
10278
10279
  "type": "SolanaOffChainMessageFormat",
10279
- "id": 4,
10280
- "options": {
10281
- "default": "V0_RESTRICTED_ASCII"
10282
- }
10280
+ "id": 4
10283
10281
  },
10284
10282
  "application_domain": {
10285
10283
  "type": "bytes",
10286
10284
  "id": 5
10285
+ },
10286
+ "required_signers": {
10287
+ "rule": "repeated",
10288
+ "type": "bytes",
10289
+ "id": 7
10287
10290
  }
10288
10291
  }
10289
10292
  },