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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -3,9 +3,11 @@ import {
3
3
  CHAIN_FINGERPRINT_PATHS,
4
4
  DEVICE,
5
5
  DeviceJobQueue,
6
+ EConnectorInteraction,
6
7
  HardwareErrorCode as HardwareErrorCode2,
7
8
  TypedEventEmitter,
8
9
  UI_REQUEST,
10
+ UI_REQUEST_PREEMPTED_TAG,
9
11
  UiRequestRegistry,
10
12
  deriveDeviceFingerprint,
11
13
  failure,
@@ -14,24 +16,32 @@ import {
14
16
 
15
17
  // src/errors.ts
16
18
  import { HardwareErrorCode, enrichErrorMessage } from "@onekeyfe/hwk-adapter-core";
17
- function ledgerFailure(code, error, appName) {
19
+ var MULTIPLE_USB_LEDGER_DEVICES_ERROR_MESSAGE = "Multiple Ledger USB devices are connected. Please connect only one Ledger device and try again.";
20
+ function createMultipleUsbLedgerDevicesError() {
21
+ return Object.assign(new Error(MULTIPLE_USB_LEDGER_DEVICES_ERROR_MESSAGE), {
22
+ code: HardwareErrorCode.DeviceOneDeviceOnly
23
+ });
24
+ }
25
+ function ledgerFailure(code, error, appName, tag, params) {
18
26
  const payload = { error, code };
19
27
  if (appName !== void 0) payload.appName = appName;
28
+ if (tag !== void 0) payload._tag = tag;
29
+ if (params !== void 0) payload.params = params;
20
30
  return { success: false, payload };
21
31
  }
22
32
  var LOCKED_ERROR_CODES = /* @__PURE__ */ new Set(["5515", "21781", "6982", "27010", "5303", "21251"]);
23
33
  var USER_REJECTED_CODES = /* @__PURE__ */ new Set(["6985", "27013"]);
24
34
  var WRONG_APP_CODES = /* @__PURE__ */ new Set(["6e00", "28160", "6d00", "27904", "6a83", "27267"]);
25
35
  var APP_NOT_INSTALLED_CODES = /* @__PURE__ */ new Set(["6807", "26631"]);
26
- var STEP_BLIND_SIGN_FALLBACK = "signer.eth.steps.blindSignTransactionFallback";
36
+ var STEP_BLIND_SIGN_TRANSACTION_FALLBACK = "signer.eth.steps.blindSignTransactionFallback";
27
37
  function getEthAppErrorCode(err) {
28
38
  if (!err || typeof err !== "object") return null;
29
39
  const e = err;
30
- if (e._tag === "EthAppCommandError" && typeof e.errorCode === "string") {
40
+ if (e._tag === ERROR_TAG.EthAppCommand && typeof e.errorCode === "string") {
31
41
  return e.errorCode.toLowerCase();
32
42
  }
33
43
  const orig = e.originalError;
34
- if (orig?._tag === "EthAppCommandError" && typeof orig.errorCode === "string") {
44
+ if (orig?._tag === ERROR_TAG.EthAppCommand && typeof orig.errorCode === "string") {
35
45
  return orig.errorCode.toLowerCase();
36
46
  }
37
47
  return null;
@@ -88,13 +98,8 @@ function mapBtcAppError(hex) {
88
98
  return null;
89
99
  }
90
100
  }
91
- function mapEthAppError(ethCode, lastStep) {
101
+ function mapEthAppErrorCode(ethCode) {
92
102
  switch (ethCode) {
93
- case "6a80":
94
- if (lastStep === STEP_BLIND_SIGN_FALLBACK) {
95
- return HardwareErrorCode.EvmBlindSigningRequired;
96
- }
97
- return null;
98
103
  case "6984":
99
104
  return HardwareErrorCode.EvmClearSignPluginMissing;
100
105
  case "6a84":
@@ -107,16 +112,133 @@ function mapEthAppError(ethCode, lastStep) {
107
112
  return null;
108
113
  }
109
114
  }
115
+ function classifyEthAppError(err) {
116
+ const ethCode = getEthAppErrorCode(err);
117
+ if (!ethCode) return null;
118
+ if (ethCode === "6a80") {
119
+ return hasBlindSignFallbackStep(err) ? HardwareErrorCode.EvmBlindSigningRequired : null;
120
+ }
121
+ return mapEthAppErrorCode(ethCode);
122
+ }
123
+ var ERROR_TAG = {
124
+ // SDK-mint
125
+ DeviceNotAdvertising: "DeviceNotAdvertisingError",
126
+ // BLE scan miss
127
+ DeviceNotInDiscoveryCache: "DeviceNotInDiscoveryCacheError",
128
+ // dm.connect() before enumerate
129
+ BlePairingTimeout: "BlePairingTimeoutError",
130
+ // SMP 30s timeout
131
+ BleGattBondingFailed: "BleGattBondingFailedError",
132
+ // other GATT failure
133
+ UserAborted: "UserAborted",
134
+ DeviceAppStuck: "DeviceAppStuck",
135
+ // chain app wedged (APDU 0x6901)
136
+ DeviceTransportStuck: "DeviceTransportStuck",
137
+ // DMK transport queue wedged
138
+ // DMK-reuse (DMK throws same string; we synthesize too)
139
+ DeviceLocked: "DeviceLockedError",
140
+ DeviceNotRecognized: "DeviceNotRecognizedError",
141
+ DeviceSessionNotFound: "DeviceSessionNotFound",
142
+ OpenAppCommand: "OpenAppCommandError",
143
+ // DMK-only (read only)
144
+ EthAppCommand: "EthAppCommandError",
145
+ UserRefusedOnDevice: "UserRefusedOnDevice",
146
+ WrongAppOpened: "WrongAppOpenedError",
147
+ InvalidStatusWord: "InvalidStatusWordError",
148
+ AlreadySendingApdu: "AlreadySendingApduError",
149
+ UnknownDeviceExchange: "UnknownDeviceExchangeError",
150
+ NoAccessibleDevice: "NoAccessibleDeviceError",
151
+ UnknownDevice: "UnknownDeviceError",
152
+ DeviceSessionRefresher: "DeviceSessionRefresherError",
153
+ DeviceNotInitialized: "DeviceNotInitializedError",
154
+ OpeningConnection: "OpeningConnectionError",
155
+ DeviceDisconnectedBeforeSendingApdu: "DeviceDisconnectedBeforeSendingApdu",
156
+ DeviceDisconnectedWhileSending: "DeviceDisconnectedWhileSendingError",
157
+ Disconnect: "DisconnectError",
158
+ ReconnectionFailed: "ReconnectionFailedError",
159
+ WebHIDDisconnect: "WebHIDDisconnectError",
160
+ // ble-plx surfaces this when GATT notification setup fails — typical
161
+ // outcome when the user doesn't confirm pairing on the device, or the
162
+ // existing bond is invalid. Observed in production after ~30s.
163
+ PairingRefused: "PairingRefusedError"
164
+ };
110
165
  function isDeviceLockedError(err) {
111
166
  if (!err || typeof err !== "object") return false;
112
167
  const e = err;
113
168
  if (e.errorCode != null && LOCKED_ERROR_CODES.has(String(e.errorCode))) return true;
114
169
  if (e.statusCode != null && LOCKED_ERROR_CODES.has(String(e.statusCode))) return true;
115
- if (e._tag === "DeviceLockedError") return true;
170
+ if (e._tag === ERROR_TAG.DeviceLocked) return true;
116
171
  if (e.originalError != null && isDeviceLockedError(e.originalError)) return true;
117
172
  if (e.error != null && e._tag && isDeviceLockedError(e.error)) return true;
118
173
  return false;
119
174
  }
175
+ function isDeviceNotAdvertisingError(err) {
176
+ if (!err || typeof err !== "object") return false;
177
+ return err._tag === ERROR_TAG.DeviceNotAdvertising;
178
+ }
179
+ var PAIRING_FAILURE_TAGS = /* @__PURE__ */ new Set([
180
+ ERROR_TAG.BlePairingTimeout,
181
+ ERROR_TAG.BleGattBondingFailed,
182
+ ERROR_TAG.PairingRefused
183
+ ]);
184
+ function isBlePairingFailureError(err) {
185
+ if (!err || typeof err !== "object") return false;
186
+ const tag = err._tag;
187
+ if (tag && PAIRING_FAILURE_TAGS.has(tag)) return true;
188
+ const orig = err.originalError;
189
+ if (orig != null && isBlePairingFailureError(orig)) return true;
190
+ return false;
191
+ }
192
+ var CONNECTION_LEVEL_TAGS = /* @__PURE__ */ new Set([
193
+ ERROR_TAG.DeviceLocked,
194
+ ERROR_TAG.DeviceNotAdvertising,
195
+ ERROR_TAG.BlePairingTimeout,
196
+ ERROR_TAG.BleGattBondingFailed,
197
+ ERROR_TAG.PairingRefused,
198
+ ERROR_TAG.DeviceNotRecognized,
199
+ ERROR_TAG.NoAccessibleDevice,
200
+ ERROR_TAG.UnknownDevice,
201
+ ERROR_TAG.DeviceSessionNotFound,
202
+ ERROR_TAG.DeviceSessionRefresher,
203
+ ERROR_TAG.DeviceNotInitialized,
204
+ ERROR_TAG.OpeningConnection,
205
+ ERROR_TAG.DeviceDisconnectedBeforeSendingApdu,
206
+ ERROR_TAG.DeviceDisconnectedWhileSending,
207
+ ERROR_TAG.Disconnect,
208
+ ERROR_TAG.ReconnectionFailed,
209
+ ERROR_TAG.WebHIDDisconnect
210
+ ]);
211
+ var DEVICE_NOT_FOUND_TAGS = /* @__PURE__ */ new Set([
212
+ ERROR_TAG.NoAccessibleDevice,
213
+ ERROR_TAG.UnknownDevice,
214
+ ERROR_TAG.DeviceNotInitialized,
215
+ // SDK-internal: dm.connect() called before _discovered was populated.
216
+ // Map to DeviceNotFound so non-BLE-direct paths get a sensible error code.
217
+ ERROR_TAG.DeviceNotInDiscoveryCache
218
+ ]);
219
+ var DEVICE_BUSY_TAGS = /* @__PURE__ */ new Set([ERROR_TAG.OpeningConnection]);
220
+ var DEVICE_DISCONNECTED_TAGS = /* @__PURE__ */ new Set([
221
+ ERROR_TAG.DeviceNotRecognized,
222
+ ERROR_TAG.DeviceSessionNotFound,
223
+ ERROR_TAG.DeviceSessionRefresher,
224
+ ERROR_TAG.DeviceDisconnectedBeforeSendingApdu,
225
+ ERROR_TAG.DeviceDisconnectedWhileSending,
226
+ ERROR_TAG.Disconnect,
227
+ ERROR_TAG.ReconnectionFailed,
228
+ ERROR_TAG.WebHIDDisconnect
229
+ ]);
230
+ function isConnectionLevelError(err) {
231
+ if (!err || typeof err !== "object") return false;
232
+ const tag = err._tag;
233
+ if (tag && CONNECTION_LEVEL_TAGS.has(tag)) return true;
234
+ const e = err;
235
+ if (e.originalError != null && isConnectionLevelError(e.originalError)) return true;
236
+ if (e.error != null && e._tag && isConnectionLevelError(e.error)) return true;
237
+ return false;
238
+ }
239
+ function isKnownConnectionTag(tag) {
240
+ return typeof tag === "string" && CONNECTION_LEVEL_TAGS.has(tag);
241
+ }
120
242
  function hasStatusCode(err, codeSet) {
121
243
  if (!err || typeof err !== "object") return false;
122
244
  const e = err;
@@ -126,10 +248,39 @@ function hasStatusCode(err, codeSet) {
126
248
  if (e.error != null && e._tag && hasStatusCode(e.error, codeSet)) return true;
127
249
  return false;
128
250
  }
251
+ function hasBlindSignFallbackStep(err) {
252
+ if (!err || typeof err !== "object") return false;
253
+ const e = err;
254
+ if (e._lastStep === STEP_BLIND_SIGN_TRANSACTION_FALLBACK) return true;
255
+ if (Array.isArray(e._deviceActionSteps) && e._deviceActionSteps.includes(STEP_BLIND_SIGN_TRANSACTION_FALLBACK)) {
256
+ return true;
257
+ }
258
+ if (e.originalError != null && hasBlindSignFallbackStep(e.originalError)) return true;
259
+ if (e.error != null && e._tag && hasBlindSignFallbackStep(e.error)) return true;
260
+ return false;
261
+ }
262
+ function isDeviceNotFoundError(err) {
263
+ if (!err || typeof err !== "object") return false;
264
+ const tag = err._tag;
265
+ if (tag && DEVICE_NOT_FOUND_TAGS.has(tag)) return true;
266
+ const e = err;
267
+ if (e.originalError != null && isDeviceNotFoundError(e.originalError)) return true;
268
+ if (e.error != null && e._tag && isDeviceNotFoundError(e.error)) return true;
269
+ return false;
270
+ }
271
+ function isDeviceBusyError(err) {
272
+ if (!err || typeof err !== "object") return false;
273
+ const tag = err._tag;
274
+ if (tag && DEVICE_BUSY_TAGS.has(tag)) return true;
275
+ const e = err;
276
+ if (e.originalError != null && isDeviceBusyError(e.originalError)) return true;
277
+ if (e.error != null && e._tag && isDeviceBusyError(e.error)) return true;
278
+ return false;
279
+ }
129
280
  function isUserRejectedError(err) {
130
281
  if (!err || typeof err !== "object") return false;
131
282
  const e = err;
132
- if (e._tag === "UserRefusedOnDevice") return true;
283
+ if (e._tag === ERROR_TAG.UserRefusedOnDevice) return true;
133
284
  if (typeof e.message === "string" && /denied|rejected|refused/i.test(e.message)) return true;
134
285
  if (hasStatusCode(err, USER_REJECTED_CODES)) return true;
135
286
  return false;
@@ -137,7 +288,7 @@ function isUserRejectedError(err) {
137
288
  function isWrongAppError(err) {
138
289
  if (!err || typeof err !== "object") return false;
139
290
  const e = err;
140
- if (e._tag === "WrongAppOpenedError" || e._tag === "InvalidStatusWordError") {
291
+ if (e._tag === ERROR_TAG.WrongAppOpened || e._tag === ERROR_TAG.InvalidStatusWord) {
141
292
  if (hasStatusCode(err, WRONG_APP_CODES)) return true;
142
293
  }
143
294
  if (typeof e.message === "string") {
@@ -151,7 +302,6 @@ function isWrongAppError(err) {
151
302
  function isAppNotInstalledError(err) {
152
303
  if (!err || typeof err !== "object") return false;
153
304
  const e = err;
154
- if (e._tag === "OpenAppCommandError") return true;
155
305
  if (typeof e.message === "string" && /unknown application/i.test(e.message)) return true;
156
306
  if (hasStatusCode(err, APP_NOT_INSTALLED_CODES)) return true;
157
307
  return false;
@@ -159,7 +309,8 @@ function isAppNotInstalledError(err) {
159
309
  function isDeviceDisconnectedError(err) {
160
310
  if (!err || typeof err !== "object") return false;
161
311
  const e = err;
162
- if (e._tag === "DeviceNotRecognizedError" || e._tag === "DeviceSessionNotFound") return true;
312
+ const tag = e._tag;
313
+ if (typeof tag === "string" && DEVICE_DISCONNECTED_TAGS.has(tag)) return true;
163
314
  if (typeof e.message === "string") {
164
315
  const msg = e.message.toLowerCase();
165
316
  if (msg.includes("disconnected") || msg.includes("not found") || msg.includes("no device") || msg.includes("unplugged"))
@@ -179,6 +330,21 @@ function isTimeoutError(err) {
179
330
  if (e.code === HardwareErrorCode.OperationTimeout) return true;
180
331
  return false;
181
332
  }
333
+ function isAppStuckByApdu(err) {
334
+ if (!err || typeof err !== "object") return false;
335
+ const tag = err._tag;
336
+ if (tag === ERROR_TAG.DeviceAppStuck) return true;
337
+ return extractApduHex(err) === "6901";
338
+ }
339
+ function isTransportStuck(err) {
340
+ if (!err || typeof err !== "object") return false;
341
+ const tag = err._tag;
342
+ return tag === ERROR_TAG.DeviceTransportStuck || // already wrapped form
343
+ tag === ERROR_TAG.UnknownDeviceExchange || tag === ERROR_TAG.AlreadySendingApdu;
344
+ }
345
+ function isStuckAppStateError(err) {
346
+ return isAppStuckByApdu(err) || isTransportStuck(err);
347
+ }
182
348
  function mapLedgerError(err, opts) {
183
349
  let originalMessage = "Unknown Ledger error";
184
350
  if (err instanceof Error) {
@@ -190,20 +356,24 @@ function mapLedgerError(err, opts) {
190
356
  let code;
191
357
  if (isDeviceLockedError(err)) {
192
358
  code = HardwareErrorCode.DeviceLocked;
359
+ } else if (isDeviceNotAdvertisingError(err) || isDeviceNotFoundError(err)) {
360
+ code = HardwareErrorCode.DeviceNotFound;
361
+ } else if (isDeviceBusyError(err)) {
362
+ code = HardwareErrorCode.DeviceBusy;
363
+ } else if (isBlePairingFailureError(err)) {
364
+ code = HardwareErrorCode.BlePairingTimeout;
193
365
  } else if (isUserRejectedError(err)) {
194
366
  code = HardwareErrorCode.UserRejected;
195
367
  } else if (isWrongAppError(err)) {
196
368
  code = HardwareErrorCode.WrongApp;
197
369
  } else if (isAppNotInstalledError(err)) {
198
- code = HardwareErrorCode.AppNotOpen;
370
+ code = HardwareErrorCode.AppNotInstalled;
199
371
  } else if (isDeviceDisconnectedError(err)) {
200
372
  code = HardwareErrorCode.DeviceDisconnected;
201
373
  } else if (isTimeoutError(err)) {
202
374
  code = HardwareErrorCode.OperationTimeout;
203
375
  } else {
204
- const ethCode = getEthAppErrorCode(err);
205
- const lastStep = err && typeof err === "object" ? err._lastStep : void 0;
206
- const ethMapped = ethCode ? mapEthAppError(ethCode, lastStep) : null;
376
+ const ethMapped = classifyEthAppError(err);
207
377
  const apduHex = ethMapped ? null : extractApduHex(err);
208
378
  const chainMapped = apduHex ? mapSolanaAppError(apduHex) ?? mapTronAppError(apduHex) ?? mapBtcAppError(apduHex) : null;
209
379
  code = ethMapped ?? chainMapped ?? HardwareErrorCode.UnknownError;
@@ -213,38 +383,100 @@ function mapLedgerError(err, opts) {
213
383
  return { code, message: enrichErrorMessage(code, originalMessage), appName };
214
384
  }
215
385
 
216
- // src/utils/debugLog.ts
217
- var enabled = false;
218
- function setDebugEnabled(value) {
219
- enabled = value;
386
+ // src/utils/ledgerDmkTransport.ts
387
+ function isLedgerDmkBleTransport(transport) {
388
+ const normalized = transport?.toUpperCase();
389
+ return normalized === "BLE" || normalized?.endsWith("_BLE") === true;
220
390
  }
221
- function isDebugEnabled() {
222
- return enabled;
391
+ function isLedgerBleConnectionType(connectionType) {
392
+ return connectionType === "ble";
223
393
  }
224
- function debugLog(...args) {
225
- if (enabled) {
226
- console.debug(...args);
394
+ function isLedgerBleDescriptor(connectionType, descriptor) {
395
+ return isLedgerBleConnectionType(connectionType) || isLedgerDmkBleTransport(descriptor.transport);
396
+ }
397
+
398
+ // src/utils/sdkEventBus.ts
399
+ var listeners = /* @__PURE__ */ new Set();
400
+ function onSdkEvent(listener) {
401
+ listeners.add(listener);
402
+ return () => {
403
+ listeners.delete(listener);
404
+ };
405
+ }
406
+ function offSdkEvent(listener) {
407
+ listeners.delete(listener);
408
+ }
409
+ function emitSdkEvent(event) {
410
+ if (listeners.size === 0) return;
411
+ for (const listener of listeners) {
412
+ try {
413
+ listener(event);
414
+ } catch {
415
+ }
227
416
  }
228
417
  }
229
- function debugError(...args) {
230
- if (enabled) {
231
- console.error(...args);
418
+ function emitLog(level, ...args) {
419
+ if (listeners.size === 0) return;
420
+ const message = args.map((a) => typeof a === "string" ? a : safeStringify(a)).join(" ");
421
+ emitSdkEvent({ type: "log", level, message });
422
+ }
423
+ function safeStringify(value) {
424
+ if (value instanceof Error) {
425
+ return value.stack ? `${value.message}
426
+ ${value.stack}` : value.message;
232
427
  }
428
+ try {
429
+ return JSON.stringify(value);
430
+ } catch {
431
+ return String(value);
432
+ }
433
+ }
434
+
435
+ // src/utils/debugLog.ts
436
+ function debugLog(...args) {
437
+ emitLog("debug", ...args);
438
+ }
439
+ function debugError(...args) {
440
+ emitLog("error", ...args);
233
441
  }
234
442
 
235
443
  // src/adapter/LedgerAdapter.ts
236
444
  function formatDeviceMismatchError(expected, actual) {
237
445
  return `Wrong device: expected ${expected}, got ${actual}`;
238
446
  }
447
+ var BTC_HIGH_INDEX_THRESHOLD = 100;
448
+ function btcAccountIndexFromPath(path) {
449
+ const segments = path.replace(/^m\//, "").split("/");
450
+ if (segments.length < 3) return null;
451
+ const accountSeg = segments[2].replace(/['h]$/i, "");
452
+ const accountIndex = parseInt(accountSeg, 10);
453
+ return Number.isFinite(accountIndex) ? accountIndex : null;
454
+ }
239
455
  var _LedgerAdapter = class _LedgerAdapter {
240
- constructor(connector, options) {
456
+ constructor(connector) {
241
457
  this.vendor = "ledger";
242
458
  this.emitter = new TypedEventEmitter();
243
- // Device cache: tracks discovered devices from connector events
244
459
  this._discoveredDevices = /* @__PURE__ */ new Map();
245
- // Session tracking: maps connectId -> sessionId
246
460
  this._sessions = /* @__PURE__ */ new Map();
247
461
  this._uiRegistry = new UiRequestRegistry();
462
+ // BTC App rejects account index >= 100 unless display=true. Cached per
463
+ // adapter instance: first 100+ path asks the user once via UI request,
464
+ // subsequent 100+ paths in the same session auto-promote silently.
465
+ // The Ledger device itself still requires a per-call confirmation — that's
466
+ // the Ledger app's safety boundary, not ours to bypass.
467
+ this._btcHighIndexConfirmedThisSession = false;
468
+ // Shared across concurrent callers — only `cancel()` aborts.
469
+ this._doConnectAbortController = null;
470
+ // ---------------------------------------------------------------------------
471
+ // Private helpers
472
+ // ---------------------------------------------------------------------------
473
+ /**
474
+ * Ensure at least one device is connected and return a valid connectId.
475
+ *
476
+ * - If a session already exists for the given connectId, reuse it.
477
+ * - If ANY session exists (Ledger IDs are ephemeral), reuse it.
478
+ * - Otherwise: search → exactly 1 USB device auto-connects; multiple or none throws.
479
+ */
248
480
  // Mutex for ensureConnected — prevents concurrent calls from establishing duplicate connections
249
481
  this._connectingPromise = null;
250
482
  // ---------------------------------------------------------------------------
@@ -274,40 +506,23 @@ var _LedgerAdapter = class _LedgerAdapter {
274
506
  this.emitter.emit("ui-event", event);
275
507
  };
276
508
  this.connector = connector;
277
- this._handleSelectDevice = options?.handleSelectDevice ?? false;
278
- this._jobQueue = new DeviceJobQueue({
279
- emit: (event, data) => this.emitter.emit(event, data),
280
- uiRegistry: this._uiRegistry
281
- });
509
+ this._jobQueue = new DeviceJobQueue();
282
510
  this.registerEventListeners();
283
511
  }
284
- /**
285
- * Classify a method's interruptibility.
286
- * - Signing / typed data / transaction → 'confirm' (user may decide via preemption UI)
287
- * - Read-only queries (getAddress / getPublicKey / getMasterFingerprint) → 'safe'
288
- * (auto-cancels any pending read for the same device)
289
- */
290
- static _getInterruptibility(method) {
291
- if (method.toLowerCase().includes("sign")) return "confirm";
292
- return "safe";
293
- }
294
- // ---------------------------------------------------------------------------
295
512
  // Transport
296
- // ---------------------------------------------------------------------------
297
- // Transport is decided at connector creation time. These methods
298
- // satisfy the IHardwareWallet interface with sensible defaults.
299
513
  get activeTransport() {
300
- return this.connector.connectionType === "ble" ? "ble" : "hid";
514
+ return isLedgerBleConnectionType(this.connector.connectionType) ? "ble" : "hid";
301
515
  }
302
516
  getAvailableTransports() {
303
517
  return this.activeTransport ? [this.activeTransport] : [];
304
518
  }
305
- async switchTransport(_type) {
519
+ // Connector is bound at construction; switching requires a new adapter.
520
+ switchTransport(_type) {
521
+ return Promise.resolve();
306
522
  }
307
- // ---------------------------------------------------------------------------
308
523
  // Lifecycle
309
- // ---------------------------------------------------------------------------
310
- async init(_config) {
524
+ init(_config) {
525
+ return Promise.resolve();
311
526
  }
312
527
  /**
313
528
  * Clear cached device/session state without tearing down the adapter.
@@ -320,6 +535,7 @@ var _LedgerAdapter = class _LedgerAdapter {
320
535
  this._connectingPromise = null;
321
536
  this._uiRegistry.reset();
322
537
  this._jobQueue.clear();
538
+ this._btcHighIndexConfirmedThisSession = false;
323
539
  }
324
540
  async dispose() {
325
541
  this._uiRegistry.reset();
@@ -328,6 +544,7 @@ var _LedgerAdapter = class _LedgerAdapter {
328
544
  this.connector.reset();
329
545
  this._discoveredDevices.clear();
330
546
  this._sessions.clear();
547
+ this._btcHighIndexConfirmedThisSession = false;
331
548
  this.emitter.removeAllListeners();
332
549
  }
333
550
  uiResponse(response) {
@@ -336,10 +553,16 @@ var _LedgerAdapter = class _LedgerAdapter {
336
553
  // ---------------------------------------------------------------------------
337
554
  // Device management
338
555
  // ---------------------------------------------------------------------------
339
- async searchDevices() {
556
+ async searchDevices(options) {
557
+ if (options?.resetSession) {
558
+ this._doConnectAbortController?.abort();
559
+ this._sessions.clear();
560
+ this._connectingPromise = null;
561
+ this._doConnectAbortController = null;
562
+ this._btcHighIndexConfirmedThisSession = false;
563
+ }
340
564
  await this._ensureDevicePermission();
341
565
  const devices = await this.connector.searchDevices();
342
- debugLog("[DMK] adapter.searchDevices raw:", JSON.stringify(devices));
343
566
  this._discoveredDevices.clear();
344
567
  for (const d of devices) {
345
568
  if (d.connectId) {
@@ -349,11 +572,41 @@ var _LedgerAdapter = class _LedgerAdapter {
349
572
  if (this._discoveredDevices.size === 0) {
350
573
  await this._ensureDevicePermission();
351
574
  }
575
+ debugLog(
576
+ `[LedgerAdapter] searchDevices() return count=${this._discoveredDevices.size} ids=[${[
577
+ ...this._discoveredDevices.keys()
578
+ ].join(",")}]`
579
+ );
352
580
  return Array.from(this._discoveredDevices.values());
353
581
  }
582
+ // USB single-session invariant: evict all sessions, best-effort (see connectDevice).
583
+ async _evictAllSessions() {
584
+ if (this._sessions.size === 0) return;
585
+ const stale = [...this._sessions.values()];
586
+ this._sessions.clear();
587
+ for (const sid of stale) {
588
+ try {
589
+ await this.connector.disconnect(sid);
590
+ } catch {
591
+ }
592
+ }
593
+ }
594
+ static _createDeviceBusyError(method) {
595
+ return Object.assign(new Error(`Ledger device is busy while calling ${method}`), {
596
+ code: HardwareErrorCode2.DeviceBusy
597
+ });
598
+ }
354
599
  async connectDevice(connectId) {
355
- await this._ensureDevicePermission(connectId);
356
600
  try {
601
+ if (isLedgerBleConnectionType(this.connector.connectionType) && !connectId) {
602
+ throw Object.assign(new Error("Ledger BLE connectId is required."), {
603
+ code: HardwareErrorCode2.DeviceNotFound
604
+ });
605
+ }
606
+ if (!isLedgerBleConnectionType(this.connector.connectionType)) {
607
+ await this._evictAllSessions();
608
+ }
609
+ await this._ensureDevicePermission(connectId);
357
610
  const session = await this.connector.connect(connectId);
358
611
  this._sessions.set(connectId, session.sessionId);
359
612
  if (session.deviceInfo) {
@@ -389,7 +642,6 @@ var _LedgerAdapter = class _LedgerAdapter {
389
642
  // Chain call helper
390
643
  // ---------------------------------------------------------------------------
391
644
  async callChain(connectId, deviceId, chain, method, params, skipFingerprint = false) {
392
- await this._ensureDevicePermission(connectId, deviceId);
393
645
  try {
394
646
  const result = await this.connectorCall(connectId, method, params, {
395
647
  chain,
@@ -401,45 +653,12 @@ var _LedgerAdapter = class _LedgerAdapter {
401
653
  return this.errorToFailure(err);
402
654
  }
403
655
  }
404
- /**
405
- * Batch version of callChain — checks permission once,
406
- * fingerprint is verified on the first call inside connectorCall.
407
- */
408
- async callChainBatch(connectId, deviceId, chain, method, params, onProgress, skipFingerprint = false) {
409
- await this._ensureDevicePermission(connectId, deviceId);
410
- const results = [];
411
- for (let i = 0; i < params.length; i++) {
412
- try {
413
- const result = await this.connectorCall(connectId, method, params[i], {
414
- chain,
415
- deviceId,
416
- // Only verify fingerprint on the first call in the batch
417
- skipFingerprint: skipFingerprint || i > 0
418
- });
419
- results.push(result);
420
- onProgress?.({ index: i, total: params.length });
421
- } catch (err) {
422
- return this.errorToFailure(err);
423
- }
424
- }
425
- return success(results);
426
- }
427
656
  // ---------------------------------------------------------------------------
428
657
  // EVM chain methods
429
658
  // ---------------------------------------------------------------------------
430
659
  evmGetAddress(connectId, deviceId, params) {
431
660
  return this.callChain(connectId, deviceId, "evm", "evmGetAddress", params);
432
661
  }
433
- evmGetAddresses(connectId, deviceId, params, onProgress) {
434
- return this.callChainBatch(
435
- connectId,
436
- deviceId,
437
- "evm",
438
- "evmGetAddress",
439
- params,
440
- onProgress
441
- );
442
- }
443
662
  evmSignTransaction(connectId, deviceId, params) {
444
663
  return this.callChain(connectId, deviceId, "evm", "evmSignTransaction", params);
445
664
  }
@@ -455,16 +674,6 @@ var _LedgerAdapter = class _LedgerAdapter {
455
674
  btcGetAddress(connectId, deviceId, params) {
456
675
  return this.callChain(connectId, deviceId, "btc", "btcGetAddress", params);
457
676
  }
458
- btcGetAddresses(connectId, deviceId, params, onProgress) {
459
- return this.callChainBatch(
460
- connectId,
461
- deviceId,
462
- "btc",
463
- "btcGetAddress",
464
- params,
465
- onProgress
466
- );
467
- }
468
677
  btcGetPublicKey(connectId, deviceId, params) {
469
678
  return this.callChain(connectId, deviceId, "btc", "btcGetPublicKey", params);
470
679
  }
@@ -492,16 +701,6 @@ var _LedgerAdapter = class _LedgerAdapter {
492
701
  solGetAddress(connectId, deviceId, params) {
493
702
  return this.callChain(connectId, deviceId, "sol", "solGetAddress", params);
494
703
  }
495
- solGetAddresses(connectId, deviceId, params, onProgress) {
496
- return this.callChainBatch(
497
- connectId,
498
- deviceId,
499
- "sol",
500
- "solGetAddress",
501
- params,
502
- onProgress
503
- );
504
- }
505
704
  solSignTransaction(connectId, deviceId, params) {
506
705
  return this.callChain(connectId, deviceId, "sol", "solSignTransaction", params);
507
706
  }
@@ -512,38 +711,13 @@ var _LedgerAdapter = class _LedgerAdapter {
512
711
  // TRON chain methods
513
712
  // ---------------------------------------------------------------------------
514
713
  tronGetAddress(connectId, deviceId, params) {
515
- return this.callChain(connectId, deviceId, "tron", "tronGetAddress", params, true);
516
- }
517
- tronGetAddresses(connectId, deviceId, params, onProgress) {
518
- return this.callChainBatch(
519
- connectId,
520
- deviceId,
521
- "tron",
522
- "tronGetAddress",
523
- params,
524
- onProgress,
525
- true
526
- );
714
+ return this.callChain(connectId, deviceId, "tron", "tronGetAddress", params);
527
715
  }
528
716
  tronSignTransaction(connectId, deviceId, params) {
529
- return this.callChain(
530
- connectId,
531
- deviceId,
532
- "tron",
533
- "tronSignTransaction",
534
- params,
535
- true
536
- );
717
+ return this.callChain(connectId, deviceId, "tron", "tronSignTransaction", params);
537
718
  }
538
719
  tronSignMessage(connectId, deviceId, params) {
539
- return this.callChain(
540
- connectId,
541
- deviceId,
542
- "tron",
543
- "tronSignMessage",
544
- params,
545
- true
546
- );
720
+ return this.callChain(connectId, deviceId, "tron", "tronSignMessage", params);
547
721
  }
548
722
  on(event, listener) {
549
723
  this.emitter.on(event, listener);
@@ -552,30 +726,41 @@ var _LedgerAdapter = class _LedgerAdapter {
552
726
  this.emitter.off(event, listener);
553
727
  }
554
728
  cancel(connectId) {
555
- const sessionId = this._sessions.get(connectId) ?? connectId;
556
- this._jobQueue.forceCancelActive(connectId || "__ledger_default__");
557
- void this.connector.cancel(sessionId);
729
+ const userAbortReason = Object.assign(new Error("User aborted operation"), {
730
+ code: HardwareErrorCode2.UserAborted,
731
+ _tag: ERROR_TAG.UserAborted
732
+ });
733
+ this._lastCancelReason = userAbortReason;
734
+ setTimeout(() => {
735
+ if (this._lastCancelReason === userAbortReason) {
736
+ this._lastCancelReason = void 0;
737
+ }
738
+ }, 2e3);
739
+ this._uiRegistry.cancel();
740
+ if (connectId) {
741
+ this._jobQueue.cancelActiveAndPending(connectId, userAbortReason);
742
+ } else {
743
+ this._jobQueue.cancelActiveAndPending(void 0, userAbortReason);
744
+ }
745
+ if (connectId) {
746
+ const sessionId = this._sessions.get(connectId) ?? connectId;
747
+ void this.connector.cancel(sessionId);
748
+ } else {
749
+ for (const sid of this._sessions.values()) void this.connector.cancel(sid);
750
+ }
751
+ if (this._connectingPromise) {
752
+ this._doConnectAbortController?.abort(userAbortReason);
753
+ }
558
754
  }
559
755
  // ---------------------------------------------------------------------------
560
756
  // Chain fingerprint
561
757
  // ---------------------------------------------------------------------------
562
758
  async getChainFingerprint(connectId, deviceId, chain) {
563
- debugLog(
564
- "[LedgerAdapter] getChainFingerprint called, chain:",
565
- chain,
566
- "connectId:",
567
- connectId || "(empty)",
568
- "sessions:",
569
- this._sessions.size
570
- );
571
- await this._ensureDevicePermission(connectId, deviceId);
572
- debugLog("[LedgerAdapter] getChainFingerprint permission ok, computing fingerprint");
573
759
  try {
574
760
  const fingerprint = await this._computeChainFingerprint(
575
761
  chain,
576
- (method, params) => this.connectorCall(connectId, method, params)
762
+ (method, params) => this.connectorCall(connectId, method, params, void 0, deviceId)
577
763
  );
578
- debugLog("[LedgerAdapter] getChainFingerprint result:", fingerprint?.substring(0, 20));
579
764
  return success(fingerprint);
580
765
  } catch (err) {
581
766
  debugError("[LedgerAdapter] getChainFingerprint error:", chain, err);
@@ -646,103 +831,250 @@ var _LedgerAdapter = class _LedgerAdapter {
646
831
  if (signal?.aborted) {
647
832
  _LedgerAdapter._throwIfAborted(signal);
648
833
  }
834
+ const waitPromise = this._uiRegistry.wait(
835
+ UI_REQUEST.REQUEST_DEVICE_CONNECT
836
+ );
649
837
  this.emitter.emit(UI_REQUEST.REQUEST_DEVICE_CONNECT, {
650
838
  type: UI_REQUEST.REQUEST_DEVICE_CONNECT,
651
839
  payload: {
840
+ vendor: "ledger",
841
+ reason: "device-not-found",
652
842
  message: "Please connect and unlock your Ledger device"
653
843
  }
654
844
  });
655
- const waitPromise = this._uiRegistry.wait(
656
- UI_REQUEST.REQUEST_DEVICE_CONNECT
657
- );
658
845
  let payload;
659
- if (signal) {
660
- const onAbort = () => {
661
- this._uiRegistry.cancel(UI_REQUEST.REQUEST_DEVICE_CONNECT);
662
- };
663
- signal.addEventListener("abort", onAbort, { once: true });
664
- try {
846
+ try {
847
+ if (signal) {
848
+ const onAbort = () => {
849
+ this._uiRegistry.cancel(UI_REQUEST.REQUEST_DEVICE_CONNECT);
850
+ };
851
+ signal.addEventListener("abort", onAbort, { once: true });
852
+ try {
853
+ payload = await waitPromise;
854
+ } finally {
855
+ signal.removeEventListener("abort", onAbort);
856
+ }
857
+ } else {
665
858
  payload = await waitPromise;
666
- } finally {
667
- signal.removeEventListener("abort", onAbort);
668
859
  }
669
- } else {
670
- payload = await waitPromise;
860
+ } catch (err) {
861
+ this.emitter.emit(UI_REQUEST.CLOSE_UI_WINDOW, {
862
+ type: UI_REQUEST.CLOSE_UI_WINDOW,
863
+ payload: {}
864
+ });
865
+ throw Object.assign(new Error("User cancelled Ledger connection"), {
866
+ _tag: ERROR_TAG.UserAborted,
867
+ code: HardwareErrorCode2.UserAborted,
868
+ cause: err
869
+ });
671
870
  }
672
871
  if (!payload?.confirmed) {
673
872
  throw Object.assign(new Error("User cancelled Ledger connection"), {
674
- _tag: "DeviceNotRecognizedError"
873
+ _tag: ERROR_TAG.UserAborted,
874
+ code: HardwareErrorCode2.UserAborted
675
875
  });
676
876
  }
877
+ await new Promise((resolve) => {
878
+ setTimeout(resolve, 800);
879
+ });
677
880
  }
678
- async ensureConnected(connectId) {
679
- if (connectId && this._sessions.has(connectId)) {
680
- return connectId;
681
- }
682
- if (this._sessions.size > 0) {
683
- const firstKey = this._sessions.keys().next().value;
684
- return firstKey;
881
+ /**
882
+ * Decide whether a BTC pubkey call needs `showOnDevice=true` because of
883
+ * the BTC App's account-index policy, asking the user once per session.
884
+ *
885
+ * Returns the params to pass through (with `showOnDevice` possibly
886
+ * promoted), or `null` if the user declined.
887
+ */
888
+ async _gateBtcHighIndex(params) {
889
+ const accountIndex = btcAccountIndexFromPath(params.path);
890
+ if (params.showOnDevice) return params;
891
+ if (accountIndex === null || accountIndex < BTC_HIGH_INDEX_THRESHOLD) {
892
+ return params;
685
893
  }
686
- if (this._connectingPromise) {
687
- return this._connectingPromise;
894
+ if (this._btcHighIndexConfirmedThisSession) {
895
+ return { ...params, showOnDevice: true };
688
896
  }
689
- this._connectingPromise = this._doConnect();
897
+ const confirmed = await this._waitForBtcHighIndexConfirm(params.path, accountIndex);
898
+ if (!confirmed) return null;
899
+ this._btcHighIndexConfirmedThisSession = true;
900
+ return { ...params, showOnDevice: true };
901
+ }
902
+ async _waitForBtcHighIndexConfirm(path, accountIndex) {
903
+ const waitPromise = this._uiRegistry.wait(
904
+ UI_REQUEST.REQUEST_BTC_HIGH_INDEX_CONFIRM
905
+ );
906
+ this.emitter.emit(UI_REQUEST.REQUEST_BTC_HIGH_INDEX_CONFIRM, {
907
+ type: UI_REQUEST.REQUEST_BTC_HIGH_INDEX_CONFIRM,
908
+ payload: {
909
+ vendor: "ledger",
910
+ path,
911
+ accountIndex
912
+ }
913
+ });
690
914
  try {
691
- return await this._connectingPromise;
692
- } finally {
693
- this._connectingPromise = null;
915
+ const payload = await waitPromise;
916
+ return !!payload?.confirmed;
917
+ } catch (err) {
918
+ this.emitter.emit(UI_REQUEST.CLOSE_UI_WINDOW, {
919
+ type: UI_REQUEST.CLOSE_UI_WINDOW,
920
+ payload: {}
921
+ });
922
+ return false;
923
+ }
924
+ }
925
+ // Layer 1 entry. Caller signal only races the outer awaiter; the shared
926
+ // `_doConnect` runs under its own internal controller so caller A's cancel
927
+ // doesn't kill caller B's await.
928
+ async ensureConnected(connectId, signal) {
929
+ if (signal.aborted) _LedgerAdapter._throwIfAborted(signal);
930
+ if (isLedgerBleConnectionType(this.connector.connectionType) && !connectId) {
931
+ throw Object.assign(new Error("Ledger BLE connectId is required."), {
932
+ code: HardwareErrorCode2.DeviceNotFound
933
+ });
934
+ }
935
+ if (connectId && this._sessions.has(connectId)) return connectId;
936
+ if (!connectId && this._sessions.size > 0) {
937
+ if (!isLedgerBleConnectionType(this.connector.connectionType) && this._sessions.size > 1) {
938
+ throw Object.assign(
939
+ new Error(
940
+ "Ledger USB session invariant violated: more than one session is active. Please reconnect the device."
941
+ ),
942
+ { code: HardwareErrorCode2.DeviceOneDeviceOnly }
943
+ );
944
+ }
945
+ return this._sessions.keys().next().value;
946
+ }
947
+ if (!this._connectingPromise) {
948
+ this._doConnectAbortController = new AbortController();
949
+ const innerSignal = this._doConnectAbortController.signal;
950
+ this._connectingPromise = (async () => {
951
+ try {
952
+ return await this._doConnect(innerSignal, connectId);
953
+ } finally {
954
+ this._connectingPromise = null;
955
+ this._doConnectAbortController = null;
956
+ }
957
+ })();
694
958
  }
959
+ return this._abortable(signal, this._connectingPromise);
695
960
  }
696
- async _doConnect() {
697
- for (let attempt = 0; attempt < _LedgerAdapter.MAX_DEVICE_RETRY; attempt++) {
698
- const devices = await this.searchDevices();
961
+ // Layer 1 main loop — the ONLY place in SDK that emits unlock dialog.
962
+ // Bounded by MAX_DOCONNECT_CONFIRMS after N Confirms with no progress,
963
+ // throw DeviceNotFound so the user is kicked out of the loop.
964
+ async _doConnect(internalSignal, targetConnectId) {
965
+ if (isLedgerBleConnectionType(this.connector.connectionType) && targetConnectId) {
966
+ try {
967
+ return await this._connectDeviceOrThrow(targetConnectId);
968
+ } catch (err) {
969
+ if (!isDeviceLockedError(err) && !isDeviceNotAdvertisingError(err) && !isDeviceDisconnectedError(err)) {
970
+ throw err;
971
+ }
972
+ this._discoveredDevices.delete(targetConnectId);
973
+ if (isDeviceDisconnectedError(err)) {
974
+ try {
975
+ this.connector.reset?.();
976
+ } catch {
977
+ }
978
+ }
979
+ }
980
+ }
981
+ let confirms = 0;
982
+ while (!internalSignal.aborted) {
983
+ this.emitter.emit("ui-event", {
984
+ type: EConnectorInteraction.Searching,
985
+ payload: { sessionId: "" }
986
+ });
987
+ let devices = await this.searchDevices();
988
+ if (devices.length === 0) {
989
+ for (let i = 0; i < 3 && !internalSignal.aborted; i += 1) {
990
+ await new Promise((resolve) => {
991
+ setTimeout(resolve, 1e3);
992
+ });
993
+ devices = await this.searchDevices();
994
+ if (devices.length > 0) break;
995
+ }
996
+ }
699
997
  if (devices.length > 0) {
700
- return this._connectFirstOrSelect(devices);
998
+ try {
999
+ return await this._connectFirstOrSelect(devices, targetConnectId);
1000
+ } catch (err) {
1001
+ if (!isDeviceLockedError(err) && !isDeviceNotAdvertisingError(err) && !isDeviceDisconnectedError(err)) {
1002
+ throw err;
1003
+ }
1004
+ this._discoveredDevices.clear();
1005
+ if (isDeviceDisconnectedError(err)) {
1006
+ try {
1007
+ this.connector.reset?.();
1008
+ } catch {
1009
+ }
1010
+ }
1011
+ }
701
1012
  }
702
- if (attempt < _LedgerAdapter.MAX_DEVICE_RETRY - 1) {
703
- await this._waitForDeviceConnect();
1013
+ if (confirms >= _LedgerAdapter.MAX_DOCONNECT_CONFIRMS) {
1014
+ throw Object.assign(
1015
+ new Error(
1016
+ "Device not connected after multiple attempts. Please ensure your Ledger is awake, unlocked, and in range, then try again."
1017
+ ),
1018
+ { code: HardwareErrorCode2.DeviceNotFound }
1019
+ );
704
1020
  }
1021
+ await this._waitForDeviceConnect(internalSignal);
1022
+ confirms += 1;
705
1023
  }
706
- throw Object.assign(
707
- new Error(
708
- "No Ledger device found after multiple attempts. Please connect and unlock your device."
709
- ),
710
- { _tag: "DeviceNotRecognizedError" }
711
- );
1024
+ _LedgerAdapter._throwIfAborted(internalSignal);
1025
+ throw new Error("_doConnect aborted");
1026
+ }
1027
+ async _connectFirstOrSelect(devices, targetConnectId) {
1028
+ if (targetConnectId) {
1029
+ const target = devices.find(
1030
+ (d) => d.connectId === targetConnectId || d.deviceId === targetConnectId
1031
+ );
1032
+ if (target) {
1033
+ return this._connectDeviceOrThrow(target.connectId);
1034
+ }
1035
+ if (!isLedgerBleConnectionType(this.connector.connectionType) && devices.length === 1) {
1036
+ debugLog(
1037
+ `[LedgerAdapter] target ${targetConnectId} not in fresh enumeration; accepting sole USB device ${devices[0].connectId} (assumed ephemeral path change)`
1038
+ );
1039
+ return this._connectDeviceOrThrow(devices[0].connectId);
1040
+ }
1041
+ const err = Object.assign(new Error(`Target Ledger unavailable: ${targetConnectId}`), {
1042
+ code: HardwareErrorCode2.DeviceNotFound
1043
+ });
1044
+ if (isLedgerBleConnectionType(this.connector.connectionType)) {
1045
+ err._tag = ERROR_TAG.DeviceNotAdvertising;
1046
+ }
1047
+ throw err;
1048
+ }
1049
+ if (isLedgerBleConnectionType(this.connector.connectionType)) {
1050
+ throw Object.assign(new Error("Ledger BLE connectId is required."), {
1051
+ code: HardwareErrorCode2.DeviceNotFound
1052
+ });
1053
+ }
1054
+ if (devices.length > 1) {
1055
+ throw createMultipleUsbLedgerDevicesError();
1056
+ }
1057
+ if (devices.length !== 1) {
1058
+ throw Object.assign(new Error("Ledger device not found."), {
1059
+ code: HardwareErrorCode2.DeviceNotFound
1060
+ });
1061
+ }
1062
+ return this._connectDeviceOrThrow(devices[0].connectId);
712
1063
  }
713
- async _connectFirstOrSelect(devices) {
714
- const chosenConnectId = devices.length === 1 ? devices[0].connectId : await this._chooseDeviceFromList(devices);
1064
+ async _connectDeviceOrThrow(chosenConnectId) {
715
1065
  const result = await this.connectDevice(chosenConnectId);
716
1066
  if (!result.success) {
717
- throw Object.assign(new Error(result.payload.error), {
718
- _tag: "DeviceNotRecognizedError"
1067
+ const payload = result.payload;
1068
+ const rethrow = Object.assign(new Error(payload.error), {
1069
+ code: payload.code
719
1070
  });
1071
+ if (payload._tag !== void 0) {
1072
+ rethrow._tag = payload._tag;
1073
+ }
1074
+ throw rethrow;
720
1075
  }
721
1076
  return chosenConnectId;
722
1077
  }
723
- async _chooseDeviceFromList(devices) {
724
- if (!this._handleSelectDevice) {
725
- debugLog(
726
- `[DMK] Multiple Ledger devices found (${devices.length}); handleSelectDevice=false, picking first (${devices[0].connectId}).`
727
- );
728
- return devices[0].connectId;
729
- }
730
- this.emitter.emit(UI_REQUEST.REQUEST_SELECT_DEVICE, {
731
- type: UI_REQUEST.REQUEST_SELECT_DEVICE,
732
- payload: { devices }
733
- });
734
- const response = await this._uiRegistry.wait(
735
- UI_REQUEST.REQUEST_SELECT_DEVICE
736
- );
737
- const chosen = devices.find((d) => d.connectId === response?.sdkConnectId);
738
- if (!chosen) {
739
- throw Object.assign(
740
- new Error(`Selected sdkConnectId '${response?.sdkConnectId}' not in discovered list`),
741
- { _tag: "DeviceNotRecognizedError" }
742
- );
743
- }
744
- return chosen.connectId;
745
- }
746
1078
  /**
747
1079
  * Call the connector with automatic session resolution and disconnect retry.
748
1080
  *
@@ -751,30 +1083,31 @@ var _LedgerAdapter = class _LedgerAdapter {
751
1083
  * 3. Calls connector.call()
752
1084
  * 4. On disconnect error: clears stale session, re-connects, retries once
753
1085
  */
754
- async connectorCall(connectId, method, params, fingerprint) {
1086
+ async connectorCall(connectId, method, params, fingerprint, permissionDeviceId) {
755
1087
  debugLog("[LedgerAdapter] connectorCall:", method, "connectId:", connectId || "(empty)");
756
1088
  const queueKey = connectId || "__ledger_default__";
757
- const interruptibility = _LedgerAdapter._getInterruptibility(method);
758
1089
  return this._jobQueue.enqueue(
759
1090
  queueKey,
760
- async (signal) => this._runConnectorCall(connectId, method, params, signal, fingerprint),
761
- { interruptibility, label: method }
1091
+ async (signal) => this._runConnectorCall(connectId, method, params, signal, fingerprint, permissionDeviceId),
1092
+ {
1093
+ label: method,
1094
+ rejectIfBusy: true,
1095
+ busyError: _LedgerAdapter._createDeviceBusyError(method)
1096
+ }
762
1097
  );
763
1098
  }
764
1099
  /**
765
- * Race a promise against an abort signal. If the signal fires, rejects with the
766
- * signal's reason (or a generic Error). The underlying connector.call() cannot
767
- * actually be cancelled on Ledger DMK, but the caller gets the abort immediately.
1100
+ * Race a promise against an abort signal. On abort, rejects with
1101
+ * signal.reason instance _lastCancelReason → generic Error('Aborted').
768
1102
  */
769
- static _abortable(signal, promise) {
1103
+ _abortable(signal, promise) {
1104
+ const getAbortReason = () => signal.reason ?? this._lastCancelReason ?? new Error("Aborted");
770
1105
  if (signal.aborted) {
771
- return Promise.reject(
772
- signal.reason ?? new Error("Aborted")
773
- );
1106
+ return Promise.reject(getAbortReason());
774
1107
  }
775
1108
  return new Promise((resolve, reject) => {
776
1109
  const onAbort = () => {
777
- reject(signal.reason ?? new Error("Aborted"));
1110
+ reject(getAbortReason());
778
1111
  };
779
1112
  signal.addEventListener("abort", onAbort, { once: true });
780
1113
  promise.then(
@@ -796,40 +1129,49 @@ var _LedgerAdapter = class _LedgerAdapter {
796
1129
  }
797
1130
  }
798
1131
  /** Actual work done under the job queue — connection, fingerprint, call, and recovery. */
799
- async _runConnectorCall(connectId, method, params, signal, fingerprint) {
1132
+ async _runConnectorCall(connectId, method, params, signal, fingerprint, permissionDeviceId) {
800
1133
  _LedgerAdapter._throwIfAborted(signal);
801
- const resolvedConnectId = await _LedgerAdapter._abortable(
802
- signal,
803
- this.ensureConnected(connectId)
1134
+ await this._ensureDevicePermission(
1135
+ connectId,
1136
+ permissionDeviceId ?? fingerprint?.deviceId,
1137
+ signal
804
1138
  );
805
1139
  _LedgerAdapter._throwIfAborted(signal);
1140
+ let effectiveParams = params;
1141
+ if (method === "btcGetPublicKey") {
1142
+ const gatedParams = await this._gateBtcHighIndex(params);
1143
+ if (gatedParams === null) {
1144
+ throw Object.assign(new Error("User cancelled BTC high-index confirmation"), {
1145
+ _tag: ERROR_TAG.UserAborted,
1146
+ code: HardwareErrorCode2.UserAborted
1147
+ });
1148
+ }
1149
+ effectiveParams = gatedParams;
1150
+ }
1151
+ const resolvedConnectId = await this.ensureConnected(connectId, signal);
806
1152
  const sessionId = this._sessions.get(resolvedConnectId);
807
- debugLog(
808
- "[LedgerAdapter] connectorCall resolved:",
809
- method,
810
- "resolvedConnectId:",
811
- resolvedConnectId,
812
- "sessionId:",
813
- sessionId
814
- );
815
1153
  if (!sessionId) {
816
1154
  throw Object.assign(new Error("Auto-connect succeeded but no session found"), {
817
- _tag: "DeviceSessionNotFound"
1155
+ _tag: ERROR_TAG.DeviceSessionNotFound
818
1156
  });
819
1157
  }
820
- if (fingerprint && !fingerprint.skipFingerprint && fingerprint.deviceId) {
821
- const fp = await _LedgerAdapter._abortable(
822
- signal,
823
- this._verifyDeviceFingerprintWithSession(sessionId, fingerprint.deviceId, fingerprint.chain)
824
- );
825
- if (!fp.success) {
826
- throw Object.assign(new Error(formatDeviceMismatchError(fp.expected, fp.actual)), {
827
- code: HardwareErrorCode2.DeviceMismatch
828
- });
829
- }
830
- }
831
1158
  try {
832
- return await _LedgerAdapter._abortable(signal, this.connector.call(sessionId, method, params));
1159
+ if (fingerprint && !fingerprint.skipFingerprint && fingerprint.deviceId) {
1160
+ const fp = await this._abortable(
1161
+ signal,
1162
+ this._verifyDeviceFingerprintWithSession(
1163
+ sessionId,
1164
+ fingerprint.deviceId,
1165
+ fingerprint.chain
1166
+ )
1167
+ );
1168
+ if (!fp.success) {
1169
+ throw Object.assign(new Error(formatDeviceMismatchError(fp.expected, fp.actual)), {
1170
+ code: HardwareErrorCode2.DeviceMismatch
1171
+ });
1172
+ }
1173
+ }
1174
+ return await this._abortable(signal, this.connector.call(sessionId, method, effectiveParams));
833
1175
  } catch (err) {
834
1176
  if (signal.aborted) throw err;
835
1177
  const errObj = err;
@@ -839,37 +1181,241 @@ var _LedgerAdapter = class _LedgerAdapter {
839
1181
  errorCode: errObj?.errorCode,
840
1182
  statusCode: errObj?.statusCode,
841
1183
  isDisconnected: isDeviceDisconnectedError(err),
842
- isLocked: isDeviceLockedError(err)
1184
+ isLocked: isDeviceLockedError(err),
1185
+ isNotAdvertising: isDeviceNotAdvertisingError(err),
1186
+ isStuckApp: isStuckAppStateError(err)
843
1187
  });
844
- if (isDeviceDisconnectedError(err)) {
845
- debugLog("[LedgerAdapter] disconnected, retrying with fresh connection...");
846
- this._discoveredDevices.clear();
847
- return this._retryWithFreshConnection(resolvedConnectId, method, params, signal, err);
1188
+ if (isStuckAppStateError(err)) {
1189
+ try {
1190
+ this._sessions.delete(resolvedConnectId);
1191
+ this._discoveredDevices.delete(resolvedConnectId);
1192
+ this.connector.reset?.();
1193
+ } catch {
1194
+ }
1195
+ debugLog(
1196
+ "[LedgerAdapter] stuck-app retry: method=",
1197
+ method,
1198
+ "delayMs=",
1199
+ _LedgerAdapter.STUCK_APP_RETRY_DELAY_MS,
1200
+ "_tag=",
1201
+ err?._tag
1202
+ );
1203
+ try {
1204
+ const retryResult = await this._retryAfterStuckApp(
1205
+ resolvedConnectId,
1206
+ method,
1207
+ effectiveParams,
1208
+ signal,
1209
+ err,
1210
+ fingerprint
1211
+ );
1212
+ debugLog("[LedgerAdapter] stuck-app retry succeeded: method=", method);
1213
+ return retryResult;
1214
+ } catch (retryErr) {
1215
+ if (signal.aborted) throw retryErr;
1216
+ if (isStuckAppStateError(retryErr)) {
1217
+ debugLog("[LedgerAdapter] stuck-app retry exhausted (2nd 6901): method=", method);
1218
+ throw err;
1219
+ }
1220
+ debugLog(
1221
+ "[LedgerAdapter] stuck-app retry threw non-stuck error: method=",
1222
+ method,
1223
+ "retryErrTag=",
1224
+ retryErr?._tag
1225
+ );
1226
+ throw retryErr;
1227
+ }
848
1228
  }
849
- if (isDeviceLockedError(err)) {
850
- await this._waitForDeviceConnect(signal);
851
- _LedgerAdapter._throwIfAborted(signal);
852
- return _LedgerAdapter._abortable(signal, this.connector.call(sessionId, method, params));
1229
+ if (isDeviceLockedError(err) || isDeviceNotAdvertisingError(err) || isDeviceDisconnectedError(err)) {
1230
+ let lastErr = err;
1231
+ for (let attempt = 0; attempt < _LedgerAdapter.MAX_BUSINESS_RETRY_BUDGET; attempt += 1) {
1232
+ if (signal.aborted) throw lastErr;
1233
+ try {
1234
+ this._sessions.delete(resolvedConnectId);
1235
+ this._discoveredDevices.delete(resolvedConnectId);
1236
+ if (isDeviceDisconnectedError(lastErr)) {
1237
+ try {
1238
+ this.connector.reset?.();
1239
+ } catch {
1240
+ }
1241
+ }
1242
+ const reConnectId = await this.ensureConnected(resolvedConnectId, signal);
1243
+ const reSessionId = this._sessions.get(reConnectId);
1244
+ if (!reSessionId) throw lastErr;
1245
+ if (fingerprint && !fingerprint.skipFingerprint && fingerprint.deviceId) {
1246
+ const fp = await this._abortable(
1247
+ signal,
1248
+ this._verifyDeviceFingerprintWithSession(
1249
+ reSessionId,
1250
+ fingerprint.deviceId,
1251
+ fingerprint.chain
1252
+ )
1253
+ );
1254
+ if (!fp.success) {
1255
+ throw Object.assign(new Error(formatDeviceMismatchError(fp.expected, fp.actual)), {
1256
+ code: HardwareErrorCode2.DeviceMismatch
1257
+ });
1258
+ }
1259
+ }
1260
+ return await this._abortable(
1261
+ signal,
1262
+ this.connector.call(reSessionId, method, effectiveParams)
1263
+ );
1264
+ } catch (retryErr) {
1265
+ if (signal.aborted) throw retryErr;
1266
+ lastErr = retryErr;
1267
+ const canRetry = attempt < _LedgerAdapter.MAX_BUSINESS_RETRY_BUDGET - 1 && (isDeviceLockedError(retryErr) || isDeviceNotAdvertisingError(retryErr) || isDeviceDisconnectedError(retryErr));
1268
+ if (!canRetry) {
1269
+ throw retryErr;
1270
+ }
1271
+ }
1272
+ }
1273
+ throw lastErr;
853
1274
  }
854
1275
  if (isTimeoutError(err)) {
855
1276
  debugLog("[LedgerAdapter] timeout, retrying with fresh connection...");
856
1277
  this._discoveredDevices.delete(resolvedConnectId);
857
- return this._retryWithFreshConnection(resolvedConnectId, method, params, signal, err);
1278
+ return this._retryWithFreshConnection(
1279
+ resolvedConnectId,
1280
+ method,
1281
+ effectiveParams,
1282
+ signal,
1283
+ err,
1284
+ fingerprint
1285
+ );
1286
+ }
1287
+ if (isConnectionLevelError(err)) {
1288
+ debugLog("[LedgerAdapter] connection-level fail-closed reset");
1289
+ this._sessions.delete(resolvedConnectId);
1290
+ this._discoveredDevices.delete(resolvedConnectId);
1291
+ this.connector.reset();
1292
+ const codeNum = err?.code;
1293
+ throw Object.assign(err, {
1294
+ code: codeNum ?? HardwareErrorCode2.DeviceDisconnected
1295
+ });
858
1296
  }
859
1297
  throw err;
860
1298
  }
861
1299
  }
862
- /** Clear stale session, reconnect, and retry the call. */
863
- async _retryWithFreshConnection(resolvedConnectId, method, params, signal, originalErr) {
864
- this._sessions.delete(resolvedConnectId);
865
- _LedgerAdapter._throwIfAborted(signal);
866
- const retryConnectId = await this.ensureConnected();
867
- _LedgerAdapter._throwIfAborted(signal);
1300
+ /**
1301
+ * Stuck-app recovery: pause for the device's UI transition, then retry once.
1302
+ *
1303
+ * Caller has already cleared the session + reset connector. We wait so Stax
1304
+ * finishes its post-CloseApp animation, then go through ensureConnected +
1305
+ * fingerprint check + call exactly once. Caller decides what to do on a
1306
+ * second stuck-app hit.
1307
+ */
1308
+ async _retryAfterStuckApp(resolvedConnectId, method, params, signal, originalErr, fingerprint) {
1309
+ await this._sleepAbortable(_LedgerAdapter.STUCK_APP_RETRY_DELAY_MS, signal);
1310
+ const retryConnectId = await this.ensureConnected(resolvedConnectId, signal);
1311
+ const retrySessionId = this._sessions.get(retryConnectId);
1312
+ if (!retrySessionId) throw originalErr;
1313
+ if (fingerprint && !fingerprint.skipFingerprint && fingerprint.deviceId) {
1314
+ const fp = await this._abortable(
1315
+ signal,
1316
+ this._verifyDeviceFingerprintWithSession(
1317
+ retrySessionId,
1318
+ fingerprint.deviceId,
1319
+ fingerprint.chain
1320
+ )
1321
+ );
1322
+ if (!fp.success) {
1323
+ throw Object.assign(new Error(formatDeviceMismatchError(fp.expected, fp.actual)), {
1324
+ code: HardwareErrorCode2.DeviceMismatch
1325
+ });
1326
+ }
1327
+ }
1328
+ return this._abortable(signal, this.connector.call(retrySessionId, method, params));
1329
+ }
1330
+ _sleepAbortable(ms, signal) {
1331
+ return new Promise((resolve, reject) => {
1332
+ if (signal.aborted) {
1333
+ reject(signal.reason ?? new Error("Aborted"));
1334
+ return;
1335
+ }
1336
+ const timer = setTimeout(() => {
1337
+ signal.removeEventListener("abort", onAbort);
1338
+ resolve();
1339
+ }, ms);
1340
+ const onAbort = () => {
1341
+ clearTimeout(timer);
1342
+ reject(signal.reason ?? new Error("Aborted"));
1343
+ };
1344
+ signal.addEventListener("abort", onAbort, { once: true });
1345
+ });
1346
+ }
1347
+ /**
1348
+ * Clear stale session, reconnect, and retry the call.
1349
+ *
1350
+ * Timeout recovery starts with a full connector reset. After an APDU
1351
+ * timeout, DMK/transport state may still emit malformed responses; retrying
1352
+ * on the same DMK can poison the next chain switch.
1353
+ */
1354
+ async _retryWithFreshConnection(targetConnectId, method, params, signal, originalErr, fingerprint) {
1355
+ this.connector.reset();
1356
+ this._sessions.clear();
1357
+ this._discoveredDevices.clear();
1358
+ this._connectingPromise = null;
1359
+ const retryConnectId = await this.ensureConnected(targetConnectId, signal);
868
1360
  const retrySessionId = this._sessions.get(retryConnectId);
869
1361
  if (!retrySessionId) {
870
1362
  throw originalErr;
871
1363
  }
872
- return _LedgerAdapter._abortable(signal, this.connector.call(retrySessionId, method, params));
1364
+ try {
1365
+ if (fingerprint && !fingerprint.skipFingerprint && fingerprint.deviceId) {
1366
+ const fp = await this._abortable(
1367
+ signal,
1368
+ this._verifyDeviceFingerprintWithSession(
1369
+ retrySessionId,
1370
+ fingerprint.deviceId,
1371
+ fingerprint.chain
1372
+ )
1373
+ );
1374
+ if (!fp.success) {
1375
+ throw Object.assign(new Error(formatDeviceMismatchError(fp.expected, fp.actual)), {
1376
+ code: HardwareErrorCode2.DeviceMismatch
1377
+ });
1378
+ }
1379
+ }
1380
+ return await this._abortable(signal, this.connector.call(retrySessionId, method, params));
1381
+ } catch (retryErr) {
1382
+ if (signal.aborted) throw retryErr;
1383
+ this.connector.reset();
1384
+ this._sessions.clear();
1385
+ this._discoveredDevices.clear();
1386
+ this._connectingPromise = null;
1387
+ if (!isDeviceDisconnectedError(retryErr) && !isTimeoutError(retryErr)) {
1388
+ throw retryErr;
1389
+ }
1390
+ debugLog(
1391
+ "[LedgerAdapter] fresh-session retry still failed; resetting connector and rebuilding DMK"
1392
+ );
1393
+ this.connector.reset();
1394
+ this._sessions.clear();
1395
+ this._discoveredDevices.clear();
1396
+ this._connectingPromise = null;
1397
+ const finalConnectId = await this.ensureConnected(targetConnectId, signal);
1398
+ const finalSessionId = this._sessions.get(finalConnectId);
1399
+ if (!finalSessionId) {
1400
+ throw originalErr;
1401
+ }
1402
+ if (fingerprint && !fingerprint.skipFingerprint && fingerprint.deviceId) {
1403
+ const fp = await this._abortable(
1404
+ signal,
1405
+ this._verifyDeviceFingerprintWithSession(
1406
+ finalSessionId,
1407
+ fingerprint.deviceId,
1408
+ fingerprint.chain
1409
+ )
1410
+ );
1411
+ if (!fp.success) {
1412
+ throw Object.assign(new Error(formatDeviceMismatchError(fp.expected, fp.actual)), {
1413
+ code: HardwareErrorCode2.DeviceMismatch
1414
+ });
1415
+ }
1416
+ }
1417
+ return this._abortable(signal, this.connector.call(finalSessionId, method, params));
1418
+ }
873
1419
  }
874
1420
  /**
875
1421
  * Ensure OS-level device permission (Bluetooth / USB) before proceeding.
@@ -883,7 +1429,10 @@ var _LedgerAdapter = class _LedgerAdapter {
883
1429
  * - No connectId (searchDevices): environment-level permission
884
1430
  * - With connectId (business methods): device-level permission
885
1431
  */
886
- async _ensureDevicePermission(connectId, deviceId) {
1432
+ async _ensureDevicePermission(connectId, deviceId, signal) {
1433
+ if (signal?.aborted) {
1434
+ _LedgerAdapter._throwIfAborted(signal);
1435
+ }
887
1436
  const transportType = this.activeTransport ?? "hid";
888
1437
  const waitPromise = this._uiRegistry.wait(
889
1438
  UI_REQUEST.REQUEST_DEVICE_PERMISSION,
@@ -893,10 +1442,37 @@ var _LedgerAdapter = class _LedgerAdapter {
893
1442
  type: UI_REQUEST.REQUEST_DEVICE_PERMISSION,
894
1443
  payload: { transportType, connectId, deviceId }
895
1444
  });
896
- const { granted } = await waitPromise;
1445
+ let response;
1446
+ const onAbort = () => {
1447
+ this._uiRegistry.cancel(UI_REQUEST.REQUEST_DEVICE_PERMISSION);
1448
+ };
1449
+ try {
1450
+ signal?.addEventListener("abort", onAbort, { once: true });
1451
+ response = await waitPromise;
1452
+ } catch (err) {
1453
+ if (signal?.aborted) {
1454
+ _LedgerAdapter._throwIfAborted(signal);
1455
+ }
1456
+ if (err?._tag === UI_REQUEST_PREEMPTED_TAG) {
1457
+ this.emitter.emit(UI_REQUEST.CLOSE_UI_WINDOW, {
1458
+ type: UI_REQUEST.CLOSE_UI_WINDOW,
1459
+ payload: {}
1460
+ });
1461
+ throw Object.assign(new Error("Device permission request superseded"), {
1462
+ _tag: ERROR_TAG.UserAborted,
1463
+ code: HardwareErrorCode2.UserAborted,
1464
+ cause: err
1465
+ });
1466
+ }
1467
+ throw err;
1468
+ } finally {
1469
+ signal?.removeEventListener("abort", onAbort);
1470
+ }
1471
+ const { granted, reason, message } = response;
897
1472
  if (!granted) {
898
- throw Object.assign(new Error("Device permission denied"), {
899
- code: HardwareErrorCode2.DevicePermissionDenied
1473
+ throw Object.assign(new Error(message ?? "Device permission denied"), {
1474
+ code: HardwareErrorCode2.DevicePermissionDenied,
1475
+ reason
900
1476
  });
901
1477
  }
902
1478
  }
@@ -906,12 +1482,14 @@ var _LedgerAdapter = class _LedgerAdapter {
906
1482
  */
907
1483
  errorToFailure(err) {
908
1484
  debugError("[LedgerAdapter] error:", err);
1485
+ const tag = err && typeof err === "object" ? err._tag : void 0;
909
1486
  if (err && typeof err === "object" && "code" in err && typeof err.code === "number") {
910
1487
  const e = err;
911
- return ledgerFailure(e.code, e.message ?? "Unknown error", e.appName);
1488
+ const params = e.code === HardwareErrorCode2.DevicePermissionDenied && e.reason ? { permissionDeniedReason: e.reason } : void 0;
1489
+ return ledgerFailure(e.code, e.message ?? "Unknown error", e.appName, tag, params);
912
1490
  }
913
1491
  const mapped = mapLedgerError(err);
914
- return ledgerFailure(mapped.code, mapped.message, mapped.appName);
1492
+ return ledgerFailure(mapped.code, mapped.message, mapped.appName, tag);
915
1493
  }
916
1494
  registerEventListeners() {
917
1495
  this.connector.on("device-connect", this.deviceConnectHandler);
@@ -927,32 +1505,38 @@ var _LedgerAdapter = class _LedgerAdapter {
927
1505
  // Device info mapping
928
1506
  // ---------------------------------------------------------------------------
929
1507
  connectorDeviceToDeviceInfo(device) {
930
- const isBle = device.connectId && /^[0-9A-Fa-f]{4}$/.test(device.connectId);
931
1508
  return {
932
1509
  vendor: "ledger",
933
1510
  model: device.model ?? "unknown",
1511
+ modelName: device.modelName,
934
1512
  firmwareVersion: "",
935
1513
  deviceId: device.deviceId,
936
1514
  connectId: device.connectId,
937
1515
  label: device.name,
938
- connectionType: isBle ? "ble" : "usb",
1516
+ connectionType: this.connector.connectionType,
1517
+ rssi: device.rssi,
1518
+ isConnectable: device.isConnectable,
1519
+ serialNumber: device.serialNumber,
939
1520
  capabilities: device.capabilities
940
1521
  };
941
1522
  }
942
1523
  };
943
- // ---------------------------------------------------------------------------
944
- // Private helpers
945
- // ---------------------------------------------------------------------------
946
- /**
947
- * Ensure at least one device is connected and return a valid connectId.
948
- *
949
- * - If a session already exists for the given connectId, reuse it.
950
- * - If ANY session exists (Ledger IDs are ephemeral), reuse it.
951
- * - Otherwise: search 1 device: auto-connect, multiple: ask user, 0: throw.
952
- */
953
- _LedgerAdapter.MAX_DEVICE_RETRY = 3;
1524
+ // Layer 2 retry budget after connection-class error. Each round delegates
1525
+ // to Layer 1 (which owns the unlock prompt). Layer 2 never emits UI itself.
1526
+ _LedgerAdapter.MAX_BUSINESS_RETRY_BUDGET = 3;
1527
+ // Stuck-app (APDU 0x6901) retry pause. Ledger Stax often returns 6901 when
1528
+ // OpenAppCommand lands mid-transition right after CloseApp; a short wait
1529
+ // lets the device finish its UI animation before we retry once.
1530
+ _LedgerAdapter.STUCK_APP_RETRY_DELAY_MS = 500;
1531
+ // Layer 1 confirm budget. After N failed Confirm cycles (user keeps clicking
1532
+ // Confirm but device never shows up / never unlocks), throw DeviceNotFound
1533
+ // instead of looping forever.
1534
+ _LedgerAdapter.MAX_DOCONNECT_CONFIRMS = 3;
954
1535
  var LedgerAdapter = _LedgerAdapter;
955
1536
 
1537
+ // src/connector/LedgerConnectorBase.ts
1538
+ import { HardwareErrorCode as HardwareErrorCode7 } from "@onekeyfe/hwk-adapter-core";
1539
+
956
1540
  // src/device/LedgerDeviceManager.ts
957
1541
  var LedgerDeviceManager = class {
958
1542
  constructor(dmk) {
@@ -989,7 +1573,9 @@ var LedgerDeviceManager = class {
989
1573
  if (resolved) return;
990
1574
  resolved = true;
991
1575
  this._discovered.clear();
992
- debugLog("[DMK] enumerate raw devices:", devices.length);
1576
+ debugLog(
1577
+ `[DMK] enumerate count=${devices.length} ids=[${devices.map((d) => d.id).join(",")}]`
1578
+ );
993
1579
  for (const d of devices) {
994
1580
  this._discovered.set(d.id, d);
995
1581
  }
@@ -999,8 +1585,10 @@ var LedgerDeviceManager = class {
999
1585
  devices.map((d) => ({
1000
1586
  path: d.id,
1001
1587
  type: d.deviceModel.model,
1588
+ modelName: d.deviceModel.name,
1002
1589
  name: d.name,
1003
- transport: d.transport
1590
+ transport: d.transport,
1591
+ rssi: d.rssi
1004
1592
  }))
1005
1593
  );
1006
1594
  } else {
@@ -1021,8 +1609,10 @@ var LedgerDeviceManager = class {
1021
1609
  devices.map((d) => ({
1022
1610
  path: d.id,
1023
1611
  type: d.deviceModel.model,
1612
+ modelName: d.deviceModel.name,
1024
1613
  name: d.name,
1025
- transport: d.transport
1614
+ transport: d.transport,
1615
+ rssi: d.rssi
1026
1616
  }))
1027
1617
  );
1028
1618
  }
@@ -1046,8 +1636,10 @@ var LedgerDeviceManager = class {
1046
1636
  descriptor: {
1047
1637
  path: d.id,
1048
1638
  type: d.deviceModel.model,
1639
+ modelName: d.deviceModel.name,
1049
1640
  name: d.name,
1050
- transport: d.transport
1641
+ transport: d.transport,
1642
+ rssi: d.rssi
1051
1643
  }
1052
1644
  });
1053
1645
  }
@@ -1066,6 +1658,54 @@ var LedgerDeviceManager = class {
1066
1658
  this._listenSub?.unsubscribe();
1067
1659
  this._listenSub = null;
1068
1660
  }
1661
+ /**
1662
+ * Resolve to the latest snapshot of advertising devices observed within
1663
+ * `timeoutMs`. Rewrites `_discovered` so it tracks the live list.
1664
+ *
1665
+ * `listenToAvailableDevices` is a BehaviorSubject — it emits the cached
1666
+ * list on subscribe and *only* re-emits when the list changes. A stable
1667
+ * list of currently-advertising devices therefore produces only the
1668
+ * subscription frame; treating that frame as "fossil to be discarded"
1669
+ * caused devices to flicker in/out of the UI as searches alternated
1670
+ * between "saw a change frame" and "saw only the cached frame".
1671
+ *
1672
+ * The window still gives a chance for the active scan to update the list
1673
+ * (e.g. drop a peripheral that just stopped broadcasting), but if nothing
1674
+ * changes we trust the snapshot — DMK silence on a stable list means
1675
+ * "everything's still there".
1676
+ */
1677
+ getLiveDevices(timeoutMs = 1500) {
1678
+ debugLog(`[DMK] getLiveDevices() called, timeoutMs=${timeoutMs}`);
1679
+ void this.requestDevice();
1680
+ return new Promise((resolve) => {
1681
+ let done = false;
1682
+ let lastSeen = [];
1683
+ let sub = null;
1684
+ const finish = (devices) => {
1685
+ if (done) return;
1686
+ done = true;
1687
+ sub?.unsubscribe();
1688
+ clearTimeout(timer);
1689
+ this._discovered.clear();
1690
+ for (const d of devices) this._discovered.set(d.id, d);
1691
+ debugLog(
1692
+ `[DMK] getLiveDevices() resolved count=${devices.length} ids=[${devices.map((d) => d.id).join(",")}]`
1693
+ );
1694
+ resolve(devices);
1695
+ };
1696
+ const timer = setTimeout(() => finish(lastSeen), timeoutMs);
1697
+ sub = this._dmk.listenToAvailableDevices({}).subscribe({
1698
+ next: (devices) => {
1699
+ lastSeen = devices;
1700
+ },
1701
+ error: (err) => {
1702
+ debugError("[DMK] getLiveDevices() error:", err);
1703
+ finish(lastSeen);
1704
+ },
1705
+ complete: () => finish(lastSeen)
1706
+ });
1707
+ });
1708
+ }
1069
1709
  requestDevice() {
1070
1710
  if (this._discoverySub) {
1071
1711
  return Promise.resolve();
@@ -1083,11 +1723,33 @@ var LedgerDeviceManager = class {
1083
1723
  });
1084
1724
  return Promise.resolve();
1085
1725
  }
1726
+ /** Lookup a previously-discovered device's display name. */
1727
+ getDeviceName(deviceId) {
1728
+ return this._discovered.get(deviceId)?.name;
1729
+ }
1730
+ /** Lookup minimal model/signal info from a previously-discovered device. */
1731
+ getDiscoveredDeviceInfo(deviceId) {
1732
+ const d = this._discovered.get(deviceId);
1733
+ if (!d) return void 0;
1734
+ return {
1735
+ model: d.deviceModel?.model,
1736
+ modelName: d.deviceModel?.name,
1737
+ name: d.name,
1738
+ rssi: d.rssi
1739
+ };
1740
+ }
1741
+ hasDiscoveredDevice(deviceId) {
1742
+ return this._discovered.has(deviceId);
1743
+ }
1086
1744
  /** Connect to a previously discovered device. Returns sessionId. */
1087
1745
  async connect(deviceId) {
1088
1746
  const device = this._discovered.get(deviceId);
1089
1747
  if (!device) {
1090
- throw new Error(`Device "${deviceId}" not found. Call enumerate() or listen() first.`);
1748
+ const err = new Error(
1749
+ `Device "${deviceId}" not found in discovery cache. Call enumerate() or listen() first.`
1750
+ );
1751
+ err._tag = ERROR_TAG.DeviceNotInDiscoveryCache;
1752
+ throw err;
1091
1753
  }
1092
1754
  const sessionId = await this._dmk.connect({ device });
1093
1755
  this._sessions.set(deviceId, sessionId);
@@ -1126,6 +1788,19 @@ var LedgerDeviceManager = class {
1126
1788
  this._sessionToDevice.clear();
1127
1789
  this._dmk.close();
1128
1790
  }
1791
+ /**
1792
+ * Tear down RxJS subscriptions and local state without closing the DMK.
1793
+ * For light-reset paths that want to drop session state but keep the
1794
+ * underlying transport alive for retry. Call this instead of orphaning
1795
+ * the manager — bare null assignment leaks _listenSub / _discoverySub.
1796
+ */
1797
+ disposeKeepingDmk() {
1798
+ this.stopListening();
1799
+ this.stopDiscovery();
1800
+ this._discovered.clear();
1801
+ this._sessions.clear();
1802
+ this._sessionToDevice.clear();
1803
+ }
1129
1804
  };
1130
1805
 
1131
1806
  // src/signer/SignerManager.ts
@@ -1137,11 +1812,12 @@ import { hexToBytes } from "@onekeyfe/hwk-adapter-core";
1137
1812
 
1138
1813
  // src/signer/deviceActionToPromise.ts
1139
1814
  import { DeviceActionStatus } from "@ledgerhq/device-management-kit";
1140
- var DEFAULT_TIMEOUT_MS = 3e4;
1141
- function deviceActionToPromise(action, onInteraction, timeoutMs = DEFAULT_TIMEOUT_MS, onRegisterCanceller) {
1815
+ var IDLE_WATCHDOG_MS = 65e3;
1816
+ function deviceActionToPromise(action, onInteraction, timeoutMs = IDLE_WATCHDOG_MS, onRegisterCanceller) {
1142
1817
  return new Promise((resolve, reject) => {
1143
1818
  let settled = false;
1144
1819
  let lastStep;
1820
+ const observedSteps = [];
1145
1821
  let sub;
1146
1822
  let timer = null;
1147
1823
  const cancelAction = () => {
@@ -1154,44 +1830,59 @@ function deviceActionToPromise(action, onInteraction, timeoutMs = DEFAULT_TIMEOU
1154
1830
  } catch {
1155
1831
  }
1156
1832
  };
1157
- const resetTimer = () => {
1833
+ const armIdleWatchdog = () => {
1158
1834
  if (timer) clearTimeout(timer);
1159
1835
  if (timeoutMs > 0) {
1160
1836
  timer = setTimeout(() => {
1161
1837
  if (!settled) {
1162
1838
  settled = true;
1163
1839
  cancelAction();
1164
- reject(new Error("Device action timed out \u2014 device may be locked or disconnected"));
1840
+ reject(
1841
+ new Error(
1842
+ "Ledger transport unresponsive: no DMK state change within the watchdog window. The device may have lost connection."
1843
+ )
1844
+ );
1165
1845
  }
1166
1846
  }, timeoutMs);
1167
1847
  }
1168
1848
  };
1169
- resetTimer();
1849
+ armIdleWatchdog();
1170
1850
  if (onRegisterCanceller) {
1171
- onRegisterCanceller(() => {
1172
- if (settled) return;
1851
+ onRegisterCanceller((reason) => {
1852
+ if (settled) {
1853
+ return;
1854
+ }
1173
1855
  settled = true;
1174
1856
  if (timer) clearTimeout(timer);
1175
1857
  cancelAction();
1176
- reject(new Error("Device action cancelled"));
1858
+ const err = new Error(reason?.message ?? "Device action cancelled");
1859
+ if (reason?.code !== void 0) {
1860
+ err.code = reason.code;
1861
+ }
1862
+ if (reason?.tag) {
1863
+ Object.defineProperty(err, "_tag", {
1864
+ value: reason.tag,
1865
+ configurable: true,
1866
+ enumerable: false,
1867
+ writable: true
1868
+ });
1869
+ }
1870
+ reject(err);
1177
1871
  });
1178
1872
  }
1179
- debugLog("[DMK-Observable] subscribing to action.observable...");
1180
1873
  sub = action.observable.subscribe({
1181
1874
  next: (state) => {
1182
- if (settled) return;
1183
- resetTimer();
1875
+ if (settled) {
1876
+ return;
1877
+ }
1878
+ armIdleWatchdog();
1184
1879
  const step = state?.intermediateValue?.step;
1185
- if (step) lastStep = step;
1186
- debugLog(
1187
- "[DMK-Observable] state:",
1188
- JSON.stringify({
1189
- status: state.status,
1190
- intermediateValue: state.status === DeviceActionStatus.Pending ? state.intermediateValue : void 0,
1191
- hasOutput: state.status === DeviceActionStatus.Completed,
1192
- hasError: state.status === DeviceActionStatus.Error
1193
- })
1194
- );
1880
+ if (step) {
1881
+ lastStep = step;
1882
+ if (observedSteps[observedSteps.length - 1] !== step) {
1883
+ observedSteps.push(step);
1884
+ }
1885
+ }
1195
1886
  if (state.status === DeviceActionStatus.Completed) {
1196
1887
  settled = true;
1197
1888
  if (timer) clearTimeout(timer);
@@ -1203,10 +1894,14 @@ function deviceActionToPromise(action, onInteraction, timeoutMs = DEFAULT_TIMEOU
1203
1894
  if (timer) clearTimeout(timer);
1204
1895
  onInteraction?.("interaction-complete");
1205
1896
  sub?.unsubscribe();
1206
- rejectWithLastStep(state.error, lastStep, reject);
1897
+ rejectWithStepContext(state.error, lastStep, observedSteps, reject);
1207
1898
  } else if (state.status === DeviceActionStatus.Pending && onInteraction) {
1208
1899
  const interaction = state.intermediateValue?.requiredUserInteraction;
1209
1900
  if (interaction && interaction !== "none") {
1901
+ if (interaction === "unlock-device" && timer) {
1902
+ clearTimeout(timer);
1903
+ timer = null;
1904
+ }
1210
1905
  onInteraction(String(interaction));
1211
1906
  }
1212
1907
  }
@@ -1216,7 +1911,7 @@ function deviceActionToPromise(action, onInteraction, timeoutMs = DEFAULT_TIMEOU
1216
1911
  settled = true;
1217
1912
  if (timer) clearTimeout(timer);
1218
1913
  sub?.unsubscribe();
1219
- rejectWithLastStep(err, lastStep, reject);
1914
+ rejectWithStepContext(err, lastStep, observedSteps, reject);
1220
1915
  }
1221
1916
  },
1222
1917
  complete: () => {
@@ -1229,15 +1924,25 @@ function deviceActionToPromise(action, onInteraction, timeoutMs = DEFAULT_TIMEOU
1229
1924
  });
1230
1925
  });
1231
1926
  }
1232
- function rejectWithLastStep(err, lastStep, reject) {
1233
- if (err && typeof err === "object" && lastStep) {
1927
+ function rejectWithStepContext(err, lastStep, observedSteps, reject) {
1928
+ if (err && typeof err === "object" && (lastStep || observedSteps.length > 0)) {
1234
1929
  try {
1235
- Object.defineProperty(err, "_lastStep", {
1236
- value: lastStep,
1237
- configurable: true,
1238
- enumerable: false,
1239
- writable: true
1240
- });
1930
+ if (lastStep) {
1931
+ Object.defineProperty(err, "_lastStep", {
1932
+ value: lastStep,
1933
+ configurable: true,
1934
+ enumerable: false,
1935
+ writable: true
1936
+ });
1937
+ }
1938
+ if (observedSteps.length > 0) {
1939
+ Object.defineProperty(err, "_deviceActionSteps", {
1940
+ value: [...observedSteps],
1941
+ configurable: true,
1942
+ enumerable: false,
1943
+ writable: true
1944
+ });
1945
+ }
1241
1946
  } catch {
1242
1947
  }
1243
1948
  }
@@ -1303,7 +2008,8 @@ var SignerManager = class _SignerManager {
1303
2008
  async getOrCreate(sessionId) {
1304
2009
  debugLog("[DMK] SignerManager.getOrCreate:", { sessionId });
1305
2010
  const builder = await this._builderFn({ dmk: this._dmk, sessionId });
1306
- return new SignerEth(builder.build());
2011
+ const builderWithContext = builder.withContextModule ? builder.withContextModule(_SignerManager._createContextModule()) : builder;
2012
+ return new SignerEth(builderWithContext.build());
1307
2013
  }
1308
2014
  // eslint-disable-next-line @typescript-eslint/no-unused-vars, class-methods-use-this
1309
2015
  invalidate(_sessionId) {
@@ -1312,10 +2018,24 @@ var SignerManager = class _SignerManager {
1312
2018
  clearAll() {
1313
2019
  }
1314
2020
  static _defaultBuilder() {
1315
- return (args) => {
1316
- const contextModule = new ContextModuleBuilder({}).removeDefaultLoaders().build();
1317
- return new SignerEthBuilder(args).withContextModule(contextModule);
2021
+ return (args) => new SignerEthBuilder(args);
2022
+ }
2023
+ static _createContextModule() {
2024
+ const contextModule = new ContextModuleBuilder({}).removeDefaultLoaders().build();
2025
+ return _SignerManager.wrapBlindSigningReportNonBlocking(contextModule);
2026
+ }
2027
+ static wrapBlindSigningReportNonBlocking(contextModule) {
2028
+ const report = contextModule.report.bind(contextModule);
2029
+ contextModule.report = async (params) => {
2030
+ try {
2031
+ void report(params).catch((err) => {
2032
+ debugLog("[DMK] ContextModule.report failed:", err);
2033
+ });
2034
+ } catch (err) {
2035
+ debugLog("[DMK] ContextModule.report failed:", err);
2036
+ }
1318
2037
  };
2038
+ return contextModule;
1319
2039
  }
1320
2040
  };
1321
2041
 
@@ -1323,20 +2043,20 @@ var SignerManager = class _SignerManager {
1323
2043
  import { HardwareErrorCode as HardwareErrorCode3, padHex64, stripHex } from "@onekeyfe/hwk-adapter-core";
1324
2044
 
1325
2045
  // src/connector/chains/utils.ts
1326
- import { EConnectorInteraction } from "@onekeyfe/hwk-adapter-core";
2046
+ import { EConnectorInteraction as EConnectorInteraction2 } from "@onekeyfe/hwk-adapter-core";
1327
2047
  function normalizePath(path) {
1328
2048
  return path.startsWith("m/") ? path.slice(2) : path;
1329
2049
  }
1330
2050
  function collapseSignerInteraction(interaction) {
1331
2051
  switch (interaction) {
1332
2052
  case "confirm-open-app":
1333
- return EConnectorInteraction.ConfirmOpenApp;
2053
+ return EConnectorInteraction2.ConfirmOpenApp;
1334
2054
  case "unlock-device":
1335
- return EConnectorInteraction.UnlockDevice;
2055
+ return EConnectorInteraction2.UnlockDevice;
1336
2056
  case "interaction-complete":
1337
- return EConnectorInteraction.InteractionComplete;
2057
+ return EConnectorInteraction2.InteractionComplete;
1338
2058
  default:
1339
- return EConnectorInteraction.ConfirmOnDevice;
2059
+ return EConnectorInteraction2.ConfirmOnDevice;
1340
2060
  }
1341
2061
  }
1342
2062
 
@@ -1425,6 +2145,7 @@ async function _getEthSigner(ctx, sessionId) {
1425
2145
  const signerManager = await ctx.getSignerManager();
1426
2146
  const signer = await signerManager.getOrCreate(sessionId);
1427
2147
  signer.onInteraction = (interaction) => {
2148
+ debugLog("[LedgerConnector] evm.onInteraction:", interaction);
1428
2149
  ctx.emit("ui-event", {
1429
2150
  type: collapseSignerInteraction(interaction),
1430
2151
  payload: { sessionId }
@@ -1700,6 +2421,7 @@ async function _createBtcSigner(ctx, sessionId) {
1700
2421
  const sdkSigner = new SignerBtcBuilder({ dmk, sessionId }).build();
1701
2422
  const signer = new SignerBtc(sdkSigner);
1702
2423
  signer.onInteraction = (interaction) => {
2424
+ debugLog("[LedgerConnector] btc.onInteraction:", interaction);
1703
2425
  ctx.emit("ui-event", {
1704
2426
  type: collapseSignerInteraction(interaction),
1705
2427
  payload: { sessionId }
@@ -1870,6 +2592,7 @@ async function _createSolSigner(ctx, sessionId) {
1870
2592
  const sdkSigner = new SignerSolanaBuilder({ dmk, sessionId }).withContextModule(contextModule).build();
1871
2593
  const signer = new SignerSol(sdkSigner);
1872
2594
  signer.onInteraction = (interaction) => {
2595
+ debugLog("[LedgerConnector] sol.onInteraction:", interaction);
1873
2596
  ctx.emit("ui-event", {
1874
2597
  type: collapseSignerInteraction(interaction),
1875
2598
  payload: { sessionId }
@@ -1882,11 +2605,11 @@ async function _createSolSigner(ctx, sessionId) {
1882
2605
  }
1883
2606
 
1884
2607
  // src/connector/chains/tron.ts
1885
- import { HardwareErrorCode as HardwareErrorCode5 } from "@onekeyfe/hwk-adapter-core";
2608
+ import { HardwareErrorCode as HardwareErrorCode6 } from "@onekeyfe/hwk-adapter-core";
1886
2609
  import Trx from "@ledgerhq/hw-app-trx";
1887
2610
 
1888
2611
  // src/connector/chains/legacyChainCall.ts
1889
- import { EConnectorInteraction as EConnectorInteraction2 } from "@onekeyfe/hwk-adapter-core";
2612
+ import { EConnectorInteraction as EConnectorInteraction3 } from "@onekeyfe/hwk-adapter-core";
1890
2613
 
1891
2614
  // src/app/AppManager.ts
1892
2615
  import {
@@ -1895,6 +2618,7 @@ import {
1895
2618
  OpenAppCommand,
1896
2619
  isSuccessCommandResult
1897
2620
  } from "@ledgerhq/device-management-kit";
2621
+ import { HardwareErrorCode as HardwareErrorCode5 } from "@onekeyfe/hwk-adapter-core";
1898
2622
  var APP_NAME_MAP = {
1899
2623
  ETH: "Ethereum",
1900
2624
  BTC: "Bitcoin",
@@ -1966,7 +2690,31 @@ var AppManager = class {
1966
2690
  debugLog("[AppManager] currentApp:", result.data.name);
1967
2691
  return result.data.name;
1968
2692
  }
1969
- throw new Error("Failed to get current app from device");
2693
+ const errResult = result;
2694
+ const dmkErr = errResult.error ?? {};
2695
+ const original = dmkErr.originalError;
2696
+ debugLog(
2697
+ "[AppManager] _getCurrentApp failed sessionId=",
2698
+ sessionId,
2699
+ "tag=",
2700
+ dmkErr._tag,
2701
+ "errorCode=",
2702
+ dmkErr.errorCode,
2703
+ "message=",
2704
+ dmkErr.message,
2705
+ "originalErrorMessage=",
2706
+ original?.message ?? String(original ?? "")
2707
+ );
2708
+ throw Object.assign(
2709
+ new Error(
2710
+ dmkErr.message ?? "Failed to get current app from device"
2711
+ ),
2712
+ {
2713
+ _tag: dmkErr._tag,
2714
+ errorCode: dmkErr.errorCode,
2715
+ originalError: original
2716
+ }
2717
+ );
1970
2718
  }
1971
2719
  async _openApp(sessionId, appName) {
1972
2720
  const result = await this._dmk.sendCommand({
@@ -1975,9 +2723,11 @@ var AppManager = class {
1975
2723
  });
1976
2724
  if (!isSuccessCommandResult(result)) {
1977
2725
  const { statusCode } = result;
2726
+ const hasStatusCode2 = statusCode != null && statusCode !== "";
1978
2727
  debugLog("[AppManager] openApp failed:", appName, "statusCode:", statusCode);
1979
2728
  throw Object.assign(new Error(`Failed to open "${appName}"`), {
1980
- _tag: "OpenAppCommandError",
2729
+ _tag: ERROR_TAG.OpenAppCommand,
2730
+ code: hasStatusCode2 ? void 0 : HardwareErrorCode5.AppNotInstalled,
1981
2731
  errorCode: String(statusCode ?? ""),
1982
2732
  statusCode,
1983
2733
  appName
@@ -2033,14 +2783,14 @@ async function withLegacyChainCall(ctx, sessionId, options, action) {
2033
2783
  const onAppOpenPrompt = () => {
2034
2784
  openAppPromptShown = true;
2035
2785
  ctx.emit("ui-event", {
2036
- type: EConnectorInteraction2.ConfirmOpenApp,
2786
+ type: EConnectorInteraction3.ConfirmOpenApp,
2037
2787
  payload: { sessionId }
2038
2788
  });
2039
2789
  };
2040
2790
  const closeOpenAppUiIfShown = () => {
2041
2791
  if (openAppPromptShown) {
2042
2792
  ctx.emit("ui-event", {
2043
- type: EConnectorInteraction2.InteractionComplete,
2793
+ type: EConnectorInteraction3.InteractionComplete,
2044
2794
  payload: { sessionId }
2045
2795
  });
2046
2796
  openAppPromptShown = false;
@@ -2061,7 +2811,7 @@ async function withLegacyChainCall(ctx, sessionId, options, action) {
2061
2811
  let confirmEmitted = false;
2062
2812
  if (needsConfirmation) {
2063
2813
  ctx.emit("ui-event", {
2064
- type: EConnectorInteraction2.ConfirmOnDevice,
2814
+ type: EConnectorInteraction3.ConfirmOnDevice,
2065
2815
  payload: { sessionId }
2066
2816
  });
2067
2817
  confirmEmitted = true;
@@ -2071,7 +2821,7 @@ async function withLegacyChainCall(ctx, sessionId, options, action) {
2071
2821
  } finally {
2072
2822
  if (confirmEmitted || openAppPromptShown) {
2073
2823
  ctx.emit("ui-event", {
2074
- type: EConnectorInteraction2.InteractionComplete,
2824
+ type: EConnectorInteraction3.InteractionComplete,
2075
2825
  payload: { sessionId }
2076
2826
  });
2077
2827
  openAppPromptShown = false;
@@ -2159,7 +2909,7 @@ async function tronSignTransaction(ctx, sessionId, params) {
2159
2909
  if (!params.rawTxHex) {
2160
2910
  throw Object.assign(
2161
2911
  new Error("TRON signing requires a protobuf-encoded raw transaction hex (rawTxHex)."),
2162
- { code: HardwareErrorCode5.InvalidParams }
2912
+ { code: HardwareErrorCode6.InvalidParams }
2163
2913
  );
2164
2914
  }
2165
2915
  const path = normalizePath(params.path);
@@ -2203,6 +2953,10 @@ var METHOD_PREFIX_TO_APP_NAME = {
2203
2953
  sol: "Solana",
2204
2954
  tron: "Tron"
2205
2955
  };
2956
+ var HARDWARE_ERROR_CODE_VALUES = new Set(
2957
+ Object.values(HardwareErrorCode7).filter((value) => typeof value === "number")
2958
+ );
2959
+ var BLE_CONNECT_SCAN_TIMEOUT_MS = 1500;
2206
2960
  async function defaultLedgerKitImporter(pkg) {
2207
2961
  switch (pkg) {
2208
2962
  case "@ledgerhq/device-management-kit":
@@ -2226,16 +2980,6 @@ var LedgerConnectorBase = class {
2226
2980
  this._dmk = null;
2227
2981
  this._eventHandlers = /* @__PURE__ */ new Map();
2228
2982
  // ---------------------------------------------------------------------------
2229
- // ConnectId <-> DMK path mapping
2230
- //
2231
- // DMK uses internal paths (BLE MAC, USB UUID) that may change across sessions.
2232
- // _resolveConnectId() maps these to stable external IDs (BLE: "A58F", USB: same).
2233
- // This bidirectional map is the SINGLE SOURCE OF TRUTH for all connectId usage.
2234
- // ---------------------------------------------------------------------------
2235
- this._connectIdToPath = /* @__PURE__ */ new Map();
2236
- // "A58F" -> "D5:75:7D:4B:51:E8"
2237
- this._pathToConnectId = /* @__PURE__ */ new Map();
2238
- // ---------------------------------------------------------------------------
2239
2983
  // Per-session DeviceAction cancellers
2240
2984
  //
2241
2985
  // Each chain handler registers its active DeviceAction's canceller via
@@ -2244,10 +2988,23 @@ var LedgerConnectorBase = class {
2244
2988
  // unsubscribes the observable and releases DMK's IntentQueue slot.
2245
2989
  // ---------------------------------------------------------------------------
2246
2990
  this._cancellers = /* @__PURE__ */ new Map();
2991
+ // ---------------------------------------------------------------------------
2992
+ // Per-session DMK state subscriptions
2993
+ //
2994
+ // DMK only emits `device-disconnect` on the connector when `disconnect()`
2995
+ // is called explicitly. To pick up autonomous disconnects (USB unplug,
2996
+ // BLE drop, device sleep, DMK transport reset) we subscribe to
2997
+ // `_dmk.getDeviceSessionState({sessionId})` per active session and emit
2998
+ // `device-disconnect` ourselves the moment DMK reports the device went
2999
+ // to NOT_CONNECTED. The subscription is torn down on disconnect /
3000
+ // reset / observable completion to avoid leaks.
3001
+ // ---------------------------------------------------------------------------
3002
+ this._sessionStateSubs = /* @__PURE__ */ new Map();
2247
3003
  this._createTransport = createTransport;
2248
3004
  this.connectionType = options?.connectionType ?? "usb";
2249
3005
  this._providedDmk = options?.dmk;
2250
3006
  this._importLedgerKit = options?.importLedgerKit ?? defaultLedgerKitImporter;
3007
+ this._requirePreFlightScan = options?.requirePreFlightScan ?? false;
2251
3008
  if (this._providedDmk) {
2252
3009
  this._initManagers(this._providedDmk);
2253
3010
  }
@@ -2265,38 +3022,23 @@ var LedgerConnectorBase = class {
2265
3022
  importLedgerKit: this._importLedgerKit
2266
3023
  };
2267
3024
  }
2268
- // "D5:75:7D:4B:51:E8" -> "A58F"
2269
- /** Register a connectId <-> path mapping from a device descriptor. */
2270
- _registerDeviceId(descriptor) {
2271
- const connectId = this._resolveConnectId(descriptor);
2272
- this._connectIdToPath.set(connectId, descriptor.path);
2273
- this._pathToConnectId.set(descriptor.path, connectId);
2274
- return connectId;
2275
- }
2276
- /** Get DMK path from external connectId. Falls back to connectId itself. */
2277
- _getPathForConnectId(connectId) {
2278
- return this._connectIdToPath.get(connectId) ?? connectId;
2279
- }
2280
- /** Get external connectId from DMK path. Falls back to path itself. */
2281
- _getConnectIdForPath(path) {
2282
- return this._pathToConnectId.get(path) ?? path;
2283
- }
2284
3025
  // ---------------------------------------------------------------------------
2285
3026
  // Protected — hooks for subclasses
2286
3027
  // ---------------------------------------------------------------------------
2287
3028
  /**
2288
3029
  * Resolve the connectId for a discovered device descriptor.
2289
3030
  * Default: use the DMK path (ephemeral UUID).
2290
- * Override in subclasses to extract stable identifiers (e.g. BLE hex ID).
3031
+ * Override in subclasses only when the public connectId differs from the transport path.
2291
3032
  */
2292
3033
  _resolveConnectId(descriptor) {
2293
3034
  return descriptor.path;
2294
3035
  }
2295
- // ---------------------------------------------------------------------------
2296
- // IConnector -- Device discovery
2297
- // ---------------------------------------------------------------------------
2298
- async searchDevices() {
2299
- const dm = await this._getDeviceManager();
3036
+ /**
3037
+ * Authoritative discovery for this transport. Default = enumerate snapshot
3038
+ * (USB/HID). BLE overrides to drive from a live scan window since DMK's
3039
+ * enumerate() cache survives peripherals going offline.
3040
+ */
3041
+ async _discoverDescriptors(dm) {
2300
3042
  let descriptors = await dm.enumerate();
2301
3043
  if (descriptors.length === 0) {
2302
3044
  try {
@@ -2305,92 +3047,276 @@ var LedgerConnectorBase = class {
2305
3047
  }
2306
3048
  descriptors = await dm.enumerate();
2307
3049
  }
2308
- const result = descriptors.map((d) => {
2309
- const connectId = this._registerDeviceId(d);
2310
- return {
2311
- connectId,
2312
- deviceId: d.path,
2313
- name: d.name || d.type || "Ledger",
2314
- model: d.type
2315
- };
2316
- });
3050
+ return descriptors;
3051
+ }
3052
+ // ---------------------------------------------------------------------------
3053
+ // IConnector -- Device discovery
3054
+ //
3055
+ // Discovery never throws on multiple USB devices — it returns the full list.
3056
+ // The "connect exactly one device" rule is enforced upstream: an explicit
3057
+ // connectId connects that device (or fails as not-found), and only the
3058
+ // no-connectId USB path rejects when more than one device is present
3059
+ // (see LedgerAdapter._connectFirstOrSelect).
3060
+ // ---------------------------------------------------------------------------
3061
+ async searchDevices() {
3062
+ const dm = await this._getDeviceManager();
3063
+ const descriptors = await this._discoverDescriptors(dm);
3064
+ const resolvedDescriptors = descriptors.map((d) => ({
3065
+ descriptor: d,
3066
+ connectId: this._resolveConnectId(d)
3067
+ }));
3068
+ const result = resolvedDescriptors.map(({ descriptor: d, connectId }) => ({
3069
+ connectId,
3070
+ deviceId: d.path,
3071
+ name: d.name || d.type || "Ledger",
3072
+ model: d.type,
3073
+ modelName: d.modelName,
3074
+ rssi: d.rssi,
3075
+ isConnectable: d.isConnectable,
3076
+ serialNumber: d.serialNumber
3077
+ }));
3078
+ debugLog(
3079
+ `[DMK] connector.searchDevices() return count=${result.length} ids=[${result.map((r) => r.connectId).join(",")}] paths=[${result.map((r) => r.deviceId).join(",")}]`
3080
+ );
2317
3081
  return result;
2318
3082
  }
2319
3083
  // ---------------------------------------------------------------------------
2320
3084
  // IConnector -- Connection
2321
3085
  // ---------------------------------------------------------------------------
2322
3086
  async connect(deviceId) {
2323
- const dm = await this._getDeviceManager();
2324
- let discovered = await this.searchDevices();
2325
- const dmkPath = deviceId ? this._getPathForConnectId(deviceId) : void 0;
2326
- let targetPath = dmkPath;
3087
+ const callerSuppliedConnectId = Boolean(deviceId);
3088
+ let targetPath = deviceId;
2327
3089
  if (!targetPath) {
2328
- const descriptors = await dm.enumerate();
2329
- if (descriptors.length === 0) {
3090
+ const discovered = await this.searchDevices();
3091
+ if (discovered.length === 0) {
2330
3092
  throw new Error(
2331
- `No Ledger device found. Make sure the device is connected${this.connectionType === "ble" ? " nearby with Bluetooth enabled" : " via USB"} and unlocked.`
3093
+ `No Ledger device found. Make sure the device is connected${isLedgerBleConnectionType(this.connectionType) ? " nearby with Bluetooth enabled" : " via USB"} and unlocked.`
2332
3094
  );
2333
3095
  }
2334
- targetPath = descriptors[0].path;
2335
- }
2336
- const externalConnectId = this._getConnectIdForPath(targetPath);
3096
+ targetPath = discovered[0].deviceId;
3097
+ }
3098
+ const externalConnectId = targetPath;
3099
+ const HANG_CEILING_MS = 5 * 6e4;
3100
+ const dmConnectWithObserve = async (dm, path) => {
3101
+ if (!isLedgerBleConnectionType(this.connectionType)) return dm.connect(path);
3102
+ let timeoutId;
3103
+ let timedOut = false;
3104
+ const connectPromise = dm.connect(path);
3105
+ const hangPromise = new Promise((_, reject) => {
3106
+ timeoutId = setTimeout(() => {
3107
+ timedOut = true;
3108
+ const err = new Error(
3109
+ `Ledger BLE connect did not return within ${HANG_CEILING_MS / 6e4}min \u2014 DMK hang fallback.`
3110
+ );
3111
+ err._tag = ERROR_TAG.BlePairingTimeout;
3112
+ err.code = HardwareErrorCode7.BlePairingTimeout;
3113
+ reject(err);
3114
+ }, HANG_CEILING_MS);
3115
+ });
3116
+ try {
3117
+ return await Promise.race([connectPromise, hangPromise]);
3118
+ } catch (err) {
3119
+ const e = err;
3120
+ debugLog("[DMK] dm.connect rejected \u2014 observed:", {
3121
+ _tag: e?._tag,
3122
+ message: e?.message,
3123
+ errorCode: e?.errorCode,
3124
+ statusCode: e?.statusCode,
3125
+ originalTag: e?.originalError?._tag,
3126
+ originalMessage: e?.originalError?.message
3127
+ });
3128
+ if (timedOut) {
3129
+ void connectPromise.then(
3130
+ (sessionId) => dm.disconnect(sessionId).catch(() => void 0),
3131
+ () => void 0
3132
+ );
3133
+ }
3134
+ throw err;
3135
+ } finally {
3136
+ if (timeoutId) clearTimeout(timeoutId);
3137
+ }
3138
+ };
3139
+ const isBleDirectConnect = isLedgerBleConnectionType(this.connectionType) && callerSuppliedConnectId;
3140
+ const throwNotAdvertising = () => {
3141
+ const err = new Error(
3142
+ "Ledger device is not currently advertising. Wake up and unlock the device, keep it nearby, then try again."
3143
+ );
3144
+ err._tag = ERROR_TAG.DeviceNotAdvertising;
3145
+ err.code = HardwareErrorCode7.DeviceNotFound;
3146
+ throw err;
3147
+ };
2337
3148
  const doConnect = async (path) => {
2338
- const sessionId = await dm.connect(path);
3149
+ const dm = await this._getDeviceManager();
3150
+ if (isBleDirectConnect && !dm.hasDiscoveredDevice(path)) {
3151
+ const live = await dm.getLiveDevices(BLE_CONNECT_SCAN_TIMEOUT_MS);
3152
+ if (this._requirePreFlightScan && !live.some((d) => d.id === path)) {
3153
+ throwNotAdvertising();
3154
+ }
3155
+ }
3156
+ let sessionId;
3157
+ try {
3158
+ sessionId = await dmConnectWithObserve(dm, path);
3159
+ } catch (err) {
3160
+ if (isBleDirectConnect && err?._tag === ERROR_TAG.DeviceNotInDiscoveryCache) {
3161
+ throwNotAdvertising();
3162
+ }
3163
+ throw err;
3164
+ }
3165
+ try {
3166
+ this._watchSessionState(sessionId, externalConnectId);
3167
+ } catch (subErr) {
3168
+ debugLog("[DMK] state subscription failed during connect; disconnecting session:", subErr);
3169
+ try {
3170
+ await dm.disconnect(sessionId);
3171
+ } catch {
3172
+ }
3173
+ throw subErr;
3174
+ }
3175
+ const info = dm.getDiscoveredDeviceInfo(path);
2339
3176
  const session = {
2340
3177
  sessionId,
2341
3178
  deviceInfo: {
2342
3179
  vendor: "ledger",
2343
- model: "unknown",
3180
+ model: info?.model ?? "unknown",
3181
+ modelName: info?.modelName,
2344
3182
  firmwareVersion: "unknown",
2345
3183
  deviceId: path,
2346
3184
  connectId: externalConnectId,
2347
3185
  connectionType: this.connectionType,
3186
+ rssi: info?.rssi,
2348
3187
  capabilities: { persistentDeviceIdentity: false }
2349
3188
  }
2350
3189
  };
2351
- const realName = discovered.find((d) => d.connectId === externalConnectId)?.name ?? "Ledger";
3190
+ const name = dm.getDeviceName(path) ?? "Ledger";
2352
3191
  this._emit("device-connect", {
2353
- device: {
2354
- connectId: externalConnectId,
2355
- deviceId: path,
2356
- name: realName
2357
- }
3192
+ device: { connectId: externalConnectId, deviceId: path, name }
2358
3193
  });
2359
3194
  return session;
2360
3195
  };
2361
3196
  try {
2362
3197
  return await doConnect(targetPath);
2363
- } catch {
3198
+ } catch (err) {
2364
3199
  this._resetSignersAndSessions();
2365
- const dm2 = await this._getDeviceManager();
2366
- discovered = await this.searchDevices();
2367
- const retryPath = this._getPathForConnectId(externalConnectId);
2368
- if (!retryPath || retryPath === externalConnectId) {
2369
- const descriptors = await dm2.enumerate();
2370
- if (descriptors.length === 0) {
2371
- throw new Error(
2372
- `No Ledger device found after retry. Make sure the device is connected${this.connectionType === "ble" ? " nearby with Bluetooth enabled" : " via USB"} and unlocked.`
2373
- );
3200
+ if (isLedgerBleConnectionType(this.connectionType)) {
3201
+ const tag = err?._tag;
3202
+ if (isKnownConnectionTag(tag)) {
3203
+ throw err;
2374
3204
  }
2375
- return doConnect(descriptors[0].path);
3205
+ const wrapped = new Error(
3206
+ "Ledger Bluetooth pairing failed. Make sure the device is unlocked and nearby, then try again."
3207
+ );
3208
+ wrapped._tag = ERROR_TAG.BleGattBondingFailed;
3209
+ wrapped.code = HardwareErrorCode7.BlePairingTimeout;
3210
+ wrapped.originalError = err;
3211
+ throw wrapped;
2376
3212
  }
2377
- return doConnect(retryPath);
3213
+ if (err && typeof err === "object" && err._tag) {
3214
+ const taggedError = err instanceof Error ? err : Object.assign(
3215
+ new Error(err.message ?? "Ledger device error"),
3216
+ err
3217
+ );
3218
+ throw taggedError;
3219
+ }
3220
+ throw new Error("Ledger device appears unplugged. Plug in the device and try again.");
2378
3221
  }
2379
3222
  }
2380
3223
  async disconnect(sessionId) {
2381
3224
  if (!this._deviceManager) return;
2382
3225
  const deviceId = this._deviceManager.getDeviceId(sessionId);
2383
3226
  this._signerManager?.invalidate(sessionId);
3227
+ this._unwatchSessionState(sessionId);
2384
3228
  await this._deviceManager.disconnect(sessionId);
2385
3229
  if (deviceId) {
2386
3230
  this._emit("device-disconnect", { connectId: deviceId });
2387
3231
  }
2388
3232
  }
3233
+ /**
3234
+ * Subscribe to DMK's per-session state observable so that any autonomous
3235
+ * disconnect (USB unplug, BLE drop, device sleep, transport reset) is
3236
+ * surfaced as a `device-disconnect` event — without this, the upstream
3237
+ * `LedgerAdapter._sessions` map would hold a dead session entry until
3238
+ * the next call hit `DeviceSessionNotFound`.
3239
+ *
3240
+ * Subscribe failure is fatal — see the inline note at the subscribe call.
3241
+ */
3242
+ _watchSessionState(sessionId, externalConnectId) {
3243
+ const dmk = this._dmk;
3244
+ if (!dmk) return;
3245
+ const previous = this._sessionStateSubs.get(sessionId);
3246
+ if (previous) {
3247
+ try {
3248
+ previous.unsubscribe();
3249
+ } catch {
3250
+ }
3251
+ }
3252
+ const sub = dmk.getDeviceSessionState({ sessionId }).subscribe({
3253
+ next: (state) => {
3254
+ if (state?.deviceStatus === "NOT CONNECTED") {
3255
+ this._handleAutonomousDisconnect(sessionId, externalConnectId);
3256
+ }
3257
+ },
3258
+ error: () => {
3259
+ this._handleAutonomousDisconnect(sessionId, externalConnectId);
3260
+ },
3261
+ complete: () => {
3262
+ this._handleAutonomousDisconnect(sessionId, externalConnectId);
3263
+ }
3264
+ });
3265
+ this._sessionStateSubs.set(sessionId, sub);
3266
+ }
3267
+ _unwatchSessionState(sessionId) {
3268
+ const sub = this._sessionStateSubs.get(sessionId);
3269
+ if (!sub) return;
3270
+ try {
3271
+ sub.unsubscribe();
3272
+ } catch {
3273
+ }
3274
+ this._sessionStateSubs.delete(sessionId);
3275
+ }
3276
+ _handleAutonomousDisconnect(sessionId, externalConnectId) {
3277
+ if (!this._sessionStateSubs.has(sessionId)) return;
3278
+ debugLog(
3279
+ "[DMK] autonomous disconnect detected \u2014 sessionId:",
3280
+ sessionId,
3281
+ "connectId:",
3282
+ externalConnectId
3283
+ );
3284
+ this._unwatchSessionState(sessionId);
3285
+ this._signerManager?.invalidate(sessionId);
3286
+ this._cancellers.get(sessionId)?.({
3287
+ code: HardwareErrorCode7.DeviceDisconnected,
3288
+ tag: "DeviceDisconnected",
3289
+ message: "Device disconnected"
3290
+ });
3291
+ this._cancellers.delete(sessionId);
3292
+ this._emit("device-disconnect", { connectId: externalConnectId });
3293
+ }
2389
3294
  // ---------------------------------------------------------------------------
2390
3295
  // IConnector -- Method dispatch
2391
3296
  // ---------------------------------------------------------------------------
2392
3297
  async call(sessionId, method, params) {
2393
3298
  debugLog("[DMK] call:", method, JSON.stringify(params));
3299
+ try {
3300
+ return await this._dispatch(sessionId, method, params);
3301
+ } catch (err) {
3302
+ if (isAppStuckByApdu(err)) {
3303
+ throw Object.assign(new Error("Ledger app is unresponsive"), {
3304
+ code: HardwareErrorCode7.DeviceAppStuck,
3305
+ _tag: ERROR_TAG.DeviceAppStuck,
3306
+ originalError: err
3307
+ });
3308
+ }
3309
+ if (isTransportStuck(err)) {
3310
+ throw Object.assign(new Error("Device communication interrupted, please retry"), {
3311
+ code: HardwareErrorCode7.TransportError,
3312
+ _tag: ERROR_TAG.DeviceTransportStuck,
3313
+ originalError: err
3314
+ });
3315
+ }
3316
+ throw err;
3317
+ }
3318
+ }
3319
+ async _dispatch(sessionId, method, params) {
2394
3320
  const ctx = this._ctxForMethod(method);
2395
3321
  switch (method) {
2396
3322
  // EVM
@@ -2530,14 +3456,13 @@ var LedgerConnectorBase = class {
2530
3456
  }
2531
3457
  /**
2532
3458
  * Replace an old session with a new one after app switch.
2533
- * Updates the connectId→path mapping so subsequent calls() use the new session,
2534
- * and emits device-connect so the adapter updates its _sessions Map.
3459
+ * Emits device-connect so the adapter updates its _sessions Map.
2535
3460
  */
2536
3461
  _replaceSession(oldSessionId, _newSessionId) {
2537
3462
  const dm = this._deviceManager;
2538
3463
  if (!dm) return;
2539
3464
  const oldDeviceId = dm.getDeviceId(oldSessionId);
2540
- const connectId = oldDeviceId ? this._pathToConnectId.get(oldDeviceId) : void 0;
3465
+ const connectId = oldDeviceId;
2541
3466
  this._signerManager?.invalidate(oldSessionId);
2542
3467
  if (connectId) {
2543
3468
  this._emit("device-connect", {
@@ -2550,13 +3475,18 @@ var LedgerConnectorBase = class {
2550
3475
  }
2551
3476
  }
2552
3477
  /**
2553
- * Light reset: clear signer/session state but keep DMK, BLE scan, and ID mapping alive.
3478
+ * Light reset: clear signer/session state but keep DMK alive.
2554
3479
  * Used by connect() retry — we want to re-discover with the same transport.
3480
+ *
3481
+ * Note: drops the device manager but tears down its RxJS subs first via
3482
+ * disposeKeepingDmk(). Bare null-assignment would leak _listenSub /
3483
+ * _discoverySub and double-scan after the next _initManagers().
2555
3484
  */
2556
3485
  _resetSignersAndSessions() {
2557
3486
  debugLog("[DMK] _resetSignersAndSessions called");
2558
3487
  this._signerManager?.clearAll();
2559
3488
  this._signerManager = null;
3489
+ this._deviceManager?.disposeKeepingDmk();
2560
3490
  this._deviceManager = null;
2561
3491
  }
2562
3492
  _resetAll() {
@@ -2568,13 +3498,18 @@ var LedgerConnectorBase = class {
2568
3498
  }
2569
3499
  }
2570
3500
  this._cancellers.clear();
3501
+ for (const sub of this._sessionStateSubs.values()) {
3502
+ try {
3503
+ sub.unsubscribe();
3504
+ } catch {
3505
+ }
3506
+ }
3507
+ this._sessionStateSubs.clear();
2571
3508
  this._signerManager?.clearAll();
2572
3509
  this._deviceManager?.dispose();
2573
3510
  this._deviceManager = null;
2574
3511
  this._signerManager = null;
2575
3512
  this._dmk = null;
2576
- this._connectIdToPath.clear();
2577
- this._pathToConnectId.clear();
2578
3513
  }
2579
3514
  // ---------------------------------------------------------------------------
2580
3515
  // Private -- Events
@@ -2608,15 +3543,21 @@ var LedgerConnectorBase = class {
2608
3543
  };
2609
3544
  }
2610
3545
  _wrapError(err, opts) {
2611
- const mapped = mapLedgerError(err, opts);
2612
- const error = new Error(mapped.message);
2613
3546
  const src = err && typeof err === "object" ? err : {};
3547
+ const hasSerializedCode = typeof src.code === "number" && HARDWARE_ERROR_CODE_VALUES.has(src.code);
3548
+ const mapped = hasSerializedCode ? {
3549
+ code: src.code,
3550
+ message: typeof src.message === "string" ? src.message : "Unknown Ledger error",
3551
+ appName: typeof src.appName === "string" ? src.appName : opts?.defaultAppName
3552
+ } : mapLedgerError(err, opts);
3553
+ const error = new Error(mapped.message);
2614
3554
  Object.assign(error, {
2615
3555
  code: mapped.code,
2616
3556
  appName: mapped.appName,
2617
3557
  _tag: src._tag,
2618
3558
  errorCode: src.errorCode,
2619
- _lastStep: src._lastStep
3559
+ _lastStep: src._lastStep,
3560
+ _deviceActionSteps: src._deviceActionSteps
2620
3561
  });
2621
3562
  Object.defineProperty(error, "originalError", {
2622
3563
  value: err,
@@ -2627,13 +3568,6 @@ var LedgerConnectorBase = class {
2627
3568
  return error;
2628
3569
  }
2629
3570
  };
2630
-
2631
- // src/utils/bleIdentity.ts
2632
- function extractBleHexId(name) {
2633
- if (!name) return void 0;
2634
- const match = name.match(/\b([0-9A-Fa-f]{4})$/);
2635
- return match ? match[1].toUpperCase() : void 0;
2636
- }
2637
3571
  export {
2638
3572
  AppManager,
2639
3573
  DmkTransport,
@@ -2644,12 +3578,15 @@ export {
2644
3578
  SignerEth,
2645
3579
  SignerManager,
2646
3580
  SignerSol,
3581
+ debugLog,
2647
3582
  deviceActionToPromise,
2648
- extractBleHexId,
2649
- isDebugEnabled,
2650
3583
  isDeviceLockedError,
3584
+ isLedgerBleConnectionType,
3585
+ isLedgerBleDescriptor,
3586
+ isLedgerDmkBleTransport,
2651
3587
  ledgerFailure,
2652
3588
  mapLedgerError,
2653
- setDebugEnabled
3589
+ offSdkEvent,
3590
+ onSdkEvent
2654
3591
  };
2655
3592
  //# sourceMappingURL=index.mjs.map