@onekeyfe/hwk-ledger-adapter 1.1.26-alpha.9 → 1.1.26-patch.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -39,13 +39,16 @@ __export(index_exports, {
39
39
  SignerEth: () => SignerEth,
40
40
  SignerManager: () => SignerManager,
41
41
  SignerSol: () => SignerSol,
42
+ debugLog: () => debugLog,
42
43
  deviceActionToPromise: () => deviceActionToPromise,
43
- extractBleHexId: () => extractBleHexId,
44
- isDebugEnabled: () => isDebugEnabled,
45
44
  isDeviceLockedError: () => isDeviceLockedError,
45
+ isLedgerBleConnectionType: () => isLedgerBleConnectionType,
46
+ isLedgerBleDescriptor: () => isLedgerBleDescriptor,
47
+ isLedgerDmkBleTransport: () => isLedgerDmkBleTransport,
46
48
  ledgerFailure: () => ledgerFailure,
47
49
  mapLedgerError: () => mapLedgerError,
48
- setDebugEnabled: () => setDebugEnabled
50
+ offSdkEvent: () => offSdkEvent,
51
+ onSdkEvent: () => onSdkEvent
49
52
  });
50
53
  module.exports = __toCommonJS(index_exports);
51
54
 
@@ -54,24 +57,32 @@ var import_hwk_adapter_core2 = require("@onekeyfe/hwk-adapter-core");
54
57
 
55
58
  // src/errors.ts
56
59
  var import_hwk_adapter_core = require("@onekeyfe/hwk-adapter-core");
57
- function ledgerFailure(code, error, appName) {
60
+ var MULTIPLE_USB_LEDGER_DEVICES_ERROR_MESSAGE = "Multiple Ledger USB devices are connected. Please connect only one Ledger device and try again.";
61
+ function createMultipleUsbLedgerDevicesError() {
62
+ return Object.assign(new Error(MULTIPLE_USB_LEDGER_DEVICES_ERROR_MESSAGE), {
63
+ code: import_hwk_adapter_core.HardwareErrorCode.DeviceOneDeviceOnly
64
+ });
65
+ }
66
+ function ledgerFailure(code, error, appName, tag, params) {
58
67
  const payload = { error, code };
59
68
  if (appName !== void 0) payload.appName = appName;
69
+ if (tag !== void 0) payload._tag = tag;
70
+ if (params !== void 0) payload.params = params;
60
71
  return { success: false, payload };
61
72
  }
62
73
  var LOCKED_ERROR_CODES = /* @__PURE__ */ new Set(["5515", "21781", "6982", "27010", "5303", "21251"]);
63
74
  var USER_REJECTED_CODES = /* @__PURE__ */ new Set(["6985", "27013"]);
64
75
  var WRONG_APP_CODES = /* @__PURE__ */ new Set(["6e00", "28160", "6d00", "27904", "6a83", "27267"]);
65
76
  var APP_NOT_INSTALLED_CODES = /* @__PURE__ */ new Set(["6807", "26631"]);
66
- var STEP_BLIND_SIGN_FALLBACK = "signer.eth.steps.blindSignTransactionFallback";
77
+ var STEP_BLIND_SIGN_TRANSACTION_FALLBACK = "signer.eth.steps.blindSignTransactionFallback";
67
78
  function getEthAppErrorCode(err) {
68
79
  if (!err || typeof err !== "object") return null;
69
80
  const e = err;
70
- if (e._tag === "EthAppCommandError" && typeof e.errorCode === "string") {
81
+ if (e._tag === ERROR_TAG.EthAppCommand && typeof e.errorCode === "string") {
71
82
  return e.errorCode.toLowerCase();
72
83
  }
73
84
  const orig = e.originalError;
74
- if (orig?._tag === "EthAppCommandError" && typeof orig.errorCode === "string") {
85
+ if (orig?._tag === ERROR_TAG.EthAppCommand && typeof orig.errorCode === "string") {
75
86
  return orig.errorCode.toLowerCase();
76
87
  }
77
88
  return null;
@@ -128,13 +139,8 @@ function mapBtcAppError(hex) {
128
139
  return null;
129
140
  }
130
141
  }
131
- function mapEthAppError(ethCode, lastStep) {
142
+ function mapEthAppErrorCode(ethCode) {
132
143
  switch (ethCode) {
133
- case "6a80":
134
- if (lastStep === STEP_BLIND_SIGN_FALLBACK) {
135
- return import_hwk_adapter_core.HardwareErrorCode.EvmBlindSigningRequired;
136
- }
137
- return null;
138
144
  case "6984":
139
145
  return import_hwk_adapter_core.HardwareErrorCode.EvmClearSignPluginMissing;
140
146
  case "6a84":
@@ -147,16 +153,133 @@ function mapEthAppError(ethCode, lastStep) {
147
153
  return null;
148
154
  }
149
155
  }
156
+ function classifyEthAppError(err) {
157
+ const ethCode = getEthAppErrorCode(err);
158
+ if (!ethCode) return null;
159
+ if (ethCode === "6a80") {
160
+ return hasBlindSignFallbackStep(err) ? import_hwk_adapter_core.HardwareErrorCode.EvmBlindSigningRequired : null;
161
+ }
162
+ return mapEthAppErrorCode(ethCode);
163
+ }
164
+ var ERROR_TAG = {
165
+ // SDK-mint
166
+ DeviceNotAdvertising: "DeviceNotAdvertisingError",
167
+ // BLE scan miss
168
+ DeviceNotInDiscoveryCache: "DeviceNotInDiscoveryCacheError",
169
+ // dm.connect() before enumerate
170
+ BlePairingTimeout: "BlePairingTimeoutError",
171
+ // SMP 30s timeout
172
+ BleGattBondingFailed: "BleGattBondingFailedError",
173
+ // other GATT failure
174
+ UserAborted: "UserAborted",
175
+ DeviceAppStuck: "DeviceAppStuck",
176
+ // chain app wedged (APDU 0x6901)
177
+ DeviceTransportStuck: "DeviceTransportStuck",
178
+ // DMK transport queue wedged
179
+ // DMK-reuse (DMK throws same string; we synthesize too)
180
+ DeviceLocked: "DeviceLockedError",
181
+ DeviceNotRecognized: "DeviceNotRecognizedError",
182
+ DeviceSessionNotFound: "DeviceSessionNotFound",
183
+ OpenAppCommand: "OpenAppCommandError",
184
+ // DMK-only (read only)
185
+ EthAppCommand: "EthAppCommandError",
186
+ UserRefusedOnDevice: "UserRefusedOnDevice",
187
+ WrongAppOpened: "WrongAppOpenedError",
188
+ InvalidStatusWord: "InvalidStatusWordError",
189
+ AlreadySendingApdu: "AlreadySendingApduError",
190
+ UnknownDeviceExchange: "UnknownDeviceExchangeError",
191
+ NoAccessibleDevice: "NoAccessibleDeviceError",
192
+ UnknownDevice: "UnknownDeviceError",
193
+ DeviceSessionRefresher: "DeviceSessionRefresherError",
194
+ DeviceNotInitialized: "DeviceNotInitializedError",
195
+ OpeningConnection: "OpeningConnectionError",
196
+ DeviceDisconnectedBeforeSendingApdu: "DeviceDisconnectedBeforeSendingApdu",
197
+ DeviceDisconnectedWhileSending: "DeviceDisconnectedWhileSendingError",
198
+ Disconnect: "DisconnectError",
199
+ ReconnectionFailed: "ReconnectionFailedError",
200
+ WebHIDDisconnect: "WebHIDDisconnectError",
201
+ // ble-plx surfaces this when GATT notification setup fails — typical
202
+ // outcome when the user doesn't confirm pairing on the device, or the
203
+ // existing bond is invalid. Observed in production after ~30s.
204
+ PairingRefused: "PairingRefusedError"
205
+ };
150
206
  function isDeviceLockedError(err) {
151
207
  if (!err || typeof err !== "object") return false;
152
208
  const e = err;
153
209
  if (e.errorCode != null && LOCKED_ERROR_CODES.has(String(e.errorCode))) return true;
154
210
  if (e.statusCode != null && LOCKED_ERROR_CODES.has(String(e.statusCode))) return true;
155
- if (e._tag === "DeviceLockedError") return true;
211
+ if (e._tag === ERROR_TAG.DeviceLocked) return true;
156
212
  if (e.originalError != null && isDeviceLockedError(e.originalError)) return true;
157
213
  if (e.error != null && e._tag && isDeviceLockedError(e.error)) return true;
158
214
  return false;
159
215
  }
216
+ function isDeviceNotAdvertisingError(err) {
217
+ if (!err || typeof err !== "object") return false;
218
+ return err._tag === ERROR_TAG.DeviceNotAdvertising;
219
+ }
220
+ var PAIRING_FAILURE_TAGS = /* @__PURE__ */ new Set([
221
+ ERROR_TAG.BlePairingTimeout,
222
+ ERROR_TAG.BleGattBondingFailed,
223
+ ERROR_TAG.PairingRefused
224
+ ]);
225
+ function isBlePairingFailureError(err) {
226
+ if (!err || typeof err !== "object") return false;
227
+ const tag = err._tag;
228
+ if (tag && PAIRING_FAILURE_TAGS.has(tag)) return true;
229
+ const orig = err.originalError;
230
+ if (orig != null && isBlePairingFailureError(orig)) return true;
231
+ return false;
232
+ }
233
+ var CONNECTION_LEVEL_TAGS = /* @__PURE__ */ new Set([
234
+ ERROR_TAG.DeviceLocked,
235
+ ERROR_TAG.DeviceNotAdvertising,
236
+ ERROR_TAG.BlePairingTimeout,
237
+ ERROR_TAG.BleGattBondingFailed,
238
+ ERROR_TAG.PairingRefused,
239
+ ERROR_TAG.DeviceNotRecognized,
240
+ ERROR_TAG.NoAccessibleDevice,
241
+ ERROR_TAG.UnknownDevice,
242
+ ERROR_TAG.DeviceSessionNotFound,
243
+ ERROR_TAG.DeviceSessionRefresher,
244
+ ERROR_TAG.DeviceNotInitialized,
245
+ ERROR_TAG.OpeningConnection,
246
+ ERROR_TAG.DeviceDisconnectedBeforeSendingApdu,
247
+ ERROR_TAG.DeviceDisconnectedWhileSending,
248
+ ERROR_TAG.Disconnect,
249
+ ERROR_TAG.ReconnectionFailed,
250
+ ERROR_TAG.WebHIDDisconnect
251
+ ]);
252
+ var DEVICE_NOT_FOUND_TAGS = /* @__PURE__ */ new Set([
253
+ ERROR_TAG.NoAccessibleDevice,
254
+ ERROR_TAG.UnknownDevice,
255
+ ERROR_TAG.DeviceNotInitialized,
256
+ // SDK-internal: dm.connect() called before _discovered was populated.
257
+ // Map to DeviceNotFound so non-BLE-direct paths get a sensible error code.
258
+ ERROR_TAG.DeviceNotInDiscoveryCache
259
+ ]);
260
+ var DEVICE_BUSY_TAGS = /* @__PURE__ */ new Set([ERROR_TAG.OpeningConnection]);
261
+ var DEVICE_DISCONNECTED_TAGS = /* @__PURE__ */ new Set([
262
+ ERROR_TAG.DeviceNotRecognized,
263
+ ERROR_TAG.DeviceSessionNotFound,
264
+ ERROR_TAG.DeviceSessionRefresher,
265
+ ERROR_TAG.DeviceDisconnectedBeforeSendingApdu,
266
+ ERROR_TAG.DeviceDisconnectedWhileSending,
267
+ ERROR_TAG.Disconnect,
268
+ ERROR_TAG.ReconnectionFailed,
269
+ ERROR_TAG.WebHIDDisconnect
270
+ ]);
271
+ function isConnectionLevelError(err) {
272
+ if (!err || typeof err !== "object") return false;
273
+ const tag = err._tag;
274
+ if (tag && CONNECTION_LEVEL_TAGS.has(tag)) return true;
275
+ const e = err;
276
+ if (e.originalError != null && isConnectionLevelError(e.originalError)) return true;
277
+ if (e.error != null && e._tag && isConnectionLevelError(e.error)) return true;
278
+ return false;
279
+ }
280
+ function isKnownConnectionTag(tag) {
281
+ return typeof tag === "string" && CONNECTION_LEVEL_TAGS.has(tag);
282
+ }
160
283
  function hasStatusCode(err, codeSet) {
161
284
  if (!err || typeof err !== "object") return false;
162
285
  const e = err;
@@ -166,10 +289,39 @@ function hasStatusCode(err, codeSet) {
166
289
  if (e.error != null && e._tag && hasStatusCode(e.error, codeSet)) return true;
167
290
  return false;
168
291
  }
292
+ function hasBlindSignFallbackStep(err) {
293
+ if (!err || typeof err !== "object") return false;
294
+ const e = err;
295
+ if (e._lastStep === STEP_BLIND_SIGN_TRANSACTION_FALLBACK) return true;
296
+ if (Array.isArray(e._deviceActionSteps) && e._deviceActionSteps.includes(STEP_BLIND_SIGN_TRANSACTION_FALLBACK)) {
297
+ return true;
298
+ }
299
+ if (e.originalError != null && hasBlindSignFallbackStep(e.originalError)) return true;
300
+ if (e.error != null && e._tag && hasBlindSignFallbackStep(e.error)) return true;
301
+ return false;
302
+ }
303
+ function isDeviceNotFoundError(err) {
304
+ if (!err || typeof err !== "object") return false;
305
+ const tag = err._tag;
306
+ if (tag && DEVICE_NOT_FOUND_TAGS.has(tag)) return true;
307
+ const e = err;
308
+ if (e.originalError != null && isDeviceNotFoundError(e.originalError)) return true;
309
+ if (e.error != null && e._tag && isDeviceNotFoundError(e.error)) return true;
310
+ return false;
311
+ }
312
+ function isDeviceBusyError(err) {
313
+ if (!err || typeof err !== "object") return false;
314
+ const tag = err._tag;
315
+ if (tag && DEVICE_BUSY_TAGS.has(tag)) return true;
316
+ const e = err;
317
+ if (e.originalError != null && isDeviceBusyError(e.originalError)) return true;
318
+ if (e.error != null && e._tag && isDeviceBusyError(e.error)) return true;
319
+ return false;
320
+ }
169
321
  function isUserRejectedError(err) {
170
322
  if (!err || typeof err !== "object") return false;
171
323
  const e = err;
172
- if (e._tag === "UserRefusedOnDevice") return true;
324
+ if (e._tag === ERROR_TAG.UserRefusedOnDevice) return true;
173
325
  if (typeof e.message === "string" && /denied|rejected|refused/i.test(e.message)) return true;
174
326
  if (hasStatusCode(err, USER_REJECTED_CODES)) return true;
175
327
  return false;
@@ -177,7 +329,7 @@ function isUserRejectedError(err) {
177
329
  function isWrongAppError(err) {
178
330
  if (!err || typeof err !== "object") return false;
179
331
  const e = err;
180
- if (e._tag === "WrongAppOpenedError" || e._tag === "InvalidStatusWordError") {
332
+ if (e._tag === ERROR_TAG.WrongAppOpened || e._tag === ERROR_TAG.InvalidStatusWord) {
181
333
  if (hasStatusCode(err, WRONG_APP_CODES)) return true;
182
334
  }
183
335
  if (typeof e.message === "string") {
@@ -191,7 +343,6 @@ function isWrongAppError(err) {
191
343
  function isAppNotInstalledError(err) {
192
344
  if (!err || typeof err !== "object") return false;
193
345
  const e = err;
194
- if (e._tag === "OpenAppCommandError") return true;
195
346
  if (typeof e.message === "string" && /unknown application/i.test(e.message)) return true;
196
347
  if (hasStatusCode(err, APP_NOT_INSTALLED_CODES)) return true;
197
348
  return false;
@@ -199,7 +350,8 @@ function isAppNotInstalledError(err) {
199
350
  function isDeviceDisconnectedError(err) {
200
351
  if (!err || typeof err !== "object") return false;
201
352
  const e = err;
202
- if (e._tag === "DeviceNotRecognizedError" || e._tag === "DeviceSessionNotFound") return true;
353
+ const tag = e._tag;
354
+ if (typeof tag === "string" && DEVICE_DISCONNECTED_TAGS.has(tag)) return true;
203
355
  if (typeof e.message === "string") {
204
356
  const msg = e.message.toLowerCase();
205
357
  if (msg.includes("disconnected") || msg.includes("not found") || msg.includes("no device") || msg.includes("unplugged"))
@@ -219,6 +371,21 @@ function isTimeoutError(err) {
219
371
  if (e.code === import_hwk_adapter_core.HardwareErrorCode.OperationTimeout) return true;
220
372
  return false;
221
373
  }
374
+ function isAppStuckByApdu(err) {
375
+ if (!err || typeof err !== "object") return false;
376
+ const tag = err._tag;
377
+ if (tag === ERROR_TAG.DeviceAppStuck) return true;
378
+ return extractApduHex(err) === "6901";
379
+ }
380
+ function isTransportStuck(err) {
381
+ if (!err || typeof err !== "object") return false;
382
+ const tag = err._tag;
383
+ return tag === ERROR_TAG.DeviceTransportStuck || // already wrapped form
384
+ tag === ERROR_TAG.UnknownDeviceExchange || tag === ERROR_TAG.AlreadySendingApdu;
385
+ }
386
+ function isStuckAppStateError(err) {
387
+ return isAppStuckByApdu(err) || isTransportStuck(err);
388
+ }
222
389
  function mapLedgerError(err, opts) {
223
390
  let originalMessage = "Unknown Ledger error";
224
391
  if (err instanceof Error) {
@@ -230,20 +397,24 @@ function mapLedgerError(err, opts) {
230
397
  let code;
231
398
  if (isDeviceLockedError(err)) {
232
399
  code = import_hwk_adapter_core.HardwareErrorCode.DeviceLocked;
400
+ } else if (isDeviceNotAdvertisingError(err) || isDeviceNotFoundError(err)) {
401
+ code = import_hwk_adapter_core.HardwareErrorCode.DeviceNotFound;
402
+ } else if (isDeviceBusyError(err)) {
403
+ code = import_hwk_adapter_core.HardwareErrorCode.DeviceBusy;
404
+ } else if (isBlePairingFailureError(err)) {
405
+ code = import_hwk_adapter_core.HardwareErrorCode.BlePairingTimeout;
233
406
  } else if (isUserRejectedError(err)) {
234
407
  code = import_hwk_adapter_core.HardwareErrorCode.UserRejected;
235
408
  } else if (isWrongAppError(err)) {
236
409
  code = import_hwk_adapter_core.HardwareErrorCode.WrongApp;
237
410
  } else if (isAppNotInstalledError(err)) {
238
- code = import_hwk_adapter_core.HardwareErrorCode.AppNotOpen;
411
+ code = import_hwk_adapter_core.HardwareErrorCode.AppNotInstalled;
239
412
  } else if (isDeviceDisconnectedError(err)) {
240
413
  code = import_hwk_adapter_core.HardwareErrorCode.DeviceDisconnected;
241
414
  } else if (isTimeoutError(err)) {
242
415
  code = import_hwk_adapter_core.HardwareErrorCode.OperationTimeout;
243
416
  } else {
244
- const ethCode = getEthAppErrorCode(err);
245
- const lastStep = err && typeof err === "object" ? err._lastStep : void 0;
246
- const ethMapped = ethCode ? mapEthAppError(ethCode, lastStep) : null;
417
+ const ethMapped = classifyEthAppError(err);
247
418
  const apduHex = ethMapped ? null : extractApduHex(err);
248
419
  const chainMapped = apduHex ? mapSolanaAppError(apduHex) ?? mapTronAppError(apduHex) ?? mapBtcAppError(apduHex) : null;
249
420
  code = ethMapped ?? chainMapped ?? import_hwk_adapter_core.HardwareErrorCode.UnknownError;
@@ -253,38 +424,100 @@ function mapLedgerError(err, opts) {
253
424
  return { code, message: (0, import_hwk_adapter_core.enrichErrorMessage)(code, originalMessage), appName };
254
425
  }
255
426
 
256
- // src/utils/debugLog.ts
257
- var enabled = false;
258
- function setDebugEnabled(value) {
259
- enabled = value;
427
+ // src/utils/ledgerDmkTransport.ts
428
+ function isLedgerDmkBleTransport(transport) {
429
+ const normalized = transport?.toUpperCase();
430
+ return normalized === "BLE" || normalized?.endsWith("_BLE") === true;
260
431
  }
261
- function isDebugEnabled() {
262
- return enabled;
432
+ function isLedgerBleConnectionType(connectionType) {
433
+ return connectionType === "ble";
263
434
  }
264
- function debugLog(...args) {
265
- if (enabled) {
266
- console.debug(...args);
435
+ function isLedgerBleDescriptor(connectionType, descriptor) {
436
+ return isLedgerBleConnectionType(connectionType) || isLedgerDmkBleTransport(descriptor.transport);
437
+ }
438
+
439
+ // src/utils/sdkEventBus.ts
440
+ var listeners = /* @__PURE__ */ new Set();
441
+ function onSdkEvent(listener) {
442
+ listeners.add(listener);
443
+ return () => {
444
+ listeners.delete(listener);
445
+ };
446
+ }
447
+ function offSdkEvent(listener) {
448
+ listeners.delete(listener);
449
+ }
450
+ function emitSdkEvent(event) {
451
+ if (listeners.size === 0) return;
452
+ for (const listener of listeners) {
453
+ try {
454
+ listener(event);
455
+ } catch {
456
+ }
267
457
  }
268
458
  }
269
- function debugError(...args) {
270
- if (enabled) {
271
- console.error(...args);
459
+ function emitLog(level, ...args) {
460
+ if (listeners.size === 0) return;
461
+ const message = args.map((a) => typeof a === "string" ? a : safeStringify(a)).join(" ");
462
+ emitSdkEvent({ type: "log", level, message });
463
+ }
464
+ function safeStringify(value) {
465
+ if (value instanceof Error) {
466
+ return value.stack ? `${value.message}
467
+ ${value.stack}` : value.message;
272
468
  }
469
+ try {
470
+ return JSON.stringify(value);
471
+ } catch {
472
+ return String(value);
473
+ }
474
+ }
475
+
476
+ // src/utils/debugLog.ts
477
+ function debugLog(...args) {
478
+ emitLog("debug", ...args);
479
+ }
480
+ function debugError(...args) {
481
+ emitLog("error", ...args);
273
482
  }
274
483
 
275
484
  // src/adapter/LedgerAdapter.ts
276
485
  function formatDeviceMismatchError(expected, actual) {
277
486
  return `Wrong device: expected ${expected}, got ${actual}`;
278
487
  }
488
+ var BTC_HIGH_INDEX_THRESHOLD = 100;
489
+ function btcAccountIndexFromPath(path) {
490
+ const segments = path.replace(/^m\//, "").split("/");
491
+ if (segments.length < 3) return null;
492
+ const accountSeg = segments[2].replace(/['h]$/i, "");
493
+ const accountIndex = parseInt(accountSeg, 10);
494
+ return Number.isFinite(accountIndex) ? accountIndex : null;
495
+ }
279
496
  var _LedgerAdapter = class _LedgerAdapter {
280
- constructor(connector, options) {
497
+ constructor(connector) {
281
498
  this.vendor = "ledger";
282
499
  this.emitter = new import_hwk_adapter_core2.TypedEventEmitter();
283
- // Device cache: tracks discovered devices from connector events
284
500
  this._discoveredDevices = /* @__PURE__ */ new Map();
285
- // Session tracking: maps connectId -> sessionId
286
501
  this._sessions = /* @__PURE__ */ new Map();
287
502
  this._uiRegistry = new import_hwk_adapter_core2.UiRequestRegistry();
503
+ // BTC App rejects account index >= 100 unless display=true. Cached per
504
+ // adapter instance: first 100+ path asks the user once via UI request,
505
+ // subsequent 100+ paths in the same session auto-promote silently.
506
+ // The Ledger device itself still requires a per-call confirmation — that's
507
+ // the Ledger app's safety boundary, not ours to bypass.
508
+ this._btcHighIndexConfirmedThisSession = false;
509
+ // Shared across concurrent callers — only `cancel()` aborts.
510
+ this._doConnectAbortController = null;
511
+ // ---------------------------------------------------------------------------
512
+ // Private helpers
513
+ // ---------------------------------------------------------------------------
514
+ /**
515
+ * Ensure at least one device is connected and return a valid connectId.
516
+ *
517
+ * - If a session already exists for the given connectId, reuse it.
518
+ * - If ANY session exists (Ledger IDs are ephemeral), reuse it.
519
+ * - Otherwise: search → exactly 1 USB device auto-connects; multiple or none throws.
520
+ */
288
521
  // Mutex for ensureConnected — prevents concurrent calls from establishing duplicate connections
289
522
  this._connectingPromise = null;
290
523
  // ---------------------------------------------------------------------------
@@ -314,40 +547,23 @@ var _LedgerAdapter = class _LedgerAdapter {
314
547
  this.emitter.emit("ui-event", event);
315
548
  };
316
549
  this.connector = connector;
317
- this._handleSelectDevice = options?.handleSelectDevice ?? false;
318
- this._jobQueue = new import_hwk_adapter_core2.DeviceJobQueue({
319
- emit: (event, data) => this.emitter.emit(event, data),
320
- uiRegistry: this._uiRegistry
321
- });
550
+ this._jobQueue = new import_hwk_adapter_core2.DeviceJobQueue();
322
551
  this.registerEventListeners();
323
552
  }
324
- /**
325
- * Classify a method's interruptibility.
326
- * - Signing / typed data / transaction → 'confirm' (user may decide via preemption UI)
327
- * - Read-only queries (getAddress / getPublicKey / getMasterFingerprint) → 'safe'
328
- * (auto-cancels any pending read for the same device)
329
- */
330
- static _getInterruptibility(method) {
331
- if (method.toLowerCase().includes("sign")) return "confirm";
332
- return "safe";
333
- }
334
- // ---------------------------------------------------------------------------
335
553
  // Transport
336
- // ---------------------------------------------------------------------------
337
- // Transport is decided at connector creation time. These methods
338
- // satisfy the IHardwareWallet interface with sensible defaults.
339
554
  get activeTransport() {
340
- return this.connector.connectionType === "ble" ? "ble" : "hid";
555
+ return isLedgerBleConnectionType(this.connector.connectionType) ? "ble" : "hid";
341
556
  }
342
557
  getAvailableTransports() {
343
558
  return this.activeTransport ? [this.activeTransport] : [];
344
559
  }
345
- async switchTransport(_type) {
560
+ // Connector is bound at construction; switching requires a new adapter.
561
+ switchTransport(_type) {
562
+ return Promise.resolve();
346
563
  }
347
- // ---------------------------------------------------------------------------
348
564
  // Lifecycle
349
- // ---------------------------------------------------------------------------
350
- async init(_config) {
565
+ init(_config) {
566
+ return Promise.resolve();
351
567
  }
352
568
  /**
353
569
  * Clear cached device/session state without tearing down the adapter.
@@ -360,6 +576,7 @@ var _LedgerAdapter = class _LedgerAdapter {
360
576
  this._connectingPromise = null;
361
577
  this._uiRegistry.reset();
362
578
  this._jobQueue.clear();
579
+ this._btcHighIndexConfirmedThisSession = false;
363
580
  }
364
581
  async dispose() {
365
582
  this._uiRegistry.reset();
@@ -368,6 +585,7 @@ var _LedgerAdapter = class _LedgerAdapter {
368
585
  this.connector.reset();
369
586
  this._discoveredDevices.clear();
370
587
  this._sessions.clear();
588
+ this._btcHighIndexConfirmedThisSession = false;
371
589
  this.emitter.removeAllListeners();
372
590
  }
373
591
  uiResponse(response) {
@@ -376,10 +594,16 @@ var _LedgerAdapter = class _LedgerAdapter {
376
594
  // ---------------------------------------------------------------------------
377
595
  // Device management
378
596
  // ---------------------------------------------------------------------------
379
- async searchDevices() {
597
+ async searchDevices(options) {
598
+ if (options?.resetSession) {
599
+ this._doConnectAbortController?.abort();
600
+ this._sessions.clear();
601
+ this._connectingPromise = null;
602
+ this._doConnectAbortController = null;
603
+ this._btcHighIndexConfirmedThisSession = false;
604
+ }
380
605
  await this._ensureDevicePermission();
381
606
  const devices = await this.connector.searchDevices();
382
- debugLog("[DMK] adapter.searchDevices raw:", JSON.stringify(devices));
383
607
  this._discoveredDevices.clear();
384
608
  for (const d of devices) {
385
609
  if (d.connectId) {
@@ -389,11 +613,41 @@ var _LedgerAdapter = class _LedgerAdapter {
389
613
  if (this._discoveredDevices.size === 0) {
390
614
  await this._ensureDevicePermission();
391
615
  }
616
+ debugLog(
617
+ `[LedgerAdapter] searchDevices() return count=${this._discoveredDevices.size} ids=[${[
618
+ ...this._discoveredDevices.keys()
619
+ ].join(",")}]`
620
+ );
392
621
  return Array.from(this._discoveredDevices.values());
393
622
  }
623
+ // USB single-session invariant: evict all sessions, best-effort (see connectDevice).
624
+ async _evictAllSessions() {
625
+ if (this._sessions.size === 0) return;
626
+ const stale = [...this._sessions.values()];
627
+ this._sessions.clear();
628
+ for (const sid of stale) {
629
+ try {
630
+ await this.connector.disconnect(sid);
631
+ } catch {
632
+ }
633
+ }
634
+ }
635
+ static _createDeviceBusyError(method) {
636
+ return Object.assign(new Error(`Ledger device is busy while calling ${method}`), {
637
+ code: import_hwk_adapter_core2.HardwareErrorCode.DeviceBusy
638
+ });
639
+ }
394
640
  async connectDevice(connectId) {
395
- await this._ensureDevicePermission(connectId);
396
641
  try {
642
+ if (isLedgerBleConnectionType(this.connector.connectionType) && !connectId) {
643
+ throw Object.assign(new Error("Ledger BLE connectId is required."), {
644
+ code: import_hwk_adapter_core2.HardwareErrorCode.DeviceNotFound
645
+ });
646
+ }
647
+ if (!isLedgerBleConnectionType(this.connector.connectionType)) {
648
+ await this._evictAllSessions();
649
+ }
650
+ await this._ensureDevicePermission(connectId);
397
651
  const session = await this.connector.connect(connectId);
398
652
  this._sessions.set(connectId, session.sessionId);
399
653
  if (session.deviceInfo) {
@@ -429,7 +683,6 @@ var _LedgerAdapter = class _LedgerAdapter {
429
683
  // Chain call helper
430
684
  // ---------------------------------------------------------------------------
431
685
  async callChain(connectId, deviceId, chain, method, params, skipFingerprint = false) {
432
- await this._ensureDevicePermission(connectId, deviceId);
433
686
  try {
434
687
  const result = await this.connectorCall(connectId, method, params, {
435
688
  chain,
@@ -441,45 +694,12 @@ var _LedgerAdapter = class _LedgerAdapter {
441
694
  return this.errorToFailure(err);
442
695
  }
443
696
  }
444
- /**
445
- * Batch version of callChain — checks permission once,
446
- * fingerprint is verified on the first call inside connectorCall.
447
- */
448
- async callChainBatch(connectId, deviceId, chain, method, params, onProgress, skipFingerprint = false) {
449
- await this._ensureDevicePermission(connectId, deviceId);
450
- const results = [];
451
- for (let i = 0; i < params.length; i++) {
452
- try {
453
- const result = await this.connectorCall(connectId, method, params[i], {
454
- chain,
455
- deviceId,
456
- // Only verify fingerprint on the first call in the batch
457
- skipFingerprint: skipFingerprint || i > 0
458
- });
459
- results.push(result);
460
- onProgress?.({ index: i, total: params.length });
461
- } catch (err) {
462
- return this.errorToFailure(err);
463
- }
464
- }
465
- return (0, import_hwk_adapter_core2.success)(results);
466
- }
467
697
  // ---------------------------------------------------------------------------
468
698
  // EVM chain methods
469
699
  // ---------------------------------------------------------------------------
470
700
  evmGetAddress(connectId, deviceId, params) {
471
701
  return this.callChain(connectId, deviceId, "evm", "evmGetAddress", params);
472
702
  }
473
- evmGetAddresses(connectId, deviceId, params, onProgress) {
474
- return this.callChainBatch(
475
- connectId,
476
- deviceId,
477
- "evm",
478
- "evmGetAddress",
479
- params,
480
- onProgress
481
- );
482
- }
483
703
  evmSignTransaction(connectId, deviceId, params) {
484
704
  return this.callChain(connectId, deviceId, "evm", "evmSignTransaction", params);
485
705
  }
@@ -495,16 +715,6 @@ var _LedgerAdapter = class _LedgerAdapter {
495
715
  btcGetAddress(connectId, deviceId, params) {
496
716
  return this.callChain(connectId, deviceId, "btc", "btcGetAddress", params);
497
717
  }
498
- btcGetAddresses(connectId, deviceId, params, onProgress) {
499
- return this.callChainBatch(
500
- connectId,
501
- deviceId,
502
- "btc",
503
- "btcGetAddress",
504
- params,
505
- onProgress
506
- );
507
- }
508
718
  btcGetPublicKey(connectId, deviceId, params) {
509
719
  return this.callChain(connectId, deviceId, "btc", "btcGetPublicKey", params);
510
720
  }
@@ -532,16 +742,6 @@ var _LedgerAdapter = class _LedgerAdapter {
532
742
  solGetAddress(connectId, deviceId, params) {
533
743
  return this.callChain(connectId, deviceId, "sol", "solGetAddress", params);
534
744
  }
535
- solGetAddresses(connectId, deviceId, params, onProgress) {
536
- return this.callChainBatch(
537
- connectId,
538
- deviceId,
539
- "sol",
540
- "solGetAddress",
541
- params,
542
- onProgress
543
- );
544
- }
545
745
  solSignTransaction(connectId, deviceId, params) {
546
746
  return this.callChain(connectId, deviceId, "sol", "solSignTransaction", params);
547
747
  }
@@ -552,38 +752,13 @@ var _LedgerAdapter = class _LedgerAdapter {
552
752
  // TRON chain methods
553
753
  // ---------------------------------------------------------------------------
554
754
  tronGetAddress(connectId, deviceId, params) {
555
- return this.callChain(connectId, deviceId, "tron", "tronGetAddress", params, true);
556
- }
557
- tronGetAddresses(connectId, deviceId, params, onProgress) {
558
- return this.callChainBatch(
559
- connectId,
560
- deviceId,
561
- "tron",
562
- "tronGetAddress",
563
- params,
564
- onProgress,
565
- true
566
- );
755
+ return this.callChain(connectId, deviceId, "tron", "tronGetAddress", params);
567
756
  }
568
757
  tronSignTransaction(connectId, deviceId, params) {
569
- return this.callChain(
570
- connectId,
571
- deviceId,
572
- "tron",
573
- "tronSignTransaction",
574
- params,
575
- true
576
- );
758
+ return this.callChain(connectId, deviceId, "tron", "tronSignTransaction", params);
577
759
  }
578
760
  tronSignMessage(connectId, deviceId, params) {
579
- return this.callChain(
580
- connectId,
581
- deviceId,
582
- "tron",
583
- "tronSignMessage",
584
- params,
585
- true
586
- );
761
+ return this.callChain(connectId, deviceId, "tron", "tronSignMessage", params);
587
762
  }
588
763
  on(event, listener) {
589
764
  this.emitter.on(event, listener);
@@ -592,30 +767,41 @@ var _LedgerAdapter = class _LedgerAdapter {
592
767
  this.emitter.off(event, listener);
593
768
  }
594
769
  cancel(connectId) {
595
- const sessionId = this._sessions.get(connectId) ?? connectId;
596
- this._jobQueue.forceCancelActive(connectId || "__ledger_default__");
597
- void this.connector.cancel(sessionId);
770
+ const userAbortReason = Object.assign(new Error("User aborted operation"), {
771
+ code: import_hwk_adapter_core2.HardwareErrorCode.UserAborted,
772
+ _tag: ERROR_TAG.UserAborted
773
+ });
774
+ this._lastCancelReason = userAbortReason;
775
+ setTimeout(() => {
776
+ if (this._lastCancelReason === userAbortReason) {
777
+ this._lastCancelReason = void 0;
778
+ }
779
+ }, 2e3);
780
+ this._uiRegistry.cancel();
781
+ if (connectId) {
782
+ this._jobQueue.cancelActiveAndPending(connectId, userAbortReason);
783
+ } else {
784
+ this._jobQueue.cancelActiveAndPending(void 0, userAbortReason);
785
+ }
786
+ if (connectId) {
787
+ const sessionId = this._sessions.get(connectId) ?? connectId;
788
+ void this.connector.cancel(sessionId);
789
+ } else {
790
+ for (const sid of this._sessions.values()) void this.connector.cancel(sid);
791
+ }
792
+ if (this._connectingPromise) {
793
+ this._doConnectAbortController?.abort(userAbortReason);
794
+ }
598
795
  }
599
796
  // ---------------------------------------------------------------------------
600
797
  // Chain fingerprint
601
798
  // ---------------------------------------------------------------------------
602
799
  async getChainFingerprint(connectId, deviceId, chain) {
603
- debugLog(
604
- "[LedgerAdapter] getChainFingerprint called, chain:",
605
- chain,
606
- "connectId:",
607
- connectId || "(empty)",
608
- "sessions:",
609
- this._sessions.size
610
- );
611
- await this._ensureDevicePermission(connectId, deviceId);
612
- debugLog("[LedgerAdapter] getChainFingerprint permission ok, computing fingerprint");
613
800
  try {
614
801
  const fingerprint = await this._computeChainFingerprint(
615
802
  chain,
616
- (method, params) => this.connectorCall(connectId, method, params)
803
+ (method, params) => this.connectorCall(connectId, method, params, void 0, deviceId)
617
804
  );
618
- debugLog("[LedgerAdapter] getChainFingerprint result:", fingerprint?.substring(0, 20));
619
805
  return (0, import_hwk_adapter_core2.success)(fingerprint);
620
806
  } catch (err) {
621
807
  debugError("[LedgerAdapter] getChainFingerprint error:", chain, err);
@@ -686,103 +872,250 @@ var _LedgerAdapter = class _LedgerAdapter {
686
872
  if (signal?.aborted) {
687
873
  _LedgerAdapter._throwIfAborted(signal);
688
874
  }
875
+ const waitPromise = this._uiRegistry.wait(
876
+ import_hwk_adapter_core2.UI_REQUEST.REQUEST_DEVICE_CONNECT
877
+ );
689
878
  this.emitter.emit(import_hwk_adapter_core2.UI_REQUEST.REQUEST_DEVICE_CONNECT, {
690
879
  type: import_hwk_adapter_core2.UI_REQUEST.REQUEST_DEVICE_CONNECT,
691
880
  payload: {
881
+ vendor: "ledger",
882
+ reason: "device-not-found",
692
883
  message: "Please connect and unlock your Ledger device"
693
884
  }
694
885
  });
695
- const waitPromise = this._uiRegistry.wait(
696
- import_hwk_adapter_core2.UI_REQUEST.REQUEST_DEVICE_CONNECT
697
- );
698
886
  let payload;
699
- if (signal) {
700
- const onAbort = () => {
701
- this._uiRegistry.cancel(import_hwk_adapter_core2.UI_REQUEST.REQUEST_DEVICE_CONNECT);
702
- };
703
- signal.addEventListener("abort", onAbort, { once: true });
704
- try {
887
+ try {
888
+ if (signal) {
889
+ const onAbort = () => {
890
+ this._uiRegistry.cancel(import_hwk_adapter_core2.UI_REQUEST.REQUEST_DEVICE_CONNECT);
891
+ };
892
+ signal.addEventListener("abort", onAbort, { once: true });
893
+ try {
894
+ payload = await waitPromise;
895
+ } finally {
896
+ signal.removeEventListener("abort", onAbort);
897
+ }
898
+ } else {
705
899
  payload = await waitPromise;
706
- } finally {
707
- signal.removeEventListener("abort", onAbort);
708
900
  }
709
- } else {
710
- payload = await waitPromise;
901
+ } catch (err) {
902
+ this.emitter.emit(import_hwk_adapter_core2.UI_REQUEST.CLOSE_UI_WINDOW, {
903
+ type: import_hwk_adapter_core2.UI_REQUEST.CLOSE_UI_WINDOW,
904
+ payload: {}
905
+ });
906
+ throw Object.assign(new Error("User cancelled Ledger connection"), {
907
+ _tag: ERROR_TAG.UserAborted,
908
+ code: import_hwk_adapter_core2.HardwareErrorCode.UserAborted,
909
+ cause: err
910
+ });
711
911
  }
712
912
  if (!payload?.confirmed) {
713
913
  throw Object.assign(new Error("User cancelled Ledger connection"), {
714
- _tag: "DeviceNotRecognizedError"
914
+ _tag: ERROR_TAG.UserAborted,
915
+ code: import_hwk_adapter_core2.HardwareErrorCode.UserAborted
715
916
  });
716
917
  }
918
+ await new Promise((resolve) => {
919
+ setTimeout(resolve, 800);
920
+ });
717
921
  }
718
- async ensureConnected(connectId) {
719
- if (connectId && this._sessions.has(connectId)) {
720
- return connectId;
721
- }
722
- if (this._sessions.size > 0) {
723
- const firstKey = this._sessions.keys().next().value;
724
- return firstKey;
922
+ /**
923
+ * Decide whether a BTC pubkey call needs `showOnDevice=true` because of
924
+ * the BTC App's account-index policy, asking the user once per session.
925
+ *
926
+ * Returns the params to pass through (with `showOnDevice` possibly
927
+ * promoted), or `null` if the user declined.
928
+ */
929
+ async _gateBtcHighIndex(params) {
930
+ const accountIndex = btcAccountIndexFromPath(params.path);
931
+ if (params.showOnDevice) return params;
932
+ if (accountIndex === null || accountIndex < BTC_HIGH_INDEX_THRESHOLD) {
933
+ return params;
725
934
  }
726
- if (this._connectingPromise) {
727
- return this._connectingPromise;
935
+ if (this._btcHighIndexConfirmedThisSession) {
936
+ return { ...params, showOnDevice: true };
728
937
  }
729
- this._connectingPromise = this._doConnect();
938
+ const confirmed = await this._waitForBtcHighIndexConfirm(params.path, accountIndex);
939
+ if (!confirmed) return null;
940
+ this._btcHighIndexConfirmedThisSession = true;
941
+ return { ...params, showOnDevice: true };
942
+ }
943
+ async _waitForBtcHighIndexConfirm(path, accountIndex) {
944
+ const waitPromise = this._uiRegistry.wait(
945
+ import_hwk_adapter_core2.UI_REQUEST.REQUEST_BTC_HIGH_INDEX_CONFIRM
946
+ );
947
+ this.emitter.emit(import_hwk_adapter_core2.UI_REQUEST.REQUEST_BTC_HIGH_INDEX_CONFIRM, {
948
+ type: import_hwk_adapter_core2.UI_REQUEST.REQUEST_BTC_HIGH_INDEX_CONFIRM,
949
+ payload: {
950
+ vendor: "ledger",
951
+ path,
952
+ accountIndex
953
+ }
954
+ });
730
955
  try {
731
- return await this._connectingPromise;
732
- } finally {
733
- this._connectingPromise = null;
956
+ const payload = await waitPromise;
957
+ return !!payload?.confirmed;
958
+ } catch (err) {
959
+ this.emitter.emit(import_hwk_adapter_core2.UI_REQUEST.CLOSE_UI_WINDOW, {
960
+ type: import_hwk_adapter_core2.UI_REQUEST.CLOSE_UI_WINDOW,
961
+ payload: {}
962
+ });
963
+ return false;
964
+ }
965
+ }
966
+ // Layer 1 entry. Caller signal only races the outer awaiter; the shared
967
+ // `_doConnect` runs under its own internal controller so caller A's cancel
968
+ // doesn't kill caller B's await.
969
+ async ensureConnected(connectId, signal) {
970
+ if (signal.aborted) _LedgerAdapter._throwIfAborted(signal);
971
+ if (isLedgerBleConnectionType(this.connector.connectionType) && !connectId) {
972
+ throw Object.assign(new Error("Ledger BLE connectId is required."), {
973
+ code: import_hwk_adapter_core2.HardwareErrorCode.DeviceNotFound
974
+ });
975
+ }
976
+ if (connectId && this._sessions.has(connectId)) return connectId;
977
+ if (!connectId && this._sessions.size > 0) {
978
+ if (!isLedgerBleConnectionType(this.connector.connectionType) && this._sessions.size > 1) {
979
+ throw Object.assign(
980
+ new Error(
981
+ "Ledger USB session invariant violated: more than one session is active. Please reconnect the device."
982
+ ),
983
+ { code: import_hwk_adapter_core2.HardwareErrorCode.DeviceOneDeviceOnly }
984
+ );
985
+ }
986
+ return this._sessions.keys().next().value;
987
+ }
988
+ if (!this._connectingPromise) {
989
+ this._doConnectAbortController = new AbortController();
990
+ const innerSignal = this._doConnectAbortController.signal;
991
+ this._connectingPromise = (async () => {
992
+ try {
993
+ return await this._doConnect(innerSignal, connectId);
994
+ } finally {
995
+ this._connectingPromise = null;
996
+ this._doConnectAbortController = null;
997
+ }
998
+ })();
734
999
  }
1000
+ return this._abortable(signal, this._connectingPromise);
735
1001
  }
736
- async _doConnect() {
737
- for (let attempt = 0; attempt < _LedgerAdapter.MAX_DEVICE_RETRY; attempt++) {
738
- const devices = await this.searchDevices();
1002
+ // Layer 1 main loop — the ONLY place in SDK that emits unlock dialog.
1003
+ // Bounded by MAX_DOCONNECT_CONFIRMS after N Confirms with no progress,
1004
+ // throw DeviceNotFound so the user is kicked out of the loop.
1005
+ async _doConnect(internalSignal, targetConnectId) {
1006
+ if (isLedgerBleConnectionType(this.connector.connectionType) && targetConnectId) {
1007
+ try {
1008
+ return await this._connectDeviceOrThrow(targetConnectId);
1009
+ } catch (err) {
1010
+ if (!isDeviceLockedError(err) && !isDeviceNotAdvertisingError(err) && !isDeviceDisconnectedError(err)) {
1011
+ throw err;
1012
+ }
1013
+ this._discoveredDevices.delete(targetConnectId);
1014
+ if (isDeviceDisconnectedError(err)) {
1015
+ try {
1016
+ this.connector.reset?.();
1017
+ } catch {
1018
+ }
1019
+ }
1020
+ }
1021
+ }
1022
+ let confirms = 0;
1023
+ while (!internalSignal.aborted) {
1024
+ this.emitter.emit("ui-event", {
1025
+ type: import_hwk_adapter_core2.EConnectorInteraction.Searching,
1026
+ payload: { sessionId: "" }
1027
+ });
1028
+ let devices = await this.searchDevices();
1029
+ if (devices.length === 0) {
1030
+ for (let i = 0; i < 3 && !internalSignal.aborted; i += 1) {
1031
+ await new Promise((resolve) => {
1032
+ setTimeout(resolve, 1e3);
1033
+ });
1034
+ devices = await this.searchDevices();
1035
+ if (devices.length > 0) break;
1036
+ }
1037
+ }
739
1038
  if (devices.length > 0) {
740
- return this._connectFirstOrSelect(devices);
1039
+ try {
1040
+ return await this._connectFirstOrSelect(devices, targetConnectId);
1041
+ } catch (err) {
1042
+ if (!isDeviceLockedError(err) && !isDeviceNotAdvertisingError(err) && !isDeviceDisconnectedError(err)) {
1043
+ throw err;
1044
+ }
1045
+ this._discoveredDevices.clear();
1046
+ if (isDeviceDisconnectedError(err)) {
1047
+ try {
1048
+ this.connector.reset?.();
1049
+ } catch {
1050
+ }
1051
+ }
1052
+ }
741
1053
  }
742
- if (attempt < _LedgerAdapter.MAX_DEVICE_RETRY - 1) {
743
- await this._waitForDeviceConnect();
1054
+ if (confirms >= _LedgerAdapter.MAX_DOCONNECT_CONFIRMS) {
1055
+ throw Object.assign(
1056
+ new Error(
1057
+ "Device not connected after multiple attempts. Please ensure your Ledger is awake, unlocked, and in range, then try again."
1058
+ ),
1059
+ { code: import_hwk_adapter_core2.HardwareErrorCode.DeviceNotFound }
1060
+ );
744
1061
  }
1062
+ await this._waitForDeviceConnect(internalSignal);
1063
+ confirms += 1;
745
1064
  }
746
- throw Object.assign(
747
- new Error(
748
- "No Ledger device found after multiple attempts. Please connect and unlock your device."
749
- ),
750
- { _tag: "DeviceNotRecognizedError" }
751
- );
1065
+ _LedgerAdapter._throwIfAborted(internalSignal);
1066
+ throw new Error("_doConnect aborted");
1067
+ }
1068
+ async _connectFirstOrSelect(devices, targetConnectId) {
1069
+ if (targetConnectId) {
1070
+ const target = devices.find(
1071
+ (d) => d.connectId === targetConnectId || d.deviceId === targetConnectId
1072
+ );
1073
+ if (target) {
1074
+ return this._connectDeviceOrThrow(target.connectId);
1075
+ }
1076
+ if (!isLedgerBleConnectionType(this.connector.connectionType) && devices.length === 1) {
1077
+ debugLog(
1078
+ `[LedgerAdapter] target ${targetConnectId} not in fresh enumeration; accepting sole USB device ${devices[0].connectId} (assumed ephemeral path change)`
1079
+ );
1080
+ return this._connectDeviceOrThrow(devices[0].connectId);
1081
+ }
1082
+ const err = Object.assign(new Error(`Target Ledger unavailable: ${targetConnectId}`), {
1083
+ code: import_hwk_adapter_core2.HardwareErrorCode.DeviceNotFound
1084
+ });
1085
+ if (isLedgerBleConnectionType(this.connector.connectionType)) {
1086
+ err._tag = ERROR_TAG.DeviceNotAdvertising;
1087
+ }
1088
+ throw err;
1089
+ }
1090
+ if (isLedgerBleConnectionType(this.connector.connectionType)) {
1091
+ throw Object.assign(new Error("Ledger BLE connectId is required."), {
1092
+ code: import_hwk_adapter_core2.HardwareErrorCode.DeviceNotFound
1093
+ });
1094
+ }
1095
+ if (devices.length > 1) {
1096
+ throw createMultipleUsbLedgerDevicesError();
1097
+ }
1098
+ if (devices.length !== 1) {
1099
+ throw Object.assign(new Error("Ledger device not found."), {
1100
+ code: import_hwk_adapter_core2.HardwareErrorCode.DeviceNotFound
1101
+ });
1102
+ }
1103
+ return this._connectDeviceOrThrow(devices[0].connectId);
752
1104
  }
753
- async _connectFirstOrSelect(devices) {
754
- const chosenConnectId = devices.length === 1 ? devices[0].connectId : await this._chooseDeviceFromList(devices);
1105
+ async _connectDeviceOrThrow(chosenConnectId) {
755
1106
  const result = await this.connectDevice(chosenConnectId);
756
1107
  if (!result.success) {
757
- throw Object.assign(new Error(result.payload.error), {
758
- _tag: "DeviceNotRecognizedError"
1108
+ const payload = result.payload;
1109
+ const rethrow = Object.assign(new Error(payload.error), {
1110
+ code: payload.code
759
1111
  });
1112
+ if (payload._tag !== void 0) {
1113
+ rethrow._tag = payload._tag;
1114
+ }
1115
+ throw rethrow;
760
1116
  }
761
1117
  return chosenConnectId;
762
1118
  }
763
- async _chooseDeviceFromList(devices) {
764
- if (!this._handleSelectDevice) {
765
- debugLog(
766
- `[DMK] Multiple Ledger devices found (${devices.length}); handleSelectDevice=false, picking first (${devices[0].connectId}).`
767
- );
768
- return devices[0].connectId;
769
- }
770
- this.emitter.emit(import_hwk_adapter_core2.UI_REQUEST.REQUEST_SELECT_DEVICE, {
771
- type: import_hwk_adapter_core2.UI_REQUEST.REQUEST_SELECT_DEVICE,
772
- payload: { devices }
773
- });
774
- const response = await this._uiRegistry.wait(
775
- import_hwk_adapter_core2.UI_REQUEST.REQUEST_SELECT_DEVICE
776
- );
777
- const chosen = devices.find((d) => d.connectId === response?.sdkConnectId);
778
- if (!chosen) {
779
- throw Object.assign(
780
- new Error(`Selected sdkConnectId '${response?.sdkConnectId}' not in discovered list`),
781
- { _tag: "DeviceNotRecognizedError" }
782
- );
783
- }
784
- return chosen.connectId;
785
- }
786
1119
  /**
787
1120
  * Call the connector with automatic session resolution and disconnect retry.
788
1121
  *
@@ -791,30 +1124,31 @@ var _LedgerAdapter = class _LedgerAdapter {
791
1124
  * 3. Calls connector.call()
792
1125
  * 4. On disconnect error: clears stale session, re-connects, retries once
793
1126
  */
794
- async connectorCall(connectId, method, params, fingerprint) {
1127
+ async connectorCall(connectId, method, params, fingerprint, permissionDeviceId) {
795
1128
  debugLog("[LedgerAdapter] connectorCall:", method, "connectId:", connectId || "(empty)");
796
1129
  const queueKey = connectId || "__ledger_default__";
797
- const interruptibility = _LedgerAdapter._getInterruptibility(method);
798
1130
  return this._jobQueue.enqueue(
799
1131
  queueKey,
800
- async (signal) => this._runConnectorCall(connectId, method, params, signal, fingerprint),
801
- { interruptibility, label: method }
1132
+ async (signal) => this._runConnectorCall(connectId, method, params, signal, fingerprint, permissionDeviceId),
1133
+ {
1134
+ label: method,
1135
+ rejectIfBusy: true,
1136
+ busyError: _LedgerAdapter._createDeviceBusyError(method)
1137
+ }
802
1138
  );
803
1139
  }
804
1140
  /**
805
- * Race a promise against an abort signal. If the signal fires, rejects with the
806
- * signal's reason (or a generic Error). The underlying connector.call() cannot
807
- * actually be cancelled on Ledger DMK, but the caller gets the abort immediately.
1141
+ * Race a promise against an abort signal. On abort, rejects with
1142
+ * signal.reason instance _lastCancelReason → generic Error('Aborted').
808
1143
  */
809
- static _abortable(signal, promise) {
1144
+ _abortable(signal, promise) {
1145
+ const getAbortReason = () => signal.reason ?? this._lastCancelReason ?? new Error("Aborted");
810
1146
  if (signal.aborted) {
811
- return Promise.reject(
812
- signal.reason ?? new Error("Aborted")
813
- );
1147
+ return Promise.reject(getAbortReason());
814
1148
  }
815
1149
  return new Promise((resolve, reject) => {
816
1150
  const onAbort = () => {
817
- reject(signal.reason ?? new Error("Aborted"));
1151
+ reject(getAbortReason());
818
1152
  };
819
1153
  signal.addEventListener("abort", onAbort, { once: true });
820
1154
  promise.then(
@@ -836,40 +1170,49 @@ var _LedgerAdapter = class _LedgerAdapter {
836
1170
  }
837
1171
  }
838
1172
  /** Actual work done under the job queue — connection, fingerprint, call, and recovery. */
839
- async _runConnectorCall(connectId, method, params, signal, fingerprint) {
1173
+ async _runConnectorCall(connectId, method, params, signal, fingerprint, permissionDeviceId) {
840
1174
  _LedgerAdapter._throwIfAborted(signal);
841
- const resolvedConnectId = await _LedgerAdapter._abortable(
842
- signal,
843
- this.ensureConnected(connectId)
1175
+ await this._ensureDevicePermission(
1176
+ connectId,
1177
+ permissionDeviceId ?? fingerprint?.deviceId,
1178
+ signal
844
1179
  );
845
1180
  _LedgerAdapter._throwIfAborted(signal);
1181
+ let effectiveParams = params;
1182
+ if (method === "btcGetPublicKey") {
1183
+ const gatedParams = await this._gateBtcHighIndex(params);
1184
+ if (gatedParams === null) {
1185
+ throw Object.assign(new Error("User cancelled BTC high-index confirmation"), {
1186
+ _tag: ERROR_TAG.UserAborted,
1187
+ code: import_hwk_adapter_core2.HardwareErrorCode.UserAborted
1188
+ });
1189
+ }
1190
+ effectiveParams = gatedParams;
1191
+ }
1192
+ const resolvedConnectId = await this.ensureConnected(connectId, signal);
846
1193
  const sessionId = this._sessions.get(resolvedConnectId);
847
- debugLog(
848
- "[LedgerAdapter] connectorCall resolved:",
849
- method,
850
- "resolvedConnectId:",
851
- resolvedConnectId,
852
- "sessionId:",
853
- sessionId
854
- );
855
1194
  if (!sessionId) {
856
1195
  throw Object.assign(new Error("Auto-connect succeeded but no session found"), {
857
- _tag: "DeviceSessionNotFound"
1196
+ _tag: ERROR_TAG.DeviceSessionNotFound
858
1197
  });
859
1198
  }
860
- if (fingerprint && !fingerprint.skipFingerprint && fingerprint.deviceId) {
861
- const fp = await _LedgerAdapter._abortable(
862
- signal,
863
- this._verifyDeviceFingerprintWithSession(sessionId, fingerprint.deviceId, fingerprint.chain)
864
- );
865
- if (!fp.success) {
866
- throw Object.assign(new Error(formatDeviceMismatchError(fp.expected, fp.actual)), {
867
- code: import_hwk_adapter_core2.HardwareErrorCode.DeviceMismatch
868
- });
869
- }
870
- }
871
1199
  try {
872
- return await _LedgerAdapter._abortable(signal, this.connector.call(sessionId, method, params));
1200
+ if (fingerprint && !fingerprint.skipFingerprint && fingerprint.deviceId) {
1201
+ const fp = await this._abortable(
1202
+ signal,
1203
+ this._verifyDeviceFingerprintWithSession(
1204
+ sessionId,
1205
+ fingerprint.deviceId,
1206
+ fingerprint.chain
1207
+ )
1208
+ );
1209
+ if (!fp.success) {
1210
+ throw Object.assign(new Error(formatDeviceMismatchError(fp.expected, fp.actual)), {
1211
+ code: import_hwk_adapter_core2.HardwareErrorCode.DeviceMismatch
1212
+ });
1213
+ }
1214
+ }
1215
+ return await this._abortable(signal, this.connector.call(sessionId, method, effectiveParams));
873
1216
  } catch (err) {
874
1217
  if (signal.aborted) throw err;
875
1218
  const errObj = err;
@@ -879,37 +1222,241 @@ var _LedgerAdapter = class _LedgerAdapter {
879
1222
  errorCode: errObj?.errorCode,
880
1223
  statusCode: errObj?.statusCode,
881
1224
  isDisconnected: isDeviceDisconnectedError(err),
882
- isLocked: isDeviceLockedError(err)
1225
+ isLocked: isDeviceLockedError(err),
1226
+ isNotAdvertising: isDeviceNotAdvertisingError(err),
1227
+ isStuckApp: isStuckAppStateError(err)
883
1228
  });
884
- if (isDeviceDisconnectedError(err)) {
885
- debugLog("[LedgerAdapter] disconnected, retrying with fresh connection...");
886
- this._discoveredDevices.clear();
887
- return this._retryWithFreshConnection(resolvedConnectId, method, params, signal, err);
1229
+ if (isStuckAppStateError(err)) {
1230
+ try {
1231
+ this._sessions.delete(resolvedConnectId);
1232
+ this._discoveredDevices.delete(resolvedConnectId);
1233
+ this.connector.reset?.();
1234
+ } catch {
1235
+ }
1236
+ debugLog(
1237
+ "[LedgerAdapter] stuck-app retry: method=",
1238
+ method,
1239
+ "delayMs=",
1240
+ _LedgerAdapter.STUCK_APP_RETRY_DELAY_MS,
1241
+ "_tag=",
1242
+ err?._tag
1243
+ );
1244
+ try {
1245
+ const retryResult = await this._retryAfterStuckApp(
1246
+ resolvedConnectId,
1247
+ method,
1248
+ effectiveParams,
1249
+ signal,
1250
+ err,
1251
+ fingerprint
1252
+ );
1253
+ debugLog("[LedgerAdapter] stuck-app retry succeeded: method=", method);
1254
+ return retryResult;
1255
+ } catch (retryErr) {
1256
+ if (signal.aborted) throw retryErr;
1257
+ if (isStuckAppStateError(retryErr)) {
1258
+ debugLog("[LedgerAdapter] stuck-app retry exhausted (2nd 6901): method=", method);
1259
+ throw err;
1260
+ }
1261
+ debugLog(
1262
+ "[LedgerAdapter] stuck-app retry threw non-stuck error: method=",
1263
+ method,
1264
+ "retryErrTag=",
1265
+ retryErr?._tag
1266
+ );
1267
+ throw retryErr;
1268
+ }
888
1269
  }
889
- if (isDeviceLockedError(err)) {
890
- await this._waitForDeviceConnect(signal);
891
- _LedgerAdapter._throwIfAborted(signal);
892
- return _LedgerAdapter._abortable(signal, this.connector.call(sessionId, method, params));
1270
+ if (isDeviceLockedError(err) || isDeviceNotAdvertisingError(err) || isDeviceDisconnectedError(err)) {
1271
+ let lastErr = err;
1272
+ for (let attempt = 0; attempt < _LedgerAdapter.MAX_BUSINESS_RETRY_BUDGET; attempt += 1) {
1273
+ if (signal.aborted) throw lastErr;
1274
+ try {
1275
+ this._sessions.delete(resolvedConnectId);
1276
+ this._discoveredDevices.delete(resolvedConnectId);
1277
+ if (isDeviceDisconnectedError(lastErr)) {
1278
+ try {
1279
+ this.connector.reset?.();
1280
+ } catch {
1281
+ }
1282
+ }
1283
+ const reConnectId = await this.ensureConnected(resolvedConnectId, signal);
1284
+ const reSessionId = this._sessions.get(reConnectId);
1285
+ if (!reSessionId) throw lastErr;
1286
+ if (fingerprint && !fingerprint.skipFingerprint && fingerprint.deviceId) {
1287
+ const fp = await this._abortable(
1288
+ signal,
1289
+ this._verifyDeviceFingerprintWithSession(
1290
+ reSessionId,
1291
+ fingerprint.deviceId,
1292
+ fingerprint.chain
1293
+ )
1294
+ );
1295
+ if (!fp.success) {
1296
+ throw Object.assign(new Error(formatDeviceMismatchError(fp.expected, fp.actual)), {
1297
+ code: import_hwk_adapter_core2.HardwareErrorCode.DeviceMismatch
1298
+ });
1299
+ }
1300
+ }
1301
+ return await this._abortable(
1302
+ signal,
1303
+ this.connector.call(reSessionId, method, effectiveParams)
1304
+ );
1305
+ } catch (retryErr) {
1306
+ if (signal.aborted) throw retryErr;
1307
+ lastErr = retryErr;
1308
+ const canRetry = attempt < _LedgerAdapter.MAX_BUSINESS_RETRY_BUDGET - 1 && (isDeviceLockedError(retryErr) || isDeviceNotAdvertisingError(retryErr) || isDeviceDisconnectedError(retryErr));
1309
+ if (!canRetry) {
1310
+ throw retryErr;
1311
+ }
1312
+ }
1313
+ }
1314
+ throw lastErr;
893
1315
  }
894
1316
  if (isTimeoutError(err)) {
895
1317
  debugLog("[LedgerAdapter] timeout, retrying with fresh connection...");
896
1318
  this._discoveredDevices.delete(resolvedConnectId);
897
- return this._retryWithFreshConnection(resolvedConnectId, method, params, signal, err);
1319
+ return this._retryWithFreshConnection(
1320
+ resolvedConnectId,
1321
+ method,
1322
+ effectiveParams,
1323
+ signal,
1324
+ err,
1325
+ fingerprint
1326
+ );
1327
+ }
1328
+ if (isConnectionLevelError(err)) {
1329
+ debugLog("[LedgerAdapter] connection-level fail-closed reset");
1330
+ this._sessions.delete(resolvedConnectId);
1331
+ this._discoveredDevices.delete(resolvedConnectId);
1332
+ this.connector.reset();
1333
+ const codeNum = err?.code;
1334
+ throw Object.assign(err, {
1335
+ code: codeNum ?? import_hwk_adapter_core2.HardwareErrorCode.DeviceDisconnected
1336
+ });
898
1337
  }
899
1338
  throw err;
900
1339
  }
901
1340
  }
902
- /** Clear stale session, reconnect, and retry the call. */
903
- async _retryWithFreshConnection(resolvedConnectId, method, params, signal, originalErr) {
904
- this._sessions.delete(resolvedConnectId);
905
- _LedgerAdapter._throwIfAborted(signal);
906
- const retryConnectId = await this.ensureConnected();
907
- _LedgerAdapter._throwIfAborted(signal);
1341
+ /**
1342
+ * Stuck-app recovery: pause for the device's UI transition, then retry once.
1343
+ *
1344
+ * Caller has already cleared the session + reset connector. We wait so Stax
1345
+ * finishes its post-CloseApp animation, then go through ensureConnected +
1346
+ * fingerprint check + call exactly once. Caller decides what to do on a
1347
+ * second stuck-app hit.
1348
+ */
1349
+ async _retryAfterStuckApp(resolvedConnectId, method, params, signal, originalErr, fingerprint) {
1350
+ await this._sleepAbortable(_LedgerAdapter.STUCK_APP_RETRY_DELAY_MS, signal);
1351
+ const retryConnectId = await this.ensureConnected(resolvedConnectId, signal);
1352
+ const retrySessionId = this._sessions.get(retryConnectId);
1353
+ if (!retrySessionId) throw originalErr;
1354
+ if (fingerprint && !fingerprint.skipFingerprint && fingerprint.deviceId) {
1355
+ const fp = await this._abortable(
1356
+ signal,
1357
+ this._verifyDeviceFingerprintWithSession(
1358
+ retrySessionId,
1359
+ fingerprint.deviceId,
1360
+ fingerprint.chain
1361
+ )
1362
+ );
1363
+ if (!fp.success) {
1364
+ throw Object.assign(new Error(formatDeviceMismatchError(fp.expected, fp.actual)), {
1365
+ code: import_hwk_adapter_core2.HardwareErrorCode.DeviceMismatch
1366
+ });
1367
+ }
1368
+ }
1369
+ return this._abortable(signal, this.connector.call(retrySessionId, method, params));
1370
+ }
1371
+ _sleepAbortable(ms, signal) {
1372
+ return new Promise((resolve, reject) => {
1373
+ if (signal.aborted) {
1374
+ reject(signal.reason ?? new Error("Aborted"));
1375
+ return;
1376
+ }
1377
+ const timer = setTimeout(() => {
1378
+ signal.removeEventListener("abort", onAbort);
1379
+ resolve();
1380
+ }, ms);
1381
+ const onAbort = () => {
1382
+ clearTimeout(timer);
1383
+ reject(signal.reason ?? new Error("Aborted"));
1384
+ };
1385
+ signal.addEventListener("abort", onAbort, { once: true });
1386
+ });
1387
+ }
1388
+ /**
1389
+ * Clear stale session, reconnect, and retry the call.
1390
+ *
1391
+ * Timeout recovery starts with a full connector reset. After an APDU
1392
+ * timeout, DMK/transport state may still emit malformed responses; retrying
1393
+ * on the same DMK can poison the next chain switch.
1394
+ */
1395
+ async _retryWithFreshConnection(targetConnectId, method, params, signal, originalErr, fingerprint) {
1396
+ this.connector.reset();
1397
+ this._sessions.clear();
1398
+ this._discoveredDevices.clear();
1399
+ this._connectingPromise = null;
1400
+ const retryConnectId = await this.ensureConnected(targetConnectId, signal);
908
1401
  const retrySessionId = this._sessions.get(retryConnectId);
909
1402
  if (!retrySessionId) {
910
1403
  throw originalErr;
911
1404
  }
912
- return _LedgerAdapter._abortable(signal, this.connector.call(retrySessionId, method, params));
1405
+ try {
1406
+ if (fingerprint && !fingerprint.skipFingerprint && fingerprint.deviceId) {
1407
+ const fp = await this._abortable(
1408
+ signal,
1409
+ this._verifyDeviceFingerprintWithSession(
1410
+ retrySessionId,
1411
+ fingerprint.deviceId,
1412
+ fingerprint.chain
1413
+ )
1414
+ );
1415
+ if (!fp.success) {
1416
+ throw Object.assign(new Error(formatDeviceMismatchError(fp.expected, fp.actual)), {
1417
+ code: import_hwk_adapter_core2.HardwareErrorCode.DeviceMismatch
1418
+ });
1419
+ }
1420
+ }
1421
+ return await this._abortable(signal, this.connector.call(retrySessionId, method, params));
1422
+ } catch (retryErr) {
1423
+ if (signal.aborted) throw retryErr;
1424
+ this.connector.reset();
1425
+ this._sessions.clear();
1426
+ this._discoveredDevices.clear();
1427
+ this._connectingPromise = null;
1428
+ if (!isDeviceDisconnectedError(retryErr) && !isTimeoutError(retryErr)) {
1429
+ throw retryErr;
1430
+ }
1431
+ debugLog(
1432
+ "[LedgerAdapter] fresh-session retry still failed; resetting connector and rebuilding DMK"
1433
+ );
1434
+ this.connector.reset();
1435
+ this._sessions.clear();
1436
+ this._discoveredDevices.clear();
1437
+ this._connectingPromise = null;
1438
+ const finalConnectId = await this.ensureConnected(targetConnectId, signal);
1439
+ const finalSessionId = this._sessions.get(finalConnectId);
1440
+ if (!finalSessionId) {
1441
+ throw originalErr;
1442
+ }
1443
+ if (fingerprint && !fingerprint.skipFingerprint && fingerprint.deviceId) {
1444
+ const fp = await this._abortable(
1445
+ signal,
1446
+ this._verifyDeviceFingerprintWithSession(
1447
+ finalSessionId,
1448
+ fingerprint.deviceId,
1449
+ fingerprint.chain
1450
+ )
1451
+ );
1452
+ if (!fp.success) {
1453
+ throw Object.assign(new Error(formatDeviceMismatchError(fp.expected, fp.actual)), {
1454
+ code: import_hwk_adapter_core2.HardwareErrorCode.DeviceMismatch
1455
+ });
1456
+ }
1457
+ }
1458
+ return this._abortable(signal, this.connector.call(finalSessionId, method, params));
1459
+ }
913
1460
  }
914
1461
  /**
915
1462
  * Ensure OS-level device permission (Bluetooth / USB) before proceeding.
@@ -923,7 +1470,10 @@ var _LedgerAdapter = class _LedgerAdapter {
923
1470
  * - No connectId (searchDevices): environment-level permission
924
1471
  * - With connectId (business methods): device-level permission
925
1472
  */
926
- async _ensureDevicePermission(connectId, deviceId) {
1473
+ async _ensureDevicePermission(connectId, deviceId, signal) {
1474
+ if (signal?.aborted) {
1475
+ _LedgerAdapter._throwIfAborted(signal);
1476
+ }
927
1477
  const transportType = this.activeTransport ?? "hid";
928
1478
  const waitPromise = this._uiRegistry.wait(
929
1479
  import_hwk_adapter_core2.UI_REQUEST.REQUEST_DEVICE_PERMISSION,
@@ -933,10 +1483,37 @@ var _LedgerAdapter = class _LedgerAdapter {
933
1483
  type: import_hwk_adapter_core2.UI_REQUEST.REQUEST_DEVICE_PERMISSION,
934
1484
  payload: { transportType, connectId, deviceId }
935
1485
  });
936
- const { granted } = await waitPromise;
1486
+ let response;
1487
+ const onAbort = () => {
1488
+ this._uiRegistry.cancel(import_hwk_adapter_core2.UI_REQUEST.REQUEST_DEVICE_PERMISSION);
1489
+ };
1490
+ try {
1491
+ signal?.addEventListener("abort", onAbort, { once: true });
1492
+ response = await waitPromise;
1493
+ } catch (err) {
1494
+ if (signal?.aborted) {
1495
+ _LedgerAdapter._throwIfAborted(signal);
1496
+ }
1497
+ if (err?._tag === import_hwk_adapter_core2.UI_REQUEST_PREEMPTED_TAG) {
1498
+ this.emitter.emit(import_hwk_adapter_core2.UI_REQUEST.CLOSE_UI_WINDOW, {
1499
+ type: import_hwk_adapter_core2.UI_REQUEST.CLOSE_UI_WINDOW,
1500
+ payload: {}
1501
+ });
1502
+ throw Object.assign(new Error("Device permission request superseded"), {
1503
+ _tag: ERROR_TAG.UserAborted,
1504
+ code: import_hwk_adapter_core2.HardwareErrorCode.UserAborted,
1505
+ cause: err
1506
+ });
1507
+ }
1508
+ throw err;
1509
+ } finally {
1510
+ signal?.removeEventListener("abort", onAbort);
1511
+ }
1512
+ const { granted, reason, message } = response;
937
1513
  if (!granted) {
938
- throw Object.assign(new Error("Device permission denied"), {
939
- code: import_hwk_adapter_core2.HardwareErrorCode.DevicePermissionDenied
1514
+ throw Object.assign(new Error(message ?? "Device permission denied"), {
1515
+ code: import_hwk_adapter_core2.HardwareErrorCode.DevicePermissionDenied,
1516
+ reason
940
1517
  });
941
1518
  }
942
1519
  }
@@ -946,12 +1523,14 @@ var _LedgerAdapter = class _LedgerAdapter {
946
1523
  */
947
1524
  errorToFailure(err) {
948
1525
  debugError("[LedgerAdapter] error:", err);
1526
+ const tag = err && typeof err === "object" ? err._tag : void 0;
949
1527
  if (err && typeof err === "object" && "code" in err && typeof err.code === "number") {
950
1528
  const e = err;
951
- return ledgerFailure(e.code, e.message ?? "Unknown error", e.appName);
1529
+ const params = e.code === import_hwk_adapter_core2.HardwareErrorCode.DevicePermissionDenied && e.reason ? { permissionDeniedReason: e.reason } : void 0;
1530
+ return ledgerFailure(e.code, e.message ?? "Unknown error", e.appName, tag, params);
952
1531
  }
953
1532
  const mapped = mapLedgerError(err);
954
- return ledgerFailure(mapped.code, mapped.message, mapped.appName);
1533
+ return ledgerFailure(mapped.code, mapped.message, mapped.appName, tag);
955
1534
  }
956
1535
  registerEventListeners() {
957
1536
  this.connector.on("device-connect", this.deviceConnectHandler);
@@ -967,32 +1546,38 @@ var _LedgerAdapter = class _LedgerAdapter {
967
1546
  // Device info mapping
968
1547
  // ---------------------------------------------------------------------------
969
1548
  connectorDeviceToDeviceInfo(device) {
970
- const isBle = device.connectId && /^[0-9A-Fa-f]{4}$/.test(device.connectId);
971
1549
  return {
972
1550
  vendor: "ledger",
973
1551
  model: device.model ?? "unknown",
1552
+ modelName: device.modelName,
974
1553
  firmwareVersion: "",
975
1554
  deviceId: device.deviceId,
976
1555
  connectId: device.connectId,
977
1556
  label: device.name,
978
- connectionType: isBle ? "ble" : "usb",
1557
+ connectionType: this.connector.connectionType,
1558
+ rssi: device.rssi,
1559
+ isConnectable: device.isConnectable,
1560
+ serialNumber: device.serialNumber,
979
1561
  capabilities: device.capabilities
980
1562
  };
981
1563
  }
982
1564
  };
983
- // ---------------------------------------------------------------------------
984
- // Private helpers
985
- // ---------------------------------------------------------------------------
986
- /**
987
- * Ensure at least one device is connected and return a valid connectId.
988
- *
989
- * - If a session already exists for the given connectId, reuse it.
990
- * - If ANY session exists (Ledger IDs are ephemeral), reuse it.
991
- * - Otherwise: search 1 device: auto-connect, multiple: ask user, 0: throw.
992
- */
993
- _LedgerAdapter.MAX_DEVICE_RETRY = 3;
1565
+ // Layer 2 retry budget after connection-class error. Each round delegates
1566
+ // to Layer 1 (which owns the unlock prompt). Layer 2 never emits UI itself.
1567
+ _LedgerAdapter.MAX_BUSINESS_RETRY_BUDGET = 3;
1568
+ // Stuck-app (APDU 0x6901) retry pause. Ledger Stax often returns 6901 when
1569
+ // OpenAppCommand lands mid-transition right after CloseApp; a short wait
1570
+ // lets the device finish its UI animation before we retry once.
1571
+ _LedgerAdapter.STUCK_APP_RETRY_DELAY_MS = 500;
1572
+ // Layer 1 confirm budget. After N failed Confirm cycles (user keeps clicking
1573
+ // Confirm but device never shows up / never unlocks), throw DeviceNotFound
1574
+ // instead of looping forever.
1575
+ _LedgerAdapter.MAX_DOCONNECT_CONFIRMS = 3;
994
1576
  var LedgerAdapter = _LedgerAdapter;
995
1577
 
1578
+ // src/connector/LedgerConnectorBase.ts
1579
+ var import_hwk_adapter_core12 = require("@onekeyfe/hwk-adapter-core");
1580
+
996
1581
  // src/device/LedgerDeviceManager.ts
997
1582
  var LedgerDeviceManager = class {
998
1583
  constructor(dmk) {
@@ -1029,7 +1614,9 @@ var LedgerDeviceManager = class {
1029
1614
  if (resolved) return;
1030
1615
  resolved = true;
1031
1616
  this._discovered.clear();
1032
- debugLog("[DMK] enumerate raw devices:", devices.length);
1617
+ debugLog(
1618
+ `[DMK] enumerate count=${devices.length} ids=[${devices.map((d) => d.id).join(",")}]`
1619
+ );
1033
1620
  for (const d of devices) {
1034
1621
  this._discovered.set(d.id, d);
1035
1622
  }
@@ -1039,8 +1626,10 @@ var LedgerDeviceManager = class {
1039
1626
  devices.map((d) => ({
1040
1627
  path: d.id,
1041
1628
  type: d.deviceModel.model,
1629
+ modelName: d.deviceModel.name,
1042
1630
  name: d.name,
1043
- transport: d.transport
1631
+ transport: d.transport,
1632
+ rssi: d.rssi
1044
1633
  }))
1045
1634
  );
1046
1635
  } else {
@@ -1061,8 +1650,10 @@ var LedgerDeviceManager = class {
1061
1650
  devices.map((d) => ({
1062
1651
  path: d.id,
1063
1652
  type: d.deviceModel.model,
1653
+ modelName: d.deviceModel.name,
1064
1654
  name: d.name,
1065
- transport: d.transport
1655
+ transport: d.transport,
1656
+ rssi: d.rssi
1066
1657
  }))
1067
1658
  );
1068
1659
  }
@@ -1086,8 +1677,10 @@ var LedgerDeviceManager = class {
1086
1677
  descriptor: {
1087
1678
  path: d.id,
1088
1679
  type: d.deviceModel.model,
1680
+ modelName: d.deviceModel.name,
1089
1681
  name: d.name,
1090
- transport: d.transport
1682
+ transport: d.transport,
1683
+ rssi: d.rssi
1091
1684
  }
1092
1685
  });
1093
1686
  }
@@ -1106,6 +1699,54 @@ var LedgerDeviceManager = class {
1106
1699
  this._listenSub?.unsubscribe();
1107
1700
  this._listenSub = null;
1108
1701
  }
1702
+ /**
1703
+ * Resolve to the latest snapshot of advertising devices observed within
1704
+ * `timeoutMs`. Rewrites `_discovered` so it tracks the live list.
1705
+ *
1706
+ * `listenToAvailableDevices` is a BehaviorSubject — it emits the cached
1707
+ * list on subscribe and *only* re-emits when the list changes. A stable
1708
+ * list of currently-advertising devices therefore produces only the
1709
+ * subscription frame; treating that frame as "fossil to be discarded"
1710
+ * caused devices to flicker in/out of the UI as searches alternated
1711
+ * between "saw a change frame" and "saw only the cached frame".
1712
+ *
1713
+ * The window still gives a chance for the active scan to update the list
1714
+ * (e.g. drop a peripheral that just stopped broadcasting), but if nothing
1715
+ * changes we trust the snapshot — DMK silence on a stable list means
1716
+ * "everything's still there".
1717
+ */
1718
+ getLiveDevices(timeoutMs = 1500) {
1719
+ debugLog(`[DMK] getLiveDevices() called, timeoutMs=${timeoutMs}`);
1720
+ void this.requestDevice();
1721
+ return new Promise((resolve) => {
1722
+ let done = false;
1723
+ let lastSeen = [];
1724
+ let sub = null;
1725
+ const finish = (devices) => {
1726
+ if (done) return;
1727
+ done = true;
1728
+ sub?.unsubscribe();
1729
+ clearTimeout(timer);
1730
+ this._discovered.clear();
1731
+ for (const d of devices) this._discovered.set(d.id, d);
1732
+ debugLog(
1733
+ `[DMK] getLiveDevices() resolved count=${devices.length} ids=[${devices.map((d) => d.id).join(",")}]`
1734
+ );
1735
+ resolve(devices);
1736
+ };
1737
+ const timer = setTimeout(() => finish(lastSeen), timeoutMs);
1738
+ sub = this._dmk.listenToAvailableDevices({}).subscribe({
1739
+ next: (devices) => {
1740
+ lastSeen = devices;
1741
+ },
1742
+ error: (err) => {
1743
+ debugError("[DMK] getLiveDevices() error:", err);
1744
+ finish(lastSeen);
1745
+ },
1746
+ complete: () => finish(lastSeen)
1747
+ });
1748
+ });
1749
+ }
1109
1750
  requestDevice() {
1110
1751
  if (this._discoverySub) {
1111
1752
  return Promise.resolve();
@@ -1123,11 +1764,33 @@ var LedgerDeviceManager = class {
1123
1764
  });
1124
1765
  return Promise.resolve();
1125
1766
  }
1767
+ /** Lookup a previously-discovered device's display name. */
1768
+ getDeviceName(deviceId) {
1769
+ return this._discovered.get(deviceId)?.name;
1770
+ }
1771
+ /** Lookup minimal model/signal info from a previously-discovered device. */
1772
+ getDiscoveredDeviceInfo(deviceId) {
1773
+ const d = this._discovered.get(deviceId);
1774
+ if (!d) return void 0;
1775
+ return {
1776
+ model: d.deviceModel?.model,
1777
+ modelName: d.deviceModel?.name,
1778
+ name: d.name,
1779
+ rssi: d.rssi
1780
+ };
1781
+ }
1782
+ hasDiscoveredDevice(deviceId) {
1783
+ return this._discovered.has(deviceId);
1784
+ }
1126
1785
  /** Connect to a previously discovered device. Returns sessionId. */
1127
1786
  async connect(deviceId) {
1128
1787
  const device = this._discovered.get(deviceId);
1129
1788
  if (!device) {
1130
- throw new Error(`Device "${deviceId}" not found. Call enumerate() or listen() first.`);
1789
+ const err = new Error(
1790
+ `Device "${deviceId}" not found in discovery cache. Call enumerate() or listen() first.`
1791
+ );
1792
+ err._tag = ERROR_TAG.DeviceNotInDiscoveryCache;
1793
+ throw err;
1131
1794
  }
1132
1795
  const sessionId = await this._dmk.connect({ device });
1133
1796
  this._sessions.set(deviceId, sessionId);
@@ -1166,6 +1829,19 @@ var LedgerDeviceManager = class {
1166
1829
  this._sessionToDevice.clear();
1167
1830
  this._dmk.close();
1168
1831
  }
1832
+ /**
1833
+ * Tear down RxJS subscriptions and local state without closing the DMK.
1834
+ * For light-reset paths that want to drop session state but keep the
1835
+ * underlying transport alive for retry. Call this instead of orphaning
1836
+ * the manager — bare null assignment leaks _listenSub / _discoverySub.
1837
+ */
1838
+ disposeKeepingDmk() {
1839
+ this.stopListening();
1840
+ this.stopDiscovery();
1841
+ this._discovered.clear();
1842
+ this._sessions.clear();
1843
+ this._sessionToDevice.clear();
1844
+ }
1169
1845
  };
1170
1846
 
1171
1847
  // src/signer/SignerManager.ts
@@ -1177,11 +1853,12 @@ var import_hwk_adapter_core3 = require("@onekeyfe/hwk-adapter-core");
1177
1853
 
1178
1854
  // src/signer/deviceActionToPromise.ts
1179
1855
  var import_device_management_kit = require("@ledgerhq/device-management-kit");
1180
- var DEFAULT_TIMEOUT_MS = 3e4;
1181
- function deviceActionToPromise(action, onInteraction, timeoutMs = DEFAULT_TIMEOUT_MS, onRegisterCanceller) {
1856
+ var IDLE_WATCHDOG_MS = 65e3;
1857
+ function deviceActionToPromise(action, onInteraction, timeoutMs = IDLE_WATCHDOG_MS, onRegisterCanceller) {
1182
1858
  return new Promise((resolve, reject) => {
1183
1859
  let settled = false;
1184
1860
  let lastStep;
1861
+ const observedSteps = [];
1185
1862
  let sub;
1186
1863
  let timer = null;
1187
1864
  const cancelAction = () => {
@@ -1194,44 +1871,59 @@ function deviceActionToPromise(action, onInteraction, timeoutMs = DEFAULT_TIMEOU
1194
1871
  } catch {
1195
1872
  }
1196
1873
  };
1197
- const resetTimer = () => {
1874
+ const armIdleWatchdog = () => {
1198
1875
  if (timer) clearTimeout(timer);
1199
1876
  if (timeoutMs > 0) {
1200
1877
  timer = setTimeout(() => {
1201
1878
  if (!settled) {
1202
1879
  settled = true;
1203
1880
  cancelAction();
1204
- reject(new Error("Device action timed out \u2014 device may be locked or disconnected"));
1881
+ reject(
1882
+ new Error(
1883
+ "Ledger transport unresponsive: no DMK state change within the watchdog window. The device may have lost connection."
1884
+ )
1885
+ );
1205
1886
  }
1206
1887
  }, timeoutMs);
1207
1888
  }
1208
1889
  };
1209
- resetTimer();
1890
+ armIdleWatchdog();
1210
1891
  if (onRegisterCanceller) {
1211
- onRegisterCanceller(() => {
1212
- if (settled) return;
1892
+ onRegisterCanceller((reason) => {
1893
+ if (settled) {
1894
+ return;
1895
+ }
1213
1896
  settled = true;
1214
1897
  if (timer) clearTimeout(timer);
1215
1898
  cancelAction();
1216
- reject(new Error("Device action cancelled"));
1899
+ const err = new Error(reason?.message ?? "Device action cancelled");
1900
+ if (reason?.code !== void 0) {
1901
+ err.code = reason.code;
1902
+ }
1903
+ if (reason?.tag) {
1904
+ Object.defineProperty(err, "_tag", {
1905
+ value: reason.tag,
1906
+ configurable: true,
1907
+ enumerable: false,
1908
+ writable: true
1909
+ });
1910
+ }
1911
+ reject(err);
1217
1912
  });
1218
1913
  }
1219
- debugLog("[DMK-Observable] subscribing to action.observable...");
1220
1914
  sub = action.observable.subscribe({
1221
1915
  next: (state) => {
1222
- if (settled) return;
1223
- resetTimer();
1916
+ if (settled) {
1917
+ return;
1918
+ }
1919
+ armIdleWatchdog();
1224
1920
  const step = state?.intermediateValue?.step;
1225
- if (step) lastStep = step;
1226
- debugLog(
1227
- "[DMK-Observable] state:",
1228
- JSON.stringify({
1229
- status: state.status,
1230
- intermediateValue: state.status === import_device_management_kit.DeviceActionStatus.Pending ? state.intermediateValue : void 0,
1231
- hasOutput: state.status === import_device_management_kit.DeviceActionStatus.Completed,
1232
- hasError: state.status === import_device_management_kit.DeviceActionStatus.Error
1233
- })
1234
- );
1921
+ if (step) {
1922
+ lastStep = step;
1923
+ if (observedSteps[observedSteps.length - 1] !== step) {
1924
+ observedSteps.push(step);
1925
+ }
1926
+ }
1235
1927
  if (state.status === import_device_management_kit.DeviceActionStatus.Completed) {
1236
1928
  settled = true;
1237
1929
  if (timer) clearTimeout(timer);
@@ -1243,10 +1935,14 @@ function deviceActionToPromise(action, onInteraction, timeoutMs = DEFAULT_TIMEOU
1243
1935
  if (timer) clearTimeout(timer);
1244
1936
  onInteraction?.("interaction-complete");
1245
1937
  sub?.unsubscribe();
1246
- rejectWithLastStep(state.error, lastStep, reject);
1938
+ rejectWithStepContext(state.error, lastStep, observedSteps, reject);
1247
1939
  } else if (state.status === import_device_management_kit.DeviceActionStatus.Pending && onInteraction) {
1248
1940
  const interaction = state.intermediateValue?.requiredUserInteraction;
1249
1941
  if (interaction && interaction !== "none") {
1942
+ if (interaction === "unlock-device" && timer) {
1943
+ clearTimeout(timer);
1944
+ timer = null;
1945
+ }
1250
1946
  onInteraction(String(interaction));
1251
1947
  }
1252
1948
  }
@@ -1256,7 +1952,7 @@ function deviceActionToPromise(action, onInteraction, timeoutMs = DEFAULT_TIMEOU
1256
1952
  settled = true;
1257
1953
  if (timer) clearTimeout(timer);
1258
1954
  sub?.unsubscribe();
1259
- rejectWithLastStep(err, lastStep, reject);
1955
+ rejectWithStepContext(err, lastStep, observedSteps, reject);
1260
1956
  }
1261
1957
  },
1262
1958
  complete: () => {
@@ -1269,15 +1965,25 @@ function deviceActionToPromise(action, onInteraction, timeoutMs = DEFAULT_TIMEOU
1269
1965
  });
1270
1966
  });
1271
1967
  }
1272
- function rejectWithLastStep(err, lastStep, reject) {
1273
- if (err && typeof err === "object" && lastStep) {
1968
+ function rejectWithStepContext(err, lastStep, observedSteps, reject) {
1969
+ if (err && typeof err === "object" && (lastStep || observedSteps.length > 0)) {
1274
1970
  try {
1275
- Object.defineProperty(err, "_lastStep", {
1276
- value: lastStep,
1277
- configurable: true,
1278
- enumerable: false,
1279
- writable: true
1280
- });
1971
+ if (lastStep) {
1972
+ Object.defineProperty(err, "_lastStep", {
1973
+ value: lastStep,
1974
+ configurable: true,
1975
+ enumerable: false,
1976
+ writable: true
1977
+ });
1978
+ }
1979
+ if (observedSteps.length > 0) {
1980
+ Object.defineProperty(err, "_deviceActionSteps", {
1981
+ value: [...observedSteps],
1982
+ configurable: true,
1983
+ enumerable: false,
1984
+ writable: true
1985
+ });
1986
+ }
1281
1987
  } catch {
1282
1988
  }
1283
1989
  }
@@ -1343,7 +2049,8 @@ var SignerManager = class _SignerManager {
1343
2049
  async getOrCreate(sessionId) {
1344
2050
  debugLog("[DMK] SignerManager.getOrCreate:", { sessionId });
1345
2051
  const builder = await this._builderFn({ dmk: this._dmk, sessionId });
1346
- return new SignerEth(builder.build());
2052
+ const builderWithContext = builder.withContextModule ? builder.withContextModule(_SignerManager._createContextModule()) : builder;
2053
+ return new SignerEth(builderWithContext.build());
1347
2054
  }
1348
2055
  // eslint-disable-next-line @typescript-eslint/no-unused-vars, class-methods-use-this
1349
2056
  invalidate(_sessionId) {
@@ -1352,10 +2059,24 @@ var SignerManager = class _SignerManager {
1352
2059
  clearAll() {
1353
2060
  }
1354
2061
  static _defaultBuilder() {
1355
- return (args) => {
1356
- const contextModule = new import_context_module.ContextModuleBuilder({}).removeDefaultLoaders().build();
1357
- return new import_device_signer_kit_ethereum.SignerEthBuilder(args).withContextModule(contextModule);
2062
+ return (args) => new import_device_signer_kit_ethereum.SignerEthBuilder(args);
2063
+ }
2064
+ static _createContextModule() {
2065
+ const contextModule = new import_context_module.ContextModuleBuilder({}).removeDefaultLoaders().build();
2066
+ return _SignerManager.wrapBlindSigningReportNonBlocking(contextModule);
2067
+ }
2068
+ static wrapBlindSigningReportNonBlocking(contextModule) {
2069
+ const report = contextModule.report.bind(contextModule);
2070
+ contextModule.report = async (params) => {
2071
+ try {
2072
+ void report(params).catch((err) => {
2073
+ debugLog("[DMK] ContextModule.report failed:", err);
2074
+ });
2075
+ } catch (err) {
2076
+ debugLog("[DMK] ContextModule.report failed:", err);
2077
+ }
1358
2078
  };
2079
+ return contextModule;
1359
2080
  }
1360
2081
  };
1361
2082
 
@@ -1465,6 +2186,7 @@ async function _getEthSigner(ctx, sessionId) {
1465
2186
  const signerManager = await ctx.getSignerManager();
1466
2187
  const signer = await signerManager.getOrCreate(sessionId);
1467
2188
  signer.onInteraction = (interaction) => {
2189
+ debugLog("[LedgerConnector] evm.onInteraction:", interaction);
1468
2190
  ctx.emit("ui-event", {
1469
2191
  type: collapseSignerInteraction(interaction),
1470
2192
  payload: { sessionId }
@@ -1740,6 +2462,7 @@ async function _createBtcSigner(ctx, sessionId) {
1740
2462
  const sdkSigner = new SignerBtcBuilder({ dmk, sessionId }).build();
1741
2463
  const signer = new SignerBtc(sdkSigner);
1742
2464
  signer.onInteraction = (interaction) => {
2465
+ debugLog("[LedgerConnector] btc.onInteraction:", interaction);
1743
2466
  ctx.emit("ui-event", {
1744
2467
  type: collapseSignerInteraction(interaction),
1745
2468
  payload: { sessionId }
@@ -1910,6 +2633,7 @@ async function _createSolSigner(ctx, sessionId) {
1910
2633
  const sdkSigner = new SignerSolanaBuilder({ dmk, sessionId }).withContextModule(contextModule).build();
1911
2634
  const signer = new SignerSol(sdkSigner);
1912
2635
  signer.onInteraction = (interaction) => {
2636
+ debugLog("[LedgerConnector] sol.onInteraction:", interaction);
1913
2637
  ctx.emit("ui-event", {
1914
2638
  type: collapseSignerInteraction(interaction),
1915
2639
  payload: { sessionId }
@@ -1922,14 +2646,15 @@ async function _createSolSigner(ctx, sessionId) {
1922
2646
  }
1923
2647
 
1924
2648
  // src/connector/chains/tron.ts
1925
- var import_hwk_adapter_core10 = require("@onekeyfe/hwk-adapter-core");
2649
+ var import_hwk_adapter_core11 = require("@onekeyfe/hwk-adapter-core");
1926
2650
  var import_hw_app_trx = __toESM(require("@ledgerhq/hw-app-trx"));
1927
2651
 
1928
2652
  // src/connector/chains/legacyChainCall.ts
1929
- var import_hwk_adapter_core9 = require("@onekeyfe/hwk-adapter-core");
2653
+ var import_hwk_adapter_core10 = require("@onekeyfe/hwk-adapter-core");
1930
2654
 
1931
2655
  // src/app/AppManager.ts
1932
2656
  var import_device_management_kit2 = require("@ledgerhq/device-management-kit");
2657
+ var import_hwk_adapter_core9 = require("@onekeyfe/hwk-adapter-core");
1933
2658
  var APP_NAME_MAP = {
1934
2659
  ETH: "Ethereum",
1935
2660
  BTC: "Bitcoin",
@@ -2001,7 +2726,31 @@ var AppManager = class {
2001
2726
  debugLog("[AppManager] currentApp:", result.data.name);
2002
2727
  return result.data.name;
2003
2728
  }
2004
- throw new Error("Failed to get current app from device");
2729
+ const errResult = result;
2730
+ const dmkErr = errResult.error ?? {};
2731
+ const original = dmkErr.originalError;
2732
+ debugLog(
2733
+ "[AppManager] _getCurrentApp failed sessionId=",
2734
+ sessionId,
2735
+ "tag=",
2736
+ dmkErr._tag,
2737
+ "errorCode=",
2738
+ dmkErr.errorCode,
2739
+ "message=",
2740
+ dmkErr.message,
2741
+ "originalErrorMessage=",
2742
+ original?.message ?? String(original ?? "")
2743
+ );
2744
+ throw Object.assign(
2745
+ new Error(
2746
+ dmkErr.message ?? "Failed to get current app from device"
2747
+ ),
2748
+ {
2749
+ _tag: dmkErr._tag,
2750
+ errorCode: dmkErr.errorCode,
2751
+ originalError: original
2752
+ }
2753
+ );
2005
2754
  }
2006
2755
  async _openApp(sessionId, appName) {
2007
2756
  const result = await this._dmk.sendCommand({
@@ -2010,9 +2759,11 @@ var AppManager = class {
2010
2759
  });
2011
2760
  if (!(0, import_device_management_kit2.isSuccessCommandResult)(result)) {
2012
2761
  const { statusCode } = result;
2762
+ const hasStatusCode2 = statusCode != null && statusCode !== "";
2013
2763
  debugLog("[AppManager] openApp failed:", appName, "statusCode:", statusCode);
2014
2764
  throw Object.assign(new Error(`Failed to open "${appName}"`), {
2015
- _tag: "OpenAppCommandError",
2765
+ _tag: ERROR_TAG.OpenAppCommand,
2766
+ code: hasStatusCode2 ? void 0 : import_hwk_adapter_core9.HardwareErrorCode.AppNotInstalled,
2016
2767
  errorCode: String(statusCode ?? ""),
2017
2768
  statusCode,
2018
2769
  appName
@@ -2068,14 +2819,14 @@ async function withLegacyChainCall(ctx, sessionId, options, action) {
2068
2819
  const onAppOpenPrompt = () => {
2069
2820
  openAppPromptShown = true;
2070
2821
  ctx.emit("ui-event", {
2071
- type: import_hwk_adapter_core9.EConnectorInteraction.ConfirmOpenApp,
2822
+ type: import_hwk_adapter_core10.EConnectorInteraction.ConfirmOpenApp,
2072
2823
  payload: { sessionId }
2073
2824
  });
2074
2825
  };
2075
2826
  const closeOpenAppUiIfShown = () => {
2076
2827
  if (openAppPromptShown) {
2077
2828
  ctx.emit("ui-event", {
2078
- type: import_hwk_adapter_core9.EConnectorInteraction.InteractionComplete,
2829
+ type: import_hwk_adapter_core10.EConnectorInteraction.InteractionComplete,
2079
2830
  payload: { sessionId }
2080
2831
  });
2081
2832
  openAppPromptShown = false;
@@ -2096,7 +2847,7 @@ async function withLegacyChainCall(ctx, sessionId, options, action) {
2096
2847
  let confirmEmitted = false;
2097
2848
  if (needsConfirmation) {
2098
2849
  ctx.emit("ui-event", {
2099
- type: import_hwk_adapter_core9.EConnectorInteraction.ConfirmOnDevice,
2850
+ type: import_hwk_adapter_core10.EConnectorInteraction.ConfirmOnDevice,
2100
2851
  payload: { sessionId }
2101
2852
  });
2102
2853
  confirmEmitted = true;
@@ -2106,7 +2857,7 @@ async function withLegacyChainCall(ctx, sessionId, options, action) {
2106
2857
  } finally {
2107
2858
  if (confirmEmitted || openAppPromptShown) {
2108
2859
  ctx.emit("ui-event", {
2109
- type: import_hwk_adapter_core9.EConnectorInteraction.InteractionComplete,
2860
+ type: import_hwk_adapter_core10.EConnectorInteraction.InteractionComplete,
2110
2861
  payload: { sessionId }
2111
2862
  });
2112
2863
  openAppPromptShown = false;
@@ -2194,7 +2945,7 @@ async function tronSignTransaction(ctx, sessionId, params) {
2194
2945
  if (!params.rawTxHex) {
2195
2946
  throw Object.assign(
2196
2947
  new Error("TRON signing requires a protobuf-encoded raw transaction hex (rawTxHex)."),
2197
- { code: import_hwk_adapter_core10.HardwareErrorCode.InvalidParams }
2948
+ { code: import_hwk_adapter_core11.HardwareErrorCode.InvalidParams }
2198
2949
  );
2199
2950
  }
2200
2951
  const path = normalizePath(params.path);
@@ -2238,6 +2989,10 @@ var METHOD_PREFIX_TO_APP_NAME = {
2238
2989
  sol: "Solana",
2239
2990
  tron: "Tron"
2240
2991
  };
2992
+ var HARDWARE_ERROR_CODE_VALUES = new Set(
2993
+ Object.values(import_hwk_adapter_core12.HardwareErrorCode).filter((value) => typeof value === "number")
2994
+ );
2995
+ var BLE_CONNECT_SCAN_TIMEOUT_MS = 1500;
2241
2996
  async function defaultLedgerKitImporter(pkg) {
2242
2997
  switch (pkg) {
2243
2998
  case "@ledgerhq/device-management-kit":
@@ -2261,16 +3016,6 @@ var LedgerConnectorBase = class {
2261
3016
  this._dmk = null;
2262
3017
  this._eventHandlers = /* @__PURE__ */ new Map();
2263
3018
  // ---------------------------------------------------------------------------
2264
- // ConnectId <-> DMK path mapping
2265
- //
2266
- // DMK uses internal paths (BLE MAC, USB UUID) that may change across sessions.
2267
- // _resolveConnectId() maps these to stable external IDs (BLE: "A58F", USB: same).
2268
- // This bidirectional map is the SINGLE SOURCE OF TRUTH for all connectId usage.
2269
- // ---------------------------------------------------------------------------
2270
- this._connectIdToPath = /* @__PURE__ */ new Map();
2271
- // "A58F" -> "D5:75:7D:4B:51:E8"
2272
- this._pathToConnectId = /* @__PURE__ */ new Map();
2273
- // ---------------------------------------------------------------------------
2274
3019
  // Per-session DeviceAction cancellers
2275
3020
  //
2276
3021
  // Each chain handler registers its active DeviceAction's canceller via
@@ -2279,10 +3024,23 @@ var LedgerConnectorBase = class {
2279
3024
  // unsubscribes the observable and releases DMK's IntentQueue slot.
2280
3025
  // ---------------------------------------------------------------------------
2281
3026
  this._cancellers = /* @__PURE__ */ new Map();
3027
+ // ---------------------------------------------------------------------------
3028
+ // Per-session DMK state subscriptions
3029
+ //
3030
+ // DMK only emits `device-disconnect` on the connector when `disconnect()`
3031
+ // is called explicitly. To pick up autonomous disconnects (USB unplug,
3032
+ // BLE drop, device sleep, DMK transport reset) we subscribe to
3033
+ // `_dmk.getDeviceSessionState({sessionId})` per active session and emit
3034
+ // `device-disconnect` ourselves the moment DMK reports the device went
3035
+ // to NOT_CONNECTED. The subscription is torn down on disconnect /
3036
+ // reset / observable completion to avoid leaks.
3037
+ // ---------------------------------------------------------------------------
3038
+ this._sessionStateSubs = /* @__PURE__ */ new Map();
2282
3039
  this._createTransport = createTransport;
2283
3040
  this.connectionType = options?.connectionType ?? "usb";
2284
3041
  this._providedDmk = options?.dmk;
2285
3042
  this._importLedgerKit = options?.importLedgerKit ?? defaultLedgerKitImporter;
3043
+ this._requirePreFlightScan = options?.requirePreFlightScan ?? false;
2286
3044
  if (this._providedDmk) {
2287
3045
  this._initManagers(this._providedDmk);
2288
3046
  }
@@ -2300,38 +3058,23 @@ var LedgerConnectorBase = class {
2300
3058
  importLedgerKit: this._importLedgerKit
2301
3059
  };
2302
3060
  }
2303
- // "D5:75:7D:4B:51:E8" -> "A58F"
2304
- /** Register a connectId <-> path mapping from a device descriptor. */
2305
- _registerDeviceId(descriptor) {
2306
- const connectId = this._resolveConnectId(descriptor);
2307
- this._connectIdToPath.set(connectId, descriptor.path);
2308
- this._pathToConnectId.set(descriptor.path, connectId);
2309
- return connectId;
2310
- }
2311
- /** Get DMK path from external connectId. Falls back to connectId itself. */
2312
- _getPathForConnectId(connectId) {
2313
- return this._connectIdToPath.get(connectId) ?? connectId;
2314
- }
2315
- /** Get external connectId from DMK path. Falls back to path itself. */
2316
- _getConnectIdForPath(path) {
2317
- return this._pathToConnectId.get(path) ?? path;
2318
- }
2319
3061
  // ---------------------------------------------------------------------------
2320
3062
  // Protected — hooks for subclasses
2321
3063
  // ---------------------------------------------------------------------------
2322
3064
  /**
2323
3065
  * Resolve the connectId for a discovered device descriptor.
2324
3066
  * Default: use the DMK path (ephemeral UUID).
2325
- * Override in subclasses to extract stable identifiers (e.g. BLE hex ID).
3067
+ * Override in subclasses only when the public connectId differs from the transport path.
2326
3068
  */
2327
3069
  _resolveConnectId(descriptor) {
2328
3070
  return descriptor.path;
2329
3071
  }
2330
- // ---------------------------------------------------------------------------
2331
- // IConnector -- Device discovery
2332
- // ---------------------------------------------------------------------------
2333
- async searchDevices() {
2334
- const dm = await this._getDeviceManager();
3072
+ /**
3073
+ * Authoritative discovery for this transport. Default = enumerate snapshot
3074
+ * (USB/HID). BLE overrides to drive from a live scan window since DMK's
3075
+ * enumerate() cache survives peripherals going offline.
3076
+ */
3077
+ async _discoverDescriptors(dm) {
2335
3078
  let descriptors = await dm.enumerate();
2336
3079
  if (descriptors.length === 0) {
2337
3080
  try {
@@ -2340,92 +3083,276 @@ var LedgerConnectorBase = class {
2340
3083
  }
2341
3084
  descriptors = await dm.enumerate();
2342
3085
  }
2343
- const result = descriptors.map((d) => {
2344
- const connectId = this._registerDeviceId(d);
2345
- return {
2346
- connectId,
2347
- deviceId: d.path,
2348
- name: d.name || d.type || "Ledger",
2349
- model: d.type
2350
- };
2351
- });
3086
+ return descriptors;
3087
+ }
3088
+ // ---------------------------------------------------------------------------
3089
+ // IConnector -- Device discovery
3090
+ //
3091
+ // Discovery never throws on multiple USB devices — it returns the full list.
3092
+ // The "connect exactly one device" rule is enforced upstream: an explicit
3093
+ // connectId connects that device (or fails as not-found), and only the
3094
+ // no-connectId USB path rejects when more than one device is present
3095
+ // (see LedgerAdapter._connectFirstOrSelect).
3096
+ // ---------------------------------------------------------------------------
3097
+ async searchDevices() {
3098
+ const dm = await this._getDeviceManager();
3099
+ const descriptors = await this._discoverDescriptors(dm);
3100
+ const resolvedDescriptors = descriptors.map((d) => ({
3101
+ descriptor: d,
3102
+ connectId: this._resolveConnectId(d)
3103
+ }));
3104
+ const result = resolvedDescriptors.map(({ descriptor: d, connectId }) => ({
3105
+ connectId,
3106
+ deviceId: d.path,
3107
+ name: d.name || d.type || "Ledger",
3108
+ model: d.type,
3109
+ modelName: d.modelName,
3110
+ rssi: d.rssi,
3111
+ isConnectable: d.isConnectable,
3112
+ serialNumber: d.serialNumber
3113
+ }));
3114
+ debugLog(
3115
+ `[DMK] connector.searchDevices() return count=${result.length} ids=[${result.map((r) => r.connectId).join(",")}] paths=[${result.map((r) => r.deviceId).join(",")}]`
3116
+ );
2352
3117
  return result;
2353
3118
  }
2354
3119
  // ---------------------------------------------------------------------------
2355
3120
  // IConnector -- Connection
2356
3121
  // ---------------------------------------------------------------------------
2357
3122
  async connect(deviceId) {
2358
- const dm = await this._getDeviceManager();
2359
- let discovered = await this.searchDevices();
2360
- const dmkPath = deviceId ? this._getPathForConnectId(deviceId) : void 0;
2361
- let targetPath = dmkPath;
3123
+ const callerSuppliedConnectId = Boolean(deviceId);
3124
+ let targetPath = deviceId;
2362
3125
  if (!targetPath) {
2363
- const descriptors = await dm.enumerate();
2364
- if (descriptors.length === 0) {
3126
+ const discovered = await this.searchDevices();
3127
+ if (discovered.length === 0) {
2365
3128
  throw new Error(
2366
- `No Ledger device found. Make sure the device is connected${this.connectionType === "ble" ? " nearby with Bluetooth enabled" : " via USB"} and unlocked.`
3129
+ `No Ledger device found. Make sure the device is connected${isLedgerBleConnectionType(this.connectionType) ? " nearby with Bluetooth enabled" : " via USB"} and unlocked.`
2367
3130
  );
2368
3131
  }
2369
- targetPath = descriptors[0].path;
2370
- }
2371
- const externalConnectId = this._getConnectIdForPath(targetPath);
3132
+ targetPath = discovered[0].deviceId;
3133
+ }
3134
+ const externalConnectId = targetPath;
3135
+ const HANG_CEILING_MS = 5 * 6e4;
3136
+ const dmConnectWithObserve = async (dm, path) => {
3137
+ if (!isLedgerBleConnectionType(this.connectionType)) return dm.connect(path);
3138
+ let timeoutId;
3139
+ let timedOut = false;
3140
+ const connectPromise = dm.connect(path);
3141
+ const hangPromise = new Promise((_, reject) => {
3142
+ timeoutId = setTimeout(() => {
3143
+ timedOut = true;
3144
+ const err = new Error(
3145
+ `Ledger BLE connect did not return within ${HANG_CEILING_MS / 6e4}min \u2014 DMK hang fallback.`
3146
+ );
3147
+ err._tag = ERROR_TAG.BlePairingTimeout;
3148
+ err.code = import_hwk_adapter_core12.HardwareErrorCode.BlePairingTimeout;
3149
+ reject(err);
3150
+ }, HANG_CEILING_MS);
3151
+ });
3152
+ try {
3153
+ return await Promise.race([connectPromise, hangPromise]);
3154
+ } catch (err) {
3155
+ const e = err;
3156
+ debugLog("[DMK] dm.connect rejected \u2014 observed:", {
3157
+ _tag: e?._tag,
3158
+ message: e?.message,
3159
+ errorCode: e?.errorCode,
3160
+ statusCode: e?.statusCode,
3161
+ originalTag: e?.originalError?._tag,
3162
+ originalMessage: e?.originalError?.message
3163
+ });
3164
+ if (timedOut) {
3165
+ void connectPromise.then(
3166
+ (sessionId) => dm.disconnect(sessionId).catch(() => void 0),
3167
+ () => void 0
3168
+ );
3169
+ }
3170
+ throw err;
3171
+ } finally {
3172
+ if (timeoutId) clearTimeout(timeoutId);
3173
+ }
3174
+ };
3175
+ const isBleDirectConnect = isLedgerBleConnectionType(this.connectionType) && callerSuppliedConnectId;
3176
+ const throwNotAdvertising = () => {
3177
+ const err = new Error(
3178
+ "Ledger device is not currently advertising. Wake up and unlock the device, keep it nearby, then try again."
3179
+ );
3180
+ err._tag = ERROR_TAG.DeviceNotAdvertising;
3181
+ err.code = import_hwk_adapter_core12.HardwareErrorCode.DeviceNotFound;
3182
+ throw err;
3183
+ };
2372
3184
  const doConnect = async (path) => {
2373
- const sessionId = await dm.connect(path);
3185
+ const dm = await this._getDeviceManager();
3186
+ if (isBleDirectConnect && !dm.hasDiscoveredDevice(path)) {
3187
+ const live = await dm.getLiveDevices(BLE_CONNECT_SCAN_TIMEOUT_MS);
3188
+ if (this._requirePreFlightScan && !live.some((d) => d.id === path)) {
3189
+ throwNotAdvertising();
3190
+ }
3191
+ }
3192
+ let sessionId;
3193
+ try {
3194
+ sessionId = await dmConnectWithObserve(dm, path);
3195
+ } catch (err) {
3196
+ if (isBleDirectConnect && err?._tag === ERROR_TAG.DeviceNotInDiscoveryCache) {
3197
+ throwNotAdvertising();
3198
+ }
3199
+ throw err;
3200
+ }
3201
+ try {
3202
+ this._watchSessionState(sessionId, externalConnectId);
3203
+ } catch (subErr) {
3204
+ debugLog("[DMK] state subscription failed during connect; disconnecting session:", subErr);
3205
+ try {
3206
+ await dm.disconnect(sessionId);
3207
+ } catch {
3208
+ }
3209
+ throw subErr;
3210
+ }
3211
+ const info = dm.getDiscoveredDeviceInfo(path);
2374
3212
  const session = {
2375
3213
  sessionId,
2376
3214
  deviceInfo: {
2377
3215
  vendor: "ledger",
2378
- model: "unknown",
3216
+ model: info?.model ?? "unknown",
3217
+ modelName: info?.modelName,
2379
3218
  firmwareVersion: "unknown",
2380
3219
  deviceId: path,
2381
3220
  connectId: externalConnectId,
2382
3221
  connectionType: this.connectionType,
3222
+ rssi: info?.rssi,
2383
3223
  capabilities: { persistentDeviceIdentity: false }
2384
3224
  }
2385
3225
  };
2386
- const realName = discovered.find((d) => d.connectId === externalConnectId)?.name ?? "Ledger";
3226
+ const name = dm.getDeviceName(path) ?? "Ledger";
2387
3227
  this._emit("device-connect", {
2388
- device: {
2389
- connectId: externalConnectId,
2390
- deviceId: path,
2391
- name: realName
2392
- }
3228
+ device: { connectId: externalConnectId, deviceId: path, name }
2393
3229
  });
2394
3230
  return session;
2395
3231
  };
2396
3232
  try {
2397
3233
  return await doConnect(targetPath);
2398
- } catch {
3234
+ } catch (err) {
2399
3235
  this._resetSignersAndSessions();
2400
- const dm2 = await this._getDeviceManager();
2401
- discovered = await this.searchDevices();
2402
- const retryPath = this._getPathForConnectId(externalConnectId);
2403
- if (!retryPath || retryPath === externalConnectId) {
2404
- const descriptors = await dm2.enumerate();
2405
- if (descriptors.length === 0) {
2406
- throw new Error(
2407
- `No Ledger device found after retry. Make sure the device is connected${this.connectionType === "ble" ? " nearby with Bluetooth enabled" : " via USB"} and unlocked.`
2408
- );
3236
+ if (isLedgerBleConnectionType(this.connectionType)) {
3237
+ const tag = err?._tag;
3238
+ if (isKnownConnectionTag(tag)) {
3239
+ throw err;
2409
3240
  }
2410
- return doConnect(descriptors[0].path);
3241
+ const wrapped = new Error(
3242
+ "Ledger Bluetooth pairing failed. Make sure the device is unlocked and nearby, then try again."
3243
+ );
3244
+ wrapped._tag = ERROR_TAG.BleGattBondingFailed;
3245
+ wrapped.code = import_hwk_adapter_core12.HardwareErrorCode.BlePairingTimeout;
3246
+ wrapped.originalError = err;
3247
+ throw wrapped;
2411
3248
  }
2412
- return doConnect(retryPath);
3249
+ if (err && typeof err === "object" && err._tag) {
3250
+ const taggedError = err instanceof Error ? err : Object.assign(
3251
+ new Error(err.message ?? "Ledger device error"),
3252
+ err
3253
+ );
3254
+ throw taggedError;
3255
+ }
3256
+ throw new Error("Ledger device appears unplugged. Plug in the device and try again.");
2413
3257
  }
2414
3258
  }
2415
3259
  async disconnect(sessionId) {
2416
3260
  if (!this._deviceManager) return;
2417
3261
  const deviceId = this._deviceManager.getDeviceId(sessionId);
2418
3262
  this._signerManager?.invalidate(sessionId);
3263
+ this._unwatchSessionState(sessionId);
2419
3264
  await this._deviceManager.disconnect(sessionId);
2420
3265
  if (deviceId) {
2421
3266
  this._emit("device-disconnect", { connectId: deviceId });
2422
3267
  }
2423
3268
  }
3269
+ /**
3270
+ * Subscribe to DMK's per-session state observable so that any autonomous
3271
+ * disconnect (USB unplug, BLE drop, device sleep, transport reset) is
3272
+ * surfaced as a `device-disconnect` event — without this, the upstream
3273
+ * `LedgerAdapter._sessions` map would hold a dead session entry until
3274
+ * the next call hit `DeviceSessionNotFound`.
3275
+ *
3276
+ * Subscribe failure is fatal — see the inline note at the subscribe call.
3277
+ */
3278
+ _watchSessionState(sessionId, externalConnectId) {
3279
+ const dmk = this._dmk;
3280
+ if (!dmk) return;
3281
+ const previous = this._sessionStateSubs.get(sessionId);
3282
+ if (previous) {
3283
+ try {
3284
+ previous.unsubscribe();
3285
+ } catch {
3286
+ }
3287
+ }
3288
+ const sub = dmk.getDeviceSessionState({ sessionId }).subscribe({
3289
+ next: (state) => {
3290
+ if (state?.deviceStatus === "NOT CONNECTED") {
3291
+ this._handleAutonomousDisconnect(sessionId, externalConnectId);
3292
+ }
3293
+ },
3294
+ error: () => {
3295
+ this._handleAutonomousDisconnect(sessionId, externalConnectId);
3296
+ },
3297
+ complete: () => {
3298
+ this._handleAutonomousDisconnect(sessionId, externalConnectId);
3299
+ }
3300
+ });
3301
+ this._sessionStateSubs.set(sessionId, sub);
3302
+ }
3303
+ _unwatchSessionState(sessionId) {
3304
+ const sub = this._sessionStateSubs.get(sessionId);
3305
+ if (!sub) return;
3306
+ try {
3307
+ sub.unsubscribe();
3308
+ } catch {
3309
+ }
3310
+ this._sessionStateSubs.delete(sessionId);
3311
+ }
3312
+ _handleAutonomousDisconnect(sessionId, externalConnectId) {
3313
+ if (!this._sessionStateSubs.has(sessionId)) return;
3314
+ debugLog(
3315
+ "[DMK] autonomous disconnect detected \u2014 sessionId:",
3316
+ sessionId,
3317
+ "connectId:",
3318
+ externalConnectId
3319
+ );
3320
+ this._unwatchSessionState(sessionId);
3321
+ this._signerManager?.invalidate(sessionId);
3322
+ this._cancellers.get(sessionId)?.({
3323
+ code: import_hwk_adapter_core12.HardwareErrorCode.DeviceDisconnected,
3324
+ tag: "DeviceDisconnected",
3325
+ message: "Device disconnected"
3326
+ });
3327
+ this._cancellers.delete(sessionId);
3328
+ this._emit("device-disconnect", { connectId: externalConnectId });
3329
+ }
2424
3330
  // ---------------------------------------------------------------------------
2425
3331
  // IConnector -- Method dispatch
2426
3332
  // ---------------------------------------------------------------------------
2427
3333
  async call(sessionId, method, params) {
2428
3334
  debugLog("[DMK] call:", method, JSON.stringify(params));
3335
+ try {
3336
+ return await this._dispatch(sessionId, method, params);
3337
+ } catch (err) {
3338
+ if (isAppStuckByApdu(err)) {
3339
+ throw Object.assign(new Error("Ledger app is unresponsive"), {
3340
+ code: import_hwk_adapter_core12.HardwareErrorCode.DeviceAppStuck,
3341
+ _tag: ERROR_TAG.DeviceAppStuck,
3342
+ originalError: err
3343
+ });
3344
+ }
3345
+ if (isTransportStuck(err)) {
3346
+ throw Object.assign(new Error("Device communication interrupted, please retry"), {
3347
+ code: import_hwk_adapter_core12.HardwareErrorCode.TransportError,
3348
+ _tag: ERROR_TAG.DeviceTransportStuck,
3349
+ originalError: err
3350
+ });
3351
+ }
3352
+ throw err;
3353
+ }
3354
+ }
3355
+ async _dispatch(sessionId, method, params) {
2429
3356
  const ctx = this._ctxForMethod(method);
2430
3357
  switch (method) {
2431
3358
  // EVM
@@ -2565,14 +3492,13 @@ var LedgerConnectorBase = class {
2565
3492
  }
2566
3493
  /**
2567
3494
  * Replace an old session with a new one after app switch.
2568
- * Updates the connectId→path mapping so subsequent calls() use the new session,
2569
- * and emits device-connect so the adapter updates its _sessions Map.
3495
+ * Emits device-connect so the adapter updates its _sessions Map.
2570
3496
  */
2571
3497
  _replaceSession(oldSessionId, _newSessionId) {
2572
3498
  const dm = this._deviceManager;
2573
3499
  if (!dm) return;
2574
3500
  const oldDeviceId = dm.getDeviceId(oldSessionId);
2575
- const connectId = oldDeviceId ? this._pathToConnectId.get(oldDeviceId) : void 0;
3501
+ const connectId = oldDeviceId;
2576
3502
  this._signerManager?.invalidate(oldSessionId);
2577
3503
  if (connectId) {
2578
3504
  this._emit("device-connect", {
@@ -2585,13 +3511,18 @@ var LedgerConnectorBase = class {
2585
3511
  }
2586
3512
  }
2587
3513
  /**
2588
- * Light reset: clear signer/session state but keep DMK, BLE scan, and ID mapping alive.
3514
+ * Light reset: clear signer/session state but keep DMK alive.
2589
3515
  * Used by connect() retry — we want to re-discover with the same transport.
3516
+ *
3517
+ * Note: drops the device manager but tears down its RxJS subs first via
3518
+ * disposeKeepingDmk(). Bare null-assignment would leak _listenSub /
3519
+ * _discoverySub and double-scan after the next _initManagers().
2590
3520
  */
2591
3521
  _resetSignersAndSessions() {
2592
3522
  debugLog("[DMK] _resetSignersAndSessions called");
2593
3523
  this._signerManager?.clearAll();
2594
3524
  this._signerManager = null;
3525
+ this._deviceManager?.disposeKeepingDmk();
2595
3526
  this._deviceManager = null;
2596
3527
  }
2597
3528
  _resetAll() {
@@ -2603,13 +3534,18 @@ var LedgerConnectorBase = class {
2603
3534
  }
2604
3535
  }
2605
3536
  this._cancellers.clear();
3537
+ for (const sub of this._sessionStateSubs.values()) {
3538
+ try {
3539
+ sub.unsubscribe();
3540
+ } catch {
3541
+ }
3542
+ }
3543
+ this._sessionStateSubs.clear();
2606
3544
  this._signerManager?.clearAll();
2607
3545
  this._deviceManager?.dispose();
2608
3546
  this._deviceManager = null;
2609
3547
  this._signerManager = null;
2610
3548
  this._dmk = null;
2611
- this._connectIdToPath.clear();
2612
- this._pathToConnectId.clear();
2613
3549
  }
2614
3550
  // ---------------------------------------------------------------------------
2615
3551
  // Private -- Events
@@ -2643,15 +3579,21 @@ var LedgerConnectorBase = class {
2643
3579
  };
2644
3580
  }
2645
3581
  _wrapError(err, opts) {
2646
- const mapped = mapLedgerError(err, opts);
2647
- const error = new Error(mapped.message);
2648
3582
  const src = err && typeof err === "object" ? err : {};
3583
+ const hasSerializedCode = typeof src.code === "number" && HARDWARE_ERROR_CODE_VALUES.has(src.code);
3584
+ const mapped = hasSerializedCode ? {
3585
+ code: src.code,
3586
+ message: typeof src.message === "string" ? src.message : "Unknown Ledger error",
3587
+ appName: typeof src.appName === "string" ? src.appName : opts?.defaultAppName
3588
+ } : mapLedgerError(err, opts);
3589
+ const error = new Error(mapped.message);
2649
3590
  Object.assign(error, {
2650
3591
  code: mapped.code,
2651
3592
  appName: mapped.appName,
2652
3593
  _tag: src._tag,
2653
3594
  errorCode: src.errorCode,
2654
- _lastStep: src._lastStep
3595
+ _lastStep: src._lastStep,
3596
+ _deviceActionSteps: src._deviceActionSteps
2655
3597
  });
2656
3598
  Object.defineProperty(error, "originalError", {
2657
3599
  value: err,
@@ -2662,13 +3604,6 @@ var LedgerConnectorBase = class {
2662
3604
  return error;
2663
3605
  }
2664
3606
  };
2665
-
2666
- // src/utils/bleIdentity.ts
2667
- function extractBleHexId(name) {
2668
- if (!name) return void 0;
2669
- const match = name.match(/\b([0-9A-Fa-f]{4})$/);
2670
- return match ? match[1].toUpperCase() : void 0;
2671
- }
2672
3607
  // Annotate the CommonJS export names for ESM import in node:
2673
3608
  0 && (module.exports = {
2674
3609
  AppManager,
@@ -2680,12 +3615,15 @@ function extractBleHexId(name) {
2680
3615
  SignerEth,
2681
3616
  SignerManager,
2682
3617
  SignerSol,
3618
+ debugLog,
2683
3619
  deviceActionToPromise,
2684
- extractBleHexId,
2685
- isDebugEnabled,
2686
3620
  isDeviceLockedError,
3621
+ isLedgerBleConnectionType,
3622
+ isLedgerBleDescriptor,
3623
+ isLedgerDmkBleTransport,
2687
3624
  ledgerFailure,
2688
3625
  mapLedgerError,
2689
- setDebugEnabled
3626
+ offSdkEvent,
3627
+ onSdkEvent
2690
3628
  });
2691
3629
  //# sourceMappingURL=index.js.map