@onekeyfe/hwk-ledger-adapter 1.2.2-alpha.9 → 1.2.3-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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"]);
@@ -213,7 +224,6 @@ function isBlePairingFailureError(err) {
213
224
  return false;
214
225
  }
215
226
  var CONNECTION_LEVEL_TAGS = /* @__PURE__ */ new Set([
216
- ERROR_TAG.DeviceLocked,
217
227
  ERROR_TAG.DeviceNotAdvertising,
218
228
  ERROR_TAG.BlePairingTimeout,
219
229
  ERROR_TAG.BleGattBondingFailed,
@@ -448,11 +458,21 @@ function mapLedgerError(err, opts) {
448
458
  }
449
459
  const errAppName = err && typeof err === "object" ? err.appName : void 0;
450
460
  const appName = errAppName ?? opts?.defaultAppName;
451
- return { code, message: enrichErrorMessage(code, originalMessage), appName };
461
+ return {
462
+ code,
463
+ message: enrichErrorMessage(code, originalMessage),
464
+ origin: defaultOriginForCode(code),
465
+ appName
466
+ };
452
467
  }
453
468
 
454
469
  // src/adapter/methods/allNetworkGetAddress.ts
455
- import { HardwareErrorCode as HardwareErrorCode2, failure, runAllNetworkGetAddress } from "@onekeyfe/hwk-adapter-core";
470
+ import {
471
+ HardwareErrorCode as HardwareErrorCode2,
472
+ failure,
473
+ resolveHardwareOperationTarget,
474
+ runAllNetworkGetAddress
475
+ } from "@onekeyfe/hwk-adapter-core";
456
476
 
457
477
  // src/utils/sdkEventBus.ts
458
478
  var listeners = /* @__PURE__ */ new Set();
@@ -509,57 +529,81 @@ var LEDGER_BTC_NETWORK_COIN_MAP = {
509
529
  var LEDGER_UNSUPPORTED_ALLNETWORK_NETWORKS = /* @__PURE__ */ new Set(["doge", "dogecoin"]);
510
530
  function createAllNetworkGetAddress({
511
531
  callChain,
512
- getChainFingerprint
532
+ getChainFingerprint,
533
+ retainOperation,
534
+ errorToFailure
513
535
  }) {
514
536
  return async function allNetworkGetAddress(connectId, _deviceId, params) {
515
- debugLog("[LedgerAdapter][REQ]", { method: "allNetworkGetAddress", connectId, params });
537
+ debugLog("[LedgerAdapter][REQ]", {
538
+ method: "allNetworkGetAddress",
539
+ connectId,
540
+ itemCount: params.bundle.length
541
+ });
542
+ const target = resolveHardwareOperationTarget(connectId, params.operationId, "ledger");
543
+ if (!target.success) return target;
544
+ const effectiveTargetId = target.payload.targetId ?? "";
545
+ let releaseOperationRetention;
546
+ try {
547
+ releaseOperationRetention = target.payload.operationId ? retainOperation(target.payload.operationId) : void 0;
548
+ } catch (error) {
549
+ return errorToFailure(error);
550
+ }
516
551
  const installContext = {};
517
552
  const commonParams = {
518
- autoInstallApp: params.autoInstallApp
553
+ autoInstallApp: params.autoInstallApp,
554
+ operationId: target.payload.operationId,
555
+ knownConnections: params.knownConnections,
556
+ extra: params.extra,
557
+ allowDeviceSelection: params.allowDeviceSelection
519
558
  };
520
559
  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,
560
+ try {
561
+ const result = await runAllNetworkGetAddress({
562
+ connectId: effectiveTargetId,
563
+ deviceId: _deviceId,
564
+ params,
565
+ normalizeItem: normalizeLedgerAllNetworkItem,
566
+ buildUnsupportedNetworkResponse: (item) => isUnsupportedLedgerAllNetworkNetwork(item) ? buildUnsupportedNetworkResponse(item) : void 0,
567
+ callItem: async ({ method, chain, item }) => {
568
+ const itemDeviceId = getItemDeviceId(item) ?? chainFingerprints.get(chain) ?? "";
569
+ return callAllNetworkMethod(
570
+ callChain,
571
+ effectiveTargetId,
572
+ itemDeviceId,
573
+ method,
574
+ item,
575
+ commonParams,
576
+ installContext
577
+ );
578
+ },
579
+ attachIdentity: async ({ item, chain, payload }) => attachLedgerIdentity(
580
+ getChainFingerprint,
581
+ effectiveTargetId,
534
582
  item,
535
- commonParams,
583
+ chain,
584
+ payload,
585
+ chainFingerprints,
536
586
  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;
587
+ ),
588
+ shouldAbortBundle: isTopLevelAllNetworkFailure,
589
+ buildTopLevelFailure: (response) => {
590
+ const code = response.payload?.code ?? HardwareErrorCode2.DeviceMismatch;
591
+ return failure(
592
+ code,
593
+ response.payload?.error ?? "All-network get-address aborted",
594
+ response.payload?.params
595
+ );
596
+ }
597
+ });
598
+ debugLog("[LedgerAdapter][RES]", {
599
+ method: "allNetworkGetAddress",
600
+ success: result.success,
601
+ payload: result
602
+ });
603
+ return result;
604
+ } finally {
605
+ releaseOperationRetention?.();
606
+ }
563
607
  };
564
608
  }
565
609
  function isTopLevelAllNetworkFailure(response) {
@@ -567,7 +611,7 @@ function isTopLevelAllNetworkFailure(response) {
567
611
  return false;
568
612
  }
569
613
  const code = response.payload?.code;
570
- return code === HardwareErrorCode2.DeviceMismatch || code === HardwareErrorCode2.UserAborted || code === HardwareErrorCode2.UserRejected;
614
+ return code === HardwareErrorCode2.DeviceMismatch || code === HardwareErrorCode2.DeviceDisconnected || code === HardwareErrorCode2.OperationTimeout || code === HardwareErrorCode2.TransportError || code === HardwareErrorCode2.OperationEnded || code === HardwareErrorCode2.OperationNotFound || code === HardwareErrorCode2.UserAborted || code === HardwareErrorCode2.UserRejected;
571
615
  }
572
616
  function getItemDeviceId(item) {
573
617
  const { deviceId } = item;
@@ -597,8 +641,13 @@ function normalizeLedgerAllNetworkItem(method, item) {
597
641
  const coin = LEDGER_BTC_NETWORK_COIN_MAP[item.network];
598
642
  return coin ? { ...item, coin } : item;
599
643
  }
600
- async function attachLedgerIdentity(getChainFingerprint, connectId, item, chain, payload, chainFingerprints) {
601
- const fingerprint = getItemDeviceId(item) || chainFingerprints.get(chain) || await bootstrapChainFingerprint(getChainFingerprint, connectId, chain);
644
+ async function attachLedgerIdentity(getChainFingerprint, connectId, item, chain, payload, chainFingerprints, context) {
645
+ const fingerprint = getItemDeviceId(item) || chainFingerprints.get(chain) || await bootstrapChainFingerprint(
646
+ getChainFingerprint,
647
+ context.connection?.connectId ?? connectId,
648
+ chain,
649
+ context
650
+ );
602
651
  if (!fingerprint) {
603
652
  return buildFingerprintBootstrapFailure(item, chain);
604
653
  }
@@ -619,8 +668,8 @@ async function attachLedgerIdentity(getChainFingerprint, connectId, item, chain,
619
668
  }
620
669
  };
621
670
  }
622
- async function bootstrapChainFingerprint(getChainFingerprint, connectId, chain) {
623
- const response = await getChainFingerprint(connectId, chain);
671
+ async function bootstrapChainFingerprint(getChainFingerprint, connectId, chain, context) {
672
+ const response = await getChainFingerprint(connectId, chain, context);
624
673
  return response.success ? response.payload : "";
625
674
  }
626
675
  function buildFingerprintBootstrapFailure(item, chain) {
@@ -725,8 +774,27 @@ var _LedgerAdapter = class _LedgerAdapter {
725
774
  constructor(connector, options) {
726
775
  this.vendor = "ledger";
727
776
  this.emitter = new TypedEventEmitter();
777
+ this._operations = new OperationRegistry({
778
+ vendor: "ledger",
779
+ onEnded: (operation, reason) => {
780
+ const binding = this._pendingOperationBindings.get(operation.operationId);
781
+ if (binding?.selectedConnection?.requestId === this._bindingSelectionRequestId) {
782
+ this._finishBleBinding("cancelled");
783
+ }
784
+ this._pendingOperationBindings.delete(operation.operationId);
785
+ this.emitter.emit(SDK.OPERATION_ENDED, {
786
+ type: SDK.OPERATION_ENDED,
787
+ payload: { operationId: operation.operationId, reason }
788
+ });
789
+ if (reason === "timeout") {
790
+ void this._releaseOperationConnection(operation);
791
+ }
792
+ }
793
+ });
728
794
  this._discoveredDevices = /* @__PURE__ */ new Map();
729
795
  this._sessions = /* @__PURE__ */ new Map();
796
+ this._pendingOperationBindings = /* @__PURE__ */ new Map();
797
+ this._verifiedBleReconnectTargets = /* @__PURE__ */ new Map();
730
798
  this._uiRegistry = new UiRequestRegistry();
731
799
  // BTC App rejects account index >= 100 unless display=true. Cached per
732
800
  // adapter instance: first 100+ path asks the user once via UI request,
@@ -740,10 +808,30 @@ var _LedgerAdapter = class _LedgerAdapter {
740
808
  this._deviceAuthenticityQueueTail = Promise.resolve();
741
809
  // Shared across concurrent callers — only `cancel()` aborts.
742
810
  this._doConnectAbortController = null;
811
+ this._unsettledConnectorOperations = /* @__PURE__ */ new Map();
812
+ this._connectorIdleWaiters = /* @__PURE__ */ new Set();
813
+ this._resetPromise = null;
814
+ this._stateGeneration = 0;
815
+ this._connectorTeardownTail = Promise.resolve();
816
+ this._pendingConnectorTeardowns = 0;
817
+ this._activeOperationJobs = /* @__PURE__ */ new Set();
818
+ this._pendingOperationDisconnects = /* @__PURE__ */ new Set();
743
819
  this._installProgressLastEmittedValue = -Infinity;
744
820
  this.allNetworkGetAddress = createAllNetworkGetAddress({
745
821
  callChain: this.callChain.bind(this),
746
- getChainFingerprint: (connectId, chain) => this.getChainFingerprint(connectId, "", chain)
822
+ getChainFingerprint: async (connectId, chain, context) => {
823
+ try {
824
+ const fingerprint = await this._computeChainFingerprint(
825
+ chain,
826
+ (method, params) => this.connectorCall(connectId, method, params, void 0, void 0, void 0, context)
827
+ );
828
+ return success(fingerprint);
829
+ } catch (error) {
830
+ return this.errorToFailure(error);
831
+ }
832
+ },
833
+ retainOperation: (operationId) => this._operations.retain(operationId),
834
+ errorToFailure: (error) => this.errorToFailure(error)
747
835
  });
748
836
  // ---------------------------------------------------------------------------
749
837
  // Private helpers
@@ -753,7 +841,7 @@ var _LedgerAdapter = class _LedgerAdapter {
753
841
  *
754
842
  * - If a session already exists for the given connectId, reuse it.
755
843
  * - If ANY session exists (Ledger IDs are ephemeral), reuse it.
756
- * - Otherwise: search → exactly 1 USB device auto-connects; multiple or none throws.
844
+ * - Otherwise: search → one USB device auto-connects; multiple asks the host to choose.
757
845
  */
758
846
  // Mutex for ensureConnected — prevents concurrent calls from establishing duplicate connections
759
847
  this._connectingPromise = null;
@@ -770,6 +858,12 @@ var _LedgerAdapter = class _LedgerAdapter {
770
858
  });
771
859
  };
772
860
  this.deviceDisconnectHandler = (data) => {
861
+ const activeOperation = this._operations.findActiveByConnectionKey(data.connectId);
862
+ if (activeOperation && this._activeOperationJobs.has(activeOperation.operationId)) {
863
+ this._pendingOperationDisconnects.add(activeOperation.operationId);
864
+ } else {
865
+ this._operations.endByConnectionKey(data.connectId, "disconnect");
866
+ }
773
867
  this._discoveredDevices.delete(data.connectId);
774
868
  this._sessions.delete(data.connectId);
775
869
  this.emitter.emit(DEVICE.DISCONNECT, {
@@ -826,11 +920,28 @@ var _LedgerAdapter = class _LedgerAdapter {
826
920
  this._jobQueue = new DeviceJobQueue();
827
921
  this.registerEventListeners();
828
922
  }
923
+ _finishBleBinding(status) {
924
+ const selectionRequestId = this._bindingSelectionRequestId;
925
+ this._bindingSelectionRequestId = void 0;
926
+ if (selectionRequestId)
927
+ this.emitter.emit(UI_REQUEST.DEVICE_BINDING_STATUS, {
928
+ type: UI_REQUEST.DEVICE_BINDING_STATUS,
929
+ payload: { selectionRequestId, status }
930
+ });
931
+ }
932
+ _isBleConnection() {
933
+ return isLedgerBleConnectionType(this._activeConnectionType ?? this.connector.connectionType);
934
+ }
829
935
  // Transport
830
936
  get activeTransport() {
831
- return isLedgerBleConnectionType(this.connector.connectionType) ? "ble" : "hid";
937
+ return this._isBleConnection() ? "ble" : "hid";
832
938
  }
833
939
  getAvailableTransports() {
940
+ if (this.connector.availableTransports) {
941
+ return this.connector.availableTransports.map(
942
+ (transport) => transport === "ble" ? "ble" : "hid"
943
+ );
944
+ }
834
945
  return this.activeTransport ? [this.activeTransport] : [];
835
946
  }
836
947
  // Connector is bound at construction; switching requires a new adapter.
@@ -847,21 +958,39 @@ var _LedgerAdapter = class _LedgerAdapter {
847
958
  * The next operation will re-discover and re-connect automatically.
848
959
  */
849
960
  resetState() {
961
+ void this._resetStateAndDisconnectSessions();
962
+ }
963
+ _resetStateAndDisconnectSessions() {
964
+ if (this._resetPromise) return this._resetPromise;
965
+ const sessionIds = new Set(this._sessions.values());
966
+ this._stateGeneration += 1;
967
+ this._finishBleBinding("cancelled");
968
+ this._operations.endAll("runtime-reset");
969
+ this._doConnectAbortController?.abort();
850
970
  this._discoveredDevices.clear();
851
971
  this._sessions.clear();
972
+ this._verifiedBleReconnectTargets.clear();
852
973
  this._connectingPromise = null;
974
+ this._doConnectAbortController = null;
853
975
  this._uiRegistry.reset();
854
976
  this._jobQueue.clear();
855
977
  this._btcHighIndexConfirmedThisSession = false;
978
+ const resetPromise = this._runConnectorTeardown(async () => {
979
+ for (const sessionId of sessionIds) {
980
+ await this.connector.disconnect(sessionId).catch(() => void 0);
981
+ }
982
+ });
983
+ this._resetPromise = resetPromise;
984
+ return resetPromise.finally(() => {
985
+ if (this._resetPromise === resetPromise) {
986
+ this._resetPromise = null;
987
+ }
988
+ });
856
989
  }
857
990
  async dispose() {
858
- this._uiRegistry.reset();
859
- this._jobQueue.clear();
991
+ await this._resetStateAndDisconnectSessions();
860
992
  this.unregisterEventListeners();
861
993
  this.connector.reset();
862
- this._discoveredDevices.clear();
863
- this._sessions.clear();
864
- this._btcHighIndexConfirmedThisSession = false;
865
994
  this.emitter.removeAllListeners();
866
995
  }
867
996
  uiResponse(response) {
@@ -871,17 +1000,32 @@ var _LedgerAdapter = class _LedgerAdapter {
871
1000
  // Device management
872
1001
  // ---------------------------------------------------------------------------
873
1002
  async searchDevices(options) {
1003
+ return this._searchDevices(options);
1004
+ }
1005
+ async _searchDevices(options, signal) {
874
1006
  debugLog("[LedgerAdapter][REQ]", { method: "searchDevices", params: options });
875
1007
  try {
876
1008
  if (options?.resetSession) {
877
- this._doConnectAbortController?.abort();
878
- this._sessions.clear();
879
- this._connectingPromise = null;
880
- this._doConnectAbortController = null;
881
- this._btcHighIndexConfirmedThisSession = false;
1009
+ await this._resetStateAndDisconnectSessions();
1010
+ } else {
1011
+ await this._connectorTeardownTail;
1012
+ }
1013
+ await this._ensureDevicePermission(void 0, void 0, signal);
1014
+ if (signal) _LedgerAdapter._throwIfAborted(signal);
1015
+ const stateGeneration = this._stateGeneration;
1016
+ const devices = await this.connector.searchDevices(
1017
+ options?.transportType ? {
1018
+ transportType: options.transportType,
1019
+ waitForAll: options.waitForAllTransports
1020
+ } : void 0
1021
+ );
1022
+ if (signal) _LedgerAdapter._throwIfAborted(signal);
1023
+ if (stateGeneration !== this._stateGeneration) {
1024
+ throw createHwkError({
1025
+ code: HardwareErrorCode3.UserAborted,
1026
+ message: "Ledger discovery was reset"
1027
+ });
882
1028
  }
883
- await this._ensureDevicePermission();
884
- const devices = await this.connector.searchDevices();
885
1029
  this._discoveredDevices.clear();
886
1030
  for (const d of devices) {
887
1031
  if (d.connectId) {
@@ -889,7 +1033,7 @@ var _LedgerAdapter = class _LedgerAdapter {
889
1033
  }
890
1034
  }
891
1035
  if (this._discoveredDevices.size === 0) {
892
- await this._ensureDevicePermission();
1036
+ await this._ensureDevicePermission(void 0, void 0, signal);
893
1037
  }
894
1038
  const result = Array.from(this._discoveredDevices.values());
895
1039
  debugLog("[LedgerAdapter][RES]", {
@@ -908,41 +1052,285 @@ var _LedgerAdapter = class _LedgerAdapter {
908
1052
  throw err;
909
1053
  }
910
1054
  }
1055
+ async searchDeviceTargets(options) {
1056
+ const devices = await this.searchDevices(options);
1057
+ return devices.map((device) => ({
1058
+ searchTargetId: device.connectId,
1059
+ searchTargetReusePolicy: resolveSearchTargetReusePolicy(device),
1060
+ vendor: "ledger",
1061
+ connectionType: device.connectionType,
1062
+ kind: "physical",
1063
+ label: device.label,
1064
+ model: device.model,
1065
+ modelName: device.modelName,
1066
+ serialNumber: device.serialNumber
1067
+ }));
1068
+ }
1069
+ async listConnectionTargets(options) {
1070
+ const targets = await this.searchDeviceTargets(options);
1071
+ return targets.map(({ searchTargetId, ...target }) => ({
1072
+ ...target,
1073
+ targetId: searchTargetId
1074
+ }));
1075
+ }
911
1076
  // USB single-session invariant: evict all sessions, best-effort (see connectDevice).
912
- async _evictAllSessions() {
1077
+ async _evictAllSessions(preserveOperationId) {
1078
+ this._operations.endAll("explicit", preserveOperationId);
913
1079
  if (this._sessions.size === 0) return;
914
1080
  const stale = [...this._sessions.values()];
915
1081
  this._sessions.clear();
916
- for (const sid of stale) {
917
- try {
918
- await this.connector.disconnect(sid);
919
- } catch {
1082
+ await this._runConnectorTeardown(async () => {
1083
+ for (const sid of stale) {
1084
+ try {
1085
+ await this.connector.disconnect(sid);
1086
+ } catch {
1087
+ }
920
1088
  }
921
- }
1089
+ });
922
1090
  }
923
1091
  static _createDeviceBusyError(method) {
924
1092
  return Object.assign(new Error(`Ledger device is busy while calling ${method}`), {
925
1093
  code: HardwareErrorCode3.DeviceBusy
926
1094
  });
927
1095
  }
928
- async connectDevice(connectId) {
1096
+ async connectDevice(searchTargetId) {
1097
+ try {
1098
+ return await this._jobQueue.enqueue(
1099
+ searchTargetId || "__ledger_connect__",
1100
+ async () => {
1101
+ const connected = await this._connectTarget(searchTargetId);
1102
+ if (!connected.success) return connected;
1103
+ return this._createOperation(searchTargetId, connected.payload);
1104
+ },
1105
+ {
1106
+ label: "connectDevice",
1107
+ rejectIfBusy: true,
1108
+ busyError: _LedgerAdapter._createDeviceBusyError("connectDevice")
1109
+ }
1110
+ );
1111
+ } catch (error) {
1112
+ return this.errorToFailure(error);
1113
+ }
1114
+ }
1115
+ async bindBleDevice(params) {
1116
+ if (params.identity.vendor !== "ledger" || !params.identity.value) {
1117
+ return failure2(HardwareErrorCode3.InvalidParams, "Ledger wallet identity is required");
1118
+ }
1119
+ const { chain, value: expectedFingerprint } = params.identity;
1120
+ try {
1121
+ return await this._jobQueue.enqueue(
1122
+ expectedFingerprint,
1123
+ async (signal) => {
1124
+ if (!this.getAvailableTransports().includes("ble")) {
1125
+ throw createHwkError({
1126
+ code: HardwareErrorCode3.TransportNotAvailable,
1127
+ message: "Ledger Bluetooth transport is not available"
1128
+ });
1129
+ }
1130
+ this._activeConnectionType = "ble";
1131
+ await this._ensureDevicePermission(void 0, void 0, signal);
1132
+ const attempt = {
1133
+ extra: params.extra,
1134
+ bindingReason: "manual-rebind"
1135
+ };
1136
+ try {
1137
+ for (; ; ) {
1138
+ const connectId = await this._connectFirstOrSelect(
1139
+ [],
1140
+ void 0,
1141
+ true,
1142
+ void 0,
1143
+ attempt,
1144
+ signal
1145
+ );
1146
+ const sessionId = this._sessions.get(connectId);
1147
+ if (!sessionId) {
1148
+ throw createHwkError({
1149
+ code: HardwareErrorCode3.DeviceDisconnected,
1150
+ message: "Selected Ledger connection ended"
1151
+ });
1152
+ }
1153
+ let saved = false;
1154
+ try {
1155
+ const installContext = {
1156
+ connection: { connectId, sessionId }
1157
+ };
1158
+ const fingerprint = await this._computeChainFingerprint(
1159
+ chain,
1160
+ (method, callParams) => this._runConnectorCall(
1161
+ connectId,
1162
+ method,
1163
+ callParams,
1164
+ signal,
1165
+ void 0,
1166
+ void 0,
1167
+ { autoInstallApp: true },
1168
+ installContext
1169
+ )
1170
+ );
1171
+ if (fingerprint !== expectedFingerprint) {
1172
+ attempt.rejectedConnectIds ?? (attempt.rejectedConnectIds = /* @__PURE__ */ new Set());
1173
+ attempt.rejectedConnectIds.add(connectId);
1174
+ attempt.rejectedConnectId = connectId;
1175
+ } else {
1176
+ const persisted = await this._publishVerifiedBleBinding(
1177
+ connectId,
1178
+ chain,
1179
+ fingerprint,
1180
+ attempt,
1181
+ void 0,
1182
+ signal
1183
+ );
1184
+ if (!persisted) {
1185
+ throw createHwkError({
1186
+ code: HardwareErrorCode3.UnknownError,
1187
+ message: "Bluetooth binding could not be saved",
1188
+ origin: "host"
1189
+ });
1190
+ }
1191
+ saved = true;
1192
+ return success(connectId);
1193
+ }
1194
+ } finally {
1195
+ if (!saved && this._sessions.get(connectId) === sessionId) {
1196
+ this._sessions.delete(connectId);
1197
+ const teardown = this._runConnectorTeardown(
1198
+ () => this.connector.disconnect(sessionId)
1199
+ ).catch(() => void 0);
1200
+ if (!signal.aborted) await teardown;
1201
+ }
1202
+ }
1203
+ }
1204
+ } catch (error) {
1205
+ this._finishBleBinding(signal.aborted ? "cancelled" : "failed");
1206
+ throw error;
1207
+ }
1208
+ },
1209
+ {
1210
+ label: "bindBleDevice",
1211
+ rejectIfBusy: true,
1212
+ busyError: _LedgerAdapter._createDeviceBusyError("bindBleDevice")
1213
+ }
1214
+ );
1215
+ } catch (error) {
1216
+ return this.errorToFailure(error);
1217
+ }
1218
+ }
1219
+ async acquireOperation(connectId, context) {
1220
+ try {
1221
+ return await this._jobQueue.enqueue(
1222
+ connectId || "__ledger_acquire__",
1223
+ async (signal) => {
1224
+ const transport = this._isBleConnection() ? "ble" : "usb";
1225
+ const hint = context.knownConnections?.find(
1226
+ (connection) => connection.transport === transport
1227
+ );
1228
+ const target = hint && hint.transport !== "qr" ? hint.connectId : connectId;
1229
+ await this._ensureDevicePermission(target, void 0, signal);
1230
+ const attempt = {
1231
+ ...context,
1232
+ extra: context.extra ? { ...context.extra } : void 0
1233
+ };
1234
+ try {
1235
+ const resolvedConnectId = await this.ensureConnected(
1236
+ target,
1237
+ signal,
1238
+ true,
1239
+ void 0,
1240
+ attempt
1241
+ );
1242
+ _LedgerAdapter._throwIfAborted(signal);
1243
+ const result = this._createOperation(connectId, resolvedConnectId);
1244
+ if (result.success && attempt.selectedConnection) {
1245
+ this._pendingOperationBindings.set(result.payload, attempt);
1246
+ }
1247
+ return result;
1248
+ } catch (error) {
1249
+ this._finishBleBinding(signal.aborted ? "cancelled" : "failed");
1250
+ throw error;
1251
+ }
1252
+ },
1253
+ {
1254
+ label: "acquireOperation",
1255
+ rejectIfBusy: true,
1256
+ busyError: _LedgerAdapter._createDeviceBusyError("acquireOperation")
1257
+ }
1258
+ );
1259
+ } catch (error) {
1260
+ return this.errorToFailure(error);
1261
+ }
1262
+ }
1263
+ _createOperation(searchTargetId, resolvedConnectId) {
1264
+ this._operations.endByConnectionKey(resolvedConnectId, "explicit");
1265
+ const sessionId = this._sessions.get(resolvedConnectId);
1266
+ if (sessionId) this._operations.endByConnectionKey(sessionId, "explicit");
1267
+ const device = this._discoveredDevices.get(resolvedConnectId) ?? {
1268
+ vendor: "ledger",
1269
+ model: "unknown",
1270
+ firmwareVersion: "",
1271
+ deviceId: "",
1272
+ connectId: resolvedConnectId,
1273
+ connectionType: this.connector.connectionType
1274
+ };
1275
+ const operation = this._operations.create({
1276
+ searchTargetId,
1277
+ connectId: resolvedConnectId,
1278
+ device,
1279
+ connectionKeys: [this._sessions.get(resolvedConnectId) ?? ""]
1280
+ });
1281
+ return success(operation.operationId);
1282
+ }
1283
+ async _connectTarget(connectId, preserveOperationId, signal) {
929
1284
  debugLog("[LedgerAdapter][REQ]", { method: "connectDevice", connectId, params: { connectId } });
930
1285
  try {
931
- if (isLedgerBleConnectionType(this.connector.connectionType) && !connectId) {
1286
+ this._assertConnectorReady("connectDevice");
1287
+ const discoveredType = this._discoveredDevices.get(connectId)?.connectionType;
1288
+ if (discoveredType === "usb" || discoveredType === "ble") {
1289
+ this._activeConnectionType = discoveredType;
1290
+ }
1291
+ if (this._isBleConnection() && !connectId) {
932
1292
  throw Object.assign(new Error("Ledger BLE connectId is required."), {
933
1293
  code: HardwareErrorCode3.DeviceNotFound
934
1294
  });
935
1295
  }
936
- if (!isLedgerBleConnectionType(this.connector.connectionType)) {
937
- await this._evictAllSessions();
1296
+ await this._ensureDevicePermission(connectId, void 0, signal);
1297
+ if (signal) _LedgerAdapter._throwIfAborted(signal);
1298
+ if (this.connector.availableTransports && this.connector.availableTransports.length > 1) {
1299
+ await this._evictAllSessions(preserveOperationId);
1300
+ } else if (this._isBleConnection()) {
1301
+ const previousSessionId = this._sessions.get(connectId);
1302
+ this._operations.endByConnectionKey(connectId, "explicit", preserveOperationId);
1303
+ if (previousSessionId) {
1304
+ this._operations.endByConnectionKey(previousSessionId, "explicit", preserveOperationId);
1305
+ this._sessions.delete(connectId);
1306
+ await this.connector.disconnect(previousSessionId).catch(() => void 0);
1307
+ }
1308
+ } else {
1309
+ await this._evictAllSessions(preserveOperationId);
1310
+ }
1311
+ if (signal) _LedgerAdapter._throwIfAborted(signal);
1312
+ const stateGeneration = this._stateGeneration;
1313
+ const releaseOperation = this._retainConnectorOperation(`connect:${connectId}`);
1314
+ let session;
1315
+ try {
1316
+ session = this.connector.availableTransports?.length ? await this.connector.connect(connectId, {
1317
+ transportType: this._activeConnectionType ?? "usb"
1318
+ }) : await this.connector.connect(connectId);
1319
+ if (signal?.aborted || stateGeneration !== this._stateGeneration) {
1320
+ await this.connector.disconnect(session.sessionId).catch(() => void 0);
1321
+ throw Object.assign(new Error("Ledger connection aborted"), {
1322
+ code: HardwareErrorCode3.UserAborted
1323
+ });
1324
+ }
1325
+ } finally {
1326
+ releaseOperation();
938
1327
  }
939
- await this._ensureDevicePermission(connectId);
940
- const session = await this.connector.connect(connectId);
941
- this._sessions.set(connectId, session.sessionId);
1328
+ const resolvedConnectId = session.deviceInfo?.connectId || connectId;
1329
+ this._sessions.set(resolvedConnectId, session.sessionId);
942
1330
  if (session.deviceInfo) {
943
- this._discoveredDevices.set(connectId, session.deviceInfo);
1331
+ this._discoveredDevices.set(resolvedConnectId, session.deviceInfo);
944
1332
  }
945
- const result = success(connectId);
1333
+ const result = success(resolvedConnectId);
946
1334
  debugLog("[LedgerAdapter][RES]", { method: "connectDevice", success: true, payload: result });
947
1335
  return result;
948
1336
  } catch (err) {
@@ -955,30 +1343,60 @@ var _LedgerAdapter = class _LedgerAdapter {
955
1343
  return failureResult;
956
1344
  }
957
1345
  }
958
- async disconnectDevice(connectId) {
1346
+ async releaseOperation(operationId) {
1347
+ const operation = this._operations.find(operationId);
1348
+ if (!operation) {
1349
+ this._operations.resolve(operationId);
1350
+ return;
1351
+ }
1352
+ const endedOperation = this._operations.end(operationId, "explicit");
1353
+ if (!endedOperation) return;
1354
+ const { connectId } = operation;
959
1355
  debugLog("[LedgerAdapter][REQ]", {
960
- method: "disconnectDevice",
1356
+ method: "releaseOperation",
961
1357
  connectId,
962
1358
  params: { connectId }
963
1359
  });
964
1360
  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 });
1361
+ await this._releaseOperationConnection(endedOperation);
1362
+ debugLog("[LedgerAdapter][RES]", { method: "releaseOperation", success: true });
971
1363
  } catch (err) {
972
1364
  const e = err;
973
1365
  debugLog("[LedgerAdapter][RES]", {
974
- method: "disconnectDevice",
1366
+ method: "releaseOperation",
975
1367
  success: false,
976
1368
  error: { message: e?.message, _tag: e?._tag, code: e?.code ?? e?.errorCode }
977
1369
  });
978
1370
  throw err;
979
1371
  }
980
1372
  }
981
- async getDeviceInfo(connectId, deviceId) {
1373
+ async _releaseOperationConnection(operation) {
1374
+ const sessionIds = /* @__PURE__ */ new Set();
1375
+ for (const [connectId, sessionId] of this._sessions) {
1376
+ if (connectId === operation.connectId || operation.connectionKeys.includes(connectId) || operation.connectionKeys.includes(sessionId)) {
1377
+ this._sessions.delete(connectId);
1378
+ sessionIds.add(sessionId);
1379
+ }
1380
+ }
1381
+ for (const sessionId of sessionIds) {
1382
+ await this._runConnectorTeardown(
1383
+ () => this.connector.disconnect(sessionId).catch(() => void 0)
1384
+ );
1385
+ }
1386
+ }
1387
+ async _releaseLostOperationConnection(operationId) {
1388
+ const endedOperation = this._operations.end(operationId, "disconnect");
1389
+ if (!endedOperation) return;
1390
+ this._discoveredDevices.delete(endedOperation.connectId);
1391
+ await this._releaseOperationConnection(endedOperation);
1392
+ }
1393
+ async getDeviceInfo(connectIdOrOperationId, deviceId) {
1394
+ let connectId;
1395
+ try {
1396
+ connectId = isHardwareOperationId(connectIdOrOperationId) ? this._operations.resolve(connectIdOrOperationId).connectId : connectIdOrOperationId;
1397
+ } catch (error) {
1398
+ return this.errorToFailure(error);
1399
+ }
982
1400
  debugLog("[LedgerAdapter][REQ]", {
983
1401
  method: "getDeviceInfo",
984
1402
  connectId,
@@ -1016,9 +1434,6 @@ var _LedgerAdapter = class _LedgerAdapter {
1016
1434
  throw err;
1017
1435
  }
1018
1436
  }
1019
- getSupportedChains() {
1020
- return ["evm", "btc", "sol", "tron"];
1021
- }
1022
1437
  // ---------------------------------------------------------------------------
1023
1438
  // Chain call helper
1024
1439
  // ---------------------------------------------------------------------------
@@ -1053,13 +1468,21 @@ var _LedgerAdapter = class _LedgerAdapter {
1053
1468
  if (params && typeof params === "object") {
1054
1469
  const {
1055
1470
  autoInstallApp,
1471
+ operationId,
1056
1472
  passphraseState: _passphraseState,
1057
1473
  useEmptyPassphrase: _useEmptyPassphrase,
1474
+ knownConnections,
1475
+ extra,
1476
+ allowDeviceSelection,
1058
1477
  ...rest
1059
1478
  } = params;
1060
1479
  return {
1061
1480
  commonParams: {
1062
- autoInstallApp: typeof autoInstallApp === "boolean" ? autoInstallApp : void 0
1481
+ autoInstallApp: typeof autoInstallApp === "boolean" ? autoInstallApp : void 0,
1482
+ operationId: typeof operationId === "string" ? operationId : void 0,
1483
+ knownConnections,
1484
+ extra,
1485
+ allowDeviceSelection: typeof allowDeviceSelection === "boolean" ? allowDeviceSelection : void 0
1063
1486
  },
1064
1487
  rest
1065
1488
  };
@@ -1235,6 +1658,27 @@ var _LedgerAdapter = class _LedgerAdapter {
1235
1658
  );
1236
1659
  }
1237
1660
  // ---------------------------------------------------------------------------
1661
+ // Zcash chain methods (viewing key + shielded address reads; Zcash app >= 3.8.0)
1662
+ // ---------------------------------------------------------------------------
1663
+ zcashGetFullViewingKey(connectId, deviceId, params) {
1664
+ return this.callChainWithMergedParams(
1665
+ connectId,
1666
+ deviceId,
1667
+ "zcash",
1668
+ "zcashGetFullViewingKey",
1669
+ params
1670
+ );
1671
+ }
1672
+ zcashGetShieldedAddress(connectId, deviceId, params) {
1673
+ return this.callChainWithMergedParams(
1674
+ connectId,
1675
+ deviceId,
1676
+ "zcash",
1677
+ "zcashGetShieldedAddress",
1678
+ params
1679
+ );
1680
+ }
1681
+ // ---------------------------------------------------------------------------
1238
1682
  // App management — OS-level Ledger app install / list. Bypasses fingerprint
1239
1683
  // and chain-handler dispatch; installApp progress is forwarded to the adapter
1240
1684
  // emitter via 'ui-event' AppInstallProgress events.
@@ -1323,7 +1767,7 @@ var _LedgerAdapter = class _LedgerAdapter {
1323
1767
  );
1324
1768
  }
1325
1769
  await this.connector.configure({ ledgerGenuineCheckWebSocketUrl: relayUrl });
1326
- this.resetState();
1770
+ await this._resetStateAndDisconnectSessions();
1327
1771
  }
1328
1772
  const result = await this.connectorCall(connectId, "getDeviceGenuineCheck", {});
1329
1773
  if (!result.isGenuine) {
@@ -1349,10 +1793,10 @@ var _LedgerAdapter = class _LedgerAdapter {
1349
1793
  if (relayUrl) {
1350
1794
  try {
1351
1795
  await this.connector.configure?.({ ledgerGenuineCheckWebSocketUrl: void 0 });
1352
- this.resetState();
1796
+ await this._resetStateAndDisconnectSessions();
1353
1797
  } catch {
1354
1798
  this.connector.reset();
1355
- this.resetState();
1799
+ await this._resetStateAndDisconnectSessions();
1356
1800
  }
1357
1801
  }
1358
1802
  }
@@ -1364,6 +1808,20 @@ var _LedgerAdapter = class _LedgerAdapter {
1364
1808
  this.emitter.off(event, listener);
1365
1809
  }
1366
1810
  cancel(connectId) {
1811
+ const namedOperationIsLive = (id) => {
1812
+ try {
1813
+ this._operations.resolve(id);
1814
+ return true;
1815
+ } catch {
1816
+ return false;
1817
+ }
1818
+ };
1819
+ if (isHardwareOperationId(connectId) && !namedOperationIsLive(connectId)) {
1820
+ debugLog("[LedgerAdapter] cancel target already ended; nothing to cancel", {
1821
+ connectId
1822
+ });
1823
+ return;
1824
+ }
1367
1825
  const userAbortReason = Object.assign(new Error("User aborted operation"), {
1368
1826
  code: HardwareErrorCode3.UserAborted,
1369
1827
  _tag: ERROR_TAG.UserAborted
@@ -1374,14 +1832,37 @@ var _LedgerAdapter = class _LedgerAdapter {
1374
1832
  this._lastCancelReason = void 0;
1375
1833
  }
1376
1834
  }, 2e3);
1835
+ const activeJobId = this._jobQueue.getActiveJob()?.deviceId;
1836
+ let operationId;
1837
+ if (isHardwareOperationId(connectId)) {
1838
+ operationId = connectId;
1839
+ } else if (!connectId && isHardwareOperationId(activeJobId)) {
1840
+ operationId = activeJobId;
1841
+ }
1842
+ let resolvedConnectId = connectId;
1843
+ if (operationId) {
1844
+ try {
1845
+ resolvedConnectId = this._operations.resolve(operationId).connectId;
1846
+ } catch {
1847
+ resolvedConnectId = void 0;
1848
+ }
1849
+ }
1377
1850
  this._uiRegistry.cancel();
1378
- if (connectId) {
1379
- this._jobQueue.cancelActiveAndPending(connectId, userAbortReason);
1851
+ this._finishBleBinding("cancelled");
1852
+ const interactionForPhysicalId = !operationId && connectId ? this._operations.findActiveByConnectionKey(connectId) : void 0;
1853
+ const queueKey = operationId ?? (activeJobId === connectId ? connectId : interactionForPhysicalId?.operationId) ?? connectId;
1854
+ const pendingOperationId = operationId ?? interactionForPhysicalId?.operationId;
1855
+ if (!connectId) this._pendingOperationBindings.clear();
1856
+ else if (pendingOperationId) {
1857
+ this._pendingOperationBindings.delete(pendingOperationId);
1858
+ }
1859
+ if (queueKey) {
1860
+ this._jobQueue.cancelActiveAndPending(queueKey, userAbortReason);
1380
1861
  } else {
1381
1862
  this._jobQueue.cancelActiveAndPending(void 0, userAbortReason);
1382
1863
  }
1383
- if (connectId) {
1384
- const sessionId = this._sessions.get(connectId) ?? connectId;
1864
+ if (resolvedConnectId) {
1865
+ const sessionId = this._sessions.get(resolvedConnectId) ?? resolvedConnectId;
1385
1866
  void this.connector.cancel(sessionId);
1386
1867
  } else {
1387
1868
  for (const sid of this._sessions.values()) void this.connector.cancel(sid);
@@ -1393,41 +1874,98 @@ var _LedgerAdapter = class _LedgerAdapter {
1393
1874
  // ---------------------------------------------------------------------------
1394
1875
  // Chain fingerprint
1395
1876
  // ---------------------------------------------------------------------------
1877
+ /** A non-empty deviceId is an expected chain fingerprint, not a transport identifier. */
1396
1878
  async getChainFingerprint(connectId, deviceId, chain) {
1397
1879
  try {
1398
1880
  const fingerprint = await this._computeChainFingerprint(
1399
1881
  chain,
1400
1882
  (method, params) => this.connectorCall(connectId, method, params, void 0, deviceId)
1401
1883
  );
1884
+ if (deviceId) {
1885
+ if (fingerprint !== deviceId) {
1886
+ return failure2(
1887
+ HardwareErrorCode3.DeviceMismatch,
1888
+ formatDeviceMismatchError(deviceId, fingerprint)
1889
+ );
1890
+ }
1891
+ if (isHardwareOperationId(connectId)) {
1892
+ const operation = this._operations.resolve(connectId);
1893
+ await this._publishVerifiedBleBinding(
1894
+ operation.connectId,
1895
+ chain,
1896
+ fingerprint,
1897
+ void 0,
1898
+ connectId
1899
+ );
1900
+ }
1901
+ }
1402
1902
  return success(fingerprint);
1403
1903
  } catch (err) {
1404
1904
  debugError("[LedgerAdapter] getChainFingerprint error:", chain, err);
1405
1905
  return this.errorToFailure(err);
1406
1906
  }
1407
1907
  }
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 };
1908
+ /** Discovery may select a BLE target before a later call verifies its wallet. */
1909
+ async _publishVerifiedBleBinding(connectId, chain, fingerprint, attempt, operationId, signal) {
1910
+ const binding = operationId ? this._pendingOperationBindings.get(operationId) : attempt;
1911
+ if (!this._isBleConnection() || binding?.selectedConnection?.connectId !== connectId) {
1912
+ return false;
1913
+ }
1414
1914
  try {
1415
- const fingerprint = await this._computeChainFingerprint(
1416
- chain,
1417
- (method, params) => this._callConnector(sessionId, method, params)
1915
+ const outcome = await requestSaveDeviceBinding(
1916
+ this.emitter,
1917
+ this._uiRegistry,
1918
+ {
1919
+ selectionRequestId: binding.selectedConnection.requestId,
1920
+ connection: { transport: "ble", connectId },
1921
+ identity: { vendor: "ledger", type: "chainFingerprint", chain, value: fingerprint },
1922
+ extra: binding.extra
1923
+ },
1924
+ signal
1418
1925
  );
1419
- if (fingerprint === deviceId) {
1420
- return { success: true };
1926
+ if (operationId) this._operations.resolve(operationId);
1927
+ if (!outcome.saved) {
1928
+ debugLog("[LedgerAdapter] BLE binding not persisted by host", {
1929
+ connectId,
1930
+ reason: outcome.reason
1931
+ });
1421
1932
  }
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 };
1933
+ return outcome.saved;
1934
+ } catch (error) {
1935
+ if (operationId) {
1936
+ this._pendingOperationBindings.delete(operationId);
1937
+ this._operations.end(operationId, "explicit");
1938
+ }
1939
+ const sessionId = this._sessions.get(connectId);
1940
+ this._sessions.delete(connectId);
1941
+ if (sessionId) {
1942
+ const teardown = this._runConnectorTeardown(
1943
+ () => this.connector.disconnect(sessionId)
1944
+ ).catch(() => void 0);
1945
+ if (!signal?.aborted) await teardown;
1946
+ }
1947
+ throw error;
1948
+ } finally {
1949
+ if (this._bindingSelectionRequestId === binding.selectedConnection.requestId) {
1950
+ this._bindingSelectionRequestId = void 0;
1951
+ }
1952
+ if (operationId) {
1953
+ this._pendingOperationBindings.delete(operationId);
1427
1954
  }
1428
- throw err;
1429
1955
  }
1430
1956
  }
1957
+ /** Verify on the acquired session without re-entering the job queue. */
1958
+ async _verifyDeviceFingerprintWithSession(sessionId, deviceId, chain) {
1959
+ if (!deviceId) return { success: true };
1960
+ const fingerprint = await this._computeChainFingerprint(
1961
+ chain,
1962
+ (method, params) => this._callConnector(sessionId, method, params)
1963
+ );
1964
+ if (fingerprint === deviceId) {
1965
+ return { success: true };
1966
+ }
1967
+ return { success: false, expected: deviceId, actual: fingerprint };
1968
+ }
1431
1969
  /**
1432
1970
  * Compute the chain fingerprint via a caller-supplied call strategy.
1433
1971
  *
@@ -1455,6 +1993,8 @@ var _LedgerAdapter = class _LedgerAdapter {
1455
1993
  address = (await callMethod("solGetAddress", { path, showOnDevice: false })).address;
1456
1994
  } else if (chain === "tron") {
1457
1995
  address = (await callMethod("tronGetAddress", { path, showOnDevice: false })).address;
1996
+ } else if (chain === "zcash") {
1997
+ address = (await callMethod("zcashGetShieldedAddress", { path, showOnDevice: false })).address;
1458
1998
  } else {
1459
1999
  throw new Error(`Unsupported chain for fingerprint: ${chain}`);
1460
2000
  }
@@ -1584,16 +2124,17 @@ var _LedgerAdapter = class _LedgerAdapter {
1584
2124
  // Layer 1 entry. Caller signal only races the outer awaiter; the shared
1585
2125
  // `_doConnect` runs under its own internal controller so caller A's cancel
1586
2126
  // doesn't kill caller B's await.
1587
- async ensureConnected(connectId, signal, allowUsbEphemeralFallback = false) {
2127
+ async ensureConnected(connectId, signal, allowUsbEphemeralFallback = false, preserveOperationId, context) {
1588
2128
  if (signal.aborted) _LedgerAdapter._throwIfAborted(signal);
1589
- if (isLedgerBleConnectionType(this.connector.connectionType) && !connectId) {
2129
+ this._assertConnectorReady("connectDevice");
2130
+ if (this._isBleConnection() && !connectId && !allowUsbEphemeralFallback) {
1590
2131
  throw Object.assign(new Error("Ledger BLE connectId is required."), {
1591
2132
  code: HardwareErrorCode3.DeviceNotFound
1592
2133
  });
1593
2134
  }
1594
2135
  if (connectId && this._sessions.has(connectId)) return connectId;
1595
2136
  if (!connectId && this._sessions.size > 0) {
1596
- if (!isLedgerBleConnectionType(this.connector.connectionType) && this._sessions.size > 1) {
2137
+ if (!this._isBleConnection() && this._sessions.size > 1) {
1597
2138
  throw Object.assign(
1598
2139
  new Error(
1599
2140
  "Ledger USB session invariant violated: more than one session is active. Please reconnect the device."
@@ -1603,15 +2144,24 @@ var _LedgerAdapter = class _LedgerAdapter {
1603
2144
  }
1604
2145
  return this._sessions.keys().next().value;
1605
2146
  }
1606
- if (!this._connectingPromise) {
1607
- this._doConnectAbortController = new AbortController();
1608
- const innerSignal = this._doConnectAbortController.signal;
2147
+ if (!this._connectingPromise || this._doConnectAbortController?.signal.aborted) {
2148
+ const controller = new AbortController();
2149
+ this._doConnectAbortController = controller;
2150
+ const innerSignal = controller.signal;
1609
2151
  this._connectingPromise = (async () => {
1610
2152
  try {
1611
- return await this._doConnect(innerSignal, connectId, allowUsbEphemeralFallback);
2153
+ return await this._doConnect(
2154
+ innerSignal,
2155
+ connectId,
2156
+ allowUsbEphemeralFallback,
2157
+ preserveOperationId,
2158
+ context
2159
+ );
1612
2160
  } finally {
1613
- this._connectingPromise = null;
1614
- this._doConnectAbortController = null;
2161
+ if (this._doConnectAbortController === controller) {
2162
+ this._connectingPromise = null;
2163
+ this._doConnectAbortController = null;
2164
+ }
1615
2165
  }
1616
2166
  })();
1617
2167
  }
@@ -1620,22 +2170,51 @@ var _LedgerAdapter = class _LedgerAdapter {
1620
2170
  // Layer 1 main loop — the ONLY place in SDK that emits unlock dialog.
1621
2171
  // Bounded by MAX_DOCONNECT_CONFIRMS — after N Confirms with no progress,
1622
2172
  // 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
- }
2173
+ async _doConnect(internalSignal, targetConnectId, allowUsbEphemeralFallback = false, preserveOperationId, context) {
2174
+ _LedgerAdapter._throwIfAborted(internalSignal);
2175
+ if (this.connector.availableTransports?.includes("usb") && this.connector.availableTransports.includes("ble")) {
2176
+ this._activeConnectionType = "usb";
2177
+ const usbDevices = await this._searchDevices({ transportType: "usb" }, internalSignal);
2178
+ _LedgerAdapter._throwIfAborted(internalSignal);
2179
+ if (usbDevices.length > 0) {
2180
+ const knownUsb = context?.knownConnections?.find(
2181
+ (connection) => connection.transport === "usb"
2182
+ );
2183
+ const usbTarget = knownUsb?.transport === "usb" ? knownUsb.connectId : usbDevices.find((device) => device.connectId === targetConnectId)?.connectId;
2184
+ return this._connectFirstOrSelect(
2185
+ usbDevices,
2186
+ usbTarget,
2187
+ allowUsbEphemeralFallback,
2188
+ preserveOperationId,
2189
+ context,
2190
+ internalSignal
2191
+ );
2192
+ }
2193
+ this._activeConnectionType = "ble";
2194
+ const knownBle = context?.knownConnections?.find(
2195
+ (connection) => connection.transport === "ble"
2196
+ );
2197
+ if (knownBle?.transport === "ble") {
2198
+ return this._connectDeviceOrThrow(knownBle.connectId, preserveOperationId, internalSignal);
2199
+ }
2200
+ if (targetConnectId && context?.knownConnections === void 0) {
2201
+ throw createHwkError({
2202
+ code: HardwareErrorCode3.DeviceNotFound,
2203
+ message: "Ledger connection metadata is required before starting Bluetooth binding"
2204
+ });
1638
2205
  }
2206
+ const bleDevices = await this._searchDevices({ transportType: "ble" }, internalSignal);
2207
+ return this._connectFirstOrSelect(
2208
+ bleDevices,
2209
+ void 0,
2210
+ allowUsbEphemeralFallback,
2211
+ preserveOperationId,
2212
+ context,
2213
+ internalSignal
2214
+ );
2215
+ }
2216
+ if (this._isBleConnection() && targetConnectId) {
2217
+ return this._connectDeviceOrThrow(targetConnectId, preserveOperationId, internalSignal);
1639
2218
  }
1640
2219
  let confirms = 0;
1641
2220
  while (!internalSignal.aborted) {
@@ -1643,22 +2222,28 @@ var _LedgerAdapter = class _LedgerAdapter {
1643
2222
  type: EConnectorInteraction.Searching,
1644
2223
  payload: { sessionId: "" }
1645
2224
  });
1646
- let devices = await this.searchDevices();
2225
+ let devices = await this._searchDevices(void 0, internalSignal);
2226
+ _LedgerAdapter._throwIfAborted(internalSignal);
1647
2227
  if (devices.length === 0) {
1648
2228
  for (let i = 0; i < 3 && !internalSignal.aborted; i += 1) {
1649
2229
  await new Promise((resolve) => {
1650
2230
  setTimeout(resolve, DEVICE_CONNECT_RETRY_DELAY_MS);
1651
2231
  });
1652
- devices = await this.searchDevices();
2232
+ _LedgerAdapter._throwIfAborted(internalSignal);
2233
+ devices = await this._searchDevices(void 0, internalSignal);
2234
+ _LedgerAdapter._throwIfAborted(internalSignal);
1653
2235
  if (devices.length > 0) break;
1654
2236
  }
1655
2237
  }
1656
- if (devices.length > 0) {
2238
+ if (devices.length > 0 || this._isBleConnection() && allowUsbEphemeralFallback) {
1657
2239
  try {
1658
2240
  return await this._connectFirstOrSelect(
1659
2241
  devices,
1660
2242
  targetConnectId,
1661
- allowUsbEphemeralFallback
2243
+ allowUsbEphemeralFallback,
2244
+ preserveOperationId,
2245
+ context,
2246
+ internalSignal
1662
2247
  );
1663
2248
  } catch (err) {
1664
2249
  if (!isDeviceLockedError(err) && !isDeviceNotAdvertisingError(err) && !isDeviceDisconnectedError(err)) {
@@ -1687,45 +2272,133 @@ var _LedgerAdapter = class _LedgerAdapter {
1687
2272
  _LedgerAdapter._throwIfAborted(internalSignal);
1688
2273
  throw new Error("_doConnect aborted");
1689
2274
  }
1690
- async _connectFirstOrSelect(devices, targetConnectId, allowUsbEphemeralFallback = false) {
2275
+ async _connectFirstOrSelect(devices, targetConnectId, allowUsbEphemeralFallback, preserveOperationId, context, signal) {
2276
+ _LedgerAdapter._throwIfAborted(signal);
1691
2277
  if (targetConnectId) {
1692
2278
  const target = devices.find(
1693
2279
  (d) => d.connectId === targetConnectId || d.deviceId === targetConnectId
1694
2280
  );
1695
2281
  if (target) {
1696
- return this._connectDeviceOrThrow(target.connectId);
2282
+ return this._connectDeviceOrThrow(target.connectId, preserveOperationId, signal);
1697
2283
  }
1698
- if (!isLedgerBleConnectionType(this.connector.connectionType) && devices.length === 1 && allowUsbEphemeralFallback) {
2284
+ if (!this._isBleConnection() && devices.length === 1 && allowUsbEphemeralFallback) {
1699
2285
  debugLog(
1700
2286
  `[LedgerAdapter] target ${targetConnectId} not in fresh enumeration; accepting sole USB device ${devices[0].connectId} for fingerprint-verified recovery`
1701
2287
  );
1702
- return this._connectDeviceOrThrow(devices[0].connectId);
1703
- }
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;
2288
+ return this._connectDeviceOrThrow(devices[0].connectId, preserveOperationId, signal);
1709
2289
  }
1710
- throw err;
1711
- }
1712
- if (isLedgerBleConnectionType(this.connector.connectionType)) {
2290
+ if (!this._isBleConnection() || !allowUsbEphemeralFallback) {
2291
+ const err = Object.assign(new Error(`Target Ledger unavailable: ${targetConnectId}`), {
2292
+ code: HardwareErrorCode3.DeviceNotFound
2293
+ });
2294
+ if (this._isBleConnection()) {
2295
+ err._tag = ERROR_TAG.DeviceNotAdvertising;
2296
+ }
2297
+ throw err;
2298
+ }
2299
+ }
2300
+ const requiresBleSelection = this._isBleConnection();
2301
+ if (requiresBleSelection && !allowUsbEphemeralFallback) {
1713
2302
  throw Object.assign(new Error("Ledger BLE connectId is required."), {
1714
2303
  code: HardwareErrorCode3.DeviceNotFound
1715
2304
  });
1716
2305
  }
1717
- if (devices.length > 1) {
1718
- throw createMultipleUsbLedgerDevicesError();
2306
+ if (requiresBleSelection && context?.allowDeviceSelection !== false) {
2307
+ const bindingSessionId = context?.bindingSessionId ?? this._uiRegistry.createRequestId();
2308
+ if (context) context.bindingSessionId = bindingSessionId;
2309
+ const allowUsbFallback = context?.bindingReason !== "manual-rebind" && Boolean(this.connector.availableTransports?.includes("usb"));
2310
+ const knownUsb = context?.knownConnections?.find(
2311
+ (connection) => connection.transport === "usb"
2312
+ );
2313
+ const usbConnectId = knownUsb?.transport === "usb" ? knownUsb.connectId : targetConnectId;
2314
+ const { device, requestId } = await requestBleDeviceSelection({
2315
+ emitter: this.emitter,
2316
+ registry: this._uiRegistry,
2317
+ signal,
2318
+ allowUsbFallback,
2319
+ scan: async () => {
2320
+ if (allowUsbFallback) {
2321
+ const usbDevices = await this._searchDevices({ transportType: "usb" }, signal);
2322
+ const candidate = usbDevices.find(
2323
+ (device2) => device2.connectionType === "usb" && device2.connectId === usbConnectId
2324
+ ) ?? (usbDevices.length === 1 && usbDevices[0].connectionType === "usb" ? usbDevices[0] : void 0);
2325
+ if (candidate) return [candidate];
2326
+ }
2327
+ return (await this._searchDevices({ transportType: "ble", waitForAllTransports: true }, signal)).filter((device2) => !context?.rejectedConnectIds?.has(device2.connectId));
2328
+ },
2329
+ request: {
2330
+ devices: devices.filter((device2) => !context?.rejectedConnectIds?.has(device2.connectId)),
2331
+ bindingSessionId,
2332
+ rejectedConnectId: context?.rejectedConnectId,
2333
+ context: {
2334
+ kind: "bind-connection",
2335
+ transport: "ble",
2336
+ reason: context?.bindingReason ?? (targetConnectId ? "known-connection-unavailable" : "missing-binding")
2337
+ },
2338
+ extra: context?.extra
2339
+ }
2340
+ });
2341
+ if (device.connectionType === "usb") {
2342
+ this._activeConnectionType = "usb";
2343
+ if (context) context.selectedConnection = void 0;
2344
+ if (preserveOperationId) this._pendingOperationBindings.delete(preserveOperationId);
2345
+ this._bindingSelectionRequestId = void 0;
2346
+ this.emitter.emit(UI_REQUEST.DEVICE_BINDING_STATUS, {
2347
+ type: UI_REQUEST.DEVICE_BINDING_STATUS,
2348
+ payload: { selectionRequestId: requestId, status: "cancelled" }
2349
+ });
2350
+ return this._connectDeviceOrThrow(device.connectId, preserveOperationId, signal);
2351
+ }
2352
+ if (context) context.selectedConnection = { connectId: device.connectId, requestId };
2353
+ this._bindingSelectionRequestId = requestId;
2354
+ return this._connectDeviceOrThrow(device.connectId, preserveOperationId, signal);
2355
+ }
2356
+ if (devices.length > 0 && (devices.length > 1 || requiresBleSelection)) {
2357
+ if (context?.allowDeviceSelection === false || !this.emitter.listenerCount(UI_REQUEST.REQUEST_SELECT_DEVICE)) {
2358
+ throw createHwkError({
2359
+ code: HardwareErrorCode3.DeviceNotFound,
2360
+ message: "Select a Ledger device before continuing"
2361
+ });
2362
+ }
2363
+ const requestId = this._uiRegistry.createRequestId();
2364
+ const waitPromise = this._uiRegistry.wait(
2365
+ UI_REQUEST.REQUEST_SELECT_DEVICE,
2366
+ { requestId }
2367
+ );
2368
+ this.emitter.emit(UI_REQUEST.REQUEST_SELECT_DEVICE, {
2369
+ type: UI_REQUEST.REQUEST_SELECT_DEVICE,
2370
+ payload: {
2371
+ devices,
2372
+ requestId,
2373
+ context: requiresBleSelection ? {
2374
+ kind: "bind-connection",
2375
+ transport: "ble",
2376
+ reason: targetConnectId ? "known-connection-unavailable" : "missing-binding"
2377
+ } : { kind: "select-device", transport: "usb", reason: "multiple-candidates" },
2378
+ extra: context?.extra
2379
+ }
2380
+ });
2381
+ const { sdkConnectId } = await (signal ? this._abortable(signal, waitPromise) : waitPromise);
2382
+ if (signal) _LedgerAdapter._throwIfAborted(signal);
2383
+ const selected = devices.find((device) => device.connectId === sdkConnectId);
2384
+ if (!selected) {
2385
+ throw Object.assign(new Error("Selected Ledger is no longer available"), {
2386
+ code: HardwareErrorCode3.DeviceNotFound
2387
+ });
2388
+ }
2389
+ if (context && requiresBleSelection)
2390
+ context.selectedConnection = { connectId: selected.connectId, requestId };
2391
+ return this._connectDeviceOrThrow(selected.connectId, preserveOperationId, signal);
1719
2392
  }
1720
2393
  if (devices.length !== 1) {
1721
2394
  throw Object.assign(new Error("Ledger device not found."), {
1722
2395
  code: HardwareErrorCode3.DeviceNotFound
1723
2396
  });
1724
2397
  }
1725
- return this._connectDeviceOrThrow(devices[0].connectId);
2398
+ return this._connectDeviceOrThrow(devices[0].connectId, preserveOperationId, signal);
1726
2399
  }
1727
- async _connectDeviceOrThrow(chosenConnectId) {
1728
- const result = await this.connectDevice(chosenConnectId);
2400
+ async _connectDeviceOrThrow(chosenConnectId, preserveOperationId, signal) {
2401
+ const result = await this._connectTarget(chosenConnectId, preserveOperationId, signal);
1729
2402
  if (!result.success) {
1730
2403
  const payload = result.payload;
1731
2404
  const rethrow = Object.assign(new Error(payload.error), {
@@ -1736,7 +2409,7 @@ var _LedgerAdapter = class _LedgerAdapter {
1736
2409
  }
1737
2410
  throw rethrow;
1738
2411
  }
1739
- return chosenConnectId;
2412
+ return result.payload;
1740
2413
  }
1741
2414
  /**
1742
2415
  * Call the connector with automatic session resolution and disconnect retry.
@@ -1766,26 +2439,122 @@ var _LedgerAdapter = class _LedgerAdapter {
1766
2439
  * lives in one place.
1767
2440
  */
1768
2441
  async _callConnector(sessionId, method, params, signal) {
1769
- const promise = this.connector.call(sessionId, method, params);
2442
+ this._assertConnectorReady(method);
2443
+ if (signal?.aborted) throw this._abortReason(signal);
2444
+ const releaseOperation = this._retainConnectorOperation(`call:${sessionId}`);
2445
+ let promise;
2446
+ try {
2447
+ promise = this.connector.call(sessionId, method, params).finally(releaseOperation);
2448
+ } catch (error) {
2449
+ releaseOperation();
2450
+ throw error;
2451
+ }
1770
2452
  const result = signal ? await this._abortable(signal, promise) : await promise;
1771
2453
  return this._unwrapConnectorResult(result);
1772
2454
  }
2455
+ _assertConnectorReady(method) {
2456
+ if (this._resetPromise || this._pendingConnectorTeardowns > 0 || this._unsettledConnectorOperations.size > 0) {
2457
+ throw _LedgerAdapter._createDeviceBusyError(method);
2458
+ }
2459
+ }
2460
+ _runConnectorTeardown(task) {
2461
+ const previous = this._connectorTeardownTail;
2462
+ let releaseTail = () => void 0;
2463
+ this._connectorTeardownTail = new Promise((resolve) => {
2464
+ releaseTail = resolve;
2465
+ });
2466
+ this._pendingConnectorTeardowns += 1;
2467
+ return (async () => {
2468
+ try {
2469
+ await previous;
2470
+ await this._waitForConnectorOperationsToDrain();
2471
+ await task();
2472
+ } finally {
2473
+ this._pendingConnectorTeardowns -= 1;
2474
+ releaseTail();
2475
+ }
2476
+ })();
2477
+ }
2478
+ _waitForConnectorOperationsToDrain() {
2479
+ if (this._unsettledConnectorOperations.size === 0) {
2480
+ return Promise.resolve();
2481
+ }
2482
+ return new Promise((resolve) => {
2483
+ this._connectorIdleWaiters.add(resolve);
2484
+ });
2485
+ }
2486
+ _retainConnectorOperation(key) {
2487
+ this._unsettledConnectorOperations.set(
2488
+ key,
2489
+ (this._unsettledConnectorOperations.get(key) ?? 0) + 1
2490
+ );
2491
+ let released = false;
2492
+ return () => {
2493
+ if (released) return;
2494
+ released = true;
2495
+ const remaining = (this._unsettledConnectorOperations.get(key) ?? 1) - 1;
2496
+ if (remaining > 0) {
2497
+ this._unsettledConnectorOperations.set(key, remaining);
2498
+ } else {
2499
+ this._unsettledConnectorOperations.delete(key);
2500
+ }
2501
+ if (this._unsettledConnectorOperations.size === 0) {
2502
+ for (const resolve of this._connectorIdleWaiters) resolve();
2503
+ this._connectorIdleWaiters.clear();
2504
+ }
2505
+ };
2506
+ }
1773
2507
  async connectorCall(connectId, method, params, fingerprint, permissionDeviceId, commonParams, installContext) {
1774
- debugLog("[LedgerAdapter][REQ]", { method, connectId: connectId || "(empty)", params });
1775
- const queueKey = connectId || "__ledger_default__";
2508
+ const positionalOperationId = isHardwareOperationId(connectId) ? connectId : void 0;
2509
+ if (positionalOperationId && commonParams?.operationId && positionalOperationId !== commonParams.operationId) {
2510
+ throw createHwkError({
2511
+ code: HardwareErrorCode3.InvalidParams,
2512
+ message: "Conflicting Ledger operation ids",
2513
+ params: {
2514
+ positionalOperationId,
2515
+ commonOperationId: commonParams.operationId
2516
+ }
2517
+ });
2518
+ }
2519
+ const operationId = commonParams?.operationId ?? positionalOperationId;
2520
+ const operation = operationId ? this._operations.resolve(operationId) : void 0;
2521
+ const releaseOperationRetention = operationId ? this._operations.retain(operationId) : void 0;
2522
+ const effectiveConnectId = operation?.connectId ?? connectId;
2523
+ debugLog("[LedgerAdapter][REQ]", {
2524
+ method,
2525
+ connectId: effectiveConnectId || "(empty)",
2526
+ params
2527
+ });
2528
+ const queueKey = (operationId ?? effectiveConnectId) || "__ledger_default__";
1776
2529
  try {
1777
2530
  const result = await this._jobQueue.enqueue(
1778
2531
  queueKey,
1779
- async (signal) => this._runConnectorCall(
1780
- connectId,
1781
- method,
1782
- params,
1783
- signal,
1784
- fingerprint,
1785
- permissionDeviceId,
1786
- commonParams,
1787
- installContext
1788
- ),
2532
+ async (signal) => {
2533
+ if (operationId) this._activeOperationJobs.add(operationId);
2534
+ try {
2535
+ return await this._runConnectorCall(
2536
+ effectiveConnectId,
2537
+ method,
2538
+ params,
2539
+ signal,
2540
+ fingerprint,
2541
+ permissionDeviceId,
2542
+ commonParams,
2543
+ installContext ?? {},
2544
+ operationId
2545
+ );
2546
+ } catch (error) {
2547
+ this._finishBleBinding(signal.aborted ? "cancelled" : "failed");
2548
+ throw error;
2549
+ } finally {
2550
+ if (operationId) {
2551
+ this._activeOperationJobs.delete(operationId);
2552
+ if (this._pendingOperationDisconnects.delete(operationId)) {
2553
+ await this._releaseLostOperationConnection(operationId);
2554
+ }
2555
+ }
2556
+ }
2557
+ },
1789
2558
  {
1790
2559
  label: method,
1791
2560
  rejectIfBusy: true,
@@ -1806,15 +2575,22 @@ var _LedgerAdapter = class _LedgerAdapter {
1806
2575
  }
1807
2576
  });
1808
2577
  throw err;
2578
+ } finally {
2579
+ releaseOperationRetention?.();
1809
2580
  }
1810
2581
  }
1811
2582
  /**
1812
2583
  * Race a promise against an abort signal. On abort, rejects with
1813
2584
  * signal.reason → instance _lastCancelReason → generic Error('Aborted').
1814
2585
  */
2586
+ /** Hermes/RN polyfills don't always populate signal.reason; fall back. */
2587
+ _abortReason(signal) {
2588
+ return signal.reason ?? this._lastCancelReason ?? new Error("Aborted");
2589
+ }
1815
2590
  _abortable(signal, promise) {
1816
- const getAbortReason = () => signal.reason ?? this._lastCancelReason ?? new Error("Aborted");
2591
+ const getAbortReason = () => this._abortReason(signal);
1817
2592
  if (signal.aborted) {
2593
+ void promise.catch(() => void 0);
1818
2594
  return Promise.reject(getAbortReason());
1819
2595
  }
1820
2596
  return new Promise((resolve, reject) => {
@@ -1841,13 +2617,15 @@ var _LedgerAdapter = class _LedgerAdapter {
1841
2617
  }
1842
2618
  }
1843
2619
  /** Actual work done under the job queue — connection, fingerprint, call, and recovery. */
1844
- async _runConnectorCall(connectId, method, params, signal, fingerprint, permissionDeviceId, commonParams, installContext) {
2620
+ async _runConnectorCall(connectId, method, params, signal, fingerprint, permissionDeviceId, commonParams, installContext, operationId, lockedRetryBudget = _LedgerAdapter.MAX_BUSINESS_RETRY_BUDGET) {
1845
2621
  _LedgerAdapter._throwIfAborted(signal);
1846
- await this._ensureDevicePermission(
1847
- connectId,
1848
- permissionDeviceId ?? fingerprint?.deviceId,
1849
- signal
1850
- );
2622
+ if (!operationId) {
2623
+ await this._ensureDevicePermission(
2624
+ connectId,
2625
+ permissionDeviceId ?? fingerprint?.deviceId,
2626
+ signal
2627
+ );
2628
+ }
1851
2629
  _LedgerAdapter._throwIfAborted(signal);
1852
2630
  let effectiveParams = params;
1853
2631
  if (method === "btcGetPublicKey") {
@@ -1861,36 +2639,132 @@ var _LedgerAdapter = class _LedgerAdapter {
1861
2639
  effectiveParams = gatedParams;
1862
2640
  }
1863
2641
  const allowUsbEphemeralFallback = !!fingerprint?.deviceId && !fingerprint.skipFingerprint;
1864
- const resolvedConnectId = await this.ensureConnected(
1865
- connectId,
2642
+ let businessCallStarted = false;
2643
+ const verifiedBleTarget = connectId ? this._verifiedBleReconnectTargets.get(connectId) : void 0;
2644
+ const knownTransport = this._isBleConnection() ? "ble" : "usb";
2645
+ const hintedConnectId = commonParams?.knownConnections?.find(
2646
+ (connection) => connection.transport === knownTransport
2647
+ );
2648
+ const inputConnectId = hintedConnectId && hintedConnectId.transport !== "qr" ? hintedConnectId.connectId : connectId;
2649
+ const preferredConnectId = fingerprint && !fingerprint.skipFingerprint && verifiedBleTarget?.chain === fingerprint.chain && verifiedBleTarget.fingerprint === fingerprint.deviceId ? verifiedBleTarget.connectId : inputConnectId;
2650
+ const connectionAttempt = { ...commonParams };
2651
+ const bundleConnection = installContext?.connection;
2652
+ if (bundleConnection && this._sessions.get(bundleConnection.connectId) !== bundleConnection.sessionId) {
2653
+ throw createHwkError({
2654
+ code: HardwareErrorCode3.DeviceDisconnected,
2655
+ message: "Ledger all-network connection ended"
2656
+ });
2657
+ }
2658
+ let resolvedConnectId = operationId ? this._operations.resolve(operationId).connectId : bundleConnection?.connectId ?? await this.ensureConnected(
2659
+ preferredConnectId,
1866
2660
  signal,
1867
- allowUsbEphemeralFallback
2661
+ allowUsbEphemeralFallback,
2662
+ void 0,
2663
+ connectionAttempt
1868
2664
  );
1869
- const sessionId = this._sessions.get(resolvedConnectId);
2665
+ let sessionId = this._sessions.get(resolvedConnectId);
2666
+ if (sessionId && installContext && !installContext.connection) {
2667
+ installContext.connection = { connectId: resolvedConnectId, sessionId };
2668
+ }
1870
2669
  if (!sessionId) {
2670
+ if (operationId) {
2671
+ this._operations.end(operationId, "disconnect");
2672
+ throw createHwkError({
2673
+ code: HardwareErrorCode3.OperationEnded,
2674
+ message: "Ledger operation connection is no longer active",
2675
+ params: { operationId, reason: "disconnect" }
2676
+ });
2677
+ }
1871
2678
  throw Object.assign(new Error("Auto-connect succeeded but no session found"), {
1872
2679
  _tag: ERROR_TAG.DeviceSessionNotFound
1873
2680
  });
1874
2681
  }
1875
2682
  try {
1876
2683
  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
- )
2684
+ for (; ; ) {
2685
+ const fp = await this._abortable(
2686
+ signal,
2687
+ this._verifyDeviceFingerprintWithSession(
2688
+ sessionId,
2689
+ fingerprint.deviceId,
2690
+ fingerprint.chain
2691
+ )
2692
+ );
2693
+ if (fp.success) break;
2694
+ const binding = operationId ? this._pendingOperationBindings.get(operationId) : connectionAttempt;
2695
+ if (!this._isBleConnection() || binding?.selectedConnection?.connectId !== resolvedConnectId) {
2696
+ throw Object.assign(new Error(formatDeviceMismatchError(fp.expected, fp.actual)), {
2697
+ code: HardwareErrorCode3.DeviceMismatch
2698
+ });
2699
+ }
2700
+ binding.rejectedConnectIds ?? (binding.rejectedConnectIds = /* @__PURE__ */ new Set());
2701
+ binding.rejectedConnectIds.add(resolvedConnectId);
2702
+ binding.rejectedConnectId = resolvedConnectId;
2703
+ this._sessions.delete(resolvedConnectId);
2704
+ await this.connector.disconnect(sessionId);
2705
+ if (operationId) this._pendingOperationDisconnects.delete(operationId);
2706
+ _LedgerAdapter._throwIfAborted(signal);
2707
+ resolvedConnectId = await this._connectFirstOrSelect(
2708
+ [],
2709
+ void 0,
2710
+ true,
2711
+ operationId,
2712
+ binding,
2713
+ signal
2714
+ );
2715
+ const selectedSession = this._sessions.get(resolvedConnectId);
2716
+ const selectedDevice = this._discoveredDevices.get(resolvedConnectId);
2717
+ if (!selectedSession || !selectedDevice)
2718
+ throw createHwkError({
2719
+ code: HardwareErrorCode3.DeviceDisconnected,
2720
+ message: "Selected Ledger connection ended"
2721
+ });
2722
+ sessionId = selectedSession;
2723
+ if (operationId)
2724
+ this._operations.rebind(operationId, {
2725
+ connectId: resolvedConnectId,
2726
+ device: selectedDevice,
2727
+ connectionKeys: [sessionId]
2728
+ });
2729
+ if (installContext)
2730
+ installContext.connection = { connectId: resolvedConnectId, sessionId };
2731
+ }
2732
+ await this._publishVerifiedBleBinding(
2733
+ resolvedConnectId,
2734
+ fingerprint.chain,
2735
+ fingerprint.deviceId,
2736
+ connectionAttempt,
2737
+ operationId,
2738
+ signal
1884
2739
  );
1885
- if (!fp.success) {
1886
- throw Object.assign(new Error(formatDeviceMismatchError(fp.expected, fp.actual)), {
1887
- code: HardwareErrorCode3.DeviceMismatch
2740
+ if (!operationId && connectionAttempt.selectedConnection?.connectId === resolvedConnectId && this._isBleConnection()) {
2741
+ this._verifiedBleReconnectTargets.set(connectId, {
2742
+ connectId: resolvedConnectId,
2743
+ chain: fingerprint.chain,
2744
+ fingerprint: fingerprint.deviceId
1888
2745
  });
1889
2746
  }
1890
2747
  }
2748
+ businessCallStarted = true;
1891
2749
  return await this._callConnector(sessionId, method, effectiveParams, signal);
1892
2750
  } catch (err) {
1893
2751
  if (signal.aborted) throw err;
2752
+ if (isDeviceDisconnectedError(err) || isDeviceNotAdvertisingError(err) || isTimeoutError(err) || isConnectionLevelError(err)) {
2753
+ this._discoveredDevices.delete(resolvedConnectId);
2754
+ if (operationId) await this._releaseLostOperationConnection(operationId);
2755
+ else {
2756
+ this._sessions.delete(resolvedConnectId);
2757
+ await this.connector.disconnect(sessionId).catch(() => void 0);
2758
+ }
2759
+ const ambiguous = businessCallStarted && !canReplayHardwareMethodAfterTransportFailure(method);
2760
+ const interactionParams = operationId ? { operationId, reason: "disconnect" } : void 0;
2761
+ throw createHwkError({
2762
+ code: operationId ? HardwareErrorCode3.OperationEnded : mapLedgerError(err).code,
2763
+ message: "Ledger operation connection was lost; start a new operation",
2764
+ params: ambiguous ? operationMayHaveCompletedParams(method, { operationId }) : interactionParams,
2765
+ recovery: ambiguous ? { scope: "unknown" } : void 0
2766
+ });
2767
+ }
1894
2768
  const errObj = err;
1895
2769
  debugLog("[LedgerAdapter] connectorCall error:", method, {
1896
2770
  message: errObj?.message,
@@ -1902,6 +2776,65 @@ var _LedgerAdapter = class _LedgerAdapter {
1902
2776
  isNotAdvertising: isDeviceNotAdvertisingError(err),
1903
2777
  isStuckApp: isStuckAppStateError(err)
1904
2778
  });
2779
+ if ((isDeviceLockedError(err) || errObj?.code === HardwareErrorCode3.DeviceLocked) && lockedRetryBudget > 0) {
2780
+ await this._waitForDeviceConnect(signal);
2781
+ if (this._sessions.get(resolvedConnectId) !== sessionId) {
2782
+ throw createHwkError({
2783
+ code: operationId ? HardwareErrorCode3.OperationEnded : HardwareErrorCode3.DeviceDisconnected,
2784
+ message: "Ledger connection ended while waiting for unlock"
2785
+ });
2786
+ }
2787
+ return this._runConnectorCall(
2788
+ resolvedConnectId,
2789
+ method,
2790
+ effectiveParams,
2791
+ signal,
2792
+ fingerprint,
2793
+ permissionDeviceId,
2794
+ commonParams,
2795
+ installContext,
2796
+ operationId,
2797
+ lockedRetryBudget - 1
2798
+ );
2799
+ }
2800
+ if (businessCallStarted && isStuckAppStateError(err)) {
2801
+ await this._sleepAbortable(_LedgerAdapter.STUCK_APP_RETRY_DELAY_MS, signal);
2802
+ if (this._sessions.get(resolvedConnectId) !== sessionId) {
2803
+ throw createHwkError({
2804
+ code: operationId ? HardwareErrorCode3.OperationEnded : HardwareErrorCode3.DeviceDisconnected,
2805
+ message: "Ledger connection ended during the app transition"
2806
+ });
2807
+ }
2808
+ try {
2809
+ return await this._callConnector(sessionId, method, effectiveParams, signal);
2810
+ } catch (retryErr) {
2811
+ if (isStuckAppStateError(retryErr)) throw err;
2812
+ if (isDeviceDisconnectedError(retryErr) || isDeviceNotAdvertisingError(retryErr) || isTimeoutError(retryErr) || isConnectionLevelError(retryErr)) {
2813
+ if (operationId) await this._releaseLostOperationConnection(operationId);
2814
+ else {
2815
+ this._sessions.delete(resolvedConnectId);
2816
+ this._discoveredDevices.delete(resolvedConnectId);
2817
+ await this.connector.disconnect(sessionId).catch(() => void 0);
2818
+ }
2819
+ throw createHwkError({
2820
+ code: operationId ? HardwareErrorCode3.OperationEnded : mapLedgerError(retryErr).code,
2821
+ message: `Ledger ${method} may have completed before the connection was lost`,
2822
+ params: !canReplayHardwareMethodAfterTransportFailure(method) ? operationMayHaveCompletedParams(method, {
2823
+ operationId,
2824
+ reason: "disconnect"
2825
+ }) : { operationId, reason: "disconnect" },
2826
+ recovery: !canReplayHardwareMethodAfterTransportFailure(method) ? { scope: "unknown" } : void 0
2827
+ });
2828
+ }
2829
+ throw retryErr;
2830
+ }
2831
+ }
2832
+ if (!operationId && err?.code === HardwareErrorCode3.DeviceMismatch) {
2833
+ this._sessions.delete(resolvedConnectId);
2834
+ this._discoveredDevices.delete(resolvedConnectId);
2835
+ await this.connector.disconnect(sessionId).catch(() => void 0);
2836
+ throw err;
2837
+ }
1905
2838
  const autoInstallApp = commonParams?.autoInstallApp ?? this._defaultAutoInstallApp;
1906
2839
  const isAppMissing = isAppNotInstalledError(err) || err?.code === HardwareErrorCode3.AppNotInstalled;
1907
2840
  if (autoInstallApp && isAppMissing) {
@@ -1957,157 +2890,14 @@ var _LedgerAdapter = class _LedgerAdapter {
1957
2890
  fingerprint,
1958
2891
  permissionDeviceId,
1959
2892
  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
2893
+ installContext,
2894
+ operationId
1987
2895
  );
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
2001
- );
2002
- throw retryErr;
2003
- }
2004
- }
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
2896
  }
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
2897
  }
2074
2898
  throw err;
2075
2899
  }
2076
2900
  }
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
2901
  _sleepAbortable(ms, signal) {
2112
2902
  return new Promise((resolve, reject) => {
2113
2903
  if (signal.aborted) {
@@ -2125,88 +2915,6 @@ var _LedgerAdapter = class _LedgerAdapter {
2125
2915
  signal.addEventListener("abort", onAbort, { once: true });
2126
2916
  });
2127
2917
  }
2128
- /**
2129
- * Clear stale session, reconnect, and retry the call.
2130
- *
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.
2134
- */
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
2918
  /**
2211
2919
  * Ensure OS-level device permission (Bluetooth / USB) before proceeding.
2212
2920
  *
@@ -2276,7 +2984,15 @@ var _LedgerAdapter = class _LedgerAdapter {
2276
2984
  if (err && typeof err === "object" && "code" in err && typeof err.code === "number") {
2277
2985
  const e = err;
2278
2986
  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);
2987
+ return ledgerFailure(
2988
+ e.code,
2989
+ e.message ?? "Unknown error",
2990
+ e.appName,
2991
+ tag,
2992
+ params,
2993
+ void 0,
2994
+ isHwkRecoveryHint(e.recovery) ? e.recovery : void 0
2995
+ );
2280
2996
  }
2281
2997
  const mapped = mapLedgerError(err);
2282
2998
  return ledgerFailure(mapped.code, mapped.message, mapped.appName, tag);
@@ -2303,7 +3019,7 @@ var _LedgerAdapter = class _LedgerAdapter {
2303
3019
  deviceId: device.deviceId,
2304
3020
  connectId: device.connectId,
2305
3021
  label: device.name,
2306
- connectionType: this.connector.connectionType,
3022
+ connectionType: device.connectionType ?? this.connector.connectionType,
2307
3023
  rssi: device.rssi,
2308
3024
  isConnectable: device.isConnectable,
2309
3025
  serialNumber: device.serialNumber,
@@ -2329,8 +3045,8 @@ var LedgerAdapter = _LedgerAdapter;
2329
3045
 
2330
3046
  // src/connector/LedgerConnectorBase.ts
2331
3047
  import {
2332
- EConnectorInteraction as EConnectorInteraction4,
2333
- HardwareErrorCode as HardwareErrorCode8,
3048
+ EConnectorInteraction as EConnectorInteraction3,
3049
+ HardwareErrorCode as HardwareErrorCode7,
2334
3050
  isKnownNonTargetHardwareVendor,
2335
3051
  serializeConnectorError
2336
3052
  } from "@onekeyfe/hwk-adapter-core";
@@ -2603,7 +3319,7 @@ var LedgerDeviceManager = class {
2603
3319
 
2604
3320
  // src/signer/SignerManager.ts
2605
3321
  import { SignerEthBuilder } from "@ledgerhq/device-signer-kit-ethereum";
2606
- import { ContextModuleBuilder } from "@ledgerhq/context-module";
3322
+ import { ContextModuleBuilder, ContextModuleChainID } from "@ledgerhq/context-module";
2607
3323
 
2608
3324
  // src/signer/SignerEth.ts
2609
3325
  import { hexToBytes } from "@onekeyfe/hwk-adapter-core";
@@ -2869,7 +3585,7 @@ var SignerManager = class _SignerManager {
2869
3585
  return (args) => new SignerEthBuilder(args);
2870
3586
  }
2871
3587
  static _createContextModule() {
2872
- const contextModule = new ContextModuleBuilder({}).removeDefaultLoaders().build();
3588
+ const contextModule = new ContextModuleBuilder({}).setChain(ContextModuleChainID.Ethereum).removeDefaultLoaders().build();
2873
3589
  return _SignerManager.wrapBlindSigningReportNonBlocking(contextModule);
2874
3590
  }
2875
3591
  static wrapBlindSigningReportNonBlocking(contextModule) {
@@ -3339,7 +4055,8 @@ function _applySignaturesToPsbt(psbtHex, signatures) {
3339
4055
  }
3340
4056
 
3341
4057
  // src/connector/chains/sol.ts
3342
- import { bytesToHex, hexToBytes as hexToBytes3 } from "@onekeyfe/hwk-adapter-core";
4058
+ import { bytesToHex, hexToBytes as hexToBytes3, prepareSolanaOffchainMessageV1 } from "@onekeyfe/hwk-adapter-core";
4059
+ import bs58 from "bs58";
3343
4060
 
3344
4061
  // src/signer/SignerSol.ts
3345
4062
  var SignerSol = class {
@@ -3423,8 +4140,21 @@ async function solSignMessage(ctx, sessionId, params) {
3423
4140
  const path = normalizePath(params.path);
3424
4141
  const messageBytes = hexToBytes3(params.message);
3425
4142
  try {
4143
+ if (params.messageVersion === 1) {
4144
+ const preparedMessage = prepareSolanaOffchainMessageV1({
4145
+ message: messageBytes,
4146
+ requiredSigners: params.requiredSigners
4147
+ });
4148
+ const { SignMessageVersion } = await ctx.importLedgerKit(
4149
+ "@ledgerhq/device-signer-kit-solana"
4150
+ );
4151
+ const result2 = await solSigner.signMessage(path, preparedMessage.serializedMessage, {
4152
+ version: SignMessageVersion.Raw
4153
+ });
4154
+ return { signature: decodeBase58Signature(result2.signature) };
4155
+ }
3426
4156
  const result = await solSigner.signMessage(path, messageBytes);
3427
- return { signature: result.signature };
4157
+ return { signature: decodeBase58EnvelopeSignature(result.signature) };
3428
4158
  } catch (err) {
3429
4159
  ctx.invalidateSession(sessionId);
3430
4160
  throw ctx.wrapError(err);
@@ -3432,11 +4162,27 @@ async function solSignMessage(ctx, sessionId, params) {
3432
4162
  ctx.clearCanceller(sessionId);
3433
4163
  }
3434
4164
  }
4165
+ function decodeBase58Signature(signature) {
4166
+ const bytes = bs58.decode(signature);
4167
+ if (bytes.length !== 64) {
4168
+ throw new Error(`Ledger Solana signature must be 64 bytes, received ${bytes.length}`);
4169
+ }
4170
+ return bytesToHex(bytes);
4171
+ }
4172
+ function decodeBase58EnvelopeSignature(envelope) {
4173
+ const bytes = bs58.decode(envelope);
4174
+ if (bytes.length < 65 || bytes[0] !== 1) {
4175
+ throw new Error("Ledger Solana signature envelope is invalid");
4176
+ }
4177
+ return bytesToHex(bytes.subarray(1, 65));
4178
+ }
3435
4179
  async function _createSolSigner(ctx, sessionId) {
3436
4180
  const dmk = await ctx.getOrCreateDmk();
3437
- const { ContextModuleBuilder: ContextModuleBuilder2 } = await ctx.importLedgerKit("@ledgerhq/context-module");
4181
+ const { ContextModuleBuilder: ContextModuleBuilder2, ContextModuleChainID: ContextModuleChainID2 } = await ctx.importLedgerKit(
4182
+ "@ledgerhq/context-module"
4183
+ );
3438
4184
  const { SignerSolanaBuilder } = await ctx.importLedgerKit("@ledgerhq/device-signer-kit-solana");
3439
- const contextModule = new ContextModuleBuilder2({}).removeDefaultLoaders().build();
4185
+ const contextModule = new ContextModuleBuilder2({}).setChain(ContextModuleChainID2.Solana).removeDefaultLoaders().build();
3440
4186
  const sdkSigner = new SignerSolanaBuilder({ dmk, sessionId }).withContextModule(contextModule).build();
3441
4187
  const signer = new SignerSol(sdkSigner);
3442
4188
  signer.onInteraction = (interaction) => {
@@ -3452,361 +4198,192 @@ async function _createSolSigner(ctx, sessionId) {
3452
4198
  return signer;
3453
4199
  }
3454
4200
 
3455
- // src/connector/chains/tron.ts
3456
- import { HardwareErrorCode as HardwareErrorCode7 } from "@onekeyfe/hwk-adapter-core";
3457
- import Trx from "@ledgerhq/hw-app-trx";
4201
+ // src/connector/chains/zcash.ts
4202
+ import { bytesToHex as bytesToHex2 } from "@onekeyfe/hwk-adapter-core";
3458
4203
 
3459
- // src/connector/chains/legacyChainCall.ts
3460
- import { EConnectorInteraction as EConnectorInteraction3 } from "@onekeyfe/hwk-adapter-core";
4204
+ // src/signer/SignerZcash.ts
4205
+ var SignerZcash = class {
4206
+ // eslint-disable-next-line no-useless-constructor, no-empty-function
4207
+ constructor(_sdk) {
4208
+ this._sdk = _sdk;
4209
+ }
4210
+ /** GET_VK: UFVK string (default) or raw 96-byte Orchard FVK. */
4211
+ async getFullViewingKey(derivationPath, options) {
4212
+ const action = this._sdk.getFullViewingKey(derivationPath, options);
4213
+ return deviceActionToPromise(
4214
+ action,
4215
+ this.onInteraction,
4216
+ void 0,
4217
+ this.onRegisterCanceller
4218
+ );
4219
+ }
4220
+ /** GET_SHIELDED_ADDRESS: single-Orchard-receiver UA for a 5-level transparent path. */
4221
+ async getShieldedAddress(derivationPath, options) {
4222
+ const action = this._sdk.getShieldedAddress(derivationPath, options);
4223
+ return deviceActionToPromise(
4224
+ action,
4225
+ this.onInteraction,
4226
+ void 0,
4227
+ this.onRegisterCanceller
4228
+ );
4229
+ }
4230
+ };
3461
4231
 
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);
3528
- }
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
- }
3565
- );
3566
- }
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
- });
4232
+ // src/connector/chains/zcash.ts
4233
+ async function zcashGetFullViewingKey(ctx, sessionId, params) {
4234
+ const signer = await _createZcashSigner(ctx, sessionId);
4235
+ const path = normalizePath(params.path);
4236
+ const mode = params.mode ?? "ufvk";
4237
+ try {
4238
+ const result = await signer.getFullViewingKey(path, { mode });
4239
+ if (result.mode === "ufvk") {
4240
+ return { path: params.path, mode, ufvk: result.fullViewingKey };
3598
4241
  }
4242
+ return { path: params.path, mode, orchardFvk: bytesToHex2(result.fullViewingKey) };
4243
+ } catch (err) {
4244
+ ctx.invalidateSession(sessionId);
4245
+ throw ctx.wrapError(err);
4246
+ } finally {
4247
+ ctx.clearCanceller(sessionId);
3599
4248
  }
3600
- async _closeCurrentApp(sessionId) {
3601
- debugLog("[AppManager] closeCurrentApp");
3602
- await this._dmk.sendCommand({
3603
- sessionId,
3604
- command: new CloseAppCommand()
4249
+ }
4250
+ async function zcashGetShieldedAddress(ctx, sessionId, params) {
4251
+ const signer = await _createZcashSigner(ctx, sessionId);
4252
+ const path = normalizePath(params.path);
4253
+ try {
4254
+ const result = await signer.getShieldedAddress(path, {
4255
+ checkOnDevice: params.showOnDevice ?? false
3605
4256
  });
4257
+ return { address: result.address, path: params.path };
4258
+ } catch (err) {
4259
+ ctx.invalidateSession(sessionId);
4260
+ throw ctx.wrapError(err);
4261
+ } finally {
4262
+ ctx.clearCanceller(sessionId);
3606
4263
  }
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})`
3629
- );
3630
- }
3631
- _isDashboard(appName) {
3632
- return appName === DASHBOARD_APP_NAME;
3633
- }
3634
- _wait() {
3635
- return new Promise((resolve) => setTimeout(resolve, this._waitMs));
3636
- }
3637
- };
3638
-
3639
- // src/connector/chains/legacyChainCall.ts
3640
- function isLegacyWrongAppError(err, _appName) {
3641
- return isWrongAppError(err);
3642
4264
  }
3643
- async function withLegacyChainCall(ctx, sessionId, options, action) {
3644
- const { appName, needsConfirmation } = options;
3645
- let openAppPromptShown = false;
3646
- const onAppOpenPrompt = () => {
3647
- openAppPromptShown = true;
4265
+ async function _createZcashSigner(ctx, sessionId) {
4266
+ const dmk = await ctx.getOrCreateDmk();
4267
+ const { SignerZcashBuilder } = await ctx.importLedgerKit("@ledgerhq/device-signer-kit-zcash");
4268
+ const sdkSigner = new SignerZcashBuilder({ dmk, sessionId }).build();
4269
+ const signer = new SignerZcash(sdkSigner);
4270
+ signer.onInteraction = (interaction) => {
4271
+ debugLog("[LedgerConnector] zcash.onInteraction:", interaction);
3648
4272
  ctx.emit("ui-event", {
3649
- type: EConnectorInteraction3.ConfirmOpenApp,
4273
+ type: collapseSignerInteraction(interaction),
3650
4274
  payload: { sessionId }
3651
4275
  });
3652
4276
  };
3653
- const closeOpenAppUiIfShown = () => {
3654
- if (openAppPromptShown) {
3655
- ctx.emit("ui-event", {
3656
- type: EConnectorInteraction3.InteractionComplete,
3657
- payload: { sessionId }
3658
- });
3659
- openAppPromptShown = false;
3660
- }
3661
- };
3662
- 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
- }
3692
- }
3693
- };
3694
- try {
3695
- return await runOnce();
3696
- } 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;
3718
- }
3719
- }
3720
- async function _ensureAppOpen(ctx, sessionId, appName, onPrompt) {
3721
- const dmk = await ctx.getOrCreateDmk();
3722
- const appManager = new AppManager(dmk);
3723
- await appManager.ensureAppOpen(sessionId, appName, onPrompt);
4277
+ signer.onRegisterCanceller = (cancel) => ctx.registerCanceller(sessionId, cancel);
4278
+ return signer;
3724
4279
  }
3725
4280
 
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;
4281
+ // src/connector/chains/tron.ts
4282
+ import { HardwareErrorCode as HardwareErrorCode6, bytesToHex as bytesToHex3, hexToBytes as hexToBytes4 } from "@onekeyfe/hwk-adapter-core";
4283
+
4284
+ // src/signer/SignerTron.ts
4285
+ var SignerTron = class {
4286
+ // eslint-disable-next-line no-useless-constructor, no-empty-function
4287
+ constructor(_sdk) {
4288
+ this._sdk = _sdk;
3733
4289
  }
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;
4290
+ /** Base58 Tron address plus the uncompressed public key at `derivationPath`. */
4291
+ async getAddress(derivationPath, options) {
4292
+ const action = this._sdk.getAddress(derivationPath, options);
4293
+ const result = await deviceActionToPromise(
4294
+ action,
4295
+ this.onInteraction,
4296
+ void 0,
4297
+ this.onRegisterCanceller
4298
+ );
4299
+ return { address: result.address, publicKey: result.publicKey };
3746
4300
  }
3747
- async close() {
4301
+ /** Sign a protobuf-encoded raw transaction. */
4302
+ async signTransaction(derivationPath, transaction, options) {
4303
+ const action = this._sdk.signTransaction(derivationPath, transaction, options);
4304
+ return deviceActionToPromise(
4305
+ action,
4306
+ this.onInteraction,
4307
+ void 0,
4308
+ this.onRegisterCanceller
4309
+ );
4310
+ }
4311
+ /** Sign a personal message (TIP-191). */
4312
+ async signPersonalMessage(derivationPath, message, options) {
4313
+ const action = this._sdk.signPersonalMessage(derivationPath, message, options);
4314
+ return deviceActionToPromise(
4315
+ action,
4316
+ this.onInteraction,
4317
+ void 0,
4318
+ this.onRegisterCanceller
4319
+ );
3748
4320
  }
3749
4321
  };
3750
4322
 
3751
4323
  // src/connector/chains/tron.ts
3752
4324
  async function tronGetAddress(ctx, sessionId, params) {
4325
+ const tronSigner = await _createTronSigner(ctx, sessionId);
3753
4326
  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
- );
4327
+ try {
4328
+ const result = await tronSigner.getAddress(path, {
4329
+ checkOnDevice: params.showOnDevice ?? false
4330
+ });
4331
+ return { address: result.address, publicKey: result.publicKey, path: params.path };
4332
+ } catch (err) {
4333
+ ctx.invalidateSession(sessionId);
4334
+ throw ctx.wrapError(err);
4335
+ } finally {
4336
+ ctx.clearCanceller(sessionId);
4337
+ }
3770
4338
  }
3771
4339
  async function tronSignTransaction(ctx, sessionId, params) {
3772
4340
  if (!params.rawTxHex) {
3773
4341
  throw Object.assign(
3774
4342
  new Error("TRON signing requires a protobuf-encoded raw transaction hex (rawTxHex)."),
3775
- { code: HardwareErrorCode7.InvalidParams }
4343
+ { code: HardwareErrorCode6.InvalidParams }
3776
4344
  );
3777
4345
  }
4346
+ const tronSigner = await _createTronSigner(ctx, sessionId);
3778
4347
  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
- );
4348
+ try {
4349
+ const signature = await tronSigner.signTransaction(path, hexToBytes4(params.rawTxHex));
4350
+ return { signature: bytesToHex3(signature) };
4351
+ } catch (err) {
4352
+ ctx.invalidateSession(sessionId);
4353
+ throw ctx.wrapError(err);
4354
+ } finally {
4355
+ ctx.clearCanceller(sessionId);
4356
+ }
3793
4357
  }
3794
4358
  async function tronSignMessage(ctx, sessionId, params) {
4359
+ const tronSigner = await _createTronSigner(ctx, sessionId);
3795
4360
  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
- );
4361
+ try {
4362
+ const signature = await tronSigner.signPersonalMessage(path, hexToBytes4(params.messageHex));
4363
+ return { signature: bytesToHex3(signature) };
4364
+ } catch (err) {
4365
+ ctx.invalidateSession(sessionId);
4366
+ throw ctx.wrapError(err);
4367
+ } finally {
4368
+ ctx.clearCanceller(sessionId);
4369
+ }
3806
4370
  }
3807
- async function _createTrx(ctx, sessionId) {
4371
+ async function _createTronSigner(ctx, sessionId) {
3808
4372
  const dmk = await ctx.getOrCreateDmk();
3809
- return new Trx(new DmkTransport(dmk, sessionId));
4373
+ const { SignerTrxBuilder } = await ctx.importLedgerKit("@ledgerhq/device-signer-kit-tron");
4374
+ const sdkSigner = new SignerTrxBuilder({ dmk, sessionId }).build();
4375
+ const signer = new SignerTron(sdkSigner);
4376
+ signer.onInteraction = (interaction) => {
4377
+ debugLog("[LedgerConnector] tron.onInteraction:", interaction);
4378
+ ctx.emit("ui-event", {
4379
+ type: collapseSignerInteraction(interaction),
4380
+ payload: { sessionId }
4381
+ });
4382
+ };
4383
+ signer.onRegisterCanceller = (cancel) => {
4384
+ ctx.registerCanceller(sessionId, cancel);
4385
+ };
4386
+ return signer;
3810
4387
  }
3811
4388
 
3812
4389
  // src/device-apps/customActions.ts
@@ -3985,7 +4562,7 @@ var DeviceApps = class {
3985
4562
  seTargetId: v.seTargetId,
3986
4563
  mcuTargetId: v.mcuTargetId,
3987
4564
  seVersion: v.seVersion,
3988
- seFlagsHex: bytesToHex2(v.seFlags),
4565
+ seFlagsHex: bytesToHex4(v.seFlags),
3989
4566
  mcuSephVersion: v.mcuSephVersion,
3990
4567
  mcuBootloaderVersion: v.mcuBootloaderVersion,
3991
4568
  hwVersion: v.hwVersion
@@ -4059,7 +4636,7 @@ var DeviceApps = class {
4059
4636
  }
4060
4637
  }
4061
4638
  };
4062
- function bytesToHex2(bytes) {
4639
+ function bytesToHex4(bytes) {
4063
4640
  if (!bytes) return "";
4064
4641
  return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
4065
4642
  }
@@ -4102,10 +4679,11 @@ var METHOD_PREFIX_TO_APP_NAME = {
4102
4679
  evm: "Ethereum",
4103
4680
  btc: "Bitcoin",
4104
4681
  sol: "Solana",
4105
- tron: "Tron"
4682
+ tron: "Tron",
4683
+ zcash: "Zcash"
4106
4684
  };
4107
4685
  var HARDWARE_ERROR_CODE_VALUES = new Set(
4108
- Object.values(HardwareErrorCode8).filter((value) => typeof value === "number")
4686
+ Object.values(HardwareErrorCode7).filter((value) => typeof value === "number")
4109
4687
  );
4110
4688
  var BLE_CONNECT_SCAN_TIMEOUT_MS = 1500;
4111
4689
  var LEDGER_RELAY_ALLOWED_ROOT_DOMAINS = ["onekeytest.com", "onekey.com"];
@@ -4142,6 +4720,10 @@ async function defaultLedgerKitImporter(pkg) {
4142
4720
  return import("@ledgerhq/device-signer-kit-bitcoin");
4143
4721
  case "@ledgerhq/device-signer-kit-solana":
4144
4722
  return import("@ledgerhq/device-signer-kit-solana");
4723
+ case "@ledgerhq/device-signer-kit-tron":
4724
+ return import("@ledgerhq/device-signer-kit-tron");
4725
+ case "@ledgerhq/device-signer-kit-zcash":
4726
+ return import("@ledgerhq/device-signer-kit-zcash");
4145
4727
  case "@ledgerhq/context-module":
4146
4728
  return import("@ledgerhq/context-module");
4147
4729
  default:
@@ -4300,7 +4882,7 @@ var LedgerConnectorBase = class {
4300
4882
  `Ledger BLE connect did not return within ${HANG_CEILING_MS / 6e4}min \u2014 DMK hang fallback.`
4301
4883
  );
4302
4884
  err._tag = ERROR_TAG.BlePairingTimeout;
4303
- err.code = HardwareErrorCode8.BlePairingTimeout;
4885
+ err.code = HardwareErrorCode7.BlePairingTimeout;
4304
4886
  reject(err);
4305
4887
  }, HANG_CEILING_MS);
4306
4888
  });
@@ -4333,7 +4915,7 @@ var LedgerConnectorBase = class {
4333
4915
  "Ledger device is not currently advertising. Wake up and unlock the device, keep it nearby, then try again."
4334
4916
  );
4335
4917
  err._tag = ERROR_TAG.DeviceNotAdvertising;
4336
- err.code = HardwareErrorCode8.DeviceNotFound;
4918
+ err.code = HardwareErrorCode7.DeviceNotFound;
4337
4919
  throw err;
4338
4920
  };
4339
4921
  const doConnect = async (path) => {
@@ -4397,7 +4979,7 @@ var LedgerConnectorBase = class {
4397
4979
  "Ledger Bluetooth pairing failed. Make sure the device is unlocked and nearby, then try again."
4398
4980
  );
4399
4981
  wrapped._tag = ERROR_TAG.BleGattBondingFailed;
4400
- wrapped.code = HardwareErrorCode8.BlePairingTimeout;
4982
+ wrapped.code = HardwareErrorCode7.BlePairingTimeout;
4401
4983
  wrapped.originalError = err;
4402
4984
  throw wrapped;
4403
4985
  }
@@ -4475,7 +5057,7 @@ var LedgerConnectorBase = class {
4475
5057
  this._unwatchSessionState(sessionId);
4476
5058
  this._signerManager?.invalidate(sessionId);
4477
5059
  this._cancellers.get(sessionId)?.({
4478
- code: HardwareErrorCode8.DeviceDisconnected,
5060
+ code: HardwareErrorCode7.DeviceDisconnected,
4479
5061
  tag: "DeviceDisconnected",
4480
5062
  message: "Device disconnected"
4481
5063
  });
@@ -4501,7 +5083,7 @@ var LedgerConnectorBase = class {
4501
5083
  success: false,
4502
5084
  error: serializeConnectorError(
4503
5085
  Object.assign(new Error("Ledger app is unresponsive"), {
4504
- code: HardwareErrorCode8.DeviceAppStuck,
5086
+ code: HardwareErrorCode7.DeviceAppStuck,
4505
5087
  _tag: ERROR_TAG.DeviceAppStuck,
4506
5088
  originalError: err
4507
5089
  })
@@ -4513,7 +5095,7 @@ var LedgerConnectorBase = class {
4513
5095
  success: false,
4514
5096
  error: serializeConnectorError(
4515
5097
  Object.assign(new Error("Device communication interrupted, please retry"), {
4516
- code: HardwareErrorCode8.TransportError,
5098
+ code: HardwareErrorCode7.TransportError,
4517
5099
  _tag: ERROR_TAG.DeviceTransportStuck,
4518
5100
  originalError: err
4519
5101
  })
@@ -4559,6 +5141,11 @@ var LedgerConnectorBase = class {
4559
5141
  return solSignTransaction(ctx, sessionId, params);
4560
5142
  case "solSignMessage":
4561
5143
  return solSignMessage(ctx, sessionId, params);
5144
+ // ZCASH
5145
+ case "zcashGetFullViewingKey":
5146
+ return zcashGetFullViewingKey(ctx, sessionId, params);
5147
+ case "zcashGetShieldedAddress":
5148
+ return zcashGetShieldedAddress(ctx, sessionId, params);
4562
5149
  // TRON
4563
5150
  case "tronGetAddress":
4564
5151
  return tronGetAddress(ctx, sessionId, params);
@@ -4583,7 +5170,7 @@ var LedgerConnectorBase = class {
4583
5170
  try {
4584
5171
  return await apps.install(p.appName, ({ progress }) => {
4585
5172
  ctx.emit("ui-event", {
4586
- type: EConnectorInteraction4.AppInstallProgress,
5173
+ type: EConnectorInteraction3.AppInstallProgress,
4587
5174
  payload: {
4588
5175
  sessionId,
4589
5176
  appName: p.appName,
@@ -4888,7 +5475,7 @@ var LedgerConnectorBase = class {
4888
5475
  * at every catch site. Falls through unchanged for unknown methods.
4889
5476
  */
4890
5477
  _ctxForMethod(method) {
4891
- const prefix = /^(evm|btc|sol|tron)/.exec(method)?.[1];
5478
+ const prefix = /^(evm|btc|sol|tron|zcash)/.exec(method)?.[1];
4892
5479
  const defaultAppName = prefix ? METHOD_PREFIX_TO_APP_NAME[prefix] : void 0;
4893
5480
  if (!defaultAppName) return this._ctx;
4894
5481
  return {
@@ -4922,6 +5509,209 @@ var LedgerConnectorBase = class {
4922
5509
  return error;
4923
5510
  }
4924
5511
  };
5512
+
5513
+ // src/transport/DmkTransport.ts
5514
+ import Transport from "@ledgerhq/hw-transport";
5515
+ var DmkTransport = class extends Transport {
5516
+ constructor(dmk, sessionId) {
5517
+ super();
5518
+ this._dmk = dmk;
5519
+ this._sessionId = sessionId;
5520
+ }
5521
+ async exchange(apdu) {
5522
+ const response = await this._dmk.sendApdu({
5523
+ sessionId: this._sessionId,
5524
+ apdu: new Uint8Array(apdu)
5525
+ });
5526
+ const { data, statusCode } = response;
5527
+ const result = Buffer.alloc(data.length + 2);
5528
+ if (data.length > 0) {
5529
+ result.set(data, 0);
5530
+ }
5531
+ result.set(statusCode, data.length);
5532
+ return result;
5533
+ }
5534
+ async close() {
5535
+ }
5536
+ };
5537
+
5538
+ // src/app/AppManager.ts
5539
+ import {
5540
+ CloseAppCommand,
5541
+ GetAppAndVersionCommand,
5542
+ OpenAppCommand,
5543
+ isSuccessCommandResult
5544
+ } from "@ledgerhq/device-management-kit";
5545
+ import { HardwareErrorCode as HardwareErrorCode8 } from "@onekeyfe/hwk-adapter-core";
5546
+ var APP_NAME_MAP = {
5547
+ ETH: "Ethereum",
5548
+ BTC: "Bitcoin",
5549
+ SOL: "Solana",
5550
+ TRX: "Tron",
5551
+ ZEC: "Zcash",
5552
+ XRP: "XRP",
5553
+ ADA: "Cardano",
5554
+ DOT: "Polkadot",
5555
+ ATOM: "Cosmos"
5556
+ };
5557
+ var DASHBOARD_APP_NAME = "BOLOS";
5558
+ var AppManager = class {
5559
+ constructor(dmk, options) {
5560
+ this._dmk = dmk;
5561
+ this._waitMs = options?.waitMs ?? 1e3;
5562
+ this._maxRetries = options?.maxRetries ?? 10;
5563
+ }
5564
+ /**
5565
+ * Return the Ledger app name for a given chain ticker,
5566
+ * or undefined if the chain is not supported.
5567
+ */
5568
+ static getAppName(chain) {
5569
+ return APP_NAME_MAP[chain];
5570
+ }
5571
+ /**
5572
+ * Ensure the target app is open on the device identified by `sessionId`.
5573
+ *
5574
+ * Flow:
5575
+ * 1. Check the currently running app.
5576
+ * 2. If it is already the target, return immediately.
5577
+ * 3. If a different app is running (not dashboard), close it first.
5578
+ * 4. Open the target app.
5579
+ * 5. Poll until the device confirms the target app is running.
5580
+ */
5581
+ /**
5582
+ * @param onConfirmOnDevice Called BEFORE OpenAppCommand is issued — the
5583
+ * device is about to display "Open <app>" on screen and wait for the
5584
+ * user's button press. UI consumers should show their "open app" prompt
5585
+ * in response. NOT called when the target app is already open (no user
5586
+ * interaction needed in that case).
5587
+ *
5588
+ * Important: OpenAppCommand is blocking. It does not resolve until the user
5589
+ * has physically confirmed on the device, so anything that runs AFTER
5590
+ * `await this._openApp(...)` lands AFTER the prompt is already gone.
5591
+ * Hence the callback must fire BEFORE that await.
5592
+ */
5593
+ async ensureAppOpen(sessionId, targetAppName, onConfirmOnDevice) {
5594
+ const currentApp = await this._getCurrentApp(sessionId);
5595
+ if (currentApp === targetAppName) {
5596
+ return;
5597
+ }
5598
+ if (!this._isDashboard(currentApp)) {
5599
+ await this._closeCurrentApp(sessionId);
5600
+ await this._waitForApp(sessionId, DASHBOARD_APP_NAME);
5601
+ }
5602
+ onConfirmOnDevice?.();
5603
+ await this._openApp(sessionId, targetAppName);
5604
+ await this._waitForApp(sessionId, targetAppName);
5605
+ }
5606
+ // ---------------------------------------------------------------------------
5607
+ // Private helpers
5608
+ // ---------------------------------------------------------------------------
5609
+ async _getCurrentApp(sessionId) {
5610
+ const result = await this._dmk.sendCommand({
5611
+ sessionId,
5612
+ command: new GetAppAndVersionCommand()
5613
+ });
5614
+ if (isSuccessCommandResult(result)) {
5615
+ debugLog("[AppManager] currentApp:", result.data.name);
5616
+ return result.data.name;
5617
+ }
5618
+ const errResult = result;
5619
+ const dmkErr = errResult.error ?? {};
5620
+ const original = dmkErr.originalError;
5621
+ debugLog(
5622
+ "[AppManager] _getCurrentApp failed sessionId=",
5623
+ sessionId,
5624
+ "tag=",
5625
+ dmkErr._tag,
5626
+ "errorCode=",
5627
+ dmkErr.errorCode,
5628
+ "message=",
5629
+ dmkErr.message,
5630
+ "originalErrorMessage=",
5631
+ original?.message ?? String(original ?? "")
5632
+ );
5633
+ throw Object.assign(
5634
+ new Error(
5635
+ dmkErr.message ?? "Failed to get current app from device"
5636
+ ),
5637
+ {
5638
+ _tag: dmkErr._tag,
5639
+ errorCode: dmkErr.errorCode,
5640
+ originalError: original
5641
+ }
5642
+ );
5643
+ }
5644
+ async _openApp(sessionId, appName) {
5645
+ const result = await this._dmk.sendCommand({
5646
+ sessionId,
5647
+ command: new OpenAppCommand({ appName })
5648
+ });
5649
+ if (!isSuccessCommandResult(result)) {
5650
+ const dmkErr = result.error;
5651
+ const errorCode = "errorCode" in dmkErr && dmkErr.errorCode != null ? String(dmkErr.errorCode) : "";
5652
+ const message = "message" in dmkErr && typeof dmkErr.message === "string" ? dmkErr.message : "";
5653
+ debugLog(
5654
+ "[AppManager] openApp failed:",
5655
+ appName,
5656
+ "errorCode:",
5657
+ errorCode,
5658
+ "tag:",
5659
+ dmkErr._tag
5660
+ );
5661
+ let code;
5662
+ if (errorCode === "6807" || /unknown application/i.test(message)) {
5663
+ code = HardwareErrorCode8.AppNotInstalled;
5664
+ } else if (errorCode === "5501" || dmkErr._tag === "ActionRefusedError") {
5665
+ code = HardwareErrorCode8.UserRejected;
5666
+ }
5667
+ throw Object.assign(new Error(`Failed to open "${appName}"`), {
5668
+ _tag: ERROR_TAG.OpenAppCommand,
5669
+ code,
5670
+ errorCode,
5671
+ statusCode: errorCode,
5672
+ appName,
5673
+ originalError: dmkErr
5674
+ });
5675
+ }
5676
+ }
5677
+ async _closeCurrentApp(sessionId) {
5678
+ debugLog("[AppManager] closeCurrentApp");
5679
+ await this._dmk.sendCommand({
5680
+ sessionId,
5681
+ command: new CloseAppCommand()
5682
+ });
5683
+ }
5684
+ /**
5685
+ * Poll the device until the expected app is reported as running,
5686
+ * or throw after `_maxRetries` attempts.
5687
+ */
5688
+ async _waitForApp(sessionId, expectedAppName) {
5689
+ let lastSeen = "";
5690
+ for (let i = 0; i < this._maxRetries; i++) {
5691
+ await this._wait();
5692
+ const current = await this._getCurrentApp(sessionId);
5693
+ lastSeen = current;
5694
+ if (current === expectedAppName) {
5695
+ return;
5696
+ }
5697
+ }
5698
+ debugLog(
5699
+ "[AppManager] waitForApp exhausted: expected=",
5700
+ expectedAppName,
5701
+ "lastSeen=",
5702
+ lastSeen
5703
+ );
5704
+ throw new Error(
5705
+ `Ledger: failed to open "${expectedAppName}" after ${this._maxRetries} retries (last seen: ${lastSeen})`
5706
+ );
5707
+ }
5708
+ _isDashboard(appName) {
5709
+ return appName === DASHBOARD_APP_NAME;
5710
+ }
5711
+ _wait() {
5712
+ return new Promise((resolve) => setTimeout(resolve, this._waitMs));
5713
+ }
5714
+ };
4925
5715
  export {
4926
5716
  AppManager,
4927
5717
  DmkTransport,
@@ -4932,6 +5722,7 @@ export {
4932
5722
  SignerEth,
4933
5723
  SignerManager,
4934
5724
  SignerSol,
5725
+ SignerZcash,
4935
5726
  debugLog,
4936
5727
  deviceActionToPromise,
4937
5728
  isDeviceLockedError,