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

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;
468
+ }
469
+ try {
470
+ return JSON.stringify(value);
471
+ } catch {
472
+ return String(value);
272
473
  }
273
474
  }
274
475
 
476
+ // src/utils/debugLog.ts
477
+ function debugLog(...args) {
478
+ emitLog("debug", ...args);
479
+ }
480
+ function debugError(...args) {
481
+ emitLog("error", ...args);
482
+ }
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,102 +872,252 @@ 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;
734
964
  }
735
965
  }
736
- async _doConnect() {
737
- for (let attempt = 0; attempt < _LedgerAdapter.MAX_DEVICE_RETRY; attempt++) {
738
- const devices = await this.searchDevices();
739
- if (devices.length > 0) {
740
- return this._connectFirstOrSelect(devices);
741
- }
742
- if (attempt < _LedgerAdapter.MAX_DEVICE_RETRY - 1) {
743
- await this._waitForDeviceConnect();
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
+ );
744
985
  }
986
+ return this._sessions.keys().next().value;
745
987
  }
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
- );
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
+ })();
999
+ }
1000
+ return this._abortable(signal, this._connectingPromise);
752
1001
  }
753
- async _connectFirstOrSelect(devices) {
754
- const chosenConnectId = devices.length === 1 ? devices[0].connectId : await this._chooseDeviceFromList(devices);
755
- const result = await this.connectDevice(chosenConnectId);
756
- if (!result.success) {
757
- throw Object.assign(new Error(result.payload.error), {
758
- _tag: "DeviceNotRecognizedError"
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: "" }
759
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
+ }
1038
+ if (devices.length > 0) {
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
+ }
1053
+ }
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
+ );
1061
+ }
1062
+ await this._waitForDeviceConnect(internalSignal);
1063
+ confirms += 1;
760
1064
  }
761
- return chosenConnectId;
1065
+ _LedgerAdapter._throwIfAborted(internalSignal);
1066
+ throw new Error("_doConnect aborted");
762
1067
  }
763
- async _chooseDeviceFromList(devices) {
764
- if (!this._handleSelectDevice) {
1068
+ async _connectFirstOrSelect(devices, targetConnectId) {
1069
+ if (!isLedgerBleConnectionType(this.connector.connectionType) && devices.length > 1) {
765
1070
  debugLog(
766
- `[DMK] Multiple Ledger devices found (${devices.length}); handleSelectDevice=false, picking first (${devices[0].connectId}).`
1071
+ `[DMK] Multiple Ledger USB devices found (${devices.length}); refusing to auto-select by ephemeral connectId.`
767
1072
  );
768
- return devices[0].connectId;
1073
+ throw createMultipleUsbLedgerDevicesError();
769
1074
  }
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" }
1075
+ if (targetConnectId) {
1076
+ const target = devices.find(
1077
+ (d) => d.connectId === targetConnectId || d.deviceId === targetConnectId
782
1078
  );
1079
+ if (target) {
1080
+ return this._connectDeviceOrThrow(target.connectId);
1081
+ }
1082
+ if (!isLedgerBleConnectionType(this.connector.connectionType) && devices.length === 1) {
1083
+ debugLog(
1084
+ `[LedgerAdapter] target ${targetConnectId} not in fresh enumeration; accepting sole USB device ${devices[0].connectId} (assumed ephemeral path change)`
1085
+ );
1086
+ return this._connectDeviceOrThrow(devices[0].connectId);
1087
+ }
1088
+ const err = Object.assign(new Error(`Target Ledger unavailable: ${targetConnectId}`), {
1089
+ code: import_hwk_adapter_core2.HardwareErrorCode.DeviceNotFound
1090
+ });
1091
+ if (isLedgerBleConnectionType(this.connector.connectionType)) {
1092
+ err._tag = ERROR_TAG.DeviceNotAdvertising;
1093
+ }
1094
+ throw err;
783
1095
  }
784
- return chosen.connectId;
1096
+ if (isLedgerBleConnectionType(this.connector.connectionType)) {
1097
+ throw Object.assign(new Error("Ledger BLE connectId is required."), {
1098
+ code: import_hwk_adapter_core2.HardwareErrorCode.DeviceNotFound
1099
+ });
1100
+ }
1101
+ if (devices.length !== 1) {
1102
+ throw Object.assign(new Error("Ledger device not found."), {
1103
+ code: import_hwk_adapter_core2.HardwareErrorCode.DeviceNotFound
1104
+ });
1105
+ }
1106
+ return this._connectDeviceOrThrow(devices[0].connectId);
1107
+ }
1108
+ async _connectDeviceOrThrow(chosenConnectId) {
1109
+ const result = await this.connectDevice(chosenConnectId);
1110
+ if (!result.success) {
1111
+ const payload = result.payload;
1112
+ const rethrow = Object.assign(new Error(payload.error), {
1113
+ code: payload.code
1114
+ });
1115
+ if (payload._tag !== void 0) {
1116
+ rethrow._tag = payload._tag;
1117
+ }
1118
+ throw rethrow;
1119
+ }
1120
+ return chosenConnectId;
785
1121
  }
786
1122
  /**
787
1123
  * Call the connector with automatic session resolution and disconnect retry.
@@ -791,30 +1127,31 @@ var _LedgerAdapter = class _LedgerAdapter {
791
1127
  * 3. Calls connector.call()
792
1128
  * 4. On disconnect error: clears stale session, re-connects, retries once
793
1129
  */
794
- async connectorCall(connectId, method, params, fingerprint) {
1130
+ async connectorCall(connectId, method, params, fingerprint, permissionDeviceId) {
795
1131
  debugLog("[LedgerAdapter] connectorCall:", method, "connectId:", connectId || "(empty)");
796
1132
  const queueKey = connectId || "__ledger_default__";
797
- const interruptibility = _LedgerAdapter._getInterruptibility(method);
798
1133
  return this._jobQueue.enqueue(
799
1134
  queueKey,
800
- async (signal) => this._runConnectorCall(connectId, method, params, signal, fingerprint),
801
- { interruptibility, label: method }
1135
+ async (signal) => this._runConnectorCall(connectId, method, params, signal, fingerprint, permissionDeviceId),
1136
+ {
1137
+ label: method,
1138
+ rejectIfBusy: true,
1139
+ busyError: _LedgerAdapter._createDeviceBusyError(method)
1140
+ }
802
1141
  );
803
1142
  }
804
1143
  /**
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.
1144
+ * Race a promise against an abort signal. On abort, rejects with
1145
+ * signal.reason instance _lastCancelReason → generic Error('Aborted').
808
1146
  */
809
- static _abortable(signal, promise) {
1147
+ _abortable(signal, promise) {
1148
+ const getAbortReason = () => signal.reason ?? this._lastCancelReason ?? new Error("Aborted");
810
1149
  if (signal.aborted) {
811
- return Promise.reject(
812
- signal.reason ?? new Error("Aborted")
813
- );
1150
+ return Promise.reject(getAbortReason());
814
1151
  }
815
1152
  return new Promise((resolve, reject) => {
816
1153
  const onAbort = () => {
817
- reject(signal.reason ?? new Error("Aborted"));
1154
+ reject(getAbortReason());
818
1155
  };
819
1156
  signal.addEventListener("abort", onAbort, { once: true });
820
1157
  promise.then(
@@ -836,31 +1173,195 @@ var _LedgerAdapter = class _LedgerAdapter {
836
1173
  }
837
1174
  }
838
1175
  /** Actual work done under the job queue — connection, fingerprint, call, and recovery. */
839
- async _runConnectorCall(connectId, method, params, signal, fingerprint) {
1176
+ async _runConnectorCall(connectId, method, params, signal, fingerprint, permissionDeviceId) {
840
1177
  _LedgerAdapter._throwIfAborted(signal);
841
- const resolvedConnectId = await _LedgerAdapter._abortable(
842
- signal,
843
- this.ensureConnected(connectId)
1178
+ await this._ensureDevicePermission(
1179
+ connectId,
1180
+ permissionDeviceId ?? fingerprint?.deviceId,
1181
+ signal
844
1182
  );
845
1183
  _LedgerAdapter._throwIfAborted(signal);
1184
+ let effectiveParams = params;
1185
+ if (method === "btcGetPublicKey") {
1186
+ const gatedParams = await this._gateBtcHighIndex(params);
1187
+ if (gatedParams === null) {
1188
+ throw Object.assign(new Error("User cancelled BTC high-index confirmation"), {
1189
+ _tag: ERROR_TAG.UserAborted,
1190
+ code: import_hwk_adapter_core2.HardwareErrorCode.UserAborted
1191
+ });
1192
+ }
1193
+ effectiveParams = gatedParams;
1194
+ }
1195
+ const resolvedConnectId = await this.ensureConnected(connectId, signal);
846
1196
  const sessionId = this._sessions.get(resolvedConnectId);
847
- debugLog(
848
- "[LedgerAdapter] connectorCall resolved:",
849
- method,
850
- "resolvedConnectId:",
851
- resolvedConnectId,
852
- "sessionId:",
853
- sessionId
854
- );
855
1197
  if (!sessionId) {
856
1198
  throw Object.assign(new Error("Auto-connect succeeded but no session found"), {
857
- _tag: "DeviceSessionNotFound"
1199
+ _tag: ERROR_TAG.DeviceSessionNotFound
858
1200
  });
859
1201
  }
1202
+ try {
1203
+ if (fingerprint && !fingerprint.skipFingerprint && fingerprint.deviceId) {
1204
+ const fp = await this._abortable(
1205
+ signal,
1206
+ this._verifyDeviceFingerprintWithSession(
1207
+ sessionId,
1208
+ fingerprint.deviceId,
1209
+ fingerprint.chain
1210
+ )
1211
+ );
1212
+ if (!fp.success) {
1213
+ throw Object.assign(new Error(formatDeviceMismatchError(fp.expected, fp.actual)), {
1214
+ code: import_hwk_adapter_core2.HardwareErrorCode.DeviceMismatch
1215
+ });
1216
+ }
1217
+ }
1218
+ return await this._abortable(signal, this.connector.call(sessionId, method, effectiveParams));
1219
+ } catch (err) {
1220
+ if (signal.aborted) throw err;
1221
+ const errObj = err;
1222
+ debugLog("[LedgerAdapter] connectorCall error:", method, {
1223
+ message: errObj?.message,
1224
+ _tag: errObj?._tag,
1225
+ errorCode: errObj?.errorCode,
1226
+ statusCode: errObj?.statusCode,
1227
+ isDisconnected: isDeviceDisconnectedError(err),
1228
+ isLocked: isDeviceLockedError(err),
1229
+ isNotAdvertising: isDeviceNotAdvertisingError(err),
1230
+ isStuckApp: isStuckAppStateError(err)
1231
+ });
1232
+ if (isStuckAppStateError(err)) {
1233
+ try {
1234
+ this._sessions.delete(resolvedConnectId);
1235
+ this._discoveredDevices.delete(resolvedConnectId);
1236
+ this.connector.reset?.();
1237
+ } catch {
1238
+ }
1239
+ debugLog(
1240
+ "[LedgerAdapter] stuck-app retry: method=",
1241
+ method,
1242
+ "delayMs=",
1243
+ _LedgerAdapter.STUCK_APP_RETRY_DELAY_MS,
1244
+ "_tag=",
1245
+ err?._tag
1246
+ );
1247
+ try {
1248
+ const retryResult = await this._retryAfterStuckApp(
1249
+ resolvedConnectId,
1250
+ method,
1251
+ effectiveParams,
1252
+ signal,
1253
+ err,
1254
+ fingerprint
1255
+ );
1256
+ debugLog("[LedgerAdapter] stuck-app retry succeeded: method=", method);
1257
+ return retryResult;
1258
+ } catch (retryErr) {
1259
+ if (signal.aborted) throw retryErr;
1260
+ if (isStuckAppStateError(retryErr)) {
1261
+ debugLog("[LedgerAdapter] stuck-app retry exhausted (2nd 6901): method=", method);
1262
+ throw err;
1263
+ }
1264
+ debugLog(
1265
+ "[LedgerAdapter] stuck-app retry threw non-stuck error: method=",
1266
+ method,
1267
+ "retryErrTag=",
1268
+ retryErr?._tag
1269
+ );
1270
+ throw retryErr;
1271
+ }
1272
+ }
1273
+ if (isDeviceLockedError(err) || isDeviceNotAdvertisingError(err) || isDeviceDisconnectedError(err)) {
1274
+ let lastErr = err;
1275
+ for (let attempt = 0; attempt < _LedgerAdapter.MAX_BUSINESS_RETRY_BUDGET; attempt += 1) {
1276
+ if (signal.aborted) throw lastErr;
1277
+ try {
1278
+ this._sessions.delete(resolvedConnectId);
1279
+ this._discoveredDevices.delete(resolvedConnectId);
1280
+ if (isDeviceDisconnectedError(lastErr)) {
1281
+ try {
1282
+ this.connector.reset?.();
1283
+ } catch {
1284
+ }
1285
+ }
1286
+ const reConnectId = await this.ensureConnected(resolvedConnectId, signal);
1287
+ const reSessionId = this._sessions.get(reConnectId);
1288
+ if (!reSessionId) throw lastErr;
1289
+ if (fingerprint && !fingerprint.skipFingerprint && fingerprint.deviceId) {
1290
+ const fp = await this._abortable(
1291
+ signal,
1292
+ this._verifyDeviceFingerprintWithSession(
1293
+ reSessionId,
1294
+ fingerprint.deviceId,
1295
+ fingerprint.chain
1296
+ )
1297
+ );
1298
+ if (!fp.success) {
1299
+ throw Object.assign(new Error(formatDeviceMismatchError(fp.expected, fp.actual)), {
1300
+ code: import_hwk_adapter_core2.HardwareErrorCode.DeviceMismatch
1301
+ });
1302
+ }
1303
+ }
1304
+ return await this._abortable(
1305
+ signal,
1306
+ this.connector.call(reSessionId, method, effectiveParams)
1307
+ );
1308
+ } catch (retryErr) {
1309
+ if (signal.aborted) throw retryErr;
1310
+ lastErr = retryErr;
1311
+ const canRetry = attempt < _LedgerAdapter.MAX_BUSINESS_RETRY_BUDGET - 1 && (isDeviceLockedError(retryErr) || isDeviceNotAdvertisingError(retryErr) || isDeviceDisconnectedError(retryErr));
1312
+ if (!canRetry) {
1313
+ throw retryErr;
1314
+ }
1315
+ }
1316
+ }
1317
+ throw lastErr;
1318
+ }
1319
+ if (isTimeoutError(err)) {
1320
+ debugLog("[LedgerAdapter] timeout, retrying with fresh connection...");
1321
+ this._discoveredDevices.delete(resolvedConnectId);
1322
+ return this._retryWithFreshConnection(
1323
+ resolvedConnectId,
1324
+ method,
1325
+ effectiveParams,
1326
+ signal,
1327
+ err,
1328
+ fingerprint
1329
+ );
1330
+ }
1331
+ if (isConnectionLevelError(err)) {
1332
+ debugLog("[LedgerAdapter] connection-level fail-closed reset");
1333
+ this._sessions.delete(resolvedConnectId);
1334
+ this._discoveredDevices.delete(resolvedConnectId);
1335
+ this.connector.reset();
1336
+ const codeNum = err?.code;
1337
+ throw Object.assign(err, {
1338
+ code: codeNum ?? import_hwk_adapter_core2.HardwareErrorCode.DeviceDisconnected
1339
+ });
1340
+ }
1341
+ throw err;
1342
+ }
1343
+ }
1344
+ /**
1345
+ * Stuck-app recovery: pause for the device's UI transition, then retry once.
1346
+ *
1347
+ * Caller has already cleared the session + reset connector. We wait so Stax
1348
+ * finishes its post-CloseApp animation, then go through ensureConnected +
1349
+ * fingerprint check + call exactly once. Caller decides what to do on a
1350
+ * second stuck-app hit.
1351
+ */
1352
+ async _retryAfterStuckApp(resolvedConnectId, method, params, signal, originalErr, fingerprint) {
1353
+ await this._sleepAbortable(_LedgerAdapter.STUCK_APP_RETRY_DELAY_MS, signal);
1354
+ const retryConnectId = await this.ensureConnected(resolvedConnectId, signal);
1355
+ const retrySessionId = this._sessions.get(retryConnectId);
1356
+ if (!retrySessionId) throw originalErr;
860
1357
  if (fingerprint && !fingerprint.skipFingerprint && fingerprint.deviceId) {
861
- const fp = await _LedgerAdapter._abortable(
1358
+ const fp = await this._abortable(
862
1359
  signal,
863
- this._verifyDeviceFingerprintWithSession(sessionId, fingerprint.deviceId, fingerprint.chain)
1360
+ this._verifyDeviceFingerprintWithSession(
1361
+ retrySessionId,
1362
+ fingerprint.deviceId,
1363
+ fingerprint.chain
1364
+ )
864
1365
  );
865
1366
  if (!fp.success) {
866
1367
  throw Object.assign(new Error(formatDeviceMismatchError(fp.expected, fp.actual)), {
@@ -868,48 +1369,97 @@ var _LedgerAdapter = class _LedgerAdapter {
868
1369
  });
869
1370
  }
870
1371
  }
871
- try {
872
- return await _LedgerAdapter._abortable(signal, this.connector.call(sessionId, method, params));
873
- } catch (err) {
874
- if (signal.aborted) throw err;
875
- const errObj = err;
876
- debugLog("[LedgerAdapter] connectorCall error:", method, {
877
- message: errObj?.message,
878
- _tag: errObj?._tag,
879
- errorCode: errObj?.errorCode,
880
- statusCode: errObj?.statusCode,
881
- isDisconnected: isDeviceDisconnectedError(err),
882
- isLocked: isDeviceLockedError(err)
883
- });
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);
888
- }
889
- if (isDeviceLockedError(err)) {
890
- await this._waitForDeviceConnect(signal);
891
- _LedgerAdapter._throwIfAborted(signal);
892
- return _LedgerAdapter._abortable(signal, this.connector.call(sessionId, method, params));
893
- }
894
- if (isTimeoutError(err)) {
895
- debugLog("[LedgerAdapter] timeout, retrying with fresh connection...");
896
- this._discoveredDevices.delete(resolvedConnectId);
897
- return this._retryWithFreshConnection(resolvedConnectId, method, params, signal, err);
1372
+ return this._abortable(signal, this.connector.call(retrySessionId, method, params));
1373
+ }
1374
+ _sleepAbortable(ms, signal) {
1375
+ return new Promise((resolve, reject) => {
1376
+ if (signal.aborted) {
1377
+ reject(signal.reason ?? new Error("Aborted"));
1378
+ return;
898
1379
  }
899
- throw err;
900
- }
1380
+ const timer = setTimeout(() => {
1381
+ signal.removeEventListener("abort", onAbort);
1382
+ resolve();
1383
+ }, ms);
1384
+ const onAbort = () => {
1385
+ clearTimeout(timer);
1386
+ reject(signal.reason ?? new Error("Aborted"));
1387
+ };
1388
+ signal.addEventListener("abort", onAbort, { once: true });
1389
+ });
901
1390
  }
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);
1391
+ /**
1392
+ * Clear stale session, reconnect, and retry the call.
1393
+ *
1394
+ * Timeout recovery starts with a full connector reset. After an APDU
1395
+ * timeout, DMK/transport state may still emit malformed responses; retrying
1396
+ * on the same DMK can poison the next chain switch.
1397
+ */
1398
+ async _retryWithFreshConnection(targetConnectId, method, params, signal, originalErr, fingerprint) {
1399
+ this.connector.reset();
1400
+ this._sessions.clear();
1401
+ this._discoveredDevices.clear();
1402
+ this._connectingPromise = null;
1403
+ const retryConnectId = await this.ensureConnected(targetConnectId, signal);
908
1404
  const retrySessionId = this._sessions.get(retryConnectId);
909
1405
  if (!retrySessionId) {
910
1406
  throw originalErr;
911
1407
  }
912
- return _LedgerAdapter._abortable(signal, this.connector.call(retrySessionId, method, params));
1408
+ try {
1409
+ if (fingerprint && !fingerprint.skipFingerprint && fingerprint.deviceId) {
1410
+ const fp = await this._abortable(
1411
+ signal,
1412
+ this._verifyDeviceFingerprintWithSession(
1413
+ retrySessionId,
1414
+ fingerprint.deviceId,
1415
+ fingerprint.chain
1416
+ )
1417
+ );
1418
+ if (!fp.success) {
1419
+ throw Object.assign(new Error(formatDeviceMismatchError(fp.expected, fp.actual)), {
1420
+ code: import_hwk_adapter_core2.HardwareErrorCode.DeviceMismatch
1421
+ });
1422
+ }
1423
+ }
1424
+ return await this._abortable(signal, this.connector.call(retrySessionId, method, params));
1425
+ } catch (retryErr) {
1426
+ if (signal.aborted) throw retryErr;
1427
+ this.connector.reset();
1428
+ this._sessions.clear();
1429
+ this._discoveredDevices.clear();
1430
+ this._connectingPromise = null;
1431
+ if (!isDeviceDisconnectedError(retryErr) && !isTimeoutError(retryErr)) {
1432
+ throw retryErr;
1433
+ }
1434
+ debugLog(
1435
+ "[LedgerAdapter] fresh-session retry still failed; resetting connector and rebuilding DMK"
1436
+ );
1437
+ this.connector.reset();
1438
+ this._sessions.clear();
1439
+ this._discoveredDevices.clear();
1440
+ this._connectingPromise = null;
1441
+ const finalConnectId = await this.ensureConnected(targetConnectId, signal);
1442
+ const finalSessionId = this._sessions.get(finalConnectId);
1443
+ if (!finalSessionId) {
1444
+ throw originalErr;
1445
+ }
1446
+ if (fingerprint && !fingerprint.skipFingerprint && fingerprint.deviceId) {
1447
+ const fp = await this._abortable(
1448
+ signal,
1449
+ this._verifyDeviceFingerprintWithSession(
1450
+ finalSessionId,
1451
+ fingerprint.deviceId,
1452
+ fingerprint.chain
1453
+ )
1454
+ );
1455
+ if (!fp.success) {
1456
+ throw Object.assign(new Error(formatDeviceMismatchError(fp.expected, fp.actual)), {
1457
+ code: import_hwk_adapter_core2.HardwareErrorCode.DeviceMismatch
1458
+ });
1459
+ }
1460
+ }
1461
+ return this._abortable(signal, this.connector.call(finalSessionId, method, params));
1462
+ }
913
1463
  }
914
1464
  /**
915
1465
  * Ensure OS-level device permission (Bluetooth / USB) before proceeding.
@@ -923,7 +1473,10 @@ var _LedgerAdapter = class _LedgerAdapter {
923
1473
  * - No connectId (searchDevices): environment-level permission
924
1474
  * - With connectId (business methods): device-level permission
925
1475
  */
926
- async _ensureDevicePermission(connectId, deviceId) {
1476
+ async _ensureDevicePermission(connectId, deviceId, signal) {
1477
+ if (signal?.aborted) {
1478
+ _LedgerAdapter._throwIfAborted(signal);
1479
+ }
927
1480
  const transportType = this.activeTransport ?? "hid";
928
1481
  const waitPromise = this._uiRegistry.wait(
929
1482
  import_hwk_adapter_core2.UI_REQUEST.REQUEST_DEVICE_PERMISSION,
@@ -933,10 +1486,37 @@ var _LedgerAdapter = class _LedgerAdapter {
933
1486
  type: import_hwk_adapter_core2.UI_REQUEST.REQUEST_DEVICE_PERMISSION,
934
1487
  payload: { transportType, connectId, deviceId }
935
1488
  });
936
- const { granted } = await waitPromise;
1489
+ let response;
1490
+ const onAbort = () => {
1491
+ this._uiRegistry.cancel(import_hwk_adapter_core2.UI_REQUEST.REQUEST_DEVICE_PERMISSION);
1492
+ };
1493
+ try {
1494
+ signal?.addEventListener("abort", onAbort, { once: true });
1495
+ response = await waitPromise;
1496
+ } catch (err) {
1497
+ if (signal?.aborted) {
1498
+ _LedgerAdapter._throwIfAborted(signal);
1499
+ }
1500
+ if (err?._tag === import_hwk_adapter_core2.UI_REQUEST_PREEMPTED_TAG) {
1501
+ this.emitter.emit(import_hwk_adapter_core2.UI_REQUEST.CLOSE_UI_WINDOW, {
1502
+ type: import_hwk_adapter_core2.UI_REQUEST.CLOSE_UI_WINDOW,
1503
+ payload: {}
1504
+ });
1505
+ throw Object.assign(new Error("Device permission request superseded"), {
1506
+ _tag: ERROR_TAG.UserAborted,
1507
+ code: import_hwk_adapter_core2.HardwareErrorCode.UserAborted,
1508
+ cause: err
1509
+ });
1510
+ }
1511
+ throw err;
1512
+ } finally {
1513
+ signal?.removeEventListener("abort", onAbort);
1514
+ }
1515
+ const { granted, reason, message } = response;
937
1516
  if (!granted) {
938
- throw Object.assign(new Error("Device permission denied"), {
939
- code: import_hwk_adapter_core2.HardwareErrorCode.DevicePermissionDenied
1517
+ throw Object.assign(new Error(message ?? "Device permission denied"), {
1518
+ code: import_hwk_adapter_core2.HardwareErrorCode.DevicePermissionDenied,
1519
+ reason
940
1520
  });
941
1521
  }
942
1522
  }
@@ -946,12 +1526,14 @@ var _LedgerAdapter = class _LedgerAdapter {
946
1526
  */
947
1527
  errorToFailure(err) {
948
1528
  debugError("[LedgerAdapter] error:", err);
1529
+ const tag = err && typeof err === "object" ? err._tag : void 0;
949
1530
  if (err && typeof err === "object" && "code" in err && typeof err.code === "number") {
950
1531
  const e = err;
951
- return ledgerFailure(e.code, e.message ?? "Unknown error", e.appName);
1532
+ const params = e.code === import_hwk_adapter_core2.HardwareErrorCode.DevicePermissionDenied && e.reason ? { permissionDeniedReason: e.reason } : void 0;
1533
+ return ledgerFailure(e.code, e.message ?? "Unknown error", e.appName, tag, params);
952
1534
  }
953
1535
  const mapped = mapLedgerError(err);
954
- return ledgerFailure(mapped.code, mapped.message, mapped.appName);
1536
+ return ledgerFailure(mapped.code, mapped.message, mapped.appName, tag);
955
1537
  }
956
1538
  registerEventListeners() {
957
1539
  this.connector.on("device-connect", this.deviceConnectHandler);
@@ -967,32 +1549,38 @@ var _LedgerAdapter = class _LedgerAdapter {
967
1549
  // Device info mapping
968
1550
  // ---------------------------------------------------------------------------
969
1551
  connectorDeviceToDeviceInfo(device) {
970
- const isBle = device.connectId && /^[0-9A-Fa-f]{4}$/.test(device.connectId);
971
1552
  return {
972
1553
  vendor: "ledger",
973
1554
  model: device.model ?? "unknown",
1555
+ modelName: device.modelName,
974
1556
  firmwareVersion: "",
975
1557
  deviceId: device.deviceId,
976
1558
  connectId: device.connectId,
977
1559
  label: device.name,
978
- connectionType: isBle ? "ble" : "usb",
1560
+ connectionType: this.connector.connectionType,
1561
+ rssi: device.rssi,
1562
+ isConnectable: device.isConnectable,
1563
+ serialNumber: device.serialNumber,
979
1564
  capabilities: device.capabilities
980
1565
  };
981
1566
  }
982
1567
  };
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;
1568
+ // Layer 2 retry budget after connection-class error. Each round delegates
1569
+ // to Layer 1 (which owns the unlock prompt). Layer 2 never emits UI itself.
1570
+ _LedgerAdapter.MAX_BUSINESS_RETRY_BUDGET = 3;
1571
+ // Stuck-app (APDU 0x6901) retry pause. Ledger Stax often returns 6901 when
1572
+ // OpenAppCommand lands mid-transition right after CloseApp; a short wait
1573
+ // lets the device finish its UI animation before we retry once.
1574
+ _LedgerAdapter.STUCK_APP_RETRY_DELAY_MS = 500;
1575
+ // Layer 1 confirm budget. After N failed Confirm cycles (user keeps clicking
1576
+ // Confirm but device never shows up / never unlocks), throw DeviceNotFound
1577
+ // instead of looping forever.
1578
+ _LedgerAdapter.MAX_DOCONNECT_CONFIRMS = 3;
994
1579
  var LedgerAdapter = _LedgerAdapter;
995
1580
 
1581
+ // src/connector/LedgerConnectorBase.ts
1582
+ var import_hwk_adapter_core12 = require("@onekeyfe/hwk-adapter-core");
1583
+
996
1584
  // src/device/LedgerDeviceManager.ts
997
1585
  var LedgerDeviceManager = class {
998
1586
  constructor(dmk) {
@@ -1029,7 +1617,9 @@ var LedgerDeviceManager = class {
1029
1617
  if (resolved) return;
1030
1618
  resolved = true;
1031
1619
  this._discovered.clear();
1032
- debugLog("[DMK] enumerate raw devices:", devices.length);
1620
+ debugLog(
1621
+ `[DMK] enumerate count=${devices.length} ids=[${devices.map((d) => d.id).join(",")}]`
1622
+ );
1033
1623
  for (const d of devices) {
1034
1624
  this._discovered.set(d.id, d);
1035
1625
  }
@@ -1039,8 +1629,10 @@ var LedgerDeviceManager = class {
1039
1629
  devices.map((d) => ({
1040
1630
  path: d.id,
1041
1631
  type: d.deviceModel.model,
1632
+ modelName: d.deviceModel.name,
1042
1633
  name: d.name,
1043
- transport: d.transport
1634
+ transport: d.transport,
1635
+ rssi: d.rssi
1044
1636
  }))
1045
1637
  );
1046
1638
  } else {
@@ -1061,8 +1653,10 @@ var LedgerDeviceManager = class {
1061
1653
  devices.map((d) => ({
1062
1654
  path: d.id,
1063
1655
  type: d.deviceModel.model,
1656
+ modelName: d.deviceModel.name,
1064
1657
  name: d.name,
1065
- transport: d.transport
1658
+ transport: d.transport,
1659
+ rssi: d.rssi
1066
1660
  }))
1067
1661
  );
1068
1662
  }
@@ -1086,8 +1680,10 @@ var LedgerDeviceManager = class {
1086
1680
  descriptor: {
1087
1681
  path: d.id,
1088
1682
  type: d.deviceModel.model,
1683
+ modelName: d.deviceModel.name,
1089
1684
  name: d.name,
1090
- transport: d.transport
1685
+ transport: d.transport,
1686
+ rssi: d.rssi
1091
1687
  }
1092
1688
  });
1093
1689
  }
@@ -1106,6 +1702,54 @@ var LedgerDeviceManager = class {
1106
1702
  this._listenSub?.unsubscribe();
1107
1703
  this._listenSub = null;
1108
1704
  }
1705
+ /**
1706
+ * Resolve to the latest snapshot of advertising devices observed within
1707
+ * `timeoutMs`. Rewrites `_discovered` so it tracks the live list.
1708
+ *
1709
+ * `listenToAvailableDevices` is a BehaviorSubject — it emits the cached
1710
+ * list on subscribe and *only* re-emits when the list changes. A stable
1711
+ * list of currently-advertising devices therefore produces only the
1712
+ * subscription frame; treating that frame as "fossil to be discarded"
1713
+ * caused devices to flicker in/out of the UI as searches alternated
1714
+ * between "saw a change frame" and "saw only the cached frame".
1715
+ *
1716
+ * The window still gives a chance for the active scan to update the list
1717
+ * (e.g. drop a peripheral that just stopped broadcasting), but if nothing
1718
+ * changes we trust the snapshot — DMK silence on a stable list means
1719
+ * "everything's still there".
1720
+ */
1721
+ getLiveDevices(timeoutMs = 1500) {
1722
+ debugLog(`[DMK] getLiveDevices() called, timeoutMs=${timeoutMs}`);
1723
+ void this.requestDevice();
1724
+ return new Promise((resolve) => {
1725
+ let done = false;
1726
+ let lastSeen = [];
1727
+ let sub = null;
1728
+ const finish = (devices) => {
1729
+ if (done) return;
1730
+ done = true;
1731
+ sub?.unsubscribe();
1732
+ clearTimeout(timer);
1733
+ this._discovered.clear();
1734
+ for (const d of devices) this._discovered.set(d.id, d);
1735
+ debugLog(
1736
+ `[DMK] getLiveDevices() resolved count=${devices.length} ids=[${devices.map((d) => d.id).join(",")}]`
1737
+ );
1738
+ resolve(devices);
1739
+ };
1740
+ const timer = setTimeout(() => finish(lastSeen), timeoutMs);
1741
+ sub = this._dmk.listenToAvailableDevices({}).subscribe({
1742
+ next: (devices) => {
1743
+ lastSeen = devices;
1744
+ },
1745
+ error: (err) => {
1746
+ debugError("[DMK] getLiveDevices() error:", err);
1747
+ finish(lastSeen);
1748
+ },
1749
+ complete: () => finish(lastSeen)
1750
+ });
1751
+ });
1752
+ }
1109
1753
  requestDevice() {
1110
1754
  if (this._discoverySub) {
1111
1755
  return Promise.resolve();
@@ -1123,11 +1767,33 @@ var LedgerDeviceManager = class {
1123
1767
  });
1124
1768
  return Promise.resolve();
1125
1769
  }
1770
+ /** Lookup a previously-discovered device's display name. */
1771
+ getDeviceName(deviceId) {
1772
+ return this._discovered.get(deviceId)?.name;
1773
+ }
1774
+ /** Lookup minimal model/signal info from a previously-discovered device. */
1775
+ getDiscoveredDeviceInfo(deviceId) {
1776
+ const d = this._discovered.get(deviceId);
1777
+ if (!d) return void 0;
1778
+ return {
1779
+ model: d.deviceModel?.model,
1780
+ modelName: d.deviceModel?.name,
1781
+ name: d.name,
1782
+ rssi: d.rssi
1783
+ };
1784
+ }
1785
+ hasDiscoveredDevice(deviceId) {
1786
+ return this._discovered.has(deviceId);
1787
+ }
1126
1788
  /** Connect to a previously discovered device. Returns sessionId. */
1127
1789
  async connect(deviceId) {
1128
1790
  const device = this._discovered.get(deviceId);
1129
1791
  if (!device) {
1130
- throw new Error(`Device "${deviceId}" not found. Call enumerate() or listen() first.`);
1792
+ const err = new Error(
1793
+ `Device "${deviceId}" not found in discovery cache. Call enumerate() or listen() first.`
1794
+ );
1795
+ err._tag = ERROR_TAG.DeviceNotInDiscoveryCache;
1796
+ throw err;
1131
1797
  }
1132
1798
  const sessionId = await this._dmk.connect({ device });
1133
1799
  this._sessions.set(deviceId, sessionId);
@@ -1166,6 +1832,19 @@ var LedgerDeviceManager = class {
1166
1832
  this._sessionToDevice.clear();
1167
1833
  this._dmk.close();
1168
1834
  }
1835
+ /**
1836
+ * Tear down RxJS subscriptions and local state without closing the DMK.
1837
+ * For light-reset paths that want to drop session state but keep the
1838
+ * underlying transport alive for retry. Call this instead of orphaning
1839
+ * the manager — bare null assignment leaks _listenSub / _discoverySub.
1840
+ */
1841
+ disposeKeepingDmk() {
1842
+ this.stopListening();
1843
+ this.stopDiscovery();
1844
+ this._discovered.clear();
1845
+ this._sessions.clear();
1846
+ this._sessionToDevice.clear();
1847
+ }
1169
1848
  };
1170
1849
 
1171
1850
  // src/signer/SignerManager.ts
@@ -1177,11 +1856,12 @@ var import_hwk_adapter_core3 = require("@onekeyfe/hwk-adapter-core");
1177
1856
 
1178
1857
  // src/signer/deviceActionToPromise.ts
1179
1858
  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) {
1859
+ var IDLE_WATCHDOG_MS = 65e3;
1860
+ function deviceActionToPromise(action, onInteraction, timeoutMs = IDLE_WATCHDOG_MS, onRegisterCanceller) {
1182
1861
  return new Promise((resolve, reject) => {
1183
1862
  let settled = false;
1184
1863
  let lastStep;
1864
+ const observedSteps = [];
1185
1865
  let sub;
1186
1866
  let timer = null;
1187
1867
  const cancelAction = () => {
@@ -1194,44 +1874,59 @@ function deviceActionToPromise(action, onInteraction, timeoutMs = DEFAULT_TIMEOU
1194
1874
  } catch {
1195
1875
  }
1196
1876
  };
1197
- const resetTimer = () => {
1877
+ const armIdleWatchdog = () => {
1198
1878
  if (timer) clearTimeout(timer);
1199
1879
  if (timeoutMs > 0) {
1200
1880
  timer = setTimeout(() => {
1201
1881
  if (!settled) {
1202
1882
  settled = true;
1203
1883
  cancelAction();
1204
- reject(new Error("Device action timed out \u2014 device may be locked or disconnected"));
1884
+ reject(
1885
+ new Error(
1886
+ "Ledger transport unresponsive: no DMK state change within the watchdog window. The device may have lost connection."
1887
+ )
1888
+ );
1205
1889
  }
1206
1890
  }, timeoutMs);
1207
1891
  }
1208
1892
  };
1209
- resetTimer();
1893
+ armIdleWatchdog();
1210
1894
  if (onRegisterCanceller) {
1211
- onRegisterCanceller(() => {
1212
- if (settled) return;
1895
+ onRegisterCanceller((reason) => {
1896
+ if (settled) {
1897
+ return;
1898
+ }
1213
1899
  settled = true;
1214
1900
  if (timer) clearTimeout(timer);
1215
1901
  cancelAction();
1216
- reject(new Error("Device action cancelled"));
1902
+ const err = new Error(reason?.message ?? "Device action cancelled");
1903
+ if (reason?.code !== void 0) {
1904
+ err.code = reason.code;
1905
+ }
1906
+ if (reason?.tag) {
1907
+ Object.defineProperty(err, "_tag", {
1908
+ value: reason.tag,
1909
+ configurable: true,
1910
+ enumerable: false,
1911
+ writable: true
1912
+ });
1913
+ }
1914
+ reject(err);
1217
1915
  });
1218
1916
  }
1219
- debugLog("[DMK-Observable] subscribing to action.observable...");
1220
1917
  sub = action.observable.subscribe({
1221
1918
  next: (state) => {
1222
- if (settled) return;
1223
- resetTimer();
1919
+ if (settled) {
1920
+ return;
1921
+ }
1922
+ armIdleWatchdog();
1224
1923
  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
- );
1924
+ if (step) {
1925
+ lastStep = step;
1926
+ if (observedSteps[observedSteps.length - 1] !== step) {
1927
+ observedSteps.push(step);
1928
+ }
1929
+ }
1235
1930
  if (state.status === import_device_management_kit.DeviceActionStatus.Completed) {
1236
1931
  settled = true;
1237
1932
  if (timer) clearTimeout(timer);
@@ -1243,10 +1938,14 @@ function deviceActionToPromise(action, onInteraction, timeoutMs = DEFAULT_TIMEOU
1243
1938
  if (timer) clearTimeout(timer);
1244
1939
  onInteraction?.("interaction-complete");
1245
1940
  sub?.unsubscribe();
1246
- rejectWithLastStep(state.error, lastStep, reject);
1941
+ rejectWithStepContext(state.error, lastStep, observedSteps, reject);
1247
1942
  } else if (state.status === import_device_management_kit.DeviceActionStatus.Pending && onInteraction) {
1248
1943
  const interaction = state.intermediateValue?.requiredUserInteraction;
1249
1944
  if (interaction && interaction !== "none") {
1945
+ if (interaction === "unlock-device" && timer) {
1946
+ clearTimeout(timer);
1947
+ timer = null;
1948
+ }
1250
1949
  onInteraction(String(interaction));
1251
1950
  }
1252
1951
  }
@@ -1256,7 +1955,7 @@ function deviceActionToPromise(action, onInteraction, timeoutMs = DEFAULT_TIMEOU
1256
1955
  settled = true;
1257
1956
  if (timer) clearTimeout(timer);
1258
1957
  sub?.unsubscribe();
1259
- rejectWithLastStep(err, lastStep, reject);
1958
+ rejectWithStepContext(err, lastStep, observedSteps, reject);
1260
1959
  }
1261
1960
  },
1262
1961
  complete: () => {
@@ -1269,15 +1968,25 @@ function deviceActionToPromise(action, onInteraction, timeoutMs = DEFAULT_TIMEOU
1269
1968
  });
1270
1969
  });
1271
1970
  }
1272
- function rejectWithLastStep(err, lastStep, reject) {
1273
- if (err && typeof err === "object" && lastStep) {
1971
+ function rejectWithStepContext(err, lastStep, observedSteps, reject) {
1972
+ if (err && typeof err === "object" && (lastStep || observedSteps.length > 0)) {
1274
1973
  try {
1275
- Object.defineProperty(err, "_lastStep", {
1276
- value: lastStep,
1277
- configurable: true,
1278
- enumerable: false,
1279
- writable: true
1280
- });
1974
+ if (lastStep) {
1975
+ Object.defineProperty(err, "_lastStep", {
1976
+ value: lastStep,
1977
+ configurable: true,
1978
+ enumerable: false,
1979
+ writable: true
1980
+ });
1981
+ }
1982
+ if (observedSteps.length > 0) {
1983
+ Object.defineProperty(err, "_deviceActionSteps", {
1984
+ value: [...observedSteps],
1985
+ configurable: true,
1986
+ enumerable: false,
1987
+ writable: true
1988
+ });
1989
+ }
1281
1990
  } catch {
1282
1991
  }
1283
1992
  }
@@ -1343,7 +2052,8 @@ var SignerManager = class _SignerManager {
1343
2052
  async getOrCreate(sessionId) {
1344
2053
  debugLog("[DMK] SignerManager.getOrCreate:", { sessionId });
1345
2054
  const builder = await this._builderFn({ dmk: this._dmk, sessionId });
1346
- return new SignerEth(builder.build());
2055
+ const builderWithContext = builder.withContextModule ? builder.withContextModule(_SignerManager._createContextModule()) : builder;
2056
+ return new SignerEth(builderWithContext.build());
1347
2057
  }
1348
2058
  // eslint-disable-next-line @typescript-eslint/no-unused-vars, class-methods-use-this
1349
2059
  invalidate(_sessionId) {
@@ -1352,10 +2062,24 @@ var SignerManager = class _SignerManager {
1352
2062
  clearAll() {
1353
2063
  }
1354
2064
  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);
2065
+ return (args) => new import_device_signer_kit_ethereum.SignerEthBuilder(args);
2066
+ }
2067
+ static _createContextModule() {
2068
+ const contextModule = new import_context_module.ContextModuleBuilder({}).removeDefaultLoaders().build();
2069
+ return _SignerManager.wrapBlindSigningReportNonBlocking(contextModule);
2070
+ }
2071
+ static wrapBlindSigningReportNonBlocking(contextModule) {
2072
+ const report = contextModule.report.bind(contextModule);
2073
+ contextModule.report = async (params) => {
2074
+ try {
2075
+ void report(params).catch((err) => {
2076
+ debugLog("[DMK] ContextModule.report failed:", err);
2077
+ });
2078
+ } catch (err) {
2079
+ debugLog("[DMK] ContextModule.report failed:", err);
2080
+ }
1358
2081
  };
2082
+ return contextModule;
1359
2083
  }
1360
2084
  };
1361
2085
 
@@ -1465,6 +2189,7 @@ async function _getEthSigner(ctx, sessionId) {
1465
2189
  const signerManager = await ctx.getSignerManager();
1466
2190
  const signer = await signerManager.getOrCreate(sessionId);
1467
2191
  signer.onInteraction = (interaction) => {
2192
+ debugLog("[LedgerConnector] evm.onInteraction:", interaction);
1468
2193
  ctx.emit("ui-event", {
1469
2194
  type: collapseSignerInteraction(interaction),
1470
2195
  payload: { sessionId }
@@ -1740,6 +2465,7 @@ async function _createBtcSigner(ctx, sessionId) {
1740
2465
  const sdkSigner = new SignerBtcBuilder({ dmk, sessionId }).build();
1741
2466
  const signer = new SignerBtc(sdkSigner);
1742
2467
  signer.onInteraction = (interaction) => {
2468
+ debugLog("[LedgerConnector] btc.onInteraction:", interaction);
1743
2469
  ctx.emit("ui-event", {
1744
2470
  type: collapseSignerInteraction(interaction),
1745
2471
  payload: { sessionId }
@@ -1910,6 +2636,7 @@ async function _createSolSigner(ctx, sessionId) {
1910
2636
  const sdkSigner = new SignerSolanaBuilder({ dmk, sessionId }).withContextModule(contextModule).build();
1911
2637
  const signer = new SignerSol(sdkSigner);
1912
2638
  signer.onInteraction = (interaction) => {
2639
+ debugLog("[LedgerConnector] sol.onInteraction:", interaction);
1913
2640
  ctx.emit("ui-event", {
1914
2641
  type: collapseSignerInteraction(interaction),
1915
2642
  payload: { sessionId }
@@ -1922,14 +2649,15 @@ async function _createSolSigner(ctx, sessionId) {
1922
2649
  }
1923
2650
 
1924
2651
  // src/connector/chains/tron.ts
1925
- var import_hwk_adapter_core10 = require("@onekeyfe/hwk-adapter-core");
2652
+ var import_hwk_adapter_core11 = require("@onekeyfe/hwk-adapter-core");
1926
2653
  var import_hw_app_trx = __toESM(require("@ledgerhq/hw-app-trx"));
1927
2654
 
1928
2655
  // src/connector/chains/legacyChainCall.ts
1929
- var import_hwk_adapter_core9 = require("@onekeyfe/hwk-adapter-core");
2656
+ var import_hwk_adapter_core10 = require("@onekeyfe/hwk-adapter-core");
1930
2657
 
1931
2658
  // src/app/AppManager.ts
1932
2659
  var import_device_management_kit2 = require("@ledgerhq/device-management-kit");
2660
+ var import_hwk_adapter_core9 = require("@onekeyfe/hwk-adapter-core");
1933
2661
  var APP_NAME_MAP = {
1934
2662
  ETH: "Ethereum",
1935
2663
  BTC: "Bitcoin",
@@ -2001,7 +2729,31 @@ var AppManager = class {
2001
2729
  debugLog("[AppManager] currentApp:", result.data.name);
2002
2730
  return result.data.name;
2003
2731
  }
2004
- throw new Error("Failed to get current app from device");
2732
+ const errResult = result;
2733
+ const dmkErr = errResult.error ?? {};
2734
+ const original = dmkErr.originalError;
2735
+ debugLog(
2736
+ "[AppManager] _getCurrentApp failed sessionId=",
2737
+ sessionId,
2738
+ "tag=",
2739
+ dmkErr._tag,
2740
+ "errorCode=",
2741
+ dmkErr.errorCode,
2742
+ "message=",
2743
+ dmkErr.message,
2744
+ "originalErrorMessage=",
2745
+ original?.message ?? String(original ?? "")
2746
+ );
2747
+ throw Object.assign(
2748
+ new Error(
2749
+ dmkErr.message ?? "Failed to get current app from device"
2750
+ ),
2751
+ {
2752
+ _tag: dmkErr._tag,
2753
+ errorCode: dmkErr.errorCode,
2754
+ originalError: original
2755
+ }
2756
+ );
2005
2757
  }
2006
2758
  async _openApp(sessionId, appName) {
2007
2759
  const result = await this._dmk.sendCommand({
@@ -2010,9 +2762,11 @@ var AppManager = class {
2010
2762
  });
2011
2763
  if (!(0, import_device_management_kit2.isSuccessCommandResult)(result)) {
2012
2764
  const { statusCode } = result;
2765
+ const hasStatusCode2 = statusCode != null && statusCode !== "";
2013
2766
  debugLog("[AppManager] openApp failed:", appName, "statusCode:", statusCode);
2014
2767
  throw Object.assign(new Error(`Failed to open "${appName}"`), {
2015
- _tag: "OpenAppCommandError",
2768
+ _tag: ERROR_TAG.OpenAppCommand,
2769
+ code: hasStatusCode2 ? void 0 : import_hwk_adapter_core9.HardwareErrorCode.AppNotInstalled,
2016
2770
  errorCode: String(statusCode ?? ""),
2017
2771
  statusCode,
2018
2772
  appName
@@ -2068,14 +2822,14 @@ async function withLegacyChainCall(ctx, sessionId, options, action) {
2068
2822
  const onAppOpenPrompt = () => {
2069
2823
  openAppPromptShown = true;
2070
2824
  ctx.emit("ui-event", {
2071
- type: import_hwk_adapter_core9.EConnectorInteraction.ConfirmOpenApp,
2825
+ type: import_hwk_adapter_core10.EConnectorInteraction.ConfirmOpenApp,
2072
2826
  payload: { sessionId }
2073
2827
  });
2074
2828
  };
2075
2829
  const closeOpenAppUiIfShown = () => {
2076
2830
  if (openAppPromptShown) {
2077
2831
  ctx.emit("ui-event", {
2078
- type: import_hwk_adapter_core9.EConnectorInteraction.InteractionComplete,
2832
+ type: import_hwk_adapter_core10.EConnectorInteraction.InteractionComplete,
2079
2833
  payload: { sessionId }
2080
2834
  });
2081
2835
  openAppPromptShown = false;
@@ -2096,7 +2850,7 @@ async function withLegacyChainCall(ctx, sessionId, options, action) {
2096
2850
  let confirmEmitted = false;
2097
2851
  if (needsConfirmation) {
2098
2852
  ctx.emit("ui-event", {
2099
- type: import_hwk_adapter_core9.EConnectorInteraction.ConfirmOnDevice,
2853
+ type: import_hwk_adapter_core10.EConnectorInteraction.ConfirmOnDevice,
2100
2854
  payload: { sessionId }
2101
2855
  });
2102
2856
  confirmEmitted = true;
@@ -2106,7 +2860,7 @@ async function withLegacyChainCall(ctx, sessionId, options, action) {
2106
2860
  } finally {
2107
2861
  if (confirmEmitted || openAppPromptShown) {
2108
2862
  ctx.emit("ui-event", {
2109
- type: import_hwk_adapter_core9.EConnectorInteraction.InteractionComplete,
2863
+ type: import_hwk_adapter_core10.EConnectorInteraction.InteractionComplete,
2110
2864
  payload: { sessionId }
2111
2865
  });
2112
2866
  openAppPromptShown = false;
@@ -2194,7 +2948,7 @@ async function tronSignTransaction(ctx, sessionId, params) {
2194
2948
  if (!params.rawTxHex) {
2195
2949
  throw Object.assign(
2196
2950
  new Error("TRON signing requires a protobuf-encoded raw transaction hex (rawTxHex)."),
2197
- { code: import_hwk_adapter_core10.HardwareErrorCode.InvalidParams }
2951
+ { code: import_hwk_adapter_core11.HardwareErrorCode.InvalidParams }
2198
2952
  );
2199
2953
  }
2200
2954
  const path = normalizePath(params.path);
@@ -2238,6 +2992,10 @@ var METHOD_PREFIX_TO_APP_NAME = {
2238
2992
  sol: "Solana",
2239
2993
  tron: "Tron"
2240
2994
  };
2995
+ var HARDWARE_ERROR_CODE_VALUES = new Set(
2996
+ Object.values(import_hwk_adapter_core12.HardwareErrorCode).filter((value) => typeof value === "number")
2997
+ );
2998
+ var BLE_CONNECT_SCAN_TIMEOUT_MS = 1500;
2241
2999
  async function defaultLedgerKitImporter(pkg) {
2242
3000
  switch (pkg) {
2243
3001
  case "@ledgerhq/device-management-kit":
@@ -2261,16 +3019,6 @@ var LedgerConnectorBase = class {
2261
3019
  this._dmk = null;
2262
3020
  this._eventHandlers = /* @__PURE__ */ new Map();
2263
3021
  // ---------------------------------------------------------------------------
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
3022
  // Per-session DeviceAction cancellers
2275
3023
  //
2276
3024
  // Each chain handler registers its active DeviceAction's canceller via
@@ -2279,10 +3027,23 @@ var LedgerConnectorBase = class {
2279
3027
  // unsubscribes the observable and releases DMK's IntentQueue slot.
2280
3028
  // ---------------------------------------------------------------------------
2281
3029
  this._cancellers = /* @__PURE__ */ new Map();
3030
+ // ---------------------------------------------------------------------------
3031
+ // Per-session DMK state subscriptions
3032
+ //
3033
+ // DMK only emits `device-disconnect` on the connector when `disconnect()`
3034
+ // is called explicitly. To pick up autonomous disconnects (USB unplug,
3035
+ // BLE drop, device sleep, DMK transport reset) we subscribe to
3036
+ // `_dmk.getDeviceSessionState({sessionId})` per active session and emit
3037
+ // `device-disconnect` ourselves the moment DMK reports the device went
3038
+ // to NOT_CONNECTED. The subscription is torn down on disconnect /
3039
+ // reset / observable completion to avoid leaks.
3040
+ // ---------------------------------------------------------------------------
3041
+ this._sessionStateSubs = /* @__PURE__ */ new Map();
2282
3042
  this._createTransport = createTransport;
2283
3043
  this.connectionType = options?.connectionType ?? "usb";
2284
3044
  this._providedDmk = options?.dmk;
2285
3045
  this._importLedgerKit = options?.importLedgerKit ?? defaultLedgerKitImporter;
3046
+ this._requirePreFlightScan = options?.requirePreFlightScan ?? false;
2286
3047
  if (this._providedDmk) {
2287
3048
  this._initManagers(this._providedDmk);
2288
3049
  }
@@ -2300,38 +3061,23 @@ var LedgerConnectorBase = class {
2300
3061
  importLedgerKit: this._importLedgerKit
2301
3062
  };
2302
3063
  }
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
3064
  // ---------------------------------------------------------------------------
2320
3065
  // Protected — hooks for subclasses
2321
3066
  // ---------------------------------------------------------------------------
2322
3067
  /**
2323
3068
  * Resolve the connectId for a discovered device descriptor.
2324
3069
  * Default: use the DMK path (ephemeral UUID).
2325
- * Override in subclasses to extract stable identifiers (e.g. BLE hex ID).
3070
+ * Override in subclasses only when the public connectId differs from the transport path.
2326
3071
  */
2327
3072
  _resolveConnectId(descriptor) {
2328
3073
  return descriptor.path;
2329
3074
  }
2330
- // ---------------------------------------------------------------------------
2331
- // IConnector -- Device discovery
2332
- // ---------------------------------------------------------------------------
2333
- async searchDevices() {
2334
- const dm = await this._getDeviceManager();
3075
+ /**
3076
+ * Authoritative discovery for this transport. Default = enumerate snapshot
3077
+ * (USB/HID). BLE overrides to drive from a live scan window since DMK's
3078
+ * enumerate() cache survives peripherals going offline.
3079
+ */
3080
+ async _discoverDescriptors(dm) {
2335
3081
  let descriptors = await dm.enumerate();
2336
3082
  if (descriptors.length === 0) {
2337
3083
  try {
@@ -2340,92 +3086,284 @@ var LedgerConnectorBase = class {
2340
3086
  }
2341
3087
  descriptors = await dm.enumerate();
2342
3088
  }
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
- });
3089
+ return descriptors;
3090
+ }
3091
+ _assertSingleUsbDescriptor(descriptors) {
3092
+ if (isLedgerBleConnectionType(this.connectionType) || descriptors.length <= 1) {
3093
+ return;
3094
+ }
3095
+ debugLog(
3096
+ `[DMK] Multiple Ledger USB devices found (${descriptors.length}); refusing to choose by ephemeral path. paths=[${descriptors.map((d) => d.path).join(",")}]`
3097
+ );
3098
+ throw createMultipleUsbLedgerDevicesError();
3099
+ }
3100
+ // ---------------------------------------------------------------------------
3101
+ // IConnector -- Device discovery
3102
+ // ---------------------------------------------------------------------------
3103
+ async searchDevices() {
3104
+ const dm = await this._getDeviceManager();
3105
+ const descriptors = await this._discoverDescriptors(dm);
3106
+ this._assertSingleUsbDescriptor(descriptors);
3107
+ const resolvedDescriptors = descriptors.map((d) => ({
3108
+ descriptor: d,
3109
+ connectId: this._resolveConnectId(d)
3110
+ }));
3111
+ const result = resolvedDescriptors.map(({ descriptor: d, connectId }) => ({
3112
+ connectId,
3113
+ deviceId: d.path,
3114
+ name: d.name || d.type || "Ledger",
3115
+ model: d.type,
3116
+ modelName: d.modelName,
3117
+ rssi: d.rssi,
3118
+ isConnectable: d.isConnectable,
3119
+ serialNumber: d.serialNumber
3120
+ }));
3121
+ debugLog(
3122
+ `[DMK] connector.searchDevices() return count=${result.length} ids=[${result.map((r) => r.connectId).join(",")}] paths=[${result.map((r) => r.deviceId).join(",")}]`
3123
+ );
2352
3124
  return result;
2353
3125
  }
2354
3126
  // ---------------------------------------------------------------------------
2355
3127
  // IConnector -- Connection
2356
3128
  // ---------------------------------------------------------------------------
2357
3129
  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;
3130
+ const callerSuppliedConnectId = Boolean(deviceId);
3131
+ let targetPath = deviceId;
3132
+ if (callerSuppliedConnectId && !isLedgerBleConnectionType(this.connectionType)) {
3133
+ const dm = await this._getDeviceManager();
3134
+ this._assertSingleUsbDescriptor(await this._discoverDescriptors(dm));
3135
+ }
2362
3136
  if (!targetPath) {
2363
- const descriptors = await dm.enumerate();
2364
- if (descriptors.length === 0) {
3137
+ const discovered = await this.searchDevices();
3138
+ if (discovered.length === 0) {
2365
3139
  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.`
3140
+ `No Ledger device found. Make sure the device is connected${isLedgerBleConnectionType(this.connectionType) ? " nearby with Bluetooth enabled" : " via USB"} and unlocked.`
2367
3141
  );
2368
3142
  }
2369
- targetPath = descriptors[0].path;
2370
- }
2371
- const externalConnectId = this._getConnectIdForPath(targetPath);
3143
+ targetPath = discovered[0].deviceId;
3144
+ }
3145
+ const externalConnectId = targetPath;
3146
+ const HANG_CEILING_MS = 5 * 6e4;
3147
+ const dmConnectWithObserve = async (dm, path) => {
3148
+ if (!isLedgerBleConnectionType(this.connectionType)) return dm.connect(path);
3149
+ let timeoutId;
3150
+ let timedOut = false;
3151
+ const connectPromise = dm.connect(path);
3152
+ const hangPromise = new Promise((_, reject) => {
3153
+ timeoutId = setTimeout(() => {
3154
+ timedOut = true;
3155
+ const err = new Error(
3156
+ `Ledger BLE connect did not return within ${HANG_CEILING_MS / 6e4}min \u2014 DMK hang fallback.`
3157
+ );
3158
+ err._tag = ERROR_TAG.BlePairingTimeout;
3159
+ err.code = import_hwk_adapter_core12.HardwareErrorCode.BlePairingTimeout;
3160
+ reject(err);
3161
+ }, HANG_CEILING_MS);
3162
+ });
3163
+ try {
3164
+ return await Promise.race([connectPromise, hangPromise]);
3165
+ } catch (err) {
3166
+ const e = err;
3167
+ debugLog("[DMK] dm.connect rejected \u2014 observed:", {
3168
+ _tag: e?._tag,
3169
+ message: e?.message,
3170
+ errorCode: e?.errorCode,
3171
+ statusCode: e?.statusCode,
3172
+ originalTag: e?.originalError?._tag,
3173
+ originalMessage: e?.originalError?.message
3174
+ });
3175
+ if (timedOut) {
3176
+ void connectPromise.then(
3177
+ (sessionId) => dm.disconnect(sessionId).catch(() => void 0),
3178
+ () => void 0
3179
+ );
3180
+ }
3181
+ throw err;
3182
+ } finally {
3183
+ if (timeoutId) clearTimeout(timeoutId);
3184
+ }
3185
+ };
3186
+ const isBleDirectConnect = isLedgerBleConnectionType(this.connectionType) && callerSuppliedConnectId;
3187
+ const throwNotAdvertising = () => {
3188
+ const err = new Error(
3189
+ "Ledger device is not currently advertising. Wake up and unlock the device, keep it nearby, then try again."
3190
+ );
3191
+ err._tag = ERROR_TAG.DeviceNotAdvertising;
3192
+ err.code = import_hwk_adapter_core12.HardwareErrorCode.DeviceNotFound;
3193
+ throw err;
3194
+ };
2372
3195
  const doConnect = async (path) => {
2373
- const sessionId = await dm.connect(path);
3196
+ const dm = await this._getDeviceManager();
3197
+ if (isBleDirectConnect && !dm.hasDiscoveredDevice(path)) {
3198
+ const live = await dm.getLiveDevices(BLE_CONNECT_SCAN_TIMEOUT_MS);
3199
+ if (this._requirePreFlightScan && !live.some((d) => d.id === path)) {
3200
+ throwNotAdvertising();
3201
+ }
3202
+ }
3203
+ let sessionId;
3204
+ try {
3205
+ sessionId = await dmConnectWithObserve(dm, path);
3206
+ } catch (err) {
3207
+ if (isBleDirectConnect && err?._tag === ERROR_TAG.DeviceNotInDiscoveryCache) {
3208
+ throwNotAdvertising();
3209
+ }
3210
+ throw err;
3211
+ }
3212
+ try {
3213
+ this._watchSessionState(sessionId, externalConnectId);
3214
+ } catch (subErr) {
3215
+ debugLog("[DMK] state subscription failed during connect; disconnecting session:", subErr);
3216
+ try {
3217
+ await dm.disconnect(sessionId);
3218
+ } catch {
3219
+ }
3220
+ throw subErr;
3221
+ }
3222
+ const info = dm.getDiscoveredDeviceInfo(path);
2374
3223
  const session = {
2375
3224
  sessionId,
2376
3225
  deviceInfo: {
2377
3226
  vendor: "ledger",
2378
- model: "unknown",
3227
+ model: info?.model ?? "unknown",
3228
+ modelName: info?.modelName,
2379
3229
  firmwareVersion: "unknown",
2380
3230
  deviceId: path,
2381
3231
  connectId: externalConnectId,
2382
3232
  connectionType: this.connectionType,
3233
+ rssi: info?.rssi,
2383
3234
  capabilities: { persistentDeviceIdentity: false }
2384
3235
  }
2385
3236
  };
2386
- const realName = discovered.find((d) => d.connectId === externalConnectId)?.name ?? "Ledger";
3237
+ const name = dm.getDeviceName(path) ?? "Ledger";
2387
3238
  this._emit("device-connect", {
2388
- device: {
2389
- connectId: externalConnectId,
2390
- deviceId: path,
2391
- name: realName
2392
- }
3239
+ device: { connectId: externalConnectId, deviceId: path, name }
2393
3240
  });
2394
3241
  return session;
2395
3242
  };
2396
3243
  try {
2397
3244
  return await doConnect(targetPath);
2398
- } catch {
3245
+ } catch (err) {
2399
3246
  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
- );
3247
+ if (isLedgerBleConnectionType(this.connectionType)) {
3248
+ const tag = err?._tag;
3249
+ if (isKnownConnectionTag(tag)) {
3250
+ throw err;
2409
3251
  }
2410
- return doConnect(descriptors[0].path);
3252
+ const wrapped = new Error(
3253
+ "Ledger Bluetooth pairing failed. Make sure the device is unlocked and nearby, then try again."
3254
+ );
3255
+ wrapped._tag = ERROR_TAG.BleGattBondingFailed;
3256
+ wrapped.code = import_hwk_adapter_core12.HardwareErrorCode.BlePairingTimeout;
3257
+ wrapped.originalError = err;
3258
+ throw wrapped;
3259
+ }
3260
+ if (err && typeof err === "object" && err._tag) {
3261
+ const taggedError = err instanceof Error ? err : Object.assign(
3262
+ new Error(err.message ?? "Ledger device error"),
3263
+ err
3264
+ );
3265
+ throw taggedError;
2411
3266
  }
2412
- return doConnect(retryPath);
3267
+ throw new Error("Ledger device appears unplugged. Plug in the device and try again.");
2413
3268
  }
2414
3269
  }
2415
3270
  async disconnect(sessionId) {
2416
3271
  if (!this._deviceManager) return;
2417
3272
  const deviceId = this._deviceManager.getDeviceId(sessionId);
2418
3273
  this._signerManager?.invalidate(sessionId);
3274
+ this._unwatchSessionState(sessionId);
2419
3275
  await this._deviceManager.disconnect(sessionId);
2420
3276
  if (deviceId) {
2421
3277
  this._emit("device-disconnect", { connectId: deviceId });
2422
3278
  }
2423
3279
  }
3280
+ /**
3281
+ * Subscribe to DMK's per-session state observable so that any autonomous
3282
+ * disconnect (USB unplug, BLE drop, device sleep, transport reset) is
3283
+ * surfaced as a `device-disconnect` event — without this, the upstream
3284
+ * `LedgerAdapter._sessions` map would hold a dead session entry until
3285
+ * the next call hit `DeviceSessionNotFound`.
3286
+ *
3287
+ * Subscribe failure is fatal — see the inline note at the subscribe call.
3288
+ */
3289
+ _watchSessionState(sessionId, externalConnectId) {
3290
+ const dmk = this._dmk;
3291
+ if (!dmk) return;
3292
+ const previous = this._sessionStateSubs.get(sessionId);
3293
+ if (previous) {
3294
+ try {
3295
+ previous.unsubscribe();
3296
+ } catch {
3297
+ }
3298
+ }
3299
+ const sub = dmk.getDeviceSessionState({ sessionId }).subscribe({
3300
+ next: (state) => {
3301
+ if (state?.deviceStatus === "NOT CONNECTED") {
3302
+ this._handleAutonomousDisconnect(sessionId, externalConnectId);
3303
+ }
3304
+ },
3305
+ error: () => {
3306
+ this._handleAutonomousDisconnect(sessionId, externalConnectId);
3307
+ },
3308
+ complete: () => {
3309
+ this._handleAutonomousDisconnect(sessionId, externalConnectId);
3310
+ }
3311
+ });
3312
+ this._sessionStateSubs.set(sessionId, sub);
3313
+ }
3314
+ _unwatchSessionState(sessionId) {
3315
+ const sub = this._sessionStateSubs.get(sessionId);
3316
+ if (!sub) return;
3317
+ try {
3318
+ sub.unsubscribe();
3319
+ } catch {
3320
+ }
3321
+ this._sessionStateSubs.delete(sessionId);
3322
+ }
3323
+ _handleAutonomousDisconnect(sessionId, externalConnectId) {
3324
+ if (!this._sessionStateSubs.has(sessionId)) return;
3325
+ debugLog(
3326
+ "[DMK] autonomous disconnect detected \u2014 sessionId:",
3327
+ sessionId,
3328
+ "connectId:",
3329
+ externalConnectId
3330
+ );
3331
+ this._unwatchSessionState(sessionId);
3332
+ this._signerManager?.invalidate(sessionId);
3333
+ this._cancellers.get(sessionId)?.({
3334
+ code: import_hwk_adapter_core12.HardwareErrorCode.DeviceDisconnected,
3335
+ tag: "DeviceDisconnected",
3336
+ message: "Device disconnected"
3337
+ });
3338
+ this._cancellers.delete(sessionId);
3339
+ this._emit("device-disconnect", { connectId: externalConnectId });
3340
+ }
2424
3341
  // ---------------------------------------------------------------------------
2425
3342
  // IConnector -- Method dispatch
2426
3343
  // ---------------------------------------------------------------------------
2427
3344
  async call(sessionId, method, params) {
2428
3345
  debugLog("[DMK] call:", method, JSON.stringify(params));
3346
+ try {
3347
+ return await this._dispatch(sessionId, method, params);
3348
+ } catch (err) {
3349
+ if (isAppStuckByApdu(err)) {
3350
+ throw Object.assign(new Error("Ledger app is unresponsive"), {
3351
+ code: import_hwk_adapter_core12.HardwareErrorCode.DeviceAppStuck,
3352
+ _tag: ERROR_TAG.DeviceAppStuck,
3353
+ originalError: err
3354
+ });
3355
+ }
3356
+ if (isTransportStuck(err)) {
3357
+ throw Object.assign(new Error("Device communication interrupted, please retry"), {
3358
+ code: import_hwk_adapter_core12.HardwareErrorCode.TransportError,
3359
+ _tag: ERROR_TAG.DeviceTransportStuck,
3360
+ originalError: err
3361
+ });
3362
+ }
3363
+ throw err;
3364
+ }
3365
+ }
3366
+ async _dispatch(sessionId, method, params) {
2429
3367
  const ctx = this._ctxForMethod(method);
2430
3368
  switch (method) {
2431
3369
  // EVM
@@ -2565,14 +3503,13 @@ var LedgerConnectorBase = class {
2565
3503
  }
2566
3504
  /**
2567
3505
  * 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.
3506
+ * Emits device-connect so the adapter updates its _sessions Map.
2570
3507
  */
2571
3508
  _replaceSession(oldSessionId, _newSessionId) {
2572
3509
  const dm = this._deviceManager;
2573
3510
  if (!dm) return;
2574
3511
  const oldDeviceId = dm.getDeviceId(oldSessionId);
2575
- const connectId = oldDeviceId ? this._pathToConnectId.get(oldDeviceId) : void 0;
3512
+ const connectId = oldDeviceId;
2576
3513
  this._signerManager?.invalidate(oldSessionId);
2577
3514
  if (connectId) {
2578
3515
  this._emit("device-connect", {
@@ -2585,13 +3522,18 @@ var LedgerConnectorBase = class {
2585
3522
  }
2586
3523
  }
2587
3524
  /**
2588
- * Light reset: clear signer/session state but keep DMK, BLE scan, and ID mapping alive.
3525
+ * Light reset: clear signer/session state but keep DMK alive.
2589
3526
  * Used by connect() retry — we want to re-discover with the same transport.
3527
+ *
3528
+ * Note: drops the device manager but tears down its RxJS subs first via
3529
+ * disposeKeepingDmk(). Bare null-assignment would leak _listenSub /
3530
+ * _discoverySub and double-scan after the next _initManagers().
2590
3531
  */
2591
3532
  _resetSignersAndSessions() {
2592
3533
  debugLog("[DMK] _resetSignersAndSessions called");
2593
3534
  this._signerManager?.clearAll();
2594
3535
  this._signerManager = null;
3536
+ this._deviceManager?.disposeKeepingDmk();
2595
3537
  this._deviceManager = null;
2596
3538
  }
2597
3539
  _resetAll() {
@@ -2603,13 +3545,18 @@ var LedgerConnectorBase = class {
2603
3545
  }
2604
3546
  }
2605
3547
  this._cancellers.clear();
3548
+ for (const sub of this._sessionStateSubs.values()) {
3549
+ try {
3550
+ sub.unsubscribe();
3551
+ } catch {
3552
+ }
3553
+ }
3554
+ this._sessionStateSubs.clear();
2606
3555
  this._signerManager?.clearAll();
2607
3556
  this._deviceManager?.dispose();
2608
3557
  this._deviceManager = null;
2609
3558
  this._signerManager = null;
2610
3559
  this._dmk = null;
2611
- this._connectIdToPath.clear();
2612
- this._pathToConnectId.clear();
2613
3560
  }
2614
3561
  // ---------------------------------------------------------------------------
2615
3562
  // Private -- Events
@@ -2643,15 +3590,21 @@ var LedgerConnectorBase = class {
2643
3590
  };
2644
3591
  }
2645
3592
  _wrapError(err, opts) {
2646
- const mapped = mapLedgerError(err, opts);
2647
- const error = new Error(mapped.message);
2648
3593
  const src = err && typeof err === "object" ? err : {};
3594
+ const hasSerializedCode = typeof src.code === "number" && HARDWARE_ERROR_CODE_VALUES.has(src.code);
3595
+ const mapped = hasSerializedCode ? {
3596
+ code: src.code,
3597
+ message: typeof src.message === "string" ? src.message : "Unknown Ledger error",
3598
+ appName: typeof src.appName === "string" ? src.appName : opts?.defaultAppName
3599
+ } : mapLedgerError(err, opts);
3600
+ const error = new Error(mapped.message);
2649
3601
  Object.assign(error, {
2650
3602
  code: mapped.code,
2651
3603
  appName: mapped.appName,
2652
3604
  _tag: src._tag,
2653
3605
  errorCode: src.errorCode,
2654
- _lastStep: src._lastStep
3606
+ _lastStep: src._lastStep,
3607
+ _deviceActionSteps: src._deviceActionSteps
2655
3608
  });
2656
3609
  Object.defineProperty(error, "originalError", {
2657
3610
  value: err,
@@ -2662,13 +3615,6 @@ var LedgerConnectorBase = class {
2662
3615
  return error;
2663
3616
  }
2664
3617
  };
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
3618
  // Annotate the CommonJS export names for ESM import in node:
2673
3619
  0 && (module.exports = {
2674
3620
  AppManager,
@@ -2680,12 +3626,15 @@ function extractBleHexId(name) {
2680
3626
  SignerEth,
2681
3627
  SignerManager,
2682
3628
  SignerSol,
3629
+ debugLog,
2683
3630
  deviceActionToPromise,
2684
- extractBleHexId,
2685
- isDebugEnabled,
2686
3631
  isDeviceLockedError,
3632
+ isLedgerBleConnectionType,
3633
+ isLedgerBleDescriptor,
3634
+ isLedgerDmkBleTransport,
2687
3635
  ledgerFailure,
2688
3636
  mapLedgerError,
2689
- setDebugEnabled
3637
+ offSdkEvent,
3638
+ onSdkEvent
2690
3639
  });
2691
3640
  //# sourceMappingURL=index.js.map