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

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