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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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,26 @@ 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
+ function ledgerFailure(code, error, appName, tag, params) {
18
20
  const payload = { error, code };
19
21
  if (appName !== void 0) payload.appName = appName;
22
+ if (tag !== void 0) payload._tag = tag;
23
+ if (params !== void 0) payload.params = params;
20
24
  return { success: false, payload };
21
25
  }
22
26
  var LOCKED_ERROR_CODES = /* @__PURE__ */ new Set(["5515", "21781", "6982", "27010", "5303", "21251"]);
23
27
  var USER_REJECTED_CODES = /* @__PURE__ */ new Set(["6985", "27013"]);
24
28
  var WRONG_APP_CODES = /* @__PURE__ */ new Set(["6e00", "28160", "6d00", "27904", "6a83", "27267"]);
25
29
  var APP_NOT_INSTALLED_CODES = /* @__PURE__ */ new Set(["6807", "26631"]);
26
- var STEP_BLIND_SIGN_FALLBACK = "signer.eth.steps.blindSignTransactionFallback";
30
+ var STEP_BLIND_SIGN_TRANSACTION_FALLBACK = "signer.eth.steps.blindSignTransactionFallback";
27
31
  function getEthAppErrorCode(err) {
28
32
  if (!err || typeof err !== "object") return null;
29
33
  const e = err;
30
- if (e._tag === "EthAppCommandError" && typeof e.errorCode === "string") {
34
+ if (e._tag === ERROR_TAG.EthAppCommand && typeof e.errorCode === "string") {
31
35
  return e.errorCode.toLowerCase();
32
36
  }
33
37
  const orig = e.originalError;
34
- if (orig?._tag === "EthAppCommandError" && typeof orig.errorCode === "string") {
38
+ if (orig?._tag === ERROR_TAG.EthAppCommand && typeof orig.errorCode === "string") {
35
39
  return orig.errorCode.toLowerCase();
36
40
  }
37
41
  return null;
@@ -88,13 +92,8 @@ function mapBtcAppError(hex) {
88
92
  return null;
89
93
  }
90
94
  }
91
- function mapEthAppError(ethCode, lastStep) {
95
+ function mapEthAppErrorCode(ethCode) {
92
96
  switch (ethCode) {
93
- case "6a80":
94
- if (lastStep === STEP_BLIND_SIGN_FALLBACK) {
95
- return HardwareErrorCode.EvmBlindSigningRequired;
96
- }
97
- return null;
98
97
  case "6984":
99
98
  return HardwareErrorCode.EvmClearSignPluginMissing;
100
99
  case "6a84":
@@ -107,16 +106,133 @@ function mapEthAppError(ethCode, lastStep) {
107
106
  return null;
108
107
  }
109
108
  }
109
+ function classifyEthAppError(err) {
110
+ const ethCode = getEthAppErrorCode(err);
111
+ if (!ethCode) return null;
112
+ if (ethCode === "6a80") {
113
+ return hasBlindSignFallbackStep(err) ? HardwareErrorCode.EvmBlindSigningRequired : null;
114
+ }
115
+ return mapEthAppErrorCode(ethCode);
116
+ }
117
+ var ERROR_TAG = {
118
+ // SDK-mint
119
+ DeviceNotAdvertising: "DeviceNotAdvertisingError",
120
+ // BLE scan miss
121
+ DeviceNotInDiscoveryCache: "DeviceNotInDiscoveryCacheError",
122
+ // dm.connect() before enumerate
123
+ BlePairingTimeout: "BlePairingTimeoutError",
124
+ // SMP 30s timeout
125
+ BleGattBondingFailed: "BleGattBondingFailedError",
126
+ // other GATT failure
127
+ UserAborted: "UserAborted",
128
+ DeviceAppStuck: "DeviceAppStuck",
129
+ // chain app wedged (APDU 0x6901)
130
+ DeviceTransportStuck: "DeviceTransportStuck",
131
+ // DMK transport queue wedged
132
+ // DMK-reuse (DMK throws same string; we synthesize too)
133
+ DeviceLocked: "DeviceLockedError",
134
+ DeviceNotRecognized: "DeviceNotRecognizedError",
135
+ DeviceSessionNotFound: "DeviceSessionNotFound",
136
+ OpenAppCommand: "OpenAppCommandError",
137
+ // DMK-only (read only)
138
+ EthAppCommand: "EthAppCommandError",
139
+ UserRefusedOnDevice: "UserRefusedOnDevice",
140
+ WrongAppOpened: "WrongAppOpenedError",
141
+ InvalidStatusWord: "InvalidStatusWordError",
142
+ AlreadySendingApdu: "AlreadySendingApduError",
143
+ UnknownDeviceExchange: "UnknownDeviceExchangeError",
144
+ NoAccessibleDevice: "NoAccessibleDeviceError",
145
+ UnknownDevice: "UnknownDeviceError",
146
+ DeviceSessionRefresher: "DeviceSessionRefresherError",
147
+ DeviceNotInitialized: "DeviceNotInitializedError",
148
+ OpeningConnection: "OpeningConnectionError",
149
+ DeviceDisconnectedBeforeSendingApdu: "DeviceDisconnectedBeforeSendingApdu",
150
+ DeviceDisconnectedWhileSending: "DeviceDisconnectedWhileSendingError",
151
+ Disconnect: "DisconnectError",
152
+ ReconnectionFailed: "ReconnectionFailedError",
153
+ WebHIDDisconnect: "WebHIDDisconnectError",
154
+ // ble-plx surfaces this when GATT notification setup fails — typical
155
+ // outcome when the user doesn't confirm pairing on the device, or the
156
+ // existing bond is invalid. Observed in production after ~30s.
157
+ PairingRefused: "PairingRefusedError"
158
+ };
110
159
  function isDeviceLockedError(err) {
111
160
  if (!err || typeof err !== "object") return false;
112
161
  const e = err;
113
162
  if (e.errorCode != null && LOCKED_ERROR_CODES.has(String(e.errorCode))) return true;
114
163
  if (e.statusCode != null && LOCKED_ERROR_CODES.has(String(e.statusCode))) return true;
115
- if (e._tag === "DeviceLockedError") return true;
164
+ if (e._tag === ERROR_TAG.DeviceLocked) return true;
116
165
  if (e.originalError != null && isDeviceLockedError(e.originalError)) return true;
117
166
  if (e.error != null && e._tag && isDeviceLockedError(e.error)) return true;
118
167
  return false;
119
168
  }
169
+ function isDeviceNotAdvertisingError(err) {
170
+ if (!err || typeof err !== "object") return false;
171
+ return err._tag === ERROR_TAG.DeviceNotAdvertising;
172
+ }
173
+ var PAIRING_FAILURE_TAGS = /* @__PURE__ */ new Set([
174
+ ERROR_TAG.BlePairingTimeout,
175
+ ERROR_TAG.BleGattBondingFailed,
176
+ ERROR_TAG.PairingRefused
177
+ ]);
178
+ function isBlePairingFailureError(err) {
179
+ if (!err || typeof err !== "object") return false;
180
+ const tag = err._tag;
181
+ if (tag && PAIRING_FAILURE_TAGS.has(tag)) return true;
182
+ const orig = err.originalError;
183
+ if (orig != null && isBlePairingFailureError(orig)) return true;
184
+ return false;
185
+ }
186
+ var CONNECTION_LEVEL_TAGS = /* @__PURE__ */ new Set([
187
+ ERROR_TAG.DeviceLocked,
188
+ ERROR_TAG.DeviceNotAdvertising,
189
+ ERROR_TAG.BlePairingTimeout,
190
+ ERROR_TAG.BleGattBondingFailed,
191
+ ERROR_TAG.PairingRefused,
192
+ ERROR_TAG.DeviceNotRecognized,
193
+ ERROR_TAG.NoAccessibleDevice,
194
+ ERROR_TAG.UnknownDevice,
195
+ ERROR_TAG.DeviceSessionNotFound,
196
+ ERROR_TAG.DeviceSessionRefresher,
197
+ ERROR_TAG.DeviceNotInitialized,
198
+ ERROR_TAG.OpeningConnection,
199
+ ERROR_TAG.DeviceDisconnectedBeforeSendingApdu,
200
+ ERROR_TAG.DeviceDisconnectedWhileSending,
201
+ ERROR_TAG.Disconnect,
202
+ ERROR_TAG.ReconnectionFailed,
203
+ ERROR_TAG.WebHIDDisconnect
204
+ ]);
205
+ var DEVICE_NOT_FOUND_TAGS = /* @__PURE__ */ new Set([
206
+ ERROR_TAG.NoAccessibleDevice,
207
+ ERROR_TAG.UnknownDevice,
208
+ ERROR_TAG.DeviceNotInitialized,
209
+ // SDK-internal: dm.connect() called before _discovered was populated.
210
+ // Map to DeviceNotFound so non-BLE-direct paths get a sensible error code.
211
+ ERROR_TAG.DeviceNotInDiscoveryCache
212
+ ]);
213
+ var DEVICE_BUSY_TAGS = /* @__PURE__ */ new Set([ERROR_TAG.OpeningConnection]);
214
+ var DEVICE_DISCONNECTED_TAGS = /* @__PURE__ */ new Set([
215
+ ERROR_TAG.DeviceNotRecognized,
216
+ ERROR_TAG.DeviceSessionNotFound,
217
+ ERROR_TAG.DeviceSessionRefresher,
218
+ ERROR_TAG.DeviceDisconnectedBeforeSendingApdu,
219
+ ERROR_TAG.DeviceDisconnectedWhileSending,
220
+ ERROR_TAG.Disconnect,
221
+ ERROR_TAG.ReconnectionFailed,
222
+ ERROR_TAG.WebHIDDisconnect
223
+ ]);
224
+ function isConnectionLevelError(err) {
225
+ if (!err || typeof err !== "object") return false;
226
+ const tag = err._tag;
227
+ if (tag && CONNECTION_LEVEL_TAGS.has(tag)) return true;
228
+ const e = err;
229
+ if (e.originalError != null && isConnectionLevelError(e.originalError)) return true;
230
+ if (e.error != null && e._tag && isConnectionLevelError(e.error)) return true;
231
+ return false;
232
+ }
233
+ function isKnownConnectionTag(tag) {
234
+ return typeof tag === "string" && CONNECTION_LEVEL_TAGS.has(tag);
235
+ }
120
236
  function hasStatusCode(err, codeSet) {
121
237
  if (!err || typeof err !== "object") return false;
122
238
  const e = err;
@@ -126,10 +242,39 @@ function hasStatusCode(err, codeSet) {
126
242
  if (e.error != null && e._tag && hasStatusCode(e.error, codeSet)) return true;
127
243
  return false;
128
244
  }
245
+ function hasBlindSignFallbackStep(err) {
246
+ if (!err || typeof err !== "object") return false;
247
+ const e = err;
248
+ if (e._lastStep === STEP_BLIND_SIGN_TRANSACTION_FALLBACK) return true;
249
+ if (Array.isArray(e._deviceActionSteps) && e._deviceActionSteps.includes(STEP_BLIND_SIGN_TRANSACTION_FALLBACK)) {
250
+ return true;
251
+ }
252
+ if (e.originalError != null && hasBlindSignFallbackStep(e.originalError)) return true;
253
+ if (e.error != null && e._tag && hasBlindSignFallbackStep(e.error)) return true;
254
+ return false;
255
+ }
256
+ function isDeviceNotFoundError(err) {
257
+ if (!err || typeof err !== "object") return false;
258
+ const tag = err._tag;
259
+ if (tag && DEVICE_NOT_FOUND_TAGS.has(tag)) return true;
260
+ const e = err;
261
+ if (e.originalError != null && isDeviceNotFoundError(e.originalError)) return true;
262
+ if (e.error != null && e._tag && isDeviceNotFoundError(e.error)) return true;
263
+ return false;
264
+ }
265
+ function isDeviceBusyError(err) {
266
+ if (!err || typeof err !== "object") return false;
267
+ const tag = err._tag;
268
+ if (tag && DEVICE_BUSY_TAGS.has(tag)) return true;
269
+ const e = err;
270
+ if (e.originalError != null && isDeviceBusyError(e.originalError)) return true;
271
+ if (e.error != null && e._tag && isDeviceBusyError(e.error)) return true;
272
+ return false;
273
+ }
129
274
  function isUserRejectedError(err) {
130
275
  if (!err || typeof err !== "object") return false;
131
276
  const e = err;
132
- if (e._tag === "UserRefusedOnDevice") return true;
277
+ if (e._tag === ERROR_TAG.UserRefusedOnDevice) return true;
133
278
  if (typeof e.message === "string" && /denied|rejected|refused/i.test(e.message)) return true;
134
279
  if (hasStatusCode(err, USER_REJECTED_CODES)) return true;
135
280
  return false;
@@ -137,7 +282,7 @@ function isUserRejectedError(err) {
137
282
  function isWrongAppError(err) {
138
283
  if (!err || typeof err !== "object") return false;
139
284
  const e = err;
140
- if (e._tag === "WrongAppOpenedError" || e._tag === "InvalidStatusWordError") {
285
+ if (e._tag === ERROR_TAG.WrongAppOpened || e._tag === ERROR_TAG.InvalidStatusWord) {
141
286
  if (hasStatusCode(err, WRONG_APP_CODES)) return true;
142
287
  }
143
288
  if (typeof e.message === "string") {
@@ -151,7 +296,6 @@ function isWrongAppError(err) {
151
296
  function isAppNotInstalledError(err) {
152
297
  if (!err || typeof err !== "object") return false;
153
298
  const e = err;
154
- if (e._tag === "OpenAppCommandError") return true;
155
299
  if (typeof e.message === "string" && /unknown application/i.test(e.message)) return true;
156
300
  if (hasStatusCode(err, APP_NOT_INSTALLED_CODES)) return true;
157
301
  return false;
@@ -159,7 +303,8 @@ function isAppNotInstalledError(err) {
159
303
  function isDeviceDisconnectedError(err) {
160
304
  if (!err || typeof err !== "object") return false;
161
305
  const e = err;
162
- if (e._tag === "DeviceNotRecognizedError" || e._tag === "DeviceSessionNotFound") return true;
306
+ const tag = e._tag;
307
+ if (typeof tag === "string" && DEVICE_DISCONNECTED_TAGS.has(tag)) return true;
163
308
  if (typeof e.message === "string") {
164
309
  const msg = e.message.toLowerCase();
165
310
  if (msg.includes("disconnected") || msg.includes("not found") || msg.includes("no device") || msg.includes("unplugged"))
@@ -179,7 +324,22 @@ function isTimeoutError(err) {
179
324
  if (e.code === HardwareErrorCode.OperationTimeout) return true;
180
325
  return false;
181
326
  }
182
- function mapLedgerError(err) {
327
+ function isAppStuckByApdu(err) {
328
+ if (!err || typeof err !== "object") return false;
329
+ const tag = err._tag;
330
+ if (tag === ERROR_TAG.DeviceAppStuck) return true;
331
+ return extractApduHex(err) === "6901";
332
+ }
333
+ function isTransportStuck(err) {
334
+ if (!err || typeof err !== "object") return false;
335
+ const tag = err._tag;
336
+ return tag === ERROR_TAG.DeviceTransportStuck || // already wrapped form
337
+ tag === ERROR_TAG.UnknownDeviceExchange || tag === ERROR_TAG.AlreadySendingApdu;
338
+ }
339
+ function isStuckAppStateError(err) {
340
+ return isAppStuckByApdu(err) || isTransportStuck(err);
341
+ }
342
+ function mapLedgerError(err, opts) {
183
343
  let originalMessage = "Unknown Ledger error";
184
344
  if (err instanceof Error) {
185
345
  originalMessage = err.message;
@@ -190,60 +350,127 @@ function mapLedgerError(err) {
190
350
  let code;
191
351
  if (isDeviceLockedError(err)) {
192
352
  code = HardwareErrorCode.DeviceLocked;
353
+ } else if (isDeviceNotAdvertisingError(err) || isDeviceNotFoundError(err)) {
354
+ code = HardwareErrorCode.DeviceNotFound;
355
+ } else if (isDeviceBusyError(err)) {
356
+ code = HardwareErrorCode.DeviceBusy;
357
+ } else if (isBlePairingFailureError(err)) {
358
+ code = HardwareErrorCode.BlePairingTimeout;
193
359
  } else if (isUserRejectedError(err)) {
194
360
  code = HardwareErrorCode.UserRejected;
195
361
  } else if (isWrongAppError(err)) {
196
362
  code = HardwareErrorCode.WrongApp;
197
363
  } else if (isAppNotInstalledError(err)) {
198
- code = HardwareErrorCode.AppNotOpen;
364
+ code = HardwareErrorCode.AppNotInstalled;
199
365
  } else if (isDeviceDisconnectedError(err)) {
200
366
  code = HardwareErrorCode.DeviceDisconnected;
201
367
  } else if (isTimeoutError(err)) {
202
368
  code = HardwareErrorCode.OperationTimeout;
203
369
  } 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;
370
+ const ethMapped = classifyEthAppError(err);
207
371
  const apduHex = ethMapped ? null : extractApduHex(err);
208
372
  const chainMapped = apduHex ? mapSolanaAppError(apduHex) ?? mapTronAppError(apduHex) ?? mapBtcAppError(apduHex) : null;
209
373
  code = ethMapped ?? chainMapped ?? HardwareErrorCode.UnknownError;
210
374
  }
211
- const appName = err && typeof err === "object" ? err.appName : void 0;
375
+ const errAppName = err && typeof err === "object" ? err.appName : void 0;
376
+ const appName = errAppName ?? opts?.defaultAppName;
212
377
  return { code, message: enrichErrorMessage(code, originalMessage), appName };
213
378
  }
214
379
 
215
- // src/utils/debugLog.ts
216
- var enabled = false;
217
- function setDebugEnabled(value) {
218
- enabled = value;
380
+ // src/utils/ledgerDmkTransport.ts
381
+ function isLedgerDmkBleTransport(transport) {
382
+ const normalized = transport?.toUpperCase();
383
+ return normalized === "BLE" || normalized?.endsWith("_BLE") === true;
219
384
  }
220
- function isDebugEnabled() {
221
- return enabled;
385
+ function isLedgerBleConnectionType(connectionType) {
386
+ return connectionType === "ble";
222
387
  }
223
- function debugLog(...args) {
224
- if (enabled) {
225
- console.debug(...args);
388
+ function isLedgerBleDescriptor(connectionType, descriptor) {
389
+ return isLedgerBleConnectionType(connectionType) || isLedgerDmkBleTransport(descriptor.transport);
390
+ }
391
+
392
+ // src/utils/sdkEventBus.ts
393
+ var listeners = /* @__PURE__ */ new Set();
394
+ function onSdkEvent(listener) {
395
+ listeners.add(listener);
396
+ return () => {
397
+ listeners.delete(listener);
398
+ };
399
+ }
400
+ function offSdkEvent(listener) {
401
+ listeners.delete(listener);
402
+ }
403
+ function emitSdkEvent(event) {
404
+ if (listeners.size === 0) return;
405
+ for (const listener of listeners) {
406
+ try {
407
+ listener(event);
408
+ } catch {
409
+ }
226
410
  }
227
411
  }
228
- function debugError(...args) {
229
- if (enabled) {
230
- console.error(...args);
412
+ function emitLog(level, ...args) {
413
+ if (listeners.size === 0) return;
414
+ const message = args.map((a) => typeof a === "string" ? a : safeStringify(a)).join(" ");
415
+ emitSdkEvent({ type: "log", level, message });
416
+ }
417
+ function safeStringify(value) {
418
+ if (value instanceof Error) {
419
+ return value.stack ? `${value.message}
420
+ ${value.stack}` : value.message;
421
+ }
422
+ try {
423
+ return JSON.stringify(value);
424
+ } catch {
425
+ return String(value);
231
426
  }
232
427
  }
233
428
 
429
+ // src/utils/debugLog.ts
430
+ function debugLog(...args) {
431
+ emitLog("debug", ...args);
432
+ }
433
+ function debugError(...args) {
434
+ emitLog("error", ...args);
435
+ }
436
+
234
437
  // src/adapter/LedgerAdapter.ts
235
438
  function formatDeviceMismatchError(expected, actual) {
236
439
  return `Wrong device: expected ${expected}, got ${actual}`;
237
440
  }
441
+ var BTC_HIGH_INDEX_THRESHOLD = 100;
442
+ function btcAccountIndexFromPath(path) {
443
+ const segments = path.replace(/^m\//, "").split("/");
444
+ if (segments.length < 3) return null;
445
+ const accountSeg = segments[2].replace(/['h]$/i, "");
446
+ const accountIndex = parseInt(accountSeg, 10);
447
+ return Number.isFinite(accountIndex) ? accountIndex : null;
448
+ }
238
449
  var _LedgerAdapter = class _LedgerAdapter {
239
450
  constructor(connector, options) {
240
451
  this.vendor = "ledger";
241
452
  this.emitter = new TypedEventEmitter();
242
- // Device cache: tracks discovered devices from connector events
243
453
  this._discoveredDevices = /* @__PURE__ */ new Map();
244
- // Session tracking: maps connectId -> sessionId
245
454
  this._sessions = /* @__PURE__ */ new Map();
246
455
  this._uiRegistry = new UiRequestRegistry();
456
+ // BTC App rejects account index >= 100 unless display=true. Cached per
457
+ // adapter instance: first 100+ path asks the user once via UI request,
458
+ // subsequent 100+ paths in the same session auto-promote silently.
459
+ // The Ledger device itself still requires a per-call confirmation — that's
460
+ // the Ledger app's safety boundary, not ours to bypass.
461
+ this._btcHighIndexConfirmedThisSession = false;
462
+ // Shared across concurrent callers — only `cancel()` aborts.
463
+ this._doConnectAbortController = null;
464
+ // ---------------------------------------------------------------------------
465
+ // Private helpers
466
+ // ---------------------------------------------------------------------------
467
+ /**
468
+ * Ensure at least one device is connected and return a valid connectId.
469
+ *
470
+ * - If a session already exists for the given connectId, reuse it.
471
+ * - If ANY session exists (Ledger IDs are ephemeral), reuse it.
472
+ * - Otherwise: search → 1 device: auto-connect, multiple: ask user, 0: throw.
473
+ */
247
474
  // Mutex for ensureConnected — prevents concurrent calls from establishing duplicate connections
248
475
  this._connectingPromise = null;
249
476
  // ---------------------------------------------------------------------------
@@ -266,47 +493,31 @@ var _LedgerAdapter = class _LedgerAdapter {
266
493
  payload: { connectId: data.connectId }
267
494
  });
268
495
  };
269
- this.uiRequestHandler = (data) => {
270
- this.handleUiEvent(data);
271
- };
272
- this.uiEventHandler = (data) => {
273
- this.handleUiEvent(data);
496
+ // Forward low-level connector 'ui-event' (the four EConnectorInteraction values)
497
+ // to the public hw.emitter so consumers only need to subscribe in one place
498
+ // (hw.on instead of also reaching into connector.on).
499
+ this.uiEventForwarder = (event) => {
500
+ this.emitter.emit("ui-event", event);
274
501
  };
275
502
  this.connector = connector;
276
503
  this._handleSelectDevice = options?.handleSelectDevice ?? false;
277
- this._jobQueue = new DeviceJobQueue({
278
- emit: (event, data) => this.emitter.emit(event, data),
279
- uiRegistry: this._uiRegistry
280
- });
504
+ this._jobQueue = new DeviceJobQueue();
281
505
  this.registerEventListeners();
282
506
  }
283
- /**
284
- * Classify a method's interruptibility.
285
- * - Signing / typed data / transaction → 'confirm' (user may decide via preemption UI)
286
- * - Read-only queries (getAddress / getPublicKey / getMasterFingerprint) → 'safe'
287
- * (auto-cancels any pending read for the same device)
288
- */
289
- static _getInterruptibility(method) {
290
- if (method.toLowerCase().includes("sign")) return "confirm";
291
- return "safe";
292
- }
293
- // ---------------------------------------------------------------------------
294
507
  // Transport
295
- // ---------------------------------------------------------------------------
296
- // Transport is decided at connector creation time. These methods
297
- // satisfy the IHardwareWallet interface with sensible defaults.
298
508
  get activeTransport() {
299
- return this.connector.connectionType === "ble" ? "ble" : "hid";
509
+ return isLedgerBleConnectionType(this.connector.connectionType) ? "ble" : "hid";
300
510
  }
301
511
  getAvailableTransports() {
302
512
  return this.activeTransport ? [this.activeTransport] : [];
303
513
  }
304
- async switchTransport(_type) {
514
+ // Connector is bound at construction; switching requires a new adapter.
515
+ switchTransport(_type) {
516
+ return Promise.resolve();
305
517
  }
306
- // ---------------------------------------------------------------------------
307
518
  // Lifecycle
308
- // ---------------------------------------------------------------------------
309
- async init(_config) {
519
+ init(_config) {
520
+ return Promise.resolve();
310
521
  }
311
522
  /**
312
523
  * Clear cached device/session state without tearing down the adapter.
@@ -319,6 +530,7 @@ var _LedgerAdapter = class _LedgerAdapter {
319
530
  this._connectingPromise = null;
320
531
  this._uiRegistry.reset();
321
532
  this._jobQueue.clear();
533
+ this._btcHighIndexConfirmedThisSession = false;
322
534
  }
323
535
  async dispose() {
324
536
  this._uiRegistry.reset();
@@ -327,6 +539,7 @@ var _LedgerAdapter = class _LedgerAdapter {
327
539
  this.connector.reset();
328
540
  this._discoveredDevices.clear();
329
541
  this._sessions.clear();
542
+ this._btcHighIndexConfirmedThisSession = false;
330
543
  this.emitter.removeAllListeners();
331
544
  }
332
545
  uiResponse(response) {
@@ -335,10 +548,17 @@ var _LedgerAdapter = class _LedgerAdapter {
335
548
  // ---------------------------------------------------------------------------
336
549
  // Device management
337
550
  // ---------------------------------------------------------------------------
338
- async searchDevices() {
551
+ async searchDevices(options) {
552
+ if (options?.resetSession) {
553
+ this._doConnectAbortController?.abort();
554
+ this._sessions.clear();
555
+ this._connectingPromise = null;
556
+ this._doConnectAbortController = null;
557
+ this._btcHighIndexConfirmedThisSession = false;
558
+ }
339
559
  await this._ensureDevicePermission();
340
560
  const devices = await this.connector.searchDevices();
341
- debugLog("[DMK] adapter.searchDevices raw:", JSON.stringify(devices));
561
+ this._discoveredDevices.clear();
342
562
  for (const d of devices) {
343
563
  if (d.connectId) {
344
564
  this._discoveredDevices.set(d.connectId, this.connectorDeviceToDeviceInfo(d));
@@ -347,11 +567,26 @@ var _LedgerAdapter = class _LedgerAdapter {
347
567
  if (this._discoveredDevices.size === 0) {
348
568
  await this._ensureDevicePermission();
349
569
  }
570
+ debugLog(
571
+ `[LedgerAdapter] searchDevices() return count=${this._discoveredDevices.size} ids=[${[
572
+ ...this._discoveredDevices.keys()
573
+ ].join(",")}]`
574
+ );
350
575
  return Array.from(this._discoveredDevices.values());
351
576
  }
577
+ static _createDeviceBusyError(method) {
578
+ return Object.assign(new Error(`Ledger device is busy while calling ${method}`), {
579
+ code: HardwareErrorCode2.DeviceBusy
580
+ });
581
+ }
352
582
  async connectDevice(connectId) {
353
- await this._ensureDevicePermission(connectId);
354
583
  try {
584
+ if (isLedgerBleConnectionType(this.connector.connectionType) && !connectId) {
585
+ throw Object.assign(new Error("Ledger BLE connectId is required."), {
586
+ code: HardwareErrorCode2.DeviceNotFound
587
+ });
588
+ }
589
+ await this._ensureDevicePermission(connectId);
355
590
  const session = await this.connector.connect(connectId);
356
591
  this._sessions.set(connectId, session.sessionId);
357
592
  if (session.deviceInfo) {
@@ -387,7 +622,6 @@ var _LedgerAdapter = class _LedgerAdapter {
387
622
  // Chain call helper
388
623
  // ---------------------------------------------------------------------------
389
624
  async callChain(connectId, deviceId, chain, method, params, skipFingerprint = false) {
390
- await this._ensureDevicePermission(connectId, deviceId);
391
625
  try {
392
626
  const result = await this.connectorCall(connectId, method, params, {
393
627
  chain,
@@ -399,45 +633,12 @@ var _LedgerAdapter = class _LedgerAdapter {
399
633
  return this.errorToFailure(err);
400
634
  }
401
635
  }
402
- /**
403
- * Batch version of callChain — checks permission once,
404
- * fingerprint is verified on the first call inside connectorCall.
405
- */
406
- async callChainBatch(connectId, deviceId, chain, method, params, onProgress, skipFingerprint = false) {
407
- await this._ensureDevicePermission(connectId, deviceId);
408
- const results = [];
409
- for (let i = 0; i < params.length; i++) {
410
- try {
411
- const result = await this.connectorCall(connectId, method, params[i], {
412
- chain,
413
- deviceId,
414
- // Only verify fingerprint on the first call in the batch
415
- skipFingerprint: skipFingerprint || i > 0
416
- });
417
- results.push(result);
418
- onProgress?.({ index: i, total: params.length });
419
- } catch (err) {
420
- return this.errorToFailure(err);
421
- }
422
- }
423
- return success(results);
424
- }
425
636
  // ---------------------------------------------------------------------------
426
637
  // EVM chain methods
427
638
  // ---------------------------------------------------------------------------
428
639
  evmGetAddress(connectId, deviceId, params) {
429
640
  return this.callChain(connectId, deviceId, "evm", "evmGetAddress", params);
430
641
  }
431
- evmGetAddresses(connectId, deviceId, params, onProgress) {
432
- return this.callChainBatch(
433
- connectId,
434
- deviceId,
435
- "evm",
436
- "evmGetAddress",
437
- params,
438
- onProgress
439
- );
440
- }
441
642
  evmSignTransaction(connectId, deviceId, params) {
442
643
  return this.callChain(connectId, deviceId, "evm", "evmSignTransaction", params);
443
644
  }
@@ -453,16 +654,6 @@ var _LedgerAdapter = class _LedgerAdapter {
453
654
  btcGetAddress(connectId, deviceId, params) {
454
655
  return this.callChain(connectId, deviceId, "btc", "btcGetAddress", params);
455
656
  }
456
- btcGetAddresses(connectId, deviceId, params, onProgress) {
457
- return this.callChainBatch(
458
- connectId,
459
- deviceId,
460
- "btc",
461
- "btcGetAddress",
462
- params,
463
- onProgress
464
- );
465
- }
466
657
  btcGetPublicKey(connectId, deviceId, params) {
467
658
  return this.callChain(connectId, deviceId, "btc", "btcGetPublicKey", params);
468
659
  }
@@ -490,16 +681,6 @@ var _LedgerAdapter = class _LedgerAdapter {
490
681
  solGetAddress(connectId, deviceId, params) {
491
682
  return this.callChain(connectId, deviceId, "sol", "solGetAddress", params);
492
683
  }
493
- solGetAddresses(connectId, deviceId, params, onProgress) {
494
- return this.callChainBatch(
495
- connectId,
496
- deviceId,
497
- "sol",
498
- "solGetAddress",
499
- params,
500
- onProgress
501
- );
502
- }
503
684
  solSignTransaction(connectId, deviceId, params) {
504
685
  return this.callChain(connectId, deviceId, "sol", "solSignTransaction", params);
505
686
  }
@@ -510,38 +691,13 @@ var _LedgerAdapter = class _LedgerAdapter {
510
691
  // TRON chain methods
511
692
  // ---------------------------------------------------------------------------
512
693
  tronGetAddress(connectId, deviceId, params) {
513
- return this.callChain(connectId, deviceId, "tron", "tronGetAddress", params, true);
514
- }
515
- tronGetAddresses(connectId, deviceId, params, onProgress) {
516
- return this.callChainBatch(
517
- connectId,
518
- deviceId,
519
- "tron",
520
- "tronGetAddress",
521
- params,
522
- onProgress,
523
- true
524
- );
694
+ return this.callChain(connectId, deviceId, "tron", "tronGetAddress", params);
525
695
  }
526
696
  tronSignTransaction(connectId, deviceId, params) {
527
- return this.callChain(
528
- connectId,
529
- deviceId,
530
- "tron",
531
- "tronSignTransaction",
532
- params,
533
- true
534
- );
697
+ return this.callChain(connectId, deviceId, "tron", "tronSignTransaction", params);
535
698
  }
536
699
  tronSignMessage(connectId, deviceId, params) {
537
- return this.callChain(
538
- connectId,
539
- deviceId,
540
- "tron",
541
- "tronSignMessage",
542
- params,
543
- true
544
- );
700
+ return this.callChain(connectId, deviceId, "tron", "tronSignMessage", params);
545
701
  }
546
702
  on(event, listener) {
547
703
  this.emitter.on(event, listener);
@@ -550,30 +706,41 @@ var _LedgerAdapter = class _LedgerAdapter {
550
706
  this.emitter.off(event, listener);
551
707
  }
552
708
  cancel(connectId) {
553
- const sessionId = this._sessions.get(connectId) ?? connectId;
554
- this._jobQueue.forceCancelActive(connectId || "__ledger_default__");
555
- void this.connector.cancel(sessionId);
709
+ const userAbortReason = Object.assign(new Error("User aborted operation"), {
710
+ code: HardwareErrorCode2.UserAborted,
711
+ _tag: ERROR_TAG.UserAborted
712
+ });
713
+ this._lastCancelReason = userAbortReason;
714
+ setTimeout(() => {
715
+ if (this._lastCancelReason === userAbortReason) {
716
+ this._lastCancelReason = void 0;
717
+ }
718
+ }, 2e3);
719
+ this._uiRegistry.cancel();
720
+ if (connectId) {
721
+ this._jobQueue.cancelActiveAndPending(connectId, userAbortReason);
722
+ } else {
723
+ this._jobQueue.cancelActiveAndPending(void 0, userAbortReason);
724
+ }
725
+ if (connectId) {
726
+ const sessionId = this._sessions.get(connectId) ?? connectId;
727
+ void this.connector.cancel(sessionId);
728
+ } else {
729
+ for (const sid of this._sessions.values()) void this.connector.cancel(sid);
730
+ }
731
+ if (this._connectingPromise) {
732
+ this._doConnectAbortController?.abort(userAbortReason);
733
+ }
556
734
  }
557
735
  // ---------------------------------------------------------------------------
558
736
  // Chain fingerprint
559
737
  // ---------------------------------------------------------------------------
560
738
  async getChainFingerprint(connectId, deviceId, chain) {
561
- debugLog(
562
- "[LedgerAdapter] getChainFingerprint called, chain:",
563
- chain,
564
- "connectId:",
565
- connectId || "(empty)",
566
- "sessions:",
567
- this._sessions.size
568
- );
569
- await this._ensureDevicePermission(connectId, deviceId);
570
- debugLog("[LedgerAdapter] getChainFingerprint permission ok, computing fingerprint");
571
739
  try {
572
740
  const fingerprint = await this._computeChainFingerprint(
573
741
  chain,
574
- (method, params) => this.connectorCall(connectId, method, params)
742
+ (method, params) => this.connectorCall(connectId, method, params, void 0, deviceId)
575
743
  );
576
- debugLog("[LedgerAdapter] getChainFingerprint result:", fingerprint?.substring(0, 20));
577
744
  return success(fingerprint);
578
745
  } catch (err) {
579
746
  debugError("[LedgerAdapter] getChainFingerprint error:", chain, err);
@@ -644,77 +811,226 @@ var _LedgerAdapter = class _LedgerAdapter {
644
811
  if (signal?.aborted) {
645
812
  _LedgerAdapter._throwIfAborted(signal);
646
813
  }
814
+ const waitPromise = this._uiRegistry.wait(
815
+ UI_REQUEST.REQUEST_DEVICE_CONNECT
816
+ );
647
817
  this.emitter.emit(UI_REQUEST.REQUEST_DEVICE_CONNECT, {
648
818
  type: UI_REQUEST.REQUEST_DEVICE_CONNECT,
649
819
  payload: {
820
+ vendor: "ledger",
821
+ reason: "device-not-found",
650
822
  message: "Please connect and unlock your Ledger device"
651
823
  }
652
824
  });
653
- const waitPromise = this._uiRegistry.wait(
654
- UI_REQUEST.REQUEST_DEVICE_CONNECT
655
- );
656
825
  let payload;
657
- if (signal) {
658
- const onAbort = () => {
659
- this._uiRegistry.cancel(UI_REQUEST.REQUEST_DEVICE_CONNECT);
660
- };
661
- signal.addEventListener("abort", onAbort, { once: true });
662
- try {
826
+ try {
827
+ if (signal) {
828
+ const onAbort = () => {
829
+ this._uiRegistry.cancel(UI_REQUEST.REQUEST_DEVICE_CONNECT);
830
+ };
831
+ signal.addEventListener("abort", onAbort, { once: true });
832
+ try {
833
+ payload = await waitPromise;
834
+ } finally {
835
+ signal.removeEventListener("abort", onAbort);
836
+ }
837
+ } else {
663
838
  payload = await waitPromise;
664
- } finally {
665
- signal.removeEventListener("abort", onAbort);
666
839
  }
667
- } else {
668
- payload = await waitPromise;
840
+ } catch (err) {
841
+ this.emitter.emit(UI_REQUEST.CLOSE_UI_WINDOW, {
842
+ type: UI_REQUEST.CLOSE_UI_WINDOW,
843
+ payload: {}
844
+ });
845
+ throw Object.assign(new Error("User cancelled Ledger connection"), {
846
+ _tag: ERROR_TAG.UserAborted,
847
+ code: HardwareErrorCode2.UserAborted,
848
+ cause: err
849
+ });
669
850
  }
670
851
  if (!payload?.confirmed) {
671
852
  throw Object.assign(new Error("User cancelled Ledger connection"), {
672
- _tag: "DeviceNotRecognizedError"
853
+ _tag: ERROR_TAG.UserAborted,
854
+ code: HardwareErrorCode2.UserAborted
673
855
  });
674
856
  }
857
+ await new Promise((resolve) => {
858
+ setTimeout(resolve, 800);
859
+ });
675
860
  }
676
- async ensureConnected(connectId) {
677
- if (connectId && this._sessions.has(connectId)) {
678
- return connectId;
679
- }
680
- if (this._sessions.size > 0) {
681
- const firstKey = this._sessions.keys().next().value;
682
- return firstKey;
861
+ /**
862
+ * Decide whether a BTC pubkey call needs `showOnDevice=true` because of
863
+ * the BTC App's account-index policy, asking the user once per session.
864
+ *
865
+ * Returns the params to pass through (with `showOnDevice` possibly
866
+ * promoted), or `null` if the user declined.
867
+ */
868
+ async _gateBtcHighIndex(params) {
869
+ const accountIndex = btcAccountIndexFromPath(params.path);
870
+ if (params.showOnDevice) return params;
871
+ if (accountIndex === null || accountIndex < BTC_HIGH_INDEX_THRESHOLD) {
872
+ return params;
683
873
  }
684
- if (this._connectingPromise) {
685
- return this._connectingPromise;
874
+ if (this._btcHighIndexConfirmedThisSession) {
875
+ return { ...params, showOnDevice: true };
686
876
  }
687
- this._connectingPromise = this._doConnect();
877
+ const confirmed = await this._waitForBtcHighIndexConfirm(params.path, accountIndex);
878
+ if (!confirmed) return null;
879
+ this._btcHighIndexConfirmedThisSession = true;
880
+ return { ...params, showOnDevice: true };
881
+ }
882
+ async _waitForBtcHighIndexConfirm(path, accountIndex) {
883
+ const waitPromise = this._uiRegistry.wait(
884
+ UI_REQUEST.REQUEST_BTC_HIGH_INDEX_CONFIRM
885
+ );
886
+ this.emitter.emit(UI_REQUEST.REQUEST_BTC_HIGH_INDEX_CONFIRM, {
887
+ type: UI_REQUEST.REQUEST_BTC_HIGH_INDEX_CONFIRM,
888
+ payload: {
889
+ vendor: "ledger",
890
+ path,
891
+ accountIndex
892
+ }
893
+ });
688
894
  try {
689
- return await this._connectingPromise;
690
- } finally {
691
- this._connectingPromise = null;
895
+ const payload = await waitPromise;
896
+ return !!payload?.confirmed;
897
+ } catch (err) {
898
+ this.emitter.emit(UI_REQUEST.CLOSE_UI_WINDOW, {
899
+ type: UI_REQUEST.CLOSE_UI_WINDOW,
900
+ payload: {}
901
+ });
902
+ return false;
903
+ }
904
+ }
905
+ // Layer 1 entry. Caller signal only races the outer awaiter; the shared
906
+ // `_doConnect` runs under its own internal controller so caller A's cancel
907
+ // doesn't kill caller B's await.
908
+ async ensureConnected(connectId, signal) {
909
+ if (signal.aborted) _LedgerAdapter._throwIfAborted(signal);
910
+ if (isLedgerBleConnectionType(this.connector.connectionType) && !connectId) {
911
+ throw Object.assign(new Error("Ledger BLE connectId is required."), {
912
+ code: HardwareErrorCode2.DeviceNotFound
913
+ });
914
+ }
915
+ if (connectId && this._sessions.has(connectId)) return connectId;
916
+ if (this._sessions.size > 0) {
917
+ return this._sessions.keys().next().value;
918
+ }
919
+ if (!this._connectingPromise) {
920
+ this._doConnectAbortController = new AbortController();
921
+ const innerSignal = this._doConnectAbortController.signal;
922
+ this._connectingPromise = (async () => {
923
+ try {
924
+ return await this._doConnect(innerSignal, connectId);
925
+ } finally {
926
+ this._connectingPromise = null;
927
+ this._doConnectAbortController = null;
928
+ }
929
+ })();
692
930
  }
931
+ return this._abortable(signal, this._connectingPromise);
693
932
  }
694
- async _doConnect() {
695
- for (let attempt = 0; attempt < _LedgerAdapter.MAX_DEVICE_RETRY; attempt++) {
696
- const devices = await this.searchDevices();
933
+ // Layer 1 main loop — the ONLY place in SDK that emits unlock dialog.
934
+ // Bounded by MAX_DOCONNECT_CONFIRMS after N Confirms with no progress,
935
+ // throw DeviceNotFound so the user is kicked out of the loop.
936
+ async _doConnect(internalSignal, targetConnectId) {
937
+ if (isLedgerBleConnectionType(this.connector.connectionType) && targetConnectId) {
938
+ try {
939
+ return await this._connectDeviceOrThrow(targetConnectId);
940
+ } catch (err) {
941
+ if (!isDeviceLockedError(err) && !isDeviceNotAdvertisingError(err) && !isDeviceDisconnectedError(err)) {
942
+ throw err;
943
+ }
944
+ this._discoveredDevices.delete(targetConnectId);
945
+ if (isDeviceDisconnectedError(err)) {
946
+ try {
947
+ this.connector.reset?.();
948
+ } catch {
949
+ }
950
+ }
951
+ }
952
+ }
953
+ let confirms = 0;
954
+ while (!internalSignal.aborted) {
955
+ this.emitter.emit("ui-event", {
956
+ type: EConnectorInteraction.Searching,
957
+ payload: { sessionId: "" }
958
+ });
959
+ let devices = await this.searchDevices();
960
+ if (devices.length === 0) {
961
+ for (let i = 0; i < 3 && !internalSignal.aborted; i += 1) {
962
+ await new Promise((resolve) => {
963
+ setTimeout(resolve, 1e3);
964
+ });
965
+ devices = await this.searchDevices();
966
+ if (devices.length > 0) break;
967
+ }
968
+ }
697
969
  if (devices.length > 0) {
698
- return this._connectFirstOrSelect(devices);
970
+ try {
971
+ return await this._connectFirstOrSelect(devices, targetConnectId);
972
+ } catch (err) {
973
+ if (!isDeviceLockedError(err) && !isDeviceNotAdvertisingError(err) && !isDeviceDisconnectedError(err)) {
974
+ throw err;
975
+ }
976
+ this._discoveredDevices.clear();
977
+ if (isDeviceDisconnectedError(err)) {
978
+ try {
979
+ this.connector.reset?.();
980
+ } catch {
981
+ }
982
+ }
983
+ }
699
984
  }
700
- if (attempt < _LedgerAdapter.MAX_DEVICE_RETRY - 1) {
701
- await this._waitForDeviceConnect();
985
+ if (confirms >= _LedgerAdapter.MAX_DOCONNECT_CONFIRMS) {
986
+ throw Object.assign(
987
+ new Error(
988
+ "Device not connected after multiple attempts. Please ensure your Ledger is awake, unlocked, and in range, then try again."
989
+ ),
990
+ { code: HardwareErrorCode2.DeviceNotFound }
991
+ );
702
992
  }
993
+ await this._waitForDeviceConnect(internalSignal);
994
+ confirms += 1;
703
995
  }
704
- throw Object.assign(
705
- new Error(
706
- "No Ledger device found after multiple attempts. Please connect and unlock your device."
707
- ),
708
- { _tag: "DeviceNotRecognizedError" }
709
- );
996
+ _LedgerAdapter._throwIfAborted(internalSignal);
997
+ throw new Error("_doConnect aborted");
710
998
  }
711
- async _connectFirstOrSelect(devices) {
999
+ async _connectFirstOrSelect(devices, targetConnectId) {
1000
+ if (targetConnectId) {
1001
+ const target = devices.find(
1002
+ (d) => d.connectId === targetConnectId || d.deviceId === targetConnectId
1003
+ );
1004
+ if (!target) {
1005
+ const err = Object.assign(new Error(`Target Ledger unavailable: ${targetConnectId}`), {
1006
+ code: HardwareErrorCode2.DeviceNotFound
1007
+ });
1008
+ if (isLedgerBleConnectionType(this.connector.connectionType)) {
1009
+ err._tag = ERROR_TAG.DeviceNotAdvertising;
1010
+ }
1011
+ throw err;
1012
+ }
1013
+ return this._connectDeviceOrThrow(target.connectId);
1014
+ }
1015
+ if (isLedgerBleConnectionType(this.connector.connectionType)) {
1016
+ throw Object.assign(new Error("Ledger BLE connectId is required."), {
1017
+ code: HardwareErrorCode2.DeviceNotFound
1018
+ });
1019
+ }
712
1020
  const chosenConnectId = devices.length === 1 ? devices[0].connectId : await this._chooseDeviceFromList(devices);
1021
+ return this._connectDeviceOrThrow(chosenConnectId);
1022
+ }
1023
+ async _connectDeviceOrThrow(chosenConnectId) {
713
1024
  const result = await this.connectDevice(chosenConnectId);
714
1025
  if (!result.success) {
715
- throw Object.assign(new Error(result.payload.error), {
716
- _tag: "DeviceNotRecognizedError"
1026
+ const payload = result.payload;
1027
+ const rethrow = Object.assign(new Error(payload.error), {
1028
+ code: payload.code
717
1029
  });
1030
+ if (payload._tag !== void 0) {
1031
+ rethrow._tag = payload._tag;
1032
+ }
1033
+ throw rethrow;
718
1034
  }
719
1035
  return chosenConnectId;
720
1036
  }
@@ -725,18 +1041,35 @@ var _LedgerAdapter = class _LedgerAdapter {
725
1041
  );
726
1042
  return devices[0].connectId;
727
1043
  }
728
- this.emitter.emit(UI_REQUEST.REQUEST_SELECT_DEVICE, {
729
- type: UI_REQUEST.REQUEST_SELECT_DEVICE,
730
- payload: { devices }
731
- });
732
- const response = await this._uiRegistry.wait(
733
- UI_REQUEST.REQUEST_SELECT_DEVICE
734
- );
1044
+ let response;
1045
+ try {
1046
+ const waitPromise = this._uiRegistry.wait(
1047
+ UI_REQUEST.REQUEST_SELECT_DEVICE
1048
+ );
1049
+ this.emitter.emit(UI_REQUEST.REQUEST_SELECT_DEVICE, {
1050
+ type: UI_REQUEST.REQUEST_SELECT_DEVICE,
1051
+ payload: { devices }
1052
+ });
1053
+ response = await waitPromise;
1054
+ } catch (err) {
1055
+ if (err?._tag === UI_REQUEST_PREEMPTED_TAG) {
1056
+ this.emitter.emit(UI_REQUEST.CLOSE_UI_WINDOW, {
1057
+ type: UI_REQUEST.CLOSE_UI_WINDOW,
1058
+ payload: {}
1059
+ });
1060
+ throw Object.assign(new Error("Device selection superseded"), {
1061
+ _tag: ERROR_TAG.UserAborted,
1062
+ code: HardwareErrorCode2.UserAborted,
1063
+ cause: err
1064
+ });
1065
+ }
1066
+ throw err;
1067
+ }
735
1068
  const chosen = devices.find((d) => d.connectId === response?.sdkConnectId);
736
1069
  if (!chosen) {
737
1070
  throw Object.assign(
738
1071
  new Error(`Selected sdkConnectId '${response?.sdkConnectId}' not in discovered list`),
739
- { _tag: "DeviceNotRecognizedError" }
1072
+ { _tag: ERROR_TAG.DeviceNotRecognized }
740
1073
  );
741
1074
  }
742
1075
  return chosen.connectId;
@@ -749,30 +1082,31 @@ var _LedgerAdapter = class _LedgerAdapter {
749
1082
  * 3. Calls connector.call()
750
1083
  * 4. On disconnect error: clears stale session, re-connects, retries once
751
1084
  */
752
- async connectorCall(connectId, method, params, fingerprint) {
1085
+ async connectorCall(connectId, method, params, fingerprint, permissionDeviceId) {
753
1086
  debugLog("[LedgerAdapter] connectorCall:", method, "connectId:", connectId || "(empty)");
754
1087
  const queueKey = connectId || "__ledger_default__";
755
- const interruptibility = _LedgerAdapter._getInterruptibility(method);
756
1088
  return this._jobQueue.enqueue(
757
1089
  queueKey,
758
- async (signal) => this._runConnectorCall(connectId, method, params, signal, fingerprint),
759
- { interruptibility, label: method }
1090
+ async (signal) => this._runConnectorCall(connectId, method, params, signal, fingerprint, permissionDeviceId),
1091
+ {
1092
+ label: method,
1093
+ rejectIfBusy: true,
1094
+ busyError: _LedgerAdapter._createDeviceBusyError(method)
1095
+ }
760
1096
  );
761
1097
  }
762
1098
  /**
763
- * Race a promise against an abort signal. If the signal fires, rejects with the
764
- * signal's reason (or a generic Error). The underlying connector.call() cannot
765
- * actually be cancelled on Ledger DMK, but the caller gets the abort immediately.
1099
+ * Race a promise against an abort signal. On abort, rejects with
1100
+ * signal.reason instance _lastCancelReason → generic Error('Aborted').
766
1101
  */
767
- static _abortable(signal, promise) {
1102
+ _abortable(signal, promise) {
1103
+ const getAbortReason = () => signal.reason ?? this._lastCancelReason ?? new Error("Aborted");
768
1104
  if (signal.aborted) {
769
- return Promise.reject(
770
- signal.reason ?? new Error("Aborted")
771
- );
1105
+ return Promise.reject(getAbortReason());
772
1106
  }
773
1107
  return new Promise((resolve, reject) => {
774
1108
  const onAbort = () => {
775
- reject(signal.reason ?? new Error("Aborted"));
1109
+ reject(getAbortReason());
776
1110
  };
777
1111
  signal.addEventListener("abort", onAbort, { once: true });
778
1112
  promise.then(
@@ -794,40 +1128,49 @@ var _LedgerAdapter = class _LedgerAdapter {
794
1128
  }
795
1129
  }
796
1130
  /** Actual work done under the job queue — connection, fingerprint, call, and recovery. */
797
- async _runConnectorCall(connectId, method, params, signal, fingerprint) {
1131
+ async _runConnectorCall(connectId, method, params, signal, fingerprint, permissionDeviceId) {
798
1132
  _LedgerAdapter._throwIfAborted(signal);
799
- const resolvedConnectId = await _LedgerAdapter._abortable(
800
- signal,
801
- this.ensureConnected(connectId)
1133
+ await this._ensureDevicePermission(
1134
+ connectId,
1135
+ permissionDeviceId ?? fingerprint?.deviceId,
1136
+ signal
802
1137
  );
803
1138
  _LedgerAdapter._throwIfAborted(signal);
1139
+ let effectiveParams = params;
1140
+ if (method === "btcGetPublicKey") {
1141
+ const gatedParams = await this._gateBtcHighIndex(params);
1142
+ if (gatedParams === null) {
1143
+ throw Object.assign(new Error("User cancelled BTC high-index confirmation"), {
1144
+ _tag: ERROR_TAG.UserAborted,
1145
+ code: HardwareErrorCode2.UserAborted
1146
+ });
1147
+ }
1148
+ effectiveParams = gatedParams;
1149
+ }
1150
+ const resolvedConnectId = await this.ensureConnected(connectId, signal);
804
1151
  const sessionId = this._sessions.get(resolvedConnectId);
805
- debugLog(
806
- "[LedgerAdapter] connectorCall resolved:",
807
- method,
808
- "resolvedConnectId:",
809
- resolvedConnectId,
810
- "sessionId:",
811
- sessionId
812
- );
813
1152
  if (!sessionId) {
814
1153
  throw Object.assign(new Error("Auto-connect succeeded but no session found"), {
815
- _tag: "DeviceSessionNotFound"
1154
+ _tag: ERROR_TAG.DeviceSessionNotFound
816
1155
  });
817
1156
  }
818
- if (fingerprint && !fingerprint.skipFingerprint && fingerprint.deviceId) {
819
- const fp = await _LedgerAdapter._abortable(
820
- signal,
821
- this._verifyDeviceFingerprintWithSession(sessionId, fingerprint.deviceId, fingerprint.chain)
822
- );
823
- if (!fp.success) {
824
- throw Object.assign(new Error(formatDeviceMismatchError(fp.expected, fp.actual)), {
825
- code: HardwareErrorCode2.DeviceMismatch
826
- });
827
- }
828
- }
829
1157
  try {
830
- return await _LedgerAdapter._abortable(signal, this.connector.call(sessionId, method, params));
1158
+ if (fingerprint && !fingerprint.skipFingerprint && fingerprint.deviceId) {
1159
+ const fp = await this._abortable(
1160
+ signal,
1161
+ this._verifyDeviceFingerprintWithSession(
1162
+ sessionId,
1163
+ fingerprint.deviceId,
1164
+ fingerprint.chain
1165
+ )
1166
+ );
1167
+ if (!fp.success) {
1168
+ throw Object.assign(new Error(formatDeviceMismatchError(fp.expected, fp.actual)), {
1169
+ code: HardwareErrorCode2.DeviceMismatch
1170
+ });
1171
+ }
1172
+ }
1173
+ return await this._abortable(signal, this.connector.call(sessionId, method, effectiveParams));
831
1174
  } catch (err) {
832
1175
  if (signal.aborted) throw err;
833
1176
  const errObj = err;
@@ -837,37 +1180,243 @@ var _LedgerAdapter = class _LedgerAdapter {
837
1180
  errorCode: errObj?.errorCode,
838
1181
  statusCode: errObj?.statusCode,
839
1182
  isDisconnected: isDeviceDisconnectedError(err),
840
- isLocked: isDeviceLockedError(err)
1183
+ isLocked: isDeviceLockedError(err),
1184
+ isNotAdvertising: isDeviceNotAdvertisingError(err),
1185
+ isStuckApp: isStuckAppStateError(err)
841
1186
  });
842
- if (isDeviceDisconnectedError(err)) {
843
- debugLog("[LedgerAdapter] disconnected, retrying with fresh connection...");
844
- this._discoveredDevices.clear();
845
- return this._retryWithFreshConnection(resolvedConnectId, method, params, signal, err);
1187
+ if (isStuckAppStateError(err)) {
1188
+ try {
1189
+ this._sessions.delete(resolvedConnectId);
1190
+ this._discoveredDevices.delete(resolvedConnectId);
1191
+ this.connector.reset?.();
1192
+ } catch {
1193
+ }
1194
+ debugLog(
1195
+ "[LedgerAdapter] stuck-app retry: method=",
1196
+ method,
1197
+ "delayMs=",
1198
+ _LedgerAdapter.STUCK_APP_RETRY_DELAY_MS,
1199
+ "_tag=",
1200
+ err?._tag
1201
+ );
1202
+ try {
1203
+ const retryResult = await this._retryAfterStuckApp(
1204
+ resolvedConnectId,
1205
+ method,
1206
+ effectiveParams,
1207
+ signal,
1208
+ err,
1209
+ fingerprint
1210
+ );
1211
+ debugLog("[LedgerAdapter] stuck-app retry succeeded: method=", method);
1212
+ return retryResult;
1213
+ } catch (retryErr) {
1214
+ if (signal.aborted) throw retryErr;
1215
+ if (isStuckAppStateError(retryErr)) {
1216
+ debugLog("[LedgerAdapter] stuck-app retry exhausted (2nd 6901): method=", method);
1217
+ throw err;
1218
+ }
1219
+ debugLog(
1220
+ "[LedgerAdapter] stuck-app retry threw non-stuck error: method=",
1221
+ method,
1222
+ "retryErrTag=",
1223
+ retryErr?._tag
1224
+ );
1225
+ throw retryErr;
1226
+ }
846
1227
  }
847
- if (isDeviceLockedError(err)) {
848
- await this._waitForDeviceConnect(signal);
849
- _LedgerAdapter._throwIfAborted(signal);
850
- return _LedgerAdapter._abortable(signal, this.connector.call(sessionId, method, params));
1228
+ if (isDeviceLockedError(err) || isDeviceNotAdvertisingError(err) || isDeviceDisconnectedError(err)) {
1229
+ let lastErr = err;
1230
+ for (let attempt = 0; attempt < _LedgerAdapter.MAX_BUSINESS_RETRY_BUDGET; attempt += 1) {
1231
+ if (signal.aborted) throw lastErr;
1232
+ try {
1233
+ this._sessions.delete(resolvedConnectId);
1234
+ this._discoveredDevices.delete(resolvedConnectId);
1235
+ if (isDeviceDisconnectedError(lastErr)) {
1236
+ try {
1237
+ this.connector.reset?.();
1238
+ } catch {
1239
+ }
1240
+ }
1241
+ const retryConnectId = isLedgerBleConnectionType(this.connector.connectionType) ? resolvedConnectId : void 0;
1242
+ const reConnectId = await this.ensureConnected(retryConnectId, 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;
851
1274
  }
852
1275
  if (isTimeoutError(err)) {
853
1276
  debugLog("[LedgerAdapter] timeout, retrying with fresh connection...");
854
1277
  this._discoveredDevices.delete(resolvedConnectId);
855
- 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
+ });
856
1296
  }
857
1297
  throw err;
858
1298
  }
859
1299
  }
860
- /** Clear stale session, reconnect, and retry the call. */
861
- async _retryWithFreshConnection(resolvedConnectId, method, params, signal, originalErr) {
862
- this._sessions.delete(resolvedConnectId);
863
- _LedgerAdapter._throwIfAborted(signal);
864
- const retryConnectId = await this.ensureConnected();
865
- _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 retryTargetConnectId = isLedgerBleConnectionType(this.connector.connectionType) ? resolvedConnectId : void 0;
1311
+ const retryConnectId = await this.ensureConnected(retryTargetConnectId, signal);
1312
+ const retrySessionId = this._sessions.get(retryConnectId);
1313
+ if (!retrySessionId) throw originalErr;
1314
+ if (fingerprint && !fingerprint.skipFingerprint && fingerprint.deviceId) {
1315
+ const fp = await this._abortable(
1316
+ signal,
1317
+ this._verifyDeviceFingerprintWithSession(
1318
+ retrySessionId,
1319
+ fingerprint.deviceId,
1320
+ fingerprint.chain
1321
+ )
1322
+ );
1323
+ if (!fp.success) {
1324
+ throw Object.assign(new Error(formatDeviceMismatchError(fp.expected, fp.actual)), {
1325
+ code: HardwareErrorCode2.DeviceMismatch
1326
+ });
1327
+ }
1328
+ }
1329
+ return this._abortable(signal, this.connector.call(retrySessionId, method, params));
1330
+ }
1331
+ _sleepAbortable(ms, signal) {
1332
+ return new Promise((resolve, reject) => {
1333
+ if (signal.aborted) {
1334
+ reject(signal.reason ?? new Error("Aborted"));
1335
+ return;
1336
+ }
1337
+ const timer = setTimeout(() => {
1338
+ signal.removeEventListener("abort", onAbort);
1339
+ resolve();
1340
+ }, ms);
1341
+ const onAbort = () => {
1342
+ clearTimeout(timer);
1343
+ reject(signal.reason ?? new Error("Aborted"));
1344
+ };
1345
+ signal.addEventListener("abort", onAbort, { once: true });
1346
+ });
1347
+ }
1348
+ /**
1349
+ * Clear stale session, reconnect, and retry the call.
1350
+ *
1351
+ * Timeout recovery starts with a full connector reset. After an APDU
1352
+ * timeout, DMK/transport state may still emit malformed responses; retrying
1353
+ * on the same DMK can poison the next chain switch.
1354
+ */
1355
+ async _retryWithFreshConnection(targetConnectId, method, params, signal, originalErr, fingerprint) {
1356
+ this.connector.reset();
1357
+ this._sessions.clear();
1358
+ this._discoveredDevices.clear();
1359
+ this._connectingPromise = null;
1360
+ const retryConnectId = await this.ensureConnected(targetConnectId, signal);
866
1361
  const retrySessionId = this._sessions.get(retryConnectId);
867
1362
  if (!retrySessionId) {
868
1363
  throw originalErr;
869
1364
  }
870
- return _LedgerAdapter._abortable(signal, this.connector.call(retrySessionId, method, params));
1365
+ try {
1366
+ if (fingerprint && !fingerprint.skipFingerprint && fingerprint.deviceId) {
1367
+ const fp = await this._abortable(
1368
+ signal,
1369
+ this._verifyDeviceFingerprintWithSession(
1370
+ retrySessionId,
1371
+ fingerprint.deviceId,
1372
+ fingerprint.chain
1373
+ )
1374
+ );
1375
+ if (!fp.success) {
1376
+ throw Object.assign(new Error(formatDeviceMismatchError(fp.expected, fp.actual)), {
1377
+ code: HardwareErrorCode2.DeviceMismatch
1378
+ });
1379
+ }
1380
+ }
1381
+ return await this._abortable(signal, this.connector.call(retrySessionId, method, params));
1382
+ } catch (retryErr) {
1383
+ if (signal.aborted) throw retryErr;
1384
+ this.connector.reset();
1385
+ this._sessions.clear();
1386
+ this._discoveredDevices.clear();
1387
+ this._connectingPromise = null;
1388
+ if (!isDeviceDisconnectedError(retryErr) && !isTimeoutError(retryErr)) {
1389
+ throw retryErr;
1390
+ }
1391
+ debugLog(
1392
+ "[LedgerAdapter] fresh-session retry still failed; resetting connector and rebuilding DMK"
1393
+ );
1394
+ this.connector.reset();
1395
+ this._sessions.clear();
1396
+ this._discoveredDevices.clear();
1397
+ this._connectingPromise = null;
1398
+ const finalConnectId = await this.ensureConnected(targetConnectId, signal);
1399
+ const finalSessionId = this._sessions.get(finalConnectId);
1400
+ if (!finalSessionId) {
1401
+ throw originalErr;
1402
+ }
1403
+ if (fingerprint && !fingerprint.skipFingerprint && fingerprint.deviceId) {
1404
+ const fp = await this._abortable(
1405
+ signal,
1406
+ this._verifyDeviceFingerprintWithSession(
1407
+ finalSessionId,
1408
+ fingerprint.deviceId,
1409
+ fingerprint.chain
1410
+ )
1411
+ );
1412
+ if (!fp.success) {
1413
+ throw Object.assign(new Error(formatDeviceMismatchError(fp.expected, fp.actual)), {
1414
+ code: HardwareErrorCode2.DeviceMismatch
1415
+ });
1416
+ }
1417
+ }
1418
+ return this._abortable(signal, this.connector.call(finalSessionId, method, params));
1419
+ }
871
1420
  }
872
1421
  /**
873
1422
  * Ensure OS-level device permission (Bluetooth / USB) before proceeding.
@@ -881,7 +1430,10 @@ var _LedgerAdapter = class _LedgerAdapter {
881
1430
  * - No connectId (searchDevices): environment-level permission
882
1431
  * - With connectId (business methods): device-level permission
883
1432
  */
884
- async _ensureDevicePermission(connectId, deviceId) {
1433
+ async _ensureDevicePermission(connectId, deviceId, signal) {
1434
+ if (signal?.aborted) {
1435
+ _LedgerAdapter._throwIfAborted(signal);
1436
+ }
885
1437
  const transportType = this.activeTransport ?? "hid";
886
1438
  const waitPromise = this._uiRegistry.wait(
887
1439
  UI_REQUEST.REQUEST_DEVICE_PERMISSION,
@@ -891,10 +1443,37 @@ var _LedgerAdapter = class _LedgerAdapter {
891
1443
  type: UI_REQUEST.REQUEST_DEVICE_PERMISSION,
892
1444
  payload: { transportType, connectId, deviceId }
893
1445
  });
894
- const { granted } = await waitPromise;
1446
+ let response;
1447
+ const onAbort = () => {
1448
+ this._uiRegistry.cancel(UI_REQUEST.REQUEST_DEVICE_PERMISSION);
1449
+ };
1450
+ try {
1451
+ signal?.addEventListener("abort", onAbort, { once: true });
1452
+ response = await waitPromise;
1453
+ } catch (err) {
1454
+ if (signal?.aborted) {
1455
+ _LedgerAdapter._throwIfAborted(signal);
1456
+ }
1457
+ if (err?._tag === UI_REQUEST_PREEMPTED_TAG) {
1458
+ this.emitter.emit(UI_REQUEST.CLOSE_UI_WINDOW, {
1459
+ type: UI_REQUEST.CLOSE_UI_WINDOW,
1460
+ payload: {}
1461
+ });
1462
+ throw Object.assign(new Error("Device permission request superseded"), {
1463
+ _tag: ERROR_TAG.UserAborted,
1464
+ code: HardwareErrorCode2.UserAborted,
1465
+ cause: err
1466
+ });
1467
+ }
1468
+ throw err;
1469
+ } finally {
1470
+ signal?.removeEventListener("abort", onAbort);
1471
+ }
1472
+ const { granted, reason, message } = response;
895
1473
  if (!granted) {
896
- throw Object.assign(new Error("Device permission denied"), {
897
- code: HardwareErrorCode2.DevicePermissionDenied
1474
+ throw Object.assign(new Error(message ?? "Device permission denied"), {
1475
+ code: HardwareErrorCode2.DevicePermissionDenied,
1476
+ reason
898
1477
  });
899
1478
  }
900
1479
  }
@@ -904,89 +1483,61 @@ var _LedgerAdapter = class _LedgerAdapter {
904
1483
  */
905
1484
  errorToFailure(err) {
906
1485
  debugError("[LedgerAdapter] error:", err);
1486
+ const tag = err && typeof err === "object" ? err._tag : void 0;
907
1487
  if (err && typeof err === "object" && "code" in err && typeof err.code === "number") {
908
1488
  const e = err;
909
- return ledgerFailure(e.code, e.message ?? "Unknown error", e.appName);
1489
+ const params = e.code === HardwareErrorCode2.DevicePermissionDenied && e.reason ? { permissionDeniedReason: e.reason } : void 0;
1490
+ return ledgerFailure(e.code, e.message ?? "Unknown error", e.appName, tag, params);
910
1491
  }
911
1492
  const mapped = mapLedgerError(err);
912
- return ledgerFailure(mapped.code, mapped.message, mapped.appName);
1493
+ return ledgerFailure(mapped.code, mapped.message, mapped.appName, tag);
913
1494
  }
914
1495
  registerEventListeners() {
915
1496
  this.connector.on("device-connect", this.deviceConnectHandler);
916
1497
  this.connector.on("device-disconnect", this.deviceDisconnectHandler);
917
- this.connector.on("ui-request", this.uiRequestHandler);
918
- this.connector.on("ui-event", this.uiEventHandler);
1498
+ this.connector.on("ui-event", this.uiEventForwarder);
919
1499
  }
920
1500
  unregisterEventListeners() {
921
1501
  this.connector.off("device-connect", this.deviceConnectHandler);
922
1502
  this.connector.off("device-disconnect", this.deviceDisconnectHandler);
923
- this.connector.off("ui-request", this.uiRequestHandler);
924
- this.connector.off("ui-event", this.uiEventHandler);
925
- }
926
- handleUiEvent(event) {
927
- if (!event.type) return;
928
- const payload = event.payload;
929
- const deviceInfo = payload ? this.extractDeviceInfoFromPayload(payload) : this.unknownDevice();
930
- switch (event.type) {
931
- case "ui-request_confirmation":
932
- this.emitter.emit(UI_REQUEST.REQUEST_BUTTON, {
933
- type: UI_REQUEST.REQUEST_BUTTON,
934
- payload: { device: deviceInfo }
935
- });
936
- break;
937
- }
1503
+ this.connector.off("ui-event", this.uiEventForwarder);
938
1504
  }
939
1505
  // ---------------------------------------------------------------------------
940
1506
  // Device info mapping
941
1507
  // ---------------------------------------------------------------------------
942
1508
  connectorDeviceToDeviceInfo(device) {
943
- const isBle = device.connectId && /^[0-9A-Fa-f]{4}$/.test(device.connectId);
944
1509
  return {
945
1510
  vendor: "ledger",
946
1511
  model: device.model ?? "unknown",
1512
+ modelName: device.modelName,
947
1513
  firmwareVersion: "",
948
1514
  deviceId: device.deviceId,
949
1515
  connectId: device.connectId,
950
1516
  label: device.name,
951
- connectionType: isBle ? "ble" : "usb",
1517
+ connectionType: this.connector.connectionType,
1518
+ rssi: device.rssi,
1519
+ isConnectable: device.isConnectable,
1520
+ serialNumber: device.serialNumber,
952
1521
  capabilities: device.capabilities
953
1522
  };
954
1523
  }
955
- extractDeviceInfoFromPayload(payload) {
956
- return {
957
- vendor: "ledger",
958
- model: payload.model ?? "unknown",
959
- firmwareVersion: "",
960
- deviceId: payload.deviceId ?? payload.id ?? "",
961
- connectId: payload.connectId ?? payload.path ?? "",
962
- label: payload.label,
963
- connectionType: "usb"
964
- };
965
- }
966
- unknownDevice() {
967
- return {
968
- vendor: "ledger",
969
- model: "unknown",
970
- firmwareVersion: "",
971
- deviceId: "",
972
- connectId: "",
973
- connectionType: "usb"
974
- };
975
- }
976
1524
  };
977
- // ---------------------------------------------------------------------------
978
- // Private helpers
979
- // ---------------------------------------------------------------------------
980
- /**
981
- * Ensure at least one device is connected and return a valid connectId.
982
- *
983
- * - If a session already exists for the given connectId, reuse it.
984
- * - If ANY session exists (Ledger IDs are ephemeral), reuse it.
985
- * - Otherwise: search 1 device: auto-connect, multiple: ask user, 0: throw.
986
- */
987
- _LedgerAdapter.MAX_DEVICE_RETRY = 3;
1525
+ // Layer 2 retry budget after connection-class error. Each round delegates
1526
+ // to Layer 1 (which owns the unlock prompt). Layer 2 never emits UI itself.
1527
+ _LedgerAdapter.MAX_BUSINESS_RETRY_BUDGET = 3;
1528
+ // Stuck-app (APDU 0x6901) retry pause. Ledger Stax often returns 6901 when
1529
+ // OpenAppCommand lands mid-transition right after CloseApp; a short wait
1530
+ // lets the device finish its UI animation before we retry once.
1531
+ _LedgerAdapter.STUCK_APP_RETRY_DELAY_MS = 500;
1532
+ // Layer 1 confirm budget. After N failed Confirm cycles (user keeps clicking
1533
+ // Confirm but device never shows up / never unlocks), throw DeviceNotFound
1534
+ // instead of looping forever.
1535
+ _LedgerAdapter.MAX_DOCONNECT_CONFIRMS = 3;
988
1536
  var LedgerAdapter = _LedgerAdapter;
989
1537
 
1538
+ // src/connector/LedgerConnectorBase.ts
1539
+ import { HardwareErrorCode as HardwareErrorCode7 } from "@onekeyfe/hwk-adapter-core";
1540
+
990
1541
  // src/device/LedgerDeviceManager.ts
991
1542
  var LedgerDeviceManager = class {
992
1543
  constructor(dmk) {
@@ -1023,7 +1574,9 @@ var LedgerDeviceManager = class {
1023
1574
  if (resolved) return;
1024
1575
  resolved = true;
1025
1576
  this._discovered.clear();
1026
- debugLog("[DMK] enumerate raw devices:", devices.length);
1577
+ debugLog(
1578
+ `[DMK] enumerate count=${devices.length} ids=[${devices.map((d) => d.id).join(",")}]`
1579
+ );
1027
1580
  for (const d of devices) {
1028
1581
  this._discovered.set(d.id, d);
1029
1582
  }
@@ -1033,8 +1586,10 @@ var LedgerDeviceManager = class {
1033
1586
  devices.map((d) => ({
1034
1587
  path: d.id,
1035
1588
  type: d.deviceModel.model,
1589
+ modelName: d.deviceModel.name,
1036
1590
  name: d.name,
1037
- transport: d.transport
1591
+ transport: d.transport,
1592
+ rssi: d.rssi
1038
1593
  }))
1039
1594
  );
1040
1595
  } else {
@@ -1055,8 +1610,10 @@ var LedgerDeviceManager = class {
1055
1610
  devices.map((d) => ({
1056
1611
  path: d.id,
1057
1612
  type: d.deviceModel.model,
1613
+ modelName: d.deviceModel.name,
1058
1614
  name: d.name,
1059
- transport: d.transport
1615
+ transport: d.transport,
1616
+ rssi: d.rssi
1060
1617
  }))
1061
1618
  );
1062
1619
  }
@@ -1080,8 +1637,10 @@ var LedgerDeviceManager = class {
1080
1637
  descriptor: {
1081
1638
  path: d.id,
1082
1639
  type: d.deviceModel.model,
1640
+ modelName: d.deviceModel.name,
1083
1641
  name: d.name,
1084
- transport: d.transport
1642
+ transport: d.transport,
1643
+ rssi: d.rssi
1085
1644
  }
1086
1645
  });
1087
1646
  }
@@ -1100,6 +1659,54 @@ var LedgerDeviceManager = class {
1100
1659
  this._listenSub?.unsubscribe();
1101
1660
  this._listenSub = null;
1102
1661
  }
1662
+ /**
1663
+ * Resolve to the latest snapshot of advertising devices observed within
1664
+ * `timeoutMs`. Rewrites `_discovered` so it tracks the live list.
1665
+ *
1666
+ * `listenToAvailableDevices` is a BehaviorSubject — it emits the cached
1667
+ * list on subscribe and *only* re-emits when the list changes. A stable
1668
+ * list of currently-advertising devices therefore produces only the
1669
+ * subscription frame; treating that frame as "fossil to be discarded"
1670
+ * caused devices to flicker in/out of the UI as searches alternated
1671
+ * between "saw a change frame" and "saw only the cached frame".
1672
+ *
1673
+ * The window still gives a chance for the active scan to update the list
1674
+ * (e.g. drop a peripheral that just stopped broadcasting), but if nothing
1675
+ * changes we trust the snapshot — DMK silence on a stable list means
1676
+ * "everything's still there".
1677
+ */
1678
+ getLiveDevices(timeoutMs = 1500) {
1679
+ debugLog(`[DMK] getLiveDevices() called, timeoutMs=${timeoutMs}`);
1680
+ void this.requestDevice();
1681
+ return new Promise((resolve) => {
1682
+ let done = false;
1683
+ let lastSeen = [];
1684
+ let sub = null;
1685
+ const finish = (devices) => {
1686
+ if (done) return;
1687
+ done = true;
1688
+ sub?.unsubscribe();
1689
+ clearTimeout(timer);
1690
+ this._discovered.clear();
1691
+ for (const d of devices) this._discovered.set(d.id, d);
1692
+ debugLog(
1693
+ `[DMK] getLiveDevices() resolved count=${devices.length} ids=[${devices.map((d) => d.id).join(",")}]`
1694
+ );
1695
+ resolve(devices);
1696
+ };
1697
+ const timer = setTimeout(() => finish(lastSeen), timeoutMs);
1698
+ sub = this._dmk.listenToAvailableDevices({}).subscribe({
1699
+ next: (devices) => {
1700
+ lastSeen = devices;
1701
+ },
1702
+ error: (err) => {
1703
+ debugError("[DMK] getLiveDevices() error:", err);
1704
+ finish(lastSeen);
1705
+ },
1706
+ complete: () => finish(lastSeen)
1707
+ });
1708
+ });
1709
+ }
1103
1710
  requestDevice() {
1104
1711
  if (this._discoverySub) {
1105
1712
  return Promise.resolve();
@@ -1117,11 +1724,33 @@ var LedgerDeviceManager = class {
1117
1724
  });
1118
1725
  return Promise.resolve();
1119
1726
  }
1727
+ /** Lookup a previously-discovered device's display name. */
1728
+ getDeviceName(deviceId) {
1729
+ return this._discovered.get(deviceId)?.name;
1730
+ }
1731
+ /** Lookup minimal model/signal info from a previously-discovered device. */
1732
+ getDiscoveredDeviceInfo(deviceId) {
1733
+ const d = this._discovered.get(deviceId);
1734
+ if (!d) return void 0;
1735
+ return {
1736
+ model: d.deviceModel?.model,
1737
+ modelName: d.deviceModel?.name,
1738
+ name: d.name,
1739
+ rssi: d.rssi
1740
+ };
1741
+ }
1742
+ hasDiscoveredDevice(deviceId) {
1743
+ return this._discovered.has(deviceId);
1744
+ }
1120
1745
  /** Connect to a previously discovered device. Returns sessionId. */
1121
1746
  async connect(deviceId) {
1122
1747
  const device = this._discovered.get(deviceId);
1123
1748
  if (!device) {
1124
- throw new Error(`Device "${deviceId}" not found. Call enumerate() or listen() first.`);
1749
+ const err = new Error(
1750
+ `Device "${deviceId}" not found in discovery cache. Call enumerate() or listen() first.`
1751
+ );
1752
+ err._tag = ERROR_TAG.DeviceNotInDiscoveryCache;
1753
+ throw err;
1125
1754
  }
1126
1755
  const sessionId = await this._dmk.connect({ device });
1127
1756
  this._sessions.set(deviceId, sessionId);
@@ -1160,6 +1789,19 @@ var LedgerDeviceManager = class {
1160
1789
  this._sessionToDevice.clear();
1161
1790
  this._dmk.close();
1162
1791
  }
1792
+ /**
1793
+ * Tear down RxJS subscriptions and local state without closing the DMK.
1794
+ * For light-reset paths that want to drop session state but keep the
1795
+ * underlying transport alive for retry. Call this instead of orphaning
1796
+ * the manager — bare null assignment leaks _listenSub / _discoverySub.
1797
+ */
1798
+ disposeKeepingDmk() {
1799
+ this.stopListening();
1800
+ this.stopDiscovery();
1801
+ this._discovered.clear();
1802
+ this._sessions.clear();
1803
+ this._sessionToDevice.clear();
1804
+ }
1163
1805
  };
1164
1806
 
1165
1807
  // src/signer/SignerManager.ts
@@ -1171,11 +1813,12 @@ import { hexToBytes } from "@onekeyfe/hwk-adapter-core";
1171
1813
 
1172
1814
  // src/signer/deviceActionToPromise.ts
1173
1815
  import { DeviceActionStatus } from "@ledgerhq/device-management-kit";
1174
- var DEFAULT_TIMEOUT_MS = 3e4;
1175
- function deviceActionToPromise(action, onInteraction, timeoutMs = DEFAULT_TIMEOUT_MS, onRegisterCanceller) {
1816
+ var IDLE_WATCHDOG_MS = 65e3;
1817
+ function deviceActionToPromise(action, onInteraction, timeoutMs = IDLE_WATCHDOG_MS, onRegisterCanceller) {
1176
1818
  return new Promise((resolve, reject) => {
1177
1819
  let settled = false;
1178
1820
  let lastStep;
1821
+ const observedSteps = [];
1179
1822
  let sub;
1180
1823
  let timer = null;
1181
1824
  const cancelAction = () => {
@@ -1188,44 +1831,59 @@ function deviceActionToPromise(action, onInteraction, timeoutMs = DEFAULT_TIMEOU
1188
1831
  } catch {
1189
1832
  }
1190
1833
  };
1191
- const resetTimer = () => {
1834
+ const armIdleWatchdog = () => {
1192
1835
  if (timer) clearTimeout(timer);
1193
1836
  if (timeoutMs > 0) {
1194
1837
  timer = setTimeout(() => {
1195
1838
  if (!settled) {
1196
1839
  settled = true;
1197
1840
  cancelAction();
1198
- reject(new Error("Device action timed out \u2014 device may be locked or disconnected"));
1841
+ reject(
1842
+ new Error(
1843
+ "Ledger transport unresponsive: no DMK state change within the watchdog window. The device may have lost connection."
1844
+ )
1845
+ );
1199
1846
  }
1200
1847
  }, timeoutMs);
1201
1848
  }
1202
1849
  };
1203
- resetTimer();
1850
+ armIdleWatchdog();
1204
1851
  if (onRegisterCanceller) {
1205
- onRegisterCanceller(() => {
1206
- if (settled) return;
1852
+ onRegisterCanceller((reason) => {
1853
+ if (settled) {
1854
+ return;
1855
+ }
1207
1856
  settled = true;
1208
1857
  if (timer) clearTimeout(timer);
1209
1858
  cancelAction();
1210
- reject(new Error("Device action cancelled"));
1859
+ const err = new Error(reason?.message ?? "Device action cancelled");
1860
+ if (reason?.code !== void 0) {
1861
+ err.code = reason.code;
1862
+ }
1863
+ if (reason?.tag) {
1864
+ Object.defineProperty(err, "_tag", {
1865
+ value: reason.tag,
1866
+ configurable: true,
1867
+ enumerable: false,
1868
+ writable: true
1869
+ });
1870
+ }
1871
+ reject(err);
1211
1872
  });
1212
1873
  }
1213
- debugLog("[DMK-Observable] subscribing to action.observable...");
1214
1874
  sub = action.observable.subscribe({
1215
1875
  next: (state) => {
1216
- if (settled) return;
1217
- resetTimer();
1876
+ if (settled) {
1877
+ return;
1878
+ }
1879
+ armIdleWatchdog();
1218
1880
  const step = state?.intermediateValue?.step;
1219
- if (step) lastStep = step;
1220
- debugLog(
1221
- "[DMK-Observable] state:",
1222
- JSON.stringify({
1223
- status: state.status,
1224
- intermediateValue: state.status === DeviceActionStatus.Pending ? state.intermediateValue : void 0,
1225
- hasOutput: state.status === DeviceActionStatus.Completed,
1226
- hasError: state.status === DeviceActionStatus.Error
1227
- })
1228
- );
1881
+ if (step) {
1882
+ lastStep = step;
1883
+ if (observedSteps[observedSteps.length - 1] !== step) {
1884
+ observedSteps.push(step);
1885
+ }
1886
+ }
1229
1887
  if (state.status === DeviceActionStatus.Completed) {
1230
1888
  settled = true;
1231
1889
  if (timer) clearTimeout(timer);
@@ -1237,10 +1895,14 @@ function deviceActionToPromise(action, onInteraction, timeoutMs = DEFAULT_TIMEOU
1237
1895
  if (timer) clearTimeout(timer);
1238
1896
  onInteraction?.("interaction-complete");
1239
1897
  sub?.unsubscribe();
1240
- rejectWithLastStep(state.error, lastStep, reject);
1898
+ rejectWithStepContext(state.error, lastStep, observedSteps, reject);
1241
1899
  } else if (state.status === DeviceActionStatus.Pending && onInteraction) {
1242
1900
  const interaction = state.intermediateValue?.requiredUserInteraction;
1243
1901
  if (interaction && interaction !== "none") {
1902
+ if (interaction === "unlock-device" && timer) {
1903
+ clearTimeout(timer);
1904
+ timer = null;
1905
+ }
1244
1906
  onInteraction(String(interaction));
1245
1907
  }
1246
1908
  }
@@ -1250,7 +1912,7 @@ function deviceActionToPromise(action, onInteraction, timeoutMs = DEFAULT_TIMEOU
1250
1912
  settled = true;
1251
1913
  if (timer) clearTimeout(timer);
1252
1914
  sub?.unsubscribe();
1253
- rejectWithLastStep(err, lastStep, reject);
1915
+ rejectWithStepContext(err, lastStep, observedSteps, reject);
1254
1916
  }
1255
1917
  },
1256
1918
  complete: () => {
@@ -1263,15 +1925,25 @@ function deviceActionToPromise(action, onInteraction, timeoutMs = DEFAULT_TIMEOU
1263
1925
  });
1264
1926
  });
1265
1927
  }
1266
- function rejectWithLastStep(err, lastStep, reject) {
1267
- if (err && typeof err === "object" && lastStep) {
1928
+ function rejectWithStepContext(err, lastStep, observedSteps, reject) {
1929
+ if (err && typeof err === "object" && (lastStep || observedSteps.length > 0)) {
1268
1930
  try {
1269
- Object.defineProperty(err, "_lastStep", {
1270
- value: lastStep,
1271
- configurable: true,
1272
- enumerable: false,
1273
- writable: true
1274
- });
1931
+ if (lastStep) {
1932
+ Object.defineProperty(err, "_lastStep", {
1933
+ value: lastStep,
1934
+ configurable: true,
1935
+ enumerable: false,
1936
+ writable: true
1937
+ });
1938
+ }
1939
+ if (observedSteps.length > 0) {
1940
+ Object.defineProperty(err, "_deviceActionSteps", {
1941
+ value: [...observedSteps],
1942
+ configurable: true,
1943
+ enumerable: false,
1944
+ writable: true
1945
+ });
1946
+ }
1275
1947
  } catch {
1276
1948
  }
1277
1949
  }
@@ -1337,7 +2009,8 @@ var SignerManager = class _SignerManager {
1337
2009
  async getOrCreate(sessionId) {
1338
2010
  debugLog("[DMK] SignerManager.getOrCreate:", { sessionId });
1339
2011
  const builder = await this._builderFn({ dmk: this._dmk, sessionId });
1340
- return new SignerEth(builder.build());
2012
+ const builderWithContext = builder.withContextModule ? builder.withContextModule(_SignerManager._createContextModule()) : builder;
2013
+ return new SignerEth(builderWithContext.build());
1341
2014
  }
1342
2015
  // eslint-disable-next-line @typescript-eslint/no-unused-vars, class-methods-use-this
1343
2016
  invalidate(_sessionId) {
@@ -1346,10 +2019,24 @@ var SignerManager = class _SignerManager {
1346
2019
  clearAll() {
1347
2020
  }
1348
2021
  static _defaultBuilder() {
1349
- return (args) => {
1350
- const contextModule = new ContextModuleBuilder({}).removeDefaultLoaders().build();
1351
- return new SignerEthBuilder(args).withContextModule(contextModule);
2022
+ return (args) => new SignerEthBuilder(args);
2023
+ }
2024
+ static _createContextModule() {
2025
+ const contextModule = new ContextModuleBuilder({}).removeDefaultLoaders().build();
2026
+ return _SignerManager.wrapBlindSigningReportNonBlocking(contextModule);
2027
+ }
2028
+ static wrapBlindSigningReportNonBlocking(contextModule) {
2029
+ const report = contextModule.report.bind(contextModule);
2030
+ contextModule.report = async (params) => {
2031
+ try {
2032
+ void report(params).catch((err) => {
2033
+ debugLog("[DMK] ContextModule.report failed:", err);
2034
+ });
2035
+ } catch (err) {
2036
+ debugLog("[DMK] ContextModule.report failed:", err);
2037
+ }
1352
2038
  };
2039
+ return contextModule;
1353
2040
  }
1354
2041
  };
1355
2042
 
@@ -1357,20 +2044,20 @@ var SignerManager = class _SignerManager {
1357
2044
  import { HardwareErrorCode as HardwareErrorCode3, padHex64, stripHex } from "@onekeyfe/hwk-adapter-core";
1358
2045
 
1359
2046
  // src/connector/chains/utils.ts
1360
- import { EConnectorInteraction } from "@onekeyfe/hwk-adapter-core";
2047
+ import { EConnectorInteraction as EConnectorInteraction2 } from "@onekeyfe/hwk-adapter-core";
1361
2048
  function normalizePath(path) {
1362
2049
  return path.startsWith("m/") ? path.slice(2) : path;
1363
2050
  }
1364
2051
  function collapseSignerInteraction(interaction) {
1365
2052
  switch (interaction) {
1366
2053
  case "confirm-open-app":
1367
- return EConnectorInteraction.ConfirmOpenApp;
2054
+ return EConnectorInteraction2.ConfirmOpenApp;
1368
2055
  case "unlock-device":
1369
- return EConnectorInteraction.UnlockDevice;
2056
+ return EConnectorInteraction2.UnlockDevice;
1370
2057
  case "interaction-complete":
1371
- return EConnectorInteraction.InteractionComplete;
2058
+ return EConnectorInteraction2.InteractionComplete;
1372
2059
  default:
1373
- return EConnectorInteraction.ConfirmOnDevice;
2060
+ return EConnectorInteraction2.ConfirmOnDevice;
1374
2061
  }
1375
2062
  }
1376
2063
 
@@ -1459,6 +2146,7 @@ async function _getEthSigner(ctx, sessionId) {
1459
2146
  const signerManager = await ctx.getSignerManager();
1460
2147
  const signer = await signerManager.getOrCreate(sessionId);
1461
2148
  signer.onInteraction = (interaction) => {
2149
+ debugLog("[LedgerConnector] evm.onInteraction:", interaction);
1462
2150
  ctx.emit("ui-event", {
1463
2151
  type: collapseSignerInteraction(interaction),
1464
2152
  payload: { sessionId }
@@ -1659,6 +2347,7 @@ async function btcSignTransaction(ctx, sessionId, params) {
1659
2347
  const path = normalizePath(params.path || "84'/0'/0'");
1660
2348
  const purpose = path.split("/")[0]?.replace(/'/g, "");
1661
2349
  const template = _purposeToTemplate(purpose, DefaultDescriptorTemplate);
2350
+ debugLog("[LedgerConnector] btcSignTransaction wallet:", { path, purpose, template });
1662
2351
  const wallet = new DefaultWallet(path, template);
1663
2352
  const signedTxHex = await btcSigner.signTransaction(wallet, params.psbt);
1664
2353
  return { serializedTx: stripHex2(signedTxHex) };
@@ -1683,8 +2372,10 @@ async function btcSignPsbt(ctx, sessionId, params) {
1683
2372
  const path = normalizePath(params.path || "84'/0'/0'");
1684
2373
  const purpose = path.split("/")[0]?.replace(/'/g, "");
1685
2374
  const template = _purposeToTemplate(purpose, DefaultDescriptorTemplate);
2375
+ debugLog("[LedgerConnector] btcSignPsbt wallet:", { path, purpose, template });
1686
2376
  const wallet = new DefaultWallet(path, template);
1687
2377
  const signatures = await btcSigner.signPsbt(wallet, params.psbt);
2378
+ debugLog("[LedgerConnector] btcSignPsbt signatures received:", signatures.length);
1688
2379
  const signedPsbtHex = _applySignaturesToPsbt(params.psbt, signatures);
1689
2380
  return { signedPsbt: signedPsbtHex };
1690
2381
  } catch (err) {
@@ -1731,6 +2422,7 @@ async function _createBtcSigner(ctx, sessionId) {
1731
2422
  const sdkSigner = new SignerBtcBuilder({ dmk, sessionId }).build();
1732
2423
  const signer = new SignerBtc(sdkSigner);
1733
2424
  signer.onInteraction = (interaction) => {
2425
+ debugLog("[LedgerConnector] btc.onInteraction:", interaction);
1734
2426
  ctx.emit("ui-event", {
1735
2427
  type: collapseSignerInteraction(interaction),
1736
2428
  payload: { sessionId }
@@ -1901,6 +2593,7 @@ async function _createSolSigner(ctx, sessionId) {
1901
2593
  const sdkSigner = new SignerSolanaBuilder({ dmk, sessionId }).withContextModule(contextModule).build();
1902
2594
  const signer = new SignerSol(sdkSigner);
1903
2595
  signer.onInteraction = (interaction) => {
2596
+ debugLog("[LedgerConnector] sol.onInteraction:", interaction);
1904
2597
  ctx.emit("ui-event", {
1905
2598
  type: collapseSignerInteraction(interaction),
1906
2599
  payload: { sessionId }
@@ -1913,11 +2606,11 @@ async function _createSolSigner(ctx, sessionId) {
1913
2606
  }
1914
2607
 
1915
2608
  // src/connector/chains/tron.ts
1916
- import { EConnectorInteraction as EConnectorInteraction3, HardwareErrorCode as HardwareErrorCode5 } from "@onekeyfe/hwk-adapter-core";
2609
+ import { HardwareErrorCode as HardwareErrorCode6 } from "@onekeyfe/hwk-adapter-core";
1917
2610
  import Trx from "@ledgerhq/hw-app-trx";
1918
2611
 
1919
- // src/connector/chains/legacyAppRetry.ts
1920
- import { EConnectorInteraction as EConnectorInteraction2 } from "@onekeyfe/hwk-adapter-core";
2612
+ // src/connector/chains/legacyChainCall.ts
2613
+ import { EConnectorInteraction as EConnectorInteraction3 } from "@onekeyfe/hwk-adapter-core";
1921
2614
 
1922
2615
  // src/app/AppManager.ts
1923
2616
  import {
@@ -1926,6 +2619,7 @@ import {
1926
2619
  OpenAppCommand,
1927
2620
  isSuccessCommandResult
1928
2621
  } from "@ledgerhq/device-management-kit";
2622
+ import { HardwareErrorCode as HardwareErrorCode5 } from "@onekeyfe/hwk-adapter-core";
1929
2623
  var APP_NAME_MAP = {
1930
2624
  ETH: "Ethereum",
1931
2625
  BTC: "Bitcoin",
@@ -1961,9 +2655,16 @@ var AppManager = class {
1961
2655
  * 5. Poll until the device confirms the target app is running.
1962
2656
  */
1963
2657
  /**
1964
- * @param onConfirmOnDevice Called after OpenAppCommand succeeds
1965
- * the device is now showing a confirmation prompt to the user.
1966
- * NOT called if the app is already open or if the app is not installed.
2658
+ * @param onConfirmOnDevice Called BEFORE OpenAppCommand is issued the
2659
+ * device is about to display "Open <app>" on screen and wait for the
2660
+ * user's button press. UI consumers should show their "open app" prompt
2661
+ * in response. NOT called when the target app is already open (no user
2662
+ * interaction needed in that case).
2663
+ *
2664
+ * Important: OpenAppCommand is blocking. It does not resolve until the user
2665
+ * has physically confirmed on the device, so anything that runs AFTER
2666
+ * `await this._openApp(...)` lands AFTER the prompt is already gone.
2667
+ * Hence the callback must fire BEFORE that await.
1967
2668
  */
1968
2669
  async ensureAppOpen(sessionId, targetAppName, onConfirmOnDevice) {
1969
2670
  const currentApp = await this._getCurrentApp(sessionId);
@@ -1974,8 +2675,8 @@ var AppManager = class {
1974
2675
  await this._closeCurrentApp(sessionId);
1975
2676
  await this._waitForApp(sessionId, DASHBOARD_APP_NAME);
1976
2677
  }
1977
- await this._openApp(sessionId, targetAppName);
1978
2678
  onConfirmOnDevice?.();
2679
+ await this._openApp(sessionId, targetAppName);
1979
2680
  await this._waitForApp(sessionId, targetAppName);
1980
2681
  }
1981
2682
  // ---------------------------------------------------------------------------
@@ -1987,9 +2688,34 @@ var AppManager = class {
1987
2688
  command: new GetAppAndVersionCommand()
1988
2689
  });
1989
2690
  if (isSuccessCommandResult(result)) {
2691
+ debugLog("[AppManager] currentApp:", result.data.name);
1990
2692
  return result.data.name;
1991
2693
  }
1992
- throw new Error("Failed to get current app from device");
2694
+ const errResult = result;
2695
+ const dmkErr = errResult.error ?? {};
2696
+ const original = dmkErr.originalError;
2697
+ debugLog(
2698
+ "[AppManager] _getCurrentApp failed sessionId=",
2699
+ sessionId,
2700
+ "tag=",
2701
+ dmkErr._tag,
2702
+ "errorCode=",
2703
+ dmkErr.errorCode,
2704
+ "message=",
2705
+ dmkErr.message,
2706
+ "originalErrorMessage=",
2707
+ original?.message ?? String(original ?? "")
2708
+ );
2709
+ throw Object.assign(
2710
+ new Error(
2711
+ dmkErr.message ?? "Failed to get current app from device"
2712
+ ),
2713
+ {
2714
+ _tag: dmkErr._tag,
2715
+ errorCode: dmkErr.errorCode,
2716
+ originalError: original
2717
+ }
2718
+ );
1993
2719
  }
1994
2720
  async _openApp(sessionId, appName) {
1995
2721
  const result = await this._dmk.sendCommand({
@@ -1998,8 +2724,11 @@ var AppManager = class {
1998
2724
  });
1999
2725
  if (!isSuccessCommandResult(result)) {
2000
2726
  const { statusCode } = result;
2727
+ const hasStatusCode2 = statusCode != null && statusCode !== "";
2728
+ debugLog("[AppManager] openApp failed:", appName, "statusCode:", statusCode);
2001
2729
  throw Object.assign(new Error(`Failed to open "${appName}"`), {
2002
- _tag: "OpenAppCommandError",
2730
+ _tag: ERROR_TAG.OpenAppCommand,
2731
+ code: hasStatusCode2 ? void 0 : HardwareErrorCode5.AppNotInstalled,
2003
2732
  errorCode: String(statusCode ?? ""),
2004
2733
  statusCode,
2005
2734
  appName
@@ -2007,6 +2736,7 @@ var AppManager = class {
2007
2736
  }
2008
2737
  }
2009
2738
  async _closeCurrentApp(sessionId) {
2739
+ debugLog("[AppManager] closeCurrentApp");
2010
2740
  await this._dmk.sendCommand({
2011
2741
  sessionId,
2012
2742
  command: new CloseAppCommand()
@@ -2017,15 +2747,23 @@ var AppManager = class {
2017
2747
  * or throw after `_maxRetries` attempts.
2018
2748
  */
2019
2749
  async _waitForApp(sessionId, expectedAppName) {
2750
+ let lastSeen = "";
2020
2751
  for (let i = 0; i < this._maxRetries; i++) {
2021
2752
  await this._wait();
2022
2753
  const current = await this._getCurrentApp(sessionId);
2754
+ lastSeen = current;
2023
2755
  if (current === expectedAppName) {
2024
2756
  return;
2025
2757
  }
2026
2758
  }
2759
+ debugLog(
2760
+ "[AppManager] waitForApp exhausted: expected=",
2761
+ expectedAppName,
2762
+ "lastSeen=",
2763
+ lastSeen
2764
+ );
2027
2765
  throw new Error(
2028
- `Ledger: failed to open "${expectedAppName}" after ${this._maxRetries} retries`
2766
+ `Ledger: failed to open "${expectedAppName}" after ${this._maxRetries} retries (last seen: ${lastSeen})`
2029
2767
  );
2030
2768
  }
2031
2769
  _isDashboard(appName) {
@@ -2036,46 +2774,92 @@ var AppManager = class {
2036
2774
  }
2037
2775
  };
2038
2776
 
2039
- // src/connector/chains/legacyAppRetry.ts
2777
+ // src/connector/chains/legacyChainCall.ts
2040
2778
  function isLegacyWrongAppError(err, _appName) {
2041
2779
  return isWrongAppError(err);
2042
2780
  }
2043
- async function withLegacyAppRetry(ctx, sessionId, appName, action) {
2044
- try {
2045
- return await action(sessionId);
2046
- } catch (err) {
2781
+ async function withLegacyChainCall(ctx, sessionId, options, action) {
2782
+ const { appName, needsConfirmation } = options;
2783
+ let openAppPromptShown = false;
2784
+ const onAppOpenPrompt = () => {
2785
+ openAppPromptShown = true;
2047
2786
  ctx.emit("ui-event", {
2048
- type: EConnectorInteraction2.InteractionComplete,
2787
+ type: EConnectorInteraction3.ConfirmOpenApp,
2049
2788
  payload: { sessionId }
2050
2789
  });
2051
- if (isLegacyWrongAppError(err, appName)) {
2052
- const dmk = await ctx.getOrCreateDmk();
2053
- const appManager = new AppManager(dmk);
2054
- try {
2055
- await appManager.ensureAppOpen(sessionId, appName, () => {
2056
- ctx.emit("ui-event", {
2057
- type: EConnectorInteraction2.ConfirmOpenApp,
2058
- payload: { sessionId }
2059
- });
2060
- });
2061
- } catch (switchErr) {
2062
- ctx.emit("ui-event", {
2063
- type: EConnectorInteraction2.InteractionComplete,
2064
- payload: { sessionId }
2065
- });
2066
- throw ctx.wrapError(switchErr);
2067
- }
2068
- ctx.clearAllSigners();
2790
+ };
2791
+ const closeOpenAppUiIfShown = () => {
2792
+ if (openAppPromptShown) {
2793
+ ctx.emit("ui-event", {
2794
+ type: EConnectorInteraction3.InteractionComplete,
2795
+ payload: { sessionId }
2796
+ });
2797
+ openAppPromptShown = false;
2798
+ }
2799
+ };
2800
+ try {
2801
+ await _ensureAppOpen(ctx, sessionId, appName, onAppOpenPrompt);
2802
+ } catch (err) {
2803
+ debugLog(
2804
+ "[LegacyChainCall] pre-flight ensureAppOpen failed:",
2805
+ appName,
2806
+ err?.message
2807
+ );
2808
+ closeOpenAppUiIfShown();
2809
+ throw ctx.wrapError(err, { defaultAppName: appName });
2810
+ }
2811
+ const runOnce = async () => {
2812
+ let confirmEmitted = false;
2813
+ if (needsConfirmation) {
2069
2814
  ctx.emit("ui-event", {
2070
- type: EConnectorInteraction2.InteractionComplete,
2815
+ type: EConnectorInteraction3.ConfirmOnDevice,
2071
2816
  payload: { sessionId }
2072
2817
  });
2818
+ confirmEmitted = true;
2819
+ }
2820
+ try {
2073
2821
  return await action(sessionId);
2822
+ } finally {
2823
+ if (confirmEmitted || openAppPromptShown) {
2824
+ ctx.emit("ui-event", {
2825
+ type: EConnectorInteraction3.InteractionComplete,
2826
+ payload: { sessionId }
2827
+ });
2828
+ openAppPromptShown = false;
2829
+ }
2074
2830
  }
2075
- ctx.invalidateSession(sessionId);
2076
- throw ctx.wrapError(err);
2831
+ };
2832
+ try {
2833
+ return await runOnce();
2834
+ } catch (err) {
2835
+ if (!isLegacyWrongAppError(err, appName)) {
2836
+ debugLog("[LegacyChainCall] non-wrong-app failure:", appName, err?.message);
2837
+ ctx.invalidateSession(sessionId);
2838
+ throw ctx.wrapError(err, { defaultAppName: appName });
2839
+ }
2840
+ debugLog("[LegacyChainCall] wrong-app detected, retrying:", appName);
2841
+ try {
2842
+ await _ensureAppOpen(ctx, sessionId, appName, onAppOpenPrompt);
2843
+ } catch (switchErr) {
2844
+ debugLog(
2845
+ "[LegacyChainCall] retry ensureAppOpen failed:",
2846
+ appName,
2847
+ switchErr?.message
2848
+ );
2849
+ closeOpenAppUiIfShown();
2850
+ throw ctx.wrapError(switchErr, { defaultAppName: appName });
2851
+ }
2852
+ ctx.clearAllSigners();
2853
+ const result = await runOnce();
2854
+ debugLog("[LegacyChainCall] retry succeeded:", appName);
2855
+ return result;
2077
2856
  }
2078
2857
  }
2858
+ async function _ensureAppOpen(ctx, sessionId, appName, onPrompt) {
2859
+ const dmk = await ctx.getOrCreateDmk();
2860
+ const appManager = new AppManager(dmk);
2861
+ await appManager.ensureAppOpen(sessionId, appName, onPrompt);
2862
+ }
2079
2863
 
2080
2864
  // src/transport/DmkTransport.ts
2081
2865
  import Transport from "@ledgerhq/hw-transport";
@@ -2105,83 +2889,75 @@ var DmkTransport = class extends Transport {
2105
2889
  // src/connector/chains/tron.ts
2106
2890
  async function tronGetAddress(ctx, sessionId, params) {
2107
2891
  const path = normalizePath(params.path);
2108
- await _ensureTronAppOpen(ctx, sessionId);
2109
- return withLegacyAppRetry(ctx, sessionId, "Tron", async (sid) => {
2110
- const trx = await _createTrx(ctx, sid);
2111
- if (params.showOnDevice) {
2112
- ctx.emit("ui-event", {
2113
- type: EConnectorInteraction3.ConfirmOnDevice,
2114
- payload: { sessionId: sid }
2115
- });
2892
+ const showOnDevice = params.showOnDevice ?? false;
2893
+ return withLegacyChainCall(
2894
+ ctx,
2895
+ sessionId,
2896
+ {
2897
+ appName: "Tron",
2898
+ // Only show "confirm on device" UI when the device is actually going
2899
+ // to display the address for the user to verify.
2900
+ needsConfirmation: showOnDevice
2901
+ },
2902
+ async (sid) => {
2903
+ const trx = await _createTrx(ctx, sid);
2904
+ const result = await trx.getAddress(path, showOnDevice);
2905
+ return { address: result.address, publicKey: result.publicKey, path: params.path };
2116
2906
  }
2117
- const result = await trx.getAddress(path, params.showOnDevice ?? false);
2118
- ctx.emit("ui-event", {
2119
- type: EConnectorInteraction3.InteractionComplete,
2120
- payload: { sessionId: sid }
2121
- });
2122
- return { address: result.address, publicKey: result.publicKey, path: params.path };
2123
- });
2907
+ );
2124
2908
  }
2125
2909
  async function tronSignTransaction(ctx, sessionId, params) {
2126
2910
  if (!params.rawTxHex) {
2127
2911
  throw Object.assign(
2128
2912
  new Error("TRON signing requires a protobuf-encoded raw transaction hex (rawTxHex)."),
2129
- { code: HardwareErrorCode5.InvalidParams }
2913
+ { code: HardwareErrorCode6.InvalidParams }
2130
2914
  );
2131
2915
  }
2132
2916
  const path = normalizePath(params.path);
2133
- await _ensureTronAppOpen(ctx, sessionId);
2134
- return withLegacyAppRetry(ctx, sessionId, "Tron", async (sid) => {
2135
- const trx = await _createTrx(ctx, sid);
2136
- ctx.emit("ui-event", {
2137
- type: EConnectorInteraction3.ConfirmOnDevice,
2138
- payload: { sessionId: sid }
2139
- });
2140
- const signature = await trx.signTransaction(
2141
- path,
2142
- params.rawTxHex,
2143
- params.tokenSignatures ?? []
2144
- );
2145
- ctx.emit("ui-event", {
2146
- type: EConnectorInteraction3.InteractionComplete,
2147
- payload: { sessionId: sid }
2148
- });
2149
- return { signature };
2150
- });
2917
+ return withLegacyChainCall(
2918
+ ctx,
2919
+ sessionId,
2920
+ { appName: "Tron", needsConfirmation: true },
2921
+ async (sid) => {
2922
+ const trx = await _createTrx(ctx, sid);
2923
+ const signature = await trx.signTransaction(
2924
+ path,
2925
+ params.rawTxHex,
2926
+ params.tokenSignatures ?? []
2927
+ );
2928
+ return { signature };
2929
+ }
2930
+ );
2151
2931
  }
2152
2932
  async function tronSignMessage(ctx, sessionId, params) {
2153
2933
  const path = normalizePath(params.path);
2154
- await _ensureTronAppOpen(ctx, sessionId);
2155
- return withLegacyAppRetry(ctx, sessionId, "Tron", async (sid) => {
2156
- const trx = await _createTrx(ctx, sid);
2157
- ctx.emit("ui-event", {
2158
- type: EConnectorInteraction3.ConfirmOnDevice,
2159
- payload: { sessionId: sid }
2160
- });
2161
- const signature = await trx.signPersonalMessage(path, params.messageHex);
2162
- ctx.emit("ui-event", {
2163
- type: EConnectorInteraction3.InteractionComplete,
2164
- payload: { sessionId: sid }
2165
- });
2166
- return { signature };
2167
- });
2934
+ return withLegacyChainCall(
2935
+ ctx,
2936
+ sessionId,
2937
+ { appName: "Tron", needsConfirmation: true },
2938
+ async (sid) => {
2939
+ const trx = await _createTrx(ctx, sid);
2940
+ const signature = await trx.signPersonalMessage(path, params.messageHex);
2941
+ return { signature };
2942
+ }
2943
+ );
2168
2944
  }
2169
2945
  async function _createTrx(ctx, sessionId) {
2170
2946
  const dmk = await ctx.getOrCreateDmk();
2171
2947
  return new Trx(new DmkTransport(dmk, sessionId));
2172
2948
  }
2173
- async function _ensureTronAppOpen(ctx, sessionId) {
2174
- const dmk = await ctx.getOrCreateDmk();
2175
- const appManager = new AppManager(dmk);
2176
- await appManager.ensureAppOpen(sessionId, "Tron", () => {
2177
- ctx.emit("ui-event", {
2178
- type: EConnectorInteraction3.ConfirmOpenApp,
2179
- payload: { sessionId }
2180
- });
2181
- });
2182
- }
2183
2949
 
2184
2950
  // src/connector/LedgerConnectorBase.ts
2951
+ var METHOD_PREFIX_TO_APP_NAME = {
2952
+ evm: "Ethereum",
2953
+ btc: "Bitcoin",
2954
+ sol: "Solana",
2955
+ tron: "Tron"
2956
+ };
2957
+ var HARDWARE_ERROR_CODE_VALUES = new Set(
2958
+ Object.values(HardwareErrorCode7).filter((value) => typeof value === "number")
2959
+ );
2960
+ var BLE_CONNECT_SCAN_TIMEOUT_MS = 1500;
2185
2961
  async function defaultLedgerKitImporter(pkg) {
2186
2962
  switch (pkg) {
2187
2963
  case "@ledgerhq/device-management-kit":
@@ -2205,16 +2981,6 @@ var LedgerConnectorBase = class {
2205
2981
  this._dmk = null;
2206
2982
  this._eventHandlers = /* @__PURE__ */ new Map();
2207
2983
  // ---------------------------------------------------------------------------
2208
- // ConnectId <-> DMK path mapping
2209
- //
2210
- // DMK uses internal paths (BLE MAC, USB UUID) that may change across sessions.
2211
- // _resolveConnectId() maps these to stable external IDs (BLE: "A58F", USB: same).
2212
- // This bidirectional map is the SINGLE SOURCE OF TRUTH for all connectId usage.
2213
- // ---------------------------------------------------------------------------
2214
- this._connectIdToPath = /* @__PURE__ */ new Map();
2215
- // "A58F" -> "D5:75:7D:4B:51:E8"
2216
- this._pathToConnectId = /* @__PURE__ */ new Map();
2217
- // ---------------------------------------------------------------------------
2218
2984
  // Per-session DeviceAction cancellers
2219
2985
  //
2220
2986
  // Each chain handler registers its active DeviceAction's canceller via
@@ -2223,17 +2989,30 @@ var LedgerConnectorBase = class {
2223
2989
  // unsubscribes the observable and releases DMK's IntentQueue slot.
2224
2990
  // ---------------------------------------------------------------------------
2225
2991
  this._cancellers = /* @__PURE__ */ new Map();
2992
+ // ---------------------------------------------------------------------------
2993
+ // Per-session DMK state subscriptions
2994
+ //
2995
+ // DMK only emits `device-disconnect` on the connector when `disconnect()`
2996
+ // is called explicitly. To pick up autonomous disconnects (USB unplug,
2997
+ // BLE drop, device sleep, DMK transport reset) we subscribe to
2998
+ // `_dmk.getDeviceSessionState({sessionId})` per active session and emit
2999
+ // `device-disconnect` ourselves the moment DMK reports the device went
3000
+ // to NOT_CONNECTED. The subscription is torn down on disconnect /
3001
+ // reset / observable completion to avoid leaks.
3002
+ // ---------------------------------------------------------------------------
3003
+ this._sessionStateSubs = /* @__PURE__ */ new Map();
2226
3004
  this._createTransport = createTransport;
2227
3005
  this.connectionType = options?.connectionType ?? "usb";
2228
3006
  this._providedDmk = options?.dmk;
2229
3007
  this._importLedgerKit = options?.importLedgerKit ?? defaultLedgerKitImporter;
3008
+ this._requirePreFlightScan = options?.requirePreFlightScan ?? false;
2230
3009
  if (this._providedDmk) {
2231
3010
  this._initManagers(this._providedDmk);
2232
3011
  }
2233
3012
  this._ctx = {
2234
3013
  emit: (event, data) => this._emit(event, data),
2235
3014
  invalidateSession: (sid) => this._invalidateSession(sid),
2236
- wrapError: (err) => this._wrapError(err),
3015
+ wrapError: (err, opts) => this._wrapError(err, opts),
2237
3016
  getOrCreateDmk: () => this._getOrCreateDmk(),
2238
3017
  getDeviceManager: () => this._getDeviceManager(),
2239
3018
  getSignerManager: () => this._getSignerManager(),
@@ -2244,38 +3023,23 @@ var LedgerConnectorBase = class {
2244
3023
  importLedgerKit: this._importLedgerKit
2245
3024
  };
2246
3025
  }
2247
- // "D5:75:7D:4B:51:E8" -> "A58F"
2248
- /** Register a connectId <-> path mapping from a device descriptor. */
2249
- _registerDeviceId(descriptor) {
2250
- const connectId = this._resolveConnectId(descriptor);
2251
- this._connectIdToPath.set(connectId, descriptor.path);
2252
- this._pathToConnectId.set(descriptor.path, connectId);
2253
- return connectId;
2254
- }
2255
- /** Get DMK path from external connectId. Falls back to connectId itself. */
2256
- _getPathForConnectId(connectId) {
2257
- return this._connectIdToPath.get(connectId) ?? connectId;
2258
- }
2259
- /** Get external connectId from DMK path. Falls back to path itself. */
2260
- _getConnectIdForPath(path) {
2261
- return this._pathToConnectId.get(path) ?? path;
2262
- }
2263
3026
  // ---------------------------------------------------------------------------
2264
3027
  // Protected — hooks for subclasses
2265
3028
  // ---------------------------------------------------------------------------
2266
3029
  /**
2267
3030
  * Resolve the connectId for a discovered device descriptor.
2268
3031
  * Default: use the DMK path (ephemeral UUID).
2269
- * Override in subclasses to extract stable identifiers (e.g. BLE hex ID).
3032
+ * Override in subclasses only when the public connectId differs from the transport path.
2270
3033
  */
2271
3034
  _resolveConnectId(descriptor) {
2272
3035
  return descriptor.path;
2273
3036
  }
2274
- // ---------------------------------------------------------------------------
2275
- // IConnector -- Device discovery
2276
- // ---------------------------------------------------------------------------
2277
- async searchDevices() {
2278
- const dm = await this._getDeviceManager();
3037
+ /**
3038
+ * Authoritative discovery for this transport. Default = enumerate snapshot
3039
+ * (USB/HID). BLE overrides to drive from a live scan window since DMK's
3040
+ * enumerate() cache survives peripherals going offline.
3041
+ */
3042
+ async _discoverDescriptors(dm) {
2279
3043
  let descriptors = await dm.enumerate();
2280
3044
  if (descriptors.length === 0) {
2281
3045
  try {
@@ -2284,137 +3048,313 @@ var LedgerConnectorBase = class {
2284
3048
  }
2285
3049
  descriptors = await dm.enumerate();
2286
3050
  }
2287
- const result = descriptors.map((d) => {
2288
- const connectId = this._registerDeviceId(d);
2289
- return {
2290
- connectId,
2291
- deviceId: d.path,
2292
- name: d.name || d.type || "Ledger",
2293
- model: d.type
2294
- };
2295
- });
3051
+ return descriptors;
3052
+ }
3053
+ // ---------------------------------------------------------------------------
3054
+ // IConnector -- Device discovery
3055
+ // ---------------------------------------------------------------------------
3056
+ async searchDevices() {
3057
+ const dm = await this._getDeviceManager();
3058
+ const descriptors = await this._discoverDescriptors(dm);
3059
+ const resolvedDescriptors = descriptors.map((d) => ({
3060
+ descriptor: d,
3061
+ connectId: this._resolveConnectId(d)
3062
+ }));
3063
+ const result = resolvedDescriptors.map(({ descriptor: d, connectId }) => ({
3064
+ connectId,
3065
+ deviceId: d.path,
3066
+ name: d.name || d.type || "Ledger",
3067
+ model: d.type,
3068
+ modelName: d.modelName,
3069
+ rssi: d.rssi,
3070
+ isConnectable: d.isConnectable,
3071
+ serialNumber: d.serialNumber
3072
+ }));
3073
+ debugLog(
3074
+ `[DMK] connector.searchDevices() return count=${result.length} ids=[${result.map((r) => r.connectId).join(",")}] paths=[${result.map((r) => r.deviceId).join(",")}]`
3075
+ );
2296
3076
  return result;
2297
3077
  }
2298
3078
  // ---------------------------------------------------------------------------
2299
3079
  // IConnector -- Connection
2300
3080
  // ---------------------------------------------------------------------------
2301
3081
  async connect(deviceId) {
2302
- const dm = await this._getDeviceManager();
2303
- await this.searchDevices();
2304
- const dmkPath = deviceId ? this._getPathForConnectId(deviceId) : void 0;
2305
- let targetPath = dmkPath;
3082
+ const callerSuppliedConnectId = Boolean(deviceId);
3083
+ let targetPath = deviceId;
2306
3084
  if (!targetPath) {
2307
- const descriptors = await dm.enumerate();
2308
- if (descriptors.length === 0) {
3085
+ const discovered = await this.searchDevices();
3086
+ if (discovered.length === 0) {
2309
3087
  throw new Error(
2310
- `No Ledger device found. Make sure the device is connected${this.connectionType === "ble" ? " nearby with Bluetooth enabled" : " via USB"} and unlocked.`
3088
+ `No Ledger device found. Make sure the device is connected${isLedgerBleConnectionType(this.connectionType) ? " nearby with Bluetooth enabled" : " via USB"} and unlocked.`
2311
3089
  );
2312
3090
  }
2313
- targetPath = descriptors[0].path;
3091
+ targetPath = discovered[0].deviceId;
2314
3092
  }
2315
- const externalConnectId = this._getConnectIdForPath(targetPath);
3093
+ const externalConnectId = targetPath;
3094
+ const HANG_CEILING_MS = 5 * 6e4;
3095
+ const dmConnectWithObserve = async (dm, path) => {
3096
+ if (!isLedgerBleConnectionType(this.connectionType)) return dm.connect(path);
3097
+ let timeoutId;
3098
+ let timedOut = false;
3099
+ const connectPromise = dm.connect(path);
3100
+ const hangPromise = new Promise((_, reject) => {
3101
+ timeoutId = setTimeout(() => {
3102
+ timedOut = true;
3103
+ const err = new Error(
3104
+ `Ledger BLE connect did not return within ${HANG_CEILING_MS / 6e4}min \u2014 DMK hang fallback.`
3105
+ );
3106
+ err._tag = ERROR_TAG.BlePairingTimeout;
3107
+ err.code = HardwareErrorCode7.BlePairingTimeout;
3108
+ reject(err);
3109
+ }, HANG_CEILING_MS);
3110
+ });
3111
+ try {
3112
+ return await Promise.race([connectPromise, hangPromise]);
3113
+ } catch (err) {
3114
+ const e = err;
3115
+ debugLog("[DMK] dm.connect rejected \u2014 observed:", {
3116
+ _tag: e?._tag,
3117
+ message: e?.message,
3118
+ errorCode: e?.errorCode,
3119
+ statusCode: e?.statusCode,
3120
+ originalTag: e?.originalError?._tag,
3121
+ originalMessage: e?.originalError?.message
3122
+ });
3123
+ if (timedOut) {
3124
+ void connectPromise.then(
3125
+ (sessionId) => dm.disconnect(sessionId).catch(() => void 0),
3126
+ () => void 0
3127
+ );
3128
+ }
3129
+ throw err;
3130
+ } finally {
3131
+ if (timeoutId) clearTimeout(timeoutId);
3132
+ }
3133
+ };
3134
+ const isBleDirectConnect = isLedgerBleConnectionType(this.connectionType) && callerSuppliedConnectId;
3135
+ const throwNotAdvertising = () => {
3136
+ const err = new Error(
3137
+ "Ledger device is not currently advertising. Wake up and unlock the device, keep it nearby, then try again."
3138
+ );
3139
+ err._tag = ERROR_TAG.DeviceNotAdvertising;
3140
+ err.code = HardwareErrorCode7.DeviceNotFound;
3141
+ throw err;
3142
+ };
2316
3143
  const doConnect = async (path) => {
2317
- const sessionId = await dm.connect(path);
3144
+ const dm = await this._getDeviceManager();
3145
+ if (isBleDirectConnect && !dm.hasDiscoveredDevice(path)) {
3146
+ const live = await dm.getLiveDevices(BLE_CONNECT_SCAN_TIMEOUT_MS);
3147
+ if (this._requirePreFlightScan && !live.some((d) => d.id === path)) {
3148
+ throwNotAdvertising();
3149
+ }
3150
+ }
3151
+ let sessionId;
3152
+ try {
3153
+ sessionId = await dmConnectWithObserve(dm, path);
3154
+ } catch (err) {
3155
+ if (isBleDirectConnect && err?._tag === ERROR_TAG.DeviceNotInDiscoveryCache) {
3156
+ throwNotAdvertising();
3157
+ }
3158
+ throw err;
3159
+ }
3160
+ this._watchSessionState(sessionId, externalConnectId);
3161
+ const info = dm.getDiscoveredDeviceInfo(path);
2318
3162
  const session = {
2319
3163
  sessionId,
2320
3164
  deviceInfo: {
2321
3165
  vendor: "ledger",
2322
- model: "unknown",
3166
+ model: info?.model ?? "unknown",
3167
+ modelName: info?.modelName,
2323
3168
  firmwareVersion: "unknown",
2324
3169
  deviceId: path,
2325
3170
  connectId: externalConnectId,
2326
3171
  connectionType: this.connectionType,
3172
+ rssi: info?.rssi,
2327
3173
  capabilities: { persistentDeviceIdentity: false }
2328
3174
  }
2329
3175
  };
3176
+ const name = dm.getDeviceName(path) ?? "Ledger";
2330
3177
  this._emit("device-connect", {
2331
- device: {
2332
- connectId: externalConnectId,
2333
- deviceId: path,
2334
- name: "Ledger"
2335
- }
3178
+ device: { connectId: externalConnectId, deviceId: path, name }
2336
3179
  });
2337
3180
  return session;
2338
3181
  };
2339
3182
  try {
2340
3183
  return await doConnect(targetPath);
2341
- } catch {
3184
+ } catch (err) {
2342
3185
  this._resetSignersAndSessions();
2343
- const dm2 = await this._getDeviceManager();
2344
- await this.searchDevices();
2345
- const retryPath = this._getPathForConnectId(externalConnectId);
2346
- if (!retryPath || retryPath === externalConnectId) {
2347
- const descriptors = await dm2.enumerate();
2348
- if (descriptors.length === 0) {
2349
- throw new Error(
2350
- `No Ledger device found after retry. Make sure the device is connected${this.connectionType === "ble" ? " nearby with Bluetooth enabled" : " via USB"} and unlocked.`
2351
- );
3186
+ if (isLedgerBleConnectionType(this.connectionType)) {
3187
+ const tag = err?._tag;
3188
+ if (isKnownConnectionTag(tag)) {
3189
+ throw err;
2352
3190
  }
2353
- return doConnect(descriptors[0].path);
3191
+ const wrapped = new Error(
3192
+ "Ledger Bluetooth pairing failed. Make sure the device is unlocked and nearby, then try again."
3193
+ );
3194
+ wrapped._tag = ERROR_TAG.BleGattBondingFailed;
3195
+ wrapped.code = HardwareErrorCode7.BlePairingTimeout;
3196
+ wrapped.originalError = err;
3197
+ throw wrapped;
3198
+ }
3199
+ if (err && typeof err === "object" && err._tag) {
3200
+ const taggedError = err instanceof Error ? err : Object.assign(
3201
+ new Error(err.message ?? "Ledger device error"),
3202
+ err
3203
+ );
3204
+ throw taggedError;
2354
3205
  }
2355
- return doConnect(retryPath);
3206
+ throw new Error("Ledger device appears unplugged. Plug in the device and try again.");
2356
3207
  }
2357
3208
  }
2358
3209
  async disconnect(sessionId) {
2359
3210
  if (!this._deviceManager) return;
2360
3211
  const deviceId = this._deviceManager.getDeviceId(sessionId);
2361
3212
  this._signerManager?.invalidate(sessionId);
3213
+ this._unwatchSessionState(sessionId);
2362
3214
  await this._deviceManager.disconnect(sessionId);
2363
3215
  if (deviceId) {
2364
3216
  this._emit("device-disconnect", { connectId: deviceId });
2365
3217
  }
2366
3218
  }
3219
+ /**
3220
+ * Subscribe to DMK's per-session state observable so that any autonomous
3221
+ * disconnect (USB unplug, BLE drop, device sleep, transport reset) is
3222
+ * surfaced as a `device-disconnect` event — without this, the upstream
3223
+ * `LedgerAdapter._sessions` map would hold a dead session entry until
3224
+ * the next call hit `DeviceSessionNotFound`.
3225
+ *
3226
+ * Best-effort: any error subscribing is swallowed so a flaky DMK
3227
+ * doesn't break the connect path.
3228
+ */
3229
+ _watchSessionState(sessionId, externalConnectId) {
3230
+ const dmk = this._dmk;
3231
+ if (!dmk) return;
3232
+ const previous = this._sessionStateSubs.get(sessionId);
3233
+ if (previous) {
3234
+ try {
3235
+ previous.unsubscribe();
3236
+ } catch {
3237
+ }
3238
+ }
3239
+ try {
3240
+ const sub = dmk.getDeviceSessionState({ sessionId }).subscribe({
3241
+ next: (state) => {
3242
+ if (state?.deviceStatus === "NOT CONNECTED") {
3243
+ this._handleAutonomousDisconnect(sessionId, externalConnectId);
3244
+ }
3245
+ },
3246
+ error: () => {
3247
+ this._handleAutonomousDisconnect(sessionId, externalConnectId);
3248
+ },
3249
+ complete: () => {
3250
+ this._handleAutonomousDisconnect(sessionId, externalConnectId);
3251
+ }
3252
+ });
3253
+ this._sessionStateSubs.set(sessionId, sub);
3254
+ } catch (err) {
3255
+ debugLog("[DMK] _watchSessionState subscribe failed:", err);
3256
+ }
3257
+ }
3258
+ _unwatchSessionState(sessionId) {
3259
+ const sub = this._sessionStateSubs.get(sessionId);
3260
+ if (!sub) return;
3261
+ try {
3262
+ sub.unsubscribe();
3263
+ } catch {
3264
+ }
3265
+ this._sessionStateSubs.delete(sessionId);
3266
+ }
3267
+ _handleAutonomousDisconnect(sessionId, externalConnectId) {
3268
+ if (!this._sessionStateSubs.has(sessionId)) return;
3269
+ debugLog(
3270
+ "[DMK] autonomous disconnect detected \u2014 sessionId:",
3271
+ sessionId,
3272
+ "connectId:",
3273
+ externalConnectId
3274
+ );
3275
+ this._unwatchSessionState(sessionId);
3276
+ this._signerManager?.invalidate(sessionId);
3277
+ this._cancellers.get(sessionId)?.({
3278
+ code: HardwareErrorCode7.DeviceDisconnected,
3279
+ tag: "DeviceDisconnected",
3280
+ message: "Device disconnected"
3281
+ });
3282
+ this._cancellers.delete(sessionId);
3283
+ this._emit("device-disconnect", { connectId: externalConnectId });
3284
+ }
2367
3285
  // ---------------------------------------------------------------------------
2368
3286
  // IConnector -- Method dispatch
2369
3287
  // ---------------------------------------------------------------------------
2370
3288
  async call(sessionId, method, params) {
2371
3289
  debugLog("[DMK] call:", method, JSON.stringify(params));
3290
+ try {
3291
+ return await this._dispatch(sessionId, method, params);
3292
+ } catch (err) {
3293
+ if (isAppStuckByApdu(err)) {
3294
+ throw Object.assign(new Error("Ledger app is unresponsive"), {
3295
+ code: HardwareErrorCode7.DeviceAppStuck,
3296
+ _tag: ERROR_TAG.DeviceAppStuck,
3297
+ originalError: err
3298
+ });
3299
+ }
3300
+ if (isTransportStuck(err)) {
3301
+ throw Object.assign(new Error("Device communication interrupted, please retry"), {
3302
+ code: HardwareErrorCode7.TransportError,
3303
+ _tag: ERROR_TAG.DeviceTransportStuck,
3304
+ originalError: err
3305
+ });
3306
+ }
3307
+ throw err;
3308
+ }
3309
+ }
3310
+ async _dispatch(sessionId, method, params) {
3311
+ const ctx = this._ctxForMethod(method);
2372
3312
  switch (method) {
2373
3313
  // EVM
2374
3314
  case "evmGetAddress":
2375
- return evmGetAddress(this._ctx, sessionId, params);
3315
+ return evmGetAddress(ctx, sessionId, params);
2376
3316
  case "evmSignTransaction":
2377
- return evmSignTransaction(this._ctx, sessionId, params);
3317
+ return evmSignTransaction(ctx, sessionId, params);
2378
3318
  case "evmSignMessage":
2379
- return evmSignMessage(this._ctx, sessionId, params);
3319
+ return evmSignMessage(ctx, sessionId, params);
2380
3320
  case "evmSignTypedData":
2381
- return evmSignTypedData(this._ctx, sessionId, params);
3321
+ return evmSignTypedData(ctx, sessionId, params);
2382
3322
  // BTC
2383
3323
  case "btcGetAddress":
2384
- return btcGetAddress(this._ctx, sessionId, params);
3324
+ return btcGetAddress(ctx, sessionId, params);
2385
3325
  case "btcGetPublicKey":
2386
- return btcGetPublicKey(this._ctx, sessionId, params);
3326
+ return btcGetPublicKey(ctx, sessionId, params);
2387
3327
  case "btcSignTransaction":
2388
- return btcSignTransaction(this._ctx, sessionId, params);
3328
+ return btcSignTransaction(ctx, sessionId, params);
2389
3329
  case "btcSignPsbt":
2390
- return btcSignPsbt(this._ctx, sessionId, params);
3330
+ return btcSignPsbt(ctx, sessionId, params);
2391
3331
  case "btcSignMessage":
2392
- return btcSignMessage(this._ctx, sessionId, params);
3332
+ return btcSignMessage(ctx, sessionId, params);
2393
3333
  case "btcGetMasterFingerprint":
2394
3334
  return btcGetMasterFingerprint(
2395
- this._ctx,
3335
+ ctx,
2396
3336
  sessionId,
2397
3337
  params
2398
3338
  );
2399
3339
  // SOL
2400
3340
  case "solGetAddress":
2401
- return solGetAddress(this._ctx, sessionId, params);
3341
+ return solGetAddress(ctx, sessionId, params);
2402
3342
  case "solSignTransaction":
2403
- return solSignTransaction(this._ctx, sessionId, params);
3343
+ return solSignTransaction(ctx, sessionId, params);
2404
3344
  case "solSignMessage":
2405
- return solSignMessage(this._ctx, sessionId, params);
3345
+ return solSignMessage(ctx, sessionId, params);
2406
3346
  // TRON
2407
3347
  case "tronGetAddress":
2408
- return tronGetAddress(this._ctx, sessionId, params);
3348
+ return tronGetAddress(ctx, sessionId, params);
2409
3349
  case "tronSignTransaction":
2410
- return tronSignTransaction(this._ctx, sessionId, params);
3350
+ return tronSignTransaction(ctx, sessionId, params);
2411
3351
  case "tronSignMessage": {
2412
3352
  const p = params;
2413
3353
  const internalParams = {
2414
3354
  path: p.path,
2415
3355
  messageHex: p.messageHex
2416
3356
  };
2417
- return tronSignMessage(this._ctx, sessionId, internalParams);
3357
+ return tronSignMessage(ctx, sessionId, internalParams);
2418
3358
  }
2419
3359
  default:
2420
3360
  throw new Error(`LedgerConnector: unknown method "${method}"`);
@@ -2507,14 +3447,13 @@ var LedgerConnectorBase = class {
2507
3447
  }
2508
3448
  /**
2509
3449
  * Replace an old session with a new one after app switch.
2510
- * Updates the connectId→path mapping so subsequent calls() use the new session,
2511
- * and emits device-connect so the adapter updates its _sessions Map.
3450
+ * Emits device-connect so the adapter updates its _sessions Map.
2512
3451
  */
2513
3452
  _replaceSession(oldSessionId, _newSessionId) {
2514
3453
  const dm = this._deviceManager;
2515
3454
  if (!dm) return;
2516
3455
  const oldDeviceId = dm.getDeviceId(oldSessionId);
2517
- const connectId = oldDeviceId ? this._pathToConnectId.get(oldDeviceId) : void 0;
3456
+ const connectId = oldDeviceId;
2518
3457
  this._signerManager?.invalidate(oldSessionId);
2519
3458
  if (connectId) {
2520
3459
  this._emit("device-connect", {
@@ -2527,13 +3466,18 @@ var LedgerConnectorBase = class {
2527
3466
  }
2528
3467
  }
2529
3468
  /**
2530
- * Light reset: clear signer/session state but keep DMK, BLE scan, and ID mapping alive.
3469
+ * Light reset: clear signer/session state but keep DMK alive.
2531
3470
  * Used by connect() retry — we want to re-discover with the same transport.
3471
+ *
3472
+ * Note: drops the device manager but tears down its RxJS subs first via
3473
+ * disposeKeepingDmk(). Bare null-assignment would leak _listenSub /
3474
+ * _discoverySub and double-scan after the next _initManagers().
2532
3475
  */
2533
3476
  _resetSignersAndSessions() {
2534
3477
  debugLog("[DMK] _resetSignersAndSessions called");
2535
3478
  this._signerManager?.clearAll();
2536
3479
  this._signerManager = null;
3480
+ this._deviceManager?.disposeKeepingDmk();
2537
3481
  this._deviceManager = null;
2538
3482
  }
2539
3483
  _resetAll() {
@@ -2545,13 +3489,18 @@ var LedgerConnectorBase = class {
2545
3489
  }
2546
3490
  }
2547
3491
  this._cancellers.clear();
3492
+ for (const sub of this._sessionStateSubs.values()) {
3493
+ try {
3494
+ sub.unsubscribe();
3495
+ } catch {
3496
+ }
3497
+ }
3498
+ this._sessionStateSubs.clear();
2548
3499
  this._signerManager?.clearAll();
2549
3500
  this._deviceManager?.dispose();
2550
3501
  this._deviceManager = null;
2551
3502
  this._signerManager = null;
2552
3503
  this._dmk = null;
2553
- this._connectIdToPath.clear();
2554
- this._pathToConnectId.clear();
2555
3504
  }
2556
3505
  // ---------------------------------------------------------------------------
2557
3506
  // Private -- Events
@@ -2570,16 +3519,36 @@ var LedgerConnectorBase = class {
2570
3519
  // ---------------------------------------------------------------------------
2571
3520
  // Private -- Error handling
2572
3521
  // ---------------------------------------------------------------------------
2573
- _wrapError(err) {
2574
- const mapped = mapLedgerError(err);
2575
- const error = new Error(mapped.message);
3522
+ /**
3523
+ * Return a per-call ctx with the chain's Ledger app name pre-bound to
3524
+ * wrapError, so chain handlers don't need to repeat `{ defaultAppName: 'X' }`
3525
+ * at every catch site. Falls through unchanged for unknown methods.
3526
+ */
3527
+ _ctxForMethod(method) {
3528
+ const prefix = /^(evm|btc|sol|tron)/.exec(method)?.[1];
3529
+ const defaultAppName = prefix ? METHOD_PREFIX_TO_APP_NAME[prefix] : void 0;
3530
+ if (!defaultAppName) return this._ctx;
3531
+ return {
3532
+ ...this._ctx,
3533
+ wrapError: (err, opts) => this._wrapError(err, { defaultAppName, ...opts })
3534
+ };
3535
+ }
3536
+ _wrapError(err, opts) {
2576
3537
  const src = err && typeof err === "object" ? err : {};
3538
+ const hasSerializedCode = typeof src.code === "number" && HARDWARE_ERROR_CODE_VALUES.has(src.code);
3539
+ const mapped = hasSerializedCode ? {
3540
+ code: src.code,
3541
+ message: typeof src.message === "string" ? src.message : "Unknown Ledger error",
3542
+ appName: typeof src.appName === "string" ? src.appName : opts?.defaultAppName
3543
+ } : mapLedgerError(err, opts);
3544
+ const error = new Error(mapped.message);
2577
3545
  Object.assign(error, {
2578
3546
  code: mapped.code,
2579
3547
  appName: mapped.appName,
2580
3548
  _tag: src._tag,
2581
3549
  errorCode: src.errorCode,
2582
- _lastStep: src._lastStep
3550
+ _lastStep: src._lastStep,
3551
+ _deviceActionSteps: src._deviceActionSteps
2583
3552
  });
2584
3553
  Object.defineProperty(error, "originalError", {
2585
3554
  value: err,
@@ -2590,13 +3559,6 @@ var LedgerConnectorBase = class {
2590
3559
  return error;
2591
3560
  }
2592
3561
  };
2593
-
2594
- // src/utils/bleIdentity.ts
2595
- function extractBleHexId(name) {
2596
- if (!name) return void 0;
2597
- const match = name.match(/\b([0-9A-Fa-f]{4})$/);
2598
- return match ? match[1].toUpperCase() : void 0;
2599
- }
2600
3562
  export {
2601
3563
  AppManager,
2602
3564
  DmkTransport,
@@ -2607,12 +3569,15 @@ export {
2607
3569
  SignerEth,
2608
3570
  SignerManager,
2609
3571
  SignerSol,
3572
+ debugLog,
2610
3573
  deviceActionToPromise,
2611
- extractBleHexId,
2612
- isDebugEnabled,
2613
3574
  isDeviceLockedError,
3575
+ isLedgerBleConnectionType,
3576
+ isLedgerBleDescriptor,
3577
+ isLedgerDmkBleTransport,
2614
3578
  ledgerFailure,
2615
3579
  mapLedgerError,
2616
- setDebugEnabled
3580
+ offSdkEvent,
3581
+ onSdkEvent
2617
3582
  };
2618
3583
  //# sourceMappingURL=index.mjs.map