@onekeyfe/hd-core 1.2.2-alpha.12 → 1.2.2-alpha.120

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 (67) hide show
  1. package/README.md +1 -1
  2. package/__tests__/AllNetworkGetAddressBase.tracing.test.ts +452 -9
  3. package/__tests__/DeviceCommands.test.ts +254 -1
  4. package/__tests__/core-error-output.test.ts +92 -1
  5. package/__tests__/device-lifecycle-events.test.ts +379 -15
  6. package/__tests__/open-wallet-session-error-response.test.ts +2 -2
  7. package/__tests__/open-wallet-session.test.ts +8 -411
  8. package/__tests__/protocol-v2.test.ts +86 -0
  9. package/__tests__/public-device-state-api.test.ts +2 -7
  10. package/__tests__/sol-sign-offchain-message.test.ts +64 -0
  11. package/dist/api/GetFeatures.d.ts.map +1 -1
  12. package/dist/api/GetPassphraseState.d.ts.map +1 -1
  13. package/dist/api/OpenWalletSession.d.ts.map +1 -1
  14. package/dist/api/UploadPortfolio.d.ts +1 -0
  15. package/dist/api/UploadPortfolio.d.ts.map +1 -1
  16. package/dist/api/allnetwork/AllNetworkGetAddress.d.ts.map +1 -1
  17. package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts +6 -0
  18. package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts.map +1 -1
  19. package/dist/api/allnetwork/AllNetworkGetAddressByLoop.d.ts.map +1 -1
  20. package/dist/api/device/DeviceVerify.d.ts.map +1 -1
  21. package/dist/api/solana/SolSignOffchainMessage.d.ts.map +1 -1
  22. package/dist/core/RequestQueue.d.ts +1 -0
  23. package/dist/core/RequestQueue.d.ts.map +1 -1
  24. package/dist/core/index.d.ts +3 -1
  25. package/dist/core/index.d.ts.map +1 -1
  26. package/dist/core/uiPromiseRegistry.d.ts +1 -1
  27. package/dist/device/Device.d.ts +2 -0
  28. package/dist/device/Device.d.ts.map +1 -1
  29. package/dist/device/DeviceCommands.d.ts +5 -4
  30. package/dist/device/DeviceCommands.d.ts.map +1 -1
  31. package/dist/index.d.ts +35 -30
  32. package/dist/index.js +402 -208
  33. package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
  34. package/dist/types/api/getFeatures.d.ts.map +1 -1
  35. package/dist/types/api/getPassphraseState.d.ts.map +1 -1
  36. package/dist/types/api/openWalletSession.d.ts +1 -10
  37. package/dist/types/api/openWalletSession.d.ts.map +1 -1
  38. package/dist/types/api/protocolV2.d.ts +3 -4
  39. package/dist/types/api/protocolV2.d.ts.map +1 -1
  40. package/dist/types/api/solSignOffchainMessage.d.ts +1 -0
  41. package/dist/types/api/solSignOffchainMessage.d.ts.map +1 -1
  42. package/dist/types/params.d.ts.map +1 -1
  43. package/dist/utils/patch.d.ts +1 -1
  44. package/dist/utils/patch.d.ts.map +1 -1
  45. package/package.json +4 -4
  46. package/src/api/GetFeatures.ts +1 -0
  47. package/src/api/GetPassphraseState.ts +1 -0
  48. package/src/api/OpenWalletSession.ts +7 -77
  49. package/src/api/UploadPortfolio.ts +5 -4
  50. package/src/api/allnetwork/AllNetworkGetAddress.ts +47 -20
  51. package/src/api/allnetwork/AllNetworkGetAddressBase.ts +88 -22
  52. package/src/api/allnetwork/AllNetworkGetAddressByLoop.ts +3 -0
  53. package/src/api/device/DeviceVerify.ts +8 -0
  54. package/src/api/solana/SolSignOffchainMessage.ts +44 -4
  55. package/src/core/RequestQueue.ts +20 -0
  56. package/src/core/index.ts +116 -49
  57. package/src/data/messages/messages-protocol-v2.json +49 -33
  58. package/src/data/messages/messages.json +8 -5
  59. package/src/device/Device.ts +51 -27
  60. package/src/device/DeviceCommands.ts +29 -2
  61. package/src/protocols/protocol-v2/walletSession.ts +7 -0
  62. package/src/types/api/getFeatures.ts +2 -1
  63. package/src/types/api/getPassphraseState.ts +2 -5
  64. package/src/types/api/openWalletSession.ts +7 -19
  65. package/src/types/api/protocolV2.ts +3 -4
  66. package/src/types/api/solSignOffchainMessage.ts +2 -0
  67. 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';
@@ -83,6 +85,7 @@ import type { BaseMethod } from '../api/BaseMethod';
83
85
  const Log = getLogger(LoggerNames.Core);
84
86
  const PRE_INITIALIZE_TTL_MS = 60 * 1000;
85
87
  const PRE_PENDING_CALL_TIMEOUT_MS = 15 * 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.
@@ -312,30 +315,44 @@ const waitForPendingPromise = async (
312
315
  if (pendingPromise) {
313
316
  Log.debug('pre pending call promise before call method, wait for it');
314
317
  let timer: ReturnType<typeof setTimeout> | undefined;
315
- let timedOut = false;
316
318
  try {
317
319
  await Promise.race([
318
320
  pendingPromise,
319
- new Promise<void>(resolve => {
321
+ new Promise<void>((_, reject) => {
320
322
  timer = setTimeout(() => {
321
- timedOut = true;
322
- resolve();
323
+ reject(
324
+ ERRORS.TypedError(
325
+ HardwareErrorCode.DeviceBusy,
326
+ 'Previous device cancellation is still draining'
327
+ )
328
+ );
323
329
  }, PRE_PENDING_CALL_TIMEOUT_MS);
324
330
  }),
325
331
  ]);
326
- } catch (error) {
327
- // Cancellation is best-effort; the transport teardown owns recovery.
332
+ // A deadline is not evidence that old I/O is safe to reuse. Keep the
333
+ // barrier on failure; a later call may proceed only after cleanup settles.
334
+ removePrePendingCallPromise?.(connectId, pendingPromise);
328
335
  } finally {
329
336
  if (timer) clearTimeout(timer);
330
- removePrePendingCallPromise?.(connectId, pendingPromise);
331
- }
332
- if (timedOut) {
333
- Log.warn('pre pending call promise timed out before call method', { connectId });
334
337
  }
335
338
  Log.debug('pre pending call promise before call method done');
336
339
  }
337
340
  };
338
341
 
342
+ export function getPostCallPendingPromise(method: BaseMethod, device: Device): Promise<void> {
343
+ const cleanupPromise = device.waitForRunCleanup();
344
+ const env = DataManager.getSettings('env');
345
+ const deviceType = device.getCurrentDeviceType();
346
+ const requiresSigningCooldown =
347
+ method.name.includes('Sign') &&
348
+ (deviceType === EDeviceType.Pro2 || deviceType === EDeviceType.Neo) &&
349
+ (DataManager.isBrowserWebUsb(env) || DataManager.isDesktopWebUsb(env) || env === 'node-usb');
350
+
351
+ return requiresSigningCooldown
352
+ ? cleanupPromise.then(() => wait(PRO2_USB_SIGNING_COOLDOWN_MS)).then(() => undefined)
353
+ : cleanupPromise;
354
+ }
355
+
339
356
  const onCallDevice = async (
340
357
  context: CoreContext,
341
358
  message: CoreMessage,
@@ -366,42 +383,38 @@ const onCallDevice = async (
366
383
  DevicePool.clearDeviceCache(method.payload.connectId);
367
384
  }
368
385
 
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
-
386
+ // Register before waiting so cancellation also covers queued requests.
380
387
  const task = requestQueue.createTask(method);
381
388
 
382
389
  // Pre-warm holds the device as a per-connectId callback task so a concurrent
383
390
  // real call waits (before ensureConnected) instead of racing its Initialize.
384
391
  // Only covers pre-warm -> real-call ordering; the reverse is fail-closed.
385
392
  let preWarmCallbackTask: Deferred<void> | undefined;
386
- if (method.isPreWarmSignal && method.connectId) {
387
- preWarmCallbackTask = createDeferred<void>();
388
- context.registerCallbackTask(method.connectId, preWarmCallbackTask);
389
- }
390
-
391
393
  let device: Device;
392
394
  try {
395
+ const connectId = method.connectId ?? '';
396
+ if (connectId) {
397
+ await requestQueue.waitForTask(task, () => context.waitForCallbackTasks(connectId));
398
+ }
399
+ await requestQueue.waitForTask(task, () =>
400
+ waitForPendingPromise(
401
+ method.connectId ?? '',
402
+ getPrePendingCallPromise,
403
+ removePrePendingCallPromise
404
+ )
405
+ );
406
+ if (method.isPreWarmSignal && method.connectId) {
407
+ preWarmCallbackTask = createDeferred<void>();
408
+ context.registerCallbackTask(method.connectId, preWarmCallbackTask);
409
+ }
393
410
  /**
394
411
  * Polling to ensure successful connection
395
412
  */
396
- const connectId = method.connectId ?? '';
397
413
  const pollingId = pollingManager.start(connectId);
398
- device = await ensureConnected(
399
- context,
400
- method,
401
- connectId,
402
- pollingId,
403
- task.abortController?.signal
404
- );
414
+ device = await ensureConnected(context, method, connectId, pollingId, method.abortSignal);
415
+ if (method.abortSignal?.aborted) {
416
+ throw ERRORS.TypedError(HardwareErrorCode.CallQueueActionCancelled);
417
+ }
405
418
  } catch (e) {
406
419
  preWarmCallbackTask?.resolve();
407
420
  Log.debug('ensureConnected error: ', e);
@@ -477,14 +490,19 @@ const onCallDevice = async (
477
490
 
478
491
  try {
479
492
  // Wait for any pending task except our own (self-wait would deadlock).
480
- if (method.connectId) {
481
- await context.waitForCallbackTasks(method.connectId, preWarmCallbackTask);
493
+ const { connectId } = method;
494
+ if (connectId) {
495
+ await requestQueue.waitForTask(task, () =>
496
+ context.waitForCallbackTasks(connectId, preWarmCallbackTask)
497
+ );
482
498
  }
483
499
 
484
- await waitForPendingPromise(
485
- method.connectId ?? '',
486
- getPrePendingCallPromise,
487
- removePrePendingCallPromise
500
+ await requestQueue.waitForTask(task, () =>
501
+ waitForPendingPromise(
502
+ method.connectId ?? '',
503
+ getPrePendingCallPromise,
504
+ removePrePendingCallPromise
505
+ )
488
506
  );
489
507
 
490
508
  const inner = async (): Promise<void> => {
@@ -689,6 +707,13 @@ const onCallDevice = async (
689
707
  },
690
708
  });
691
709
  messageResponse = createResponseMessage(method.responseID, true, response);
710
+ // Preserve the acknowledged result while the next call waits for cleanup and cooldown.
711
+ if (method.connectId) {
712
+ context.setPrePendingCallPromise(
713
+ method.connectId,
714
+ getPostCallPendingPromise(method, device)
715
+ );
716
+ }
692
717
  requestQueue.resolveRequest(method.responseID, messageResponse);
693
718
  completeMethodRequestContext(method);
694
719
  } catch (error) {
@@ -700,6 +725,12 @@ const onCallDevice = async (
700
725
  }
701
726
  Log.debug(`Call API - Inner Method Run Error`, error);
702
727
  messageResponse = createResponseMessage(method.responseID, false, { error });
728
+ if (method.connectId) {
729
+ context.setPrePendingCallPromise(
730
+ method.connectId,
731
+ getPostCallPendingPromise(method, device)
732
+ );
733
+ }
703
734
  requestQueue.resolveRequest(method.responseID, messageResponse);
704
735
  completeMethodRequestContext(method, error);
705
736
 
@@ -726,6 +757,9 @@ const onCallDevice = async (
726
757
  skipInitialize: canSkipInitialize(method, device),
727
758
  ...parseInitOptions(method),
728
759
  };
760
+ if (method.abortSignal?.aborted) {
761
+ throw ERRORS.TypedError(HardwareErrorCode.CallQueueActionCancelled);
762
+ }
729
763
  const deviceRun = () => device.run(inner, runOptions);
730
764
  task.callPromise = createDeferred<any>(deviceRun);
731
765
 
@@ -746,6 +780,7 @@ const onCallDevice = async (
746
780
  );
747
781
  Log.debug('Call API - Run Error: ', error);
748
782
  completeMethodRequestContext(method, error);
783
+ return messageResponse;
749
784
  } finally {
750
785
  // Release the pre-warm callback task so the next real call can proceed.
751
786
  preWarmCallbackTask?.resolve();
@@ -945,7 +980,16 @@ export function isRetryableBleConnectionError(method: BaseMethod, error: unknown
945
980
  if (method.device?.wasInterruptedByUser()) {
946
981
  return false;
947
982
  }
948
- const typedError = error as { errorCode?: unknown };
983
+ const typedError = error as {
984
+ errorCode?: unknown;
985
+ params?: { acquireDeadlineExceeded?: unknown };
986
+ };
987
+ if (
988
+ typedError?.errorCode === HardwareErrorCode.BleTimeoutError &&
989
+ typedError.params?.acquireDeadlineExceeded === true
990
+ ) {
991
+ return false;
992
+ }
949
993
  return (
950
994
  typedError?.errorCode === HardwareErrorCode.BleTimeoutError ||
951
995
  typedError?.errorCode === HardwareErrorCode.BleConnectedError ||
@@ -964,11 +1008,13 @@ export function isMissingDetectedProtocolV2Error(method: BaseMethod, error: unkn
964
1008
  );
965
1009
  }
966
1010
 
967
- export function isProtocolV2PeerRemovedPairingError(method: BaseMethod, error: unknown) {
1011
+ export function isTerminalBleStaleBondError(error: unknown) {
1012
+ return isBleStaleBondHardwareError(error);
1013
+ }
1014
+
1015
+ export function isDeviceIdentityMismatchError(error: unknown) {
968
1016
  return (
969
- method.payload.connectProtocol === 'V2' &&
970
- (error as { errorCode?: unknown })?.errorCode ===
971
- HardwareErrorCode.BlePeerRemovedPairingInformation
1017
+ (error as { errorCode?: unknown })?.errorCode === HardwareErrorCode.DeviceCheckDeviceIdError
972
1018
  );
973
1019
  }
974
1020
 
@@ -1001,7 +1047,8 @@ function raceBleAcquire<T>(acquirePromise: Promise<T>, abortSignal?: AbortSignal
1001
1047
  reject(
1002
1048
  ERRORS.TypedError(
1003
1049
  HardwareErrorCode.BleTimeoutError,
1004
- `BLE acquire exceeded ${BLE_ACQUIRE_DEADLINE_MS}ms deadline`
1050
+ `BLE acquire exceeded ${BLE_ACQUIRE_DEADLINE_MS}ms deadline`,
1051
+ { acquireDeadlineExceeded: true }
1005
1052
  )
1006
1053
  )
1007
1054
  ),
@@ -1031,7 +1078,7 @@ async function connectDeviceForBle(
1031
1078
  retryCount = 0
1032
1079
  ) {
1033
1080
  try {
1034
- if (device.wasInterruptedByUser()) {
1081
+ if (abortSignal?.aborted || device.wasInterruptedByUser()) {
1035
1082
  throw ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromUser);
1036
1083
  }
1037
1084
  if (method.payload.forceProtocolDetection && device.hasDeviceAcquire()) {
@@ -1100,6 +1147,9 @@ async function connectDeviceForBle(
1100
1147
  DevicePool.emitter.emit(DEVICE.CONNECT, device);
1101
1148
  }
1102
1149
  } catch (err) {
1150
+ if (abortSignal?.aborted || device.wasInterruptedByUser()) {
1151
+ throw ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromUser);
1152
+ }
1103
1153
  const requiresColdReconnect = isMissingDetectedProtocolV2Error(method, err);
1104
1154
  // Device.run()'s REQUIRE_DISCONNECT handling never sees acquire/initialize
1105
1155
  // failures, so with keep-alive a wedged link would be reused by every retry
@@ -1273,6 +1323,8 @@ const ensureConnected = async (
1273
1323
  HardwareErrorCode.BleDeviceBondedCanceled,
1274
1324
  HardwareErrorCode.BleCharacteristicNotifyError,
1275
1325
  HardwareErrorCode.BleTimeoutError,
1326
+ // A transport setup reset must also stop this outer poll.
1327
+ HardwareErrorCode.PollingTimeout,
1276
1328
  HardwareErrorCode.BleWriteCharacteristicError,
1277
1329
  HardwareErrorCode.BleAlreadyConnected,
1278
1330
  HardwareErrorCode.FirmwareUpdateLimitOneDevice,
@@ -1283,7 +1335,12 @@ const ensureConnected = async (
1283
1335
  HardwareErrorCode.DeviceInterruptedFromUser,
1284
1336
  HardwareErrorCode.CallQueueActionCancelled,
1285
1337
  ].includes(error.errorCode) ||
1286
- isProtocolV2PeerRemovedPairingError(method, error)
1338
+ (env === 'react-native' &&
1339
+ [HardwareErrorCode.BleDeviceDisconnected, HardwareErrorCode.PollingTimeout].includes(
1340
+ error.errorCode
1341
+ )) ||
1342
+ isTerminalBleStaleBondError(error) ||
1343
+ isDeviceIdentityMismatchError(error)
1287
1344
  ) {
1288
1345
  reject(error);
1289
1346
  return;
@@ -1760,7 +1817,17 @@ export default class Core extends EventEmitter {
1760
1817
  this.prePendingCallPromises.delete(connectId);
1761
1818
  return;
1762
1819
  }
1763
- this.prePendingCallPromises.set(connectId, promise);
1820
+ const previous = this.prePendingCallPromises.get(connectId);
1821
+ const cleanupPromise =
1822
+ previous && previous !== promise
1823
+ ? Promise.all([previous, promise]).then(() => undefined)
1824
+ : promise;
1825
+ this.prePendingCallPromises.set(connectId, cleanupPromise);
1826
+ // cancel() is fire-and-forget. Observe failures immediately, while
1827
+ // preserving the rejected barrier for the next caller's safety check.
1828
+ cleanupPromise.catch(error => {
1829
+ Log.warn('Device cancellation cleanup failed', { errorCode: error?.errorCode });
1830
+ });
1764
1831
  },
1765
1832
  removePrePendingCallPromise: (connectId: string, promise: Promise<void>) => {
1766
1833
  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
  },
@@ -297,6 +297,8 @@ export class Device extends EventEmitter {
297
297
  /** Resolves only after the active run has completed its release path. */
298
298
  private runCleanupPromise?: Promise<void>;
299
299
 
300
+ private userInterruption?: { attempt: number; promise: Promise<void> };
301
+
300
302
  externalState: string[] = [];
301
303
 
302
304
  unavailableCapabilities: UnavailableCapabilities = {};
@@ -1462,6 +1464,10 @@ export class Device extends EventEmitter {
1462
1464
  this.invalidateProtocolV2RuntimeState();
1463
1465
  }
1464
1466
 
1467
+ waitForRunCleanup(): Promise<void> {
1468
+ return this.runCleanupPromise ?? Promise.resolve();
1469
+ }
1470
+
1465
1471
  async run(fn?: () => Promise<void>, options?: RunOptions) {
1466
1472
  if (this.runPromise) {
1467
1473
  await this.interruptionFromOutside();
@@ -1596,45 +1602,63 @@ export class Device extends EventEmitter {
1596
1602
  }
1597
1603
  }
1598
1604
 
1599
- async interruptionFromUser() {
1605
+ interruptionFromUser(): Promise<void> {
1606
+ const attempt = this.connectionAttempt;
1607
+ if (this.userInterruption?.attempt === attempt) {
1608
+ return this.userInterruption.promise;
1609
+ }
1600
1610
  const error = ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromUser);
1601
- this.interruptedAttempt = this.connectionAttempt;
1611
+ this.interruptedAttempt = attempt;
1602
1612
  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
- });
1613
+ const { cancelableAction, commands, runPromise, deviceConnector, mainId } = this;
1614
+ const sendFallbackCancel = this.shouldSendFallbackProtocolCancel();
1615
+ const acquired = this.hasDeviceAcquire();
1616
+ const promise = (async () => {
1617
+ try {
1618
+ if (cancelableAction) {
1619
+ await cancelableAction(error);
1620
+ } else if (sendFallbackCancel) {
1621
+ await commands?.cancelDevice?.().catch(cancelError => {
1622
+ Log.debug('Protocol V2 fallback cancel error', cancelError);
1623
+ });
1624
+ } else if (!acquired) {
1625
+ // Abort setup without acquiring a session just to send Cancel.
1626
+ if (mainId && deviceConnector?.disconnect) {
1627
+ await deviceConnector.disconnect(mainId);
1628
+ }
1629
+ if (this.connectionAttempt === attempt) this.markTransportDisconnected();
1630
+ }
1631
+ await commands?.cancel();
1632
+ } catch (cleanupError) {
1633
+ Log.warn('User cancellation cleanup failed; disconnecting device', cleanupError);
1634
+ if (mainId && deviceConnector?.disconnect) {
1635
+ await deviceConnector.disconnect(mainId).catch(disconnectError => {
1636
+ Log.debug('User cancellation fallback disconnect failed', disconnectError);
1637
+ });
1638
+ }
1639
+ if (this.connectionAttempt === attempt) this.markTransportDisconnected();
1640
+ } finally {
1641
+ runPromise?.reject(error);
1642
+ if (this.runPromise === runPromise) this.runPromise = null;
1643
+ await cleanupPromise?.catch(() => undefined);
1617
1644
  }
1618
- this.markTransportDisconnected();
1619
- }
1620
- await this.commands?.cancel();
1621
-
1622
- if (this.runPromise) {
1623
- this.runPromise.reject(error);
1624
- this.runPromise = null;
1625
- }
1626
- await cleanupPromise?.catch(() => undefined);
1645
+ })();
1646
+ // Keep the settled promise for this attempt so a duplicate close/finally
1647
+ // cannot send a second wire Cancel. A new attempt gets its own cleanup.
1648
+ this.userInterruption = { attempt, promise };
1649
+ return promise;
1627
1650
  }
1628
1651
 
1629
1652
  setCancelableAction(callback: (err?: Error) => Promise<unknown>) {
1630
- this.cancelableAction = (e?: Error) =>
1653
+ const action = (e?: Error) =>
1631
1654
  callback(e)
1632
1655
  .catch(e2 => {
1633
1656
  Log.debug('cancelableAction error', e2);
1634
1657
  })
1635
1658
  .finally(() => {
1636
- this.clearCancelableAction();
1659
+ if (this.cancelableAction === action) this.clearCancelableAction();
1637
1660
  });
1661
+ this.cancelableAction = action;
1638
1662
  }
1639
1663
 
1640
1664
  clearCancelableAction() {