@onekeyfe/hwk-ledger-adapter 1.2.5 → 1.2.6-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -6,30 +6,41 @@ import {
6
6
  DeviceJobQueue,
7
7
  EConnectorInteraction,
8
8
  HardwareErrorCode as HardwareErrorCode3,
9
+ OperationRegistry,
10
+ SDK,
9
11
  TypedEventEmitter,
10
12
  UI_REQUEST,
11
13
  UI_REQUEST_PREEMPTED_TAG,
12
14
  UiRequestRegistry,
15
+ canReplayHardwareMethodAfterTransportFailure,
13
16
  createHwkError,
14
17
  deriveDeviceFingerprint,
15
18
  failure as failure2,
19
+ isHardwareOperationId,
20
+ isHwkRecoveryHint,
21
+ operationMayHaveCompletedParams,
16
22
  rehydrateConnectorError,
23
+ requestBleDeviceSelection,
24
+ requestSaveDeviceBinding,
25
+ resolveSearchTargetReusePolicy,
17
26
  success
18
27
  } from "@onekeyfe/hwk-adapter-core";
19
28
 
20
29
  // src/errors.ts
21
- import { HardwareErrorCode, enrichErrorMessage } from "@onekeyfe/hwk-adapter-core";
22
- var MULTIPLE_USB_LEDGER_DEVICES_ERROR_MESSAGE = "Multiple Ledger USB devices are connected. Please connect only one Ledger device and try again.";
23
- function createMultipleUsbLedgerDevicesError() {
24
- return Object.assign(new Error(MULTIPLE_USB_LEDGER_DEVICES_ERROR_MESSAGE), {
25
- code: HardwareErrorCode.DeviceOneDeviceOnly
26
- });
27
- }
28
- function ledgerFailure(code, error, appName, tag, params) {
30
+ import {
31
+ HardwareErrorCode,
32
+ defaultOriginForCode,
33
+ defaultRecoveryForCode,
34
+ enrichErrorMessage
35
+ } from "@onekeyfe/hwk-adapter-core";
36
+ function ledgerFailure(code, error, appName, tag, params, origin, recovery) {
29
37
  const payload = { error, code };
30
38
  if (appName !== void 0) payload.appName = appName;
31
39
  if (tag !== void 0) payload._tag = tag;
32
40
  if (params !== void 0) payload.params = params;
41
+ const resolvedOrigin = origin ?? defaultOriginForCode(code);
42
+ if (resolvedOrigin !== void 0) payload.origin = resolvedOrigin;
43
+ payload.recovery = recovery ?? defaultRecoveryForCode(code);
33
44
  return { success: false, payload };
34
45
  }
35
46
  var LOCKED_ERROR_CODES = /* @__PURE__ */ new Set(["5515", "21781", "6982", "27010", "5303", "21251"]);
@@ -170,7 +181,10 @@ var ERROR_TAG = {
170
181
  UnknownDevice: "UnknownDeviceError",
171
182
  DeviceSessionRefresher: "DeviceSessionRefresherError",
172
183
  DeviceNotInitialized: "DeviceNotInitializedError",
173
- OpeningConnection: "OpeningConnectionError",
184
+ // DMK class OpeningConnectionError carries `_tag` "ConnectionOpeningError"; both
185
+ // spellings are kept so a DMK release that aligns them still classifies.
186
+ OpeningConnection: "ConnectionOpeningError",
187
+ OpeningConnectionLegacy: "OpeningConnectionError",
174
188
  DeviceDisconnectedBeforeSendingApdu: "DeviceDisconnectedBeforeSendingApdu",
175
189
  DeviceDisconnectedWhileSending: "DeviceDisconnectedWhileSendingError",
176
190
  Disconnect: "DisconnectError",
@@ -183,7 +197,16 @@ var ERROR_TAG = {
183
197
  // DMK remote-network failures (manager-api HTTP / secure-channel WS).
184
198
  WebSocketConnection: "WebSocketConnectionError",
185
199
  HttpFetch: "FetchError",
186
- InvalidFirmwareMetadataResponse: "InvalidGetFirmwareMetadataResponseError"
200
+ NetworkDA: "NetworkDAError",
201
+ InvalidFirmwareMetadataResponse: "InvalidGetFirmwareMetadataResponseError",
202
+ ApplicationsMetadataTask: "GetApplicationsMetadataTaskError",
203
+ // DMK OS device actions. SecureChannelError is what mapInstallDAErrors() leaves
204
+ // after splitting out device answers, i.e. the relay itself broke.
205
+ SecureChannel: "SecureChannelError",
206
+ RefusedByUserDA: "RefusedByUserDAError",
207
+ AppAlreadyInstalledDA: "AppAlreadyInstalledDAError",
208
+ OutOfMemoryDA: "OutOfMemoryDAError",
209
+ DeviceNotOnboarded: "DeviceNotOnboardedError"
187
210
  };
188
211
  function isDeviceLockedError(err) {
189
212
  if (!err || typeof err !== "object") return false;
@@ -213,7 +236,6 @@ function isBlePairingFailureError(err) {
213
236
  return false;
214
237
  }
215
238
  var CONNECTION_LEVEL_TAGS = /* @__PURE__ */ new Set([
216
- ERROR_TAG.DeviceLocked,
217
239
  ERROR_TAG.DeviceNotAdvertising,
218
240
  ERROR_TAG.BlePairingTimeout,
219
241
  ERROR_TAG.BleGattBondingFailed,
@@ -225,6 +247,7 @@ var CONNECTION_LEVEL_TAGS = /* @__PURE__ */ new Set([
225
247
  ERROR_TAG.DeviceSessionRefresher,
226
248
  ERROR_TAG.DeviceNotInitialized,
227
249
  ERROR_TAG.OpeningConnection,
250
+ ERROR_TAG.OpeningConnectionLegacy,
228
251
  ERROR_TAG.DeviceDisconnectedBeforeSendingApdu,
229
252
  ERROR_TAG.DeviceDisconnectedWhileSending,
230
253
  ERROR_TAG.Disconnect,
@@ -239,7 +262,10 @@ var DEVICE_NOT_FOUND_TAGS = /* @__PURE__ */ new Set([
239
262
  // Map to DeviceNotFound so non-BLE-direct paths get a sensible error code.
240
263
  ERROR_TAG.DeviceNotInDiscoveryCache
241
264
  ]);
242
- var DEVICE_BUSY_TAGS = /* @__PURE__ */ new Set([ERROR_TAG.OpeningConnection]);
265
+ var DEVICE_BUSY_TAGS = /* @__PURE__ */ new Set([
266
+ ERROR_TAG.OpeningConnection,
267
+ ERROR_TAG.OpeningConnectionLegacy
268
+ ]);
243
269
  var DEVICE_DISCONNECTED_TAGS = /* @__PURE__ */ new Set([
244
270
  ERROR_TAG.DeviceNotRecognized,
245
271
  ERROR_TAG.DeviceSessionNotFound,
@@ -251,17 +277,18 @@ var DEVICE_DISCONNECTED_TAGS = /* @__PURE__ */ new Set([
251
277
  ERROR_TAG.WebHIDDisconnect
252
278
  ]);
253
279
  function isConnectionLevelError(err) {
254
- if (!err || typeof err !== "object") return false;
255
- const tag = err._tag;
256
- if (tag && CONNECTION_LEVEL_TAGS.has(tag)) return true;
257
- const e = err;
258
- if (e.originalError != null && isConnectionLevelError(e.originalError)) return true;
259
- if (e.error != null && e._tag && isConnectionLevelError(e.error)) return true;
260
- return false;
280
+ return hasErrorTag(err, CONNECTION_LEVEL_TAGS);
261
281
  }
262
282
  function isKnownConnectionTag(tag) {
263
283
  return typeof tag === "string" && CONNECTION_LEVEL_TAGS.has(tag);
264
284
  }
285
+ var CONNECTION_OPENING_TAGS = /* @__PURE__ */ new Set([
286
+ ERROR_TAG.OpeningConnection,
287
+ ERROR_TAG.OpeningConnectionLegacy
288
+ ]);
289
+ function isConnectionOpeningTag(tag) {
290
+ return typeof tag === "string" && CONNECTION_OPENING_TAGS.has(tag);
291
+ }
265
292
  function hasStatusCode(err, codeSet) {
266
293
  if (!err || typeof err !== "object") return false;
267
294
  const e = err;
@@ -297,28 +324,28 @@ function hasInvalidArgumentCode(err) {
297
324
  if (e.error != null && e._tag && hasInvalidArgumentCode(e.error)) return true;
298
325
  return false;
299
326
  }
300
- function isDeviceNotFoundError(err) {
327
+ function hasErrorTag(err, tags) {
301
328
  if (!err || typeof err !== "object") return false;
302
- const tag = err._tag;
303
- if (tag && DEVICE_NOT_FOUND_TAGS.has(tag)) return true;
304
329
  const e = err;
305
- if (e.originalError != null && isDeviceNotFoundError(e.originalError)) return true;
306
- if (e.error != null && e._tag && isDeviceNotFoundError(e.error)) return true;
330
+ if (typeof e._tag === "string" && tags.has(e._tag)) return true;
331
+ if (e.originalError != null && hasErrorTag(e.originalError, tags)) return true;
332
+ if (e.error != null && e._tag && hasErrorTag(e.error, tags)) return true;
307
333
  return false;
308
334
  }
335
+ function isDeviceNotFoundError(err) {
336
+ return hasErrorTag(err, DEVICE_NOT_FOUND_TAGS);
337
+ }
309
338
  function isDeviceBusyError(err) {
310
- if (!err || typeof err !== "object") return false;
311
- const tag = err._tag;
312
- if (tag && DEVICE_BUSY_TAGS.has(tag)) return true;
313
- const e = err;
314
- if (e.originalError != null && isDeviceBusyError(e.originalError)) return true;
315
- if (e.error != null && e._tag && isDeviceBusyError(e.error)) return true;
316
- return false;
339
+ return hasErrorTag(err, DEVICE_BUSY_TAGS);
317
340
  }
341
+ var USER_REJECTED_TAGS = /* @__PURE__ */ new Set([
342
+ ERROR_TAG.UserRefusedOnDevice,
343
+ ERROR_TAG.RefusedByUserDA
344
+ ]);
318
345
  function isUserRejectedError(err) {
319
346
  if (!err || typeof err !== "object") return false;
320
347
  const e = err;
321
- if (e._tag === ERROR_TAG.UserRefusedOnDevice) return true;
348
+ if (hasErrorTag(err, USER_REJECTED_TAGS)) return true;
322
349
  if (typeof e.message === "string" && /denied|rejected|refused/i.test(e.message)) return true;
323
350
  if (hasStatusCode(err, USER_REJECTED_CODES)) return true;
324
351
  return false;
@@ -349,15 +376,33 @@ function isAppNotInstalledError(err) {
349
376
  if (hasStatusCode(err, APP_NOT_INSTALLED_CODES)) return true;
350
377
  return false;
351
378
  }
379
+ var OUT_OF_MEMORY_TAGS = /* @__PURE__ */ new Set([ERROR_TAG.OutOfMemoryDA]);
352
380
  function isOutOfMemoryError(err) {
353
- if (!err || typeof err !== "object") return false;
354
- const e = err;
355
- return e._tag === "OutOfMemoryDAError";
381
+ return hasErrorTag(err, OUT_OF_MEMORY_TAGS);
382
+ }
383
+ var APP_ALREADY_INSTALLED_TAGS = /* @__PURE__ */ new Set([ERROR_TAG.AppAlreadyInstalledDA]);
384
+ function isAppAlreadyInstalledError(err) {
385
+ return hasErrorTag(err, APP_ALREADY_INSTALLED_TAGS);
386
+ }
387
+ var DEVICE_NOT_ONBOARDED_TAGS = /* @__PURE__ */ new Set([ERROR_TAG.DeviceNotOnboarded]);
388
+ function isDeviceNotOnboardedError(err) {
389
+ return hasErrorTag(err, DEVICE_NOT_ONBOARDED_TAGS);
390
+ }
391
+ var FIRMWARE_METADATA_TAGS = /* @__PURE__ */ new Set([
392
+ ERROR_TAG.InvalidFirmwareMetadataResponse,
393
+ ERROR_TAG.ApplicationsMetadataTask
394
+ ]);
395
+ function isFirmwareMetadataError(err) {
396
+ return hasErrorTag(err, FIRMWARE_METADATA_TAGS);
397
+ }
398
+ var SECURE_CHANNEL_TAGS = /* @__PURE__ */ new Set([ERROR_TAG.SecureChannel]);
399
+ function isSecureChannelError(err) {
400
+ return hasErrorTag(err, SECURE_CHANNEL_TAGS);
356
401
  }
357
402
  var NETWORK_ERROR_TAGS = /* @__PURE__ */ new Set([
358
403
  ERROR_TAG.WebSocketConnection,
359
404
  ERROR_TAG.HttpFetch,
360
- ERROR_TAG.InvalidFirmwareMetadataResponse
405
+ ERROR_TAG.NetworkDA
361
406
  ]);
362
407
  function isNetworkError(err) {
363
408
  if (!err || typeof err !== "object") return false;
@@ -407,23 +452,34 @@ function isTransportStuck(err) {
407
452
  function isStuckAppStateError(err) {
408
453
  return isAppStuckByApdu(err) || isTransportStuck(err);
409
454
  }
455
+ var DMK_PLACEHOLDER_MESSAGE = "Unknown error.";
456
+ function nestedErrorMessage(nested) {
457
+ if (!nested || typeof nested !== "object") return void 0;
458
+ const { message } = nested;
459
+ if (typeof message !== "string") return void 0;
460
+ const trimmed = message.trim();
461
+ if (!trimmed || trimmed === DMK_PLACEHOLDER_MESSAGE) return void 0;
462
+ return message;
463
+ }
410
464
  function mapLedgerError(err, opts) {
411
465
  let originalMessage = "Unknown Ledger error";
412
466
  if (err instanceof Error) {
413
467
  originalMessage = err.message;
414
468
  } else if (err && typeof err === "object") {
415
469
  const e = err;
416
- originalMessage = String(e.message ?? e._tag ?? e.type ?? JSON.stringify(err));
470
+ originalMessage = String(
471
+ e.message ?? nestedErrorMessage(e.originalError) ?? e._tag ?? e.type ?? JSON.stringify(err)
472
+ );
417
473
  }
418
474
  let code;
419
475
  if (isDeviceLockedError(err)) {
420
476
  code = HardwareErrorCode.DeviceLocked;
421
477
  } else if (isDeviceNotAdvertisingError(err) || isDeviceNotFoundError(err)) {
422
478
  code = HardwareErrorCode.DeviceNotFound;
423
- } else if (isDeviceBusyError(err)) {
424
- code = HardwareErrorCode.DeviceBusy;
425
479
  } else if (isBlePairingFailureError(err)) {
426
480
  code = HardwareErrorCode.BlePairingTimeout;
481
+ } else if (isDeviceBusyError(err)) {
482
+ code = HardwareErrorCode.DeviceBusy;
427
483
  } else if (isUserAbortedError(err)) {
428
484
  code = HardwareErrorCode.UserAborted;
429
485
  } else if (isUserRejectedError(err)) {
@@ -432,10 +488,18 @@ function mapLedgerError(err, opts) {
432
488
  code = HardwareErrorCode.WrongApp;
433
489
  } else if (isAppNotInstalledError(err)) {
434
490
  code = HardwareErrorCode.AppNotInstalled;
491
+ } else if (isAppAlreadyInstalledError(err)) {
492
+ code = HardwareErrorCode.AppAlreadyInstalled;
435
493
  } else if (isOutOfMemoryError(err)) {
436
494
  code = HardwareErrorCode.DeviceOutOfMemory;
495
+ } else if (isDeviceNotOnboardedError(err)) {
496
+ code = HardwareErrorCode.DeviceNotInitialized;
497
+ } else if (isFirmwareMetadataError(err)) {
498
+ code = HardwareErrorCode.LedgerFirmwareMetadataError;
437
499
  } else if (isNetworkError(err)) {
438
500
  code = HardwareErrorCode.NetworkError;
501
+ } else if (isSecureChannelError(err)) {
502
+ code = HardwareErrorCode.LedgerSecureChannelError;
439
503
  } else if (isDeviceDisconnectedError(err)) {
440
504
  code = HardwareErrorCode.DeviceDisconnected;
441
505
  } else if (isTimeoutError(err)) {
@@ -448,11 +512,32 @@ function mapLedgerError(err, opts) {
448
512
  }
449
513
  const errAppName = err && typeof err === "object" ? err.appName : void 0;
450
514
  const appName = errAppName ?? opts?.defaultAppName;
451
- return { code, message: enrichErrorMessage(code, originalMessage), appName };
515
+ return {
516
+ code,
517
+ message: enrichErrorMessage(code, originalMessage),
518
+ origin: defaultOriginForCode(code),
519
+ appName
520
+ };
521
+ }
522
+
523
+ // src/utils/queueKey.ts
524
+ var LEDGER_DEFAULT_QUEUE_KEY = "__ledger_default__";
525
+ function ledgerQueueKey({
526
+ operationId,
527
+ connectId
528
+ }) {
529
+ return (operationId ?? connectId) || LEDGER_DEFAULT_QUEUE_KEY;
452
530
  }
453
531
 
454
532
  // src/adapter/methods/allNetworkGetAddress.ts
455
- import { HardwareErrorCode as HardwareErrorCode2, failure, runAllNetworkGetAddress } from "@onekeyfe/hwk-adapter-core";
533
+ import {
534
+ HardwareErrorCode as HardwareErrorCode2,
535
+ failure,
536
+ isConnectionLost,
537
+ isUserRefusal,
538
+ resolveHardwareOperationTarget,
539
+ runAllNetworkGetAddress
540
+ } from "@onekeyfe/hwk-adapter-core";
456
541
 
457
542
  // src/utils/sdkEventBus.ts
458
543
  var listeners = /* @__PURE__ */ new Set();
@@ -509,65 +594,103 @@ var LEDGER_BTC_NETWORK_COIN_MAP = {
509
594
  var LEDGER_UNSUPPORTED_ALLNETWORK_NETWORKS = /* @__PURE__ */ new Set(["doge", "dogecoin"]);
510
595
  function createAllNetworkGetAddress({
511
596
  callChain,
512
- getChainFingerprint
597
+ getChainFingerprint,
598
+ retainOperation,
599
+ errorToFailure,
600
+ createCancelScope
513
601
  }) {
514
602
  return async function allNetworkGetAddress(connectId, _deviceId, params) {
515
- debugLog("[LedgerAdapter][REQ]", { method: "allNetworkGetAddress", connectId, params });
603
+ debugLog("[LedgerAdapter][REQ]", {
604
+ method: "allNetworkGetAddress",
605
+ connectId,
606
+ itemCount: params.bundle.length
607
+ });
608
+ const target = resolveHardwareOperationTarget(connectId, params.operationId, "ledger");
609
+ if (!target.success) return target;
610
+ const effectiveTargetId = target.payload.targetId ?? "";
611
+ let releaseOperationRetention;
612
+ try {
613
+ releaseOperationRetention = target.payload.operationId ? retainOperation(target.payload.operationId) : void 0;
614
+ } catch (error) {
615
+ return errorToFailure(error);
616
+ }
617
+ const cancelScope = createCancelScope(
618
+ ledgerQueueKey({ operationId: target.payload.operationId, connectId: effectiveTargetId })
619
+ );
516
620
  const installContext = {};
517
621
  const commonParams = {
518
- autoInstallApp: params.autoInstallApp
622
+ autoInstallApp: params.autoInstallApp,
623
+ operationId: target.payload.operationId,
624
+ knownConnections: params.knownConnections,
625
+ extra: params.extra,
626
+ allowDeviceSelection: params.allowDeviceSelection,
627
+ supportedTransports: params.supportedTransports
519
628
  };
520
629
  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,
630
+ try {
631
+ const result = await runAllNetworkGetAddress({
632
+ connectId: effectiveTargetId,
633
+ deviceId: _deviceId,
634
+ params,
635
+ normalizeItem: normalizeLedgerAllNetworkItem,
636
+ buildUnsupportedNetworkResponse: (item) => isUnsupportedLedgerAllNetworkNetwork(item) ? buildUnsupportedNetworkResponse(item) : void 0,
637
+ callItem: async ({ method, chain, item }) => {
638
+ if (cancelScope.signal.aborted) return buildCancelledFailure(cancelScope.signal);
639
+ const itemDeviceId = getItemDeviceId(item) ?? chainFingerprints.get(chain) ?? "";
640
+ return callAllNetworkMethod(
641
+ callChain,
642
+ effectiveTargetId,
643
+ itemDeviceId,
644
+ method,
645
+ item,
646
+ commonParams,
647
+ installContext
648
+ );
649
+ },
650
+ attachIdentity: async ({ item, chain, payload }) => attachLedgerIdentity(
651
+ getChainFingerprint,
652
+ effectiveTargetId,
534
653
  item,
535
- commonParams,
536
- installContext
537
- );
538
- },
539
- attachIdentity: async ({ item, chain, payload }) => attachLedgerIdentity(
540
- getChainFingerprint,
541
- connectId,
542
- item,
543
- chain,
544
- payload,
545
- chainFingerprints
546
- ),
547
- shouldAbortBundle: isTopLevelAllNetworkFailure,
548
- buildTopLevelFailure: (response) => {
549
- const code = response.payload?.code ?? HardwareErrorCode2.DeviceMismatch;
550
- return failure(
551
- code,
552
- response.payload?.error ?? "All-network get-address aborted",
553
- response.payload?.params
554
- );
555
- }
556
- });
557
- debugLog("[LedgerAdapter][RES]", {
558
- method: "allNetworkGetAddress",
559
- success: result.success,
560
- payload: result
561
- });
562
- return result;
654
+ chain,
655
+ payload,
656
+ chainFingerprints,
657
+ installContext,
658
+ cancelScope.signal
659
+ ),
660
+ shouldAbortBundle: isTopLevelAllNetworkFailure,
661
+ buildTopLevelFailure: (response) => {
662
+ const code = response.payload?.code ?? HardwareErrorCode2.DeviceMismatch;
663
+ return failure(
664
+ code,
665
+ response.payload?.error ?? "All-network get-address aborted",
666
+ response.payload?.params
667
+ );
668
+ }
669
+ });
670
+ debugLog("[LedgerAdapter][RES]", {
671
+ method: "allNetworkGetAddress",
672
+ success: result.success,
673
+ payload: result
674
+ });
675
+ return result;
676
+ } finally {
677
+ cancelScope.release();
678
+ releaseOperationRetention?.();
679
+ }
563
680
  };
564
681
  }
682
+ function buildCancelledFailure(signal) {
683
+ const reason = signal.reason;
684
+ const code = typeof reason?.code === "number" ? reason.code : HardwareErrorCode2.UserAborted;
685
+ const message = typeof reason?.message === "string" ? reason.message : "";
686
+ return failure(code, message || "All-network get-address cancelled");
687
+ }
565
688
  function isTopLevelAllNetworkFailure(response) {
566
689
  if (response.success) {
567
690
  return false;
568
691
  }
569
692
  const code = response.payload?.code;
570
- return code === HardwareErrorCode2.DeviceMismatch || code === HardwareErrorCode2.UserAborted || code === HardwareErrorCode2.UserRejected;
693
+ return code === HardwareErrorCode2.DeviceMismatch || isUserRefusal(code) || isConnectionLost(code);
571
694
  }
572
695
  function getItemDeviceId(item) {
573
696
  const { deviceId } = item;
@@ -597,8 +720,18 @@ function normalizeLedgerAllNetworkItem(method, item) {
597
720
  const coin = LEDGER_BTC_NETWORK_COIN_MAP[item.network];
598
721
  return coin ? { ...item, coin } : item;
599
722
  }
600
- async function attachLedgerIdentity(getChainFingerprint, connectId, item, chain, payload, chainFingerprints) {
601
- const fingerprint = getItemDeviceId(item) || chainFingerprints.get(chain) || await bootstrapChainFingerprint(getChainFingerprint, connectId, chain);
723
+ async function attachLedgerIdentity(getChainFingerprint, connectId, item, chain, payload, chainFingerprints, context, cancelSignal) {
724
+ const knownFingerprint = getItemDeviceId(item) || chainFingerprints.get(chain) || "";
725
+ if (!knownFingerprint && cancelSignal.aborted) {
726
+ const cancelled = buildCancelledFailure(cancelSignal);
727
+ return { ...item, success: false, payload: cancelled.payload };
728
+ }
729
+ const fingerprint = knownFingerprint || await bootstrapChainFingerprint(
730
+ getChainFingerprint,
731
+ context.connection?.connectId ?? connectId,
732
+ chain,
733
+ context
734
+ );
602
735
  if (!fingerprint) {
603
736
  return buildFingerprintBootstrapFailure(item, chain);
604
737
  }
@@ -619,8 +752,8 @@ async function attachLedgerIdentity(getChainFingerprint, connectId, item, chain,
619
752
  }
620
753
  };
621
754
  }
622
- async function bootstrapChainFingerprint(getChainFingerprint, connectId, chain) {
623
- const response = await getChainFingerprint(connectId, chain);
755
+ async function bootstrapChainFingerprint(getChainFingerprint, connectId, chain, context) {
756
+ const response = await getChainFingerprint(connectId, chain, context);
624
757
  return response.success ? response.payload : "";
625
758
  }
626
759
  function buildFingerprintBootstrapFailure(item, chain) {
@@ -713,6 +846,16 @@ function isLedgerBleDescriptor(connectionType, descriptor) {
713
846
  function formatDeviceMismatchError(expected, actual) {
714
847
  return `Wrong device: expected ${expected}, got ${actual}`;
715
848
  }
849
+ var LOG_REDACTED_RESULT_KEYS = ["ufvk", "orchardFvk"];
850
+ function redactResultForLog(result) {
851
+ if (!result || typeof result !== "object") return result;
852
+ const keys = LOG_REDACTED_RESULT_KEYS.filter((key) => key in result);
853
+ if (!keys.length) return result;
854
+ return { ...result, ...Object.fromEntries(keys.map((key) => [key, "[redacted]"])) };
855
+ }
856
+ function isLostConnectionError(err) {
857
+ return isDeviceDisconnectedError(err) || isDeviceNotAdvertisingError(err) || isTimeoutError(err) || isConnectionLevelError(err);
858
+ }
716
859
  var BTC_HIGH_INDEX_THRESHOLD = 100;
717
860
  function btcAccountIndexFromPath(path) {
718
861
  const segments = path.replace(/^m\//, "").split("/");
@@ -724,9 +867,35 @@ function btcAccountIndexFromPath(path) {
724
867
  var _LedgerAdapter = class _LedgerAdapter {
725
868
  constructor(connector, options) {
726
869
  this.vendor = "ledger";
870
+ // Cancel frees the DMK action and its intent-queue slot, but cannot retract a
871
+ // confirmation screen the device is already showing.
872
+ this.cancelCapability = "stops-waiting";
727
873
  this.emitter = new TypedEventEmitter();
874
+ this._operations = new OperationRegistry({
875
+ vendor: "ledger",
876
+ onEnded: (operation, reason) => {
877
+ this._discardPendingBinding(this._pendingOperationBindings.get(operation.operationId));
878
+ this._pendingOperationBindings.delete(operation.operationId);
879
+ this.emitter.emit(SDK.OPERATION_ENDED, {
880
+ type: SDK.OPERATION_ENDED,
881
+ payload: { operationId: operation.operationId, reason }
882
+ });
883
+ if (reason === "timeout") {
884
+ void this._releaseOperationConnection(operation);
885
+ }
886
+ }
887
+ });
728
888
  this._discoveredDevices = /* @__PURE__ */ new Map();
729
889
  this._sessions = /* @__PURE__ */ new Map();
890
+ // An operation's BLE pick is saved by its first call that gets past the wallet check.
891
+ this._pendingOperationBindings = /* @__PURE__ */ new Map();
892
+ // A one-shot call's unchecked BLE pick, by connectId. It outlives the call only
893
+ // through an unlock retry; any other failure drops it with its session.
894
+ this._pendingPickBindings = /* @__PURE__ */ new Map();
895
+ // Transport each session was opened on, by sessionId. The adapter-wide
896
+ // `_activeConnectionType` moves with every discovery and can go stale.
897
+ this._sessionTransports = /* @__PURE__ */ new Map();
898
+ this._selectedBleReconnectTargets = /* @__PURE__ */ new Map();
730
899
  this._uiRegistry = new UiRequestRegistry();
731
900
  // BTC App rejects account index >= 100 unless display=true. Cached per
732
901
  // adapter instance: first 100+ path asks the user once via UI request,
@@ -740,10 +909,31 @@ var _LedgerAdapter = class _LedgerAdapter {
740
909
  this._deviceAuthenticityQueueTail = Promise.resolve();
741
910
  // Shared across concurrent callers — only `cancel()` aborts.
742
911
  this._doConnectAbortController = null;
912
+ this._unsettledConnectorOperations = /* @__PURE__ */ new Map();
913
+ this._connectorIdleWaiters = /* @__PURE__ */ new Set();
914
+ this._resetPromise = null;
915
+ this._stateGeneration = 0;
916
+ this._connectorTeardownTail = Promise.resolve();
917
+ this._pendingConnectorTeardowns = 0;
918
+ this._activeOperationJobs = /* @__PURE__ */ new Set();
919
+ this._pendingOperationDisconnects = /* @__PURE__ */ new Set();
743
920
  this._installProgressLastEmittedValue = -Infinity;
744
921
  this.allNetworkGetAddress = createAllNetworkGetAddress({
745
922
  callChain: this.callChain.bind(this),
746
- getChainFingerprint: (connectId, chain) => this.getChainFingerprint(connectId, "", chain)
923
+ getChainFingerprint: async (connectId, chain, context) => {
924
+ try {
925
+ const fingerprint = await this._computeChainFingerprint(
926
+ chain,
927
+ (method, params) => this.connectorCall(connectId, method, params, void 0, void 0, void 0, context)
928
+ );
929
+ return success(fingerprint);
930
+ } catch (error) {
931
+ return this.errorToFailure(error);
932
+ }
933
+ },
934
+ retainOperation: (operationId) => this._operations.retain(operationId),
935
+ errorToFailure: (error) => this.errorToFailure(error),
936
+ createCancelScope: (queueKey) => this._jobQueue.createCancelScope(queueKey)
747
937
  });
748
938
  // ---------------------------------------------------------------------------
749
939
  // Private helpers
@@ -753,7 +943,7 @@ var _LedgerAdapter = class _LedgerAdapter {
753
943
  *
754
944
  * - If a session already exists for the given connectId, reuse it.
755
945
  * - If ANY session exists (Ledger IDs are ephemeral), reuse it.
756
- * - Otherwise: search → exactly 1 USB device auto-connects; multiple or none throws.
946
+ * - Otherwise: search → one USB device auto-connects; multiple asks the host to choose.
757
947
  */
758
948
  // Mutex for ensureConnected — prevents concurrent calls from establishing duplicate connections
759
949
  this._connectingPromise = null;
@@ -770,6 +960,12 @@ var _LedgerAdapter = class _LedgerAdapter {
770
960
  });
771
961
  };
772
962
  this.deviceDisconnectHandler = (data) => {
963
+ const activeOperation = this._operations.findActiveByConnectionKey(data.connectId);
964
+ if (activeOperation && this._activeOperationJobs.has(activeOperation.operationId)) {
965
+ this._pendingOperationDisconnects.add(activeOperation.operationId);
966
+ } else {
967
+ this._operations.endByConnectionKey(data.connectId, "disconnect");
968
+ }
773
969
  this._discoveredDevices.delete(data.connectId);
774
970
  this._sessions.delete(data.connectId);
775
971
  this.emitter.emit(DEVICE.DISCONNECT, {
@@ -778,10 +974,8 @@ var _LedgerAdapter = class _LedgerAdapter {
778
974
  });
779
975
  };
780
976
  // Forward connector `ui-event` to the public hw.emitter so consumers only
781
- // need to subscribe in one place. For the AppInstallProgress variant we
782
- // re-key sessionId → connectId via the live _sessions map; if no mapping
783
- // exists (race during teardown) we drop. All other variants pass through
784
- // unchanged.
977
+ // need to subscribe in one place. The AppInstallProgress variant re-keys
978
+ // sessionId to connectId via `_sessions`, and drops if no mapping exists.
785
979
  this.uiEventForwarder = (event) => {
786
980
  if (event.type === EConnectorInteraction.AppInstallProgress) {
787
981
  let connectId;
@@ -826,11 +1020,34 @@ var _LedgerAdapter = class _LedgerAdapter {
826
1020
  this._jobQueue = new DeviceJobQueue();
827
1021
  this.registerEventListeners();
828
1022
  }
1023
+ _discardPendingBinding(owner, status = "cancelled") {
1024
+ const pending = owner?.pendingBinding;
1025
+ if (!owner || !pending) return;
1026
+ owner.pendingBinding = void 0;
1027
+ if (pending.requestId === this._bindingSelectionRequestId) this._finishBleBinding(status);
1028
+ }
1029
+ _finishBleBinding(status) {
1030
+ const selectionRequestId = this._bindingSelectionRequestId;
1031
+ this._bindingSelectionRequestId = void 0;
1032
+ if (selectionRequestId)
1033
+ this.emitter.emit(UI_REQUEST.DEVICE_BINDING_STATUS, {
1034
+ type: UI_REQUEST.DEVICE_BINDING_STATUS,
1035
+ payload: { selectionRequestId, status }
1036
+ });
1037
+ }
1038
+ _isBleConnection() {
1039
+ return isLedgerBleConnectionType(this._activeConnectionType ?? this.connector.connectionType);
1040
+ }
829
1041
  // Transport
830
1042
  get activeTransport() {
831
- return isLedgerBleConnectionType(this.connector.connectionType) ? "ble" : "hid";
1043
+ return this._isBleConnection() ? "ble" : "hid";
832
1044
  }
833
1045
  getAvailableTransports() {
1046
+ if (this.connector.availableTransports) {
1047
+ return this.connector.availableTransports.map(
1048
+ (transport) => transport === "ble" ? "ble" : "hid"
1049
+ );
1050
+ }
834
1051
  return this.activeTransport ? [this.activeTransport] : [];
835
1052
  }
836
1053
  // Connector is bound at construction; switching requires a new adapter.
@@ -847,21 +1064,42 @@ var _LedgerAdapter = class _LedgerAdapter {
847
1064
  * The next operation will re-discover and re-connect automatically.
848
1065
  */
849
1066
  resetState() {
1067
+ void this._resetStateAndDisconnectSessions();
1068
+ }
1069
+ _resetStateAndDisconnectSessions() {
1070
+ if (this._resetPromise) return this._resetPromise;
1071
+ const sessionIds = new Set(this._sessions.values());
1072
+ this._stateGeneration += 1;
1073
+ this._finishBleBinding("cancelled");
1074
+ this._operations.endAll("runtime-reset");
1075
+ this._doConnectAbortController?.abort();
850
1076
  this._discoveredDevices.clear();
851
1077
  this._sessions.clear();
1078
+ this._sessionTransports.clear();
1079
+ this._pendingOperationBindings.clear();
1080
+ this._pendingPickBindings.clear();
1081
+ this._selectedBleReconnectTargets.clear();
852
1082
  this._connectingPromise = null;
1083
+ this._doConnectAbortController = null;
853
1084
  this._uiRegistry.reset();
854
1085
  this._jobQueue.clear();
855
1086
  this._btcHighIndexConfirmedThisSession = false;
1087
+ const resetPromise = this._runConnectorTeardown(async () => {
1088
+ for (const sessionId of sessionIds) {
1089
+ await this.connector.disconnect(sessionId).catch(() => void 0);
1090
+ }
1091
+ });
1092
+ this._resetPromise = resetPromise;
1093
+ return resetPromise.finally(() => {
1094
+ if (this._resetPromise === resetPromise) {
1095
+ this._resetPromise = null;
1096
+ }
1097
+ });
856
1098
  }
857
1099
  async dispose() {
858
- this._uiRegistry.reset();
859
- this._jobQueue.clear();
1100
+ await this._resetStateAndDisconnectSessions();
860
1101
  this.unregisterEventListeners();
861
1102
  this.connector.reset();
862
- this._discoveredDevices.clear();
863
- this._sessions.clear();
864
- this._btcHighIndexConfirmedThisSession = false;
865
1103
  this.emitter.removeAllListeners();
866
1104
  }
867
1105
  uiResponse(response) {
@@ -871,17 +1109,32 @@ var _LedgerAdapter = class _LedgerAdapter {
871
1109
  // Device management
872
1110
  // ---------------------------------------------------------------------------
873
1111
  async searchDevices(options) {
1112
+ return this._searchDevices(options);
1113
+ }
1114
+ async _searchDevices(options, signal) {
874
1115
  debugLog("[LedgerAdapter][REQ]", { method: "searchDevices", params: options });
875
1116
  try {
876
1117
  if (options?.resetSession) {
877
- this._doConnectAbortController?.abort();
878
- this._sessions.clear();
879
- this._connectingPromise = null;
880
- this._doConnectAbortController = null;
881
- this._btcHighIndexConfirmedThisSession = false;
1118
+ await this._resetStateAndDisconnectSessions();
1119
+ } else {
1120
+ await this._connectorTeardownTail;
1121
+ }
1122
+ await this._ensureDevicePermission(void 0, void 0, signal);
1123
+ if (signal) _LedgerAdapter._throwIfAborted(signal);
1124
+ const stateGeneration = this._stateGeneration;
1125
+ const devices = await this.connector.searchDevices(
1126
+ options?.transportType ? {
1127
+ transportType: options.transportType,
1128
+ waitForAll: options.waitForAllTransports
1129
+ } : void 0
1130
+ );
1131
+ if (signal) _LedgerAdapter._throwIfAborted(signal);
1132
+ if (stateGeneration !== this._stateGeneration) {
1133
+ throw createHwkError({
1134
+ code: HardwareErrorCode3.UserAborted,
1135
+ message: "Ledger discovery was reset"
1136
+ });
882
1137
  }
883
- await this._ensureDevicePermission();
884
- const devices = await this.connector.searchDevices();
885
1138
  this._discoveredDevices.clear();
886
1139
  for (const d of devices) {
887
1140
  if (d.connectId) {
@@ -889,7 +1142,7 @@ var _LedgerAdapter = class _LedgerAdapter {
889
1142
  }
890
1143
  }
891
1144
  if (this._discoveredDevices.size === 0) {
892
- await this._ensureDevicePermission();
1145
+ await this._ensureDevicePermission(void 0, void 0, signal);
893
1146
  }
894
1147
  const result = Array.from(this._discoveredDevices.values());
895
1148
  debugLog("[LedgerAdapter][RES]", {
@@ -908,41 +1161,280 @@ var _LedgerAdapter = class _LedgerAdapter {
908
1161
  throw err;
909
1162
  }
910
1163
  }
1164
+ async searchDeviceTargets(options) {
1165
+ const devices = await this.searchDevices(options);
1166
+ return devices.map((device) => ({
1167
+ searchTargetId: device.connectId,
1168
+ searchTargetReusePolicy: resolveSearchTargetReusePolicy(device),
1169
+ vendor: "ledger",
1170
+ connectionType: device.connectionType,
1171
+ kind: "physical",
1172
+ label: device.label,
1173
+ model: device.model,
1174
+ modelName: device.modelName,
1175
+ serialNumber: device.serialNumber
1176
+ }));
1177
+ }
1178
+ async listConnectionTargets(options) {
1179
+ const targets = await this.searchDeviceTargets(options);
1180
+ return targets.map(({ searchTargetId, ...target }) => ({
1181
+ ...target,
1182
+ targetId: searchTargetId
1183
+ }));
1184
+ }
911
1185
  // USB single-session invariant: evict all sessions, best-effort (see connectDevice).
912
- async _evictAllSessions() {
1186
+ async _evictAllSessions(preserveOperationId) {
1187
+ this._operations.endAll("explicit", preserveOperationId);
913
1188
  if (this._sessions.size === 0) return;
914
1189
  const stale = [...this._sessions.values()];
915
1190
  this._sessions.clear();
916
- for (const sid of stale) {
917
- try {
918
- await this.connector.disconnect(sid);
919
- } catch {
1191
+ for (const sid of stale) this._sessionTransports.delete(sid);
1192
+ await this._runConnectorTeardown(async () => {
1193
+ for (const sid of stale) {
1194
+ try {
1195
+ await this.connector.disconnect(sid);
1196
+ } catch {
1197
+ }
920
1198
  }
921
- }
1199
+ });
922
1200
  }
923
1201
  static _createDeviceBusyError(method) {
924
1202
  return Object.assign(new Error(`Ledger device is busy while calling ${method}`), {
925
1203
  code: HardwareErrorCode3.DeviceBusy
926
1204
  });
927
1205
  }
928
- async connectDevice(connectId) {
1206
+ async connectDevice(searchTargetId) {
1207
+ try {
1208
+ return await this._jobQueue.enqueue(
1209
+ searchTargetId || "__ledger_connect__",
1210
+ async (signal) => {
1211
+ const connected = await this._connectTarget(searchTargetId, void 0, signal);
1212
+ if (!connected.success) return connected;
1213
+ return this._createOperation(searchTargetId, connected.payload);
1214
+ },
1215
+ {
1216
+ label: "connectDevice",
1217
+ rejectIfBusy: true,
1218
+ busyError: _LedgerAdapter._createDeviceBusyError("connectDevice")
1219
+ }
1220
+ );
1221
+ } catch (error) {
1222
+ return this.errorToFailure(error);
1223
+ }
1224
+ }
1225
+ /** Binds the picked BLE device; with a recorded wallet identity it must match first. */
1226
+ async bindBleDevice(params) {
1227
+ const identity = params.identity?.vendor === "ledger" ? params.identity : void 0;
1228
+ try {
1229
+ return await this._jobQueue.enqueue(
1230
+ "__ledger_bind_ble__",
1231
+ async (signal) => {
1232
+ if (!this.getAvailableTransports().includes("ble")) {
1233
+ throw createHwkError({
1234
+ code: HardwareErrorCode3.TransportNotAvailable,
1235
+ message: "Ledger Bluetooth transport is not available"
1236
+ });
1237
+ }
1238
+ this._activeConnectionType = "ble";
1239
+ await this._ensureDevicePermission(void 0, void 0, signal);
1240
+ const attempt = {
1241
+ extra: params.extra,
1242
+ bindingReason: "manual-rebind",
1243
+ deferBindingSave: Boolean(identity)
1244
+ };
1245
+ try {
1246
+ const connectId = await this._connectFirstOrSelect(
1247
+ [],
1248
+ void 0,
1249
+ true,
1250
+ void 0,
1251
+ attempt,
1252
+ signal
1253
+ );
1254
+ const pending = attempt.pendingBinding;
1255
+ if (identity && pending) {
1256
+ attempt.pendingBinding = void 0;
1257
+ const sessionId = this._sessions.get(connectId);
1258
+ if (!sessionId) {
1259
+ throw createHwkError({
1260
+ code: HardwareErrorCode3.DeviceDisconnected,
1261
+ message: "Selected Ledger connection ended"
1262
+ });
1263
+ }
1264
+ const fingerprint = await this._computeChainFingerprint(
1265
+ identity.chain,
1266
+ (method, callParams) => this._runConnectorCall(
1267
+ connectId,
1268
+ method,
1269
+ callParams,
1270
+ signal,
1271
+ void 0,
1272
+ void 0,
1273
+ { autoInstallApp: true },
1274
+ { connection: { connectId, sessionId } }
1275
+ )
1276
+ );
1277
+ if (fingerprint !== identity.value) {
1278
+ await this._dropSession(connectId, signal);
1279
+ throw createHwkError({
1280
+ code: HardwareErrorCode3.DeviceMismatch,
1281
+ message: formatDeviceMismatchError(identity.value, fingerprint)
1282
+ });
1283
+ }
1284
+ await this._saveSelectedBleBinding(
1285
+ connectId,
1286
+ pending.requestId,
1287
+ attempt,
1288
+ void 0,
1289
+ signal,
1290
+ identity
1291
+ );
1292
+ }
1293
+ if (!attempt.bindingSaved) {
1294
+ await this._dropSession(connectId, signal);
1295
+ throw createHwkError({
1296
+ code: HardwareErrorCode3.UnknownError,
1297
+ message: "Bluetooth binding could not be saved",
1298
+ origin: "host"
1299
+ });
1300
+ }
1301
+ return success(connectId);
1302
+ } catch (error) {
1303
+ this._finishBleBinding(signal.aborted ? "cancelled" : "failed");
1304
+ throw error;
1305
+ }
1306
+ },
1307
+ {
1308
+ label: "bindBleDevice",
1309
+ rejectIfBusy: true,
1310
+ busyError: _LedgerAdapter._createDeviceBusyError("bindBleDevice")
1311
+ }
1312
+ );
1313
+ } catch (error) {
1314
+ return this.errorToFailure(error);
1315
+ }
1316
+ }
1317
+ async acquireOperation(connectId, context) {
1318
+ try {
1319
+ return await this._jobQueue.enqueue(
1320
+ connectId || "__ledger_acquire__",
1321
+ async (signal) => {
1322
+ const transport = this._isBleConnection() ? "ble" : "usb";
1323
+ const hint = context.knownConnections?.find(
1324
+ (connection) => connection.transport === transport
1325
+ );
1326
+ const target = hint && hint.transport !== "qr" ? hint.connectId : connectId;
1327
+ await this._ensureDevicePermission(target, void 0, signal);
1328
+ const attempt = {
1329
+ ...context,
1330
+ extra: context.extra ? { ...context.extra } : void 0,
1331
+ // The operation's calls decide whether the pick is the right wallet.
1332
+ deferBindingSave: true
1333
+ };
1334
+ try {
1335
+ const resolvedConnectId = await this.ensureConnected(
1336
+ target,
1337
+ signal,
1338
+ true,
1339
+ void 0,
1340
+ attempt
1341
+ );
1342
+ _LedgerAdapter._throwIfAborted(signal);
1343
+ const result = this._createOperation(connectId, resolvedConnectId);
1344
+ if (result.success && attempt.pendingBinding) {
1345
+ this._pendingOperationBindings.set(result.payload, attempt);
1346
+ }
1347
+ return result;
1348
+ } catch (error) {
1349
+ this._finishBleBinding(signal.aborted ? "cancelled" : "failed");
1350
+ throw error;
1351
+ }
1352
+ },
1353
+ {
1354
+ label: "acquireOperation",
1355
+ rejectIfBusy: true,
1356
+ busyError: _LedgerAdapter._createDeviceBusyError("acquireOperation")
1357
+ }
1358
+ );
1359
+ } catch (error) {
1360
+ return this.errorToFailure(error);
1361
+ }
1362
+ }
1363
+ _createOperation(searchTargetId, resolvedConnectId) {
1364
+ this._operations.endByConnectionKey(resolvedConnectId, "explicit");
1365
+ const sessionId = this._sessions.get(resolvedConnectId);
1366
+ if (sessionId) this._operations.endByConnectionKey(sessionId, "explicit");
1367
+ const connectionType = sessionId && this._sessionTransports.get(sessionId) || (this._isBleConnection() ? "ble" : "usb");
1368
+ const device = this._discoveredDevices.get(resolvedConnectId) ?? {
1369
+ vendor: "ledger",
1370
+ model: "unknown",
1371
+ firmwareVersion: "",
1372
+ deviceId: "",
1373
+ connectId: resolvedConnectId,
1374
+ connectionType
1375
+ };
1376
+ const operation = this._operations.create({
1377
+ searchTargetId,
1378
+ connectId: resolvedConnectId,
1379
+ device,
1380
+ connectionType,
1381
+ connectionKeys: [this._sessions.get(resolvedConnectId) ?? ""]
1382
+ });
1383
+ return success(operation.operationId);
1384
+ }
1385
+ async _connectTarget(connectId, preserveOperationId, signal) {
929
1386
  debugLog("[LedgerAdapter][REQ]", { method: "connectDevice", connectId, params: { connectId } });
930
1387
  try {
931
- if (isLedgerBleConnectionType(this.connector.connectionType) && !connectId) {
1388
+ this._assertConnectorReady("connectDevice");
1389
+ const discoveredType = this._discoveredDevices.get(connectId)?.connectionType;
1390
+ if (discoveredType === "usb" || discoveredType === "ble") {
1391
+ this._activeConnectionType = discoveredType;
1392
+ }
1393
+ if (this._isBleConnection() && !connectId) {
932
1394
  throw Object.assign(new Error("Ledger BLE connectId is required."), {
933
1395
  code: HardwareErrorCode3.DeviceNotFound
934
1396
  });
935
1397
  }
936
- if (!isLedgerBleConnectionType(this.connector.connectionType)) {
937
- await this._evictAllSessions();
1398
+ await this._ensureDevicePermission(connectId, void 0, signal);
1399
+ if (signal) _LedgerAdapter._throwIfAborted(signal);
1400
+ const isMultiTransport = (this.connector.availableTransports?.length ?? 0) > 1;
1401
+ if (!isMultiTransport && this._isBleConnection()) {
1402
+ const previousSessionId = this._sessions.get(connectId);
1403
+ this._operations.endByConnectionKey(connectId, "explicit", preserveOperationId);
1404
+ if (previousSessionId) {
1405
+ this._operations.endByConnectionKey(previousSessionId, "explicit", preserveOperationId);
1406
+ this._sessions.delete(connectId);
1407
+ await this.connector.disconnect(previousSessionId).catch(() => void 0);
1408
+ }
1409
+ } else {
1410
+ await this._evictAllSessions(preserveOperationId);
1411
+ }
1412
+ if (signal) _LedgerAdapter._throwIfAborted(signal);
1413
+ const stateGeneration = this._stateGeneration;
1414
+ const releaseOperation = this._retainConnectorOperation(`connect:${connectId}`);
1415
+ let session;
1416
+ this._connectingConnectId = connectId;
1417
+ try {
1418
+ session = this.connector.availableTransports?.length ? await this.connector.connect(connectId, {
1419
+ transportType: this._activeConnectionType ?? "usb"
1420
+ }) : await this.connector.connect(connectId);
1421
+ if (signal?.aborted || stateGeneration !== this._stateGeneration) {
1422
+ await this.connector.disconnect(session.sessionId).catch(() => void 0);
1423
+ throw Object.assign(new Error("Ledger connection aborted"), {
1424
+ code: HardwareErrorCode3.UserAborted
1425
+ });
1426
+ }
1427
+ } finally {
1428
+ if (this._connectingConnectId === connectId) this._connectingConnectId = void 0;
1429
+ releaseOperation();
938
1430
  }
939
- await this._ensureDevicePermission(connectId);
940
- const session = await this.connector.connect(connectId);
941
- this._sessions.set(connectId, session.sessionId);
1431
+ const resolvedConnectId = session.deviceInfo?.connectId || connectId;
1432
+ this._sessions.set(resolvedConnectId, session.sessionId);
1433
+ this._sessionTransports.set(session.sessionId, this._isBleConnection() ? "ble" : "usb");
942
1434
  if (session.deviceInfo) {
943
- this._discoveredDevices.set(connectId, session.deviceInfo);
1435
+ this._discoveredDevices.set(resolvedConnectId, session.deviceInfo);
944
1436
  }
945
- const result = success(connectId);
1437
+ const result = success(resolvedConnectId);
946
1438
  debugLog("[LedgerAdapter][RES]", { method: "connectDevice", success: true, payload: result });
947
1439
  return result;
948
1440
  } catch (err) {
@@ -955,30 +1447,60 @@ var _LedgerAdapter = class _LedgerAdapter {
955
1447
  return failureResult;
956
1448
  }
957
1449
  }
958
- async disconnectDevice(connectId) {
1450
+ async releaseOperation(operationId) {
1451
+ const operation = this._operations.find(operationId);
1452
+ if (!operation) {
1453
+ this._operations.resolve(operationId);
1454
+ return;
1455
+ }
1456
+ const endedOperation = this._operations.end(operationId, "explicit");
1457
+ if (!endedOperation) return;
1458
+ const { connectId } = operation;
959
1459
  debugLog("[LedgerAdapter][REQ]", {
960
- method: "disconnectDevice",
1460
+ method: "releaseOperation",
961
1461
  connectId,
962
1462
  params: { connectId }
963
1463
  });
964
1464
  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 });
1465
+ await this._releaseOperationConnection(endedOperation);
1466
+ debugLog("[LedgerAdapter][RES]", { method: "releaseOperation", success: true });
971
1467
  } catch (err) {
972
1468
  const e = err;
973
1469
  debugLog("[LedgerAdapter][RES]", {
974
- method: "disconnectDevice",
1470
+ method: "releaseOperation",
975
1471
  success: false,
976
1472
  error: { message: e?.message, _tag: e?._tag, code: e?.code ?? e?.errorCode }
977
1473
  });
978
1474
  throw err;
979
1475
  }
980
1476
  }
981
- async getDeviceInfo(connectId, deviceId) {
1477
+ async _releaseOperationConnection(operation) {
1478
+ const sessionIds = /* @__PURE__ */ new Set();
1479
+ for (const [connectId, sessionId] of this._sessions) {
1480
+ if (connectId === operation.connectId || operation.connectionKeys.includes(connectId) || operation.connectionKeys.includes(sessionId)) {
1481
+ this._sessions.delete(connectId);
1482
+ sessionIds.add(sessionId);
1483
+ }
1484
+ }
1485
+ for (const sessionId of sessionIds) {
1486
+ await this._runConnectorTeardown(
1487
+ () => this.connector.disconnect(sessionId).catch(() => void 0)
1488
+ );
1489
+ }
1490
+ }
1491
+ async _releaseLostOperationConnection(operationId) {
1492
+ const endedOperation = this._operations.end(operationId, "disconnect");
1493
+ if (!endedOperation) return;
1494
+ this._discoveredDevices.delete(endedOperation.connectId);
1495
+ await this._releaseOperationConnection(endedOperation);
1496
+ }
1497
+ async getDeviceInfo(connectIdOrOperationId, deviceId) {
1498
+ let connectId;
1499
+ try {
1500
+ connectId = isHardwareOperationId(connectIdOrOperationId) ? this._operations.resolve(connectIdOrOperationId).connectId : connectIdOrOperationId;
1501
+ } catch (error) {
1502
+ return this.errorToFailure(error);
1503
+ }
982
1504
  debugLog("[LedgerAdapter][REQ]", {
983
1505
  method: "getDeviceInfo",
984
1506
  connectId,
@@ -1013,12 +1535,9 @@ var _LedgerAdapter = class _LedgerAdapter {
1013
1535
  success: false,
1014
1536
  error: { message: e?.message, _tag: e?._tag, code: e?.code ?? e?.errorCode }
1015
1537
  });
1016
- throw err;
1538
+ return this.errorToFailure(err);
1017
1539
  }
1018
1540
  }
1019
- getSupportedChains() {
1020
- return ["evm", "btc", "sol", "tron"];
1021
- }
1022
1541
  // ---------------------------------------------------------------------------
1023
1542
  // Chain call helper
1024
1543
  // ---------------------------------------------------------------------------
@@ -1053,13 +1572,23 @@ var _LedgerAdapter = class _LedgerAdapter {
1053
1572
  if (params && typeof params === "object") {
1054
1573
  const {
1055
1574
  autoInstallApp,
1575
+ operationId,
1056
1576
  passphraseState: _passphraseState,
1057
1577
  useEmptyPassphrase: _useEmptyPassphrase,
1578
+ knownConnections,
1579
+ extra,
1580
+ allowDeviceSelection,
1581
+ supportedTransports,
1058
1582
  ...rest
1059
1583
  } = params;
1060
1584
  return {
1061
1585
  commonParams: {
1062
- autoInstallApp: typeof autoInstallApp === "boolean" ? autoInstallApp : void 0
1586
+ autoInstallApp: typeof autoInstallApp === "boolean" ? autoInstallApp : void 0,
1587
+ operationId: typeof operationId === "string" ? operationId : void 0,
1588
+ knownConnections,
1589
+ extra,
1590
+ allowDeviceSelection: typeof allowDeviceSelection === "boolean" ? allowDeviceSelection : void 0,
1591
+ supportedTransports
1063
1592
  },
1064
1593
  rest
1065
1594
  };
@@ -1235,9 +1764,28 @@ var _LedgerAdapter = class _LedgerAdapter {
1235
1764
  );
1236
1765
  }
1237
1766
  // ---------------------------------------------------------------------------
1238
- // App management — OS-level Ledger app install / list. Bypasses fingerprint
1239
- // and chain-handler dispatch; installApp progress is forwarded to the adapter
1240
- // emitter via 'ui-event' AppInstallProgress events.
1767
+ // Zcash chain methods (viewing key + shielded address reads; Zcash app >= 3.8.0)
1768
+ // ---------------------------------------------------------------------------
1769
+ zcashGetFullViewingKey(connectId, deviceId, params) {
1770
+ return this.callChainWithMergedParams(
1771
+ connectId,
1772
+ deviceId,
1773
+ "zcash",
1774
+ "zcashGetFullViewingKey",
1775
+ params
1776
+ );
1777
+ }
1778
+ zcashGetShieldedAddress(connectId, deviceId, params) {
1779
+ return this.callChainWithMergedParams(
1780
+ connectId,
1781
+ deviceId,
1782
+ "zcash",
1783
+ "zcashGetShieldedAddress",
1784
+ params
1785
+ );
1786
+ }
1787
+ // ---------------------------------------------------------------------------
1788
+ // App management: OS-level app install/list, no fingerprint or chain dispatch.
1241
1789
  // ---------------------------------------------------------------------------
1242
1790
  async installApp(connectId, appName) {
1243
1791
  try {
@@ -1323,7 +1871,7 @@ var _LedgerAdapter = class _LedgerAdapter {
1323
1871
  );
1324
1872
  }
1325
1873
  await this.connector.configure({ ledgerGenuineCheckWebSocketUrl: relayUrl });
1326
- this.resetState();
1874
+ await this._resetStateAndDisconnectSessions();
1327
1875
  }
1328
1876
  const result = await this.connectorCall(connectId, "getDeviceGenuineCheck", {});
1329
1877
  if (!result.isGenuine) {
@@ -1349,10 +1897,10 @@ var _LedgerAdapter = class _LedgerAdapter {
1349
1897
  if (relayUrl) {
1350
1898
  try {
1351
1899
  await this.connector.configure?.({ ledgerGenuineCheckWebSocketUrl: void 0 });
1352
- this.resetState();
1900
+ await this._resetStateAndDisconnectSessions();
1353
1901
  } catch {
1354
1902
  this.connector.reset();
1355
- this.resetState();
1903
+ await this._resetStateAndDisconnectSessions();
1356
1904
  }
1357
1905
  }
1358
1906
  }
@@ -1364,6 +1912,20 @@ var _LedgerAdapter = class _LedgerAdapter {
1364
1912
  this.emitter.off(event, listener);
1365
1913
  }
1366
1914
  cancel(connectId) {
1915
+ const namedOperationIsLive = (id) => {
1916
+ try {
1917
+ this._operations.resolve(id);
1918
+ return true;
1919
+ } catch {
1920
+ return false;
1921
+ }
1922
+ };
1923
+ if (isHardwareOperationId(connectId) && !namedOperationIsLive(connectId)) {
1924
+ debugLog("[LedgerAdapter] cancel target already ended; nothing to cancel", {
1925
+ connectId
1926
+ });
1927
+ return;
1928
+ }
1367
1929
  const userAbortReason = Object.assign(new Error("User aborted operation"), {
1368
1930
  code: HardwareErrorCode3.UserAborted,
1369
1931
  _tag: ERROR_TAG.UserAborted
@@ -1374,14 +1936,48 @@ var _LedgerAdapter = class _LedgerAdapter {
1374
1936
  this._lastCancelReason = void 0;
1375
1937
  }
1376
1938
  }, 2e3);
1377
- this._uiRegistry.cancel();
1378
- if (connectId) {
1379
- this._jobQueue.cancelActiveAndPending(connectId, userAbortReason);
1939
+ const activeJobId = this._jobQueue.getActiveJob()?.deviceId;
1940
+ let operationId;
1941
+ if (isHardwareOperationId(connectId)) {
1942
+ operationId = connectId;
1943
+ } else if (!connectId && isHardwareOperationId(activeJobId)) {
1944
+ operationId = activeJobId;
1945
+ }
1946
+ let resolvedConnectId = connectId;
1947
+ if (operationId) {
1948
+ try {
1949
+ resolvedConnectId = this._operations.resolve(operationId).connectId;
1950
+ } catch {
1951
+ resolvedConnectId = void 0;
1952
+ }
1953
+ }
1954
+ const interactionForPhysicalId = !operationId && connectId ? this._operations.findActiveByConnectionKey(connectId) : void 0;
1955
+ const pendingOperationId = operationId ?? interactionForPhysicalId?.operationId;
1956
+ if (!connectId) {
1957
+ this._uiRegistry.cancel();
1958
+ } else if (pendingOperationId) {
1959
+ this._uiRegistry.cancel(void 0, void 0, pendingOperationId);
1960
+ }
1961
+ this._finishBleBinding("cancelled");
1962
+ if (!connectId) this._pendingOperationBindings.clear();
1963
+ else if (pendingOperationId) this._pendingOperationBindings.delete(pendingOperationId);
1964
+ const queueKeys = /* @__PURE__ */ new Set();
1965
+ if (connectId) queueKeys.add(ledgerQueueKey({ connectId }));
1966
+ if (pendingOperationId) queueKeys.add(ledgerQueueKey({ operationId: pendingOperationId }));
1967
+ if (queueKeys.size) {
1968
+ let cancelledAnyJob = false;
1969
+ for (const key of queueKeys) {
1970
+ cancelledAnyJob = this._jobQueue.cancelActiveAndPending(key, userAbortReason) || cancelledAnyJob;
1971
+ }
1972
+ debugLog("[LedgerAdapter] cancel routed to queue keys", {
1973
+ queueKeys: [...queueKeys],
1974
+ cancelledAnyJob
1975
+ });
1380
1976
  } else {
1381
1977
  this._jobQueue.cancelActiveAndPending(void 0, userAbortReason);
1382
1978
  }
1383
- if (connectId) {
1384
- const sessionId = this._sessions.get(connectId) ?? connectId;
1979
+ if (resolvedConnectId) {
1980
+ const sessionId = this._sessions.get(resolvedConnectId) ?? resolvedConnectId;
1385
1981
  void this.connector.cancel(sessionId);
1386
1982
  } else {
1387
1983
  for (const sid of this._sessions.values()) void this.connector.cancel(sid);
@@ -1389,45 +1985,86 @@ var _LedgerAdapter = class _LedgerAdapter {
1389
1985
  if (this._connectingPromise) {
1390
1986
  this._doConnectAbortController?.abort(userAbortReason);
1391
1987
  }
1988
+ const connectingConnectId = this._connectingConnectId;
1989
+ if (connectingConnectId) void this.connector.cancel(connectingConnectId);
1392
1990
  }
1393
1991
  // ---------------------------------------------------------------------------
1394
1992
  // Chain fingerprint
1395
1993
  // ---------------------------------------------------------------------------
1994
+ /** A non-empty deviceId is an expected chain fingerprint, not a transport identifier. */
1396
1995
  async getChainFingerprint(connectId, deviceId, chain) {
1397
1996
  try {
1398
1997
  const fingerprint = await this._computeChainFingerprint(
1399
1998
  chain,
1400
1999
  (method, params) => this.connectorCall(connectId, method, params, void 0, deviceId)
1401
2000
  );
2001
+ if (deviceId) {
2002
+ if (fingerprint !== deviceId) {
2003
+ return failure2(
2004
+ HardwareErrorCode3.DeviceMismatch,
2005
+ formatDeviceMismatchError(deviceId, fingerprint)
2006
+ );
2007
+ }
2008
+ }
1402
2009
  return success(fingerprint);
1403
2010
  } catch (err) {
1404
2011
  debugError("[LedgerAdapter] getChainFingerprint error:", chain, err);
1405
2012
  return this.errorToFailure(err);
1406
2013
  }
1407
2014
  }
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 };
2015
+ /** Saves a picked BLE endpoint; `identity` is set when its wallet was just checked. */
2016
+ async _saveSelectedBleBinding(connectId, selectionRequestId, context, operationId, signal, identity) {
1414
2017
  try {
1415
- const fingerprint = await this._computeChainFingerprint(
1416
- chain,
1417
- (method, params) => this._callConnector(sessionId, method, params)
2018
+ const outcome = await requestSaveDeviceBinding(
2019
+ this.emitter,
2020
+ this._uiRegistry,
2021
+ {
2022
+ selectionRequestId,
2023
+ connection: { transport: "ble", connectId },
2024
+ ...identity ? { identity } : {},
2025
+ extra: context?.extra,
2026
+ operationId
2027
+ },
2028
+ signal
1418
2029
  );
1419
- if (fingerprint === deviceId) {
1420
- return { success: true };
2030
+ if (context) context.bindingSaved = outcome.saved;
2031
+ if (!outcome.saved) {
2032
+ debugLog("[LedgerAdapter] BLE binding not persisted by host", {
2033
+ connectId,
2034
+ reason: outcome.reason
2035
+ });
1421
2036
  }
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 };
2037
+ } catch (error) {
2038
+ await this._dropSession(connectId, signal);
2039
+ throw error;
2040
+ } finally {
2041
+ if (this._bindingSelectionRequestId === selectionRequestId) {
2042
+ this._bindingSelectionRequestId = void 0;
1427
2043
  }
1428
- throw err;
1429
2044
  }
1430
2045
  }
2046
+ async _dropSession(connectId, signal) {
2047
+ const sessionId = this._sessions.get(connectId);
2048
+ this._sessions.delete(connectId);
2049
+ if (!sessionId) return;
2050
+ this._sessionTransports.delete(sessionId);
2051
+ const teardown = this._runConnectorTeardown(() => this.connector.disconnect(sessionId)).catch(
2052
+ () => void 0
2053
+ );
2054
+ if (!signal.aborted) await teardown;
2055
+ }
2056
+ /** Verify on the acquired session without re-entering the job queue. */
2057
+ async _verifyDeviceFingerprintWithSession(sessionId, deviceId, chain) {
2058
+ if (!deviceId) return { success: true };
2059
+ const fingerprint = await this._computeChainFingerprint(
2060
+ chain,
2061
+ (method, params) => this._callConnector(sessionId, method, params)
2062
+ );
2063
+ if (fingerprint === deviceId) {
2064
+ return { success: true };
2065
+ }
2066
+ return { success: false, expected: deviceId, actual: fingerprint };
2067
+ }
1431
2068
  /**
1432
2069
  * Compute the chain fingerprint via a caller-supplied call strategy.
1433
2070
  *
@@ -1455,11 +2092,23 @@ var _LedgerAdapter = class _LedgerAdapter {
1455
2092
  address = (await callMethod("solGetAddress", { path, showOnDevice: false })).address;
1456
2093
  } else if (chain === "tron") {
1457
2094
  address = (await callMethod("tronGetAddress", { path, showOnDevice: false })).address;
2095
+ } else if (chain === "zcash") {
2096
+ address = (await callMethod("zcashGetShieldedAddress", { path, showOnDevice: false })).address;
1458
2097
  } else {
1459
2098
  throw new Error(`Unsupported chain for fingerprint: ${chain}`);
1460
2099
  }
1461
2100
  return deriveDeviceFingerprint(address);
1462
2101
  }
2102
+ /**
2103
+ * Operation owning the running job, so a mid-call UI request can name it.
2104
+ * Undefined at cold start when no operation owns the job.
2105
+ */
2106
+ _activeOperationId() {
2107
+ const activeJobId = this._jobQueue.getActiveJob()?.deviceId;
2108
+ if (!activeJobId) return void 0;
2109
+ if (isHardwareOperationId(activeJobId)) return activeJobId;
2110
+ return this._operations.findActiveByConnectionKey(activeJobId)?.operationId;
2111
+ }
1463
2112
  // Ledger WebUSB won't expose a locked device, so we can't auto-detect unlock.
1464
2113
  // The user must press Confirm after unlocking, which triggers a search retry.
1465
2114
  // If `signal` is provided, an abort cancels the pending UI request so the
@@ -1469,15 +2118,18 @@ var _LedgerAdapter = class _LedgerAdapter {
1469
2118
  if (signal?.aborted) {
1470
2119
  _LedgerAdapter._throwIfAborted(signal);
1471
2120
  }
2121
+ const operationId = this._activeOperationId();
1472
2122
  const waitPromise = this._uiRegistry.wait(
1473
- UI_REQUEST.REQUEST_DEVICE_CONNECT
2123
+ UI_REQUEST.REQUEST_DEVICE_CONNECT,
2124
+ { operationId }
1474
2125
  );
1475
2126
  this.emitter.emit(UI_REQUEST.REQUEST_DEVICE_CONNECT, {
1476
2127
  type: UI_REQUEST.REQUEST_DEVICE_CONNECT,
1477
2128
  payload: {
1478
2129
  vendor: "ledger",
1479
2130
  reason: "device-not-found",
1480
- message: "Please connect and unlock your Ledger device"
2131
+ message: "Please connect and unlock your Ledger device",
2132
+ operationId
1481
2133
  }
1482
2134
  });
1483
2135
  let payload;
@@ -1538,15 +2190,18 @@ var _LedgerAdapter = class _LedgerAdapter {
1538
2190
  return { ...params, showOnDevice: true };
1539
2191
  }
1540
2192
  async _waitForBtcHighIndexConfirm(path, accountIndex) {
2193
+ const operationId = this._activeOperationId();
1541
2194
  const waitPromise = this._uiRegistry.wait(
1542
- UI_REQUEST.REQUEST_BTC_HIGH_INDEX_CONFIRM
2195
+ UI_REQUEST.REQUEST_BTC_HIGH_INDEX_CONFIRM,
2196
+ { operationId }
1543
2197
  );
1544
2198
  this.emitter.emit(UI_REQUEST.REQUEST_BTC_HIGH_INDEX_CONFIRM, {
1545
2199
  type: UI_REQUEST.REQUEST_BTC_HIGH_INDEX_CONFIRM,
1546
2200
  payload: {
1547
2201
  vendor: "ledger",
1548
2202
  path,
1549
- accountIndex
2203
+ accountIndex,
2204
+ operationId
1550
2205
  }
1551
2206
  });
1552
2207
  try {
@@ -1563,12 +2218,14 @@ var _LedgerAdapter = class _LedgerAdapter {
1563
2218
  // Ask the user whether to install a missing app (autoInstallApp flow).
1564
2219
  // Same register-then-emit ordering as the BTC high-index gate.
1565
2220
  async _waitForInstallAppConfirm(appName) {
2221
+ const operationId = this._activeOperationId();
1566
2222
  const waitPromise = this._uiRegistry.wait(
1567
- UI_REQUEST.REQUEST_INSTALL_APP
2223
+ UI_REQUEST.REQUEST_INSTALL_APP,
2224
+ { operationId }
1568
2225
  );
1569
2226
  this.emitter.emit(UI_REQUEST.REQUEST_INSTALL_APP, {
1570
2227
  type: UI_REQUEST.REQUEST_INSTALL_APP,
1571
- payload: { vendor: "ledger", appName }
2228
+ payload: { vendor: "ledger", appName, operationId }
1572
2229
  });
1573
2230
  try {
1574
2231
  const payload = await waitPromise;
@@ -1584,16 +2241,17 @@ var _LedgerAdapter = class _LedgerAdapter {
1584
2241
  // Layer 1 entry. Caller signal only races the outer awaiter; the shared
1585
2242
  // `_doConnect` runs under its own internal controller so caller A's cancel
1586
2243
  // doesn't kill caller B's await.
1587
- async ensureConnected(connectId, signal, allowUsbEphemeralFallback = false) {
2244
+ async ensureConnected(connectId, signal, allowUsbEphemeralFallback = false, preserveOperationId, context) {
1588
2245
  if (signal.aborted) _LedgerAdapter._throwIfAborted(signal);
1589
- if (isLedgerBleConnectionType(this.connector.connectionType) && !connectId) {
2246
+ this._assertConnectorReady("connectDevice");
2247
+ if (this._isBleConnection() && !connectId && !allowUsbEphemeralFallback) {
1590
2248
  throw Object.assign(new Error("Ledger BLE connectId is required."), {
1591
2249
  code: HardwareErrorCode3.DeviceNotFound
1592
2250
  });
1593
2251
  }
1594
2252
  if (connectId && this._sessions.has(connectId)) return connectId;
1595
2253
  if (!connectId && this._sessions.size > 0) {
1596
- if (!isLedgerBleConnectionType(this.connector.connectionType) && this._sessions.size > 1) {
2254
+ if (!this._isBleConnection() && this._sessions.size > 1) {
1597
2255
  throw Object.assign(
1598
2256
  new Error(
1599
2257
  "Ledger USB session invariant violated: more than one session is active. Please reconnect the device."
@@ -1603,15 +2261,24 @@ var _LedgerAdapter = class _LedgerAdapter {
1603
2261
  }
1604
2262
  return this._sessions.keys().next().value;
1605
2263
  }
1606
- if (!this._connectingPromise) {
1607
- this._doConnectAbortController = new AbortController();
1608
- const innerSignal = this._doConnectAbortController.signal;
2264
+ if (!this._connectingPromise || this._doConnectAbortController?.signal.aborted) {
2265
+ const controller = new AbortController();
2266
+ this._doConnectAbortController = controller;
2267
+ const innerSignal = controller.signal;
1609
2268
  this._connectingPromise = (async () => {
1610
2269
  try {
1611
- return await this._doConnect(innerSignal, connectId, allowUsbEphemeralFallback);
2270
+ return await this._doConnect(
2271
+ innerSignal,
2272
+ connectId,
2273
+ allowUsbEphemeralFallback,
2274
+ preserveOperationId,
2275
+ context
2276
+ );
1612
2277
  } finally {
1613
- this._connectingPromise = null;
1614
- this._doConnectAbortController = null;
2278
+ if (this._doConnectAbortController === controller) {
2279
+ this._connectingPromise = null;
2280
+ this._doConnectAbortController = null;
2281
+ }
1615
2282
  }
1616
2283
  })();
1617
2284
  }
@@ -1620,22 +2287,57 @@ var _LedgerAdapter = class _LedgerAdapter {
1620
2287
  // Layer 1 main loop — the ONLY place in SDK that emits unlock dialog.
1621
2288
  // Bounded by MAX_DOCONNECT_CONFIRMS — after N Confirms with no progress,
1622
2289
  // 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
- }
2290
+ async _doConnect(internalSignal, targetConnectId, allowUsbEphemeralFallback = false, preserveOperationId, context) {
2291
+ _LedgerAdapter._throwIfAborted(internalSignal);
2292
+ if (this.connector.availableTransports?.includes("usb") && this.connector.availableTransports.includes("ble")) {
2293
+ this._activeConnectionType = "usb";
2294
+ const usbDevices = await this._searchDevices({ transportType: "usb" }, internalSignal);
2295
+ _LedgerAdapter._throwIfAborted(internalSignal);
2296
+ if (usbDevices.length > 0) {
2297
+ const knownUsb = context?.knownConnections?.find(
2298
+ (connection) => connection.transport === "usb"
2299
+ );
2300
+ const usbTarget = knownUsb?.transport === "usb" ? knownUsb.connectId : usbDevices.find((device) => device.connectId === targetConnectId)?.connectId;
2301
+ return this._connectFirstOrSelect(
2302
+ usbDevices,
2303
+ usbTarget,
2304
+ allowUsbEphemeralFallback,
2305
+ preserveOperationId,
2306
+ context,
2307
+ internalSignal
2308
+ );
2309
+ }
2310
+ if (context?.supportedTransports && !context.supportedTransports.includes("ble")) {
2311
+ throw createHwkError({
2312
+ code: HardwareErrorCode3.DeviceNotFound,
2313
+ message: "This Ledger model has no Bluetooth transport; connect it over USB"
2314
+ });
1638
2315
  }
2316
+ this._activeConnectionType = "ble";
2317
+ const knownBle = context?.knownConnections?.find(
2318
+ (connection) => connection.transport === "ble"
2319
+ );
2320
+ if (knownBle?.transport === "ble") {
2321
+ return this._connectDeviceOrThrow(knownBle.connectId, preserveOperationId, internalSignal);
2322
+ }
2323
+ if (targetConnectId && context?.knownConnections === void 0) {
2324
+ throw createHwkError({
2325
+ code: HardwareErrorCode3.DeviceNotFound,
2326
+ message: "Ledger connection metadata is required before starting Bluetooth binding"
2327
+ });
2328
+ }
2329
+ const bleDevices = await this._searchDevices({ transportType: "ble" }, internalSignal);
2330
+ return this._connectFirstOrSelect(
2331
+ bleDevices,
2332
+ void 0,
2333
+ allowUsbEphemeralFallback,
2334
+ preserveOperationId,
2335
+ context,
2336
+ internalSignal
2337
+ );
2338
+ }
2339
+ if (this._isBleConnection() && targetConnectId) {
2340
+ return this._connectDeviceOrThrow(targetConnectId, preserveOperationId, internalSignal);
1639
2341
  }
1640
2342
  let confirms = 0;
1641
2343
  while (!internalSignal.aborted) {
@@ -1643,22 +2345,28 @@ var _LedgerAdapter = class _LedgerAdapter {
1643
2345
  type: EConnectorInteraction.Searching,
1644
2346
  payload: { sessionId: "" }
1645
2347
  });
1646
- let devices = await this.searchDevices();
2348
+ let devices = await this._searchDevices(void 0, internalSignal);
2349
+ _LedgerAdapter._throwIfAborted(internalSignal);
1647
2350
  if (devices.length === 0) {
1648
2351
  for (let i = 0; i < 3 && !internalSignal.aborted; i += 1) {
1649
2352
  await new Promise((resolve) => {
1650
2353
  setTimeout(resolve, DEVICE_CONNECT_RETRY_DELAY_MS);
1651
2354
  });
1652
- devices = await this.searchDevices();
2355
+ _LedgerAdapter._throwIfAborted(internalSignal);
2356
+ devices = await this._searchDevices(void 0, internalSignal);
2357
+ _LedgerAdapter._throwIfAborted(internalSignal);
1653
2358
  if (devices.length > 0) break;
1654
2359
  }
1655
2360
  }
1656
- if (devices.length > 0) {
2361
+ if (devices.length > 0 || this._isBleConnection() && allowUsbEphemeralFallback) {
1657
2362
  try {
1658
2363
  return await this._connectFirstOrSelect(
1659
2364
  devices,
1660
2365
  targetConnectId,
1661
- allowUsbEphemeralFallback
2366
+ allowUsbEphemeralFallback,
2367
+ preserveOperationId,
2368
+ context,
2369
+ internalSignal
1662
2370
  );
1663
2371
  } catch (err) {
1664
2372
  if (!isDeviceLockedError(err) && !isDeviceNotAdvertisingError(err) && !isDeviceDisconnectedError(err)) {
@@ -1687,45 +2395,147 @@ var _LedgerAdapter = class _LedgerAdapter {
1687
2395
  _LedgerAdapter._throwIfAborted(internalSignal);
1688
2396
  throw new Error("_doConnect aborted");
1689
2397
  }
1690
- async _connectFirstOrSelect(devices, targetConnectId, allowUsbEphemeralFallback = false) {
2398
+ async _connectFirstOrSelect(devices, targetConnectId, allowUsbEphemeralFallback, preserveOperationId, context, signal) {
2399
+ _LedgerAdapter._throwIfAborted(signal);
1691
2400
  if (targetConnectId) {
1692
2401
  const target = devices.find(
1693
2402
  (d) => d.connectId === targetConnectId || d.deviceId === targetConnectId
1694
2403
  );
1695
2404
  if (target) {
1696
- return this._connectDeviceOrThrow(target.connectId);
2405
+ return this._connectDeviceOrThrow(target.connectId, preserveOperationId, signal);
1697
2406
  }
1698
- if (!isLedgerBleConnectionType(this.connector.connectionType) && devices.length === 1 && allowUsbEphemeralFallback) {
2407
+ if (!this._isBleConnection() && devices.length === 1 && allowUsbEphemeralFallback) {
1699
2408
  debugLog(
1700
2409
  `[LedgerAdapter] target ${targetConnectId} not in fresh enumeration; accepting sole USB device ${devices[0].connectId} for fingerprint-verified recovery`
1701
2410
  );
1702
- return this._connectDeviceOrThrow(devices[0].connectId);
2411
+ return this._connectDeviceOrThrow(devices[0].connectId, preserveOperationId, signal);
1703
2412
  }
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;
2413
+ if (!this._isBleConnection() || !allowUsbEphemeralFallback) {
2414
+ const err = Object.assign(new Error(`Target Ledger unavailable: ${targetConnectId}`), {
2415
+ code: HardwareErrorCode3.DeviceNotFound
2416
+ });
2417
+ if (this._isBleConnection()) {
2418
+ err._tag = ERROR_TAG.DeviceNotAdvertising;
2419
+ }
2420
+ throw err;
1709
2421
  }
1710
- throw err;
1711
2422
  }
1712
- if (isLedgerBleConnectionType(this.connector.connectionType)) {
2423
+ const requiresBleSelection = this._isBleConnection();
2424
+ if (requiresBleSelection && !allowUsbEphemeralFallback) {
1713
2425
  throw Object.assign(new Error("Ledger BLE connectId is required."), {
1714
2426
  code: HardwareErrorCode3.DeviceNotFound
1715
2427
  });
1716
2428
  }
1717
- if (devices.length > 1) {
1718
- throw createMultipleUsbLedgerDevicesError();
2429
+ if (requiresBleSelection && context?.allowDeviceSelection !== false) {
2430
+ const bindingSessionId = context?.bindingSessionId ?? this._uiRegistry.createRequestId();
2431
+ if (context) context.bindingSessionId = bindingSessionId;
2432
+ const allowUsbFallback = context?.bindingReason !== "manual-rebind" && Boolean(this.connector.availableTransports?.includes("usb"));
2433
+ const knownUsb = context?.knownConnections?.find(
2434
+ (connection) => connection.transport === "usb"
2435
+ );
2436
+ const usbConnectId = knownUsb?.transport === "usb" ? knownUsb.connectId : targetConnectId;
2437
+ const { device, requestId } = await requestBleDeviceSelection({
2438
+ emitter: this.emitter,
2439
+ registry: this._uiRegistry,
2440
+ signal,
2441
+ allowUsbFallback,
2442
+ scan: async () => {
2443
+ if (allowUsbFallback) {
2444
+ const usbDevices = await this._searchDevices({ transportType: "usb" }, signal);
2445
+ const candidate = usbDevices.find(
2446
+ (device2) => device2.connectionType === "usb" && device2.connectId === usbConnectId
2447
+ ) ?? (usbDevices.length === 1 && usbDevices[0].connectionType === "usb" ? usbDevices[0] : void 0);
2448
+ if (candidate) return [candidate];
2449
+ }
2450
+ return this._searchDevices({ transportType: "ble", waitForAllTransports: true }, signal);
2451
+ },
2452
+ request: {
2453
+ devices,
2454
+ bindingSessionId,
2455
+ context: {
2456
+ kind: "bind-connection",
2457
+ transport: "ble",
2458
+ reason: context?.bindingReason ?? (targetConnectId ? "known-connection-unavailable" : "missing-binding")
2459
+ },
2460
+ extra: context?.extra,
2461
+ operationId: preserveOperationId
2462
+ }
2463
+ });
2464
+ if (device.connectionType === "usb") {
2465
+ this._activeConnectionType = "usb";
2466
+ if (context) context.selectedConnection = void 0;
2467
+ this._bindingSelectionRequestId = requestId;
2468
+ this._finishBleBinding("cancelled");
2469
+ return this._connectDeviceOrThrow(device.connectId, preserveOperationId, signal);
2470
+ }
2471
+ this._bindingSelectionRequestId = requestId;
2472
+ const connected = await this._connectDeviceOrThrow(
2473
+ device.connectId,
2474
+ preserveOperationId,
2475
+ signal
2476
+ );
2477
+ if (context) context.selectedConnection = { connectId: connected, requestId };
2478
+ if (context?.deferBindingSave) {
2479
+ context.pendingBinding = { connectId: connected, requestId };
2480
+ } else {
2481
+ await this._saveSelectedBleBinding(
2482
+ connected,
2483
+ requestId,
2484
+ context,
2485
+ preserveOperationId,
2486
+ signal
2487
+ );
2488
+ }
2489
+ return connected;
2490
+ }
2491
+ if (devices.length > 0 && (devices.length > 1 || requiresBleSelection)) {
2492
+ if (context?.allowDeviceSelection === false || !this.emitter.listenerCount(UI_REQUEST.REQUEST_SELECT_DEVICE)) {
2493
+ throw createHwkError({
2494
+ code: HardwareErrorCode3.DeviceNotFound,
2495
+ message: "Select a Ledger device before continuing"
2496
+ });
2497
+ }
2498
+ const requestId = this._uiRegistry.createRequestId();
2499
+ const operationId = this._activeOperationId();
2500
+ const waitPromise = this._uiRegistry.wait(
2501
+ UI_REQUEST.REQUEST_SELECT_DEVICE,
2502
+ { requestId, operationId }
2503
+ );
2504
+ this.emitter.emit(UI_REQUEST.REQUEST_SELECT_DEVICE, {
2505
+ type: UI_REQUEST.REQUEST_SELECT_DEVICE,
2506
+ payload: {
2507
+ devices,
2508
+ requestId,
2509
+ operationId,
2510
+ context: requiresBleSelection ? {
2511
+ kind: "bind-connection",
2512
+ transport: "ble",
2513
+ reason: targetConnectId ? "known-connection-unavailable" : "missing-binding"
2514
+ } : { kind: "select-device", transport: "usb", reason: "multiple-candidates" },
2515
+ extra: context?.extra
2516
+ }
2517
+ });
2518
+ const { sdkConnectId } = await this._abortable(signal, waitPromise);
2519
+ _LedgerAdapter._throwIfAborted(signal);
2520
+ const selected = devices.find((device) => device.connectId === sdkConnectId);
2521
+ if (!selected) {
2522
+ throw Object.assign(new Error("Selected Ledger is no longer available"), {
2523
+ code: HardwareErrorCode3.DeviceNotFound
2524
+ });
2525
+ }
2526
+ if (context && requiresBleSelection)
2527
+ context.selectedConnection = { connectId: selected.connectId, requestId };
2528
+ return this._connectDeviceOrThrow(selected.connectId, preserveOperationId, signal);
1719
2529
  }
1720
2530
  if (devices.length !== 1) {
1721
2531
  throw Object.assign(new Error("Ledger device not found."), {
1722
2532
  code: HardwareErrorCode3.DeviceNotFound
1723
2533
  });
1724
2534
  }
1725
- return this._connectDeviceOrThrow(devices[0].connectId);
2535
+ return this._connectDeviceOrThrow(devices[0].connectId, preserveOperationId, signal);
1726
2536
  }
1727
- async _connectDeviceOrThrow(chosenConnectId) {
1728
- const result = await this.connectDevice(chosenConnectId);
2537
+ async _connectDeviceOrThrow(chosenConnectId, preserveOperationId, signal) {
2538
+ const result = await this._connectTarget(chosenConnectId, preserveOperationId, signal);
1729
2539
  if (!result.success) {
1730
2540
  const payload = result.payload;
1731
2541
  const rethrow = Object.assign(new Error(payload.error), {
@@ -1736,7 +2546,7 @@ var _LedgerAdapter = class _LedgerAdapter {
1736
2546
  }
1737
2547
  throw rethrow;
1738
2548
  }
1739
- return chosenConnectId;
2549
+ return result.payload;
1740
2550
  }
1741
2551
  /**
1742
2552
  * Call the connector with automatic session resolution and disconnect retry.
@@ -1766,33 +2576,139 @@ var _LedgerAdapter = class _LedgerAdapter {
1766
2576
  * lives in one place.
1767
2577
  */
1768
2578
  async _callConnector(sessionId, method, params, signal) {
1769
- const promise = this.connector.call(sessionId, method, params);
2579
+ this._assertConnectorReady(method);
2580
+ if (signal?.aborted) throw this._abortReason(signal);
2581
+ const releaseOperation = this._retainConnectorOperation(`call:${sessionId}`);
2582
+ let promise;
2583
+ try {
2584
+ promise = this.connector.call(sessionId, method, params).finally(releaseOperation);
2585
+ } catch (error) {
2586
+ releaseOperation();
2587
+ throw error;
2588
+ }
1770
2589
  const result = signal ? await this._abortable(signal, promise) : await promise;
1771
2590
  return this._unwrapConnectorResult(result);
1772
2591
  }
2592
+ _assertConnectorReady(method) {
2593
+ if (this._resetPromise || this._pendingConnectorTeardowns > 0 || this._unsettledConnectorOperations.size > 0) {
2594
+ throw _LedgerAdapter._createDeviceBusyError(method);
2595
+ }
2596
+ }
2597
+ _runConnectorTeardown(task) {
2598
+ const previous = this._connectorTeardownTail;
2599
+ let releaseTail = () => void 0;
2600
+ this._connectorTeardownTail = new Promise((resolve) => {
2601
+ releaseTail = resolve;
2602
+ });
2603
+ this._pendingConnectorTeardowns += 1;
2604
+ return (async () => {
2605
+ try {
2606
+ await previous;
2607
+ await this._waitForConnectorOperationsToDrain();
2608
+ await task();
2609
+ } finally {
2610
+ this._pendingConnectorTeardowns -= 1;
2611
+ releaseTail();
2612
+ }
2613
+ })();
2614
+ }
2615
+ _waitForConnectorOperationsToDrain() {
2616
+ if (this._unsettledConnectorOperations.size === 0) {
2617
+ return Promise.resolve();
2618
+ }
2619
+ return new Promise((resolve) => {
2620
+ this._connectorIdleWaiters.add(resolve);
2621
+ });
2622
+ }
2623
+ _retainConnectorOperation(key) {
2624
+ this._unsettledConnectorOperations.set(
2625
+ key,
2626
+ (this._unsettledConnectorOperations.get(key) ?? 0) + 1
2627
+ );
2628
+ let released = false;
2629
+ return () => {
2630
+ if (released) return;
2631
+ released = true;
2632
+ const remaining = (this._unsettledConnectorOperations.get(key) ?? 1) - 1;
2633
+ if (remaining > 0) {
2634
+ this._unsettledConnectorOperations.set(key, remaining);
2635
+ } else {
2636
+ this._unsettledConnectorOperations.delete(key);
2637
+ }
2638
+ if (this._unsettledConnectorOperations.size === 0) {
2639
+ for (const resolve of this._connectorIdleWaiters) resolve();
2640
+ this._connectorIdleWaiters.clear();
2641
+ }
2642
+ };
2643
+ }
1773
2644
  async connectorCall(connectId, method, params, fingerprint, permissionDeviceId, commonParams, installContext) {
1774
- debugLog("[LedgerAdapter][REQ]", { method, connectId: connectId || "(empty)", params });
1775
- const queueKey = connectId || "__ledger_default__";
2645
+ const positionalOperationId = isHardwareOperationId(connectId) ? connectId : void 0;
2646
+ if (positionalOperationId && commonParams?.operationId && positionalOperationId !== commonParams.operationId) {
2647
+ throw createHwkError({
2648
+ code: HardwareErrorCode3.InvalidParams,
2649
+ message: "Conflicting Ledger operation ids",
2650
+ params: {
2651
+ positionalOperationId,
2652
+ commonOperationId: commonParams.operationId
2653
+ }
2654
+ });
2655
+ }
2656
+ const operationId = commonParams?.operationId ?? positionalOperationId;
2657
+ const operation = operationId ? this._operations.resolve(operationId) : void 0;
2658
+ const releaseOperationRetention = operationId ? this._operations.retain(operationId) : void 0;
2659
+ const effectiveConnectId = operation?.connectId ?? connectId;
2660
+ debugLog("[LedgerAdapter][REQ]", {
2661
+ method,
2662
+ connectId: effectiveConnectId || "(empty)",
2663
+ params
2664
+ });
2665
+ const queueKey = ledgerQueueKey({ operationId, connectId: effectiveConnectId });
1776
2666
  try {
1777
2667
  const result = await this._jobQueue.enqueue(
1778
2668
  queueKey,
1779
- async (signal) => this._runConnectorCall(
1780
- connectId,
1781
- method,
1782
- params,
1783
- signal,
1784
- fingerprint,
1785
- permissionDeviceId,
1786
- commonParams,
1787
- installContext
1788
- ),
2669
+ async (signal) => {
2670
+ if (operationId) this._activeOperationJobs.add(operationId);
2671
+ try {
2672
+ return await this._runConnectorCall(
2673
+ effectiveConnectId,
2674
+ method,
2675
+ params,
2676
+ signal,
2677
+ fingerprint,
2678
+ permissionDeviceId,
2679
+ commonParams,
2680
+ installContext ?? {},
2681
+ operationId
2682
+ );
2683
+ } catch (error) {
2684
+ const status = signal.aborted ? "cancelled" : "failed";
2685
+ for (const [pickedConnectId, owner] of this._pendingPickBindings) {
2686
+ this._pendingPickBindings.delete(pickedConnectId);
2687
+ this._discardPendingBinding(owner, status);
2688
+ await this._dropSession(pickedConnectId, signal);
2689
+ }
2690
+ this._finishBleBinding(status);
2691
+ throw error;
2692
+ } finally {
2693
+ if (operationId) {
2694
+ this._activeOperationJobs.delete(operationId);
2695
+ if (this._pendingOperationDisconnects.delete(operationId)) {
2696
+ await this._releaseLostOperationConnection(operationId);
2697
+ }
2698
+ }
2699
+ }
2700
+ },
1789
2701
  {
1790
2702
  label: method,
1791
2703
  rejectIfBusy: true,
1792
2704
  busyError: _LedgerAdapter._createDeviceBusyError(method)
1793
2705
  }
1794
2706
  );
1795
- debugLog("[LedgerAdapter][RES]", { method, success: true, payload: result });
2707
+ debugLog("[LedgerAdapter][RES]", {
2708
+ method,
2709
+ success: true,
2710
+ payload: redactResultForLog(result)
2711
+ });
1796
2712
  return result;
1797
2713
  } catch (err) {
1798
2714
  const e = err;
@@ -1806,15 +2722,22 @@ var _LedgerAdapter = class _LedgerAdapter {
1806
2722
  }
1807
2723
  });
1808
2724
  throw err;
2725
+ } finally {
2726
+ releaseOperationRetention?.();
1809
2727
  }
1810
2728
  }
2729
+ /** Hermes/RN polyfills don't always populate signal.reason; fall back. */
2730
+ _abortReason(signal) {
2731
+ return signal.reason ?? this._lastCancelReason ?? new Error("Aborted");
2732
+ }
1811
2733
  /**
1812
2734
  * Race a promise against an abort signal. On abort, rejects with
1813
2735
  * signal.reason → instance _lastCancelReason → generic Error('Aborted').
1814
2736
  */
1815
2737
  _abortable(signal, promise) {
1816
- const getAbortReason = () => signal.reason ?? this._lastCancelReason ?? new Error("Aborted");
2738
+ const getAbortReason = () => this._abortReason(signal);
1817
2739
  if (signal.aborted) {
2740
+ void promise.catch(() => void 0);
1818
2741
  return Promise.reject(getAbortReason());
1819
2742
  }
1820
2743
  return new Promise((resolve, reject) => {
@@ -1841,13 +2764,15 @@ var _LedgerAdapter = class _LedgerAdapter {
1841
2764
  }
1842
2765
  }
1843
2766
  /** Actual work done under the job queue — connection, fingerprint, call, and recovery. */
1844
- async _runConnectorCall(connectId, method, params, signal, fingerprint, permissionDeviceId, commonParams, installContext) {
2767
+ async _runConnectorCall(connectId, method, params, signal, fingerprint, permissionDeviceId, commonParams, installContext, operationId, lockedRetryBudget = _LedgerAdapter.MAX_BUSINESS_RETRY_BUDGET) {
1845
2768
  _LedgerAdapter._throwIfAborted(signal);
1846
- await this._ensureDevicePermission(
1847
- connectId,
1848
- permissionDeviceId ?? fingerprint?.deviceId,
1849
- signal
1850
- );
2769
+ if (!operationId) {
2770
+ await this._ensureDevicePermission(
2771
+ connectId,
2772
+ permissionDeviceId ?? fingerprint?.deviceId,
2773
+ signal
2774
+ );
2775
+ }
1851
2776
  _LedgerAdapter._throwIfAborted(signal);
1852
2777
  let effectiveParams = params;
1853
2778
  if (method === "btcGetPublicKey") {
@@ -1861,19 +2786,60 @@ var _LedgerAdapter = class _LedgerAdapter {
1861
2786
  effectiveParams = gatedParams;
1862
2787
  }
1863
2788
  const allowUsbEphemeralFallback = !!fingerprint?.deviceId && !fingerprint.skipFingerprint;
1864
- const resolvedConnectId = await this.ensureConnected(
1865
- connectId,
2789
+ let businessCallStarted = false;
2790
+ const selectedBleTarget = connectId ? this._selectedBleReconnectTargets.get(connectId) : void 0;
2791
+ const knownTransport = this._isBleConnection() ? "ble" : "usb";
2792
+ const hintedConnectId = commonParams?.knownConnections?.find(
2793
+ (connection) => connection.transport === knownTransport
2794
+ );
2795
+ const inputConnectId = hintedConnectId && hintedConnectId.transport !== "qr" ? hintedConnectId.connectId : connectId;
2796
+ const preferredConnectId = fingerprint && !fingerprint.skipFingerprint && selectedBleTarget?.chain === fingerprint.chain && selectedBleTarget.fingerprint === fingerprint.deviceId ? selectedBleTarget.connectId : inputConnectId;
2797
+ const checksWallet = Boolean(
2798
+ fingerprint && !fingerprint.skipFingerprint && fingerprint.deviceId
2799
+ );
2800
+ const connectionAttempt = {
2801
+ ...commonParams,
2802
+ deferBindingSave: checksWallet
2803
+ };
2804
+ const bundleConnection = installContext?.connection;
2805
+ if (bundleConnection && this._sessions.get(bundleConnection.connectId) !== bundleConnection.sessionId) {
2806
+ throw createHwkError({
2807
+ code: HardwareErrorCode3.DeviceDisconnected,
2808
+ message: "Ledger all-network connection ended"
2809
+ });
2810
+ }
2811
+ const resolvedConnectId = operationId ? this._operations.resolve(operationId).connectId : bundleConnection?.connectId ?? await this.ensureConnected(
2812
+ preferredConnectId,
1866
2813
  signal,
1867
- allowUsbEphemeralFallback
2814
+ allowUsbEphemeralFallback,
2815
+ void 0,
2816
+ connectionAttempt
1868
2817
  );
1869
2818
  const sessionId = this._sessions.get(resolvedConnectId);
2819
+ if (sessionId && installContext && !installContext.connection) {
2820
+ installContext.connection = { connectId: resolvedConnectId, sessionId };
2821
+ }
1870
2822
  if (!sessionId) {
2823
+ if (operationId) {
2824
+ this._operations.end(operationId, "disconnect");
2825
+ throw createHwkError({
2826
+ code: HardwareErrorCode3.OperationEnded,
2827
+ message: "Ledger operation connection is no longer active",
2828
+ params: { operationId, reason: "disconnect" }
2829
+ });
2830
+ }
1871
2831
  throw Object.assign(new Error("Auto-connect succeeded but no session found"), {
1872
2832
  _tag: ERROR_TAG.DeviceSessionNotFound
1873
2833
  });
1874
2834
  }
1875
2835
  try {
1876
- if (fingerprint && !fingerprint.skipFingerprint && fingerprint.deviceId) {
2836
+ const isBleSession = this._sessionTransports.get(sessionId) === "ble";
2837
+ if (!operationId && connectionAttempt.pendingBinding) {
2838
+ this._pendingPickBindings.set(resolvedConnectId, connectionAttempt);
2839
+ }
2840
+ const bindingOwner = operationId ? this._pendingOperationBindings.get(operationId) : this._pendingPickBindings.get(resolvedConnectId);
2841
+ const pendingBinding = bindingOwner?.pendingBinding?.connectId === resolvedConnectId ? bindingOwner.pendingBinding : void 0;
2842
+ if (fingerprint && checksWallet) {
1877
2843
  const fp = await this._abortable(
1878
2844
  signal,
1879
2845
  this._verifyDeviceFingerprintWithSession(
@@ -1883,14 +2849,65 @@ var _LedgerAdapter = class _LedgerAdapter {
1883
2849
  )
1884
2850
  );
1885
2851
  if (!fp.success) {
2852
+ if (pendingBinding) {
2853
+ this._discardPendingBinding(bindingOwner, "failed");
2854
+ this._pendingPickBindings.delete(resolvedConnectId);
2855
+ if (operationId) {
2856
+ this._pendingOperationBindings.delete(operationId);
2857
+ this._operations.end(operationId, "explicit");
2858
+ await this._dropSession(resolvedConnectId, signal);
2859
+ }
2860
+ }
1886
2861
  throw Object.assign(new Error(formatDeviceMismatchError(fp.expected, fp.actual)), {
1887
2862
  code: HardwareErrorCode3.DeviceMismatch
1888
2863
  });
1889
2864
  }
1890
2865
  }
2866
+ if (pendingBinding && bindingOwner) {
2867
+ bindingOwner.pendingBinding = void 0;
2868
+ if (operationId) this._pendingOperationBindings.delete(operationId);
2869
+ else this._pendingPickBindings.delete(resolvedConnectId);
2870
+ await this._saveSelectedBleBinding(
2871
+ pendingBinding.connectId,
2872
+ pendingBinding.requestId,
2873
+ bindingOwner,
2874
+ operationId,
2875
+ signal,
2876
+ fingerprint && checksWallet ? {
2877
+ vendor: "ledger",
2878
+ type: "chainFingerprint",
2879
+ chain: fingerprint.chain,
2880
+ value: fingerprint.deviceId
2881
+ } : void 0
2882
+ );
2883
+ }
2884
+ if (fingerprint && !fingerprint.skipFingerprint && fingerprint.deviceId && !operationId && isBleSession && connectionAttempt.selectedConnection?.connectId === resolvedConnectId) {
2885
+ this._selectedBleReconnectTargets.set(connectId, {
2886
+ connectId: resolvedConnectId,
2887
+ chain: fingerprint.chain,
2888
+ fingerprint: fingerprint.deviceId
2889
+ });
2890
+ }
2891
+ businessCallStarted = true;
1891
2892
  return await this._callConnector(sessionId, method, effectiveParams, signal);
1892
2893
  } catch (err) {
1893
2894
  if (signal.aborted) throw err;
2895
+ if (isLostConnectionError(err)) {
2896
+ this._discoveredDevices.delete(resolvedConnectId);
2897
+ if (operationId) await this._releaseLostOperationConnection(operationId);
2898
+ else {
2899
+ this._sessions.delete(resolvedConnectId);
2900
+ await this.connector.disconnect(sessionId).catch(() => void 0);
2901
+ }
2902
+ const ambiguous = businessCallStarted && !canReplayHardwareMethodAfterTransportFailure(method);
2903
+ const interactionParams = operationId ? { operationId, reason: "disconnect" } : void 0;
2904
+ throw createHwkError({
2905
+ code: operationId ? HardwareErrorCode3.OperationEnded : mapLedgerError(err).code,
2906
+ message: "Ledger operation connection was lost; start a new operation",
2907
+ params: ambiguous ? operationMayHaveCompletedParams(method, { operationId }) : interactionParams,
2908
+ recovery: ambiguous ? { scope: "unknown" } : void 0
2909
+ });
2910
+ }
1894
2911
  const errObj = err;
1895
2912
  debugLog("[LedgerAdapter] connectorCall error:", method, {
1896
2913
  message: errObj?.message,
@@ -1902,6 +2919,61 @@ var _LedgerAdapter = class _LedgerAdapter {
1902
2919
  isNotAdvertising: isDeviceNotAdvertisingError(err),
1903
2920
  isStuckApp: isStuckAppStateError(err)
1904
2921
  });
2922
+ const assertSessionCurrent = (message) => {
2923
+ if (this._sessions.get(resolvedConnectId) === sessionId) return;
2924
+ throw createHwkError({
2925
+ code: operationId ? HardwareErrorCode3.OperationEnded : HardwareErrorCode3.DeviceDisconnected,
2926
+ message
2927
+ });
2928
+ };
2929
+ if ((isDeviceLockedError(err) || errObj?.code === HardwareErrorCode3.DeviceLocked) && lockedRetryBudget > 0) {
2930
+ await this._waitForDeviceConnect(signal);
2931
+ assertSessionCurrent("Ledger connection ended while waiting for unlock");
2932
+ return this._runConnectorCall(
2933
+ resolvedConnectId,
2934
+ method,
2935
+ effectiveParams,
2936
+ signal,
2937
+ fingerprint,
2938
+ permissionDeviceId,
2939
+ commonParams,
2940
+ installContext,
2941
+ operationId,
2942
+ lockedRetryBudget - 1
2943
+ );
2944
+ }
2945
+ if (businessCallStarted && isStuckAppStateError(err)) {
2946
+ await this._sleepAbortable(_LedgerAdapter.STUCK_APP_RETRY_DELAY_MS, signal);
2947
+ assertSessionCurrent("Ledger connection ended during the app transition");
2948
+ try {
2949
+ return await this._callConnector(sessionId, method, effectiveParams, signal);
2950
+ } catch (retryErr) {
2951
+ if (isStuckAppStateError(retryErr)) throw err;
2952
+ if (isLostConnectionError(retryErr)) {
2953
+ if (operationId) await this._releaseLostOperationConnection(operationId);
2954
+ else {
2955
+ this._sessions.delete(resolvedConnectId);
2956
+ this._discoveredDevices.delete(resolvedConnectId);
2957
+ await this.connector.disconnect(sessionId).catch(() => void 0);
2958
+ }
2959
+ const ambiguous = !canReplayHardwareMethodAfterTransportFailure(method);
2960
+ throw createHwkError({
2961
+ code: operationId ? HardwareErrorCode3.OperationEnded : mapLedgerError(retryErr).code,
2962
+ message: `Ledger ${method} may have completed before the connection was lost`,
2963
+ params: ambiguous ? operationMayHaveCompletedParams(method, { operationId, reason: "disconnect" }) : { operationId, reason: "disconnect" },
2964
+ recovery: ambiguous ? { scope: "unknown" } : void 0
2965
+ });
2966
+ }
2967
+ throw retryErr;
2968
+ }
2969
+ }
2970
+ if (!operationId && errObj?.code === HardwareErrorCode3.DeviceMismatch) {
2971
+ this._sessions.delete(resolvedConnectId);
2972
+ this._sessionTransports.delete(sessionId);
2973
+ this._discoveredDevices.delete(resolvedConnectId);
2974
+ await this.connector.disconnect(sessionId).catch(() => void 0);
2975
+ throw err;
2976
+ }
1905
2977
  const autoInstallApp = commonParams?.autoInstallApp ?? this._defaultAutoInstallApp;
1906
2978
  const isAppMissing = isAppNotInstalledError(err) || err?.code === HardwareErrorCode3.AppNotInstalled;
1907
2979
  if (autoInstallApp && isAppMissing) {
@@ -1957,157 +3029,14 @@ var _LedgerAdapter = class _LedgerAdapter {
1957
3029
  fingerprint,
1958
3030
  permissionDeviceId,
1959
3031
  commonParams,
1960
- installContext
1961
- );
1962
- }
1963
- }
1964
- if (isStuckAppStateError(err)) {
1965
- try {
1966
- this._sessions.delete(resolvedConnectId);
1967
- this._discoveredDevices.delete(resolvedConnectId);
1968
- this.connector.reset?.();
1969
- } catch {
1970
- }
1971
- debugLog(
1972
- "[LedgerAdapter] stuck-app retry: method=",
1973
- method,
1974
- "delayMs=",
1975
- _LedgerAdapter.STUCK_APP_RETRY_DELAY_MS,
1976
- "_tag=",
1977
- err?._tag
1978
- );
1979
- try {
1980
- const retryResult = await this._retryAfterStuckApp(
1981
- resolvedConnectId,
1982
- method,
1983
- effectiveParams,
1984
- signal,
1985
- err,
1986
- fingerprint
1987
- );
1988
- debugLog("[LedgerAdapter] stuck-app retry succeeded: method=", method);
1989
- return retryResult;
1990
- } catch (retryErr) {
1991
- if (signal.aborted) throw retryErr;
1992
- if (isStuckAppStateError(retryErr)) {
1993
- debugLog("[LedgerAdapter] stuck-app retry exhausted (2nd 6901): method=", method);
1994
- throw err;
1995
- }
1996
- debugLog(
1997
- "[LedgerAdapter] stuck-app retry threw non-stuck error: method=",
1998
- method,
1999
- "retryErrTag=",
2000
- retryErr?._tag
3032
+ installContext,
3033
+ operationId
2001
3034
  );
2002
- throw retryErr;
2003
3035
  }
2004
3036
  }
2005
- if (isDeviceLockedError(err) || isDeviceNotAdvertisingError(err) || isDeviceDisconnectedError(err)) {
2006
- let lastErr = err;
2007
- for (let attempt = 0; attempt < _LedgerAdapter.MAX_BUSINESS_RETRY_BUDGET; attempt += 1) {
2008
- if (signal.aborted) throw lastErr;
2009
- try {
2010
- this._sessions.delete(resolvedConnectId);
2011
- this._discoveredDevices.delete(resolvedConnectId);
2012
- if (isDeviceDisconnectedError(lastErr)) {
2013
- try {
2014
- this.connector.reset?.();
2015
- } catch {
2016
- }
2017
- }
2018
- const reConnectId = await this.ensureConnected(
2019
- resolvedConnectId,
2020
- signal,
2021
- allowUsbEphemeralFallback
2022
- );
2023
- const reSessionId = this._sessions.get(reConnectId);
2024
- if (!reSessionId) throw lastErr;
2025
- if (fingerprint && !fingerprint.skipFingerprint && fingerprint.deviceId) {
2026
- const fp = await this._abortable(
2027
- signal,
2028
- this._verifyDeviceFingerprintWithSession(
2029
- reSessionId,
2030
- fingerprint.deviceId,
2031
- fingerprint.chain
2032
- )
2033
- );
2034
- if (!fp.success) {
2035
- throw Object.assign(new Error(formatDeviceMismatchError(fp.expected, fp.actual)), {
2036
- code: HardwareErrorCode3.DeviceMismatch
2037
- });
2038
- }
2039
- }
2040
- return await this._callConnector(reSessionId, method, effectiveParams, signal);
2041
- } catch (retryErr) {
2042
- if (signal.aborted) throw retryErr;
2043
- lastErr = retryErr;
2044
- const canRetry = attempt < _LedgerAdapter.MAX_BUSINESS_RETRY_BUDGET - 1 && (isDeviceLockedError(retryErr) || isDeviceNotAdvertisingError(retryErr) || isDeviceDisconnectedError(retryErr));
2045
- if (!canRetry) {
2046
- throw retryErr;
2047
- }
2048
- }
2049
- }
2050
- throw lastErr;
2051
- }
2052
- if (isTimeoutError(err)) {
2053
- debugLog("[LedgerAdapter] timeout, retrying with fresh connection...");
2054
- this._discoveredDevices.delete(resolvedConnectId);
2055
- return this._retryWithFreshConnection(
2056
- resolvedConnectId,
2057
- method,
2058
- effectiveParams,
2059
- signal,
2060
- err,
2061
- fingerprint
2062
- );
2063
- }
2064
- if (isConnectionLevelError(err)) {
2065
- debugLog("[LedgerAdapter] connection-level fail-closed reset");
2066
- this._sessions.delete(resolvedConnectId);
2067
- this._discoveredDevices.delete(resolvedConnectId);
2068
- this.connector.reset();
2069
- const codeNum = err?.code;
2070
- throw Object.assign(err, {
2071
- code: codeNum ?? HardwareErrorCode3.DeviceDisconnected
2072
- });
2073
- }
2074
3037
  throw err;
2075
3038
  }
2076
3039
  }
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
3040
  _sleepAbortable(ms, signal) {
2112
3041
  return new Promise((resolve, reject) => {
2113
3042
  if (signal.aborted) {
@@ -2126,111 +3055,22 @@ var _LedgerAdapter = class _LedgerAdapter {
2126
3055
  });
2127
3056
  }
2128
3057
  /**
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
- /**
2211
- * Ensure OS-level device permission (Bluetooth / USB) before proceeding.
2212
- *
2213
- * Emits `REQUEST_DEVICE_PERMISSION` and awaits the consumer's
2214
- * `RECEIVE_DEVICE_PERMISSION` reply (60s budget covers "probe → system
2215
- * prompt → user tap" plus a generous margin). If the consumer never wires
2216
- * a handler or never replies, the wait times out and the operation fails
2217
- * fast so scanners/callers don't hang silently.
2218
- *
2219
- * - No connectId (searchDevices): environment-level permission
2220
- * - With connectId (business methods): device-level permission
3058
+ * Ensure OS-level Bluetooth/USB permission; times out after 60s if the host never
3059
+ * replies. Without connectId the request is environment-level, else device-level.
2221
3060
  */
2222
3061
  async _ensureDevicePermission(connectId, deviceId, signal) {
2223
3062
  if (signal?.aborted) {
2224
3063
  _LedgerAdapter._throwIfAborted(signal);
2225
3064
  }
2226
3065
  const transportType = this.activeTransport ?? "hid";
3066
+ const operationId = this._activeOperationId();
2227
3067
  const waitPromise = this._uiRegistry.wait(
2228
3068
  UI_REQUEST.REQUEST_DEVICE_PERMISSION,
2229
- { timeoutMs: 6e4 }
3069
+ { timeoutMs: 6e4, operationId }
2230
3070
  );
2231
3071
  this.emitter.emit(UI_REQUEST.REQUEST_DEVICE_PERMISSION, {
2232
3072
  type: UI_REQUEST.REQUEST_DEVICE_PERMISSION,
2233
- payload: { transportType, connectId, deviceId }
3073
+ payload: { transportType, connectId, deviceId, operationId }
2234
3074
  });
2235
3075
  let response;
2236
3076
  const onAbort = () => {
@@ -2276,7 +3116,15 @@ var _LedgerAdapter = class _LedgerAdapter {
2276
3116
  if (err && typeof err === "object" && "code" in err && typeof err.code === "number") {
2277
3117
  const e = err;
2278
3118
  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);
3119
+ return ledgerFailure(
3120
+ e.code,
3121
+ e.message ?? "Unknown error",
3122
+ e.appName,
3123
+ tag,
3124
+ params,
3125
+ void 0,
3126
+ isHwkRecoveryHint(e.recovery) ? e.recovery : void 0
3127
+ );
2280
3128
  }
2281
3129
  const mapped = mapLedgerError(err);
2282
3130
  return ledgerFailure(mapped.code, mapped.message, mapped.appName, tag);
@@ -2303,7 +3151,7 @@ var _LedgerAdapter = class _LedgerAdapter {
2303
3151
  deviceId: device.deviceId,
2304
3152
  connectId: device.connectId,
2305
3153
  label: device.name,
2306
- connectionType: this.connector.connectionType,
3154
+ connectionType: device.connectionType ?? this.connector.connectionType,
2307
3155
  rssi: device.rssi,
2308
3156
  isConnectable: device.isConnectable,
2309
3157
  serialNumber: device.serialNumber,
@@ -2329,8 +3177,8 @@ var LedgerAdapter = _LedgerAdapter;
2329
3177
 
2330
3178
  // src/connector/LedgerConnectorBase.ts
2331
3179
  import {
2332
- EConnectorInteraction as EConnectorInteraction4,
2333
- HardwareErrorCode as HardwareErrorCode8,
3180
+ EConnectorInteraction as EConnectorInteraction3,
3181
+ HardwareErrorCode as HardwareErrorCode7,
2334
3182
  isKnownNonTargetHardwareVendor,
2335
3183
  serializeConnectorError
2336
3184
  } from "@onekeyfe/hwk-adapter-core";
@@ -2603,7 +3451,7 @@ var LedgerDeviceManager = class {
2603
3451
 
2604
3452
  // src/signer/SignerManager.ts
2605
3453
  import { SignerEthBuilder } from "@ledgerhq/device-signer-kit-ethereum";
2606
- import { ContextModuleBuilder } from "@ledgerhq/context-module";
3454
+ import { ContextModuleBuilder, ContextModuleChainID } from "@ledgerhq/context-module";
2607
3455
 
2608
3456
  // src/signer/SignerEth.ts
2609
3457
  import { hexToBytes } from "@onekeyfe/hwk-adapter-core";
@@ -2869,7 +3717,7 @@ var SignerManager = class _SignerManager {
2869
3717
  return (args) => new SignerEthBuilder(args);
2870
3718
  }
2871
3719
  static _createContextModule() {
2872
- const contextModule = new ContextModuleBuilder({}).removeDefaultLoaders().build();
3720
+ const contextModule = new ContextModuleBuilder({}).setChain(ContextModuleChainID.Ethereum).removeDefaultLoaders().build();
2873
3721
  return _SignerManager.wrapBlindSigningReportNonBlocking(contextModule);
2874
3722
  }
2875
3723
  static wrapBlindSigningReportNonBlocking(contextModule) {
@@ -2907,6 +3755,27 @@ function collapseSignerInteraction(interaction) {
2907
3755
  return EConnectorInteraction2.ConfirmOnDevice;
2908
3756
  }
2909
3757
  }
3758
+ function wireSignerToSession(ctx, sessionId, chain, signer) {
3759
+ signer.onInteraction = (interaction) => {
3760
+ debugLog(`[LedgerConnector] ${chain}.onInteraction:`, interaction);
3761
+ ctx.emit("ui-event", {
3762
+ type: collapseSignerInteraction(interaction),
3763
+ payload: { sessionId }
3764
+ });
3765
+ };
3766
+ signer.onRegisterCanceller = (cancel) => ctx.registerCanceller(sessionId, cancel);
3767
+ return signer;
3768
+ }
3769
+ async function runSignerCall(ctx, sessionId, call) {
3770
+ try {
3771
+ return await call();
3772
+ } catch (err) {
3773
+ ctx.invalidateSession(sessionId);
3774
+ throw ctx.wrapError(err);
3775
+ } finally {
3776
+ ctx.clearCanceller(sessionId);
3777
+ }
3778
+ }
2910
3779
 
2911
3780
  // src/connector/chains/evm.ts
2912
3781
  async function evmGetAddress(ctx, sessionId, params) {
@@ -3339,7 +4208,8 @@ function _applySignaturesToPsbt(psbtHex, signatures) {
3339
4208
  }
3340
4209
 
3341
4210
  // src/connector/chains/sol.ts
3342
- import { bytesToHex, hexToBytes as hexToBytes3 } from "@onekeyfe/hwk-adapter-core";
4211
+ import { bytesToHex, hexToBytes as hexToBytes3, prepareSolanaOffchainMessageV1 } from "@onekeyfe/hwk-adapter-core";
4212
+ import bs58 from "bs58";
3343
4213
 
3344
4214
  // src/signer/SignerSol.ts
3345
4215
  var SignerSol = class {
@@ -3423,8 +4293,21 @@ async function solSignMessage(ctx, sessionId, params) {
3423
4293
  const path = normalizePath(params.path);
3424
4294
  const messageBytes = hexToBytes3(params.message);
3425
4295
  try {
4296
+ if (params.messageVersion === 1) {
4297
+ const preparedMessage = prepareSolanaOffchainMessageV1({
4298
+ message: messageBytes,
4299
+ requiredSigners: params.requiredSigners
4300
+ });
4301
+ const { SignMessageVersion } = await ctx.importLedgerKit(
4302
+ "@ledgerhq/device-signer-kit-solana"
4303
+ );
4304
+ const result2 = await solSigner.signMessage(path, preparedMessage.serializedMessage, {
4305
+ version: SignMessageVersion.Raw
4306
+ });
4307
+ return { signature: decodeBase58Signature(result2.signature) };
4308
+ }
3426
4309
  const result = await solSigner.signMessage(path, messageBytes);
3427
- return { signature: result.signature };
4310
+ return { signature: decodeBase58EnvelopeSignature(result.signature) };
3428
4311
  } catch (err) {
3429
4312
  ctx.invalidateSession(sessionId);
3430
4313
  throw ctx.wrapError(err);
@@ -3432,11 +4315,27 @@ async function solSignMessage(ctx, sessionId, params) {
3432
4315
  ctx.clearCanceller(sessionId);
3433
4316
  }
3434
4317
  }
4318
+ function decodeBase58Signature(signature) {
4319
+ const bytes = bs58.decode(signature);
4320
+ if (bytes.length !== 64) {
4321
+ throw new Error(`Ledger Solana signature must be 64 bytes, received ${bytes.length}`);
4322
+ }
4323
+ return bytesToHex(bytes);
4324
+ }
4325
+ function decodeBase58EnvelopeSignature(envelope) {
4326
+ const bytes = bs58.decode(envelope);
4327
+ if (bytes.length < 65 || bytes[0] !== 1) {
4328
+ throw new Error("Ledger Solana signature envelope is invalid");
4329
+ }
4330
+ return bytesToHex(bytes.subarray(1, 65));
4331
+ }
3435
4332
  async function _createSolSigner(ctx, sessionId) {
3436
4333
  const dmk = await ctx.getOrCreateDmk();
3437
- const { ContextModuleBuilder: ContextModuleBuilder2 } = await ctx.importLedgerKit("@ledgerhq/context-module");
4334
+ const { ContextModuleBuilder: ContextModuleBuilder2, ContextModuleChainID: ContextModuleChainID2 } = await ctx.importLedgerKit(
4335
+ "@ledgerhq/context-module"
4336
+ );
3438
4337
  const { SignerSolanaBuilder } = await ctx.importLedgerKit("@ledgerhq/device-signer-kit-solana");
3439
- const contextModule = new ContextModuleBuilder2({}).removeDefaultLoaders().build();
4338
+ const contextModule = new ContextModuleBuilder2({}).setChain(ContextModuleChainID2.Solana).removeDefaultLoaders().build();
3440
4339
  const sdkSigner = new SignerSolanaBuilder({ dmk, sessionId }).withContextModule(contextModule).build();
3441
4340
  const signer = new SignerSol(sdkSigner);
3442
4341
  signer.onInteraction = (interaction) => {
@@ -3452,361 +4351,147 @@ async function _createSolSigner(ctx, sessionId) {
3452
4351
  return signer;
3453
4352
  }
3454
4353
 
3455
- // src/connector/chains/tron.ts
3456
- import { HardwareErrorCode as HardwareErrorCode7 } from "@onekeyfe/hwk-adapter-core";
3457
- import Trx from "@ledgerhq/hw-app-trx";
3458
-
3459
- // src/connector/chains/legacyChainCall.ts
3460
- import { EConnectorInteraction as EConnectorInteraction3 } from "@onekeyfe/hwk-adapter-core";
4354
+ // src/connector/chains/zcash.ts
4355
+ import { bytesToHex as bytesToHex2 } from "@onekeyfe/hwk-adapter-core";
3461
4356
 
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);
4357
+ // src/signer/SignerZcash.ts
4358
+ var SignerZcash = class {
4359
+ // eslint-disable-next-line no-useless-constructor, no-empty-function
4360
+ constructor(_sdk) {
4361
+ this._sdk = _sdk;
3528
4362
  }
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
- }
4363
+ /** GET_VK: UFVK string (default) or raw 96-byte Orchard FVK. */
4364
+ async getFullViewingKey(derivationPath, options) {
4365
+ const action = this._sdk.getFullViewingKey(derivationPath, options);
4366
+ return deviceActionToPromise(
4367
+ action,
4368
+ this.onInteraction,
4369
+ void 0,
4370
+ this.onRegisterCanceller
3565
4371
  );
3566
4372
  }
3567
- async _openApp(sessionId, appName) {
3568
- const result = await this._dmk.sendCommand({
3569
- sessionId,
3570
- command: new OpenAppCommand({ appName })
3571
- });
3572
- if (!isSuccessCommandResult(result)) {
3573
- const dmkErr = result.error;
3574
- const errorCode = "errorCode" in dmkErr && dmkErr.errorCode != null ? String(dmkErr.errorCode) : "";
3575
- const message = "message" in dmkErr && typeof dmkErr.message === "string" ? dmkErr.message : "";
3576
- debugLog(
3577
- "[AppManager] openApp failed:",
3578
- appName,
3579
- "errorCode:",
3580
- errorCode,
3581
- "tag:",
3582
- dmkErr._tag
3583
- );
3584
- let code;
3585
- if (errorCode === "6807" || /unknown application/i.test(message)) {
3586
- code = HardwareErrorCode6.AppNotInstalled;
3587
- } else if (errorCode === "5501" || dmkErr._tag === "ActionRefusedError") {
3588
- code = HardwareErrorCode6.UserRejected;
3589
- }
3590
- throw Object.assign(new Error(`Failed to open "${appName}"`), {
3591
- _tag: ERROR_TAG.OpenAppCommand,
3592
- code,
3593
- errorCode,
3594
- statusCode: errorCode,
3595
- appName,
3596
- originalError: dmkErr
3597
- });
3598
- }
3599
- }
3600
- async _closeCurrentApp(sessionId) {
3601
- debugLog("[AppManager] closeCurrentApp");
3602
- await this._dmk.sendCommand({
3603
- sessionId,
3604
- command: new CloseAppCommand()
3605
- });
3606
- }
3607
- /**
3608
- * Poll the device until the expected app is reported as running,
3609
- * or throw after `_maxRetries` attempts.
3610
- */
3611
- async _waitForApp(sessionId, expectedAppName) {
3612
- let lastSeen = "";
3613
- for (let i = 0; i < this._maxRetries; i++) {
3614
- await this._wait();
3615
- const current = await this._getCurrentApp(sessionId);
3616
- lastSeen = current;
3617
- if (current === expectedAppName) {
3618
- return;
3619
- }
3620
- }
3621
- debugLog(
3622
- "[AppManager] waitForApp exhausted: expected=",
3623
- expectedAppName,
3624
- "lastSeen=",
3625
- lastSeen
3626
- );
3627
- throw new Error(
3628
- `Ledger: failed to open "${expectedAppName}" after ${this._maxRetries} retries (last seen: ${lastSeen})`
4373
+ /** GET_SHIELDED_ADDRESS: single-Orchard-receiver UA for a 5-level transparent path. */
4374
+ async getShieldedAddress(derivationPath, options) {
4375
+ const action = this._sdk.getShieldedAddress(derivationPath, options);
4376
+ return deviceActionToPromise(
4377
+ action,
4378
+ this.onInteraction,
4379
+ void 0,
4380
+ this.onRegisterCanceller
3629
4381
  );
3630
4382
  }
3631
- _isDashboard(appName) {
3632
- return appName === DASHBOARD_APP_NAME;
3633
- }
3634
- _wait() {
3635
- return new Promise((resolve) => setTimeout(resolve, this._waitMs));
3636
- }
3637
4383
  };
3638
4384
 
3639
- // src/connector/chains/legacyChainCall.ts
3640
- function isLegacyWrongAppError(err, _appName) {
3641
- return isWrongAppError(err);
4385
+ // src/connector/chains/zcash.ts
4386
+ async function zcashGetFullViewingKey(ctx, sessionId, params) {
4387
+ const signer = await _createZcashSigner(ctx, sessionId);
4388
+ const path = normalizePath(params.path);
4389
+ const mode = params.mode ?? "ufvk";
4390
+ return runSignerCall(ctx, sessionId, async () => {
4391
+ const result = await signer.getFullViewingKey(path, { mode });
4392
+ if (result.mode === "ufvk") {
4393
+ return { path: params.path, mode, ufvk: result.fullViewingKey };
4394
+ }
4395
+ return { path: params.path, mode, orchardFvk: bytesToHex2(result.fullViewingKey) };
4396
+ });
3642
4397
  }
3643
- async function withLegacyChainCall(ctx, sessionId, options, action) {
3644
- const { appName, needsConfirmation } = options;
3645
- let openAppPromptShown = false;
3646
- const onAppOpenPrompt = () => {
3647
- openAppPromptShown = true;
3648
- ctx.emit("ui-event", {
3649
- type: EConnectorInteraction3.ConfirmOpenApp,
3650
- payload: { sessionId }
4398
+ async function zcashGetShieldedAddress(ctx, sessionId, params) {
4399
+ const signer = await _createZcashSigner(ctx, sessionId);
4400
+ const path = normalizePath(params.path);
4401
+ return runSignerCall(ctx, sessionId, async () => {
4402
+ const result = await signer.getShieldedAddress(path, {
4403
+ checkOnDevice: params.showOnDevice ?? false
3651
4404
  });
3652
- };
3653
- const closeOpenAppUiIfShown = () => {
3654
- if (openAppPromptShown) {
3655
- ctx.emit("ui-event", {
3656
- type: EConnectorInteraction3.InteractionComplete,
3657
- payload: { sessionId }
3658
- });
3659
- openAppPromptShown = false;
3660
- }
3661
- };
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
- }
4405
+ return { address: result.address, path: params.path };
4406
+ });
3719
4407
  }
3720
- async function _ensureAppOpen(ctx, sessionId, appName, onPrompt) {
4408
+ async function _createZcashSigner(ctx, sessionId) {
3721
4409
  const dmk = await ctx.getOrCreateDmk();
3722
- const appManager = new AppManager(dmk);
3723
- await appManager.ensureAppOpen(sessionId, appName, onPrompt);
4410
+ const { SignerZcashBuilder } = await ctx.importLedgerKit("@ledgerhq/device-signer-kit-zcash");
4411
+ const sdkSigner = new SignerZcashBuilder({ dmk, sessionId }).build();
4412
+ return wireSignerToSession(ctx, sessionId, "zcash", new SignerZcash(sdkSigner));
3724
4413
  }
3725
4414
 
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;
4415
+ // src/connector/chains/tron.ts
4416
+ import { HardwareErrorCode as HardwareErrorCode6, bytesToHex as bytesToHex3, hexToBytes as hexToBytes4 } from "@onekeyfe/hwk-adapter-core";
4417
+
4418
+ // src/signer/SignerTron.ts
4419
+ var SignerTron = class {
4420
+ // eslint-disable-next-line no-useless-constructor, no-empty-function
4421
+ constructor(_sdk) {
4422
+ this._sdk = _sdk;
4423
+ }
4424
+ /** Base58 Tron address plus the uncompressed public key at `derivationPath`. */
4425
+ async getAddress(derivationPath, options) {
4426
+ const action = this._sdk.getAddress(derivationPath, options);
4427
+ const result = await deviceActionToPromise(
4428
+ action,
4429
+ this.onInteraction,
4430
+ void 0,
4431
+ this.onRegisterCanceller
4432
+ );
4433
+ return { address: result.address, publicKey: result.publicKey };
3733
4434
  }
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;
4435
+ /** Sign a protobuf-encoded raw transaction. */
4436
+ async signTransaction(derivationPath, transaction, options) {
4437
+ const action = this._sdk.signTransaction(derivationPath, transaction, options);
4438
+ return deviceActionToPromise(
4439
+ action,
4440
+ this.onInteraction,
4441
+ void 0,
4442
+ this.onRegisterCanceller
4443
+ );
3746
4444
  }
3747
- async close() {
4445
+ /** Sign a personal message (TIP-191). */
4446
+ async signPersonalMessage(derivationPath, message, options) {
4447
+ const action = this._sdk.signPersonalMessage(derivationPath, message, options);
4448
+ return deviceActionToPromise(
4449
+ action,
4450
+ this.onInteraction,
4451
+ void 0,
4452
+ this.onRegisterCanceller
4453
+ );
3748
4454
  }
3749
4455
  };
3750
4456
 
3751
4457
  // src/connector/chains/tron.ts
3752
4458
  async function tronGetAddress(ctx, sessionId, params) {
4459
+ const tronSigner = await _createTronSigner(ctx, sessionId);
3753
4460
  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
- );
4461
+ return runSignerCall(ctx, sessionId, async () => {
4462
+ const result = await tronSigner.getAddress(path, {
4463
+ checkOnDevice: params.showOnDevice ?? false
4464
+ });
4465
+ return { address: result.address, publicKey: result.publicKey, path: params.path };
4466
+ });
3770
4467
  }
3771
4468
  async function tronSignTransaction(ctx, sessionId, params) {
3772
4469
  if (!params.rawTxHex) {
3773
4470
  throw Object.assign(
3774
4471
  new Error("TRON signing requires a protobuf-encoded raw transaction hex (rawTxHex)."),
3775
- { code: HardwareErrorCode7.InvalidParams }
4472
+ { code: HardwareErrorCode6.InvalidParams }
3776
4473
  );
3777
4474
  }
4475
+ const tronSigner = await _createTronSigner(ctx, sessionId);
3778
4476
  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
- );
4477
+ return runSignerCall(ctx, sessionId, async () => {
4478
+ const signature = await tronSigner.signTransaction(path, hexToBytes4(params.rawTxHex));
4479
+ return { signature: bytesToHex3(signature) };
4480
+ });
3793
4481
  }
3794
4482
  async function tronSignMessage(ctx, sessionId, params) {
4483
+ const tronSigner = await _createTronSigner(ctx, sessionId);
3795
4484
  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
- );
4485
+ return runSignerCall(ctx, sessionId, async () => {
4486
+ const signature = await tronSigner.signPersonalMessage(path, hexToBytes4(params.messageHex));
4487
+ return { signature: bytesToHex3(signature) };
4488
+ });
3806
4489
  }
3807
- async function _createTrx(ctx, sessionId) {
4490
+ async function _createTronSigner(ctx, sessionId) {
3808
4491
  const dmk = await ctx.getOrCreateDmk();
3809
- return new Trx(new DmkTransport(dmk, sessionId));
4492
+ const { SignerTrxBuilder } = await ctx.importLedgerKit("@ledgerhq/device-signer-kit-tron");
4493
+ const sdkSigner = new SignerTrxBuilder({ dmk, sessionId }).build();
4494
+ return wireSignerToSession(ctx, sessionId, "tron", new SignerTron(sdkSigner));
3810
4495
  }
3811
4496
 
3812
4497
  // src/device-apps/customActions.ts
@@ -3985,7 +4670,7 @@ var DeviceApps = class {
3985
4670
  seTargetId: v.seTargetId,
3986
4671
  mcuTargetId: v.mcuTargetId,
3987
4672
  seVersion: v.seVersion,
3988
- seFlagsHex: bytesToHex2(v.seFlags),
4673
+ seFlagsHex: bytesToHex4(v.seFlags),
3989
4674
  mcuSephVersion: v.mcuSephVersion,
3990
4675
  mcuBootloaderVersion: v.mcuBootloaderVersion,
3991
4676
  hwVersion: v.hwVersion
@@ -4059,7 +4744,7 @@ var DeviceApps = class {
4059
4744
  }
4060
4745
  }
4061
4746
  };
4062
- function bytesToHex2(bytes) {
4747
+ function bytesToHex4(bytes) {
4063
4748
  if (!bytes) return "";
4064
4749
  return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
4065
4750
  }
@@ -4102,10 +4787,11 @@ var METHOD_PREFIX_TO_APP_NAME = {
4102
4787
  evm: "Ethereum",
4103
4788
  btc: "Bitcoin",
4104
4789
  sol: "Solana",
4105
- tron: "Tron"
4790
+ tron: "Tron",
4791
+ zcash: "Zcash"
4106
4792
  };
4107
4793
  var HARDWARE_ERROR_CODE_VALUES = new Set(
4108
- Object.values(HardwareErrorCode8).filter((value) => typeof value === "number")
4794
+ Object.values(HardwareErrorCode7).filter((value) => typeof value === "number")
4109
4795
  );
4110
4796
  var BLE_CONNECT_SCAN_TIMEOUT_MS = 1500;
4111
4797
  var LEDGER_RELAY_ALLOWED_ROOT_DOMAINS = ["onekeytest.com", "onekey.com"];
@@ -4142,6 +4828,10 @@ async function defaultLedgerKitImporter(pkg) {
4142
4828
  return import("@ledgerhq/device-signer-kit-bitcoin");
4143
4829
  case "@ledgerhq/device-signer-kit-solana":
4144
4830
  return import("@ledgerhq/device-signer-kit-solana");
4831
+ case "@ledgerhq/device-signer-kit-tron":
4832
+ return import("@ledgerhq/device-signer-kit-tron");
4833
+ case "@ledgerhq/device-signer-kit-zcash":
4834
+ return import("@ledgerhq/device-signer-kit-zcash");
4145
4835
  case "@ledgerhq/context-module":
4146
4836
  return import("@ledgerhq/context-module");
4147
4837
  default:
@@ -4201,6 +4891,7 @@ var LedgerConnectorBase = class {
4201
4891
  this._ctx = {
4202
4892
  emit: (event, data) => this._emit(event, data),
4203
4893
  invalidateSession: (sid) => this._invalidateSession(sid),
4894
+ teardownSecureChannelSession: (sid) => this._teardownSecureChannelSession(sid),
4204
4895
  wrapError: (err, opts) => this._wrapError(err, opts),
4205
4896
  getOrCreateDmk: () => this._getOrCreateDmk(),
4206
4897
  getDeviceManager: () => this._getDeviceManager(),
@@ -4300,7 +4991,7 @@ var LedgerConnectorBase = class {
4300
4991
  `Ledger BLE connect did not return within ${HANG_CEILING_MS / 6e4}min \u2014 DMK hang fallback.`
4301
4992
  );
4302
4993
  err._tag = ERROR_TAG.BlePairingTimeout;
4303
- err.code = HardwareErrorCode8.BlePairingTimeout;
4994
+ err.code = HardwareErrorCode7.BlePairingTimeout;
4304
4995
  reject(err);
4305
4996
  }, HANG_CEILING_MS);
4306
4997
  });
@@ -4333,7 +5024,7 @@ var LedgerConnectorBase = class {
4333
5024
  "Ledger device is not currently advertising. Wake up and unlock the device, keep it nearby, then try again."
4334
5025
  );
4335
5026
  err._tag = ERROR_TAG.DeviceNotAdvertising;
4336
- err.code = HardwareErrorCode8.DeviceNotFound;
5027
+ err.code = HardwareErrorCode7.DeviceNotFound;
4337
5028
  throw err;
4338
5029
  };
4339
5030
  const doConnect = async (path) => {
@@ -4390,14 +5081,14 @@ var LedgerConnectorBase = class {
4390
5081
  this._resetSignersAndSessions();
4391
5082
  if (isLedgerBleConnectionType(this.connectionType)) {
4392
5083
  const tag = err?._tag;
4393
- if (isKnownConnectionTag(tag)) {
5084
+ if (isKnownConnectionTag(tag) && !isConnectionOpeningTag(tag)) {
4394
5085
  throw err;
4395
5086
  }
4396
5087
  const wrapped = new Error(
4397
5088
  "Ledger Bluetooth pairing failed. Make sure the device is unlocked and nearby, then try again."
4398
5089
  );
4399
5090
  wrapped._tag = ERROR_TAG.BleGattBondingFailed;
4400
- wrapped.code = HardwareErrorCode8.BlePairingTimeout;
5091
+ wrapped.code = HardwareErrorCode7.BlePairingTimeout;
4401
5092
  wrapped.originalError = err;
4402
5093
  throw wrapped;
4403
5094
  }
@@ -4475,7 +5166,7 @@ var LedgerConnectorBase = class {
4475
5166
  this._unwatchSessionState(sessionId);
4476
5167
  this._signerManager?.invalidate(sessionId);
4477
5168
  this._cancellers.get(sessionId)?.({
4478
- code: HardwareErrorCode8.DeviceDisconnected,
5169
+ code: HardwareErrorCode7.DeviceDisconnected,
4479
5170
  tag: "DeviceDisconnected",
4480
5171
  message: "Device disconnected"
4481
5172
  });
@@ -4501,7 +5192,7 @@ var LedgerConnectorBase = class {
4501
5192
  success: false,
4502
5193
  error: serializeConnectorError(
4503
5194
  Object.assign(new Error("Ledger app is unresponsive"), {
4504
- code: HardwareErrorCode8.DeviceAppStuck,
5195
+ code: HardwareErrorCode7.DeviceAppStuck,
4505
5196
  _tag: ERROR_TAG.DeviceAppStuck,
4506
5197
  originalError: err
4507
5198
  })
@@ -4513,7 +5204,7 @@ var LedgerConnectorBase = class {
4513
5204
  success: false,
4514
5205
  error: serializeConnectorError(
4515
5206
  Object.assign(new Error("Device communication interrupted, please retry"), {
4516
- code: HardwareErrorCode8.TransportError,
5207
+ code: HardwareErrorCode7.TransportError,
4517
5208
  _tag: ERROR_TAG.DeviceTransportStuck,
4518
5209
  originalError: err
4519
5210
  })
@@ -4559,6 +5250,11 @@ var LedgerConnectorBase = class {
4559
5250
  return solSignTransaction(ctx, sessionId, params);
4560
5251
  case "solSignMessage":
4561
5252
  return solSignMessage(ctx, sessionId, params);
5253
+ // ZCASH
5254
+ case "zcashGetFullViewingKey":
5255
+ return zcashGetFullViewingKey(ctx, sessionId, params);
5256
+ case "zcashGetShieldedAddress":
5257
+ return zcashGetShieldedAddress(ctx, sessionId, params);
4562
5258
  // TRON
4563
5259
  case "tronGetAddress":
4564
5260
  return tronGetAddress(ctx, sessionId, params);
@@ -4583,7 +5279,7 @@ var LedgerConnectorBase = class {
4583
5279
  try {
4584
5280
  return await apps.install(p.appName, ({ progress }) => {
4585
5281
  ctx.emit("ui-event", {
4586
- type: EConnectorInteraction4.AppInstallProgress,
5282
+ type: EConnectorInteraction3.AppInstallProgress,
4587
5283
  payload: {
4588
5284
  sessionId,
4589
5285
  appName: p.appName,
@@ -4592,7 +5288,7 @@ var LedgerConnectorBase = class {
4592
5288
  });
4593
5289
  });
4594
5290
  } catch (err) {
4595
- ctx.invalidateSession(sessionId);
5291
+ ctx.teardownSecureChannelSession(sessionId);
4596
5292
  throw ctx.wrapError(err);
4597
5293
  } finally {
4598
5294
  ctx.clearCanceller(sessionId);
@@ -4682,7 +5378,7 @@ var LedgerConnectorBase = class {
4682
5378
  );
4683
5379
  return { isGenuine: output.isGenuine, deviceId };
4684
5380
  } catch (err) {
4685
- ctx.invalidateSession(sessionId);
5381
+ ctx.teardownSecureChannelSession(sessionId);
4686
5382
  throw ctx.wrapError(err);
4687
5383
  } finally {
4688
5384
  ctx.clearCanceller(sessionId);
@@ -4801,9 +5497,26 @@ var LedgerConnectorBase = class {
4801
5497
  }
4802
5498
  return this._deviceAppsManager;
4803
5499
  }
5500
+ // DeviceAppsManager is a per-call factory with no cached session state, so
5501
+ // there is nothing to invalidate for it here.
4804
5502
  _invalidateSession(sessionId) {
4805
5503
  this._signerManager?.invalidate(sessionId);
4806
5504
  }
5505
+ /**
5506
+ * Fire the canceller so DMK closes the secure-channel WebSocket. The DMK device
5507
+ * session is kept: unlock/retry recovery reuses it.
5508
+ */
5509
+ _teardownSecureChannelSession(sessionId) {
5510
+ const cancel = this._cancellers.get(sessionId);
5511
+ this._cancellers.delete(sessionId);
5512
+ if (cancel) {
5513
+ try {
5514
+ cancel();
5515
+ } catch {
5516
+ }
5517
+ }
5518
+ this._invalidateSession(sessionId);
5519
+ }
4807
5520
  /**
4808
5521
  * Replace an old session with a new one after app switch.
4809
5522
  * Emits device-connect so the adapter updates its _sessions Map.
@@ -4888,7 +5601,7 @@ var LedgerConnectorBase = class {
4888
5601
  * at every catch site. Falls through unchanged for unknown methods.
4889
5602
  */
4890
5603
  _ctxForMethod(method) {
4891
- const prefix = /^(evm|btc|sol|tron)/.exec(method)?.[1];
5604
+ const prefix = /^(evm|btc|sol|tron|zcash)/.exec(method)?.[1];
4892
5605
  const defaultAppName = prefix ? METHOD_PREFIX_TO_APP_NAME[prefix] : void 0;
4893
5606
  if (!defaultAppName) return this._ctx;
4894
5607
  return {
@@ -4922,6 +5635,209 @@ var LedgerConnectorBase = class {
4922
5635
  return error;
4923
5636
  }
4924
5637
  };
5638
+
5639
+ // src/transport/DmkTransport.ts
5640
+ import Transport from "@ledgerhq/hw-transport";
5641
+ var DmkTransport = class extends Transport {
5642
+ constructor(dmk, sessionId) {
5643
+ super();
5644
+ this._dmk = dmk;
5645
+ this._sessionId = sessionId;
5646
+ }
5647
+ async exchange(apdu) {
5648
+ const response = await this._dmk.sendApdu({
5649
+ sessionId: this._sessionId,
5650
+ apdu: new Uint8Array(apdu)
5651
+ });
5652
+ const { data, statusCode } = response;
5653
+ const result = Buffer.alloc(data.length + 2);
5654
+ if (data.length > 0) {
5655
+ result.set(data, 0);
5656
+ }
5657
+ result.set(statusCode, data.length);
5658
+ return result;
5659
+ }
5660
+ async close() {
5661
+ }
5662
+ };
5663
+
5664
+ // src/app/AppManager.ts
5665
+ import {
5666
+ CloseAppCommand,
5667
+ GetAppAndVersionCommand,
5668
+ OpenAppCommand,
5669
+ isSuccessCommandResult
5670
+ } from "@ledgerhq/device-management-kit";
5671
+ import { HardwareErrorCode as HardwareErrorCode8 } from "@onekeyfe/hwk-adapter-core";
5672
+ var APP_NAME_MAP = {
5673
+ ETH: "Ethereum",
5674
+ BTC: "Bitcoin",
5675
+ SOL: "Solana",
5676
+ TRX: "Tron",
5677
+ ZEC: "Zcash",
5678
+ XRP: "XRP",
5679
+ ADA: "Cardano",
5680
+ DOT: "Polkadot",
5681
+ ATOM: "Cosmos"
5682
+ };
5683
+ var DASHBOARD_APP_NAME = "BOLOS";
5684
+ var AppManager = class {
5685
+ constructor(dmk, options) {
5686
+ this._dmk = dmk;
5687
+ this._waitMs = options?.waitMs ?? 1e3;
5688
+ this._maxRetries = options?.maxRetries ?? 10;
5689
+ }
5690
+ /**
5691
+ * Return the Ledger app name for a given chain ticker,
5692
+ * or undefined if the chain is not supported.
5693
+ */
5694
+ static getAppName(chain) {
5695
+ return APP_NAME_MAP[chain];
5696
+ }
5697
+ /**
5698
+ * Ensure the target app is open on the device identified by `sessionId`.
5699
+ *
5700
+ * Flow:
5701
+ * 1. Check the currently running app.
5702
+ * 2. If it is already the target, return immediately.
5703
+ * 3. If a different app is running (not dashboard), close it first.
5704
+ * 4. Open the target app.
5705
+ * 5. Poll until the device confirms the target app is running.
5706
+ */
5707
+ /**
5708
+ * @param onConfirmOnDevice Called BEFORE OpenAppCommand is issued — the
5709
+ * device is about to display "Open <app>" on screen and wait for the
5710
+ * user's button press. UI consumers should show their "open app" prompt
5711
+ * in response. NOT called when the target app is already open (no user
5712
+ * interaction needed in that case).
5713
+ *
5714
+ * Important: OpenAppCommand is blocking. It does not resolve until the user
5715
+ * has physically confirmed on the device, so anything that runs AFTER
5716
+ * `await this._openApp(...)` lands AFTER the prompt is already gone.
5717
+ * Hence the callback must fire BEFORE that await.
5718
+ */
5719
+ async ensureAppOpen(sessionId, targetAppName, onConfirmOnDevice) {
5720
+ const currentApp = await this._getCurrentApp(sessionId);
5721
+ if (currentApp === targetAppName) {
5722
+ return;
5723
+ }
5724
+ if (!this._isDashboard(currentApp)) {
5725
+ await this._closeCurrentApp(sessionId);
5726
+ await this._waitForApp(sessionId, DASHBOARD_APP_NAME);
5727
+ }
5728
+ onConfirmOnDevice?.();
5729
+ await this._openApp(sessionId, targetAppName);
5730
+ await this._waitForApp(sessionId, targetAppName);
5731
+ }
5732
+ // ---------------------------------------------------------------------------
5733
+ // Private helpers
5734
+ // ---------------------------------------------------------------------------
5735
+ async _getCurrentApp(sessionId) {
5736
+ const result = await this._dmk.sendCommand({
5737
+ sessionId,
5738
+ command: new GetAppAndVersionCommand()
5739
+ });
5740
+ if (isSuccessCommandResult(result)) {
5741
+ debugLog("[AppManager] currentApp:", result.data.name);
5742
+ return result.data.name;
5743
+ }
5744
+ const errResult = result;
5745
+ const dmkErr = errResult.error ?? {};
5746
+ const original = dmkErr.originalError;
5747
+ debugLog(
5748
+ "[AppManager] _getCurrentApp failed sessionId=",
5749
+ sessionId,
5750
+ "tag=",
5751
+ dmkErr._tag,
5752
+ "errorCode=",
5753
+ dmkErr.errorCode,
5754
+ "message=",
5755
+ dmkErr.message,
5756
+ "originalErrorMessage=",
5757
+ original?.message ?? String(original ?? "")
5758
+ );
5759
+ throw Object.assign(
5760
+ new Error(
5761
+ dmkErr.message ?? "Failed to get current app from device"
5762
+ ),
5763
+ {
5764
+ _tag: dmkErr._tag,
5765
+ errorCode: dmkErr.errorCode,
5766
+ originalError: original
5767
+ }
5768
+ );
5769
+ }
5770
+ async _openApp(sessionId, appName) {
5771
+ const result = await this._dmk.sendCommand({
5772
+ sessionId,
5773
+ command: new OpenAppCommand({ appName })
5774
+ });
5775
+ if (!isSuccessCommandResult(result)) {
5776
+ const dmkErr = result.error;
5777
+ const errorCode = "errorCode" in dmkErr && dmkErr.errorCode != null ? String(dmkErr.errorCode) : "";
5778
+ const message = "message" in dmkErr && typeof dmkErr.message === "string" ? dmkErr.message : "";
5779
+ debugLog(
5780
+ "[AppManager] openApp failed:",
5781
+ appName,
5782
+ "errorCode:",
5783
+ errorCode,
5784
+ "tag:",
5785
+ dmkErr._tag
5786
+ );
5787
+ let code;
5788
+ if (errorCode === "6807" || /unknown application/i.test(message)) {
5789
+ code = HardwareErrorCode8.AppNotInstalled;
5790
+ } else if (errorCode === "5501" || dmkErr._tag === "ActionRefusedError") {
5791
+ code = HardwareErrorCode8.UserRejected;
5792
+ }
5793
+ throw Object.assign(new Error(`Failed to open "${appName}"`), {
5794
+ _tag: ERROR_TAG.OpenAppCommand,
5795
+ code,
5796
+ errorCode,
5797
+ statusCode: errorCode,
5798
+ appName,
5799
+ originalError: dmkErr
5800
+ });
5801
+ }
5802
+ }
5803
+ async _closeCurrentApp(sessionId) {
5804
+ debugLog("[AppManager] closeCurrentApp");
5805
+ await this._dmk.sendCommand({
5806
+ sessionId,
5807
+ command: new CloseAppCommand()
5808
+ });
5809
+ }
5810
+ /**
5811
+ * Poll the device until the expected app is reported as running,
5812
+ * or throw after `_maxRetries` attempts.
5813
+ */
5814
+ async _waitForApp(sessionId, expectedAppName) {
5815
+ let lastSeen = "";
5816
+ for (let i = 0; i < this._maxRetries; i++) {
5817
+ await this._wait();
5818
+ const current = await this._getCurrentApp(sessionId);
5819
+ lastSeen = current;
5820
+ if (current === expectedAppName) {
5821
+ return;
5822
+ }
5823
+ }
5824
+ debugLog(
5825
+ "[AppManager] waitForApp exhausted: expected=",
5826
+ expectedAppName,
5827
+ "lastSeen=",
5828
+ lastSeen
5829
+ );
5830
+ throw new Error(
5831
+ `Ledger: failed to open "${expectedAppName}" after ${this._maxRetries} retries (last seen: ${lastSeen})`
5832
+ );
5833
+ }
5834
+ _isDashboard(appName) {
5835
+ return appName === DASHBOARD_APP_NAME;
5836
+ }
5837
+ _wait() {
5838
+ return new Promise((resolve) => setTimeout(resolve, this._waitMs));
5839
+ }
5840
+ };
4925
5841
  export {
4926
5842
  AppManager,
4927
5843
  DmkTransport,
@@ -4932,6 +5848,7 @@ export {
4932
5848
  SignerEth,
4933
5849
  SignerManager,
4934
5850
  SignerSol,
5851
+ SignerZcash,
4935
5852
  debugLog,
4936
5853
  deviceActionToPromise,
4937
5854
  isDeviceLockedError,