@onekeyfe/hwk-ledger-adapter 1.1.26-alpha.8 → 1.1.26

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,26 @@ 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
+ function ledgerFailure(code, error, appName, tag, params) {
58
61
  const payload = { error, code };
59
62
  if (appName !== void 0) payload.appName = appName;
63
+ if (tag !== void 0) payload._tag = tag;
64
+ if (params !== void 0) payload.params = params;
60
65
  return { success: false, payload };
61
66
  }
62
67
  var LOCKED_ERROR_CODES = /* @__PURE__ */ new Set(["5515", "21781", "6982", "27010", "5303", "21251"]);
63
68
  var USER_REJECTED_CODES = /* @__PURE__ */ new Set(["6985", "27013"]);
64
69
  var WRONG_APP_CODES = /* @__PURE__ */ new Set(["6e00", "28160", "6d00", "27904", "6a83", "27267"]);
65
70
  var APP_NOT_INSTALLED_CODES = /* @__PURE__ */ new Set(["6807", "26631"]);
66
- var STEP_BLIND_SIGN_FALLBACK = "signer.eth.steps.blindSignTransactionFallback";
71
+ var STEP_BLIND_SIGN_TRANSACTION_FALLBACK = "signer.eth.steps.blindSignTransactionFallback";
67
72
  function getEthAppErrorCode(err) {
68
73
  if (!err || typeof err !== "object") return null;
69
74
  const e = err;
70
- if (e._tag === "EthAppCommandError" && typeof e.errorCode === "string") {
75
+ if (e._tag === ERROR_TAG.EthAppCommand && typeof e.errorCode === "string") {
71
76
  return e.errorCode.toLowerCase();
72
77
  }
73
78
  const orig = e.originalError;
74
- if (orig?._tag === "EthAppCommandError" && typeof orig.errorCode === "string") {
79
+ if (orig?._tag === ERROR_TAG.EthAppCommand && typeof orig.errorCode === "string") {
75
80
  return orig.errorCode.toLowerCase();
76
81
  }
77
82
  return null;
@@ -128,13 +133,8 @@ function mapBtcAppError(hex) {
128
133
  return null;
129
134
  }
130
135
  }
131
- function mapEthAppError(ethCode, lastStep) {
136
+ function mapEthAppErrorCode(ethCode) {
132
137
  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
138
  case "6984":
139
139
  return import_hwk_adapter_core.HardwareErrorCode.EvmClearSignPluginMissing;
140
140
  case "6a84":
@@ -147,16 +147,133 @@ function mapEthAppError(ethCode, lastStep) {
147
147
  return null;
148
148
  }
149
149
  }
150
+ function classifyEthAppError(err) {
151
+ const ethCode = getEthAppErrorCode(err);
152
+ if (!ethCode) return null;
153
+ if (ethCode === "6a80") {
154
+ return hasBlindSignFallbackStep(err) ? import_hwk_adapter_core.HardwareErrorCode.EvmBlindSigningRequired : null;
155
+ }
156
+ return mapEthAppErrorCode(ethCode);
157
+ }
158
+ var ERROR_TAG = {
159
+ // SDK-mint
160
+ DeviceNotAdvertising: "DeviceNotAdvertisingError",
161
+ // BLE scan miss
162
+ DeviceNotInDiscoveryCache: "DeviceNotInDiscoveryCacheError",
163
+ // dm.connect() before enumerate
164
+ BlePairingTimeout: "BlePairingTimeoutError",
165
+ // SMP 30s timeout
166
+ BleGattBondingFailed: "BleGattBondingFailedError",
167
+ // other GATT failure
168
+ UserAborted: "UserAborted",
169
+ DeviceAppStuck: "DeviceAppStuck",
170
+ // chain app wedged (APDU 0x6901)
171
+ DeviceTransportStuck: "DeviceTransportStuck",
172
+ // DMK transport queue wedged
173
+ // DMK-reuse (DMK throws same string; we synthesize too)
174
+ DeviceLocked: "DeviceLockedError",
175
+ DeviceNotRecognized: "DeviceNotRecognizedError",
176
+ DeviceSessionNotFound: "DeviceSessionNotFound",
177
+ OpenAppCommand: "OpenAppCommandError",
178
+ // DMK-only (read only)
179
+ EthAppCommand: "EthAppCommandError",
180
+ UserRefusedOnDevice: "UserRefusedOnDevice",
181
+ WrongAppOpened: "WrongAppOpenedError",
182
+ InvalidStatusWord: "InvalidStatusWordError",
183
+ AlreadySendingApdu: "AlreadySendingApduError",
184
+ UnknownDeviceExchange: "UnknownDeviceExchangeError",
185
+ NoAccessibleDevice: "NoAccessibleDeviceError",
186
+ UnknownDevice: "UnknownDeviceError",
187
+ DeviceSessionRefresher: "DeviceSessionRefresherError",
188
+ DeviceNotInitialized: "DeviceNotInitializedError",
189
+ OpeningConnection: "OpeningConnectionError",
190
+ DeviceDisconnectedBeforeSendingApdu: "DeviceDisconnectedBeforeSendingApdu",
191
+ DeviceDisconnectedWhileSending: "DeviceDisconnectedWhileSendingError",
192
+ Disconnect: "DisconnectError",
193
+ ReconnectionFailed: "ReconnectionFailedError",
194
+ WebHIDDisconnect: "WebHIDDisconnectError",
195
+ // ble-plx surfaces this when GATT notification setup fails — typical
196
+ // outcome when the user doesn't confirm pairing on the device, or the
197
+ // existing bond is invalid. Observed in production after ~30s.
198
+ PairingRefused: "PairingRefusedError"
199
+ };
150
200
  function isDeviceLockedError(err) {
151
201
  if (!err || typeof err !== "object") return false;
152
202
  const e = err;
153
203
  if (e.errorCode != null && LOCKED_ERROR_CODES.has(String(e.errorCode))) return true;
154
204
  if (e.statusCode != null && LOCKED_ERROR_CODES.has(String(e.statusCode))) return true;
155
- if (e._tag === "DeviceLockedError") return true;
205
+ if (e._tag === ERROR_TAG.DeviceLocked) return true;
156
206
  if (e.originalError != null && isDeviceLockedError(e.originalError)) return true;
157
207
  if (e.error != null && e._tag && isDeviceLockedError(e.error)) return true;
158
208
  return false;
159
209
  }
210
+ function isDeviceNotAdvertisingError(err) {
211
+ if (!err || typeof err !== "object") return false;
212
+ return err._tag === ERROR_TAG.DeviceNotAdvertising;
213
+ }
214
+ var PAIRING_FAILURE_TAGS = /* @__PURE__ */ new Set([
215
+ ERROR_TAG.BlePairingTimeout,
216
+ ERROR_TAG.BleGattBondingFailed,
217
+ ERROR_TAG.PairingRefused
218
+ ]);
219
+ function isBlePairingFailureError(err) {
220
+ if (!err || typeof err !== "object") return false;
221
+ const tag = err._tag;
222
+ if (tag && PAIRING_FAILURE_TAGS.has(tag)) return true;
223
+ const orig = err.originalError;
224
+ if (orig != null && isBlePairingFailureError(orig)) return true;
225
+ return false;
226
+ }
227
+ var CONNECTION_LEVEL_TAGS = /* @__PURE__ */ new Set([
228
+ ERROR_TAG.DeviceLocked,
229
+ ERROR_TAG.DeviceNotAdvertising,
230
+ ERROR_TAG.BlePairingTimeout,
231
+ ERROR_TAG.BleGattBondingFailed,
232
+ ERROR_TAG.PairingRefused,
233
+ ERROR_TAG.DeviceNotRecognized,
234
+ ERROR_TAG.NoAccessibleDevice,
235
+ ERROR_TAG.UnknownDevice,
236
+ ERROR_TAG.DeviceSessionNotFound,
237
+ ERROR_TAG.DeviceSessionRefresher,
238
+ ERROR_TAG.DeviceNotInitialized,
239
+ ERROR_TAG.OpeningConnection,
240
+ ERROR_TAG.DeviceDisconnectedBeforeSendingApdu,
241
+ ERROR_TAG.DeviceDisconnectedWhileSending,
242
+ ERROR_TAG.Disconnect,
243
+ ERROR_TAG.ReconnectionFailed,
244
+ ERROR_TAG.WebHIDDisconnect
245
+ ]);
246
+ var DEVICE_NOT_FOUND_TAGS = /* @__PURE__ */ new Set([
247
+ ERROR_TAG.NoAccessibleDevice,
248
+ ERROR_TAG.UnknownDevice,
249
+ ERROR_TAG.DeviceNotInitialized,
250
+ // SDK-internal: dm.connect() called before _discovered was populated.
251
+ // Map to DeviceNotFound so non-BLE-direct paths get a sensible error code.
252
+ ERROR_TAG.DeviceNotInDiscoveryCache
253
+ ]);
254
+ var DEVICE_BUSY_TAGS = /* @__PURE__ */ new Set([ERROR_TAG.OpeningConnection]);
255
+ var DEVICE_DISCONNECTED_TAGS = /* @__PURE__ */ new Set([
256
+ ERROR_TAG.DeviceNotRecognized,
257
+ ERROR_TAG.DeviceSessionNotFound,
258
+ ERROR_TAG.DeviceSessionRefresher,
259
+ ERROR_TAG.DeviceDisconnectedBeforeSendingApdu,
260
+ ERROR_TAG.DeviceDisconnectedWhileSending,
261
+ ERROR_TAG.Disconnect,
262
+ ERROR_TAG.ReconnectionFailed,
263
+ ERROR_TAG.WebHIDDisconnect
264
+ ]);
265
+ function isConnectionLevelError(err) {
266
+ if (!err || typeof err !== "object") return false;
267
+ const tag = err._tag;
268
+ if (tag && CONNECTION_LEVEL_TAGS.has(tag)) return true;
269
+ const e = err;
270
+ if (e.originalError != null && isConnectionLevelError(e.originalError)) return true;
271
+ if (e.error != null && e._tag && isConnectionLevelError(e.error)) return true;
272
+ return false;
273
+ }
274
+ function isKnownConnectionTag(tag) {
275
+ return typeof tag === "string" && CONNECTION_LEVEL_TAGS.has(tag);
276
+ }
160
277
  function hasStatusCode(err, codeSet) {
161
278
  if (!err || typeof err !== "object") return false;
162
279
  const e = err;
@@ -166,10 +283,39 @@ function hasStatusCode(err, codeSet) {
166
283
  if (e.error != null && e._tag && hasStatusCode(e.error, codeSet)) return true;
167
284
  return false;
168
285
  }
286
+ function hasBlindSignFallbackStep(err) {
287
+ if (!err || typeof err !== "object") return false;
288
+ const e = err;
289
+ if (e._lastStep === STEP_BLIND_SIGN_TRANSACTION_FALLBACK) return true;
290
+ if (Array.isArray(e._deviceActionSteps) && e._deviceActionSteps.includes(STEP_BLIND_SIGN_TRANSACTION_FALLBACK)) {
291
+ return true;
292
+ }
293
+ if (e.originalError != null && hasBlindSignFallbackStep(e.originalError)) return true;
294
+ if (e.error != null && e._tag && hasBlindSignFallbackStep(e.error)) return true;
295
+ return false;
296
+ }
297
+ function isDeviceNotFoundError(err) {
298
+ if (!err || typeof err !== "object") return false;
299
+ const tag = err._tag;
300
+ if (tag && DEVICE_NOT_FOUND_TAGS.has(tag)) return true;
301
+ const e = err;
302
+ if (e.originalError != null && isDeviceNotFoundError(e.originalError)) return true;
303
+ if (e.error != null && e._tag && isDeviceNotFoundError(e.error)) return true;
304
+ return false;
305
+ }
306
+ function isDeviceBusyError(err) {
307
+ if (!err || typeof err !== "object") return false;
308
+ const tag = err._tag;
309
+ if (tag && DEVICE_BUSY_TAGS.has(tag)) return true;
310
+ const e = err;
311
+ if (e.originalError != null && isDeviceBusyError(e.originalError)) return true;
312
+ if (e.error != null && e._tag && isDeviceBusyError(e.error)) return true;
313
+ return false;
314
+ }
169
315
  function isUserRejectedError(err) {
170
316
  if (!err || typeof err !== "object") return false;
171
317
  const e = err;
172
- if (e._tag === "UserRefusedOnDevice") return true;
318
+ if (e._tag === ERROR_TAG.UserRefusedOnDevice) return true;
173
319
  if (typeof e.message === "string" && /denied|rejected|refused/i.test(e.message)) return true;
174
320
  if (hasStatusCode(err, USER_REJECTED_CODES)) return true;
175
321
  return false;
@@ -177,7 +323,7 @@ function isUserRejectedError(err) {
177
323
  function isWrongAppError(err) {
178
324
  if (!err || typeof err !== "object") return false;
179
325
  const e = err;
180
- if (e._tag === "WrongAppOpenedError" || e._tag === "InvalidStatusWordError") {
326
+ if (e._tag === ERROR_TAG.WrongAppOpened || e._tag === ERROR_TAG.InvalidStatusWord) {
181
327
  if (hasStatusCode(err, WRONG_APP_CODES)) return true;
182
328
  }
183
329
  if (typeof e.message === "string") {
@@ -191,7 +337,6 @@ function isWrongAppError(err) {
191
337
  function isAppNotInstalledError(err) {
192
338
  if (!err || typeof err !== "object") return false;
193
339
  const e = err;
194
- if (e._tag === "OpenAppCommandError") return true;
195
340
  if (typeof e.message === "string" && /unknown application/i.test(e.message)) return true;
196
341
  if (hasStatusCode(err, APP_NOT_INSTALLED_CODES)) return true;
197
342
  return false;
@@ -199,7 +344,8 @@ function isAppNotInstalledError(err) {
199
344
  function isDeviceDisconnectedError(err) {
200
345
  if (!err || typeof err !== "object") return false;
201
346
  const e = err;
202
- if (e._tag === "DeviceNotRecognizedError" || e._tag === "DeviceSessionNotFound") return true;
347
+ const tag = e._tag;
348
+ if (typeof tag === "string" && DEVICE_DISCONNECTED_TAGS.has(tag)) return true;
203
349
  if (typeof e.message === "string") {
204
350
  const msg = e.message.toLowerCase();
205
351
  if (msg.includes("disconnected") || msg.includes("not found") || msg.includes("no device") || msg.includes("unplugged"))
@@ -219,7 +365,22 @@ function isTimeoutError(err) {
219
365
  if (e.code === import_hwk_adapter_core.HardwareErrorCode.OperationTimeout) return true;
220
366
  return false;
221
367
  }
222
- function mapLedgerError(err) {
368
+ function isAppStuckByApdu(err) {
369
+ if (!err || typeof err !== "object") return false;
370
+ const tag = err._tag;
371
+ if (tag === ERROR_TAG.DeviceAppStuck) return true;
372
+ return extractApduHex(err) === "6901";
373
+ }
374
+ function isTransportStuck(err) {
375
+ if (!err || typeof err !== "object") return false;
376
+ const tag = err._tag;
377
+ return tag === ERROR_TAG.DeviceTransportStuck || // already wrapped form
378
+ tag === ERROR_TAG.UnknownDeviceExchange || tag === ERROR_TAG.AlreadySendingApdu;
379
+ }
380
+ function isStuckAppStateError(err) {
381
+ return isAppStuckByApdu(err) || isTransportStuck(err);
382
+ }
383
+ function mapLedgerError(err, opts) {
223
384
  let originalMessage = "Unknown Ledger error";
224
385
  if (err instanceof Error) {
225
386
  originalMessage = err.message;
@@ -230,60 +391,127 @@ function mapLedgerError(err) {
230
391
  let code;
231
392
  if (isDeviceLockedError(err)) {
232
393
  code = import_hwk_adapter_core.HardwareErrorCode.DeviceLocked;
394
+ } else if (isDeviceNotAdvertisingError(err) || isDeviceNotFoundError(err)) {
395
+ code = import_hwk_adapter_core.HardwareErrorCode.DeviceNotFound;
396
+ } else if (isDeviceBusyError(err)) {
397
+ code = import_hwk_adapter_core.HardwareErrorCode.DeviceBusy;
398
+ } else if (isBlePairingFailureError(err)) {
399
+ code = import_hwk_adapter_core.HardwareErrorCode.BlePairingTimeout;
233
400
  } else if (isUserRejectedError(err)) {
234
401
  code = import_hwk_adapter_core.HardwareErrorCode.UserRejected;
235
402
  } else if (isWrongAppError(err)) {
236
403
  code = import_hwk_adapter_core.HardwareErrorCode.WrongApp;
237
404
  } else if (isAppNotInstalledError(err)) {
238
- code = import_hwk_adapter_core.HardwareErrorCode.AppNotOpen;
405
+ code = import_hwk_adapter_core.HardwareErrorCode.AppNotInstalled;
239
406
  } else if (isDeviceDisconnectedError(err)) {
240
407
  code = import_hwk_adapter_core.HardwareErrorCode.DeviceDisconnected;
241
408
  } else if (isTimeoutError(err)) {
242
409
  code = import_hwk_adapter_core.HardwareErrorCode.OperationTimeout;
243
410
  } 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;
411
+ const ethMapped = classifyEthAppError(err);
247
412
  const apduHex = ethMapped ? null : extractApduHex(err);
248
413
  const chainMapped = apduHex ? mapSolanaAppError(apduHex) ?? mapTronAppError(apduHex) ?? mapBtcAppError(apduHex) : null;
249
414
  code = ethMapped ?? chainMapped ?? import_hwk_adapter_core.HardwareErrorCode.UnknownError;
250
415
  }
251
- const appName = err && typeof err === "object" ? err.appName : void 0;
416
+ const errAppName = err && typeof err === "object" ? err.appName : void 0;
417
+ const appName = errAppName ?? opts?.defaultAppName;
252
418
  return { code, message: (0, import_hwk_adapter_core.enrichErrorMessage)(code, originalMessage), appName };
253
419
  }
254
420
 
255
- // src/utils/debugLog.ts
256
- var enabled = false;
257
- function setDebugEnabled(value) {
258
- enabled = value;
421
+ // src/utils/ledgerDmkTransport.ts
422
+ function isLedgerDmkBleTransport(transport) {
423
+ const normalized = transport?.toUpperCase();
424
+ return normalized === "BLE" || normalized?.endsWith("_BLE") === true;
259
425
  }
260
- function isDebugEnabled() {
261
- return enabled;
426
+ function isLedgerBleConnectionType(connectionType) {
427
+ return connectionType === "ble";
262
428
  }
263
- function debugLog(...args) {
264
- if (enabled) {
265
- console.debug(...args);
429
+ function isLedgerBleDescriptor(connectionType, descriptor) {
430
+ return isLedgerBleConnectionType(connectionType) || isLedgerDmkBleTransport(descriptor.transport);
431
+ }
432
+
433
+ // src/utils/sdkEventBus.ts
434
+ var listeners = /* @__PURE__ */ new Set();
435
+ function onSdkEvent(listener) {
436
+ listeners.add(listener);
437
+ return () => {
438
+ listeners.delete(listener);
439
+ };
440
+ }
441
+ function offSdkEvent(listener) {
442
+ listeners.delete(listener);
443
+ }
444
+ function emitSdkEvent(event) {
445
+ if (listeners.size === 0) return;
446
+ for (const listener of listeners) {
447
+ try {
448
+ listener(event);
449
+ } catch {
450
+ }
266
451
  }
267
452
  }
268
- function debugError(...args) {
269
- if (enabled) {
270
- console.error(...args);
453
+ function emitLog(level, ...args) {
454
+ if (listeners.size === 0) return;
455
+ const message = args.map((a) => typeof a === "string" ? a : safeStringify(a)).join(" ");
456
+ emitSdkEvent({ type: "log", level, message });
457
+ }
458
+ function safeStringify(value) {
459
+ if (value instanceof Error) {
460
+ return value.stack ? `${value.message}
461
+ ${value.stack}` : value.message;
462
+ }
463
+ try {
464
+ return JSON.stringify(value);
465
+ } catch {
466
+ return String(value);
271
467
  }
272
468
  }
273
469
 
470
+ // src/utils/debugLog.ts
471
+ function debugLog(...args) {
472
+ emitLog("debug", ...args);
473
+ }
474
+ function debugError(...args) {
475
+ emitLog("error", ...args);
476
+ }
477
+
274
478
  // src/adapter/LedgerAdapter.ts
275
479
  function formatDeviceMismatchError(expected, actual) {
276
480
  return `Wrong device: expected ${expected}, got ${actual}`;
277
481
  }
482
+ var BTC_HIGH_INDEX_THRESHOLD = 100;
483
+ function btcAccountIndexFromPath(path) {
484
+ const segments = path.replace(/^m\//, "").split("/");
485
+ if (segments.length < 3) return null;
486
+ const accountSeg = segments[2].replace(/['h]$/i, "");
487
+ const accountIndex = parseInt(accountSeg, 10);
488
+ return Number.isFinite(accountIndex) ? accountIndex : null;
489
+ }
278
490
  var _LedgerAdapter = class _LedgerAdapter {
279
491
  constructor(connector, options) {
280
492
  this.vendor = "ledger";
281
493
  this.emitter = new import_hwk_adapter_core2.TypedEventEmitter();
282
- // Device cache: tracks discovered devices from connector events
283
494
  this._discoveredDevices = /* @__PURE__ */ new Map();
284
- // Session tracking: maps connectId -> sessionId
285
495
  this._sessions = /* @__PURE__ */ new Map();
286
496
  this._uiRegistry = new import_hwk_adapter_core2.UiRequestRegistry();
497
+ // BTC App rejects account index >= 100 unless display=true. Cached per
498
+ // adapter instance: first 100+ path asks the user once via UI request,
499
+ // subsequent 100+ paths in the same session auto-promote silently.
500
+ // The Ledger device itself still requires a per-call confirmation — that's
501
+ // the Ledger app's safety boundary, not ours to bypass.
502
+ this._btcHighIndexConfirmedThisSession = false;
503
+ // Shared across concurrent callers — only `cancel()` aborts.
504
+ this._doConnectAbortController = null;
505
+ // ---------------------------------------------------------------------------
506
+ // Private helpers
507
+ // ---------------------------------------------------------------------------
508
+ /**
509
+ * Ensure at least one device is connected and return a valid connectId.
510
+ *
511
+ * - If a session already exists for the given connectId, reuse it.
512
+ * - If ANY session exists (Ledger IDs are ephemeral), reuse it.
513
+ * - Otherwise: search → 1 device: auto-connect, multiple: ask user, 0: throw.
514
+ */
287
515
  // Mutex for ensureConnected — prevents concurrent calls from establishing duplicate connections
288
516
  this._connectingPromise = null;
289
517
  // ---------------------------------------------------------------------------
@@ -306,47 +534,31 @@ var _LedgerAdapter = class _LedgerAdapter {
306
534
  payload: { connectId: data.connectId }
307
535
  });
308
536
  };
309
- this.uiRequestHandler = (data) => {
310
- this.handleUiEvent(data);
311
- };
312
- this.uiEventHandler = (data) => {
313
- this.handleUiEvent(data);
537
+ // Forward low-level connector 'ui-event' (the four EConnectorInteraction values)
538
+ // to the public hw.emitter so consumers only need to subscribe in one place
539
+ // (hw.on instead of also reaching into connector.on).
540
+ this.uiEventForwarder = (event) => {
541
+ this.emitter.emit("ui-event", event);
314
542
  };
315
543
  this.connector = connector;
316
544
  this._handleSelectDevice = options?.handleSelectDevice ?? false;
317
- this._jobQueue = new import_hwk_adapter_core2.DeviceJobQueue({
318
- emit: (event, data) => this.emitter.emit(event, data),
319
- uiRegistry: this._uiRegistry
320
- });
545
+ this._jobQueue = new import_hwk_adapter_core2.DeviceJobQueue();
321
546
  this.registerEventListeners();
322
547
  }
323
- /**
324
- * Classify a method's interruptibility.
325
- * - Signing / typed data / transaction → 'confirm' (user may decide via preemption UI)
326
- * - Read-only queries (getAddress / getPublicKey / getMasterFingerprint) → 'safe'
327
- * (auto-cancels any pending read for the same device)
328
- */
329
- static _getInterruptibility(method) {
330
- if (method.toLowerCase().includes("sign")) return "confirm";
331
- return "safe";
332
- }
333
- // ---------------------------------------------------------------------------
334
548
  // Transport
335
- // ---------------------------------------------------------------------------
336
- // Transport is decided at connector creation time. These methods
337
- // satisfy the IHardwareWallet interface with sensible defaults.
338
549
  get activeTransport() {
339
- return this.connector.connectionType === "ble" ? "ble" : "hid";
550
+ return isLedgerBleConnectionType(this.connector.connectionType) ? "ble" : "hid";
340
551
  }
341
552
  getAvailableTransports() {
342
553
  return this.activeTransport ? [this.activeTransport] : [];
343
554
  }
344
- async switchTransport(_type) {
555
+ // Connector is bound at construction; switching requires a new adapter.
556
+ switchTransport(_type) {
557
+ return Promise.resolve();
345
558
  }
346
- // ---------------------------------------------------------------------------
347
559
  // Lifecycle
348
- // ---------------------------------------------------------------------------
349
- async init(_config) {
560
+ init(_config) {
561
+ return Promise.resolve();
350
562
  }
351
563
  /**
352
564
  * Clear cached device/session state without tearing down the adapter.
@@ -359,6 +571,7 @@ var _LedgerAdapter = class _LedgerAdapter {
359
571
  this._connectingPromise = null;
360
572
  this._uiRegistry.reset();
361
573
  this._jobQueue.clear();
574
+ this._btcHighIndexConfirmedThisSession = false;
362
575
  }
363
576
  async dispose() {
364
577
  this._uiRegistry.reset();
@@ -367,6 +580,7 @@ var _LedgerAdapter = class _LedgerAdapter {
367
580
  this.connector.reset();
368
581
  this._discoveredDevices.clear();
369
582
  this._sessions.clear();
583
+ this._btcHighIndexConfirmedThisSession = false;
370
584
  this.emitter.removeAllListeners();
371
585
  }
372
586
  uiResponse(response) {
@@ -375,10 +589,17 @@ var _LedgerAdapter = class _LedgerAdapter {
375
589
  // ---------------------------------------------------------------------------
376
590
  // Device management
377
591
  // ---------------------------------------------------------------------------
378
- async searchDevices() {
592
+ async searchDevices(options) {
593
+ if (options?.resetSession) {
594
+ this._doConnectAbortController?.abort();
595
+ this._sessions.clear();
596
+ this._connectingPromise = null;
597
+ this._doConnectAbortController = null;
598
+ this._btcHighIndexConfirmedThisSession = false;
599
+ }
379
600
  await this._ensureDevicePermission();
380
601
  const devices = await this.connector.searchDevices();
381
- debugLog("[DMK] adapter.searchDevices raw:", JSON.stringify(devices));
602
+ this._discoveredDevices.clear();
382
603
  for (const d of devices) {
383
604
  if (d.connectId) {
384
605
  this._discoveredDevices.set(d.connectId, this.connectorDeviceToDeviceInfo(d));
@@ -387,11 +608,26 @@ var _LedgerAdapter = class _LedgerAdapter {
387
608
  if (this._discoveredDevices.size === 0) {
388
609
  await this._ensureDevicePermission();
389
610
  }
611
+ debugLog(
612
+ `[LedgerAdapter] searchDevices() return count=${this._discoveredDevices.size} ids=[${[
613
+ ...this._discoveredDevices.keys()
614
+ ].join(",")}]`
615
+ );
390
616
  return Array.from(this._discoveredDevices.values());
391
617
  }
618
+ static _createDeviceBusyError(method) {
619
+ return Object.assign(new Error(`Ledger device is busy while calling ${method}`), {
620
+ code: import_hwk_adapter_core2.HardwareErrorCode.DeviceBusy
621
+ });
622
+ }
392
623
  async connectDevice(connectId) {
393
- await this._ensureDevicePermission(connectId);
394
624
  try {
625
+ if (isLedgerBleConnectionType(this.connector.connectionType) && !connectId) {
626
+ throw Object.assign(new Error("Ledger BLE connectId is required."), {
627
+ code: import_hwk_adapter_core2.HardwareErrorCode.DeviceNotFound
628
+ });
629
+ }
630
+ await this._ensureDevicePermission(connectId);
395
631
  const session = await this.connector.connect(connectId);
396
632
  this._sessions.set(connectId, session.sessionId);
397
633
  if (session.deviceInfo) {
@@ -427,7 +663,6 @@ var _LedgerAdapter = class _LedgerAdapter {
427
663
  // Chain call helper
428
664
  // ---------------------------------------------------------------------------
429
665
  async callChain(connectId, deviceId, chain, method, params, skipFingerprint = false) {
430
- await this._ensureDevicePermission(connectId, deviceId);
431
666
  try {
432
667
  const result = await this.connectorCall(connectId, method, params, {
433
668
  chain,
@@ -439,45 +674,12 @@ var _LedgerAdapter = class _LedgerAdapter {
439
674
  return this.errorToFailure(err);
440
675
  }
441
676
  }
442
- /**
443
- * Batch version of callChain — checks permission once,
444
- * fingerprint is verified on the first call inside connectorCall.
445
- */
446
- async callChainBatch(connectId, deviceId, chain, method, params, onProgress, skipFingerprint = false) {
447
- await this._ensureDevicePermission(connectId, deviceId);
448
- const results = [];
449
- for (let i = 0; i < params.length; i++) {
450
- try {
451
- const result = await this.connectorCall(connectId, method, params[i], {
452
- chain,
453
- deviceId,
454
- // Only verify fingerprint on the first call in the batch
455
- skipFingerprint: skipFingerprint || i > 0
456
- });
457
- results.push(result);
458
- onProgress?.({ index: i, total: params.length });
459
- } catch (err) {
460
- return this.errorToFailure(err);
461
- }
462
- }
463
- return (0, import_hwk_adapter_core2.success)(results);
464
- }
465
677
  // ---------------------------------------------------------------------------
466
678
  // EVM chain methods
467
679
  // ---------------------------------------------------------------------------
468
680
  evmGetAddress(connectId, deviceId, params) {
469
681
  return this.callChain(connectId, deviceId, "evm", "evmGetAddress", params);
470
682
  }
471
- evmGetAddresses(connectId, deviceId, params, onProgress) {
472
- return this.callChainBatch(
473
- connectId,
474
- deviceId,
475
- "evm",
476
- "evmGetAddress",
477
- params,
478
- onProgress
479
- );
480
- }
481
683
  evmSignTransaction(connectId, deviceId, params) {
482
684
  return this.callChain(connectId, deviceId, "evm", "evmSignTransaction", params);
483
685
  }
@@ -493,16 +695,6 @@ var _LedgerAdapter = class _LedgerAdapter {
493
695
  btcGetAddress(connectId, deviceId, params) {
494
696
  return this.callChain(connectId, deviceId, "btc", "btcGetAddress", params);
495
697
  }
496
- btcGetAddresses(connectId, deviceId, params, onProgress) {
497
- return this.callChainBatch(
498
- connectId,
499
- deviceId,
500
- "btc",
501
- "btcGetAddress",
502
- params,
503
- onProgress
504
- );
505
- }
506
698
  btcGetPublicKey(connectId, deviceId, params) {
507
699
  return this.callChain(connectId, deviceId, "btc", "btcGetPublicKey", params);
508
700
  }
@@ -530,16 +722,6 @@ var _LedgerAdapter = class _LedgerAdapter {
530
722
  solGetAddress(connectId, deviceId, params) {
531
723
  return this.callChain(connectId, deviceId, "sol", "solGetAddress", params);
532
724
  }
533
- solGetAddresses(connectId, deviceId, params, onProgress) {
534
- return this.callChainBatch(
535
- connectId,
536
- deviceId,
537
- "sol",
538
- "solGetAddress",
539
- params,
540
- onProgress
541
- );
542
- }
543
725
  solSignTransaction(connectId, deviceId, params) {
544
726
  return this.callChain(connectId, deviceId, "sol", "solSignTransaction", params);
545
727
  }
@@ -550,38 +732,13 @@ var _LedgerAdapter = class _LedgerAdapter {
550
732
  // TRON chain methods
551
733
  // ---------------------------------------------------------------------------
552
734
  tronGetAddress(connectId, deviceId, params) {
553
- return this.callChain(connectId, deviceId, "tron", "tronGetAddress", params, true);
554
- }
555
- tronGetAddresses(connectId, deviceId, params, onProgress) {
556
- return this.callChainBatch(
557
- connectId,
558
- deviceId,
559
- "tron",
560
- "tronGetAddress",
561
- params,
562
- onProgress,
563
- true
564
- );
735
+ return this.callChain(connectId, deviceId, "tron", "tronGetAddress", params);
565
736
  }
566
737
  tronSignTransaction(connectId, deviceId, params) {
567
- return this.callChain(
568
- connectId,
569
- deviceId,
570
- "tron",
571
- "tronSignTransaction",
572
- params,
573
- true
574
- );
738
+ return this.callChain(connectId, deviceId, "tron", "tronSignTransaction", params);
575
739
  }
576
740
  tronSignMessage(connectId, deviceId, params) {
577
- return this.callChain(
578
- connectId,
579
- deviceId,
580
- "tron",
581
- "tronSignMessage",
582
- params,
583
- true
584
- );
741
+ return this.callChain(connectId, deviceId, "tron", "tronSignMessage", params);
585
742
  }
586
743
  on(event, listener) {
587
744
  this.emitter.on(event, listener);
@@ -590,30 +747,41 @@ var _LedgerAdapter = class _LedgerAdapter {
590
747
  this.emitter.off(event, listener);
591
748
  }
592
749
  cancel(connectId) {
593
- const sessionId = this._sessions.get(connectId) ?? connectId;
594
- this._jobQueue.forceCancelActive(connectId || "__ledger_default__");
595
- void this.connector.cancel(sessionId);
750
+ const userAbortReason = Object.assign(new Error("User aborted operation"), {
751
+ code: import_hwk_adapter_core2.HardwareErrorCode.UserAborted,
752
+ _tag: ERROR_TAG.UserAborted
753
+ });
754
+ this._lastCancelReason = userAbortReason;
755
+ setTimeout(() => {
756
+ if (this._lastCancelReason === userAbortReason) {
757
+ this._lastCancelReason = void 0;
758
+ }
759
+ }, 2e3);
760
+ this._uiRegistry.cancel();
761
+ if (connectId) {
762
+ this._jobQueue.cancelActiveAndPending(connectId, userAbortReason);
763
+ } else {
764
+ this._jobQueue.cancelActiveAndPending(void 0, userAbortReason);
765
+ }
766
+ if (connectId) {
767
+ const sessionId = this._sessions.get(connectId) ?? connectId;
768
+ void this.connector.cancel(sessionId);
769
+ } else {
770
+ for (const sid of this._sessions.values()) void this.connector.cancel(sid);
771
+ }
772
+ if (this._connectingPromise) {
773
+ this._doConnectAbortController?.abort(userAbortReason);
774
+ }
596
775
  }
597
776
  // ---------------------------------------------------------------------------
598
777
  // Chain fingerprint
599
778
  // ---------------------------------------------------------------------------
600
779
  async getChainFingerprint(connectId, deviceId, chain) {
601
- debugLog(
602
- "[LedgerAdapter] getChainFingerprint called, chain:",
603
- chain,
604
- "connectId:",
605
- connectId || "(empty)",
606
- "sessions:",
607
- this._sessions.size
608
- );
609
- await this._ensureDevicePermission(connectId, deviceId);
610
- debugLog("[LedgerAdapter] getChainFingerprint permission ok, computing fingerprint");
611
780
  try {
612
781
  const fingerprint = await this._computeChainFingerprint(
613
782
  chain,
614
- (method, params) => this.connectorCall(connectId, method, params)
783
+ (method, params) => this.connectorCall(connectId, method, params, void 0, deviceId)
615
784
  );
616
- debugLog("[LedgerAdapter] getChainFingerprint result:", fingerprint?.substring(0, 20));
617
785
  return (0, import_hwk_adapter_core2.success)(fingerprint);
618
786
  } catch (err) {
619
787
  debugError("[LedgerAdapter] getChainFingerprint error:", chain, err);
@@ -684,77 +852,226 @@ var _LedgerAdapter = class _LedgerAdapter {
684
852
  if (signal?.aborted) {
685
853
  _LedgerAdapter._throwIfAborted(signal);
686
854
  }
855
+ const waitPromise = this._uiRegistry.wait(
856
+ import_hwk_adapter_core2.UI_REQUEST.REQUEST_DEVICE_CONNECT
857
+ );
687
858
  this.emitter.emit(import_hwk_adapter_core2.UI_REQUEST.REQUEST_DEVICE_CONNECT, {
688
859
  type: import_hwk_adapter_core2.UI_REQUEST.REQUEST_DEVICE_CONNECT,
689
860
  payload: {
861
+ vendor: "ledger",
862
+ reason: "device-not-found",
690
863
  message: "Please connect and unlock your Ledger device"
691
864
  }
692
865
  });
693
- const waitPromise = this._uiRegistry.wait(
694
- import_hwk_adapter_core2.UI_REQUEST.REQUEST_DEVICE_CONNECT
695
- );
696
866
  let payload;
697
- if (signal) {
698
- const onAbort = () => {
699
- this._uiRegistry.cancel(import_hwk_adapter_core2.UI_REQUEST.REQUEST_DEVICE_CONNECT);
700
- };
701
- signal.addEventListener("abort", onAbort, { once: true });
702
- try {
867
+ try {
868
+ if (signal) {
869
+ const onAbort = () => {
870
+ this._uiRegistry.cancel(import_hwk_adapter_core2.UI_REQUEST.REQUEST_DEVICE_CONNECT);
871
+ };
872
+ signal.addEventListener("abort", onAbort, { once: true });
873
+ try {
874
+ payload = await waitPromise;
875
+ } finally {
876
+ signal.removeEventListener("abort", onAbort);
877
+ }
878
+ } else {
703
879
  payload = await waitPromise;
704
- } finally {
705
- signal.removeEventListener("abort", onAbort);
706
880
  }
707
- } else {
708
- payload = await waitPromise;
881
+ } catch (err) {
882
+ this.emitter.emit(import_hwk_adapter_core2.UI_REQUEST.CLOSE_UI_WINDOW, {
883
+ type: import_hwk_adapter_core2.UI_REQUEST.CLOSE_UI_WINDOW,
884
+ payload: {}
885
+ });
886
+ throw Object.assign(new Error("User cancelled Ledger connection"), {
887
+ _tag: ERROR_TAG.UserAborted,
888
+ code: import_hwk_adapter_core2.HardwareErrorCode.UserAborted,
889
+ cause: err
890
+ });
709
891
  }
710
892
  if (!payload?.confirmed) {
711
893
  throw Object.assign(new Error("User cancelled Ledger connection"), {
712
- _tag: "DeviceNotRecognizedError"
894
+ _tag: ERROR_TAG.UserAborted,
895
+ code: import_hwk_adapter_core2.HardwareErrorCode.UserAborted
713
896
  });
714
897
  }
898
+ await new Promise((resolve) => {
899
+ setTimeout(resolve, 800);
900
+ });
715
901
  }
716
- async ensureConnected(connectId) {
717
- if (connectId && this._sessions.has(connectId)) {
718
- return connectId;
719
- }
720
- if (this._sessions.size > 0) {
721
- const firstKey = this._sessions.keys().next().value;
722
- return firstKey;
902
+ /**
903
+ * Decide whether a BTC pubkey call needs `showOnDevice=true` because of
904
+ * the BTC App's account-index policy, asking the user once per session.
905
+ *
906
+ * Returns the params to pass through (with `showOnDevice` possibly
907
+ * promoted), or `null` if the user declined.
908
+ */
909
+ async _gateBtcHighIndex(params) {
910
+ const accountIndex = btcAccountIndexFromPath(params.path);
911
+ if (params.showOnDevice) return params;
912
+ if (accountIndex === null || accountIndex < BTC_HIGH_INDEX_THRESHOLD) {
913
+ return params;
723
914
  }
724
- if (this._connectingPromise) {
725
- return this._connectingPromise;
915
+ if (this._btcHighIndexConfirmedThisSession) {
916
+ return { ...params, showOnDevice: true };
726
917
  }
727
- this._connectingPromise = this._doConnect();
918
+ const confirmed = await this._waitForBtcHighIndexConfirm(params.path, accountIndex);
919
+ if (!confirmed) return null;
920
+ this._btcHighIndexConfirmedThisSession = true;
921
+ return { ...params, showOnDevice: true };
922
+ }
923
+ async _waitForBtcHighIndexConfirm(path, accountIndex) {
924
+ const waitPromise = this._uiRegistry.wait(
925
+ import_hwk_adapter_core2.UI_REQUEST.REQUEST_BTC_HIGH_INDEX_CONFIRM
926
+ );
927
+ this.emitter.emit(import_hwk_adapter_core2.UI_REQUEST.REQUEST_BTC_HIGH_INDEX_CONFIRM, {
928
+ type: import_hwk_adapter_core2.UI_REQUEST.REQUEST_BTC_HIGH_INDEX_CONFIRM,
929
+ payload: {
930
+ vendor: "ledger",
931
+ path,
932
+ accountIndex
933
+ }
934
+ });
728
935
  try {
729
- return await this._connectingPromise;
730
- } finally {
731
- this._connectingPromise = null;
936
+ const payload = await waitPromise;
937
+ return !!payload?.confirmed;
938
+ } catch (err) {
939
+ this.emitter.emit(import_hwk_adapter_core2.UI_REQUEST.CLOSE_UI_WINDOW, {
940
+ type: import_hwk_adapter_core2.UI_REQUEST.CLOSE_UI_WINDOW,
941
+ payload: {}
942
+ });
943
+ return false;
732
944
  }
733
945
  }
734
- async _doConnect() {
735
- for (let attempt = 0; attempt < _LedgerAdapter.MAX_DEVICE_RETRY; attempt++) {
736
- const devices = await this.searchDevices();
946
+ // Layer 1 entry. Caller signal only races the outer awaiter; the shared
947
+ // `_doConnect` runs under its own internal controller so caller A's cancel
948
+ // doesn't kill caller B's await.
949
+ async ensureConnected(connectId, signal) {
950
+ if (signal.aborted) _LedgerAdapter._throwIfAborted(signal);
951
+ if (isLedgerBleConnectionType(this.connector.connectionType) && !connectId) {
952
+ throw Object.assign(new Error("Ledger BLE connectId is required."), {
953
+ code: import_hwk_adapter_core2.HardwareErrorCode.DeviceNotFound
954
+ });
955
+ }
956
+ if (connectId && this._sessions.has(connectId)) return connectId;
957
+ if (this._sessions.size > 0) {
958
+ return this._sessions.keys().next().value;
959
+ }
960
+ if (!this._connectingPromise) {
961
+ this._doConnectAbortController = new AbortController();
962
+ const innerSignal = this._doConnectAbortController.signal;
963
+ this._connectingPromise = (async () => {
964
+ try {
965
+ return await this._doConnect(innerSignal, connectId);
966
+ } finally {
967
+ this._connectingPromise = null;
968
+ this._doConnectAbortController = null;
969
+ }
970
+ })();
971
+ }
972
+ return this._abortable(signal, this._connectingPromise);
973
+ }
974
+ // Layer 1 main loop — the ONLY place in SDK that emits unlock dialog.
975
+ // Bounded by MAX_DOCONNECT_CONFIRMS — after N Confirms with no progress,
976
+ // throw DeviceNotFound so the user is kicked out of the loop.
977
+ async _doConnect(internalSignal, targetConnectId) {
978
+ if (isLedgerBleConnectionType(this.connector.connectionType) && targetConnectId) {
979
+ try {
980
+ return await this._connectDeviceOrThrow(targetConnectId);
981
+ } catch (err) {
982
+ if (!isDeviceLockedError(err) && !isDeviceNotAdvertisingError(err) && !isDeviceDisconnectedError(err)) {
983
+ throw err;
984
+ }
985
+ this._discoveredDevices.delete(targetConnectId);
986
+ if (isDeviceDisconnectedError(err)) {
987
+ try {
988
+ this.connector.reset?.();
989
+ } catch {
990
+ }
991
+ }
992
+ }
993
+ }
994
+ let confirms = 0;
995
+ while (!internalSignal.aborted) {
996
+ this.emitter.emit("ui-event", {
997
+ type: import_hwk_adapter_core2.EConnectorInteraction.Searching,
998
+ payload: { sessionId: "" }
999
+ });
1000
+ let devices = await this.searchDevices();
1001
+ if (devices.length === 0) {
1002
+ for (let i = 0; i < 3 && !internalSignal.aborted; i += 1) {
1003
+ await new Promise((resolve) => {
1004
+ setTimeout(resolve, 1e3);
1005
+ });
1006
+ devices = await this.searchDevices();
1007
+ if (devices.length > 0) break;
1008
+ }
1009
+ }
737
1010
  if (devices.length > 0) {
738
- return this._connectFirstOrSelect(devices);
1011
+ try {
1012
+ return await this._connectFirstOrSelect(devices, targetConnectId);
1013
+ } catch (err) {
1014
+ if (!isDeviceLockedError(err) && !isDeviceNotAdvertisingError(err) && !isDeviceDisconnectedError(err)) {
1015
+ throw err;
1016
+ }
1017
+ this._discoveredDevices.clear();
1018
+ if (isDeviceDisconnectedError(err)) {
1019
+ try {
1020
+ this.connector.reset?.();
1021
+ } catch {
1022
+ }
1023
+ }
1024
+ }
739
1025
  }
740
- if (attempt < _LedgerAdapter.MAX_DEVICE_RETRY - 1) {
741
- await this._waitForDeviceConnect();
1026
+ if (confirms >= _LedgerAdapter.MAX_DOCONNECT_CONFIRMS) {
1027
+ throw Object.assign(
1028
+ new Error(
1029
+ "Device not connected after multiple attempts. Please ensure your Ledger is awake, unlocked, and in range, then try again."
1030
+ ),
1031
+ { code: import_hwk_adapter_core2.HardwareErrorCode.DeviceNotFound }
1032
+ );
742
1033
  }
1034
+ await this._waitForDeviceConnect(internalSignal);
1035
+ confirms += 1;
743
1036
  }
744
- throw Object.assign(
745
- new Error(
746
- "No Ledger device found after multiple attempts. Please connect and unlock your device."
747
- ),
748
- { _tag: "DeviceNotRecognizedError" }
749
- );
1037
+ _LedgerAdapter._throwIfAborted(internalSignal);
1038
+ throw new Error("_doConnect aborted");
750
1039
  }
751
- async _connectFirstOrSelect(devices) {
1040
+ async _connectFirstOrSelect(devices, targetConnectId) {
1041
+ if (targetConnectId) {
1042
+ const target = devices.find(
1043
+ (d) => d.connectId === targetConnectId || d.deviceId === targetConnectId
1044
+ );
1045
+ if (!target) {
1046
+ const err = Object.assign(new Error(`Target Ledger unavailable: ${targetConnectId}`), {
1047
+ code: import_hwk_adapter_core2.HardwareErrorCode.DeviceNotFound
1048
+ });
1049
+ if (isLedgerBleConnectionType(this.connector.connectionType)) {
1050
+ err._tag = ERROR_TAG.DeviceNotAdvertising;
1051
+ }
1052
+ throw err;
1053
+ }
1054
+ return this._connectDeviceOrThrow(target.connectId);
1055
+ }
1056
+ if (isLedgerBleConnectionType(this.connector.connectionType)) {
1057
+ throw Object.assign(new Error("Ledger BLE connectId is required."), {
1058
+ code: import_hwk_adapter_core2.HardwareErrorCode.DeviceNotFound
1059
+ });
1060
+ }
752
1061
  const chosenConnectId = devices.length === 1 ? devices[0].connectId : await this._chooseDeviceFromList(devices);
1062
+ return this._connectDeviceOrThrow(chosenConnectId);
1063
+ }
1064
+ async _connectDeviceOrThrow(chosenConnectId) {
753
1065
  const result = await this.connectDevice(chosenConnectId);
754
1066
  if (!result.success) {
755
- throw Object.assign(new Error(result.payload.error), {
756
- _tag: "DeviceNotRecognizedError"
1067
+ const payload = result.payload;
1068
+ const rethrow = Object.assign(new Error(payload.error), {
1069
+ code: payload.code
757
1070
  });
1071
+ if (payload._tag !== void 0) {
1072
+ rethrow._tag = payload._tag;
1073
+ }
1074
+ throw rethrow;
758
1075
  }
759
1076
  return chosenConnectId;
760
1077
  }
@@ -765,18 +1082,35 @@ var _LedgerAdapter = class _LedgerAdapter {
765
1082
  );
766
1083
  return devices[0].connectId;
767
1084
  }
768
- this.emitter.emit(import_hwk_adapter_core2.UI_REQUEST.REQUEST_SELECT_DEVICE, {
769
- type: import_hwk_adapter_core2.UI_REQUEST.REQUEST_SELECT_DEVICE,
770
- payload: { devices }
771
- });
772
- const response = await this._uiRegistry.wait(
773
- import_hwk_adapter_core2.UI_REQUEST.REQUEST_SELECT_DEVICE
774
- );
1085
+ let response;
1086
+ try {
1087
+ const waitPromise = this._uiRegistry.wait(
1088
+ import_hwk_adapter_core2.UI_REQUEST.REQUEST_SELECT_DEVICE
1089
+ );
1090
+ this.emitter.emit(import_hwk_adapter_core2.UI_REQUEST.REQUEST_SELECT_DEVICE, {
1091
+ type: import_hwk_adapter_core2.UI_REQUEST.REQUEST_SELECT_DEVICE,
1092
+ payload: { devices }
1093
+ });
1094
+ response = await waitPromise;
1095
+ } catch (err) {
1096
+ if (err?._tag === import_hwk_adapter_core2.UI_REQUEST_PREEMPTED_TAG) {
1097
+ this.emitter.emit(import_hwk_adapter_core2.UI_REQUEST.CLOSE_UI_WINDOW, {
1098
+ type: import_hwk_adapter_core2.UI_REQUEST.CLOSE_UI_WINDOW,
1099
+ payload: {}
1100
+ });
1101
+ throw Object.assign(new Error("Device selection superseded"), {
1102
+ _tag: ERROR_TAG.UserAborted,
1103
+ code: import_hwk_adapter_core2.HardwareErrorCode.UserAborted,
1104
+ cause: err
1105
+ });
1106
+ }
1107
+ throw err;
1108
+ }
775
1109
  const chosen = devices.find((d) => d.connectId === response?.sdkConnectId);
776
1110
  if (!chosen) {
777
1111
  throw Object.assign(
778
1112
  new Error(`Selected sdkConnectId '${response?.sdkConnectId}' not in discovered list`),
779
- { _tag: "DeviceNotRecognizedError" }
1113
+ { _tag: ERROR_TAG.DeviceNotRecognized }
780
1114
  );
781
1115
  }
782
1116
  return chosen.connectId;
@@ -789,30 +1123,31 @@ var _LedgerAdapter = class _LedgerAdapter {
789
1123
  * 3. Calls connector.call()
790
1124
  * 4. On disconnect error: clears stale session, re-connects, retries once
791
1125
  */
792
- async connectorCall(connectId, method, params, fingerprint) {
1126
+ async connectorCall(connectId, method, params, fingerprint, permissionDeviceId) {
793
1127
  debugLog("[LedgerAdapter] connectorCall:", method, "connectId:", connectId || "(empty)");
794
1128
  const queueKey = connectId || "__ledger_default__";
795
- const interruptibility = _LedgerAdapter._getInterruptibility(method);
796
1129
  return this._jobQueue.enqueue(
797
1130
  queueKey,
798
- async (signal) => this._runConnectorCall(connectId, method, params, signal, fingerprint),
799
- { interruptibility, label: method }
1131
+ async (signal) => this._runConnectorCall(connectId, method, params, signal, fingerprint, permissionDeviceId),
1132
+ {
1133
+ label: method,
1134
+ rejectIfBusy: true,
1135
+ busyError: _LedgerAdapter._createDeviceBusyError(method)
1136
+ }
800
1137
  );
801
1138
  }
802
1139
  /**
803
- * Race a promise against an abort signal. If the signal fires, rejects with the
804
- * signal's reason (or a generic Error). The underlying connector.call() cannot
805
- * actually be cancelled on Ledger DMK, but the caller gets the abort immediately.
1140
+ * Race a promise against an abort signal. On abort, rejects with
1141
+ * signal.reason instance _lastCancelReason → generic Error('Aborted').
806
1142
  */
807
- static _abortable(signal, promise) {
1143
+ _abortable(signal, promise) {
1144
+ const getAbortReason = () => signal.reason ?? this._lastCancelReason ?? new Error("Aborted");
808
1145
  if (signal.aborted) {
809
- return Promise.reject(
810
- signal.reason ?? new Error("Aborted")
811
- );
1146
+ return Promise.reject(getAbortReason());
812
1147
  }
813
1148
  return new Promise((resolve, reject) => {
814
1149
  const onAbort = () => {
815
- reject(signal.reason ?? new Error("Aborted"));
1150
+ reject(getAbortReason());
816
1151
  };
817
1152
  signal.addEventListener("abort", onAbort, { once: true });
818
1153
  promise.then(
@@ -834,40 +1169,49 @@ var _LedgerAdapter = class _LedgerAdapter {
834
1169
  }
835
1170
  }
836
1171
  /** Actual work done under the job queue — connection, fingerprint, call, and recovery. */
837
- async _runConnectorCall(connectId, method, params, signal, fingerprint) {
1172
+ async _runConnectorCall(connectId, method, params, signal, fingerprint, permissionDeviceId) {
838
1173
  _LedgerAdapter._throwIfAborted(signal);
839
- const resolvedConnectId = await _LedgerAdapter._abortable(
840
- signal,
841
- this.ensureConnected(connectId)
1174
+ await this._ensureDevicePermission(
1175
+ connectId,
1176
+ permissionDeviceId ?? fingerprint?.deviceId,
1177
+ signal
842
1178
  );
843
1179
  _LedgerAdapter._throwIfAborted(signal);
1180
+ let effectiveParams = params;
1181
+ if (method === "btcGetPublicKey") {
1182
+ const gatedParams = await this._gateBtcHighIndex(params);
1183
+ if (gatedParams === null) {
1184
+ throw Object.assign(new Error("User cancelled BTC high-index confirmation"), {
1185
+ _tag: ERROR_TAG.UserAborted,
1186
+ code: import_hwk_adapter_core2.HardwareErrorCode.UserAborted
1187
+ });
1188
+ }
1189
+ effectiveParams = gatedParams;
1190
+ }
1191
+ const resolvedConnectId = await this.ensureConnected(connectId, signal);
844
1192
  const sessionId = this._sessions.get(resolvedConnectId);
845
- debugLog(
846
- "[LedgerAdapter] connectorCall resolved:",
847
- method,
848
- "resolvedConnectId:",
849
- resolvedConnectId,
850
- "sessionId:",
851
- sessionId
852
- );
853
1193
  if (!sessionId) {
854
1194
  throw Object.assign(new Error("Auto-connect succeeded but no session found"), {
855
- _tag: "DeviceSessionNotFound"
1195
+ _tag: ERROR_TAG.DeviceSessionNotFound
856
1196
  });
857
1197
  }
858
- if (fingerprint && !fingerprint.skipFingerprint && fingerprint.deviceId) {
859
- const fp = await _LedgerAdapter._abortable(
860
- signal,
861
- this._verifyDeviceFingerprintWithSession(sessionId, fingerprint.deviceId, fingerprint.chain)
862
- );
863
- if (!fp.success) {
864
- throw Object.assign(new Error(formatDeviceMismatchError(fp.expected, fp.actual)), {
865
- code: import_hwk_adapter_core2.HardwareErrorCode.DeviceMismatch
866
- });
867
- }
868
- }
869
1198
  try {
870
- return await _LedgerAdapter._abortable(signal, this.connector.call(sessionId, method, params));
1199
+ if (fingerprint && !fingerprint.skipFingerprint && fingerprint.deviceId) {
1200
+ const fp = await this._abortable(
1201
+ signal,
1202
+ this._verifyDeviceFingerprintWithSession(
1203
+ sessionId,
1204
+ fingerprint.deviceId,
1205
+ fingerprint.chain
1206
+ )
1207
+ );
1208
+ if (!fp.success) {
1209
+ throw Object.assign(new Error(formatDeviceMismatchError(fp.expected, fp.actual)), {
1210
+ code: import_hwk_adapter_core2.HardwareErrorCode.DeviceMismatch
1211
+ });
1212
+ }
1213
+ }
1214
+ return await this._abortable(signal, this.connector.call(sessionId, method, effectiveParams));
871
1215
  } catch (err) {
872
1216
  if (signal.aborted) throw err;
873
1217
  const errObj = err;
@@ -877,37 +1221,243 @@ var _LedgerAdapter = class _LedgerAdapter {
877
1221
  errorCode: errObj?.errorCode,
878
1222
  statusCode: errObj?.statusCode,
879
1223
  isDisconnected: isDeviceDisconnectedError(err),
880
- isLocked: isDeviceLockedError(err)
1224
+ isLocked: isDeviceLockedError(err),
1225
+ isNotAdvertising: isDeviceNotAdvertisingError(err),
1226
+ isStuckApp: isStuckAppStateError(err)
881
1227
  });
882
- if (isDeviceDisconnectedError(err)) {
883
- debugLog("[LedgerAdapter] disconnected, retrying with fresh connection...");
884
- this._discoveredDevices.clear();
885
- return this._retryWithFreshConnection(resolvedConnectId, method, params, signal, err);
1228
+ if (isStuckAppStateError(err)) {
1229
+ try {
1230
+ this._sessions.delete(resolvedConnectId);
1231
+ this._discoveredDevices.delete(resolvedConnectId);
1232
+ this.connector.reset?.();
1233
+ } catch {
1234
+ }
1235
+ debugLog(
1236
+ "[LedgerAdapter] stuck-app retry: method=",
1237
+ method,
1238
+ "delayMs=",
1239
+ _LedgerAdapter.STUCK_APP_RETRY_DELAY_MS,
1240
+ "_tag=",
1241
+ err?._tag
1242
+ );
1243
+ try {
1244
+ const retryResult = await this._retryAfterStuckApp(
1245
+ resolvedConnectId,
1246
+ method,
1247
+ effectiveParams,
1248
+ signal,
1249
+ err,
1250
+ fingerprint
1251
+ );
1252
+ debugLog("[LedgerAdapter] stuck-app retry succeeded: method=", method);
1253
+ return retryResult;
1254
+ } catch (retryErr) {
1255
+ if (signal.aborted) throw retryErr;
1256
+ if (isStuckAppStateError(retryErr)) {
1257
+ debugLog("[LedgerAdapter] stuck-app retry exhausted (2nd 6901): method=", method);
1258
+ throw err;
1259
+ }
1260
+ debugLog(
1261
+ "[LedgerAdapter] stuck-app retry threw non-stuck error: method=",
1262
+ method,
1263
+ "retryErrTag=",
1264
+ retryErr?._tag
1265
+ );
1266
+ throw retryErr;
1267
+ }
886
1268
  }
887
- if (isDeviceLockedError(err)) {
888
- await this._waitForDeviceConnect(signal);
889
- _LedgerAdapter._throwIfAborted(signal);
890
- return _LedgerAdapter._abortable(signal, this.connector.call(sessionId, method, params));
1269
+ if (isDeviceLockedError(err) || isDeviceNotAdvertisingError(err) || isDeviceDisconnectedError(err)) {
1270
+ let lastErr = err;
1271
+ for (let attempt = 0; attempt < _LedgerAdapter.MAX_BUSINESS_RETRY_BUDGET; attempt += 1) {
1272
+ if (signal.aborted) throw lastErr;
1273
+ try {
1274
+ this._sessions.delete(resolvedConnectId);
1275
+ this._discoveredDevices.delete(resolvedConnectId);
1276
+ if (isDeviceDisconnectedError(lastErr)) {
1277
+ try {
1278
+ this.connector.reset?.();
1279
+ } catch {
1280
+ }
1281
+ }
1282
+ const retryConnectId = isLedgerBleConnectionType(this.connector.connectionType) ? resolvedConnectId : void 0;
1283
+ const reConnectId = await this.ensureConnected(retryConnectId, signal);
1284
+ const reSessionId = this._sessions.get(reConnectId);
1285
+ if (!reSessionId) throw lastErr;
1286
+ if (fingerprint && !fingerprint.skipFingerprint && fingerprint.deviceId) {
1287
+ const fp = await this._abortable(
1288
+ signal,
1289
+ this._verifyDeviceFingerprintWithSession(
1290
+ reSessionId,
1291
+ fingerprint.deviceId,
1292
+ fingerprint.chain
1293
+ )
1294
+ );
1295
+ if (!fp.success) {
1296
+ throw Object.assign(new Error(formatDeviceMismatchError(fp.expected, fp.actual)), {
1297
+ code: import_hwk_adapter_core2.HardwareErrorCode.DeviceMismatch
1298
+ });
1299
+ }
1300
+ }
1301
+ return await this._abortable(
1302
+ signal,
1303
+ this.connector.call(reSessionId, method, effectiveParams)
1304
+ );
1305
+ } catch (retryErr) {
1306
+ if (signal.aborted) throw retryErr;
1307
+ lastErr = retryErr;
1308
+ const canRetry = attempt < _LedgerAdapter.MAX_BUSINESS_RETRY_BUDGET - 1 && (isDeviceLockedError(retryErr) || isDeviceNotAdvertisingError(retryErr) || isDeviceDisconnectedError(retryErr));
1309
+ if (!canRetry) {
1310
+ throw retryErr;
1311
+ }
1312
+ }
1313
+ }
1314
+ throw lastErr;
891
1315
  }
892
1316
  if (isTimeoutError(err)) {
893
1317
  debugLog("[LedgerAdapter] timeout, retrying with fresh connection...");
894
1318
  this._discoveredDevices.delete(resolvedConnectId);
895
- return this._retryWithFreshConnection(resolvedConnectId, method, params, signal, err);
1319
+ return this._retryWithFreshConnection(
1320
+ resolvedConnectId,
1321
+ method,
1322
+ effectiveParams,
1323
+ signal,
1324
+ err,
1325
+ fingerprint
1326
+ );
1327
+ }
1328
+ if (isConnectionLevelError(err)) {
1329
+ debugLog("[LedgerAdapter] connection-level fail-closed reset");
1330
+ this._sessions.delete(resolvedConnectId);
1331
+ this._discoveredDevices.delete(resolvedConnectId);
1332
+ this.connector.reset();
1333
+ const codeNum = err?.code;
1334
+ throw Object.assign(err, {
1335
+ code: codeNum ?? import_hwk_adapter_core2.HardwareErrorCode.DeviceDisconnected
1336
+ });
896
1337
  }
897
1338
  throw err;
898
1339
  }
899
1340
  }
900
- /** Clear stale session, reconnect, and retry the call. */
901
- async _retryWithFreshConnection(resolvedConnectId, method, params, signal, originalErr) {
902
- this._sessions.delete(resolvedConnectId);
903
- _LedgerAdapter._throwIfAborted(signal);
904
- const retryConnectId = await this.ensureConnected();
905
- _LedgerAdapter._throwIfAborted(signal);
1341
+ /**
1342
+ * Stuck-app recovery: pause for the device's UI transition, then retry once.
1343
+ *
1344
+ * Caller has already cleared the session + reset connector. We wait so Stax
1345
+ * finishes its post-CloseApp animation, then go through ensureConnected +
1346
+ * fingerprint check + call exactly once. Caller decides what to do on a
1347
+ * second stuck-app hit.
1348
+ */
1349
+ async _retryAfterStuckApp(resolvedConnectId, method, params, signal, originalErr, fingerprint) {
1350
+ await this._sleepAbortable(_LedgerAdapter.STUCK_APP_RETRY_DELAY_MS, signal);
1351
+ const retryTargetConnectId = isLedgerBleConnectionType(this.connector.connectionType) ? resolvedConnectId : void 0;
1352
+ const retryConnectId = await this.ensureConnected(retryTargetConnectId, signal);
1353
+ const retrySessionId = this._sessions.get(retryConnectId);
1354
+ if (!retrySessionId) throw originalErr;
1355
+ if (fingerprint && !fingerprint.skipFingerprint && fingerprint.deviceId) {
1356
+ const fp = await this._abortable(
1357
+ signal,
1358
+ this._verifyDeviceFingerprintWithSession(
1359
+ retrySessionId,
1360
+ fingerprint.deviceId,
1361
+ fingerprint.chain
1362
+ )
1363
+ );
1364
+ if (!fp.success) {
1365
+ throw Object.assign(new Error(formatDeviceMismatchError(fp.expected, fp.actual)), {
1366
+ code: import_hwk_adapter_core2.HardwareErrorCode.DeviceMismatch
1367
+ });
1368
+ }
1369
+ }
1370
+ return this._abortable(signal, this.connector.call(retrySessionId, method, params));
1371
+ }
1372
+ _sleepAbortable(ms, signal) {
1373
+ return new Promise((resolve, reject) => {
1374
+ if (signal.aborted) {
1375
+ reject(signal.reason ?? new Error("Aborted"));
1376
+ return;
1377
+ }
1378
+ const timer = setTimeout(() => {
1379
+ signal.removeEventListener("abort", onAbort);
1380
+ resolve();
1381
+ }, ms);
1382
+ const onAbort = () => {
1383
+ clearTimeout(timer);
1384
+ reject(signal.reason ?? new Error("Aborted"));
1385
+ };
1386
+ signal.addEventListener("abort", onAbort, { once: true });
1387
+ });
1388
+ }
1389
+ /**
1390
+ * Clear stale session, reconnect, and retry the call.
1391
+ *
1392
+ * Timeout recovery starts with a full connector reset. After an APDU
1393
+ * timeout, DMK/transport state may still emit malformed responses; retrying
1394
+ * on the same DMK can poison the next chain switch.
1395
+ */
1396
+ async _retryWithFreshConnection(targetConnectId, method, params, signal, originalErr, fingerprint) {
1397
+ this.connector.reset();
1398
+ this._sessions.clear();
1399
+ this._discoveredDevices.clear();
1400
+ this._connectingPromise = null;
1401
+ const retryConnectId = await this.ensureConnected(targetConnectId, signal);
906
1402
  const retrySessionId = this._sessions.get(retryConnectId);
907
1403
  if (!retrySessionId) {
908
1404
  throw originalErr;
909
1405
  }
910
- return _LedgerAdapter._abortable(signal, this.connector.call(retrySessionId, method, params));
1406
+ try {
1407
+ if (fingerprint && !fingerprint.skipFingerprint && fingerprint.deviceId) {
1408
+ const fp = await this._abortable(
1409
+ signal,
1410
+ this._verifyDeviceFingerprintWithSession(
1411
+ retrySessionId,
1412
+ fingerprint.deviceId,
1413
+ fingerprint.chain
1414
+ )
1415
+ );
1416
+ if (!fp.success) {
1417
+ throw Object.assign(new Error(formatDeviceMismatchError(fp.expected, fp.actual)), {
1418
+ code: import_hwk_adapter_core2.HardwareErrorCode.DeviceMismatch
1419
+ });
1420
+ }
1421
+ }
1422
+ return await this._abortable(signal, this.connector.call(retrySessionId, method, params));
1423
+ } catch (retryErr) {
1424
+ if (signal.aborted) throw retryErr;
1425
+ this.connector.reset();
1426
+ this._sessions.clear();
1427
+ this._discoveredDevices.clear();
1428
+ this._connectingPromise = null;
1429
+ if (!isDeviceDisconnectedError(retryErr) && !isTimeoutError(retryErr)) {
1430
+ throw retryErr;
1431
+ }
1432
+ debugLog(
1433
+ "[LedgerAdapter] fresh-session retry still failed; resetting connector and rebuilding DMK"
1434
+ );
1435
+ this.connector.reset();
1436
+ this._sessions.clear();
1437
+ this._discoveredDevices.clear();
1438
+ this._connectingPromise = null;
1439
+ const finalConnectId = await this.ensureConnected(targetConnectId, signal);
1440
+ const finalSessionId = this._sessions.get(finalConnectId);
1441
+ if (!finalSessionId) {
1442
+ throw originalErr;
1443
+ }
1444
+ if (fingerprint && !fingerprint.skipFingerprint && fingerprint.deviceId) {
1445
+ const fp = await this._abortable(
1446
+ signal,
1447
+ this._verifyDeviceFingerprintWithSession(
1448
+ finalSessionId,
1449
+ fingerprint.deviceId,
1450
+ fingerprint.chain
1451
+ )
1452
+ );
1453
+ if (!fp.success) {
1454
+ throw Object.assign(new Error(formatDeviceMismatchError(fp.expected, fp.actual)), {
1455
+ code: import_hwk_adapter_core2.HardwareErrorCode.DeviceMismatch
1456
+ });
1457
+ }
1458
+ }
1459
+ return this._abortable(signal, this.connector.call(finalSessionId, method, params));
1460
+ }
911
1461
  }
912
1462
  /**
913
1463
  * Ensure OS-level device permission (Bluetooth / USB) before proceeding.
@@ -921,7 +1471,10 @@ var _LedgerAdapter = class _LedgerAdapter {
921
1471
  * - No connectId (searchDevices): environment-level permission
922
1472
  * - With connectId (business methods): device-level permission
923
1473
  */
924
- async _ensureDevicePermission(connectId, deviceId) {
1474
+ async _ensureDevicePermission(connectId, deviceId, signal) {
1475
+ if (signal?.aborted) {
1476
+ _LedgerAdapter._throwIfAborted(signal);
1477
+ }
925
1478
  const transportType = this.activeTransport ?? "hid";
926
1479
  const waitPromise = this._uiRegistry.wait(
927
1480
  import_hwk_adapter_core2.UI_REQUEST.REQUEST_DEVICE_PERMISSION,
@@ -931,10 +1484,37 @@ var _LedgerAdapter = class _LedgerAdapter {
931
1484
  type: import_hwk_adapter_core2.UI_REQUEST.REQUEST_DEVICE_PERMISSION,
932
1485
  payload: { transportType, connectId, deviceId }
933
1486
  });
934
- const { granted } = await waitPromise;
1487
+ let response;
1488
+ const onAbort = () => {
1489
+ this._uiRegistry.cancel(import_hwk_adapter_core2.UI_REQUEST.REQUEST_DEVICE_PERMISSION);
1490
+ };
1491
+ try {
1492
+ signal?.addEventListener("abort", onAbort, { once: true });
1493
+ response = await waitPromise;
1494
+ } catch (err) {
1495
+ if (signal?.aborted) {
1496
+ _LedgerAdapter._throwIfAborted(signal);
1497
+ }
1498
+ if (err?._tag === import_hwk_adapter_core2.UI_REQUEST_PREEMPTED_TAG) {
1499
+ this.emitter.emit(import_hwk_adapter_core2.UI_REQUEST.CLOSE_UI_WINDOW, {
1500
+ type: import_hwk_adapter_core2.UI_REQUEST.CLOSE_UI_WINDOW,
1501
+ payload: {}
1502
+ });
1503
+ throw Object.assign(new Error("Device permission request superseded"), {
1504
+ _tag: ERROR_TAG.UserAborted,
1505
+ code: import_hwk_adapter_core2.HardwareErrorCode.UserAborted,
1506
+ cause: err
1507
+ });
1508
+ }
1509
+ throw err;
1510
+ } finally {
1511
+ signal?.removeEventListener("abort", onAbort);
1512
+ }
1513
+ const { granted, reason, message } = response;
935
1514
  if (!granted) {
936
- throw Object.assign(new Error("Device permission denied"), {
937
- code: import_hwk_adapter_core2.HardwareErrorCode.DevicePermissionDenied
1515
+ throw Object.assign(new Error(message ?? "Device permission denied"), {
1516
+ code: import_hwk_adapter_core2.HardwareErrorCode.DevicePermissionDenied,
1517
+ reason
938
1518
  });
939
1519
  }
940
1520
  }
@@ -944,89 +1524,61 @@ var _LedgerAdapter = class _LedgerAdapter {
944
1524
  */
945
1525
  errorToFailure(err) {
946
1526
  debugError("[LedgerAdapter] error:", err);
1527
+ const tag = err && typeof err === "object" ? err._tag : void 0;
947
1528
  if (err && typeof err === "object" && "code" in err && typeof err.code === "number") {
948
1529
  const e = err;
949
- return ledgerFailure(e.code, e.message ?? "Unknown error", e.appName);
1530
+ const params = e.code === import_hwk_adapter_core2.HardwareErrorCode.DevicePermissionDenied && e.reason ? { permissionDeniedReason: e.reason } : void 0;
1531
+ return ledgerFailure(e.code, e.message ?? "Unknown error", e.appName, tag, params);
950
1532
  }
951
1533
  const mapped = mapLedgerError(err);
952
- return ledgerFailure(mapped.code, mapped.message, mapped.appName);
1534
+ return ledgerFailure(mapped.code, mapped.message, mapped.appName, tag);
953
1535
  }
954
1536
  registerEventListeners() {
955
1537
  this.connector.on("device-connect", this.deviceConnectHandler);
956
1538
  this.connector.on("device-disconnect", this.deviceDisconnectHandler);
957
- this.connector.on("ui-request", this.uiRequestHandler);
958
- this.connector.on("ui-event", this.uiEventHandler);
1539
+ this.connector.on("ui-event", this.uiEventForwarder);
959
1540
  }
960
1541
  unregisterEventListeners() {
961
1542
  this.connector.off("device-connect", this.deviceConnectHandler);
962
1543
  this.connector.off("device-disconnect", this.deviceDisconnectHandler);
963
- this.connector.off("ui-request", this.uiRequestHandler);
964
- this.connector.off("ui-event", this.uiEventHandler);
965
- }
966
- handleUiEvent(event) {
967
- if (!event.type) return;
968
- const payload = event.payload;
969
- const deviceInfo = payload ? this.extractDeviceInfoFromPayload(payload) : this.unknownDevice();
970
- switch (event.type) {
971
- case "ui-request_confirmation":
972
- this.emitter.emit(import_hwk_adapter_core2.UI_REQUEST.REQUEST_BUTTON, {
973
- type: import_hwk_adapter_core2.UI_REQUEST.REQUEST_BUTTON,
974
- payload: { device: deviceInfo }
975
- });
976
- break;
977
- }
1544
+ this.connector.off("ui-event", this.uiEventForwarder);
978
1545
  }
979
1546
  // ---------------------------------------------------------------------------
980
1547
  // Device info mapping
981
1548
  // ---------------------------------------------------------------------------
982
1549
  connectorDeviceToDeviceInfo(device) {
983
- const isBle = device.connectId && /^[0-9A-Fa-f]{4}$/.test(device.connectId);
984
1550
  return {
985
1551
  vendor: "ledger",
986
1552
  model: device.model ?? "unknown",
1553
+ modelName: device.modelName,
987
1554
  firmwareVersion: "",
988
1555
  deviceId: device.deviceId,
989
1556
  connectId: device.connectId,
990
1557
  label: device.name,
991
- connectionType: isBle ? "ble" : "usb",
1558
+ connectionType: this.connector.connectionType,
1559
+ rssi: device.rssi,
1560
+ isConnectable: device.isConnectable,
1561
+ serialNumber: device.serialNumber,
992
1562
  capabilities: device.capabilities
993
1563
  };
994
1564
  }
995
- extractDeviceInfoFromPayload(payload) {
996
- return {
997
- vendor: "ledger",
998
- model: payload.model ?? "unknown",
999
- firmwareVersion: "",
1000
- deviceId: payload.deviceId ?? payload.id ?? "",
1001
- connectId: payload.connectId ?? payload.path ?? "",
1002
- label: payload.label,
1003
- connectionType: "usb"
1004
- };
1005
- }
1006
- unknownDevice() {
1007
- return {
1008
- vendor: "ledger",
1009
- model: "unknown",
1010
- firmwareVersion: "",
1011
- deviceId: "",
1012
- connectId: "",
1013
- connectionType: "usb"
1014
- };
1015
- }
1016
1565
  };
1017
- // ---------------------------------------------------------------------------
1018
- // Private helpers
1019
- // ---------------------------------------------------------------------------
1020
- /**
1021
- * Ensure at least one device is connected and return a valid connectId.
1022
- *
1023
- * - If a session already exists for the given connectId, reuse it.
1024
- * - If ANY session exists (Ledger IDs are ephemeral), reuse it.
1025
- * - Otherwise: search 1 device: auto-connect, multiple: ask user, 0: throw.
1026
- */
1027
- _LedgerAdapter.MAX_DEVICE_RETRY = 3;
1566
+ // Layer 2 retry budget after connection-class error. Each round delegates
1567
+ // to Layer 1 (which owns the unlock prompt). Layer 2 never emits UI itself.
1568
+ _LedgerAdapter.MAX_BUSINESS_RETRY_BUDGET = 3;
1569
+ // Stuck-app (APDU 0x6901) retry pause. Ledger Stax often returns 6901 when
1570
+ // OpenAppCommand lands mid-transition right after CloseApp; a short wait
1571
+ // lets the device finish its UI animation before we retry once.
1572
+ _LedgerAdapter.STUCK_APP_RETRY_DELAY_MS = 500;
1573
+ // Layer 1 confirm budget. After N failed Confirm cycles (user keeps clicking
1574
+ // Confirm but device never shows up / never unlocks), throw DeviceNotFound
1575
+ // instead of looping forever.
1576
+ _LedgerAdapter.MAX_DOCONNECT_CONFIRMS = 3;
1028
1577
  var LedgerAdapter = _LedgerAdapter;
1029
1578
 
1579
+ // src/connector/LedgerConnectorBase.ts
1580
+ var import_hwk_adapter_core12 = require("@onekeyfe/hwk-adapter-core");
1581
+
1030
1582
  // src/device/LedgerDeviceManager.ts
1031
1583
  var LedgerDeviceManager = class {
1032
1584
  constructor(dmk) {
@@ -1063,7 +1615,9 @@ var LedgerDeviceManager = class {
1063
1615
  if (resolved) return;
1064
1616
  resolved = true;
1065
1617
  this._discovered.clear();
1066
- debugLog("[DMK] enumerate raw devices:", devices.length);
1618
+ debugLog(
1619
+ `[DMK] enumerate count=${devices.length} ids=[${devices.map((d) => d.id).join(",")}]`
1620
+ );
1067
1621
  for (const d of devices) {
1068
1622
  this._discovered.set(d.id, d);
1069
1623
  }
@@ -1073,8 +1627,10 @@ var LedgerDeviceManager = class {
1073
1627
  devices.map((d) => ({
1074
1628
  path: d.id,
1075
1629
  type: d.deviceModel.model,
1630
+ modelName: d.deviceModel.name,
1076
1631
  name: d.name,
1077
- transport: d.transport
1632
+ transport: d.transport,
1633
+ rssi: d.rssi
1078
1634
  }))
1079
1635
  );
1080
1636
  } else {
@@ -1095,8 +1651,10 @@ var LedgerDeviceManager = class {
1095
1651
  devices.map((d) => ({
1096
1652
  path: d.id,
1097
1653
  type: d.deviceModel.model,
1654
+ modelName: d.deviceModel.name,
1098
1655
  name: d.name,
1099
- transport: d.transport
1656
+ transport: d.transport,
1657
+ rssi: d.rssi
1100
1658
  }))
1101
1659
  );
1102
1660
  }
@@ -1120,8 +1678,10 @@ var LedgerDeviceManager = class {
1120
1678
  descriptor: {
1121
1679
  path: d.id,
1122
1680
  type: d.deviceModel.model,
1681
+ modelName: d.deviceModel.name,
1123
1682
  name: d.name,
1124
- transport: d.transport
1683
+ transport: d.transport,
1684
+ rssi: d.rssi
1125
1685
  }
1126
1686
  });
1127
1687
  }
@@ -1140,6 +1700,54 @@ var LedgerDeviceManager = class {
1140
1700
  this._listenSub?.unsubscribe();
1141
1701
  this._listenSub = null;
1142
1702
  }
1703
+ /**
1704
+ * Resolve to the latest snapshot of advertising devices observed within
1705
+ * `timeoutMs`. Rewrites `_discovered` so it tracks the live list.
1706
+ *
1707
+ * `listenToAvailableDevices` is a BehaviorSubject — it emits the cached
1708
+ * list on subscribe and *only* re-emits when the list changes. A stable
1709
+ * list of currently-advertising devices therefore produces only the
1710
+ * subscription frame; treating that frame as "fossil to be discarded"
1711
+ * caused devices to flicker in/out of the UI as searches alternated
1712
+ * between "saw a change frame" and "saw only the cached frame".
1713
+ *
1714
+ * The window still gives a chance for the active scan to update the list
1715
+ * (e.g. drop a peripheral that just stopped broadcasting), but if nothing
1716
+ * changes we trust the snapshot — DMK silence on a stable list means
1717
+ * "everything's still there".
1718
+ */
1719
+ getLiveDevices(timeoutMs = 1500) {
1720
+ debugLog(`[DMK] getLiveDevices() called, timeoutMs=${timeoutMs}`);
1721
+ void this.requestDevice();
1722
+ return new Promise((resolve) => {
1723
+ let done = false;
1724
+ let lastSeen = [];
1725
+ let sub = null;
1726
+ const finish = (devices) => {
1727
+ if (done) return;
1728
+ done = true;
1729
+ sub?.unsubscribe();
1730
+ clearTimeout(timer);
1731
+ this._discovered.clear();
1732
+ for (const d of devices) this._discovered.set(d.id, d);
1733
+ debugLog(
1734
+ `[DMK] getLiveDevices() resolved count=${devices.length} ids=[${devices.map((d) => d.id).join(",")}]`
1735
+ );
1736
+ resolve(devices);
1737
+ };
1738
+ const timer = setTimeout(() => finish(lastSeen), timeoutMs);
1739
+ sub = this._dmk.listenToAvailableDevices({}).subscribe({
1740
+ next: (devices) => {
1741
+ lastSeen = devices;
1742
+ },
1743
+ error: (err) => {
1744
+ debugError("[DMK] getLiveDevices() error:", err);
1745
+ finish(lastSeen);
1746
+ },
1747
+ complete: () => finish(lastSeen)
1748
+ });
1749
+ });
1750
+ }
1143
1751
  requestDevice() {
1144
1752
  if (this._discoverySub) {
1145
1753
  return Promise.resolve();
@@ -1157,11 +1765,33 @@ var LedgerDeviceManager = class {
1157
1765
  });
1158
1766
  return Promise.resolve();
1159
1767
  }
1768
+ /** Lookup a previously-discovered device's display name. */
1769
+ getDeviceName(deviceId) {
1770
+ return this._discovered.get(deviceId)?.name;
1771
+ }
1772
+ /** Lookup minimal model/signal info from a previously-discovered device. */
1773
+ getDiscoveredDeviceInfo(deviceId) {
1774
+ const d = this._discovered.get(deviceId);
1775
+ if (!d) return void 0;
1776
+ return {
1777
+ model: d.deviceModel?.model,
1778
+ modelName: d.deviceModel?.name,
1779
+ name: d.name,
1780
+ rssi: d.rssi
1781
+ };
1782
+ }
1783
+ hasDiscoveredDevice(deviceId) {
1784
+ return this._discovered.has(deviceId);
1785
+ }
1160
1786
  /** Connect to a previously discovered device. Returns sessionId. */
1161
1787
  async connect(deviceId) {
1162
1788
  const device = this._discovered.get(deviceId);
1163
1789
  if (!device) {
1164
- throw new Error(`Device "${deviceId}" not found. Call enumerate() or listen() first.`);
1790
+ const err = new Error(
1791
+ `Device "${deviceId}" not found in discovery cache. Call enumerate() or listen() first.`
1792
+ );
1793
+ err._tag = ERROR_TAG.DeviceNotInDiscoveryCache;
1794
+ throw err;
1165
1795
  }
1166
1796
  const sessionId = await this._dmk.connect({ device });
1167
1797
  this._sessions.set(deviceId, sessionId);
@@ -1200,6 +1830,19 @@ var LedgerDeviceManager = class {
1200
1830
  this._sessionToDevice.clear();
1201
1831
  this._dmk.close();
1202
1832
  }
1833
+ /**
1834
+ * Tear down RxJS subscriptions and local state without closing the DMK.
1835
+ * For light-reset paths that want to drop session state but keep the
1836
+ * underlying transport alive for retry. Call this instead of orphaning
1837
+ * the manager — bare null assignment leaks _listenSub / _discoverySub.
1838
+ */
1839
+ disposeKeepingDmk() {
1840
+ this.stopListening();
1841
+ this.stopDiscovery();
1842
+ this._discovered.clear();
1843
+ this._sessions.clear();
1844
+ this._sessionToDevice.clear();
1845
+ }
1203
1846
  };
1204
1847
 
1205
1848
  // src/signer/SignerManager.ts
@@ -1211,11 +1854,12 @@ var import_hwk_adapter_core3 = require("@onekeyfe/hwk-adapter-core");
1211
1854
 
1212
1855
  // src/signer/deviceActionToPromise.ts
1213
1856
  var import_device_management_kit = require("@ledgerhq/device-management-kit");
1214
- var DEFAULT_TIMEOUT_MS = 3e4;
1215
- function deviceActionToPromise(action, onInteraction, timeoutMs = DEFAULT_TIMEOUT_MS, onRegisterCanceller) {
1857
+ var IDLE_WATCHDOG_MS = 65e3;
1858
+ function deviceActionToPromise(action, onInteraction, timeoutMs = IDLE_WATCHDOG_MS, onRegisterCanceller) {
1216
1859
  return new Promise((resolve, reject) => {
1217
1860
  let settled = false;
1218
1861
  let lastStep;
1862
+ const observedSteps = [];
1219
1863
  let sub;
1220
1864
  let timer = null;
1221
1865
  const cancelAction = () => {
@@ -1228,44 +1872,59 @@ function deviceActionToPromise(action, onInteraction, timeoutMs = DEFAULT_TIMEOU
1228
1872
  } catch {
1229
1873
  }
1230
1874
  };
1231
- const resetTimer = () => {
1875
+ const armIdleWatchdog = () => {
1232
1876
  if (timer) clearTimeout(timer);
1233
1877
  if (timeoutMs > 0) {
1234
1878
  timer = setTimeout(() => {
1235
1879
  if (!settled) {
1236
1880
  settled = true;
1237
1881
  cancelAction();
1238
- reject(new Error("Device action timed out \u2014 device may be locked or disconnected"));
1882
+ reject(
1883
+ new Error(
1884
+ "Ledger transport unresponsive: no DMK state change within the watchdog window. The device may have lost connection."
1885
+ )
1886
+ );
1239
1887
  }
1240
1888
  }, timeoutMs);
1241
1889
  }
1242
1890
  };
1243
- resetTimer();
1891
+ armIdleWatchdog();
1244
1892
  if (onRegisterCanceller) {
1245
- onRegisterCanceller(() => {
1246
- if (settled) return;
1893
+ onRegisterCanceller((reason) => {
1894
+ if (settled) {
1895
+ return;
1896
+ }
1247
1897
  settled = true;
1248
1898
  if (timer) clearTimeout(timer);
1249
1899
  cancelAction();
1250
- reject(new Error("Device action cancelled"));
1900
+ const err = new Error(reason?.message ?? "Device action cancelled");
1901
+ if (reason?.code !== void 0) {
1902
+ err.code = reason.code;
1903
+ }
1904
+ if (reason?.tag) {
1905
+ Object.defineProperty(err, "_tag", {
1906
+ value: reason.tag,
1907
+ configurable: true,
1908
+ enumerable: false,
1909
+ writable: true
1910
+ });
1911
+ }
1912
+ reject(err);
1251
1913
  });
1252
1914
  }
1253
- debugLog("[DMK-Observable] subscribing to action.observable...");
1254
1915
  sub = action.observable.subscribe({
1255
1916
  next: (state) => {
1256
- if (settled) return;
1257
- resetTimer();
1917
+ if (settled) {
1918
+ return;
1919
+ }
1920
+ armIdleWatchdog();
1258
1921
  const step = state?.intermediateValue?.step;
1259
- if (step) lastStep = step;
1260
- debugLog(
1261
- "[DMK-Observable] state:",
1262
- JSON.stringify({
1263
- status: state.status,
1264
- intermediateValue: state.status === import_device_management_kit.DeviceActionStatus.Pending ? state.intermediateValue : void 0,
1265
- hasOutput: state.status === import_device_management_kit.DeviceActionStatus.Completed,
1266
- hasError: state.status === import_device_management_kit.DeviceActionStatus.Error
1267
- })
1268
- );
1922
+ if (step) {
1923
+ lastStep = step;
1924
+ if (observedSteps[observedSteps.length - 1] !== step) {
1925
+ observedSteps.push(step);
1926
+ }
1927
+ }
1269
1928
  if (state.status === import_device_management_kit.DeviceActionStatus.Completed) {
1270
1929
  settled = true;
1271
1930
  if (timer) clearTimeout(timer);
@@ -1277,10 +1936,14 @@ function deviceActionToPromise(action, onInteraction, timeoutMs = DEFAULT_TIMEOU
1277
1936
  if (timer) clearTimeout(timer);
1278
1937
  onInteraction?.("interaction-complete");
1279
1938
  sub?.unsubscribe();
1280
- rejectWithLastStep(state.error, lastStep, reject);
1939
+ rejectWithStepContext(state.error, lastStep, observedSteps, reject);
1281
1940
  } else if (state.status === import_device_management_kit.DeviceActionStatus.Pending && onInteraction) {
1282
1941
  const interaction = state.intermediateValue?.requiredUserInteraction;
1283
1942
  if (interaction && interaction !== "none") {
1943
+ if (interaction === "unlock-device" && timer) {
1944
+ clearTimeout(timer);
1945
+ timer = null;
1946
+ }
1284
1947
  onInteraction(String(interaction));
1285
1948
  }
1286
1949
  }
@@ -1290,7 +1953,7 @@ function deviceActionToPromise(action, onInteraction, timeoutMs = DEFAULT_TIMEOU
1290
1953
  settled = true;
1291
1954
  if (timer) clearTimeout(timer);
1292
1955
  sub?.unsubscribe();
1293
- rejectWithLastStep(err, lastStep, reject);
1956
+ rejectWithStepContext(err, lastStep, observedSteps, reject);
1294
1957
  }
1295
1958
  },
1296
1959
  complete: () => {
@@ -1303,15 +1966,25 @@ function deviceActionToPromise(action, onInteraction, timeoutMs = DEFAULT_TIMEOU
1303
1966
  });
1304
1967
  });
1305
1968
  }
1306
- function rejectWithLastStep(err, lastStep, reject) {
1307
- if (err && typeof err === "object" && lastStep) {
1969
+ function rejectWithStepContext(err, lastStep, observedSteps, reject) {
1970
+ if (err && typeof err === "object" && (lastStep || observedSteps.length > 0)) {
1308
1971
  try {
1309
- Object.defineProperty(err, "_lastStep", {
1310
- value: lastStep,
1311
- configurable: true,
1312
- enumerable: false,
1313
- writable: true
1314
- });
1972
+ if (lastStep) {
1973
+ Object.defineProperty(err, "_lastStep", {
1974
+ value: lastStep,
1975
+ configurable: true,
1976
+ enumerable: false,
1977
+ writable: true
1978
+ });
1979
+ }
1980
+ if (observedSteps.length > 0) {
1981
+ Object.defineProperty(err, "_deviceActionSteps", {
1982
+ value: [...observedSteps],
1983
+ configurable: true,
1984
+ enumerable: false,
1985
+ writable: true
1986
+ });
1987
+ }
1315
1988
  } catch {
1316
1989
  }
1317
1990
  }
@@ -1377,7 +2050,8 @@ var SignerManager = class _SignerManager {
1377
2050
  async getOrCreate(sessionId) {
1378
2051
  debugLog("[DMK] SignerManager.getOrCreate:", { sessionId });
1379
2052
  const builder = await this._builderFn({ dmk: this._dmk, sessionId });
1380
- return new SignerEth(builder.build());
2053
+ const builderWithContext = builder.withContextModule ? builder.withContextModule(_SignerManager._createContextModule()) : builder;
2054
+ return new SignerEth(builderWithContext.build());
1381
2055
  }
1382
2056
  // eslint-disable-next-line @typescript-eslint/no-unused-vars, class-methods-use-this
1383
2057
  invalidate(_sessionId) {
@@ -1386,10 +2060,24 @@ var SignerManager = class _SignerManager {
1386
2060
  clearAll() {
1387
2061
  }
1388
2062
  static _defaultBuilder() {
1389
- return (args) => {
1390
- const contextModule = new import_context_module.ContextModuleBuilder({}).removeDefaultLoaders().build();
1391
- return new import_device_signer_kit_ethereum.SignerEthBuilder(args).withContextModule(contextModule);
2063
+ return (args) => new import_device_signer_kit_ethereum.SignerEthBuilder(args);
2064
+ }
2065
+ static _createContextModule() {
2066
+ const contextModule = new import_context_module.ContextModuleBuilder({}).removeDefaultLoaders().build();
2067
+ return _SignerManager.wrapBlindSigningReportNonBlocking(contextModule);
2068
+ }
2069
+ static wrapBlindSigningReportNonBlocking(contextModule) {
2070
+ const report = contextModule.report.bind(contextModule);
2071
+ contextModule.report = async (params) => {
2072
+ try {
2073
+ void report(params).catch((err) => {
2074
+ debugLog("[DMK] ContextModule.report failed:", err);
2075
+ });
2076
+ } catch (err) {
2077
+ debugLog("[DMK] ContextModule.report failed:", err);
2078
+ }
1392
2079
  };
2080
+ return contextModule;
1393
2081
  }
1394
2082
  };
1395
2083
 
@@ -1499,6 +2187,7 @@ async function _getEthSigner(ctx, sessionId) {
1499
2187
  const signerManager = await ctx.getSignerManager();
1500
2188
  const signer = await signerManager.getOrCreate(sessionId);
1501
2189
  signer.onInteraction = (interaction) => {
2190
+ debugLog("[LedgerConnector] evm.onInteraction:", interaction);
1502
2191
  ctx.emit("ui-event", {
1503
2192
  type: collapseSignerInteraction(interaction),
1504
2193
  payload: { sessionId }
@@ -1699,6 +2388,7 @@ async function btcSignTransaction(ctx, sessionId, params) {
1699
2388
  const path = normalizePath(params.path || "84'/0'/0'");
1700
2389
  const purpose = path.split("/")[0]?.replace(/'/g, "");
1701
2390
  const template = _purposeToTemplate(purpose, DefaultDescriptorTemplate);
2391
+ debugLog("[LedgerConnector] btcSignTransaction wallet:", { path, purpose, template });
1702
2392
  const wallet = new DefaultWallet(path, template);
1703
2393
  const signedTxHex = await btcSigner.signTransaction(wallet, params.psbt);
1704
2394
  return { serializedTx: (0, import_hwk_adapter_core7.stripHex)(signedTxHex) };
@@ -1723,8 +2413,10 @@ async function btcSignPsbt(ctx, sessionId, params) {
1723
2413
  const path = normalizePath(params.path || "84'/0'/0'");
1724
2414
  const purpose = path.split("/")[0]?.replace(/'/g, "");
1725
2415
  const template = _purposeToTemplate(purpose, DefaultDescriptorTemplate);
2416
+ debugLog("[LedgerConnector] btcSignPsbt wallet:", { path, purpose, template });
1726
2417
  const wallet = new DefaultWallet(path, template);
1727
2418
  const signatures = await btcSigner.signPsbt(wallet, params.psbt);
2419
+ debugLog("[LedgerConnector] btcSignPsbt signatures received:", signatures.length);
1728
2420
  const signedPsbtHex = _applySignaturesToPsbt(params.psbt, signatures);
1729
2421
  return { signedPsbt: signedPsbtHex };
1730
2422
  } catch (err) {
@@ -1771,6 +2463,7 @@ async function _createBtcSigner(ctx, sessionId) {
1771
2463
  const sdkSigner = new SignerBtcBuilder({ dmk, sessionId }).build();
1772
2464
  const signer = new SignerBtc(sdkSigner);
1773
2465
  signer.onInteraction = (interaction) => {
2466
+ debugLog("[LedgerConnector] btc.onInteraction:", interaction);
1774
2467
  ctx.emit("ui-event", {
1775
2468
  type: collapseSignerInteraction(interaction),
1776
2469
  payload: { sessionId }
@@ -1941,6 +2634,7 @@ async function _createSolSigner(ctx, sessionId) {
1941
2634
  const sdkSigner = new SignerSolanaBuilder({ dmk, sessionId }).withContextModule(contextModule).build();
1942
2635
  const signer = new SignerSol(sdkSigner);
1943
2636
  signer.onInteraction = (interaction) => {
2637
+ debugLog("[LedgerConnector] sol.onInteraction:", interaction);
1944
2638
  ctx.emit("ui-event", {
1945
2639
  type: collapseSignerInteraction(interaction),
1946
2640
  payload: { sessionId }
@@ -1953,14 +2647,15 @@ async function _createSolSigner(ctx, sessionId) {
1953
2647
  }
1954
2648
 
1955
2649
  // src/connector/chains/tron.ts
1956
- var import_hwk_adapter_core10 = require("@onekeyfe/hwk-adapter-core");
2650
+ var import_hwk_adapter_core11 = require("@onekeyfe/hwk-adapter-core");
1957
2651
  var import_hw_app_trx = __toESM(require("@ledgerhq/hw-app-trx"));
1958
2652
 
1959
- // src/connector/chains/legacyAppRetry.ts
1960
- var import_hwk_adapter_core9 = require("@onekeyfe/hwk-adapter-core");
2653
+ // src/connector/chains/legacyChainCall.ts
2654
+ var import_hwk_adapter_core10 = require("@onekeyfe/hwk-adapter-core");
1961
2655
 
1962
2656
  // src/app/AppManager.ts
1963
2657
  var import_device_management_kit2 = require("@ledgerhq/device-management-kit");
2658
+ var import_hwk_adapter_core9 = require("@onekeyfe/hwk-adapter-core");
1964
2659
  var APP_NAME_MAP = {
1965
2660
  ETH: "Ethereum",
1966
2661
  BTC: "Bitcoin",
@@ -1996,9 +2691,16 @@ var AppManager = class {
1996
2691
  * 5. Poll until the device confirms the target app is running.
1997
2692
  */
1998
2693
  /**
1999
- * @param onConfirmOnDevice Called after OpenAppCommand succeeds
2000
- * the device is now showing a confirmation prompt to the user.
2001
- * NOT called if the app is already open or if the app is not installed.
2694
+ * @param onConfirmOnDevice Called BEFORE OpenAppCommand is issued the
2695
+ * device is about to display "Open <app>" on screen and wait for the
2696
+ * user's button press. UI consumers should show their "open app" prompt
2697
+ * in response. NOT called when the target app is already open (no user
2698
+ * interaction needed in that case).
2699
+ *
2700
+ * Important: OpenAppCommand is blocking. It does not resolve until the user
2701
+ * has physically confirmed on the device, so anything that runs AFTER
2702
+ * `await this._openApp(...)` lands AFTER the prompt is already gone.
2703
+ * Hence the callback must fire BEFORE that await.
2002
2704
  */
2003
2705
  async ensureAppOpen(sessionId, targetAppName, onConfirmOnDevice) {
2004
2706
  const currentApp = await this._getCurrentApp(sessionId);
@@ -2009,8 +2711,8 @@ var AppManager = class {
2009
2711
  await this._closeCurrentApp(sessionId);
2010
2712
  await this._waitForApp(sessionId, DASHBOARD_APP_NAME);
2011
2713
  }
2012
- await this._openApp(sessionId, targetAppName);
2013
2714
  onConfirmOnDevice?.();
2715
+ await this._openApp(sessionId, targetAppName);
2014
2716
  await this._waitForApp(sessionId, targetAppName);
2015
2717
  }
2016
2718
  // ---------------------------------------------------------------------------
@@ -2022,9 +2724,34 @@ var AppManager = class {
2022
2724
  command: new import_device_management_kit2.GetAppAndVersionCommand()
2023
2725
  });
2024
2726
  if ((0, import_device_management_kit2.isSuccessCommandResult)(result)) {
2727
+ debugLog("[AppManager] currentApp:", result.data.name);
2025
2728
  return result.data.name;
2026
2729
  }
2027
- throw new Error("Failed to get current app from device");
2730
+ const errResult = result;
2731
+ const dmkErr = errResult.error ?? {};
2732
+ const original = dmkErr.originalError;
2733
+ debugLog(
2734
+ "[AppManager] _getCurrentApp failed sessionId=",
2735
+ sessionId,
2736
+ "tag=",
2737
+ dmkErr._tag,
2738
+ "errorCode=",
2739
+ dmkErr.errorCode,
2740
+ "message=",
2741
+ dmkErr.message,
2742
+ "originalErrorMessage=",
2743
+ original?.message ?? String(original ?? "")
2744
+ );
2745
+ throw Object.assign(
2746
+ new Error(
2747
+ dmkErr.message ?? "Failed to get current app from device"
2748
+ ),
2749
+ {
2750
+ _tag: dmkErr._tag,
2751
+ errorCode: dmkErr.errorCode,
2752
+ originalError: original
2753
+ }
2754
+ );
2028
2755
  }
2029
2756
  async _openApp(sessionId, appName) {
2030
2757
  const result = await this._dmk.sendCommand({
@@ -2033,8 +2760,11 @@ var AppManager = class {
2033
2760
  });
2034
2761
  if (!(0, import_device_management_kit2.isSuccessCommandResult)(result)) {
2035
2762
  const { statusCode } = result;
2763
+ const hasStatusCode2 = statusCode != null && statusCode !== "";
2764
+ debugLog("[AppManager] openApp failed:", appName, "statusCode:", statusCode);
2036
2765
  throw Object.assign(new Error(`Failed to open "${appName}"`), {
2037
- _tag: "OpenAppCommandError",
2766
+ _tag: ERROR_TAG.OpenAppCommand,
2767
+ code: hasStatusCode2 ? void 0 : import_hwk_adapter_core9.HardwareErrorCode.AppNotInstalled,
2038
2768
  errorCode: String(statusCode ?? ""),
2039
2769
  statusCode,
2040
2770
  appName
@@ -2042,6 +2772,7 @@ var AppManager = class {
2042
2772
  }
2043
2773
  }
2044
2774
  async _closeCurrentApp(sessionId) {
2775
+ debugLog("[AppManager] closeCurrentApp");
2045
2776
  await this._dmk.sendCommand({
2046
2777
  sessionId,
2047
2778
  command: new import_device_management_kit2.CloseAppCommand()
@@ -2052,15 +2783,23 @@ var AppManager = class {
2052
2783
  * or throw after `_maxRetries` attempts.
2053
2784
  */
2054
2785
  async _waitForApp(sessionId, expectedAppName) {
2786
+ let lastSeen = "";
2055
2787
  for (let i = 0; i < this._maxRetries; i++) {
2056
2788
  await this._wait();
2057
2789
  const current = await this._getCurrentApp(sessionId);
2790
+ lastSeen = current;
2058
2791
  if (current === expectedAppName) {
2059
2792
  return;
2060
2793
  }
2061
2794
  }
2795
+ debugLog(
2796
+ "[AppManager] waitForApp exhausted: expected=",
2797
+ expectedAppName,
2798
+ "lastSeen=",
2799
+ lastSeen
2800
+ );
2062
2801
  throw new Error(
2063
- `Ledger: failed to open "${expectedAppName}" after ${this._maxRetries} retries`
2802
+ `Ledger: failed to open "${expectedAppName}" after ${this._maxRetries} retries (last seen: ${lastSeen})`
2064
2803
  );
2065
2804
  }
2066
2805
  _isDashboard(appName) {
@@ -2071,46 +2810,92 @@ var AppManager = class {
2071
2810
  }
2072
2811
  };
2073
2812
 
2074
- // src/connector/chains/legacyAppRetry.ts
2813
+ // src/connector/chains/legacyChainCall.ts
2075
2814
  function isLegacyWrongAppError(err, _appName) {
2076
2815
  return isWrongAppError(err);
2077
2816
  }
2078
- async function withLegacyAppRetry(ctx, sessionId, appName, action) {
2079
- try {
2080
- return await action(sessionId);
2081
- } catch (err) {
2817
+ async function withLegacyChainCall(ctx, sessionId, options, action) {
2818
+ const { appName, needsConfirmation } = options;
2819
+ let openAppPromptShown = false;
2820
+ const onAppOpenPrompt = () => {
2821
+ openAppPromptShown = true;
2082
2822
  ctx.emit("ui-event", {
2083
- type: import_hwk_adapter_core9.EConnectorInteraction.InteractionComplete,
2823
+ type: import_hwk_adapter_core10.EConnectorInteraction.ConfirmOpenApp,
2084
2824
  payload: { sessionId }
2085
2825
  });
2086
- if (isLegacyWrongAppError(err, appName)) {
2087
- const dmk = await ctx.getOrCreateDmk();
2088
- const appManager = new AppManager(dmk);
2089
- try {
2090
- await appManager.ensureAppOpen(sessionId, appName, () => {
2091
- ctx.emit("ui-event", {
2092
- type: import_hwk_adapter_core9.EConnectorInteraction.ConfirmOpenApp,
2093
- payload: { sessionId }
2094
- });
2095
- });
2096
- } catch (switchErr) {
2097
- ctx.emit("ui-event", {
2098
- type: import_hwk_adapter_core9.EConnectorInteraction.InteractionComplete,
2099
- payload: { sessionId }
2100
- });
2101
- throw ctx.wrapError(switchErr);
2102
- }
2103
- ctx.clearAllSigners();
2826
+ };
2827
+ const closeOpenAppUiIfShown = () => {
2828
+ if (openAppPromptShown) {
2829
+ ctx.emit("ui-event", {
2830
+ type: import_hwk_adapter_core10.EConnectorInteraction.InteractionComplete,
2831
+ payload: { sessionId }
2832
+ });
2833
+ openAppPromptShown = false;
2834
+ }
2835
+ };
2836
+ try {
2837
+ await _ensureAppOpen(ctx, sessionId, appName, onAppOpenPrompt);
2838
+ } catch (err) {
2839
+ debugLog(
2840
+ "[LegacyChainCall] pre-flight ensureAppOpen failed:",
2841
+ appName,
2842
+ err?.message
2843
+ );
2844
+ closeOpenAppUiIfShown();
2845
+ throw ctx.wrapError(err, { defaultAppName: appName });
2846
+ }
2847
+ const runOnce = async () => {
2848
+ let confirmEmitted = false;
2849
+ if (needsConfirmation) {
2104
2850
  ctx.emit("ui-event", {
2105
- type: import_hwk_adapter_core9.EConnectorInteraction.InteractionComplete,
2851
+ type: import_hwk_adapter_core10.EConnectorInteraction.ConfirmOnDevice,
2106
2852
  payload: { sessionId }
2107
2853
  });
2854
+ confirmEmitted = true;
2855
+ }
2856
+ try {
2108
2857
  return await action(sessionId);
2858
+ } finally {
2859
+ if (confirmEmitted || openAppPromptShown) {
2860
+ ctx.emit("ui-event", {
2861
+ type: import_hwk_adapter_core10.EConnectorInteraction.InteractionComplete,
2862
+ payload: { sessionId }
2863
+ });
2864
+ openAppPromptShown = false;
2865
+ }
2109
2866
  }
2110
- ctx.invalidateSession(sessionId);
2111
- throw ctx.wrapError(err);
2867
+ };
2868
+ try {
2869
+ return await runOnce();
2870
+ } catch (err) {
2871
+ if (!isLegacyWrongAppError(err, appName)) {
2872
+ debugLog("[LegacyChainCall] non-wrong-app failure:", appName, err?.message);
2873
+ ctx.invalidateSession(sessionId);
2874
+ throw ctx.wrapError(err, { defaultAppName: appName });
2875
+ }
2876
+ debugLog("[LegacyChainCall] wrong-app detected, retrying:", appName);
2877
+ try {
2878
+ await _ensureAppOpen(ctx, sessionId, appName, onAppOpenPrompt);
2879
+ } catch (switchErr) {
2880
+ debugLog(
2881
+ "[LegacyChainCall] retry ensureAppOpen failed:",
2882
+ appName,
2883
+ switchErr?.message
2884
+ );
2885
+ closeOpenAppUiIfShown();
2886
+ throw ctx.wrapError(switchErr, { defaultAppName: appName });
2887
+ }
2888
+ ctx.clearAllSigners();
2889
+ const result = await runOnce();
2890
+ debugLog("[LegacyChainCall] retry succeeded:", appName);
2891
+ return result;
2112
2892
  }
2113
2893
  }
2894
+ async function _ensureAppOpen(ctx, sessionId, appName, onPrompt) {
2895
+ const dmk = await ctx.getOrCreateDmk();
2896
+ const appManager = new AppManager(dmk);
2897
+ await appManager.ensureAppOpen(sessionId, appName, onPrompt);
2898
+ }
2114
2899
 
2115
2900
  // src/transport/DmkTransport.ts
2116
2901
  var import_hw_transport = __toESM(require("@ledgerhq/hw-transport"));
@@ -2140,83 +2925,75 @@ var DmkTransport = class extends import_hw_transport.default {
2140
2925
  // src/connector/chains/tron.ts
2141
2926
  async function tronGetAddress(ctx, sessionId, params) {
2142
2927
  const path = normalizePath(params.path);
2143
- await _ensureTronAppOpen(ctx, sessionId);
2144
- return withLegacyAppRetry(ctx, sessionId, "Tron", async (sid) => {
2145
- const trx = await _createTrx(ctx, sid);
2146
- if (params.showOnDevice) {
2147
- ctx.emit("ui-event", {
2148
- type: import_hwk_adapter_core10.EConnectorInteraction.ConfirmOnDevice,
2149
- payload: { sessionId: sid }
2150
- });
2928
+ const showOnDevice = params.showOnDevice ?? false;
2929
+ return withLegacyChainCall(
2930
+ ctx,
2931
+ sessionId,
2932
+ {
2933
+ appName: "Tron",
2934
+ // Only show "confirm on device" UI when the device is actually going
2935
+ // to display the address for the user to verify.
2936
+ needsConfirmation: showOnDevice
2937
+ },
2938
+ async (sid) => {
2939
+ const trx = await _createTrx(ctx, sid);
2940
+ const result = await trx.getAddress(path, showOnDevice);
2941
+ return { address: result.address, publicKey: result.publicKey, path: params.path };
2151
2942
  }
2152
- const result = await trx.getAddress(path, params.showOnDevice ?? false);
2153
- ctx.emit("ui-event", {
2154
- type: import_hwk_adapter_core10.EConnectorInteraction.InteractionComplete,
2155
- payload: { sessionId: sid }
2156
- });
2157
- return { address: result.address, publicKey: result.publicKey, path: params.path };
2158
- });
2943
+ );
2159
2944
  }
2160
2945
  async function tronSignTransaction(ctx, sessionId, params) {
2161
2946
  if (!params.rawTxHex) {
2162
2947
  throw Object.assign(
2163
2948
  new Error("TRON signing requires a protobuf-encoded raw transaction hex (rawTxHex)."),
2164
- { code: import_hwk_adapter_core10.HardwareErrorCode.InvalidParams }
2949
+ { code: import_hwk_adapter_core11.HardwareErrorCode.InvalidParams }
2165
2950
  );
2166
2951
  }
2167
2952
  const path = normalizePath(params.path);
2168
- await _ensureTronAppOpen(ctx, sessionId);
2169
- return withLegacyAppRetry(ctx, sessionId, "Tron", async (sid) => {
2170
- const trx = await _createTrx(ctx, sid);
2171
- ctx.emit("ui-event", {
2172
- type: import_hwk_adapter_core10.EConnectorInteraction.ConfirmOnDevice,
2173
- payload: { sessionId: sid }
2174
- });
2175
- const signature = await trx.signTransaction(
2176
- path,
2177
- params.rawTxHex,
2178
- params.tokenSignatures ?? []
2179
- );
2180
- ctx.emit("ui-event", {
2181
- type: import_hwk_adapter_core10.EConnectorInteraction.InteractionComplete,
2182
- payload: { sessionId: sid }
2183
- });
2184
- return { signature };
2185
- });
2953
+ return withLegacyChainCall(
2954
+ ctx,
2955
+ sessionId,
2956
+ { appName: "Tron", needsConfirmation: true },
2957
+ async (sid) => {
2958
+ const trx = await _createTrx(ctx, sid);
2959
+ const signature = await trx.signTransaction(
2960
+ path,
2961
+ params.rawTxHex,
2962
+ params.tokenSignatures ?? []
2963
+ );
2964
+ return { signature };
2965
+ }
2966
+ );
2186
2967
  }
2187
2968
  async function tronSignMessage(ctx, sessionId, params) {
2188
2969
  const path = normalizePath(params.path);
2189
- await _ensureTronAppOpen(ctx, sessionId);
2190
- return withLegacyAppRetry(ctx, sessionId, "Tron", async (sid) => {
2191
- const trx = await _createTrx(ctx, sid);
2192
- ctx.emit("ui-event", {
2193
- type: import_hwk_adapter_core10.EConnectorInteraction.ConfirmOnDevice,
2194
- payload: { sessionId: sid }
2195
- });
2196
- const signature = await trx.signPersonalMessage(path, params.messageHex);
2197
- ctx.emit("ui-event", {
2198
- type: import_hwk_adapter_core10.EConnectorInteraction.InteractionComplete,
2199
- payload: { sessionId: sid }
2200
- });
2201
- return { signature };
2202
- });
2970
+ return withLegacyChainCall(
2971
+ ctx,
2972
+ sessionId,
2973
+ { appName: "Tron", needsConfirmation: true },
2974
+ async (sid) => {
2975
+ const trx = await _createTrx(ctx, sid);
2976
+ const signature = await trx.signPersonalMessage(path, params.messageHex);
2977
+ return { signature };
2978
+ }
2979
+ );
2203
2980
  }
2204
2981
  async function _createTrx(ctx, sessionId) {
2205
2982
  const dmk = await ctx.getOrCreateDmk();
2206
2983
  return new import_hw_app_trx.default(new DmkTransport(dmk, sessionId));
2207
2984
  }
2208
- async function _ensureTronAppOpen(ctx, sessionId) {
2209
- const dmk = await ctx.getOrCreateDmk();
2210
- const appManager = new AppManager(dmk);
2211
- await appManager.ensureAppOpen(sessionId, "Tron", () => {
2212
- ctx.emit("ui-event", {
2213
- type: import_hwk_adapter_core10.EConnectorInteraction.ConfirmOpenApp,
2214
- payload: { sessionId }
2215
- });
2216
- });
2217
- }
2218
2985
 
2219
2986
  // src/connector/LedgerConnectorBase.ts
2987
+ var METHOD_PREFIX_TO_APP_NAME = {
2988
+ evm: "Ethereum",
2989
+ btc: "Bitcoin",
2990
+ sol: "Solana",
2991
+ tron: "Tron"
2992
+ };
2993
+ var HARDWARE_ERROR_CODE_VALUES = new Set(
2994
+ Object.values(import_hwk_adapter_core12.HardwareErrorCode).filter((value) => typeof value === "number")
2995
+ );
2996
+ var BLE_CONNECT_SCAN_TIMEOUT_MS = 1500;
2220
2997
  async function defaultLedgerKitImporter(pkg) {
2221
2998
  switch (pkg) {
2222
2999
  case "@ledgerhq/device-management-kit":
@@ -2240,16 +3017,6 @@ var LedgerConnectorBase = class {
2240
3017
  this._dmk = null;
2241
3018
  this._eventHandlers = /* @__PURE__ */ new Map();
2242
3019
  // ---------------------------------------------------------------------------
2243
- // ConnectId <-> DMK path mapping
2244
- //
2245
- // DMK uses internal paths (BLE MAC, USB UUID) that may change across sessions.
2246
- // _resolveConnectId() maps these to stable external IDs (BLE: "A58F", USB: same).
2247
- // This bidirectional map is the SINGLE SOURCE OF TRUTH for all connectId usage.
2248
- // ---------------------------------------------------------------------------
2249
- this._connectIdToPath = /* @__PURE__ */ new Map();
2250
- // "A58F" -> "D5:75:7D:4B:51:E8"
2251
- this._pathToConnectId = /* @__PURE__ */ new Map();
2252
- // ---------------------------------------------------------------------------
2253
3020
  // Per-session DeviceAction cancellers
2254
3021
  //
2255
3022
  // Each chain handler registers its active DeviceAction's canceller via
@@ -2258,17 +3025,30 @@ var LedgerConnectorBase = class {
2258
3025
  // unsubscribes the observable and releases DMK's IntentQueue slot.
2259
3026
  // ---------------------------------------------------------------------------
2260
3027
  this._cancellers = /* @__PURE__ */ new Map();
3028
+ // ---------------------------------------------------------------------------
3029
+ // Per-session DMK state subscriptions
3030
+ //
3031
+ // DMK only emits `device-disconnect` on the connector when `disconnect()`
3032
+ // is called explicitly. To pick up autonomous disconnects (USB unplug,
3033
+ // BLE drop, device sleep, DMK transport reset) we subscribe to
3034
+ // `_dmk.getDeviceSessionState({sessionId})` per active session and emit
3035
+ // `device-disconnect` ourselves the moment DMK reports the device went
3036
+ // to NOT_CONNECTED. The subscription is torn down on disconnect /
3037
+ // reset / observable completion to avoid leaks.
3038
+ // ---------------------------------------------------------------------------
3039
+ this._sessionStateSubs = /* @__PURE__ */ new Map();
2261
3040
  this._createTransport = createTransport;
2262
3041
  this.connectionType = options?.connectionType ?? "usb";
2263
3042
  this._providedDmk = options?.dmk;
2264
3043
  this._importLedgerKit = options?.importLedgerKit ?? defaultLedgerKitImporter;
3044
+ this._requirePreFlightScan = options?.requirePreFlightScan ?? false;
2265
3045
  if (this._providedDmk) {
2266
3046
  this._initManagers(this._providedDmk);
2267
3047
  }
2268
3048
  this._ctx = {
2269
3049
  emit: (event, data) => this._emit(event, data),
2270
3050
  invalidateSession: (sid) => this._invalidateSession(sid),
2271
- wrapError: (err) => this._wrapError(err),
3051
+ wrapError: (err, opts) => this._wrapError(err, opts),
2272
3052
  getOrCreateDmk: () => this._getOrCreateDmk(),
2273
3053
  getDeviceManager: () => this._getDeviceManager(),
2274
3054
  getSignerManager: () => this._getSignerManager(),
@@ -2279,38 +3059,23 @@ var LedgerConnectorBase = class {
2279
3059
  importLedgerKit: this._importLedgerKit
2280
3060
  };
2281
3061
  }
2282
- // "D5:75:7D:4B:51:E8" -> "A58F"
2283
- /** Register a connectId <-> path mapping from a device descriptor. */
2284
- _registerDeviceId(descriptor) {
2285
- const connectId = this._resolveConnectId(descriptor);
2286
- this._connectIdToPath.set(connectId, descriptor.path);
2287
- this._pathToConnectId.set(descriptor.path, connectId);
2288
- return connectId;
2289
- }
2290
- /** Get DMK path from external connectId. Falls back to connectId itself. */
2291
- _getPathForConnectId(connectId) {
2292
- return this._connectIdToPath.get(connectId) ?? connectId;
2293
- }
2294
- /** Get external connectId from DMK path. Falls back to path itself. */
2295
- _getConnectIdForPath(path) {
2296
- return this._pathToConnectId.get(path) ?? path;
2297
- }
2298
3062
  // ---------------------------------------------------------------------------
2299
3063
  // Protected — hooks for subclasses
2300
3064
  // ---------------------------------------------------------------------------
2301
3065
  /**
2302
3066
  * Resolve the connectId for a discovered device descriptor.
2303
3067
  * Default: use the DMK path (ephemeral UUID).
2304
- * Override in subclasses to extract stable identifiers (e.g. BLE hex ID).
3068
+ * Override in subclasses only when the public connectId differs from the transport path.
2305
3069
  */
2306
3070
  _resolveConnectId(descriptor) {
2307
3071
  return descriptor.path;
2308
3072
  }
2309
- // ---------------------------------------------------------------------------
2310
- // IConnector -- Device discovery
2311
- // ---------------------------------------------------------------------------
2312
- async searchDevices() {
2313
- const dm = await this._getDeviceManager();
3073
+ /**
3074
+ * Authoritative discovery for this transport. Default = enumerate snapshot
3075
+ * (USB/HID). BLE overrides to drive from a live scan window since DMK's
3076
+ * enumerate() cache survives peripherals going offline.
3077
+ */
3078
+ async _discoverDescriptors(dm) {
2314
3079
  let descriptors = await dm.enumerate();
2315
3080
  if (descriptors.length === 0) {
2316
3081
  try {
@@ -2319,137 +3084,313 @@ var LedgerConnectorBase = class {
2319
3084
  }
2320
3085
  descriptors = await dm.enumerate();
2321
3086
  }
2322
- const result = descriptors.map((d) => {
2323
- const connectId = this._registerDeviceId(d);
2324
- return {
2325
- connectId,
2326
- deviceId: d.path,
2327
- name: d.name || d.type || "Ledger",
2328
- model: d.type
2329
- };
2330
- });
3087
+ return descriptors;
3088
+ }
3089
+ // ---------------------------------------------------------------------------
3090
+ // IConnector -- Device discovery
3091
+ // ---------------------------------------------------------------------------
3092
+ async searchDevices() {
3093
+ const dm = await this._getDeviceManager();
3094
+ const descriptors = await this._discoverDescriptors(dm);
3095
+ const resolvedDescriptors = descriptors.map((d) => ({
3096
+ descriptor: d,
3097
+ connectId: this._resolveConnectId(d)
3098
+ }));
3099
+ const result = resolvedDescriptors.map(({ descriptor: d, connectId }) => ({
3100
+ connectId,
3101
+ deviceId: d.path,
3102
+ name: d.name || d.type || "Ledger",
3103
+ model: d.type,
3104
+ modelName: d.modelName,
3105
+ rssi: d.rssi,
3106
+ isConnectable: d.isConnectable,
3107
+ serialNumber: d.serialNumber
3108
+ }));
3109
+ debugLog(
3110
+ `[DMK] connector.searchDevices() return count=${result.length} ids=[${result.map((r) => r.connectId).join(",")}] paths=[${result.map((r) => r.deviceId).join(",")}]`
3111
+ );
2331
3112
  return result;
2332
3113
  }
2333
3114
  // ---------------------------------------------------------------------------
2334
3115
  // IConnector -- Connection
2335
3116
  // ---------------------------------------------------------------------------
2336
3117
  async connect(deviceId) {
2337
- const dm = await this._getDeviceManager();
2338
- await this.searchDevices();
2339
- const dmkPath = deviceId ? this._getPathForConnectId(deviceId) : void 0;
2340
- let targetPath = dmkPath;
3118
+ const callerSuppliedConnectId = Boolean(deviceId);
3119
+ let targetPath = deviceId;
2341
3120
  if (!targetPath) {
2342
- const descriptors = await dm.enumerate();
2343
- if (descriptors.length === 0) {
3121
+ const discovered = await this.searchDevices();
3122
+ if (discovered.length === 0) {
2344
3123
  throw new Error(
2345
- `No Ledger device found. Make sure the device is connected${this.connectionType === "ble" ? " nearby with Bluetooth enabled" : " via USB"} and unlocked.`
3124
+ `No Ledger device found. Make sure the device is connected${isLedgerBleConnectionType(this.connectionType) ? " nearby with Bluetooth enabled" : " via USB"} and unlocked.`
2346
3125
  );
2347
3126
  }
2348
- targetPath = descriptors[0].path;
3127
+ targetPath = discovered[0].deviceId;
2349
3128
  }
2350
- const externalConnectId = this._getConnectIdForPath(targetPath);
3129
+ const externalConnectId = targetPath;
3130
+ const HANG_CEILING_MS = 5 * 6e4;
3131
+ const dmConnectWithObserve = async (dm, path) => {
3132
+ if (!isLedgerBleConnectionType(this.connectionType)) return dm.connect(path);
3133
+ let timeoutId;
3134
+ let timedOut = false;
3135
+ const connectPromise = dm.connect(path);
3136
+ const hangPromise = new Promise((_, reject) => {
3137
+ timeoutId = setTimeout(() => {
3138
+ timedOut = true;
3139
+ const err = new Error(
3140
+ `Ledger BLE connect did not return within ${HANG_CEILING_MS / 6e4}min \u2014 DMK hang fallback.`
3141
+ );
3142
+ err._tag = ERROR_TAG.BlePairingTimeout;
3143
+ err.code = import_hwk_adapter_core12.HardwareErrorCode.BlePairingTimeout;
3144
+ reject(err);
3145
+ }, HANG_CEILING_MS);
3146
+ });
3147
+ try {
3148
+ return await Promise.race([connectPromise, hangPromise]);
3149
+ } catch (err) {
3150
+ const e = err;
3151
+ debugLog("[DMK] dm.connect rejected \u2014 observed:", {
3152
+ _tag: e?._tag,
3153
+ message: e?.message,
3154
+ errorCode: e?.errorCode,
3155
+ statusCode: e?.statusCode,
3156
+ originalTag: e?.originalError?._tag,
3157
+ originalMessage: e?.originalError?.message
3158
+ });
3159
+ if (timedOut) {
3160
+ void connectPromise.then(
3161
+ (sessionId) => dm.disconnect(sessionId).catch(() => void 0),
3162
+ () => void 0
3163
+ );
3164
+ }
3165
+ throw err;
3166
+ } finally {
3167
+ if (timeoutId) clearTimeout(timeoutId);
3168
+ }
3169
+ };
3170
+ const isBleDirectConnect = isLedgerBleConnectionType(this.connectionType) && callerSuppliedConnectId;
3171
+ const throwNotAdvertising = () => {
3172
+ const err = new Error(
3173
+ "Ledger device is not currently advertising. Wake up and unlock the device, keep it nearby, then try again."
3174
+ );
3175
+ err._tag = ERROR_TAG.DeviceNotAdvertising;
3176
+ err.code = import_hwk_adapter_core12.HardwareErrorCode.DeviceNotFound;
3177
+ throw err;
3178
+ };
2351
3179
  const doConnect = async (path) => {
2352
- const sessionId = await dm.connect(path);
3180
+ const dm = await this._getDeviceManager();
3181
+ if (isBleDirectConnect && !dm.hasDiscoveredDevice(path)) {
3182
+ const live = await dm.getLiveDevices(BLE_CONNECT_SCAN_TIMEOUT_MS);
3183
+ if (this._requirePreFlightScan && !live.some((d) => d.id === path)) {
3184
+ throwNotAdvertising();
3185
+ }
3186
+ }
3187
+ let sessionId;
3188
+ try {
3189
+ sessionId = await dmConnectWithObserve(dm, path);
3190
+ } catch (err) {
3191
+ if (isBleDirectConnect && err?._tag === ERROR_TAG.DeviceNotInDiscoveryCache) {
3192
+ throwNotAdvertising();
3193
+ }
3194
+ throw err;
3195
+ }
3196
+ this._watchSessionState(sessionId, externalConnectId);
3197
+ const info = dm.getDiscoveredDeviceInfo(path);
2353
3198
  const session = {
2354
3199
  sessionId,
2355
3200
  deviceInfo: {
2356
3201
  vendor: "ledger",
2357
- model: "unknown",
3202
+ model: info?.model ?? "unknown",
3203
+ modelName: info?.modelName,
2358
3204
  firmwareVersion: "unknown",
2359
3205
  deviceId: path,
2360
3206
  connectId: externalConnectId,
2361
3207
  connectionType: this.connectionType,
3208
+ rssi: info?.rssi,
2362
3209
  capabilities: { persistentDeviceIdentity: false }
2363
3210
  }
2364
3211
  };
3212
+ const name = dm.getDeviceName(path) ?? "Ledger";
2365
3213
  this._emit("device-connect", {
2366
- device: {
2367
- connectId: externalConnectId,
2368
- deviceId: path,
2369
- name: "Ledger"
2370
- }
3214
+ device: { connectId: externalConnectId, deviceId: path, name }
2371
3215
  });
2372
3216
  return session;
2373
3217
  };
2374
3218
  try {
2375
3219
  return await doConnect(targetPath);
2376
- } catch {
3220
+ } catch (err) {
2377
3221
  this._resetSignersAndSessions();
2378
- const dm2 = await this._getDeviceManager();
2379
- await this.searchDevices();
2380
- const retryPath = this._getPathForConnectId(externalConnectId);
2381
- if (!retryPath || retryPath === externalConnectId) {
2382
- const descriptors = await dm2.enumerate();
2383
- if (descriptors.length === 0) {
2384
- throw new Error(
2385
- `No Ledger device found after retry. Make sure the device is connected${this.connectionType === "ble" ? " nearby with Bluetooth enabled" : " via USB"} and unlocked.`
2386
- );
3222
+ if (isLedgerBleConnectionType(this.connectionType)) {
3223
+ const tag = err?._tag;
3224
+ if (isKnownConnectionTag(tag)) {
3225
+ throw err;
2387
3226
  }
2388
- return doConnect(descriptors[0].path);
3227
+ const wrapped = new Error(
3228
+ "Ledger Bluetooth pairing failed. Make sure the device is unlocked and nearby, then try again."
3229
+ );
3230
+ wrapped._tag = ERROR_TAG.BleGattBondingFailed;
3231
+ wrapped.code = import_hwk_adapter_core12.HardwareErrorCode.BlePairingTimeout;
3232
+ wrapped.originalError = err;
3233
+ throw wrapped;
3234
+ }
3235
+ if (err && typeof err === "object" && err._tag) {
3236
+ const taggedError = err instanceof Error ? err : Object.assign(
3237
+ new Error(err.message ?? "Ledger device error"),
3238
+ err
3239
+ );
3240
+ throw taggedError;
2389
3241
  }
2390
- return doConnect(retryPath);
3242
+ throw new Error("Ledger device appears unplugged. Plug in the device and try again.");
2391
3243
  }
2392
3244
  }
2393
3245
  async disconnect(sessionId) {
2394
3246
  if (!this._deviceManager) return;
2395
3247
  const deviceId = this._deviceManager.getDeviceId(sessionId);
2396
3248
  this._signerManager?.invalidate(sessionId);
3249
+ this._unwatchSessionState(sessionId);
2397
3250
  await this._deviceManager.disconnect(sessionId);
2398
3251
  if (deviceId) {
2399
3252
  this._emit("device-disconnect", { connectId: deviceId });
2400
3253
  }
2401
3254
  }
3255
+ /**
3256
+ * Subscribe to DMK's per-session state observable so that any autonomous
3257
+ * disconnect (USB unplug, BLE drop, device sleep, transport reset) is
3258
+ * surfaced as a `device-disconnect` event — without this, the upstream
3259
+ * `LedgerAdapter._sessions` map would hold a dead session entry until
3260
+ * the next call hit `DeviceSessionNotFound`.
3261
+ *
3262
+ * Best-effort: any error subscribing is swallowed so a flaky DMK
3263
+ * doesn't break the connect path.
3264
+ */
3265
+ _watchSessionState(sessionId, externalConnectId) {
3266
+ const dmk = this._dmk;
3267
+ if (!dmk) return;
3268
+ const previous = this._sessionStateSubs.get(sessionId);
3269
+ if (previous) {
3270
+ try {
3271
+ previous.unsubscribe();
3272
+ } catch {
3273
+ }
3274
+ }
3275
+ try {
3276
+ const sub = dmk.getDeviceSessionState({ sessionId }).subscribe({
3277
+ next: (state) => {
3278
+ if (state?.deviceStatus === "NOT CONNECTED") {
3279
+ this._handleAutonomousDisconnect(sessionId, externalConnectId);
3280
+ }
3281
+ },
3282
+ error: () => {
3283
+ this._handleAutonomousDisconnect(sessionId, externalConnectId);
3284
+ },
3285
+ complete: () => {
3286
+ this._handleAutonomousDisconnect(sessionId, externalConnectId);
3287
+ }
3288
+ });
3289
+ this._sessionStateSubs.set(sessionId, sub);
3290
+ } catch (err) {
3291
+ debugLog("[DMK] _watchSessionState subscribe failed:", err);
3292
+ }
3293
+ }
3294
+ _unwatchSessionState(sessionId) {
3295
+ const sub = this._sessionStateSubs.get(sessionId);
3296
+ if (!sub) return;
3297
+ try {
3298
+ sub.unsubscribe();
3299
+ } catch {
3300
+ }
3301
+ this._sessionStateSubs.delete(sessionId);
3302
+ }
3303
+ _handleAutonomousDisconnect(sessionId, externalConnectId) {
3304
+ if (!this._sessionStateSubs.has(sessionId)) return;
3305
+ debugLog(
3306
+ "[DMK] autonomous disconnect detected \u2014 sessionId:",
3307
+ sessionId,
3308
+ "connectId:",
3309
+ externalConnectId
3310
+ );
3311
+ this._unwatchSessionState(sessionId);
3312
+ this._signerManager?.invalidate(sessionId);
3313
+ this._cancellers.get(sessionId)?.({
3314
+ code: import_hwk_adapter_core12.HardwareErrorCode.DeviceDisconnected,
3315
+ tag: "DeviceDisconnected",
3316
+ message: "Device disconnected"
3317
+ });
3318
+ this._cancellers.delete(sessionId);
3319
+ this._emit("device-disconnect", { connectId: externalConnectId });
3320
+ }
2402
3321
  // ---------------------------------------------------------------------------
2403
3322
  // IConnector -- Method dispatch
2404
3323
  // ---------------------------------------------------------------------------
2405
3324
  async call(sessionId, method, params) {
2406
3325
  debugLog("[DMK] call:", method, JSON.stringify(params));
3326
+ try {
3327
+ return await this._dispatch(sessionId, method, params);
3328
+ } catch (err) {
3329
+ if (isAppStuckByApdu(err)) {
3330
+ throw Object.assign(new Error("Ledger app is unresponsive"), {
3331
+ code: import_hwk_adapter_core12.HardwareErrorCode.DeviceAppStuck,
3332
+ _tag: ERROR_TAG.DeviceAppStuck,
3333
+ originalError: err
3334
+ });
3335
+ }
3336
+ if (isTransportStuck(err)) {
3337
+ throw Object.assign(new Error("Device communication interrupted, please retry"), {
3338
+ code: import_hwk_adapter_core12.HardwareErrorCode.TransportError,
3339
+ _tag: ERROR_TAG.DeviceTransportStuck,
3340
+ originalError: err
3341
+ });
3342
+ }
3343
+ throw err;
3344
+ }
3345
+ }
3346
+ async _dispatch(sessionId, method, params) {
3347
+ const ctx = this._ctxForMethod(method);
2407
3348
  switch (method) {
2408
3349
  // EVM
2409
3350
  case "evmGetAddress":
2410
- return evmGetAddress(this._ctx, sessionId, params);
3351
+ return evmGetAddress(ctx, sessionId, params);
2411
3352
  case "evmSignTransaction":
2412
- return evmSignTransaction(this._ctx, sessionId, params);
3353
+ return evmSignTransaction(ctx, sessionId, params);
2413
3354
  case "evmSignMessage":
2414
- return evmSignMessage(this._ctx, sessionId, params);
3355
+ return evmSignMessage(ctx, sessionId, params);
2415
3356
  case "evmSignTypedData":
2416
- return evmSignTypedData(this._ctx, sessionId, params);
3357
+ return evmSignTypedData(ctx, sessionId, params);
2417
3358
  // BTC
2418
3359
  case "btcGetAddress":
2419
- return btcGetAddress(this._ctx, sessionId, params);
3360
+ return btcGetAddress(ctx, sessionId, params);
2420
3361
  case "btcGetPublicKey":
2421
- return btcGetPublicKey(this._ctx, sessionId, params);
3362
+ return btcGetPublicKey(ctx, sessionId, params);
2422
3363
  case "btcSignTransaction":
2423
- return btcSignTransaction(this._ctx, sessionId, params);
3364
+ return btcSignTransaction(ctx, sessionId, params);
2424
3365
  case "btcSignPsbt":
2425
- return btcSignPsbt(this._ctx, sessionId, params);
3366
+ return btcSignPsbt(ctx, sessionId, params);
2426
3367
  case "btcSignMessage":
2427
- return btcSignMessage(this._ctx, sessionId, params);
3368
+ return btcSignMessage(ctx, sessionId, params);
2428
3369
  case "btcGetMasterFingerprint":
2429
3370
  return btcGetMasterFingerprint(
2430
- this._ctx,
3371
+ ctx,
2431
3372
  sessionId,
2432
3373
  params
2433
3374
  );
2434
3375
  // SOL
2435
3376
  case "solGetAddress":
2436
- return solGetAddress(this._ctx, sessionId, params);
3377
+ return solGetAddress(ctx, sessionId, params);
2437
3378
  case "solSignTransaction":
2438
- return solSignTransaction(this._ctx, sessionId, params);
3379
+ return solSignTransaction(ctx, sessionId, params);
2439
3380
  case "solSignMessage":
2440
- return solSignMessage(this._ctx, sessionId, params);
3381
+ return solSignMessage(ctx, sessionId, params);
2441
3382
  // TRON
2442
3383
  case "tronGetAddress":
2443
- return tronGetAddress(this._ctx, sessionId, params);
3384
+ return tronGetAddress(ctx, sessionId, params);
2444
3385
  case "tronSignTransaction":
2445
- return tronSignTransaction(this._ctx, sessionId, params);
3386
+ return tronSignTransaction(ctx, sessionId, params);
2446
3387
  case "tronSignMessage": {
2447
3388
  const p = params;
2448
3389
  const internalParams = {
2449
3390
  path: p.path,
2450
3391
  messageHex: p.messageHex
2451
3392
  };
2452
- return tronSignMessage(this._ctx, sessionId, internalParams);
3393
+ return tronSignMessage(ctx, sessionId, internalParams);
2453
3394
  }
2454
3395
  default:
2455
3396
  throw new Error(`LedgerConnector: unknown method "${method}"`);
@@ -2542,14 +3483,13 @@ var LedgerConnectorBase = class {
2542
3483
  }
2543
3484
  /**
2544
3485
  * Replace an old session with a new one after app switch.
2545
- * Updates the connectId→path mapping so subsequent calls() use the new session,
2546
- * and emits device-connect so the adapter updates its _sessions Map.
3486
+ * Emits device-connect so the adapter updates its _sessions Map.
2547
3487
  */
2548
3488
  _replaceSession(oldSessionId, _newSessionId) {
2549
3489
  const dm = this._deviceManager;
2550
3490
  if (!dm) return;
2551
3491
  const oldDeviceId = dm.getDeviceId(oldSessionId);
2552
- const connectId = oldDeviceId ? this._pathToConnectId.get(oldDeviceId) : void 0;
3492
+ const connectId = oldDeviceId;
2553
3493
  this._signerManager?.invalidate(oldSessionId);
2554
3494
  if (connectId) {
2555
3495
  this._emit("device-connect", {
@@ -2562,13 +3502,18 @@ var LedgerConnectorBase = class {
2562
3502
  }
2563
3503
  }
2564
3504
  /**
2565
- * Light reset: clear signer/session state but keep DMK, BLE scan, and ID mapping alive.
3505
+ * Light reset: clear signer/session state but keep DMK alive.
2566
3506
  * Used by connect() retry — we want to re-discover with the same transport.
3507
+ *
3508
+ * Note: drops the device manager but tears down its RxJS subs first via
3509
+ * disposeKeepingDmk(). Bare null-assignment would leak _listenSub /
3510
+ * _discoverySub and double-scan after the next _initManagers().
2567
3511
  */
2568
3512
  _resetSignersAndSessions() {
2569
3513
  debugLog("[DMK] _resetSignersAndSessions called");
2570
3514
  this._signerManager?.clearAll();
2571
3515
  this._signerManager = null;
3516
+ this._deviceManager?.disposeKeepingDmk();
2572
3517
  this._deviceManager = null;
2573
3518
  }
2574
3519
  _resetAll() {
@@ -2580,13 +3525,18 @@ var LedgerConnectorBase = class {
2580
3525
  }
2581
3526
  }
2582
3527
  this._cancellers.clear();
3528
+ for (const sub of this._sessionStateSubs.values()) {
3529
+ try {
3530
+ sub.unsubscribe();
3531
+ } catch {
3532
+ }
3533
+ }
3534
+ this._sessionStateSubs.clear();
2583
3535
  this._signerManager?.clearAll();
2584
3536
  this._deviceManager?.dispose();
2585
3537
  this._deviceManager = null;
2586
3538
  this._signerManager = null;
2587
3539
  this._dmk = null;
2588
- this._connectIdToPath.clear();
2589
- this._pathToConnectId.clear();
2590
3540
  }
2591
3541
  // ---------------------------------------------------------------------------
2592
3542
  // Private -- Events
@@ -2605,16 +3555,36 @@ var LedgerConnectorBase = class {
2605
3555
  // ---------------------------------------------------------------------------
2606
3556
  // Private -- Error handling
2607
3557
  // ---------------------------------------------------------------------------
2608
- _wrapError(err) {
2609
- const mapped = mapLedgerError(err);
2610
- const error = new Error(mapped.message);
3558
+ /**
3559
+ * Return a per-call ctx with the chain's Ledger app name pre-bound to
3560
+ * wrapError, so chain handlers don't need to repeat `{ defaultAppName: 'X' }`
3561
+ * at every catch site. Falls through unchanged for unknown methods.
3562
+ */
3563
+ _ctxForMethod(method) {
3564
+ const prefix = /^(evm|btc|sol|tron)/.exec(method)?.[1];
3565
+ const defaultAppName = prefix ? METHOD_PREFIX_TO_APP_NAME[prefix] : void 0;
3566
+ if (!defaultAppName) return this._ctx;
3567
+ return {
3568
+ ...this._ctx,
3569
+ wrapError: (err, opts) => this._wrapError(err, { defaultAppName, ...opts })
3570
+ };
3571
+ }
3572
+ _wrapError(err, opts) {
2611
3573
  const src = err && typeof err === "object" ? err : {};
3574
+ const hasSerializedCode = typeof src.code === "number" && HARDWARE_ERROR_CODE_VALUES.has(src.code);
3575
+ const mapped = hasSerializedCode ? {
3576
+ code: src.code,
3577
+ message: typeof src.message === "string" ? src.message : "Unknown Ledger error",
3578
+ appName: typeof src.appName === "string" ? src.appName : opts?.defaultAppName
3579
+ } : mapLedgerError(err, opts);
3580
+ const error = new Error(mapped.message);
2612
3581
  Object.assign(error, {
2613
3582
  code: mapped.code,
2614
3583
  appName: mapped.appName,
2615
3584
  _tag: src._tag,
2616
3585
  errorCode: src.errorCode,
2617
- _lastStep: src._lastStep
3586
+ _lastStep: src._lastStep,
3587
+ _deviceActionSteps: src._deviceActionSteps
2618
3588
  });
2619
3589
  Object.defineProperty(error, "originalError", {
2620
3590
  value: err,
@@ -2625,13 +3595,6 @@ var LedgerConnectorBase = class {
2625
3595
  return error;
2626
3596
  }
2627
3597
  };
2628
-
2629
- // src/utils/bleIdentity.ts
2630
- function extractBleHexId(name) {
2631
- if (!name) return void 0;
2632
- const match = name.match(/\b([0-9A-Fa-f]{4})$/);
2633
- return match ? match[1].toUpperCase() : void 0;
2634
- }
2635
3598
  // Annotate the CommonJS export names for ESM import in node:
2636
3599
  0 && (module.exports = {
2637
3600
  AppManager,
@@ -2643,12 +3606,15 @@ function extractBleHexId(name) {
2643
3606
  SignerEth,
2644
3607
  SignerManager,
2645
3608
  SignerSol,
3609
+ debugLog,
2646
3610
  deviceActionToPromise,
2647
- extractBleHexId,
2648
- isDebugEnabled,
2649
3611
  isDeviceLockedError,
3612
+ isLedgerBleConnectionType,
3613
+ isLedgerBleDescriptor,
3614
+ isLedgerDmkBleTransport,
2650
3615
  ledgerFailure,
2651
3616
  mapLedgerError,
2652
- setDebugEnabled
3617
+ offSdkEvent,
3618
+ onSdkEvent
2653
3619
  });
2654
3620
  //# sourceMappingURL=index.js.map