@onekeyfe/hwk-ledger-adapter 1.2.3-alpha.7 → 1.2.3-alpha.8

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.
package/dist/index.mjs CHANGED
@@ -6,30 +6,41 @@ import {
6
6
  DeviceJobQueue,
7
7
  EConnectorInteraction,
8
8
  HardwareErrorCode as HardwareErrorCode3,
9
+ OperationRegistry,
10
+ SDK,
9
11
  TypedEventEmitter,
10
12
  UI_REQUEST,
11
13
  UI_REQUEST_PREEMPTED_TAG,
12
14
  UiRequestRegistry,
15
+ canReplayHardwareMethodAfterTransportFailure,
13
16
  createHwkError,
14
17
  deriveDeviceFingerprint,
15
18
  failure as failure2,
19
+ isHardwareOperationId,
20
+ isHwkRecoveryHint,
21
+ operationMayHaveCompletedParams,
16
22
  rehydrateConnectorError,
23
+ requestBleDeviceSelection,
24
+ requestSaveDeviceBinding,
25
+ resolveSearchTargetReusePolicy,
17
26
  success
18
27
  } from "@onekeyfe/hwk-adapter-core";
19
28
 
20
29
  // src/errors.ts
21
- import { HardwareErrorCode, enrichErrorMessage } from "@onekeyfe/hwk-adapter-core";
22
- var MULTIPLE_USB_LEDGER_DEVICES_ERROR_MESSAGE = "Multiple Ledger USB devices are connected. Please connect only one Ledger device and try again.";
23
- function createMultipleUsbLedgerDevicesError() {
24
- return Object.assign(new Error(MULTIPLE_USB_LEDGER_DEVICES_ERROR_MESSAGE), {
25
- code: HardwareErrorCode.DeviceOneDeviceOnly
26
- });
27
- }
28
- function ledgerFailure(code, error, appName, tag, params) {
30
+ import {
31
+ HardwareErrorCode,
32
+ defaultOriginForCode,
33
+ defaultRecoveryForCode,
34
+ enrichErrorMessage
35
+ } from "@onekeyfe/hwk-adapter-core";
36
+ function ledgerFailure(code, error, appName, tag, params, origin, recovery) {
29
37
  const payload = { error, code };
30
38
  if (appName !== void 0) payload.appName = appName;
31
39
  if (tag !== void 0) payload._tag = tag;
32
40
  if (params !== void 0) payload.params = params;
41
+ const resolvedOrigin = origin ?? defaultOriginForCode(code);
42
+ if (resolvedOrigin !== void 0) payload.origin = resolvedOrigin;
43
+ payload.recovery = recovery ?? defaultRecoveryForCode(code);
33
44
  return { success: false, payload };
34
45
  }
35
46
  var LOCKED_ERROR_CODES = /* @__PURE__ */ new Set(["5515", "21781", "6982", "27010", "5303", "21251"]);
@@ -170,7 +181,12 @@ var ERROR_TAG = {
170
181
  UnknownDevice: "UnknownDeviceError",
171
182
  DeviceSessionRefresher: "DeviceSessionRefresherError",
172
183
  DeviceNotInitialized: "DeviceNotInitializedError",
173
- OpeningConnection: "OpeningConnectionError",
184
+ // DMK's class is named OpeningConnectionError but its `_tag` reads
185
+ // "ConnectionOpeningError" — and its typings widen `_tag` to `string`, which
186
+ // is why the class name looked authoritative. Both spellings are kept so a
187
+ // DMK version that aligns them does not silently drop back to UnknownError.
188
+ OpeningConnection: "ConnectionOpeningError",
189
+ OpeningConnectionLegacy: "OpeningConnectionError",
174
190
  DeviceDisconnectedBeforeSendingApdu: "DeviceDisconnectedBeforeSendingApdu",
175
191
  DeviceDisconnectedWhileSending: "DeviceDisconnectedWhileSendingError",
176
192
  Disconnect: "DisconnectError",
@@ -183,7 +199,18 @@ var ERROR_TAG = {
183
199
  // DMK remote-network failures (manager-api HTTP / secure-channel WS).
184
200
  WebSocketConnection: "WebSocketConnectionError",
185
201
  HttpFetch: "FetchError",
186
- InvalidFirmwareMetadataResponse: "InvalidGetFirmwareMetadataResponseError"
202
+ NetworkDA: "NetworkDAError",
203
+ InvalidFirmwareMetadataResponse: "InvalidGetFirmwareMetadataResponseError",
204
+ ApplicationsMetadataTask: "GetApplicationsMetadataTaskError",
205
+ // DMK OS/secure-channel device actions. `SecureChannelError` is the residual
206
+ // bucket left by SecureChannelError.mapInstallDAErrors() after the locked /
207
+ // refused / already-installed / OOM cases have been split out, so it means
208
+ // "the relay itself broke", not "the device answered".
209
+ SecureChannel: "SecureChannelError",
210
+ RefusedByUserDA: "RefusedByUserDAError",
211
+ AppAlreadyInstalledDA: "AppAlreadyInstalledDAError",
212
+ OutOfMemoryDA: "OutOfMemoryDAError",
213
+ DeviceNotOnboarded: "DeviceNotOnboardedError"
187
214
  };
188
215
  function isDeviceLockedError(err) {
189
216
  if (!err || typeof err !== "object") return false;
@@ -213,7 +240,6 @@ function isBlePairingFailureError(err) {
213
240
  return false;
214
241
  }
215
242
  var CONNECTION_LEVEL_TAGS = /* @__PURE__ */ new Set([
216
- ERROR_TAG.DeviceLocked,
217
243
  ERROR_TAG.DeviceNotAdvertising,
218
244
  ERROR_TAG.BlePairingTimeout,
219
245
  ERROR_TAG.BleGattBondingFailed,
@@ -225,6 +251,7 @@ var CONNECTION_LEVEL_TAGS = /* @__PURE__ */ new Set([
225
251
  ERROR_TAG.DeviceSessionRefresher,
226
252
  ERROR_TAG.DeviceNotInitialized,
227
253
  ERROR_TAG.OpeningConnection,
254
+ ERROR_TAG.OpeningConnectionLegacy,
228
255
  ERROR_TAG.DeviceDisconnectedBeforeSendingApdu,
229
256
  ERROR_TAG.DeviceDisconnectedWhileSending,
230
257
  ERROR_TAG.Disconnect,
@@ -239,7 +266,10 @@ var DEVICE_NOT_FOUND_TAGS = /* @__PURE__ */ new Set([
239
266
  // Map to DeviceNotFound so non-BLE-direct paths get a sensible error code.
240
267
  ERROR_TAG.DeviceNotInDiscoveryCache
241
268
  ]);
242
- var DEVICE_BUSY_TAGS = /* @__PURE__ */ new Set([ERROR_TAG.OpeningConnection]);
269
+ var DEVICE_BUSY_TAGS = /* @__PURE__ */ new Set([
270
+ ERROR_TAG.OpeningConnection,
271
+ ERROR_TAG.OpeningConnectionLegacy
272
+ ]);
243
273
  var DEVICE_DISCONNECTED_TAGS = /* @__PURE__ */ new Set([
244
274
  ERROR_TAG.DeviceNotRecognized,
245
275
  ERROR_TAG.DeviceSessionNotFound,
@@ -262,6 +292,13 @@ function isConnectionLevelError(err) {
262
292
  function isKnownConnectionTag(tag) {
263
293
  return typeof tag === "string" && CONNECTION_LEVEL_TAGS.has(tag);
264
294
  }
295
+ var CONNECTION_OPENING_TAGS = /* @__PURE__ */ new Set([
296
+ ERROR_TAG.OpeningConnection,
297
+ ERROR_TAG.OpeningConnectionLegacy
298
+ ]);
299
+ function isConnectionOpeningTag(tag) {
300
+ return typeof tag === "string" && CONNECTION_OPENING_TAGS.has(tag);
301
+ }
265
302
  function hasStatusCode(err, codeSet) {
266
303
  if (!err || typeof err !== "object") return false;
267
304
  const e = err;
@@ -297,6 +334,14 @@ function hasInvalidArgumentCode(err) {
297
334
  if (e.error != null && e._tag && hasInvalidArgumentCode(e.error)) return true;
298
335
  return false;
299
336
  }
337
+ function hasErrorTag(err, tags) {
338
+ if (!err || typeof err !== "object") return false;
339
+ const e = err;
340
+ if (typeof e._tag === "string" && tags.has(e._tag)) return true;
341
+ if (e.originalError != null && hasErrorTag(e.originalError, tags)) return true;
342
+ if (e.error != null && e._tag && hasErrorTag(e.error, tags)) return true;
343
+ return false;
344
+ }
300
345
  function isDeviceNotFoundError(err) {
301
346
  if (!err || typeof err !== "object") return false;
302
347
  const tag = err._tag;
@@ -315,10 +360,14 @@ function isDeviceBusyError(err) {
315
360
  if (e.error != null && e._tag && isDeviceBusyError(e.error)) return true;
316
361
  return false;
317
362
  }
363
+ var USER_REJECTED_TAGS = /* @__PURE__ */ new Set([
364
+ ERROR_TAG.UserRefusedOnDevice,
365
+ ERROR_TAG.RefusedByUserDA
366
+ ]);
318
367
  function isUserRejectedError(err) {
319
368
  if (!err || typeof err !== "object") return false;
320
369
  const e = err;
321
- if (e._tag === ERROR_TAG.UserRefusedOnDevice) return true;
370
+ if (hasErrorTag(err, USER_REJECTED_TAGS)) return true;
322
371
  if (typeof e.message === "string" && /denied|rejected|refused/i.test(e.message)) return true;
323
372
  if (hasStatusCode(err, USER_REJECTED_CODES)) return true;
324
373
  return false;
@@ -349,15 +398,37 @@ function isAppNotInstalledError(err) {
349
398
  if (hasStatusCode(err, APP_NOT_INSTALLED_CODES)) return true;
350
399
  return false;
351
400
  }
401
+ var OUT_OF_MEMORY_TAGS = /* @__PURE__ */ new Set([ERROR_TAG.OutOfMemoryDA]);
352
402
  function isOutOfMemoryError(err) {
353
- if (!err || typeof err !== "object") return false;
354
- const e = err;
355
- return e._tag === "OutOfMemoryDAError";
403
+ return hasErrorTag(err, OUT_OF_MEMORY_TAGS);
404
+ }
405
+ var APP_ALREADY_INSTALLED_TAGS = /* @__PURE__ */ new Set([
406
+ ERROR_TAG.AppAlreadyInstalledDA
407
+ ]);
408
+ function isAppAlreadyInstalledError(err) {
409
+ return hasErrorTag(err, APP_ALREADY_INSTALLED_TAGS);
410
+ }
411
+ var DEVICE_NOT_ONBOARDED_TAGS = /* @__PURE__ */ new Set([
412
+ ERROR_TAG.DeviceNotOnboarded
413
+ ]);
414
+ function isDeviceNotOnboardedError(err) {
415
+ return hasErrorTag(err, DEVICE_NOT_ONBOARDED_TAGS);
416
+ }
417
+ var FIRMWARE_METADATA_TAGS = /* @__PURE__ */ new Set([
418
+ ERROR_TAG.InvalidFirmwareMetadataResponse,
419
+ ERROR_TAG.ApplicationsMetadataTask
420
+ ]);
421
+ function isFirmwareMetadataError(err) {
422
+ return hasErrorTag(err, FIRMWARE_METADATA_TAGS);
423
+ }
424
+ var SECURE_CHANNEL_TAGS = /* @__PURE__ */ new Set([ERROR_TAG.SecureChannel]);
425
+ function isSecureChannelError(err) {
426
+ return hasErrorTag(err, SECURE_CHANNEL_TAGS);
356
427
  }
357
428
  var NETWORK_ERROR_TAGS = /* @__PURE__ */ new Set([
358
429
  ERROR_TAG.WebSocketConnection,
359
430
  ERROR_TAG.HttpFetch,
360
- ERROR_TAG.InvalidFirmwareMetadataResponse
431
+ ERROR_TAG.NetworkDA
361
432
  ]);
362
433
  function isNetworkError(err) {
363
434
  if (!err || typeof err !== "object") return false;
@@ -407,23 +478,34 @@ function isTransportStuck(err) {
407
478
  function isStuckAppStateError(err) {
408
479
  return isAppStuckByApdu(err) || isTransportStuck(err);
409
480
  }
481
+ var DMK_PLACEHOLDER_MESSAGE = "Unknown error.";
482
+ function nestedErrorMessage(nested) {
483
+ if (!nested || typeof nested !== "object") return void 0;
484
+ const { message } = nested;
485
+ if (typeof message !== "string") return void 0;
486
+ const trimmed = message.trim();
487
+ if (!trimmed || trimmed === DMK_PLACEHOLDER_MESSAGE) return void 0;
488
+ return message;
489
+ }
410
490
  function mapLedgerError(err, opts) {
411
491
  let originalMessage = "Unknown Ledger error";
412
492
  if (err instanceof Error) {
413
493
  originalMessage = err.message;
414
494
  } else if (err && typeof err === "object") {
415
495
  const e = err;
416
- originalMessage = String(e.message ?? e._tag ?? e.type ?? JSON.stringify(err));
496
+ originalMessage = String(
497
+ e.message ?? nestedErrorMessage(e.originalError) ?? e._tag ?? e.type ?? JSON.stringify(err)
498
+ );
417
499
  }
418
500
  let code;
419
501
  if (isDeviceLockedError(err)) {
420
502
  code = HardwareErrorCode.DeviceLocked;
421
503
  } else if (isDeviceNotAdvertisingError(err) || isDeviceNotFoundError(err)) {
422
504
  code = HardwareErrorCode.DeviceNotFound;
423
- } else if (isDeviceBusyError(err)) {
424
- code = HardwareErrorCode.DeviceBusy;
425
505
  } else if (isBlePairingFailureError(err)) {
426
506
  code = HardwareErrorCode.BlePairingTimeout;
507
+ } else if (isDeviceBusyError(err)) {
508
+ code = HardwareErrorCode.DeviceBusy;
427
509
  } else if (isUserAbortedError(err)) {
428
510
  code = HardwareErrorCode.UserAborted;
429
511
  } else if (isUserRejectedError(err)) {
@@ -432,10 +514,18 @@ function mapLedgerError(err, opts) {
432
514
  code = HardwareErrorCode.WrongApp;
433
515
  } else if (isAppNotInstalledError(err)) {
434
516
  code = HardwareErrorCode.AppNotInstalled;
517
+ } else if (isAppAlreadyInstalledError(err)) {
518
+ code = HardwareErrorCode.AppAlreadyInstalled;
435
519
  } else if (isOutOfMemoryError(err)) {
436
520
  code = HardwareErrorCode.DeviceOutOfMemory;
521
+ } else if (isDeviceNotOnboardedError(err)) {
522
+ code = HardwareErrorCode.DeviceNotInitialized;
523
+ } else if (isFirmwareMetadataError(err)) {
524
+ code = HardwareErrorCode.LedgerFirmwareMetadataError;
437
525
  } else if (isNetworkError(err)) {
438
526
  code = HardwareErrorCode.NetworkError;
527
+ } else if (isSecureChannelError(err)) {
528
+ code = HardwareErrorCode.LedgerSecureChannelError;
439
529
  } else if (isDeviceDisconnectedError(err)) {
440
530
  code = HardwareErrorCode.DeviceDisconnected;
441
531
  } else if (isTimeoutError(err)) {
@@ -448,11 +538,32 @@ function mapLedgerError(err, opts) {
448
538
  }
449
539
  const errAppName = err && typeof err === "object" ? err.appName : void 0;
450
540
  const appName = errAppName ?? opts?.defaultAppName;
451
- return { code, message: enrichErrorMessage(code, originalMessage), appName };
541
+ return {
542
+ code,
543
+ message: enrichErrorMessage(code, originalMessage),
544
+ origin: defaultOriginForCode(code),
545
+ appName
546
+ };
547
+ }
548
+
549
+ // src/utils/queueKey.ts
550
+ var LEDGER_DEFAULT_QUEUE_KEY = "__ledger_default__";
551
+ function ledgerQueueKey({
552
+ operationId,
553
+ connectId
554
+ }) {
555
+ return (operationId ?? connectId) || LEDGER_DEFAULT_QUEUE_KEY;
452
556
  }
453
557
 
454
558
  // src/adapter/methods/allNetworkGetAddress.ts
455
- import { HardwareErrorCode as HardwareErrorCode2, failure, runAllNetworkGetAddress } from "@onekeyfe/hwk-adapter-core";
559
+ import {
560
+ HardwareErrorCode as HardwareErrorCode2,
561
+ failure,
562
+ isConnectionLost,
563
+ isUserRefusal,
564
+ resolveHardwareOperationTarget,
565
+ runAllNetworkGetAddress
566
+ } from "@onekeyfe/hwk-adapter-core";
456
567
 
457
568
  // src/utils/sdkEventBus.ts
458
569
  var listeners = /* @__PURE__ */ new Set();
@@ -509,65 +620,102 @@ var LEDGER_BTC_NETWORK_COIN_MAP = {
509
620
  var LEDGER_UNSUPPORTED_ALLNETWORK_NETWORKS = /* @__PURE__ */ new Set(["doge", "dogecoin"]);
510
621
  function createAllNetworkGetAddress({
511
622
  callChain,
512
- getChainFingerprint
623
+ getChainFingerprint,
624
+ retainOperation,
625
+ errorToFailure,
626
+ createCancelScope
513
627
  }) {
514
628
  return async function allNetworkGetAddress(connectId, _deviceId, params) {
515
- debugLog("[LedgerAdapter][REQ]", { method: "allNetworkGetAddress", connectId, params });
629
+ debugLog("[LedgerAdapter][REQ]", {
630
+ method: "allNetworkGetAddress",
631
+ connectId,
632
+ itemCount: params.bundle.length
633
+ });
634
+ const target = resolveHardwareOperationTarget(connectId, params.operationId, "ledger");
635
+ if (!target.success) return target;
636
+ const effectiveTargetId = target.payload.targetId ?? "";
637
+ let releaseOperationRetention;
638
+ try {
639
+ releaseOperationRetention = target.payload.operationId ? retainOperation(target.payload.operationId) : void 0;
640
+ } catch (error) {
641
+ return errorToFailure(error);
642
+ }
643
+ const cancelScope = createCancelScope(
644
+ ledgerQueueKey({ operationId: target.payload.operationId, connectId: effectiveTargetId })
645
+ );
516
646
  const installContext = {};
517
647
  const commonParams = {
518
- autoInstallApp: params.autoInstallApp
648
+ autoInstallApp: params.autoInstallApp,
649
+ operationId: target.payload.operationId,
650
+ knownConnections: params.knownConnections,
651
+ extra: params.extra,
652
+ allowDeviceSelection: params.allowDeviceSelection
519
653
  };
520
654
  const chainFingerprints = /* @__PURE__ */ new Map();
521
- const result = await runAllNetworkGetAddress({
522
- connectId,
523
- deviceId: _deviceId,
524
- params,
525
- normalizeItem: normalizeLedgerAllNetworkItem,
526
- buildUnsupportedNetworkResponse: (item) => isUnsupportedLedgerAllNetworkNetwork(item) ? buildUnsupportedNetworkResponse(item) : void 0,
527
- callItem: async ({ method, chain, item }) => {
528
- const itemDeviceId = getItemDeviceId(item) ?? chainFingerprints.get(chain) ?? "";
529
- return callAllNetworkMethod(
530
- callChain,
531
- connectId,
532
- itemDeviceId,
533
- method,
655
+ try {
656
+ const result = await runAllNetworkGetAddress({
657
+ connectId: effectiveTargetId,
658
+ deviceId: _deviceId,
659
+ params,
660
+ normalizeItem: normalizeLedgerAllNetworkItem,
661
+ buildUnsupportedNetworkResponse: (item) => isUnsupportedLedgerAllNetworkNetwork(item) ? buildUnsupportedNetworkResponse(item) : void 0,
662
+ callItem: async ({ method, chain, item }) => {
663
+ if (cancelScope.signal.aborted) return buildCancelledFailure(cancelScope.signal);
664
+ const itemDeviceId = getItemDeviceId(item) ?? chainFingerprints.get(chain) ?? "";
665
+ return callAllNetworkMethod(
666
+ callChain,
667
+ effectiveTargetId,
668
+ itemDeviceId,
669
+ method,
670
+ item,
671
+ commonParams,
672
+ installContext
673
+ );
674
+ },
675
+ attachIdentity: async ({ item, chain, payload }) => attachLedgerIdentity(
676
+ getChainFingerprint,
677
+ effectiveTargetId,
534
678
  item,
535
- commonParams,
536
- installContext
537
- );
538
- },
539
- attachIdentity: async ({ item, chain, payload }) => attachLedgerIdentity(
540
- getChainFingerprint,
541
- connectId,
542
- item,
543
- chain,
544
- payload,
545
- chainFingerprints
546
- ),
547
- shouldAbortBundle: isTopLevelAllNetworkFailure,
548
- buildTopLevelFailure: (response) => {
549
- const code = response.payload?.code ?? HardwareErrorCode2.DeviceMismatch;
550
- return failure(
551
- code,
552
- response.payload?.error ?? "All-network get-address aborted",
553
- response.payload?.params
554
- );
555
- }
556
- });
557
- debugLog("[LedgerAdapter][RES]", {
558
- method: "allNetworkGetAddress",
559
- success: result.success,
560
- payload: result
561
- });
562
- return result;
679
+ chain,
680
+ payload,
681
+ chainFingerprints,
682
+ installContext,
683
+ cancelScope.signal
684
+ ),
685
+ shouldAbortBundle: isTopLevelAllNetworkFailure,
686
+ buildTopLevelFailure: (response) => {
687
+ const code = response.payload?.code ?? HardwareErrorCode2.DeviceMismatch;
688
+ return failure(
689
+ code,
690
+ response.payload?.error ?? "All-network get-address aborted",
691
+ response.payload?.params
692
+ );
693
+ }
694
+ });
695
+ debugLog("[LedgerAdapter][RES]", {
696
+ method: "allNetworkGetAddress",
697
+ success: result.success,
698
+ payload: result
699
+ });
700
+ return result;
701
+ } finally {
702
+ cancelScope.release();
703
+ releaseOperationRetention?.();
704
+ }
563
705
  };
564
706
  }
707
+ function buildCancelledFailure(signal) {
708
+ const reason = signal.reason;
709
+ const code = typeof reason?.code === "number" ? reason.code : HardwareErrorCode2.UserAborted;
710
+ const message = typeof reason?.message === "string" ? reason.message : "";
711
+ return failure(code, message || "All-network get-address cancelled");
712
+ }
565
713
  function isTopLevelAllNetworkFailure(response) {
566
714
  if (response.success) {
567
715
  return false;
568
716
  }
569
717
  const code = response.payload?.code;
570
- return code === HardwareErrorCode2.DeviceMismatch || code === HardwareErrorCode2.UserAborted || code === HardwareErrorCode2.UserRejected;
718
+ return code === HardwareErrorCode2.DeviceMismatch || isUserRefusal(code) || isConnectionLost(code);
571
719
  }
572
720
  function getItemDeviceId(item) {
573
721
  const { deviceId } = item;
@@ -597,8 +745,18 @@ function normalizeLedgerAllNetworkItem(method, item) {
597
745
  const coin = LEDGER_BTC_NETWORK_COIN_MAP[item.network];
598
746
  return coin ? { ...item, coin } : item;
599
747
  }
600
- async function attachLedgerIdentity(getChainFingerprint, connectId, item, chain, payload, chainFingerprints) {
601
- const fingerprint = getItemDeviceId(item) || chainFingerprints.get(chain) || await bootstrapChainFingerprint(getChainFingerprint, connectId, chain);
748
+ async function attachLedgerIdentity(getChainFingerprint, connectId, item, chain, payload, chainFingerprints, context, cancelSignal) {
749
+ const knownFingerprint = getItemDeviceId(item) || chainFingerprints.get(chain) || "";
750
+ if (!knownFingerprint && cancelSignal.aborted) {
751
+ const cancelled = buildCancelledFailure(cancelSignal);
752
+ return { ...item, success: false, payload: cancelled.payload };
753
+ }
754
+ const fingerprint = knownFingerprint || await bootstrapChainFingerprint(
755
+ getChainFingerprint,
756
+ context.connection?.connectId ?? connectId,
757
+ chain,
758
+ context
759
+ );
602
760
  if (!fingerprint) {
603
761
  return buildFingerprintBootstrapFailure(item, chain);
604
762
  }
@@ -619,8 +777,8 @@ async function attachLedgerIdentity(getChainFingerprint, connectId, item, chain,
619
777
  }
620
778
  };
621
779
  }
622
- async function bootstrapChainFingerprint(getChainFingerprint, connectId, chain) {
623
- const response = await getChainFingerprint(connectId, chain);
780
+ async function bootstrapChainFingerprint(getChainFingerprint, connectId, chain, context) {
781
+ const response = await getChainFingerprint(connectId, chain, context);
624
782
  return response.success ? response.payload : "";
625
783
  }
626
784
  function buildFingerprintBootstrapFailure(item, chain) {
@@ -724,9 +882,32 @@ function btcAccountIndexFromPath(path) {
724
882
  var _LedgerAdapter = class _LedgerAdapter {
725
883
  constructor(connector, options) {
726
884
  this.vendor = "ledger";
885
+ // A cancel releases the DMK device action and its intent-queue slot, so the
886
+ // next call is not blocked behind it. It still cannot retract a confirmation
887
+ // screen the device is already showing.
888
+ this.cancelCapability = "stops-waiting";
727
889
  this.emitter = new TypedEventEmitter();
890
+ this._operations = new OperationRegistry({
891
+ vendor: "ledger",
892
+ onEnded: (operation, reason) => {
893
+ const binding = this._pendingOperationBindings.get(operation.operationId);
894
+ if (binding?.selectedConnection?.requestId === this._bindingSelectionRequestId) {
895
+ this._finishBleBinding("cancelled");
896
+ }
897
+ this._pendingOperationBindings.delete(operation.operationId);
898
+ this.emitter.emit(SDK.OPERATION_ENDED, {
899
+ type: SDK.OPERATION_ENDED,
900
+ payload: { operationId: operation.operationId, reason }
901
+ });
902
+ if (reason === "timeout") {
903
+ void this._releaseOperationConnection(operation);
904
+ }
905
+ }
906
+ });
728
907
  this._discoveredDevices = /* @__PURE__ */ new Map();
729
908
  this._sessions = /* @__PURE__ */ new Map();
909
+ this._pendingOperationBindings = /* @__PURE__ */ new Map();
910
+ this._verifiedBleReconnectTargets = /* @__PURE__ */ new Map();
730
911
  this._uiRegistry = new UiRequestRegistry();
731
912
  // BTC App rejects account index >= 100 unless display=true. Cached per
732
913
  // adapter instance: first 100+ path asks the user once via UI request,
@@ -740,10 +921,31 @@ var _LedgerAdapter = class _LedgerAdapter {
740
921
  this._deviceAuthenticityQueueTail = Promise.resolve();
741
922
  // Shared across concurrent callers — only `cancel()` aborts.
742
923
  this._doConnectAbortController = null;
924
+ this._unsettledConnectorOperations = /* @__PURE__ */ new Map();
925
+ this._connectorIdleWaiters = /* @__PURE__ */ new Set();
926
+ this._resetPromise = null;
927
+ this._stateGeneration = 0;
928
+ this._connectorTeardownTail = Promise.resolve();
929
+ this._pendingConnectorTeardowns = 0;
930
+ this._activeOperationJobs = /* @__PURE__ */ new Set();
931
+ this._pendingOperationDisconnects = /* @__PURE__ */ new Set();
743
932
  this._installProgressLastEmittedValue = -Infinity;
744
933
  this.allNetworkGetAddress = createAllNetworkGetAddress({
745
934
  callChain: this.callChain.bind(this),
746
- getChainFingerprint: (connectId, chain) => this.getChainFingerprint(connectId, "", chain)
935
+ getChainFingerprint: async (connectId, chain, context) => {
936
+ try {
937
+ const fingerprint = await this._computeChainFingerprint(
938
+ chain,
939
+ (method, params) => this.connectorCall(connectId, method, params, void 0, void 0, void 0, context)
940
+ );
941
+ return success(fingerprint);
942
+ } catch (error) {
943
+ return this.errorToFailure(error);
944
+ }
945
+ },
946
+ retainOperation: (operationId) => this._operations.retain(operationId),
947
+ errorToFailure: (error) => this.errorToFailure(error),
948
+ createCancelScope: (queueKey) => this._jobQueue.createCancelScope(queueKey)
747
949
  });
748
950
  // ---------------------------------------------------------------------------
749
951
  // Private helpers
@@ -753,7 +955,7 @@ var _LedgerAdapter = class _LedgerAdapter {
753
955
  *
754
956
  * - If a session already exists for the given connectId, reuse it.
755
957
  * - If ANY session exists (Ledger IDs are ephemeral), reuse it.
756
- * - Otherwise: search → exactly 1 USB device auto-connects; multiple or none throws.
958
+ * - Otherwise: search → one USB device auto-connects; multiple asks the host to choose.
757
959
  */
758
960
  // Mutex for ensureConnected — prevents concurrent calls from establishing duplicate connections
759
961
  this._connectingPromise = null;
@@ -770,6 +972,12 @@ var _LedgerAdapter = class _LedgerAdapter {
770
972
  });
771
973
  };
772
974
  this.deviceDisconnectHandler = (data) => {
975
+ const activeOperation = this._operations.findActiveByConnectionKey(data.connectId);
976
+ if (activeOperation && this._activeOperationJobs.has(activeOperation.operationId)) {
977
+ this._pendingOperationDisconnects.add(activeOperation.operationId);
978
+ } else {
979
+ this._operations.endByConnectionKey(data.connectId, "disconnect");
980
+ }
773
981
  this._discoveredDevices.delete(data.connectId);
774
982
  this._sessions.delete(data.connectId);
775
983
  this.emitter.emit(DEVICE.DISCONNECT, {
@@ -826,11 +1034,28 @@ var _LedgerAdapter = class _LedgerAdapter {
826
1034
  this._jobQueue = new DeviceJobQueue();
827
1035
  this.registerEventListeners();
828
1036
  }
1037
+ _finishBleBinding(status) {
1038
+ const selectionRequestId = this._bindingSelectionRequestId;
1039
+ this._bindingSelectionRequestId = void 0;
1040
+ if (selectionRequestId)
1041
+ this.emitter.emit(UI_REQUEST.DEVICE_BINDING_STATUS, {
1042
+ type: UI_REQUEST.DEVICE_BINDING_STATUS,
1043
+ payload: { selectionRequestId, status }
1044
+ });
1045
+ }
1046
+ _isBleConnection() {
1047
+ return isLedgerBleConnectionType(this._activeConnectionType ?? this.connector.connectionType);
1048
+ }
829
1049
  // Transport
830
1050
  get activeTransport() {
831
- return isLedgerBleConnectionType(this.connector.connectionType) ? "ble" : "hid";
1051
+ return this._isBleConnection() ? "ble" : "hid";
832
1052
  }
833
1053
  getAvailableTransports() {
1054
+ if (this.connector.availableTransports) {
1055
+ return this.connector.availableTransports.map(
1056
+ (transport) => transport === "ble" ? "ble" : "hid"
1057
+ );
1058
+ }
834
1059
  return this.activeTransport ? [this.activeTransport] : [];
835
1060
  }
836
1061
  // Connector is bound at construction; switching requires a new adapter.
@@ -847,21 +1072,39 @@ var _LedgerAdapter = class _LedgerAdapter {
847
1072
  * The next operation will re-discover and re-connect automatically.
848
1073
  */
849
1074
  resetState() {
1075
+ void this._resetStateAndDisconnectSessions();
1076
+ }
1077
+ _resetStateAndDisconnectSessions() {
1078
+ if (this._resetPromise) return this._resetPromise;
1079
+ const sessionIds = new Set(this._sessions.values());
1080
+ this._stateGeneration += 1;
1081
+ this._finishBleBinding("cancelled");
1082
+ this._operations.endAll("runtime-reset");
1083
+ this._doConnectAbortController?.abort();
850
1084
  this._discoveredDevices.clear();
851
1085
  this._sessions.clear();
1086
+ this._verifiedBleReconnectTargets.clear();
852
1087
  this._connectingPromise = null;
1088
+ this._doConnectAbortController = null;
853
1089
  this._uiRegistry.reset();
854
1090
  this._jobQueue.clear();
855
1091
  this._btcHighIndexConfirmedThisSession = false;
1092
+ const resetPromise = this._runConnectorTeardown(async () => {
1093
+ for (const sessionId of sessionIds) {
1094
+ await this.connector.disconnect(sessionId).catch(() => void 0);
1095
+ }
1096
+ });
1097
+ this._resetPromise = resetPromise;
1098
+ return resetPromise.finally(() => {
1099
+ if (this._resetPromise === resetPromise) {
1100
+ this._resetPromise = null;
1101
+ }
1102
+ });
856
1103
  }
857
1104
  async dispose() {
858
- this._uiRegistry.reset();
859
- this._jobQueue.clear();
1105
+ await this._resetStateAndDisconnectSessions();
860
1106
  this.unregisterEventListeners();
861
1107
  this.connector.reset();
862
- this._discoveredDevices.clear();
863
- this._sessions.clear();
864
- this._btcHighIndexConfirmedThisSession = false;
865
1108
  this.emitter.removeAllListeners();
866
1109
  }
867
1110
  uiResponse(response) {
@@ -871,17 +1114,32 @@ var _LedgerAdapter = class _LedgerAdapter {
871
1114
  // Device management
872
1115
  // ---------------------------------------------------------------------------
873
1116
  async searchDevices(options) {
1117
+ return this._searchDevices(options);
1118
+ }
1119
+ async _searchDevices(options, signal) {
874
1120
  debugLog("[LedgerAdapter][REQ]", { method: "searchDevices", params: options });
875
1121
  try {
876
1122
  if (options?.resetSession) {
877
- this._doConnectAbortController?.abort();
878
- this._sessions.clear();
879
- this._connectingPromise = null;
880
- this._doConnectAbortController = null;
881
- this._btcHighIndexConfirmedThisSession = false;
1123
+ await this._resetStateAndDisconnectSessions();
1124
+ } else {
1125
+ await this._connectorTeardownTail;
1126
+ }
1127
+ await this._ensureDevicePermission(void 0, void 0, signal);
1128
+ if (signal) _LedgerAdapter._throwIfAborted(signal);
1129
+ const stateGeneration = this._stateGeneration;
1130
+ const devices = await this.connector.searchDevices(
1131
+ options?.transportType ? {
1132
+ transportType: options.transportType,
1133
+ waitForAll: options.waitForAllTransports
1134
+ } : void 0
1135
+ );
1136
+ if (signal) _LedgerAdapter._throwIfAborted(signal);
1137
+ if (stateGeneration !== this._stateGeneration) {
1138
+ throw createHwkError({
1139
+ code: HardwareErrorCode3.UserAborted,
1140
+ message: "Ledger discovery was reset"
1141
+ });
882
1142
  }
883
- await this._ensureDevicePermission();
884
- const devices = await this.connector.searchDevices();
885
1143
  this._discoveredDevices.clear();
886
1144
  for (const d of devices) {
887
1145
  if (d.connectId) {
@@ -889,7 +1147,7 @@ var _LedgerAdapter = class _LedgerAdapter {
889
1147
  }
890
1148
  }
891
1149
  if (this._discoveredDevices.size === 0) {
892
- await this._ensureDevicePermission();
1150
+ await this._ensureDevicePermission(void 0, void 0, signal);
893
1151
  }
894
1152
  const result = Array.from(this._discoveredDevices.values());
895
1153
  debugLog("[LedgerAdapter][RES]", {
@@ -908,41 +1166,287 @@ var _LedgerAdapter = class _LedgerAdapter {
908
1166
  throw err;
909
1167
  }
910
1168
  }
1169
+ async searchDeviceTargets(options) {
1170
+ const devices = await this.searchDevices(options);
1171
+ return devices.map((device) => ({
1172
+ searchTargetId: device.connectId,
1173
+ searchTargetReusePolicy: resolveSearchTargetReusePolicy(device),
1174
+ vendor: "ledger",
1175
+ connectionType: device.connectionType,
1176
+ kind: "physical",
1177
+ label: device.label,
1178
+ model: device.model,
1179
+ modelName: device.modelName,
1180
+ serialNumber: device.serialNumber
1181
+ }));
1182
+ }
1183
+ async listConnectionTargets(options) {
1184
+ const targets = await this.searchDeviceTargets(options);
1185
+ return targets.map(({ searchTargetId, ...target }) => ({
1186
+ ...target,
1187
+ targetId: searchTargetId
1188
+ }));
1189
+ }
911
1190
  // USB single-session invariant: evict all sessions, best-effort (see connectDevice).
912
- async _evictAllSessions() {
1191
+ async _evictAllSessions(preserveOperationId) {
1192
+ this._operations.endAll("explicit", preserveOperationId);
913
1193
  if (this._sessions.size === 0) return;
914
1194
  const stale = [...this._sessions.values()];
915
1195
  this._sessions.clear();
916
- for (const sid of stale) {
917
- try {
918
- await this.connector.disconnect(sid);
919
- } catch {
1196
+ await this._runConnectorTeardown(async () => {
1197
+ for (const sid of stale) {
1198
+ try {
1199
+ await this.connector.disconnect(sid);
1200
+ } catch {
1201
+ }
920
1202
  }
921
- }
1203
+ });
922
1204
  }
923
1205
  static _createDeviceBusyError(method) {
924
1206
  return Object.assign(new Error(`Ledger device is busy while calling ${method}`), {
925
1207
  code: HardwareErrorCode3.DeviceBusy
926
1208
  });
927
1209
  }
928
- async connectDevice(connectId) {
1210
+ async connectDevice(searchTargetId) {
1211
+ try {
1212
+ return await this._jobQueue.enqueue(
1213
+ searchTargetId || "__ledger_connect__",
1214
+ async () => {
1215
+ const connected = await this._connectTarget(searchTargetId);
1216
+ if (!connected.success) return connected;
1217
+ return this._createOperation(searchTargetId, connected.payload);
1218
+ },
1219
+ {
1220
+ label: "connectDevice",
1221
+ rejectIfBusy: true,
1222
+ busyError: _LedgerAdapter._createDeviceBusyError("connectDevice")
1223
+ }
1224
+ );
1225
+ } catch (error) {
1226
+ return this.errorToFailure(error);
1227
+ }
1228
+ }
1229
+ async bindBleDevice(params) {
1230
+ if (params.identity.vendor !== "ledger" || !params.identity.value) {
1231
+ return failure2(HardwareErrorCode3.InvalidParams, "Ledger wallet identity is required");
1232
+ }
1233
+ const { chain, value: expectedFingerprint } = params.identity;
1234
+ try {
1235
+ return await this._jobQueue.enqueue(
1236
+ expectedFingerprint,
1237
+ async (signal) => {
1238
+ if (!this.getAvailableTransports().includes("ble")) {
1239
+ throw createHwkError({
1240
+ code: HardwareErrorCode3.TransportNotAvailable,
1241
+ message: "Ledger Bluetooth transport is not available"
1242
+ });
1243
+ }
1244
+ this._activeConnectionType = "ble";
1245
+ await this._ensureDevicePermission(void 0, void 0, signal);
1246
+ const attempt = {
1247
+ extra: params.extra,
1248
+ bindingReason: "manual-rebind"
1249
+ };
1250
+ try {
1251
+ for (; ; ) {
1252
+ const connectId = await this._connectFirstOrSelect(
1253
+ [],
1254
+ void 0,
1255
+ true,
1256
+ void 0,
1257
+ attempt,
1258
+ signal
1259
+ );
1260
+ const sessionId = this._sessions.get(connectId);
1261
+ if (!sessionId) {
1262
+ throw createHwkError({
1263
+ code: HardwareErrorCode3.DeviceDisconnected,
1264
+ message: "Selected Ledger connection ended"
1265
+ });
1266
+ }
1267
+ let saved = false;
1268
+ try {
1269
+ const installContext = {
1270
+ connection: { connectId, sessionId }
1271
+ };
1272
+ const fingerprint = await this._computeChainFingerprint(
1273
+ chain,
1274
+ (method, callParams) => this._runConnectorCall(
1275
+ connectId,
1276
+ method,
1277
+ callParams,
1278
+ signal,
1279
+ void 0,
1280
+ void 0,
1281
+ { autoInstallApp: true },
1282
+ installContext
1283
+ )
1284
+ );
1285
+ if (fingerprint !== expectedFingerprint) {
1286
+ attempt.rejectedConnectIds ?? (attempt.rejectedConnectIds = /* @__PURE__ */ new Set());
1287
+ attempt.rejectedConnectIds.add(connectId);
1288
+ attempt.rejectedConnectId = connectId;
1289
+ } else {
1290
+ const persisted = await this._publishVerifiedBleBinding(
1291
+ connectId,
1292
+ chain,
1293
+ fingerprint,
1294
+ attempt,
1295
+ void 0,
1296
+ signal
1297
+ );
1298
+ if (!persisted) {
1299
+ throw createHwkError({
1300
+ code: HardwareErrorCode3.UnknownError,
1301
+ message: "Bluetooth binding could not be saved",
1302
+ origin: "host"
1303
+ });
1304
+ }
1305
+ saved = true;
1306
+ return success(connectId);
1307
+ }
1308
+ } finally {
1309
+ if (!saved && this._sessions.get(connectId) === sessionId) {
1310
+ this._sessions.delete(connectId);
1311
+ const teardown = this._runConnectorTeardown(
1312
+ () => this.connector.disconnect(sessionId)
1313
+ ).catch(() => void 0);
1314
+ if (!signal.aborted) await teardown;
1315
+ }
1316
+ }
1317
+ }
1318
+ } catch (error) {
1319
+ this._finishBleBinding(signal.aborted ? "cancelled" : "failed");
1320
+ throw error;
1321
+ }
1322
+ },
1323
+ {
1324
+ label: "bindBleDevice",
1325
+ rejectIfBusy: true,
1326
+ busyError: _LedgerAdapter._createDeviceBusyError("bindBleDevice")
1327
+ }
1328
+ );
1329
+ } catch (error) {
1330
+ return this.errorToFailure(error);
1331
+ }
1332
+ }
1333
+ async acquireOperation(connectId, context) {
1334
+ try {
1335
+ return await this._jobQueue.enqueue(
1336
+ connectId || "__ledger_acquire__",
1337
+ async (signal) => {
1338
+ const transport = this._isBleConnection() ? "ble" : "usb";
1339
+ const hint = context.knownConnections?.find(
1340
+ (connection) => connection.transport === transport
1341
+ );
1342
+ const target = hint && hint.transport !== "qr" ? hint.connectId : connectId;
1343
+ await this._ensureDevicePermission(target, void 0, signal);
1344
+ const attempt = {
1345
+ ...context,
1346
+ extra: context.extra ? { ...context.extra } : void 0
1347
+ };
1348
+ try {
1349
+ const resolvedConnectId = await this.ensureConnected(
1350
+ target,
1351
+ signal,
1352
+ true,
1353
+ void 0,
1354
+ attempt
1355
+ );
1356
+ _LedgerAdapter._throwIfAborted(signal);
1357
+ const result = this._createOperation(connectId, resolvedConnectId);
1358
+ if (result.success && attempt.selectedConnection) {
1359
+ this._pendingOperationBindings.set(result.payload, attempt);
1360
+ }
1361
+ return result;
1362
+ } catch (error) {
1363
+ this._finishBleBinding(signal.aborted ? "cancelled" : "failed");
1364
+ throw error;
1365
+ }
1366
+ },
1367
+ {
1368
+ label: "acquireOperation",
1369
+ rejectIfBusy: true,
1370
+ busyError: _LedgerAdapter._createDeviceBusyError("acquireOperation")
1371
+ }
1372
+ );
1373
+ } catch (error) {
1374
+ return this.errorToFailure(error);
1375
+ }
1376
+ }
1377
+ _createOperation(searchTargetId, resolvedConnectId) {
1378
+ this._operations.endByConnectionKey(resolvedConnectId, "explicit");
1379
+ const sessionId = this._sessions.get(resolvedConnectId);
1380
+ if (sessionId) this._operations.endByConnectionKey(sessionId, "explicit");
1381
+ const connectionType = this._isBleConnection() ? "ble" : "usb";
1382
+ const device = this._discoveredDevices.get(resolvedConnectId) ?? {
1383
+ vendor: "ledger",
1384
+ model: "unknown",
1385
+ firmwareVersion: "",
1386
+ deviceId: "",
1387
+ connectId: resolvedConnectId,
1388
+ connectionType
1389
+ };
1390
+ const operation = this._operations.create({
1391
+ searchTargetId,
1392
+ connectId: resolvedConnectId,
1393
+ device,
1394
+ connectionType,
1395
+ connectionKeys: [this._sessions.get(resolvedConnectId) ?? ""]
1396
+ });
1397
+ return success(operation.operationId);
1398
+ }
1399
+ async _connectTarget(connectId, preserveOperationId, signal) {
929
1400
  debugLog("[LedgerAdapter][REQ]", { method: "connectDevice", connectId, params: { connectId } });
930
1401
  try {
931
- if (isLedgerBleConnectionType(this.connector.connectionType) && !connectId) {
1402
+ this._assertConnectorReady("connectDevice");
1403
+ const discoveredType = this._discoveredDevices.get(connectId)?.connectionType;
1404
+ if (discoveredType === "usb" || discoveredType === "ble") {
1405
+ this._activeConnectionType = discoveredType;
1406
+ }
1407
+ if (this._isBleConnection() && !connectId) {
932
1408
  throw Object.assign(new Error("Ledger BLE connectId is required."), {
933
1409
  code: HardwareErrorCode3.DeviceNotFound
934
1410
  });
935
1411
  }
936
- if (!isLedgerBleConnectionType(this.connector.connectionType)) {
937
- await this._evictAllSessions();
1412
+ await this._ensureDevicePermission(connectId, void 0, signal);
1413
+ if (signal) _LedgerAdapter._throwIfAborted(signal);
1414
+ if (this.connector.availableTransports && this.connector.availableTransports.length > 1) {
1415
+ await this._evictAllSessions(preserveOperationId);
1416
+ } else if (this._isBleConnection()) {
1417
+ const previousSessionId = this._sessions.get(connectId);
1418
+ this._operations.endByConnectionKey(connectId, "explicit", preserveOperationId);
1419
+ if (previousSessionId) {
1420
+ this._operations.endByConnectionKey(previousSessionId, "explicit", preserveOperationId);
1421
+ this._sessions.delete(connectId);
1422
+ await this.connector.disconnect(previousSessionId).catch(() => void 0);
1423
+ }
1424
+ } else {
1425
+ await this._evictAllSessions(preserveOperationId);
1426
+ }
1427
+ if (signal) _LedgerAdapter._throwIfAborted(signal);
1428
+ const stateGeneration = this._stateGeneration;
1429
+ const releaseOperation = this._retainConnectorOperation(`connect:${connectId}`);
1430
+ let session;
1431
+ try {
1432
+ session = this.connector.availableTransports?.length ? await this.connector.connect(connectId, {
1433
+ transportType: this._activeConnectionType ?? "usb"
1434
+ }) : await this.connector.connect(connectId);
1435
+ if (signal?.aborted || stateGeneration !== this._stateGeneration) {
1436
+ await this.connector.disconnect(session.sessionId).catch(() => void 0);
1437
+ throw Object.assign(new Error("Ledger connection aborted"), {
1438
+ code: HardwareErrorCode3.UserAborted
1439
+ });
1440
+ }
1441
+ } finally {
1442
+ releaseOperation();
938
1443
  }
939
- await this._ensureDevicePermission(connectId);
940
- const session = await this.connector.connect(connectId);
941
- this._sessions.set(connectId, session.sessionId);
1444
+ const resolvedConnectId = session.deviceInfo?.connectId || connectId;
1445
+ this._sessions.set(resolvedConnectId, session.sessionId);
942
1446
  if (session.deviceInfo) {
943
- this._discoveredDevices.set(connectId, session.deviceInfo);
1447
+ this._discoveredDevices.set(resolvedConnectId, session.deviceInfo);
944
1448
  }
945
- const result = success(connectId);
1449
+ const result = success(resolvedConnectId);
946
1450
  debugLog("[LedgerAdapter][RES]", { method: "connectDevice", success: true, payload: result });
947
1451
  return result;
948
1452
  } catch (err) {
@@ -955,30 +1459,60 @@ var _LedgerAdapter = class _LedgerAdapter {
955
1459
  return failureResult;
956
1460
  }
957
1461
  }
958
- async disconnectDevice(connectId) {
1462
+ async releaseOperation(operationId) {
1463
+ const operation = this._operations.find(operationId);
1464
+ if (!operation) {
1465
+ this._operations.resolve(operationId);
1466
+ return;
1467
+ }
1468
+ const endedOperation = this._operations.end(operationId, "explicit");
1469
+ if (!endedOperation) return;
1470
+ const { connectId } = operation;
959
1471
  debugLog("[LedgerAdapter][REQ]", {
960
- method: "disconnectDevice",
1472
+ method: "releaseOperation",
961
1473
  connectId,
962
1474
  params: { connectId }
963
1475
  });
964
1476
  try {
965
- const sessionId = this._sessions.get(connectId);
966
- if (sessionId) {
967
- await this.connector.disconnect(sessionId);
968
- this._sessions.delete(connectId);
969
- }
970
- debugLog("[LedgerAdapter][RES]", { method: "disconnectDevice", success: true });
1477
+ await this._releaseOperationConnection(endedOperation);
1478
+ debugLog("[LedgerAdapter][RES]", { method: "releaseOperation", success: true });
971
1479
  } catch (err) {
972
1480
  const e = err;
973
1481
  debugLog("[LedgerAdapter][RES]", {
974
- method: "disconnectDevice",
1482
+ method: "releaseOperation",
975
1483
  success: false,
976
1484
  error: { message: e?.message, _tag: e?._tag, code: e?.code ?? e?.errorCode }
977
1485
  });
978
1486
  throw err;
979
1487
  }
980
1488
  }
981
- async getDeviceInfo(connectId, deviceId) {
1489
+ async _releaseOperationConnection(operation) {
1490
+ const sessionIds = /* @__PURE__ */ new Set();
1491
+ for (const [connectId, sessionId] of this._sessions) {
1492
+ if (connectId === operation.connectId || operation.connectionKeys.includes(connectId) || operation.connectionKeys.includes(sessionId)) {
1493
+ this._sessions.delete(connectId);
1494
+ sessionIds.add(sessionId);
1495
+ }
1496
+ }
1497
+ for (const sessionId of sessionIds) {
1498
+ await this._runConnectorTeardown(
1499
+ () => this.connector.disconnect(sessionId).catch(() => void 0)
1500
+ );
1501
+ }
1502
+ }
1503
+ async _releaseLostOperationConnection(operationId) {
1504
+ const endedOperation = this._operations.end(operationId, "disconnect");
1505
+ if (!endedOperation) return;
1506
+ this._discoveredDevices.delete(endedOperation.connectId);
1507
+ await this._releaseOperationConnection(endedOperation);
1508
+ }
1509
+ async getDeviceInfo(connectIdOrOperationId, deviceId) {
1510
+ let connectId;
1511
+ try {
1512
+ connectId = isHardwareOperationId(connectIdOrOperationId) ? this._operations.resolve(connectIdOrOperationId).connectId : connectIdOrOperationId;
1513
+ } catch (error) {
1514
+ return this.errorToFailure(error);
1515
+ }
982
1516
  debugLog("[LedgerAdapter][REQ]", {
983
1517
  method: "getDeviceInfo",
984
1518
  connectId,
@@ -1016,9 +1550,6 @@ var _LedgerAdapter = class _LedgerAdapter {
1016
1550
  throw err;
1017
1551
  }
1018
1552
  }
1019
- getSupportedChains() {
1020
- return ["evm", "btc", "sol", "tron"];
1021
- }
1022
1553
  // ---------------------------------------------------------------------------
1023
1554
  // Chain call helper
1024
1555
  // ---------------------------------------------------------------------------
@@ -1053,13 +1584,21 @@ var _LedgerAdapter = class _LedgerAdapter {
1053
1584
  if (params && typeof params === "object") {
1054
1585
  const {
1055
1586
  autoInstallApp,
1587
+ operationId,
1056
1588
  passphraseState: _passphraseState,
1057
1589
  useEmptyPassphrase: _useEmptyPassphrase,
1590
+ knownConnections,
1591
+ extra,
1592
+ allowDeviceSelection,
1058
1593
  ...rest
1059
1594
  } = params;
1060
1595
  return {
1061
1596
  commonParams: {
1062
- autoInstallApp: typeof autoInstallApp === "boolean" ? autoInstallApp : void 0
1597
+ autoInstallApp: typeof autoInstallApp === "boolean" ? autoInstallApp : void 0,
1598
+ operationId: typeof operationId === "string" ? operationId : void 0,
1599
+ knownConnections,
1600
+ extra,
1601
+ allowDeviceSelection: typeof allowDeviceSelection === "boolean" ? allowDeviceSelection : void 0
1063
1602
  },
1064
1603
  rest
1065
1604
  };
@@ -1235,6 +1774,27 @@ var _LedgerAdapter = class _LedgerAdapter {
1235
1774
  );
1236
1775
  }
1237
1776
  // ---------------------------------------------------------------------------
1777
+ // Zcash chain methods (viewing key + shielded address reads; Zcash app >= 3.8.0)
1778
+ // ---------------------------------------------------------------------------
1779
+ zcashGetFullViewingKey(connectId, deviceId, params) {
1780
+ return this.callChainWithMergedParams(
1781
+ connectId,
1782
+ deviceId,
1783
+ "zcash",
1784
+ "zcashGetFullViewingKey",
1785
+ params
1786
+ );
1787
+ }
1788
+ zcashGetShieldedAddress(connectId, deviceId, params) {
1789
+ return this.callChainWithMergedParams(
1790
+ connectId,
1791
+ deviceId,
1792
+ "zcash",
1793
+ "zcashGetShieldedAddress",
1794
+ params
1795
+ );
1796
+ }
1797
+ // ---------------------------------------------------------------------------
1238
1798
  // App management — OS-level Ledger app install / list. Bypasses fingerprint
1239
1799
  // and chain-handler dispatch; installApp progress is forwarded to the adapter
1240
1800
  // emitter via 'ui-event' AppInstallProgress events.
@@ -1323,7 +1883,7 @@ var _LedgerAdapter = class _LedgerAdapter {
1323
1883
  );
1324
1884
  }
1325
1885
  await this.connector.configure({ ledgerGenuineCheckWebSocketUrl: relayUrl });
1326
- this.resetState();
1886
+ await this._resetStateAndDisconnectSessions();
1327
1887
  }
1328
1888
  const result = await this.connectorCall(connectId, "getDeviceGenuineCheck", {});
1329
1889
  if (!result.isGenuine) {
@@ -1349,10 +1909,10 @@ var _LedgerAdapter = class _LedgerAdapter {
1349
1909
  if (relayUrl) {
1350
1910
  try {
1351
1911
  await this.connector.configure?.({ ledgerGenuineCheckWebSocketUrl: void 0 });
1352
- this.resetState();
1912
+ await this._resetStateAndDisconnectSessions();
1353
1913
  } catch {
1354
1914
  this.connector.reset();
1355
- this.resetState();
1915
+ await this._resetStateAndDisconnectSessions();
1356
1916
  }
1357
1917
  }
1358
1918
  }
@@ -1364,6 +1924,20 @@ var _LedgerAdapter = class _LedgerAdapter {
1364
1924
  this.emitter.off(event, listener);
1365
1925
  }
1366
1926
  cancel(connectId) {
1927
+ const namedOperationIsLive = (id) => {
1928
+ try {
1929
+ this._operations.resolve(id);
1930
+ return true;
1931
+ } catch {
1932
+ return false;
1933
+ }
1934
+ };
1935
+ if (isHardwareOperationId(connectId) && !namedOperationIsLive(connectId)) {
1936
+ debugLog("[LedgerAdapter] cancel target already ended; nothing to cancel", {
1937
+ connectId
1938
+ });
1939
+ return;
1940
+ }
1367
1941
  const userAbortReason = Object.assign(new Error("User aborted operation"), {
1368
1942
  code: HardwareErrorCode3.UserAborted,
1369
1943
  _tag: ERROR_TAG.UserAborted
@@ -1374,14 +1948,50 @@ var _LedgerAdapter = class _LedgerAdapter {
1374
1948
  this._lastCancelReason = void 0;
1375
1949
  }
1376
1950
  }, 2e3);
1377
- this._uiRegistry.cancel();
1378
- if (connectId) {
1379
- this._jobQueue.cancelActiveAndPending(connectId, userAbortReason);
1951
+ const activeJobId = this._jobQueue.getActiveJob()?.deviceId;
1952
+ let operationId;
1953
+ if (isHardwareOperationId(connectId)) {
1954
+ operationId = connectId;
1955
+ } else if (!connectId && isHardwareOperationId(activeJobId)) {
1956
+ operationId = activeJobId;
1957
+ }
1958
+ let resolvedConnectId = connectId;
1959
+ if (operationId) {
1960
+ try {
1961
+ resolvedConnectId = this._operations.resolve(operationId).connectId;
1962
+ } catch {
1963
+ resolvedConnectId = void 0;
1964
+ }
1965
+ }
1966
+ const interactionForPhysicalId = !operationId && connectId ? this._operations.findActiveByConnectionKey(connectId) : void 0;
1967
+ const pendingOperationId = operationId ?? interactionForPhysicalId?.operationId;
1968
+ if (!connectId) {
1969
+ this._uiRegistry.cancel();
1970
+ } else if (pendingOperationId) {
1971
+ this._uiRegistry.cancel(void 0, void 0, pendingOperationId);
1972
+ }
1973
+ this._finishBleBinding("cancelled");
1974
+ if (!connectId) this._pendingOperationBindings.clear();
1975
+ else if (pendingOperationId) {
1976
+ this._pendingOperationBindings.delete(pendingOperationId);
1977
+ }
1978
+ const queueKeys = /* @__PURE__ */ new Set();
1979
+ if (connectId) queueKeys.add(ledgerQueueKey({ connectId }));
1980
+ if (pendingOperationId) queueKeys.add(ledgerQueueKey({ operationId: pendingOperationId }));
1981
+ if (queueKeys.size) {
1982
+ let cancelledAnyJob = false;
1983
+ for (const key of queueKeys) {
1984
+ cancelledAnyJob = this._jobQueue.cancelActiveAndPending(key, userAbortReason) || cancelledAnyJob;
1985
+ }
1986
+ debugLog("[LedgerAdapter] cancel routed to queue keys", {
1987
+ queueKeys: [...queueKeys],
1988
+ cancelledAnyJob
1989
+ });
1380
1990
  } else {
1381
1991
  this._jobQueue.cancelActiveAndPending(void 0, userAbortReason);
1382
1992
  }
1383
- if (connectId) {
1384
- const sessionId = this._sessions.get(connectId) ?? connectId;
1993
+ if (resolvedConnectId) {
1994
+ const sessionId = this._sessions.get(resolvedConnectId) ?? resolvedConnectId;
1385
1995
  void this.connector.cancel(sessionId);
1386
1996
  } else {
1387
1997
  for (const sid of this._sessions.values()) void this.connector.cancel(sid);
@@ -1393,44 +2003,102 @@ var _LedgerAdapter = class _LedgerAdapter {
1393
2003
  // ---------------------------------------------------------------------------
1394
2004
  // Chain fingerprint
1395
2005
  // ---------------------------------------------------------------------------
2006
+ /** A non-empty deviceId is an expected chain fingerprint, not a transport identifier. */
1396
2007
  async getChainFingerprint(connectId, deviceId, chain) {
1397
2008
  try {
1398
2009
  const fingerprint = await this._computeChainFingerprint(
1399
2010
  chain,
1400
2011
  (method, params) => this.connectorCall(connectId, method, params, void 0, deviceId)
1401
2012
  );
2013
+ if (deviceId) {
2014
+ if (fingerprint !== deviceId) {
2015
+ return failure2(
2016
+ HardwareErrorCode3.DeviceMismatch,
2017
+ formatDeviceMismatchError(deviceId, fingerprint)
2018
+ );
2019
+ }
2020
+ if (isHardwareOperationId(connectId)) {
2021
+ const operation = this._operations.resolve(connectId);
2022
+ await this._publishVerifiedBleBinding(
2023
+ operation.connectId,
2024
+ chain,
2025
+ fingerprint,
2026
+ void 0,
2027
+ connectId
2028
+ );
2029
+ }
2030
+ }
1402
2031
  return success(fingerprint);
1403
2032
  } catch (err) {
1404
2033
  debugError("[LedgerAdapter] getChainFingerprint error:", chain, err);
1405
2034
  return this.errorToFailure(err);
1406
2035
  }
1407
2036
  }
1408
- /**
1409
- * Verify fingerprint using an existing sessionId directly.
1410
- * Safe to call inside connectorCall without causing queue deadlock.
1411
- */
1412
- async _verifyDeviceFingerprintWithSession(sessionId, deviceId, chain) {
1413
- if (!deviceId) return { success: true };
2037
+ /** Discovery may select a BLE target before a later call verifies its wallet. */
2038
+ async _publishVerifiedBleBinding(connectId, chain, fingerprint, attempt, operationId, signal) {
2039
+ const binding = operationId ? this._pendingOperationBindings.get(operationId) : attempt;
2040
+ if (!this._isBleConnection() || binding?.selectedConnection?.connectId !== connectId) {
2041
+ return false;
2042
+ }
1414
2043
  try {
1415
- const fingerprint = await this._computeChainFingerprint(
1416
- chain,
1417
- (method, params) => this._callConnector(sessionId, method, params)
2044
+ const outcome = await requestSaveDeviceBinding(
2045
+ this.emitter,
2046
+ this._uiRegistry,
2047
+ {
2048
+ selectionRequestId: binding.selectedConnection.requestId,
2049
+ connection: { transport: "ble", connectId },
2050
+ identity: { vendor: "ledger", type: "chainFingerprint", chain, value: fingerprint },
2051
+ extra: binding.extra,
2052
+ operationId
2053
+ },
2054
+ signal
1418
2055
  );
1419
- if (fingerprint === deviceId) {
1420
- return { success: true };
2056
+ if (operationId) this._operations.resolve(operationId);
2057
+ if (!outcome.saved) {
2058
+ debugLog("[LedgerAdapter] BLE binding not persisted by host", {
2059
+ connectId,
2060
+ reason: outcome.reason
2061
+ });
1421
2062
  }
1422
- return { success: false, expected: deviceId, actual: fingerprint };
1423
- } catch (err) {
1424
- const mapped = mapLedgerError(err);
1425
- if (mapped.code === HardwareErrorCode3.WrongApp || mapped.code === HardwareErrorCode3.DeviceLocked) {
1426
- return { success: true };
2063
+ return outcome.saved;
2064
+ } catch (error) {
2065
+ if (operationId) {
2066
+ this._pendingOperationBindings.delete(operationId);
2067
+ this._operations.end(operationId, "explicit");
1427
2068
  }
1428
- throw err;
1429
- }
1430
- }
1431
- /**
1432
- * Compute the chain fingerprint via a caller-supplied call strategy.
1433
- *
2069
+ const sessionId = this._sessions.get(connectId);
2070
+ this._sessions.delete(connectId);
2071
+ if (sessionId) {
2072
+ const teardown = this._runConnectorTeardown(
2073
+ () => this.connector.disconnect(sessionId)
2074
+ ).catch(() => void 0);
2075
+ if (!signal?.aborted) await teardown;
2076
+ }
2077
+ throw error;
2078
+ } finally {
2079
+ if (this._bindingSelectionRequestId === binding.selectedConnection.requestId) {
2080
+ this._bindingSelectionRequestId = void 0;
2081
+ }
2082
+ if (operationId) {
2083
+ this._pendingOperationBindings.delete(operationId);
2084
+ }
2085
+ }
2086
+ }
2087
+ /** Verify on the acquired session without re-entering the job queue. */
2088
+ async _verifyDeviceFingerprintWithSession(sessionId, deviceId, chain) {
2089
+ if (!deviceId) return { success: true };
2090
+ const fingerprint = await this._computeChainFingerprint(
2091
+ chain,
2092
+ (method, params) => this._callConnector(sessionId, method, params)
2093
+ );
2094
+ if (fingerprint === deviceId) {
2095
+ return { success: true };
2096
+ }
2097
+ return { success: false, expected: deviceId, actual: fingerprint };
2098
+ }
2099
+ /**
2100
+ * Compute the chain fingerprint via a caller-supplied call strategy.
2101
+ *
1434
2102
  * Chains with a native device-side identity primitive (BTC → BIP32 master
1435
2103
  * fingerprint) short-circuit at the top and return it verbatim, so the value
1436
2104
  * stays reusable for higher-level use (BIP380 descriptors, PSBT signing).
@@ -1455,12 +2123,27 @@ var _LedgerAdapter = class _LedgerAdapter {
1455
2123
  address = (await callMethod("solGetAddress", { path, showOnDevice: false })).address;
1456
2124
  } else if (chain === "tron") {
1457
2125
  address = (await callMethod("tronGetAddress", { path, showOnDevice: false })).address;
2126
+ } else if (chain === "zcash") {
2127
+ address = (await callMethod("zcashGetShieldedAddress", { path, showOnDevice: false })).address;
1458
2128
  } else {
1459
2129
  throw new Error(`Unsupported chain for fingerprint: ${chain}`);
1460
2130
  }
1461
2131
  return deriveDeviceFingerprint(address);
1462
2132
  }
1463
2133
  // Ledger WebUSB won't expose a locked device, so we can't auto-detect unlock.
2134
+ /**
2135
+ * The operation the running device job belongs to, so a UI request raised
2136
+ * mid-call can name it. A call pinned to an operation queues under its id
2137
+ * (`ledgerQueueKey`); a call without one queues under the connectId, and the
2138
+ * live operation on that connection is still the owner. Only work with no
2139
+ * operation at all (cold start) comes back undefined.
2140
+ */
2141
+ _activeOperationId() {
2142
+ const activeJobId = this._jobQueue.getActiveJob()?.deviceId;
2143
+ if (!activeJobId) return void 0;
2144
+ if (isHardwareOperationId(activeJobId)) return activeJobId;
2145
+ return this._operations.findActiveByConnectionKey(activeJobId)?.operationId;
2146
+ }
1464
2147
  // The user must press Confirm after unlocking, which triggers a search retry.
1465
2148
  // If `signal` is provided, an abort cancels the pending UI request so the
1466
2149
  // registry slot is released and a stale RECEIVE_DEVICE_CONNECT won't land in
@@ -1469,15 +2152,18 @@ var _LedgerAdapter = class _LedgerAdapter {
1469
2152
  if (signal?.aborted) {
1470
2153
  _LedgerAdapter._throwIfAborted(signal);
1471
2154
  }
2155
+ const operationId = this._activeOperationId();
1472
2156
  const waitPromise = this._uiRegistry.wait(
1473
- UI_REQUEST.REQUEST_DEVICE_CONNECT
2157
+ UI_REQUEST.REQUEST_DEVICE_CONNECT,
2158
+ { operationId }
1474
2159
  );
1475
2160
  this.emitter.emit(UI_REQUEST.REQUEST_DEVICE_CONNECT, {
1476
2161
  type: UI_REQUEST.REQUEST_DEVICE_CONNECT,
1477
2162
  payload: {
1478
2163
  vendor: "ledger",
1479
2164
  reason: "device-not-found",
1480
- message: "Please connect and unlock your Ledger device"
2165
+ message: "Please connect and unlock your Ledger device",
2166
+ operationId
1481
2167
  }
1482
2168
  });
1483
2169
  let payload;
@@ -1538,15 +2224,18 @@ var _LedgerAdapter = class _LedgerAdapter {
1538
2224
  return { ...params, showOnDevice: true };
1539
2225
  }
1540
2226
  async _waitForBtcHighIndexConfirm(path, accountIndex) {
2227
+ const operationId = this._activeOperationId();
1541
2228
  const waitPromise = this._uiRegistry.wait(
1542
- UI_REQUEST.REQUEST_BTC_HIGH_INDEX_CONFIRM
2229
+ UI_REQUEST.REQUEST_BTC_HIGH_INDEX_CONFIRM,
2230
+ { operationId }
1543
2231
  );
1544
2232
  this.emitter.emit(UI_REQUEST.REQUEST_BTC_HIGH_INDEX_CONFIRM, {
1545
2233
  type: UI_REQUEST.REQUEST_BTC_HIGH_INDEX_CONFIRM,
1546
2234
  payload: {
1547
2235
  vendor: "ledger",
1548
2236
  path,
1549
- accountIndex
2237
+ accountIndex,
2238
+ operationId
1550
2239
  }
1551
2240
  });
1552
2241
  try {
@@ -1563,12 +2252,14 @@ var _LedgerAdapter = class _LedgerAdapter {
1563
2252
  // Ask the user whether to install a missing app (autoInstallApp flow).
1564
2253
  // Same register-then-emit ordering as the BTC high-index gate.
1565
2254
  async _waitForInstallAppConfirm(appName) {
2255
+ const operationId = this._activeOperationId();
1566
2256
  const waitPromise = this._uiRegistry.wait(
1567
- UI_REQUEST.REQUEST_INSTALL_APP
2257
+ UI_REQUEST.REQUEST_INSTALL_APP,
2258
+ { operationId }
1568
2259
  );
1569
2260
  this.emitter.emit(UI_REQUEST.REQUEST_INSTALL_APP, {
1570
2261
  type: UI_REQUEST.REQUEST_INSTALL_APP,
1571
- payload: { vendor: "ledger", appName }
2262
+ payload: { vendor: "ledger", appName, operationId }
1572
2263
  });
1573
2264
  try {
1574
2265
  const payload = await waitPromise;
@@ -1584,16 +2275,17 @@ var _LedgerAdapter = class _LedgerAdapter {
1584
2275
  // Layer 1 entry. Caller signal only races the outer awaiter; the shared
1585
2276
  // `_doConnect` runs under its own internal controller so caller A's cancel
1586
2277
  // doesn't kill caller B's await.
1587
- async ensureConnected(connectId, signal, allowUsbEphemeralFallback = false) {
2278
+ async ensureConnected(connectId, signal, allowUsbEphemeralFallback = false, preserveOperationId, context) {
1588
2279
  if (signal.aborted) _LedgerAdapter._throwIfAborted(signal);
1589
- if (isLedgerBleConnectionType(this.connector.connectionType) && !connectId) {
2280
+ this._assertConnectorReady("connectDevice");
2281
+ if (this._isBleConnection() && !connectId && !allowUsbEphemeralFallback) {
1590
2282
  throw Object.assign(new Error("Ledger BLE connectId is required."), {
1591
2283
  code: HardwareErrorCode3.DeviceNotFound
1592
2284
  });
1593
2285
  }
1594
2286
  if (connectId && this._sessions.has(connectId)) return connectId;
1595
2287
  if (!connectId && this._sessions.size > 0) {
1596
- if (!isLedgerBleConnectionType(this.connector.connectionType) && this._sessions.size > 1) {
2288
+ if (!this._isBleConnection() && this._sessions.size > 1) {
1597
2289
  throw Object.assign(
1598
2290
  new Error(
1599
2291
  "Ledger USB session invariant violated: more than one session is active. Please reconnect the device."
@@ -1603,15 +2295,24 @@ var _LedgerAdapter = class _LedgerAdapter {
1603
2295
  }
1604
2296
  return this._sessions.keys().next().value;
1605
2297
  }
1606
- if (!this._connectingPromise) {
1607
- this._doConnectAbortController = new AbortController();
1608
- const innerSignal = this._doConnectAbortController.signal;
2298
+ if (!this._connectingPromise || this._doConnectAbortController?.signal.aborted) {
2299
+ const controller = new AbortController();
2300
+ this._doConnectAbortController = controller;
2301
+ const innerSignal = controller.signal;
1609
2302
  this._connectingPromise = (async () => {
1610
2303
  try {
1611
- return await this._doConnect(innerSignal, connectId, allowUsbEphemeralFallback);
2304
+ return await this._doConnect(
2305
+ innerSignal,
2306
+ connectId,
2307
+ allowUsbEphemeralFallback,
2308
+ preserveOperationId,
2309
+ context
2310
+ );
1612
2311
  } finally {
1613
- this._connectingPromise = null;
1614
- this._doConnectAbortController = null;
2312
+ if (this._doConnectAbortController === controller) {
2313
+ this._connectingPromise = null;
2314
+ this._doConnectAbortController = null;
2315
+ }
1615
2316
  }
1616
2317
  })();
1617
2318
  }
@@ -1620,22 +2321,51 @@ var _LedgerAdapter = class _LedgerAdapter {
1620
2321
  // Layer 1 main loop — the ONLY place in SDK that emits unlock dialog.
1621
2322
  // Bounded by MAX_DOCONNECT_CONFIRMS — after N Confirms with no progress,
1622
2323
  // throw DeviceNotFound so the user is kicked out of the loop.
1623
- async _doConnect(internalSignal, targetConnectId, allowUsbEphemeralFallback = false) {
1624
- if (isLedgerBleConnectionType(this.connector.connectionType) && targetConnectId) {
1625
- try {
1626
- return await this._connectDeviceOrThrow(targetConnectId);
1627
- } catch (err) {
1628
- if (!isDeviceLockedError(err) && !isDeviceNotAdvertisingError(err) && !isDeviceDisconnectedError(err)) {
1629
- throw err;
1630
- }
1631
- this._discoveredDevices.delete(targetConnectId);
1632
- if (isDeviceDisconnectedError(err)) {
1633
- try {
1634
- this.connector.reset?.();
1635
- } catch {
1636
- }
1637
- }
2324
+ async _doConnect(internalSignal, targetConnectId, allowUsbEphemeralFallback = false, preserveOperationId, context) {
2325
+ _LedgerAdapter._throwIfAborted(internalSignal);
2326
+ if (this.connector.availableTransports?.includes("usb") && this.connector.availableTransports.includes("ble")) {
2327
+ this._activeConnectionType = "usb";
2328
+ const usbDevices = await this._searchDevices({ transportType: "usb" }, internalSignal);
2329
+ _LedgerAdapter._throwIfAborted(internalSignal);
2330
+ if (usbDevices.length > 0) {
2331
+ const knownUsb = context?.knownConnections?.find(
2332
+ (connection) => connection.transport === "usb"
2333
+ );
2334
+ const usbTarget = knownUsb?.transport === "usb" ? knownUsb.connectId : usbDevices.find((device) => device.connectId === targetConnectId)?.connectId;
2335
+ return this._connectFirstOrSelect(
2336
+ usbDevices,
2337
+ usbTarget,
2338
+ allowUsbEphemeralFallback,
2339
+ preserveOperationId,
2340
+ context,
2341
+ internalSignal
2342
+ );
2343
+ }
2344
+ this._activeConnectionType = "ble";
2345
+ const knownBle = context?.knownConnections?.find(
2346
+ (connection) => connection.transport === "ble"
2347
+ );
2348
+ if (knownBle?.transport === "ble") {
2349
+ return this._connectDeviceOrThrow(knownBle.connectId, preserveOperationId, internalSignal);
2350
+ }
2351
+ if (targetConnectId && context?.knownConnections === void 0) {
2352
+ throw createHwkError({
2353
+ code: HardwareErrorCode3.DeviceNotFound,
2354
+ message: "Ledger connection metadata is required before starting Bluetooth binding"
2355
+ });
1638
2356
  }
2357
+ const bleDevices = await this._searchDevices({ transportType: "ble" }, internalSignal);
2358
+ return this._connectFirstOrSelect(
2359
+ bleDevices,
2360
+ void 0,
2361
+ allowUsbEphemeralFallback,
2362
+ preserveOperationId,
2363
+ context,
2364
+ internalSignal
2365
+ );
2366
+ }
2367
+ if (this._isBleConnection() && targetConnectId) {
2368
+ return this._connectDeviceOrThrow(targetConnectId, preserveOperationId, internalSignal);
1639
2369
  }
1640
2370
  let confirms = 0;
1641
2371
  while (!internalSignal.aborted) {
@@ -1643,22 +2373,28 @@ var _LedgerAdapter = class _LedgerAdapter {
1643
2373
  type: EConnectorInteraction.Searching,
1644
2374
  payload: { sessionId: "" }
1645
2375
  });
1646
- let devices = await this.searchDevices();
2376
+ let devices = await this._searchDevices(void 0, internalSignal);
2377
+ _LedgerAdapter._throwIfAborted(internalSignal);
1647
2378
  if (devices.length === 0) {
1648
2379
  for (let i = 0; i < 3 && !internalSignal.aborted; i += 1) {
1649
2380
  await new Promise((resolve) => {
1650
2381
  setTimeout(resolve, DEVICE_CONNECT_RETRY_DELAY_MS);
1651
2382
  });
1652
- devices = await this.searchDevices();
2383
+ _LedgerAdapter._throwIfAborted(internalSignal);
2384
+ devices = await this._searchDevices(void 0, internalSignal);
2385
+ _LedgerAdapter._throwIfAborted(internalSignal);
1653
2386
  if (devices.length > 0) break;
1654
2387
  }
1655
2388
  }
1656
- if (devices.length > 0) {
2389
+ if (devices.length > 0 || this._isBleConnection() && allowUsbEphemeralFallback) {
1657
2390
  try {
1658
2391
  return await this._connectFirstOrSelect(
1659
2392
  devices,
1660
2393
  targetConnectId,
1661
- allowUsbEphemeralFallback
2394
+ allowUsbEphemeralFallback,
2395
+ preserveOperationId,
2396
+ context,
2397
+ internalSignal
1662
2398
  );
1663
2399
  } catch (err) {
1664
2400
  if (!isDeviceLockedError(err) && !isDeviceNotAdvertisingError(err) && !isDeviceDisconnectedError(err)) {
@@ -1687,45 +2423,136 @@ var _LedgerAdapter = class _LedgerAdapter {
1687
2423
  _LedgerAdapter._throwIfAborted(internalSignal);
1688
2424
  throw new Error("_doConnect aborted");
1689
2425
  }
1690
- async _connectFirstOrSelect(devices, targetConnectId, allowUsbEphemeralFallback = false) {
2426
+ async _connectFirstOrSelect(devices, targetConnectId, allowUsbEphemeralFallback, preserveOperationId, context, signal) {
2427
+ _LedgerAdapter._throwIfAborted(signal);
1691
2428
  if (targetConnectId) {
1692
2429
  const target = devices.find(
1693
2430
  (d) => d.connectId === targetConnectId || d.deviceId === targetConnectId
1694
2431
  );
1695
2432
  if (target) {
1696
- return this._connectDeviceOrThrow(target.connectId);
2433
+ return this._connectDeviceOrThrow(target.connectId, preserveOperationId, signal);
1697
2434
  }
1698
- if (!isLedgerBleConnectionType(this.connector.connectionType) && devices.length === 1 && allowUsbEphemeralFallback) {
2435
+ if (!this._isBleConnection() && devices.length === 1 && allowUsbEphemeralFallback) {
1699
2436
  debugLog(
1700
2437
  `[LedgerAdapter] target ${targetConnectId} not in fresh enumeration; accepting sole USB device ${devices[0].connectId} for fingerprint-verified recovery`
1701
2438
  );
1702
- return this._connectDeviceOrThrow(devices[0].connectId);
2439
+ return this._connectDeviceOrThrow(devices[0].connectId, preserveOperationId, signal);
1703
2440
  }
1704
- const err = Object.assign(new Error(`Target Ledger unavailable: ${targetConnectId}`), {
1705
- code: HardwareErrorCode3.DeviceNotFound
1706
- });
1707
- if (isLedgerBleConnectionType(this.connector.connectionType)) {
1708
- err._tag = ERROR_TAG.DeviceNotAdvertising;
2441
+ if (!this._isBleConnection() || !allowUsbEphemeralFallback) {
2442
+ const err = Object.assign(new Error(`Target Ledger unavailable: ${targetConnectId}`), {
2443
+ code: HardwareErrorCode3.DeviceNotFound
2444
+ });
2445
+ if (this._isBleConnection()) {
2446
+ err._tag = ERROR_TAG.DeviceNotAdvertising;
2447
+ }
2448
+ throw err;
1709
2449
  }
1710
- throw err;
1711
2450
  }
1712
- if (isLedgerBleConnectionType(this.connector.connectionType)) {
2451
+ const requiresBleSelection = this._isBleConnection();
2452
+ if (requiresBleSelection && !allowUsbEphemeralFallback) {
1713
2453
  throw Object.assign(new Error("Ledger BLE connectId is required."), {
1714
2454
  code: HardwareErrorCode3.DeviceNotFound
1715
2455
  });
1716
2456
  }
1717
- if (devices.length > 1) {
1718
- throw createMultipleUsbLedgerDevicesError();
2457
+ if (requiresBleSelection && context?.allowDeviceSelection !== false) {
2458
+ const bindingSessionId = context?.bindingSessionId ?? this._uiRegistry.createRequestId();
2459
+ if (context) context.bindingSessionId = bindingSessionId;
2460
+ const allowUsbFallback = context?.bindingReason !== "manual-rebind" && Boolean(this.connector.availableTransports?.includes("usb"));
2461
+ const knownUsb = context?.knownConnections?.find(
2462
+ (connection) => connection.transport === "usb"
2463
+ );
2464
+ const usbConnectId = knownUsb?.transport === "usb" ? knownUsb.connectId : targetConnectId;
2465
+ const { device, requestId } = await requestBleDeviceSelection({
2466
+ emitter: this.emitter,
2467
+ registry: this._uiRegistry,
2468
+ signal,
2469
+ allowUsbFallback,
2470
+ scan: async () => {
2471
+ if (allowUsbFallback) {
2472
+ const usbDevices = await this._searchDevices({ transportType: "usb" }, signal);
2473
+ const candidate = usbDevices.find(
2474
+ (device2) => device2.connectionType === "usb" && device2.connectId === usbConnectId
2475
+ ) ?? (usbDevices.length === 1 && usbDevices[0].connectionType === "usb" ? usbDevices[0] : void 0);
2476
+ if (candidate) return [candidate];
2477
+ }
2478
+ return (await this._searchDevices({ transportType: "ble", waitForAllTransports: true }, signal)).filter((device2) => !context?.rejectedConnectIds?.has(device2.connectId));
2479
+ },
2480
+ request: {
2481
+ devices: devices.filter((device2) => !context?.rejectedConnectIds?.has(device2.connectId)),
2482
+ bindingSessionId,
2483
+ rejectedConnectId: context?.rejectedConnectId,
2484
+ context: {
2485
+ kind: "bind-connection",
2486
+ transport: "ble",
2487
+ reason: context?.bindingReason ?? (targetConnectId ? "known-connection-unavailable" : "missing-binding")
2488
+ },
2489
+ extra: context?.extra,
2490
+ operationId: preserveOperationId
2491
+ }
2492
+ });
2493
+ if (device.connectionType === "usb") {
2494
+ this._activeConnectionType = "usb";
2495
+ if (context) context.selectedConnection = void 0;
2496
+ if (preserveOperationId) this._pendingOperationBindings.delete(preserveOperationId);
2497
+ this._bindingSelectionRequestId = void 0;
2498
+ this.emitter.emit(UI_REQUEST.DEVICE_BINDING_STATUS, {
2499
+ type: UI_REQUEST.DEVICE_BINDING_STATUS,
2500
+ payload: { selectionRequestId: requestId, status: "cancelled" }
2501
+ });
2502
+ return this._connectDeviceOrThrow(device.connectId, preserveOperationId, signal);
2503
+ }
2504
+ if (context) context.selectedConnection = { connectId: device.connectId, requestId };
2505
+ this._bindingSelectionRequestId = requestId;
2506
+ return this._connectDeviceOrThrow(device.connectId, preserveOperationId, signal);
2507
+ }
2508
+ if (devices.length > 0 && (devices.length > 1 || requiresBleSelection)) {
2509
+ if (context?.allowDeviceSelection === false || !this.emitter.listenerCount(UI_REQUEST.REQUEST_SELECT_DEVICE)) {
2510
+ throw createHwkError({
2511
+ code: HardwareErrorCode3.DeviceNotFound,
2512
+ message: "Select a Ledger device before continuing"
2513
+ });
2514
+ }
2515
+ const requestId = this._uiRegistry.createRequestId();
2516
+ const operationId = this._activeOperationId();
2517
+ const waitPromise = this._uiRegistry.wait(
2518
+ UI_REQUEST.REQUEST_SELECT_DEVICE,
2519
+ { requestId, operationId }
2520
+ );
2521
+ this.emitter.emit(UI_REQUEST.REQUEST_SELECT_DEVICE, {
2522
+ type: UI_REQUEST.REQUEST_SELECT_DEVICE,
2523
+ payload: {
2524
+ devices,
2525
+ requestId,
2526
+ operationId,
2527
+ context: requiresBleSelection ? {
2528
+ kind: "bind-connection",
2529
+ transport: "ble",
2530
+ reason: targetConnectId ? "known-connection-unavailable" : "missing-binding"
2531
+ } : { kind: "select-device", transport: "usb", reason: "multiple-candidates" },
2532
+ extra: context?.extra
2533
+ }
2534
+ });
2535
+ const { sdkConnectId } = await (signal ? this._abortable(signal, waitPromise) : waitPromise);
2536
+ if (signal) _LedgerAdapter._throwIfAborted(signal);
2537
+ const selected = devices.find((device) => device.connectId === sdkConnectId);
2538
+ if (!selected) {
2539
+ throw Object.assign(new Error("Selected Ledger is no longer available"), {
2540
+ code: HardwareErrorCode3.DeviceNotFound
2541
+ });
2542
+ }
2543
+ if (context && requiresBleSelection)
2544
+ context.selectedConnection = { connectId: selected.connectId, requestId };
2545
+ return this._connectDeviceOrThrow(selected.connectId, preserveOperationId, signal);
1719
2546
  }
1720
2547
  if (devices.length !== 1) {
1721
2548
  throw Object.assign(new Error("Ledger device not found."), {
1722
2549
  code: HardwareErrorCode3.DeviceNotFound
1723
2550
  });
1724
2551
  }
1725
- return this._connectDeviceOrThrow(devices[0].connectId);
2552
+ return this._connectDeviceOrThrow(devices[0].connectId, preserveOperationId, signal);
1726
2553
  }
1727
- async _connectDeviceOrThrow(chosenConnectId) {
1728
- const result = await this.connectDevice(chosenConnectId);
2554
+ async _connectDeviceOrThrow(chosenConnectId, preserveOperationId, signal) {
2555
+ const result = await this._connectTarget(chosenConnectId, preserveOperationId, signal);
1729
2556
  if (!result.success) {
1730
2557
  const payload = result.payload;
1731
2558
  const rethrow = Object.assign(new Error(payload.error), {
@@ -1736,7 +2563,7 @@ var _LedgerAdapter = class _LedgerAdapter {
1736
2563
  }
1737
2564
  throw rethrow;
1738
2565
  }
1739
- return chosenConnectId;
2566
+ return result.payload;
1740
2567
  }
1741
2568
  /**
1742
2569
  * Call the connector with automatic session resolution and disconnect retry.
@@ -1766,26 +2593,122 @@ var _LedgerAdapter = class _LedgerAdapter {
1766
2593
  * lives in one place.
1767
2594
  */
1768
2595
  async _callConnector(sessionId, method, params, signal) {
1769
- const promise = this.connector.call(sessionId, method, params);
2596
+ this._assertConnectorReady(method);
2597
+ if (signal?.aborted) throw this._abortReason(signal);
2598
+ const releaseOperation = this._retainConnectorOperation(`call:${sessionId}`);
2599
+ let promise;
2600
+ try {
2601
+ promise = this.connector.call(sessionId, method, params).finally(releaseOperation);
2602
+ } catch (error) {
2603
+ releaseOperation();
2604
+ throw error;
2605
+ }
1770
2606
  const result = signal ? await this._abortable(signal, promise) : await promise;
1771
2607
  return this._unwrapConnectorResult(result);
1772
2608
  }
2609
+ _assertConnectorReady(method) {
2610
+ if (this._resetPromise || this._pendingConnectorTeardowns > 0 || this._unsettledConnectorOperations.size > 0) {
2611
+ throw _LedgerAdapter._createDeviceBusyError(method);
2612
+ }
2613
+ }
2614
+ _runConnectorTeardown(task) {
2615
+ const previous = this._connectorTeardownTail;
2616
+ let releaseTail = () => void 0;
2617
+ this._connectorTeardownTail = new Promise((resolve) => {
2618
+ releaseTail = resolve;
2619
+ });
2620
+ this._pendingConnectorTeardowns += 1;
2621
+ return (async () => {
2622
+ try {
2623
+ await previous;
2624
+ await this._waitForConnectorOperationsToDrain();
2625
+ await task();
2626
+ } finally {
2627
+ this._pendingConnectorTeardowns -= 1;
2628
+ releaseTail();
2629
+ }
2630
+ })();
2631
+ }
2632
+ _waitForConnectorOperationsToDrain() {
2633
+ if (this._unsettledConnectorOperations.size === 0) {
2634
+ return Promise.resolve();
2635
+ }
2636
+ return new Promise((resolve) => {
2637
+ this._connectorIdleWaiters.add(resolve);
2638
+ });
2639
+ }
2640
+ _retainConnectorOperation(key) {
2641
+ this._unsettledConnectorOperations.set(
2642
+ key,
2643
+ (this._unsettledConnectorOperations.get(key) ?? 0) + 1
2644
+ );
2645
+ let released = false;
2646
+ return () => {
2647
+ if (released) return;
2648
+ released = true;
2649
+ const remaining = (this._unsettledConnectorOperations.get(key) ?? 1) - 1;
2650
+ if (remaining > 0) {
2651
+ this._unsettledConnectorOperations.set(key, remaining);
2652
+ } else {
2653
+ this._unsettledConnectorOperations.delete(key);
2654
+ }
2655
+ if (this._unsettledConnectorOperations.size === 0) {
2656
+ for (const resolve of this._connectorIdleWaiters) resolve();
2657
+ this._connectorIdleWaiters.clear();
2658
+ }
2659
+ };
2660
+ }
1773
2661
  async connectorCall(connectId, method, params, fingerprint, permissionDeviceId, commonParams, installContext) {
1774
- debugLog("[LedgerAdapter][REQ]", { method, connectId: connectId || "(empty)", params });
1775
- const queueKey = connectId || "__ledger_default__";
2662
+ const positionalOperationId = isHardwareOperationId(connectId) ? connectId : void 0;
2663
+ if (positionalOperationId && commonParams?.operationId && positionalOperationId !== commonParams.operationId) {
2664
+ throw createHwkError({
2665
+ code: HardwareErrorCode3.InvalidParams,
2666
+ message: "Conflicting Ledger operation ids",
2667
+ params: {
2668
+ positionalOperationId,
2669
+ commonOperationId: commonParams.operationId
2670
+ }
2671
+ });
2672
+ }
2673
+ const operationId = commonParams?.operationId ?? positionalOperationId;
2674
+ const operation = operationId ? this._operations.resolve(operationId) : void 0;
2675
+ const releaseOperationRetention = operationId ? this._operations.retain(operationId) : void 0;
2676
+ const effectiveConnectId = operation?.connectId ?? connectId;
2677
+ debugLog("[LedgerAdapter][REQ]", {
2678
+ method,
2679
+ connectId: effectiveConnectId || "(empty)",
2680
+ params
2681
+ });
2682
+ const queueKey = ledgerQueueKey({ operationId, connectId: effectiveConnectId });
1776
2683
  try {
1777
2684
  const result = await this._jobQueue.enqueue(
1778
2685
  queueKey,
1779
- async (signal) => this._runConnectorCall(
1780
- connectId,
1781
- method,
1782
- params,
1783
- signal,
1784
- fingerprint,
1785
- permissionDeviceId,
1786
- commonParams,
1787
- installContext
1788
- ),
2686
+ async (signal) => {
2687
+ if (operationId) this._activeOperationJobs.add(operationId);
2688
+ try {
2689
+ return await this._runConnectorCall(
2690
+ effectiveConnectId,
2691
+ method,
2692
+ params,
2693
+ signal,
2694
+ fingerprint,
2695
+ permissionDeviceId,
2696
+ commonParams,
2697
+ installContext ?? {},
2698
+ operationId
2699
+ );
2700
+ } catch (error) {
2701
+ this._finishBleBinding(signal.aborted ? "cancelled" : "failed");
2702
+ throw error;
2703
+ } finally {
2704
+ if (operationId) {
2705
+ this._activeOperationJobs.delete(operationId);
2706
+ if (this._pendingOperationDisconnects.delete(operationId)) {
2707
+ await this._releaseLostOperationConnection(operationId);
2708
+ }
2709
+ }
2710
+ }
2711
+ },
1789
2712
  {
1790
2713
  label: method,
1791
2714
  rejectIfBusy: true,
@@ -1806,15 +2729,22 @@ var _LedgerAdapter = class _LedgerAdapter {
1806
2729
  }
1807
2730
  });
1808
2731
  throw err;
2732
+ } finally {
2733
+ releaseOperationRetention?.();
1809
2734
  }
1810
2735
  }
1811
2736
  /**
1812
2737
  * Race a promise against an abort signal. On abort, rejects with
1813
2738
  * signal.reason → instance _lastCancelReason → generic Error('Aborted').
1814
2739
  */
2740
+ /** Hermes/RN polyfills don't always populate signal.reason; fall back. */
2741
+ _abortReason(signal) {
2742
+ return signal.reason ?? this._lastCancelReason ?? new Error("Aborted");
2743
+ }
1815
2744
  _abortable(signal, promise) {
1816
- const getAbortReason = () => signal.reason ?? this._lastCancelReason ?? new Error("Aborted");
2745
+ const getAbortReason = () => this._abortReason(signal);
1817
2746
  if (signal.aborted) {
2747
+ void promise.catch(() => void 0);
1818
2748
  return Promise.reject(getAbortReason());
1819
2749
  }
1820
2750
  return new Promise((resolve, reject) => {
@@ -1841,13 +2771,15 @@ var _LedgerAdapter = class _LedgerAdapter {
1841
2771
  }
1842
2772
  }
1843
2773
  /** Actual work done under the job queue — connection, fingerprint, call, and recovery. */
1844
- async _runConnectorCall(connectId, method, params, signal, fingerprint, permissionDeviceId, commonParams, installContext) {
2774
+ async _runConnectorCall(connectId, method, params, signal, fingerprint, permissionDeviceId, commonParams, installContext, operationId, lockedRetryBudget = _LedgerAdapter.MAX_BUSINESS_RETRY_BUDGET) {
1845
2775
  _LedgerAdapter._throwIfAborted(signal);
1846
- await this._ensureDevicePermission(
1847
- connectId,
1848
- permissionDeviceId ?? fingerprint?.deviceId,
1849
- signal
1850
- );
2776
+ if (!operationId) {
2777
+ await this._ensureDevicePermission(
2778
+ connectId,
2779
+ permissionDeviceId ?? fingerprint?.deviceId,
2780
+ signal
2781
+ );
2782
+ }
1851
2783
  _LedgerAdapter._throwIfAborted(signal);
1852
2784
  let effectiveParams = params;
1853
2785
  if (method === "btcGetPublicKey") {
@@ -1861,36 +2793,135 @@ var _LedgerAdapter = class _LedgerAdapter {
1861
2793
  effectiveParams = gatedParams;
1862
2794
  }
1863
2795
  const allowUsbEphemeralFallback = !!fingerprint?.deviceId && !fingerprint.skipFingerprint;
1864
- const resolvedConnectId = await this.ensureConnected(
1865
- connectId,
2796
+ let businessCallStarted = false;
2797
+ const verifiedBleTarget = connectId ? this._verifiedBleReconnectTargets.get(connectId) : void 0;
2798
+ const knownTransport = this._isBleConnection() ? "ble" : "usb";
2799
+ const hintedConnectId = commonParams?.knownConnections?.find(
2800
+ (connection) => connection.transport === knownTransport
2801
+ );
2802
+ const inputConnectId = hintedConnectId && hintedConnectId.transport !== "qr" ? hintedConnectId.connectId : connectId;
2803
+ const preferredConnectId = fingerprint && !fingerprint.skipFingerprint && verifiedBleTarget?.chain === fingerprint.chain && verifiedBleTarget.fingerprint === fingerprint.deviceId ? verifiedBleTarget.connectId : inputConnectId;
2804
+ const connectionAttempt = { ...commonParams };
2805
+ const bundleConnection = installContext?.connection;
2806
+ if (bundleConnection && this._sessions.get(bundleConnection.connectId) !== bundleConnection.sessionId) {
2807
+ throw createHwkError({
2808
+ code: HardwareErrorCode3.DeviceDisconnected,
2809
+ message: "Ledger all-network connection ended"
2810
+ });
2811
+ }
2812
+ let resolvedConnectId = operationId ? this._operations.resolve(operationId).connectId : bundleConnection?.connectId ?? await this.ensureConnected(
2813
+ preferredConnectId,
1866
2814
  signal,
1867
- allowUsbEphemeralFallback
2815
+ allowUsbEphemeralFallback,
2816
+ void 0,
2817
+ connectionAttempt
1868
2818
  );
1869
- const sessionId = this._sessions.get(resolvedConnectId);
2819
+ let sessionId = this._sessions.get(resolvedConnectId);
2820
+ if (sessionId && installContext && !installContext.connection) {
2821
+ installContext.connection = { connectId: resolvedConnectId, sessionId };
2822
+ }
1870
2823
  if (!sessionId) {
2824
+ if (operationId) {
2825
+ this._operations.end(operationId, "disconnect");
2826
+ throw createHwkError({
2827
+ code: HardwareErrorCode3.OperationEnded,
2828
+ message: "Ledger operation connection is no longer active",
2829
+ params: { operationId, reason: "disconnect" }
2830
+ });
2831
+ }
1871
2832
  throw Object.assign(new Error("Auto-connect succeeded but no session found"), {
1872
2833
  _tag: ERROR_TAG.DeviceSessionNotFound
1873
2834
  });
1874
2835
  }
1875
2836
  try {
1876
2837
  if (fingerprint && !fingerprint.skipFingerprint && fingerprint.deviceId) {
1877
- const fp = await this._abortable(
1878
- signal,
1879
- this._verifyDeviceFingerprintWithSession(
1880
- sessionId,
1881
- fingerprint.deviceId,
1882
- fingerprint.chain
1883
- )
2838
+ for (; ; ) {
2839
+ const fp = await this._abortable(
2840
+ signal,
2841
+ this._verifyDeviceFingerprintWithSession(
2842
+ sessionId,
2843
+ fingerprint.deviceId,
2844
+ fingerprint.chain
2845
+ )
2846
+ );
2847
+ if (fp.success) break;
2848
+ const binding = operationId ? this._pendingOperationBindings.get(operationId) : connectionAttempt;
2849
+ if (!this._isBleConnection() || binding?.selectedConnection?.connectId !== resolvedConnectId) {
2850
+ throw Object.assign(new Error(formatDeviceMismatchError(fp.expected, fp.actual)), {
2851
+ code: HardwareErrorCode3.DeviceMismatch
2852
+ });
2853
+ }
2854
+ binding.rejectedConnectIds ?? (binding.rejectedConnectIds = /* @__PURE__ */ new Set());
2855
+ binding.rejectedConnectIds.add(resolvedConnectId);
2856
+ binding.rejectedConnectId = resolvedConnectId;
2857
+ this._sessions.delete(resolvedConnectId);
2858
+ await this.connector.disconnect(sessionId);
2859
+ if (operationId) this._pendingOperationDisconnects.delete(operationId);
2860
+ _LedgerAdapter._throwIfAborted(signal);
2861
+ resolvedConnectId = await this._connectFirstOrSelect(
2862
+ [],
2863
+ void 0,
2864
+ true,
2865
+ operationId,
2866
+ binding,
2867
+ signal
2868
+ );
2869
+ const selectedSession = this._sessions.get(resolvedConnectId);
2870
+ const selectedDevice = this._discoveredDevices.get(resolvedConnectId);
2871
+ if (!selectedSession || !selectedDevice)
2872
+ throw createHwkError({
2873
+ code: HardwareErrorCode3.DeviceDisconnected,
2874
+ message: "Selected Ledger connection ended"
2875
+ });
2876
+ sessionId = selectedSession;
2877
+ if (operationId)
2878
+ this._operations.rebind(operationId, {
2879
+ connectId: resolvedConnectId,
2880
+ device: selectedDevice,
2881
+ // Same source as `_createOperation`: the selected transport, not
2882
+ // the device snapshot a session connect overwrote.
2883
+ connectionType: this._isBleConnection() ? "ble" : "usb",
2884
+ connectionKeys: [sessionId]
2885
+ });
2886
+ if (installContext)
2887
+ installContext.connection = { connectId: resolvedConnectId, sessionId };
2888
+ }
2889
+ await this._publishVerifiedBleBinding(
2890
+ resolvedConnectId,
2891
+ fingerprint.chain,
2892
+ fingerprint.deviceId,
2893
+ connectionAttempt,
2894
+ operationId,
2895
+ signal
1884
2896
  );
1885
- if (!fp.success) {
1886
- throw Object.assign(new Error(formatDeviceMismatchError(fp.expected, fp.actual)), {
1887
- code: HardwareErrorCode3.DeviceMismatch
2897
+ if (!operationId && connectionAttempt.selectedConnection?.connectId === resolvedConnectId && this._isBleConnection()) {
2898
+ this._verifiedBleReconnectTargets.set(connectId, {
2899
+ connectId: resolvedConnectId,
2900
+ chain: fingerprint.chain,
2901
+ fingerprint: fingerprint.deviceId
1888
2902
  });
1889
2903
  }
1890
2904
  }
2905
+ businessCallStarted = true;
1891
2906
  return await this._callConnector(sessionId, method, effectiveParams, signal);
1892
2907
  } catch (err) {
1893
2908
  if (signal.aborted) throw err;
2909
+ if (isDeviceDisconnectedError(err) || isDeviceNotAdvertisingError(err) || isTimeoutError(err) || isConnectionLevelError(err)) {
2910
+ this._discoveredDevices.delete(resolvedConnectId);
2911
+ if (operationId) await this._releaseLostOperationConnection(operationId);
2912
+ else {
2913
+ this._sessions.delete(resolvedConnectId);
2914
+ await this.connector.disconnect(sessionId).catch(() => void 0);
2915
+ }
2916
+ const ambiguous = businessCallStarted && !canReplayHardwareMethodAfterTransportFailure(method);
2917
+ const interactionParams = operationId ? { operationId, reason: "disconnect" } : void 0;
2918
+ throw createHwkError({
2919
+ code: operationId ? HardwareErrorCode3.OperationEnded : mapLedgerError(err).code,
2920
+ message: "Ledger operation connection was lost; start a new operation",
2921
+ params: ambiguous ? operationMayHaveCompletedParams(method, { operationId }) : interactionParams,
2922
+ recovery: ambiguous ? { scope: "unknown" } : void 0
2923
+ });
2924
+ }
1894
2925
  const errObj = err;
1895
2926
  debugLog("[LedgerAdapter] connectorCall error:", method, {
1896
2927
  message: errObj?.message,
@@ -1902,6 +2933,65 @@ var _LedgerAdapter = class _LedgerAdapter {
1902
2933
  isNotAdvertising: isDeviceNotAdvertisingError(err),
1903
2934
  isStuckApp: isStuckAppStateError(err)
1904
2935
  });
2936
+ if ((isDeviceLockedError(err) || errObj?.code === HardwareErrorCode3.DeviceLocked) && lockedRetryBudget > 0) {
2937
+ await this._waitForDeviceConnect(signal);
2938
+ if (this._sessions.get(resolvedConnectId) !== sessionId) {
2939
+ throw createHwkError({
2940
+ code: operationId ? HardwareErrorCode3.OperationEnded : HardwareErrorCode3.DeviceDisconnected,
2941
+ message: "Ledger connection ended while waiting for unlock"
2942
+ });
2943
+ }
2944
+ return this._runConnectorCall(
2945
+ resolvedConnectId,
2946
+ method,
2947
+ effectiveParams,
2948
+ signal,
2949
+ fingerprint,
2950
+ permissionDeviceId,
2951
+ commonParams,
2952
+ installContext,
2953
+ operationId,
2954
+ lockedRetryBudget - 1
2955
+ );
2956
+ }
2957
+ if (businessCallStarted && isStuckAppStateError(err)) {
2958
+ await this._sleepAbortable(_LedgerAdapter.STUCK_APP_RETRY_DELAY_MS, signal);
2959
+ if (this._sessions.get(resolvedConnectId) !== sessionId) {
2960
+ throw createHwkError({
2961
+ code: operationId ? HardwareErrorCode3.OperationEnded : HardwareErrorCode3.DeviceDisconnected,
2962
+ message: "Ledger connection ended during the app transition"
2963
+ });
2964
+ }
2965
+ try {
2966
+ return await this._callConnector(sessionId, method, effectiveParams, signal);
2967
+ } catch (retryErr) {
2968
+ if (isStuckAppStateError(retryErr)) throw err;
2969
+ if (isDeviceDisconnectedError(retryErr) || isDeviceNotAdvertisingError(retryErr) || isTimeoutError(retryErr) || isConnectionLevelError(retryErr)) {
2970
+ if (operationId) await this._releaseLostOperationConnection(operationId);
2971
+ else {
2972
+ this._sessions.delete(resolvedConnectId);
2973
+ this._discoveredDevices.delete(resolvedConnectId);
2974
+ await this.connector.disconnect(sessionId).catch(() => void 0);
2975
+ }
2976
+ throw createHwkError({
2977
+ code: operationId ? HardwareErrorCode3.OperationEnded : mapLedgerError(retryErr).code,
2978
+ message: `Ledger ${method} may have completed before the connection was lost`,
2979
+ params: !canReplayHardwareMethodAfterTransportFailure(method) ? operationMayHaveCompletedParams(method, {
2980
+ operationId,
2981
+ reason: "disconnect"
2982
+ }) : { operationId, reason: "disconnect" },
2983
+ recovery: !canReplayHardwareMethodAfterTransportFailure(method) ? { scope: "unknown" } : void 0
2984
+ });
2985
+ }
2986
+ throw retryErr;
2987
+ }
2988
+ }
2989
+ if (!operationId && err?.code === HardwareErrorCode3.DeviceMismatch) {
2990
+ this._sessions.delete(resolvedConnectId);
2991
+ this._discoveredDevices.delete(resolvedConnectId);
2992
+ await this.connector.disconnect(sessionId).catch(() => void 0);
2993
+ throw err;
2994
+ }
1905
2995
  const autoInstallApp = commonParams?.autoInstallApp ?? this._defaultAutoInstallApp;
1906
2996
  const isAppMissing = isAppNotInstalledError(err) || err?.code === HardwareErrorCode3.AppNotInstalled;
1907
2997
  if (autoInstallApp && isAppMissing) {
@@ -1957,157 +3047,14 @@ var _LedgerAdapter = class _LedgerAdapter {
1957
3047
  fingerprint,
1958
3048
  permissionDeviceId,
1959
3049
  commonParams,
1960
- installContext
1961
- );
1962
- }
1963
- }
1964
- if (isStuckAppStateError(err)) {
1965
- try {
1966
- this._sessions.delete(resolvedConnectId);
1967
- this._discoveredDevices.delete(resolvedConnectId);
1968
- this.connector.reset?.();
1969
- } catch {
1970
- }
1971
- debugLog(
1972
- "[LedgerAdapter] stuck-app retry: method=",
1973
- method,
1974
- "delayMs=",
1975
- _LedgerAdapter.STUCK_APP_RETRY_DELAY_MS,
1976
- "_tag=",
1977
- err?._tag
1978
- );
1979
- try {
1980
- const retryResult = await this._retryAfterStuckApp(
1981
- resolvedConnectId,
1982
- method,
1983
- effectiveParams,
1984
- signal,
1985
- err,
1986
- fingerprint
1987
- );
1988
- debugLog("[LedgerAdapter] stuck-app retry succeeded: method=", method);
1989
- return retryResult;
1990
- } catch (retryErr) {
1991
- if (signal.aborted) throw retryErr;
1992
- if (isStuckAppStateError(retryErr)) {
1993
- debugLog("[LedgerAdapter] stuck-app retry exhausted (2nd 6901): method=", method);
1994
- throw err;
1995
- }
1996
- debugLog(
1997
- "[LedgerAdapter] stuck-app retry threw non-stuck error: method=",
1998
- method,
1999
- "retryErrTag=",
2000
- retryErr?._tag
3050
+ installContext,
3051
+ operationId
2001
3052
  );
2002
- throw retryErr;
2003
3053
  }
2004
3054
  }
2005
- if (isDeviceLockedError(err) || isDeviceNotAdvertisingError(err) || isDeviceDisconnectedError(err)) {
2006
- let lastErr = err;
2007
- for (let attempt = 0; attempt < _LedgerAdapter.MAX_BUSINESS_RETRY_BUDGET; attempt += 1) {
2008
- if (signal.aborted) throw lastErr;
2009
- try {
2010
- this._sessions.delete(resolvedConnectId);
2011
- this._discoveredDevices.delete(resolvedConnectId);
2012
- if (isDeviceDisconnectedError(lastErr)) {
2013
- try {
2014
- this.connector.reset?.();
2015
- } catch {
2016
- }
2017
- }
2018
- const reConnectId = await this.ensureConnected(
2019
- resolvedConnectId,
2020
- signal,
2021
- allowUsbEphemeralFallback
2022
- );
2023
- const reSessionId = this._sessions.get(reConnectId);
2024
- if (!reSessionId) throw lastErr;
2025
- if (fingerprint && !fingerprint.skipFingerprint && fingerprint.deviceId) {
2026
- const fp = await this._abortable(
2027
- signal,
2028
- this._verifyDeviceFingerprintWithSession(
2029
- reSessionId,
2030
- fingerprint.deviceId,
2031
- fingerprint.chain
2032
- )
2033
- );
2034
- if (!fp.success) {
2035
- throw Object.assign(new Error(formatDeviceMismatchError(fp.expected, fp.actual)), {
2036
- code: HardwareErrorCode3.DeviceMismatch
2037
- });
2038
- }
2039
- }
2040
- return await this._callConnector(reSessionId, method, effectiveParams, signal);
2041
- } catch (retryErr) {
2042
- if (signal.aborted) throw retryErr;
2043
- lastErr = retryErr;
2044
- const canRetry = attempt < _LedgerAdapter.MAX_BUSINESS_RETRY_BUDGET - 1 && (isDeviceLockedError(retryErr) || isDeviceNotAdvertisingError(retryErr) || isDeviceDisconnectedError(retryErr));
2045
- if (!canRetry) {
2046
- throw retryErr;
2047
- }
2048
- }
2049
- }
2050
- throw lastErr;
2051
- }
2052
- if (isTimeoutError(err)) {
2053
- debugLog("[LedgerAdapter] timeout, retrying with fresh connection...");
2054
- this._discoveredDevices.delete(resolvedConnectId);
2055
- return this._retryWithFreshConnection(
2056
- resolvedConnectId,
2057
- method,
2058
- effectiveParams,
2059
- signal,
2060
- err,
2061
- fingerprint
2062
- );
2063
- }
2064
- if (isConnectionLevelError(err)) {
2065
- debugLog("[LedgerAdapter] connection-level fail-closed reset");
2066
- this._sessions.delete(resolvedConnectId);
2067
- this._discoveredDevices.delete(resolvedConnectId);
2068
- this.connector.reset();
2069
- const codeNum = err?.code;
2070
- throw Object.assign(err, {
2071
- code: codeNum ?? HardwareErrorCode3.DeviceDisconnected
2072
- });
2073
- }
2074
3055
  throw err;
2075
3056
  }
2076
3057
  }
2077
- /**
2078
- * Stuck-app recovery: pause for the device's UI transition, then retry once.
2079
- *
2080
- * Caller has already cleared the session + reset connector. We wait so Stax
2081
- * finishes its post-CloseApp animation, then go through ensureConnected +
2082
- * fingerprint check + call exactly once. Caller decides what to do on a
2083
- * second stuck-app hit.
2084
- */
2085
- async _retryAfterStuckApp(resolvedConnectId, method, params, signal, originalErr, fingerprint) {
2086
- await this._sleepAbortable(_LedgerAdapter.STUCK_APP_RETRY_DELAY_MS, signal);
2087
- const retryConnectId = await this.ensureConnected(
2088
- resolvedConnectId,
2089
- signal,
2090
- !!fingerprint?.deviceId && !fingerprint.skipFingerprint
2091
- );
2092
- const retrySessionId = this._sessions.get(retryConnectId);
2093
- if (!retrySessionId) throw originalErr;
2094
- if (fingerprint && !fingerprint.skipFingerprint && fingerprint.deviceId) {
2095
- const fp = await this._abortable(
2096
- signal,
2097
- this._verifyDeviceFingerprintWithSession(
2098
- retrySessionId,
2099
- fingerprint.deviceId,
2100
- fingerprint.chain
2101
- )
2102
- );
2103
- if (!fp.success) {
2104
- throw Object.assign(new Error(formatDeviceMismatchError(fp.expected, fp.actual)), {
2105
- code: HardwareErrorCode3.DeviceMismatch
2106
- });
2107
- }
2108
- }
2109
- return this._callConnector(retrySessionId, method, params, signal);
2110
- }
2111
3058
  _sleepAbortable(ms, signal) {
2112
3059
  return new Promise((resolve, reject) => {
2113
3060
  if (signal.aborted) {
@@ -2126,111 +3073,30 @@ var _LedgerAdapter = class _LedgerAdapter {
2126
3073
  });
2127
3074
  }
2128
3075
  /**
2129
- * Clear stale session, reconnect, and retry the call.
3076
+ * Ensure OS-level device permission (Bluetooth / USB) before proceeding.
3077
+ *
3078
+ * Emits `REQUEST_DEVICE_PERMISSION` and awaits the consumer's
3079
+ * `RECEIVE_DEVICE_PERMISSION` reply (60s budget covers "probe → system
3080
+ * prompt → user tap" plus a generous margin). If the consumer never wires
3081
+ * a handler or never replies, the wait times out and the operation fails
3082
+ * fast so scanners/callers don't hang silently.
2130
3083
  *
2131
- * Timeout recovery starts with a full connector reset. After an APDU
2132
- * timeout, DMK/transport state may still emit malformed responses; retrying
2133
- * on the same DMK can poison the next chain switch.
3084
+ * - No connectId (searchDevices): environment-level permission
3085
+ * - With connectId (business methods): device-level permission
2134
3086
  */
2135
- async _retryWithFreshConnection(targetConnectId, method, params, signal, originalErr, fingerprint) {
2136
- this.connector.reset();
2137
- this._sessions.clear();
2138
- this._discoveredDevices.clear();
2139
- this._connectingPromise = null;
2140
- const allowUsbEphemeralFallback = !!fingerprint?.deviceId && !fingerprint.skipFingerprint;
2141
- const retryConnectId = await this.ensureConnected(
2142
- targetConnectId,
2143
- signal,
2144
- allowUsbEphemeralFallback
2145
- );
2146
- const retrySessionId = this._sessions.get(retryConnectId);
2147
- if (!retrySessionId) {
2148
- throw originalErr;
2149
- }
2150
- try {
2151
- if (fingerprint && !fingerprint.skipFingerprint && fingerprint.deviceId) {
2152
- const fp = await this._abortable(
2153
- signal,
2154
- this._verifyDeviceFingerprintWithSession(
2155
- retrySessionId,
2156
- fingerprint.deviceId,
2157
- fingerprint.chain
2158
- )
2159
- );
2160
- if (!fp.success) {
2161
- throw Object.assign(new Error(formatDeviceMismatchError(fp.expected, fp.actual)), {
2162
- code: HardwareErrorCode3.DeviceMismatch
2163
- });
2164
- }
2165
- }
2166
- return await this._callConnector(retrySessionId, method, params, signal);
2167
- } catch (retryErr) {
2168
- if (signal.aborted) throw retryErr;
2169
- this.connector.reset();
2170
- this._sessions.clear();
2171
- this._discoveredDevices.clear();
2172
- this._connectingPromise = null;
2173
- if (!isDeviceDisconnectedError(retryErr) && !isTimeoutError(retryErr)) {
2174
- throw retryErr;
2175
- }
2176
- debugLog(
2177
- "[LedgerAdapter] fresh-session retry still failed; resetting connector and rebuilding DMK"
2178
- );
2179
- this.connector.reset();
2180
- this._sessions.clear();
2181
- this._discoveredDevices.clear();
2182
- this._connectingPromise = null;
2183
- const finalConnectId = await this.ensureConnected(
2184
- targetConnectId,
2185
- signal,
2186
- allowUsbEphemeralFallback
2187
- );
2188
- const finalSessionId = this._sessions.get(finalConnectId);
2189
- if (!finalSessionId) {
2190
- throw originalErr;
2191
- }
2192
- if (fingerprint && !fingerprint.skipFingerprint && fingerprint.deviceId) {
2193
- const fp = await this._abortable(
2194
- signal,
2195
- this._verifyDeviceFingerprintWithSession(
2196
- finalSessionId,
2197
- fingerprint.deviceId,
2198
- fingerprint.chain
2199
- )
2200
- );
2201
- if (!fp.success) {
2202
- throw Object.assign(new Error(formatDeviceMismatchError(fp.expected, fp.actual)), {
2203
- code: HardwareErrorCode3.DeviceMismatch
2204
- });
2205
- }
2206
- }
2207
- return this._callConnector(finalSessionId, method, params, signal);
2208
- }
2209
- }
2210
- /**
2211
- * Ensure OS-level device permission (Bluetooth / USB) before proceeding.
2212
- *
2213
- * Emits `REQUEST_DEVICE_PERMISSION` and awaits the consumer's
2214
- * `RECEIVE_DEVICE_PERMISSION` reply (60s budget covers "probe → system
2215
- * prompt → user tap" plus a generous margin). If the consumer never wires
2216
- * a handler or never replies, the wait times out and the operation fails
2217
- * fast so scanners/callers don't hang silently.
2218
- *
2219
- * - No connectId (searchDevices): environment-level permission
2220
- * - With connectId (business methods): device-level permission
2221
- */
2222
- async _ensureDevicePermission(connectId, deviceId, signal) {
2223
- if (signal?.aborted) {
2224
- _LedgerAdapter._throwIfAborted(signal);
3087
+ async _ensureDevicePermission(connectId, deviceId, signal) {
3088
+ if (signal?.aborted) {
3089
+ _LedgerAdapter._throwIfAborted(signal);
2225
3090
  }
2226
3091
  const transportType = this.activeTransport ?? "hid";
3092
+ const operationId = this._activeOperationId();
2227
3093
  const waitPromise = this._uiRegistry.wait(
2228
3094
  UI_REQUEST.REQUEST_DEVICE_PERMISSION,
2229
- { timeoutMs: 6e4 }
3095
+ { timeoutMs: 6e4, operationId }
2230
3096
  );
2231
3097
  this.emitter.emit(UI_REQUEST.REQUEST_DEVICE_PERMISSION, {
2232
3098
  type: UI_REQUEST.REQUEST_DEVICE_PERMISSION,
2233
- payload: { transportType, connectId, deviceId }
3099
+ payload: { transportType, connectId, deviceId, operationId }
2234
3100
  });
2235
3101
  let response;
2236
3102
  const onAbort = () => {
@@ -2276,7 +3142,15 @@ var _LedgerAdapter = class _LedgerAdapter {
2276
3142
  if (err && typeof err === "object" && "code" in err && typeof err.code === "number") {
2277
3143
  const e = err;
2278
3144
  const params = e.code === HardwareErrorCode3.DevicePermissionDenied && e.reason ? { permissionDeniedReason: e.reason } : e.params;
2279
- return ledgerFailure(e.code, e.message ?? "Unknown error", e.appName, tag, params);
3145
+ return ledgerFailure(
3146
+ e.code,
3147
+ e.message ?? "Unknown error",
3148
+ e.appName,
3149
+ tag,
3150
+ params,
3151
+ void 0,
3152
+ isHwkRecoveryHint(e.recovery) ? e.recovery : void 0
3153
+ );
2280
3154
  }
2281
3155
  const mapped = mapLedgerError(err);
2282
3156
  return ledgerFailure(mapped.code, mapped.message, mapped.appName, tag);
@@ -2303,7 +3177,7 @@ var _LedgerAdapter = class _LedgerAdapter {
2303
3177
  deviceId: device.deviceId,
2304
3178
  connectId: device.connectId,
2305
3179
  label: device.name,
2306
- connectionType: this.connector.connectionType,
3180
+ connectionType: device.connectionType ?? this.connector.connectionType,
2307
3181
  rssi: device.rssi,
2308
3182
  isConnectable: device.isConnectable,
2309
3183
  serialNumber: device.serialNumber,
@@ -2329,8 +3203,8 @@ var LedgerAdapter = _LedgerAdapter;
2329
3203
 
2330
3204
  // src/connector/LedgerConnectorBase.ts
2331
3205
  import {
2332
- EConnectorInteraction as EConnectorInteraction4,
2333
- HardwareErrorCode as HardwareErrorCode8,
3206
+ EConnectorInteraction as EConnectorInteraction3,
3207
+ HardwareErrorCode as HardwareErrorCode7,
2334
3208
  isKnownNonTargetHardwareVendor,
2335
3209
  serializeConnectorError
2336
3210
  } from "@onekeyfe/hwk-adapter-core";
@@ -2603,7 +3477,7 @@ var LedgerDeviceManager = class {
2603
3477
 
2604
3478
  // src/signer/SignerManager.ts
2605
3479
  import { SignerEthBuilder } from "@ledgerhq/device-signer-kit-ethereum";
2606
- import { ContextModuleBuilder } from "@ledgerhq/context-module";
3480
+ import { ContextModuleBuilder, ContextModuleChainID } from "@ledgerhq/context-module";
2607
3481
 
2608
3482
  // src/signer/SignerEth.ts
2609
3483
  import { hexToBytes } from "@onekeyfe/hwk-adapter-core";
@@ -2869,7 +3743,7 @@ var SignerManager = class _SignerManager {
2869
3743
  return (args) => new SignerEthBuilder(args);
2870
3744
  }
2871
3745
  static _createContextModule() {
2872
- const contextModule = new ContextModuleBuilder({}).removeDefaultLoaders().build();
3746
+ const contextModule = new ContextModuleBuilder({}).setChain(ContextModuleChainID.Ethereum).removeDefaultLoaders().build();
2873
3747
  return _SignerManager.wrapBlindSigningReportNonBlocking(contextModule);
2874
3748
  }
2875
3749
  static wrapBlindSigningReportNonBlocking(contextModule) {
@@ -3339,7 +4213,8 @@ function _applySignaturesToPsbt(psbtHex, signatures) {
3339
4213
  }
3340
4214
 
3341
4215
  // src/connector/chains/sol.ts
3342
- import { bytesToHex, hexToBytes as hexToBytes3 } from "@onekeyfe/hwk-adapter-core";
4216
+ import { bytesToHex, hexToBytes as hexToBytes3, prepareSolanaOffchainMessageV1 } from "@onekeyfe/hwk-adapter-core";
4217
+ import bs58 from "bs58";
3343
4218
 
3344
4219
  // src/signer/SignerSol.ts
3345
4220
  var SignerSol = class {
@@ -3423,8 +4298,21 @@ async function solSignMessage(ctx, sessionId, params) {
3423
4298
  const path = normalizePath(params.path);
3424
4299
  const messageBytes = hexToBytes3(params.message);
3425
4300
  try {
4301
+ if (params.messageVersion === 1) {
4302
+ const preparedMessage = prepareSolanaOffchainMessageV1({
4303
+ message: messageBytes,
4304
+ requiredSigners: params.requiredSigners
4305
+ });
4306
+ const { SignMessageVersion } = await ctx.importLedgerKit(
4307
+ "@ledgerhq/device-signer-kit-solana"
4308
+ );
4309
+ const result2 = await solSigner.signMessage(path, preparedMessage.serializedMessage, {
4310
+ version: SignMessageVersion.Raw
4311
+ });
4312
+ return { signature: decodeBase58Signature(result2.signature) };
4313
+ }
3426
4314
  const result = await solSigner.signMessage(path, messageBytes);
3427
- return { signature: result.signature };
4315
+ return { signature: decodeBase58EnvelopeSignature(result.signature) };
3428
4316
  } catch (err) {
3429
4317
  ctx.invalidateSession(sessionId);
3430
4318
  throw ctx.wrapError(err);
@@ -3432,11 +4320,27 @@ async function solSignMessage(ctx, sessionId, params) {
3432
4320
  ctx.clearCanceller(sessionId);
3433
4321
  }
3434
4322
  }
4323
+ function decodeBase58Signature(signature) {
4324
+ const bytes = bs58.decode(signature);
4325
+ if (bytes.length !== 64) {
4326
+ throw new Error(`Ledger Solana signature must be 64 bytes, received ${bytes.length}`);
4327
+ }
4328
+ return bytesToHex(bytes);
4329
+ }
4330
+ function decodeBase58EnvelopeSignature(envelope) {
4331
+ const bytes = bs58.decode(envelope);
4332
+ if (bytes.length < 65 || bytes[0] !== 1) {
4333
+ throw new Error("Ledger Solana signature envelope is invalid");
4334
+ }
4335
+ return bytesToHex(bytes.subarray(1, 65));
4336
+ }
3435
4337
  async function _createSolSigner(ctx, sessionId) {
3436
4338
  const dmk = await ctx.getOrCreateDmk();
3437
- const { ContextModuleBuilder: ContextModuleBuilder2 } = await ctx.importLedgerKit("@ledgerhq/context-module");
4339
+ const { ContextModuleBuilder: ContextModuleBuilder2, ContextModuleChainID: ContextModuleChainID2 } = await ctx.importLedgerKit(
4340
+ "@ledgerhq/context-module"
4341
+ );
3438
4342
  const { SignerSolanaBuilder } = await ctx.importLedgerKit("@ledgerhq/device-signer-kit-solana");
3439
- const contextModule = new ContextModuleBuilder2({}).removeDefaultLoaders().build();
4343
+ const contextModule = new ContextModuleBuilder2({}).setChain(ContextModuleChainID2.Solana).removeDefaultLoaders().build();
3440
4344
  const sdkSigner = new SignerSolanaBuilder({ dmk, sessionId }).withContextModule(contextModule).build();
3441
4345
  const signer = new SignerSol(sdkSigner);
3442
4346
  signer.onInteraction = (interaction) => {
@@ -3452,361 +4356,192 @@ async function _createSolSigner(ctx, sessionId) {
3452
4356
  return signer;
3453
4357
  }
3454
4358
 
3455
- // src/connector/chains/tron.ts
3456
- import { HardwareErrorCode as HardwareErrorCode7 } from "@onekeyfe/hwk-adapter-core";
3457
- import Trx from "@ledgerhq/hw-app-trx";
3458
-
3459
- // src/connector/chains/legacyChainCall.ts
3460
- import { EConnectorInteraction as EConnectorInteraction3 } from "@onekeyfe/hwk-adapter-core";
4359
+ // src/connector/chains/zcash.ts
4360
+ import { bytesToHex as bytesToHex2 } from "@onekeyfe/hwk-adapter-core";
3461
4361
 
3462
- // src/app/AppManager.ts
3463
- import {
3464
- CloseAppCommand,
3465
- GetAppAndVersionCommand,
3466
- OpenAppCommand,
3467
- isSuccessCommandResult
3468
- } from "@ledgerhq/device-management-kit";
3469
- import { HardwareErrorCode as HardwareErrorCode6 } from "@onekeyfe/hwk-adapter-core";
3470
- var APP_NAME_MAP = {
3471
- ETH: "Ethereum",
3472
- BTC: "Bitcoin",
3473
- SOL: "Solana",
3474
- TRX: "Tron",
3475
- XRP: "XRP",
3476
- ADA: "Cardano",
3477
- DOT: "Polkadot",
3478
- ATOM: "Cosmos"
3479
- };
3480
- var DASHBOARD_APP_NAME = "BOLOS";
3481
- var AppManager = class {
3482
- constructor(dmk, options) {
3483
- this._dmk = dmk;
3484
- this._waitMs = options?.waitMs ?? 1e3;
3485
- this._maxRetries = options?.maxRetries ?? 10;
3486
- }
3487
- /**
3488
- * Return the Ledger app name for a given chain ticker,
3489
- * or undefined if the chain is not supported.
3490
- */
3491
- static getAppName(chain) {
3492
- return APP_NAME_MAP[chain];
3493
- }
3494
- /**
3495
- * Ensure the target app is open on the device identified by `sessionId`.
3496
- *
3497
- * Flow:
3498
- * 1. Check the currently running app.
3499
- * 2. If it is already the target, return immediately.
3500
- * 3. If a different app is running (not dashboard), close it first.
3501
- * 4. Open the target app.
3502
- * 5. Poll until the device confirms the target app is running.
3503
- */
3504
- /**
3505
- * @param onConfirmOnDevice Called BEFORE OpenAppCommand is issued — the
3506
- * device is about to display "Open <app>" on screen and wait for the
3507
- * user's button press. UI consumers should show their "open app" prompt
3508
- * in response. NOT called when the target app is already open (no user
3509
- * interaction needed in that case).
3510
- *
3511
- * Important: OpenAppCommand is blocking. It does not resolve until the user
3512
- * has physically confirmed on the device, so anything that runs AFTER
3513
- * `await this._openApp(...)` lands AFTER the prompt is already gone.
3514
- * Hence the callback must fire BEFORE that await.
3515
- */
3516
- async ensureAppOpen(sessionId, targetAppName, onConfirmOnDevice) {
3517
- const currentApp = await this._getCurrentApp(sessionId);
3518
- if (currentApp === targetAppName) {
3519
- return;
3520
- }
3521
- if (!this._isDashboard(currentApp)) {
3522
- await this._closeCurrentApp(sessionId);
3523
- await this._waitForApp(sessionId, DASHBOARD_APP_NAME);
3524
- }
3525
- onConfirmOnDevice?.();
3526
- await this._openApp(sessionId, targetAppName);
3527
- await this._waitForApp(sessionId, targetAppName);
4362
+ // src/signer/SignerZcash.ts
4363
+ var SignerZcash = class {
4364
+ // eslint-disable-next-line no-useless-constructor, no-empty-function
4365
+ constructor(_sdk) {
4366
+ this._sdk = _sdk;
3528
4367
  }
3529
- // ---------------------------------------------------------------------------
3530
- // Private helpers
3531
- // ---------------------------------------------------------------------------
3532
- async _getCurrentApp(sessionId) {
3533
- const result = await this._dmk.sendCommand({
3534
- sessionId,
3535
- command: new GetAppAndVersionCommand()
3536
- });
3537
- if (isSuccessCommandResult(result)) {
3538
- debugLog("[AppManager] currentApp:", result.data.name);
3539
- return result.data.name;
3540
- }
3541
- const errResult = result;
3542
- const dmkErr = errResult.error ?? {};
3543
- const original = dmkErr.originalError;
3544
- debugLog(
3545
- "[AppManager] _getCurrentApp failed sessionId=",
3546
- sessionId,
3547
- "tag=",
3548
- dmkErr._tag,
3549
- "errorCode=",
3550
- dmkErr.errorCode,
3551
- "message=",
3552
- dmkErr.message,
3553
- "originalErrorMessage=",
3554
- original?.message ?? String(original ?? "")
3555
- );
3556
- throw Object.assign(
3557
- new Error(
3558
- dmkErr.message ?? "Failed to get current app from device"
3559
- ),
3560
- {
3561
- _tag: dmkErr._tag,
3562
- errorCode: dmkErr.errorCode,
3563
- originalError: original
3564
- }
4368
+ /** GET_VK: UFVK string (default) or raw 96-byte Orchard FVK. */
4369
+ async getFullViewingKey(derivationPath, options) {
4370
+ const action = this._sdk.getFullViewingKey(derivationPath, options);
4371
+ return deviceActionToPromise(
4372
+ action,
4373
+ this.onInteraction,
4374
+ void 0,
4375
+ this.onRegisterCanceller
3565
4376
  );
3566
4377
  }
3567
- async _openApp(sessionId, appName) {
3568
- const result = await this._dmk.sendCommand({
3569
- sessionId,
3570
- command: new OpenAppCommand({ appName })
3571
- });
3572
- if (!isSuccessCommandResult(result)) {
3573
- const dmkErr = result.error;
3574
- const errorCode = "errorCode" in dmkErr && dmkErr.errorCode != null ? String(dmkErr.errorCode) : "";
3575
- const message = "message" in dmkErr && typeof dmkErr.message === "string" ? dmkErr.message : "";
3576
- debugLog(
3577
- "[AppManager] openApp failed:",
3578
- appName,
3579
- "errorCode:",
3580
- errorCode,
3581
- "tag:",
3582
- dmkErr._tag
3583
- );
3584
- let code;
3585
- if (errorCode === "6807" || /unknown application/i.test(message)) {
3586
- code = HardwareErrorCode6.AppNotInstalled;
3587
- } else if (errorCode === "5501" || dmkErr._tag === "ActionRefusedError") {
3588
- code = HardwareErrorCode6.UserRejected;
3589
- }
3590
- throw Object.assign(new Error(`Failed to open "${appName}"`), {
3591
- _tag: ERROR_TAG.OpenAppCommand,
3592
- code,
3593
- errorCode,
3594
- statusCode: errorCode,
3595
- appName,
3596
- originalError: dmkErr
3597
- });
3598
- }
3599
- }
3600
- async _closeCurrentApp(sessionId) {
3601
- debugLog("[AppManager] closeCurrentApp");
3602
- await this._dmk.sendCommand({
3603
- sessionId,
3604
- command: new CloseAppCommand()
3605
- });
3606
- }
3607
- /**
3608
- * Poll the device until the expected app is reported as running,
3609
- * or throw after `_maxRetries` attempts.
3610
- */
3611
- async _waitForApp(sessionId, expectedAppName) {
3612
- let lastSeen = "";
3613
- for (let i = 0; i < this._maxRetries; i++) {
3614
- await this._wait();
3615
- const current = await this._getCurrentApp(sessionId);
3616
- lastSeen = current;
3617
- if (current === expectedAppName) {
3618
- return;
3619
- }
3620
- }
3621
- debugLog(
3622
- "[AppManager] waitForApp exhausted: expected=",
3623
- expectedAppName,
3624
- "lastSeen=",
3625
- lastSeen
3626
- );
3627
- throw new Error(
3628
- `Ledger: failed to open "${expectedAppName}" after ${this._maxRetries} retries (last seen: ${lastSeen})`
4378
+ /** GET_SHIELDED_ADDRESS: single-Orchard-receiver UA for a 5-level transparent path. */
4379
+ async getShieldedAddress(derivationPath, options) {
4380
+ const action = this._sdk.getShieldedAddress(derivationPath, options);
4381
+ return deviceActionToPromise(
4382
+ action,
4383
+ this.onInteraction,
4384
+ void 0,
4385
+ this.onRegisterCanceller
3629
4386
  );
3630
4387
  }
3631
- _isDashboard(appName) {
3632
- return appName === DASHBOARD_APP_NAME;
3633
- }
3634
- _wait() {
3635
- return new Promise((resolve) => setTimeout(resolve, this._waitMs));
3636
- }
3637
4388
  };
3638
4389
 
3639
- // src/connector/chains/legacyChainCall.ts
3640
- function isLegacyWrongAppError(err, _appName) {
3641
- return isWrongAppError(err);
3642
- }
3643
- async function withLegacyChainCall(ctx, sessionId, options, action) {
3644
- const { appName, needsConfirmation } = options;
3645
- let openAppPromptShown = false;
3646
- const onAppOpenPrompt = () => {
3647
- openAppPromptShown = true;
3648
- ctx.emit("ui-event", {
3649
- type: EConnectorInteraction3.ConfirmOpenApp,
3650
- payload: { sessionId }
3651
- });
3652
- };
3653
- const closeOpenAppUiIfShown = () => {
3654
- if (openAppPromptShown) {
3655
- ctx.emit("ui-event", {
3656
- type: EConnectorInteraction3.InteractionComplete,
3657
- payload: { sessionId }
3658
- });
3659
- openAppPromptShown = false;
3660
- }
3661
- };
4390
+ // src/connector/chains/zcash.ts
4391
+ async function zcashGetFullViewingKey(ctx, sessionId, params) {
4392
+ const signer = await _createZcashSigner(ctx, sessionId);
4393
+ const path = normalizePath(params.path);
4394
+ const mode = params.mode ?? "ufvk";
3662
4395
  try {
3663
- await _ensureAppOpen(ctx, sessionId, appName, onAppOpenPrompt);
3664
- } catch (err) {
3665
- debugLog(
3666
- "[LegacyChainCall] pre-flight ensureAppOpen failed:",
3667
- appName,
3668
- err?.message
3669
- );
3670
- closeOpenAppUiIfShown();
3671
- throw ctx.wrapError(err, { defaultAppName: appName });
3672
- }
3673
- const runOnce = async () => {
3674
- let confirmEmitted = false;
3675
- if (needsConfirmation) {
3676
- ctx.emit("ui-event", {
3677
- type: EConnectorInteraction3.ConfirmOnDevice,
3678
- payload: { sessionId }
3679
- });
3680
- confirmEmitted = true;
3681
- }
3682
- try {
3683
- return await action(sessionId);
3684
- } finally {
3685
- if (confirmEmitted || openAppPromptShown) {
3686
- ctx.emit("ui-event", {
3687
- type: EConnectorInteraction3.InteractionComplete,
3688
- payload: { sessionId }
3689
- });
3690
- openAppPromptShown = false;
3691
- }
4396
+ const result = await signer.getFullViewingKey(path, { mode });
4397
+ if (result.mode === "ufvk") {
4398
+ return { path: params.path, mode, ufvk: result.fullViewingKey };
3692
4399
  }
3693
- };
4400
+ return { path: params.path, mode, orchardFvk: bytesToHex2(result.fullViewingKey) };
4401
+ } catch (err) {
4402
+ ctx.invalidateSession(sessionId);
4403
+ throw ctx.wrapError(err);
4404
+ } finally {
4405
+ ctx.clearCanceller(sessionId);
4406
+ }
4407
+ }
4408
+ async function zcashGetShieldedAddress(ctx, sessionId, params) {
4409
+ const signer = await _createZcashSigner(ctx, sessionId);
4410
+ const path = normalizePath(params.path);
3694
4411
  try {
3695
- return await runOnce();
4412
+ const result = await signer.getShieldedAddress(path, {
4413
+ checkOnDevice: params.showOnDevice ?? false
4414
+ });
4415
+ return { address: result.address, path: params.path };
3696
4416
  } catch (err) {
3697
- if (!isLegacyWrongAppError(err, appName)) {
3698
- debugLog("[LegacyChainCall] non-wrong-app failure:", appName, err?.message);
3699
- ctx.invalidateSession(sessionId);
3700
- throw ctx.wrapError(err, { defaultAppName: appName });
3701
- }
3702
- debugLog("[LegacyChainCall] wrong-app detected, retrying:", appName);
3703
- try {
3704
- await _ensureAppOpen(ctx, sessionId, appName, onAppOpenPrompt);
3705
- } catch (switchErr) {
3706
- debugLog(
3707
- "[LegacyChainCall] retry ensureAppOpen failed:",
3708
- appName,
3709
- switchErr?.message
3710
- );
3711
- closeOpenAppUiIfShown();
3712
- throw ctx.wrapError(switchErr, { defaultAppName: appName });
3713
- }
3714
- ctx.clearAllSigners();
3715
- const result = await runOnce();
3716
- debugLog("[LegacyChainCall] retry succeeded:", appName);
3717
- return result;
4417
+ ctx.invalidateSession(sessionId);
4418
+ throw ctx.wrapError(err);
4419
+ } finally {
4420
+ ctx.clearCanceller(sessionId);
3718
4421
  }
3719
4422
  }
3720
- async function _ensureAppOpen(ctx, sessionId, appName, onPrompt) {
4423
+ async function _createZcashSigner(ctx, sessionId) {
3721
4424
  const dmk = await ctx.getOrCreateDmk();
3722
- const appManager = new AppManager(dmk);
3723
- await appManager.ensureAppOpen(sessionId, appName, onPrompt);
4425
+ const { SignerZcashBuilder } = await ctx.importLedgerKit("@ledgerhq/device-signer-kit-zcash");
4426
+ const sdkSigner = new SignerZcashBuilder({ dmk, sessionId }).build();
4427
+ const signer = new SignerZcash(sdkSigner);
4428
+ signer.onInteraction = (interaction) => {
4429
+ debugLog("[LedgerConnector] zcash.onInteraction:", interaction);
4430
+ ctx.emit("ui-event", {
4431
+ type: collapseSignerInteraction(interaction),
4432
+ payload: { sessionId }
4433
+ });
4434
+ };
4435
+ signer.onRegisterCanceller = (cancel) => ctx.registerCanceller(sessionId, cancel);
4436
+ return signer;
3724
4437
  }
3725
4438
 
3726
- // src/transport/DmkTransport.ts
3727
- import Transport from "@ledgerhq/hw-transport";
3728
- var DmkTransport = class extends Transport {
3729
- constructor(dmk, sessionId) {
3730
- super();
3731
- this._dmk = dmk;
3732
- this._sessionId = sessionId;
4439
+ // src/connector/chains/tron.ts
4440
+ import { HardwareErrorCode as HardwareErrorCode6, bytesToHex as bytesToHex3, hexToBytes as hexToBytes4 } from "@onekeyfe/hwk-adapter-core";
4441
+
4442
+ // src/signer/SignerTron.ts
4443
+ var SignerTron = class {
4444
+ // eslint-disable-next-line no-useless-constructor, no-empty-function
4445
+ constructor(_sdk) {
4446
+ this._sdk = _sdk;
3733
4447
  }
3734
- async exchange(apdu) {
3735
- const response = await this._dmk.sendApdu({
3736
- sessionId: this._sessionId,
3737
- apdu: new Uint8Array(apdu)
3738
- });
3739
- const { data, statusCode } = response;
3740
- const result = Buffer.alloc(data.length + 2);
3741
- if (data.length > 0) {
3742
- result.set(data, 0);
3743
- }
3744
- result.set(statusCode, data.length);
3745
- return result;
4448
+ /** Base58 Tron address plus the uncompressed public key at `derivationPath`. */
4449
+ async getAddress(derivationPath, options) {
4450
+ const action = this._sdk.getAddress(derivationPath, options);
4451
+ const result = await deviceActionToPromise(
4452
+ action,
4453
+ this.onInteraction,
4454
+ void 0,
4455
+ this.onRegisterCanceller
4456
+ );
4457
+ return { address: result.address, publicKey: result.publicKey };
3746
4458
  }
3747
- async close() {
4459
+ /** Sign a protobuf-encoded raw transaction. */
4460
+ async signTransaction(derivationPath, transaction, options) {
4461
+ const action = this._sdk.signTransaction(derivationPath, transaction, options);
4462
+ return deviceActionToPromise(
4463
+ action,
4464
+ this.onInteraction,
4465
+ void 0,
4466
+ this.onRegisterCanceller
4467
+ );
4468
+ }
4469
+ /** Sign a personal message (TIP-191). */
4470
+ async signPersonalMessage(derivationPath, message, options) {
4471
+ const action = this._sdk.signPersonalMessage(derivationPath, message, options);
4472
+ return deviceActionToPromise(
4473
+ action,
4474
+ this.onInteraction,
4475
+ void 0,
4476
+ this.onRegisterCanceller
4477
+ );
3748
4478
  }
3749
4479
  };
3750
4480
 
3751
4481
  // src/connector/chains/tron.ts
3752
4482
  async function tronGetAddress(ctx, sessionId, params) {
4483
+ const tronSigner = await _createTronSigner(ctx, sessionId);
3753
4484
  const path = normalizePath(params.path);
3754
- const showOnDevice = params.showOnDevice ?? false;
3755
- return withLegacyChainCall(
3756
- ctx,
3757
- sessionId,
3758
- {
3759
- appName: "Tron",
3760
- // Only show "confirm on device" UI when the device is actually going
3761
- // to display the address for the user to verify.
3762
- needsConfirmation: showOnDevice
3763
- },
3764
- async (sid) => {
3765
- const trx = await _createTrx(ctx, sid);
3766
- const result = await trx.getAddress(path, showOnDevice);
3767
- return { address: result.address, publicKey: result.publicKey, path: params.path };
3768
- }
3769
- );
4485
+ try {
4486
+ const result = await tronSigner.getAddress(path, {
4487
+ checkOnDevice: params.showOnDevice ?? false
4488
+ });
4489
+ return { address: result.address, publicKey: result.publicKey, path: params.path };
4490
+ } catch (err) {
4491
+ ctx.invalidateSession(sessionId);
4492
+ throw ctx.wrapError(err);
4493
+ } finally {
4494
+ ctx.clearCanceller(sessionId);
4495
+ }
3770
4496
  }
3771
4497
  async function tronSignTransaction(ctx, sessionId, params) {
3772
4498
  if (!params.rawTxHex) {
3773
4499
  throw Object.assign(
3774
4500
  new Error("TRON signing requires a protobuf-encoded raw transaction hex (rawTxHex)."),
3775
- { code: HardwareErrorCode7.InvalidParams }
4501
+ { code: HardwareErrorCode6.InvalidParams }
3776
4502
  );
3777
4503
  }
4504
+ const tronSigner = await _createTronSigner(ctx, sessionId);
3778
4505
  const path = normalizePath(params.path);
3779
- return withLegacyChainCall(
3780
- ctx,
3781
- sessionId,
3782
- { appName: "Tron", needsConfirmation: true },
3783
- async (sid) => {
3784
- const trx = await _createTrx(ctx, sid);
3785
- const signature = await trx.signTransaction(
3786
- path,
3787
- params.rawTxHex,
3788
- params.tokenSignatures ?? []
3789
- );
3790
- return { signature };
3791
- }
3792
- );
4506
+ try {
4507
+ const signature = await tronSigner.signTransaction(path, hexToBytes4(params.rawTxHex));
4508
+ return { signature: bytesToHex3(signature) };
4509
+ } catch (err) {
4510
+ ctx.invalidateSession(sessionId);
4511
+ throw ctx.wrapError(err);
4512
+ } finally {
4513
+ ctx.clearCanceller(sessionId);
4514
+ }
3793
4515
  }
3794
4516
  async function tronSignMessage(ctx, sessionId, params) {
4517
+ const tronSigner = await _createTronSigner(ctx, sessionId);
3795
4518
  const path = normalizePath(params.path);
3796
- return withLegacyChainCall(
3797
- ctx,
3798
- sessionId,
3799
- { appName: "Tron", needsConfirmation: true },
3800
- async (sid) => {
3801
- const trx = await _createTrx(ctx, sid);
3802
- const signature = await trx.signPersonalMessage(path, params.messageHex);
3803
- return { signature };
3804
- }
3805
- );
4519
+ try {
4520
+ const signature = await tronSigner.signPersonalMessage(path, hexToBytes4(params.messageHex));
4521
+ return { signature: bytesToHex3(signature) };
4522
+ } catch (err) {
4523
+ ctx.invalidateSession(sessionId);
4524
+ throw ctx.wrapError(err);
4525
+ } finally {
4526
+ ctx.clearCanceller(sessionId);
4527
+ }
3806
4528
  }
3807
- async function _createTrx(ctx, sessionId) {
4529
+ async function _createTronSigner(ctx, sessionId) {
3808
4530
  const dmk = await ctx.getOrCreateDmk();
3809
- return new Trx(new DmkTransport(dmk, sessionId));
4531
+ const { SignerTrxBuilder } = await ctx.importLedgerKit("@ledgerhq/device-signer-kit-tron");
4532
+ const sdkSigner = new SignerTrxBuilder({ dmk, sessionId }).build();
4533
+ const signer = new SignerTron(sdkSigner);
4534
+ signer.onInteraction = (interaction) => {
4535
+ debugLog("[LedgerConnector] tron.onInteraction:", interaction);
4536
+ ctx.emit("ui-event", {
4537
+ type: collapseSignerInteraction(interaction),
4538
+ payload: { sessionId }
4539
+ });
4540
+ };
4541
+ signer.onRegisterCanceller = (cancel) => {
4542
+ ctx.registerCanceller(sessionId, cancel);
4543
+ };
4544
+ return signer;
3810
4545
  }
3811
4546
 
3812
4547
  // src/device-apps/customActions.ts
@@ -3985,7 +4720,7 @@ var DeviceApps = class {
3985
4720
  seTargetId: v.seTargetId,
3986
4721
  mcuTargetId: v.mcuTargetId,
3987
4722
  seVersion: v.seVersion,
3988
- seFlagsHex: bytesToHex2(v.seFlags),
4723
+ seFlagsHex: bytesToHex4(v.seFlags),
3989
4724
  mcuSephVersion: v.mcuSephVersion,
3990
4725
  mcuBootloaderVersion: v.mcuBootloaderVersion,
3991
4726
  hwVersion: v.hwVersion
@@ -4059,7 +4794,7 @@ var DeviceApps = class {
4059
4794
  }
4060
4795
  }
4061
4796
  };
4062
- function bytesToHex2(bytes) {
4797
+ function bytesToHex4(bytes) {
4063
4798
  if (!bytes) return "";
4064
4799
  return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
4065
4800
  }
@@ -4102,10 +4837,11 @@ var METHOD_PREFIX_TO_APP_NAME = {
4102
4837
  evm: "Ethereum",
4103
4838
  btc: "Bitcoin",
4104
4839
  sol: "Solana",
4105
- tron: "Tron"
4840
+ tron: "Tron",
4841
+ zcash: "Zcash"
4106
4842
  };
4107
4843
  var HARDWARE_ERROR_CODE_VALUES = new Set(
4108
- Object.values(HardwareErrorCode8).filter((value) => typeof value === "number")
4844
+ Object.values(HardwareErrorCode7).filter((value) => typeof value === "number")
4109
4845
  );
4110
4846
  var BLE_CONNECT_SCAN_TIMEOUT_MS = 1500;
4111
4847
  var LEDGER_RELAY_ALLOWED_ROOT_DOMAINS = ["onekeytest.com", "onekey.com"];
@@ -4142,6 +4878,10 @@ async function defaultLedgerKitImporter(pkg) {
4142
4878
  return import("@ledgerhq/device-signer-kit-bitcoin");
4143
4879
  case "@ledgerhq/device-signer-kit-solana":
4144
4880
  return import("@ledgerhq/device-signer-kit-solana");
4881
+ case "@ledgerhq/device-signer-kit-tron":
4882
+ return import("@ledgerhq/device-signer-kit-tron");
4883
+ case "@ledgerhq/device-signer-kit-zcash":
4884
+ return import("@ledgerhq/device-signer-kit-zcash");
4145
4885
  case "@ledgerhq/context-module":
4146
4886
  return import("@ledgerhq/context-module");
4147
4887
  default:
@@ -4201,6 +4941,7 @@ var LedgerConnectorBase = class {
4201
4941
  this._ctx = {
4202
4942
  emit: (event, data) => this._emit(event, data),
4203
4943
  invalidateSession: (sid) => this._invalidateSession(sid),
4944
+ teardownSecureChannelSession: (sid) => this._teardownSecureChannelSession(sid),
4204
4945
  wrapError: (err, opts) => this._wrapError(err, opts),
4205
4946
  getOrCreateDmk: () => this._getOrCreateDmk(),
4206
4947
  getDeviceManager: () => this._getDeviceManager(),
@@ -4300,7 +5041,7 @@ var LedgerConnectorBase = class {
4300
5041
  `Ledger BLE connect did not return within ${HANG_CEILING_MS / 6e4}min \u2014 DMK hang fallback.`
4301
5042
  );
4302
5043
  err._tag = ERROR_TAG.BlePairingTimeout;
4303
- err.code = HardwareErrorCode8.BlePairingTimeout;
5044
+ err.code = HardwareErrorCode7.BlePairingTimeout;
4304
5045
  reject(err);
4305
5046
  }, HANG_CEILING_MS);
4306
5047
  });
@@ -4333,7 +5074,7 @@ var LedgerConnectorBase = class {
4333
5074
  "Ledger device is not currently advertising. Wake up and unlock the device, keep it nearby, then try again."
4334
5075
  );
4335
5076
  err._tag = ERROR_TAG.DeviceNotAdvertising;
4336
- err.code = HardwareErrorCode8.DeviceNotFound;
5077
+ err.code = HardwareErrorCode7.DeviceNotFound;
4337
5078
  throw err;
4338
5079
  };
4339
5080
  const doConnect = async (path) => {
@@ -4390,14 +5131,14 @@ var LedgerConnectorBase = class {
4390
5131
  this._resetSignersAndSessions();
4391
5132
  if (isLedgerBleConnectionType(this.connectionType)) {
4392
5133
  const tag = err?._tag;
4393
- if (isKnownConnectionTag(tag)) {
5134
+ if (isKnownConnectionTag(tag) && !isConnectionOpeningTag(tag)) {
4394
5135
  throw err;
4395
5136
  }
4396
5137
  const wrapped = new Error(
4397
5138
  "Ledger Bluetooth pairing failed. Make sure the device is unlocked and nearby, then try again."
4398
5139
  );
4399
5140
  wrapped._tag = ERROR_TAG.BleGattBondingFailed;
4400
- wrapped.code = HardwareErrorCode8.BlePairingTimeout;
5141
+ wrapped.code = HardwareErrorCode7.BlePairingTimeout;
4401
5142
  wrapped.originalError = err;
4402
5143
  throw wrapped;
4403
5144
  }
@@ -4475,7 +5216,7 @@ var LedgerConnectorBase = class {
4475
5216
  this._unwatchSessionState(sessionId);
4476
5217
  this._signerManager?.invalidate(sessionId);
4477
5218
  this._cancellers.get(sessionId)?.({
4478
- code: HardwareErrorCode8.DeviceDisconnected,
5219
+ code: HardwareErrorCode7.DeviceDisconnected,
4479
5220
  tag: "DeviceDisconnected",
4480
5221
  message: "Device disconnected"
4481
5222
  });
@@ -4501,7 +5242,7 @@ var LedgerConnectorBase = class {
4501
5242
  success: false,
4502
5243
  error: serializeConnectorError(
4503
5244
  Object.assign(new Error("Ledger app is unresponsive"), {
4504
- code: HardwareErrorCode8.DeviceAppStuck,
5245
+ code: HardwareErrorCode7.DeviceAppStuck,
4505
5246
  _tag: ERROR_TAG.DeviceAppStuck,
4506
5247
  originalError: err
4507
5248
  })
@@ -4513,7 +5254,7 @@ var LedgerConnectorBase = class {
4513
5254
  success: false,
4514
5255
  error: serializeConnectorError(
4515
5256
  Object.assign(new Error("Device communication interrupted, please retry"), {
4516
- code: HardwareErrorCode8.TransportError,
5257
+ code: HardwareErrorCode7.TransportError,
4517
5258
  _tag: ERROR_TAG.DeviceTransportStuck,
4518
5259
  originalError: err
4519
5260
  })
@@ -4559,6 +5300,11 @@ var LedgerConnectorBase = class {
4559
5300
  return solSignTransaction(ctx, sessionId, params);
4560
5301
  case "solSignMessage":
4561
5302
  return solSignMessage(ctx, sessionId, params);
5303
+ // ZCASH
5304
+ case "zcashGetFullViewingKey":
5305
+ return zcashGetFullViewingKey(ctx, sessionId, params);
5306
+ case "zcashGetShieldedAddress":
5307
+ return zcashGetShieldedAddress(ctx, sessionId, params);
4562
5308
  // TRON
4563
5309
  case "tronGetAddress":
4564
5310
  return tronGetAddress(ctx, sessionId, params);
@@ -4583,7 +5329,7 @@ var LedgerConnectorBase = class {
4583
5329
  try {
4584
5330
  return await apps.install(p.appName, ({ progress }) => {
4585
5331
  ctx.emit("ui-event", {
4586
- type: EConnectorInteraction4.AppInstallProgress,
5332
+ type: EConnectorInteraction3.AppInstallProgress,
4587
5333
  payload: {
4588
5334
  sessionId,
4589
5335
  appName: p.appName,
@@ -4592,7 +5338,7 @@ var LedgerConnectorBase = class {
4592
5338
  });
4593
5339
  });
4594
5340
  } catch (err) {
4595
- ctx.invalidateSession(sessionId);
5341
+ ctx.teardownSecureChannelSession(sessionId);
4596
5342
  throw ctx.wrapError(err);
4597
5343
  } finally {
4598
5344
  ctx.clearCanceller(sessionId);
@@ -4682,7 +5428,7 @@ var LedgerConnectorBase = class {
4682
5428
  );
4683
5429
  return { isGenuine: output.isGenuine, deviceId };
4684
5430
  } catch (err) {
4685
- ctx.invalidateSession(sessionId);
5431
+ ctx.teardownSecureChannelSession(sessionId);
4686
5432
  throw ctx.wrapError(err);
4687
5433
  } finally {
4688
5434
  ctx.clearCanceller(sessionId);
@@ -4801,9 +5547,34 @@ var LedgerConnectorBase = class {
4801
5547
  }
4802
5548
  return this._deviceAppsManager;
4803
5549
  }
5550
+ // DeviceAppsManager is intentionally absent here: it is a per-call factory
5551
+ // with no cached session state, so there is nothing to invalidate. Cancelling
5552
+ // the device action is what closes the secure channel.
4804
5553
  _invalidateSession(sessionId) {
4805
5554
  this._signerManager?.invalidate(sessionId);
4806
5555
  }
5556
+ /**
5557
+ * Teardown for the OS-level actions that open a manager-api secure channel
5558
+ * (install, uninstall, genuine check). Cancelling the device action is what
5559
+ * stops DMK's xstate actor, which unsubscribes the secure-channel observable
5560
+ * and closes its WebSocket; dropping the canceller without firing it leaves
5561
+ * that teardown to run only if the action settled on its own.
5562
+ *
5563
+ * The DMK device session is deliberately kept: the secure channel is a
5564
+ * separate WebSocket per call, and disconnecting here would break the bounded
5565
+ * unlock/retry recovery the adapter runs on the original session.
5566
+ */
5567
+ _teardownSecureChannelSession(sessionId) {
5568
+ const cancel = this._cancellers.get(sessionId);
5569
+ this._cancellers.delete(sessionId);
5570
+ if (cancel) {
5571
+ try {
5572
+ cancel();
5573
+ } catch {
5574
+ }
5575
+ }
5576
+ this._invalidateSession(sessionId);
5577
+ }
4807
5578
  /**
4808
5579
  * Replace an old session with a new one after app switch.
4809
5580
  * Emits device-connect so the adapter updates its _sessions Map.
@@ -4888,7 +5659,7 @@ var LedgerConnectorBase = class {
4888
5659
  * at every catch site. Falls through unchanged for unknown methods.
4889
5660
  */
4890
5661
  _ctxForMethod(method) {
4891
- const prefix = /^(evm|btc|sol|tron)/.exec(method)?.[1];
5662
+ const prefix = /^(evm|btc|sol|tron|zcash)/.exec(method)?.[1];
4892
5663
  const defaultAppName = prefix ? METHOD_PREFIX_TO_APP_NAME[prefix] : void 0;
4893
5664
  if (!defaultAppName) return this._ctx;
4894
5665
  return {
@@ -4922,6 +5693,209 @@ var LedgerConnectorBase = class {
4922
5693
  return error;
4923
5694
  }
4924
5695
  };
5696
+
5697
+ // src/transport/DmkTransport.ts
5698
+ import Transport from "@ledgerhq/hw-transport";
5699
+ var DmkTransport = class extends Transport {
5700
+ constructor(dmk, sessionId) {
5701
+ super();
5702
+ this._dmk = dmk;
5703
+ this._sessionId = sessionId;
5704
+ }
5705
+ async exchange(apdu) {
5706
+ const response = await this._dmk.sendApdu({
5707
+ sessionId: this._sessionId,
5708
+ apdu: new Uint8Array(apdu)
5709
+ });
5710
+ const { data, statusCode } = response;
5711
+ const result = Buffer.alloc(data.length + 2);
5712
+ if (data.length > 0) {
5713
+ result.set(data, 0);
5714
+ }
5715
+ result.set(statusCode, data.length);
5716
+ return result;
5717
+ }
5718
+ async close() {
5719
+ }
5720
+ };
5721
+
5722
+ // src/app/AppManager.ts
5723
+ import {
5724
+ CloseAppCommand,
5725
+ GetAppAndVersionCommand,
5726
+ OpenAppCommand,
5727
+ isSuccessCommandResult
5728
+ } from "@ledgerhq/device-management-kit";
5729
+ import { HardwareErrorCode as HardwareErrorCode8 } from "@onekeyfe/hwk-adapter-core";
5730
+ var APP_NAME_MAP = {
5731
+ ETH: "Ethereum",
5732
+ BTC: "Bitcoin",
5733
+ SOL: "Solana",
5734
+ TRX: "Tron",
5735
+ ZEC: "Zcash",
5736
+ XRP: "XRP",
5737
+ ADA: "Cardano",
5738
+ DOT: "Polkadot",
5739
+ ATOM: "Cosmos"
5740
+ };
5741
+ var DASHBOARD_APP_NAME = "BOLOS";
5742
+ var AppManager = class {
5743
+ constructor(dmk, options) {
5744
+ this._dmk = dmk;
5745
+ this._waitMs = options?.waitMs ?? 1e3;
5746
+ this._maxRetries = options?.maxRetries ?? 10;
5747
+ }
5748
+ /**
5749
+ * Return the Ledger app name for a given chain ticker,
5750
+ * or undefined if the chain is not supported.
5751
+ */
5752
+ static getAppName(chain) {
5753
+ return APP_NAME_MAP[chain];
5754
+ }
5755
+ /**
5756
+ * Ensure the target app is open on the device identified by `sessionId`.
5757
+ *
5758
+ * Flow:
5759
+ * 1. Check the currently running app.
5760
+ * 2. If it is already the target, return immediately.
5761
+ * 3. If a different app is running (not dashboard), close it first.
5762
+ * 4. Open the target app.
5763
+ * 5. Poll until the device confirms the target app is running.
5764
+ */
5765
+ /**
5766
+ * @param onConfirmOnDevice Called BEFORE OpenAppCommand is issued — the
5767
+ * device is about to display "Open <app>" on screen and wait for the
5768
+ * user's button press. UI consumers should show their "open app" prompt
5769
+ * in response. NOT called when the target app is already open (no user
5770
+ * interaction needed in that case).
5771
+ *
5772
+ * Important: OpenAppCommand is blocking. It does not resolve until the user
5773
+ * has physically confirmed on the device, so anything that runs AFTER
5774
+ * `await this._openApp(...)` lands AFTER the prompt is already gone.
5775
+ * Hence the callback must fire BEFORE that await.
5776
+ */
5777
+ async ensureAppOpen(sessionId, targetAppName, onConfirmOnDevice) {
5778
+ const currentApp = await this._getCurrentApp(sessionId);
5779
+ if (currentApp === targetAppName) {
5780
+ return;
5781
+ }
5782
+ if (!this._isDashboard(currentApp)) {
5783
+ await this._closeCurrentApp(sessionId);
5784
+ await this._waitForApp(sessionId, DASHBOARD_APP_NAME);
5785
+ }
5786
+ onConfirmOnDevice?.();
5787
+ await this._openApp(sessionId, targetAppName);
5788
+ await this._waitForApp(sessionId, targetAppName);
5789
+ }
5790
+ // ---------------------------------------------------------------------------
5791
+ // Private helpers
5792
+ // ---------------------------------------------------------------------------
5793
+ async _getCurrentApp(sessionId) {
5794
+ const result = await this._dmk.sendCommand({
5795
+ sessionId,
5796
+ command: new GetAppAndVersionCommand()
5797
+ });
5798
+ if (isSuccessCommandResult(result)) {
5799
+ debugLog("[AppManager] currentApp:", result.data.name);
5800
+ return result.data.name;
5801
+ }
5802
+ const errResult = result;
5803
+ const dmkErr = errResult.error ?? {};
5804
+ const original = dmkErr.originalError;
5805
+ debugLog(
5806
+ "[AppManager] _getCurrentApp failed sessionId=",
5807
+ sessionId,
5808
+ "tag=",
5809
+ dmkErr._tag,
5810
+ "errorCode=",
5811
+ dmkErr.errorCode,
5812
+ "message=",
5813
+ dmkErr.message,
5814
+ "originalErrorMessage=",
5815
+ original?.message ?? String(original ?? "")
5816
+ );
5817
+ throw Object.assign(
5818
+ new Error(
5819
+ dmkErr.message ?? "Failed to get current app from device"
5820
+ ),
5821
+ {
5822
+ _tag: dmkErr._tag,
5823
+ errorCode: dmkErr.errorCode,
5824
+ originalError: original
5825
+ }
5826
+ );
5827
+ }
5828
+ async _openApp(sessionId, appName) {
5829
+ const result = await this._dmk.sendCommand({
5830
+ sessionId,
5831
+ command: new OpenAppCommand({ appName })
5832
+ });
5833
+ if (!isSuccessCommandResult(result)) {
5834
+ const dmkErr = result.error;
5835
+ const errorCode = "errorCode" in dmkErr && dmkErr.errorCode != null ? String(dmkErr.errorCode) : "";
5836
+ const message = "message" in dmkErr && typeof dmkErr.message === "string" ? dmkErr.message : "";
5837
+ debugLog(
5838
+ "[AppManager] openApp failed:",
5839
+ appName,
5840
+ "errorCode:",
5841
+ errorCode,
5842
+ "tag:",
5843
+ dmkErr._tag
5844
+ );
5845
+ let code;
5846
+ if (errorCode === "6807" || /unknown application/i.test(message)) {
5847
+ code = HardwareErrorCode8.AppNotInstalled;
5848
+ } else if (errorCode === "5501" || dmkErr._tag === "ActionRefusedError") {
5849
+ code = HardwareErrorCode8.UserRejected;
5850
+ }
5851
+ throw Object.assign(new Error(`Failed to open "${appName}"`), {
5852
+ _tag: ERROR_TAG.OpenAppCommand,
5853
+ code,
5854
+ errorCode,
5855
+ statusCode: errorCode,
5856
+ appName,
5857
+ originalError: dmkErr
5858
+ });
5859
+ }
5860
+ }
5861
+ async _closeCurrentApp(sessionId) {
5862
+ debugLog("[AppManager] closeCurrentApp");
5863
+ await this._dmk.sendCommand({
5864
+ sessionId,
5865
+ command: new CloseAppCommand()
5866
+ });
5867
+ }
5868
+ /**
5869
+ * Poll the device until the expected app is reported as running,
5870
+ * or throw after `_maxRetries` attempts.
5871
+ */
5872
+ async _waitForApp(sessionId, expectedAppName) {
5873
+ let lastSeen = "";
5874
+ for (let i = 0; i < this._maxRetries; i++) {
5875
+ await this._wait();
5876
+ const current = await this._getCurrentApp(sessionId);
5877
+ lastSeen = current;
5878
+ if (current === expectedAppName) {
5879
+ return;
5880
+ }
5881
+ }
5882
+ debugLog(
5883
+ "[AppManager] waitForApp exhausted: expected=",
5884
+ expectedAppName,
5885
+ "lastSeen=",
5886
+ lastSeen
5887
+ );
5888
+ throw new Error(
5889
+ `Ledger: failed to open "${expectedAppName}" after ${this._maxRetries} retries (last seen: ${lastSeen})`
5890
+ );
5891
+ }
5892
+ _isDashboard(appName) {
5893
+ return appName === DASHBOARD_APP_NAME;
5894
+ }
5895
+ _wait() {
5896
+ return new Promise((resolve) => setTimeout(resolve, this._waitMs));
5897
+ }
5898
+ };
4925
5899
  export {
4926
5900
  AppManager,
4927
5901
  DmkTransport,
@@ -4932,6 +5906,7 @@ export {
4932
5906
  SignerEth,
4933
5907
  SignerManager,
4934
5908
  SignerSol,
5909
+ SignerZcash,
4935
5910
  debugLog,
4936
5911
  deviceActionToPromise,
4937
5912
  isDeviceLockedError,