@onekeyfe/hwk-ledger-adapter 1.1.26-alpha.10

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 ADDED
@@ -0,0 +1,2684 @@
1
+ // src/adapter/LedgerAdapter.ts
2
+ import {
3
+ CHAIN_FINGERPRINT_PATHS,
4
+ DEVICE,
5
+ DeviceJobQueue,
6
+ HardwareErrorCode as HardwareErrorCode2,
7
+ TypedEventEmitter,
8
+ UI_REQUEST,
9
+ UiRequestRegistry,
10
+ deriveDeviceFingerprint,
11
+ failure,
12
+ success
13
+ } from "@onekeyfe/hwk-adapter-core";
14
+
15
+ // src/errors.ts
16
+ import { HardwareErrorCode, enrichErrorMessage } from "@onekeyfe/hwk-adapter-core";
17
+ function ledgerFailure(code, error, appName) {
18
+ const payload = { error, code };
19
+ if (appName !== void 0) payload.appName = appName;
20
+ return { success: false, payload };
21
+ }
22
+ var LOCKED_ERROR_CODES = /* @__PURE__ */ new Set(["5515", "21781", "6982", "27010", "5303", "21251"]);
23
+ var USER_REJECTED_CODES = /* @__PURE__ */ new Set(["6985", "27013"]);
24
+ var WRONG_APP_CODES = /* @__PURE__ */ new Set(["6e00", "28160", "6d00", "27904", "6a83", "27267"]);
25
+ var APP_NOT_INSTALLED_CODES = /* @__PURE__ */ new Set(["6807", "26631"]);
26
+ var STEP_BLIND_SIGN_FALLBACK = "signer.eth.steps.blindSignTransactionFallback";
27
+ function getEthAppErrorCode(err) {
28
+ if (!err || typeof err !== "object") return null;
29
+ const e = err;
30
+ if (e._tag === "EthAppCommandError" && typeof e.errorCode === "string") {
31
+ return e.errorCode.toLowerCase();
32
+ }
33
+ const orig = e.originalError;
34
+ if (orig?._tag === "EthAppCommandError" && typeof orig.errorCode === "string") {
35
+ return orig.errorCode.toLowerCase();
36
+ }
37
+ return null;
38
+ }
39
+ function extractApduHex(err) {
40
+ if (!err || typeof err !== "object") return null;
41
+ const e = err;
42
+ if (typeof e.errorCode === "string" && /^[0-9a-f]+$/i.test(e.errorCode)) {
43
+ return e.errorCode.toLowerCase();
44
+ }
45
+ if (typeof e.statusCode === "number" && Number.isFinite(e.statusCode)) {
46
+ return e.statusCode.toString(16).padStart(4, "0");
47
+ }
48
+ if (typeof e.statusCode === "string" && /^[0-9a-f]+$/i.test(e.statusCode)) {
49
+ return e.statusCode.toLowerCase();
50
+ }
51
+ if (e.originalError != null) {
52
+ const nested = extractApduHex(e.originalError);
53
+ if (nested) return nested;
54
+ }
55
+ if (e.error != null && typeof e._tag === "string") {
56
+ const nested = extractApduHex(e.error);
57
+ if (nested) return nested;
58
+ }
59
+ return null;
60
+ }
61
+ function mapSolanaAppError(hex) {
62
+ switch (hex) {
63
+ case "6808":
64
+ return HardwareErrorCode.SolanaBlindSigningRequired;
65
+ default:
66
+ return null;
67
+ }
68
+ }
69
+ function mapTronAppError(hex) {
70
+ switch (hex) {
71
+ case "6a8d":
72
+ return HardwareErrorCode.TronCustomContractRequired;
73
+ case "6a8b":
74
+ return HardwareErrorCode.TronDataSigningRequired;
75
+ case "6a8c":
76
+ return HardwareErrorCode.TronSignByHashRequired;
77
+ default:
78
+ return null;
79
+ }
80
+ }
81
+ function mapBtcAppError(hex) {
82
+ switch (hex) {
83
+ case "b008":
84
+ return HardwareErrorCode.BtcWalletPolicyHmacMismatch;
85
+ case "b007":
86
+ return HardwareErrorCode.BtcUnexpectedState;
87
+ default:
88
+ return null;
89
+ }
90
+ }
91
+ function mapEthAppError(ethCode, lastStep) {
92
+ switch (ethCode) {
93
+ case "6a80":
94
+ if (lastStep === STEP_BLIND_SIGN_FALLBACK) {
95
+ return HardwareErrorCode.EvmBlindSigningRequired;
96
+ }
97
+ return null;
98
+ case "6984":
99
+ return HardwareErrorCode.EvmClearSignPluginMissing;
100
+ case "6a84":
101
+ return HardwareErrorCode.EvmDataTooLarge;
102
+ case "6501":
103
+ return HardwareErrorCode.EvmTxTypeNotSupported;
104
+ case "911c":
105
+ return HardwareErrorCode.AppTooOld;
106
+ default:
107
+ return null;
108
+ }
109
+ }
110
+ function isDeviceLockedError(err) {
111
+ if (!err || typeof err !== "object") return false;
112
+ const e = err;
113
+ if (e.errorCode != null && LOCKED_ERROR_CODES.has(String(e.errorCode))) return true;
114
+ if (e.statusCode != null && LOCKED_ERROR_CODES.has(String(e.statusCode))) return true;
115
+ if (e._tag === "DeviceLockedError") return true;
116
+ if (e.originalError != null && isDeviceLockedError(e.originalError)) return true;
117
+ if (e.error != null && e._tag && isDeviceLockedError(e.error)) return true;
118
+ return false;
119
+ }
120
+ function hasStatusCode(err, codeSet) {
121
+ if (!err || typeof err !== "object") return false;
122
+ const e = err;
123
+ if (e.errorCode != null && codeSet.has(String(e.errorCode))) return true;
124
+ if (e.statusCode != null && codeSet.has(String(e.statusCode))) return true;
125
+ if (e.originalError != null && hasStatusCode(e.originalError, codeSet)) return true;
126
+ if (e.error != null && e._tag && hasStatusCode(e.error, codeSet)) return true;
127
+ return false;
128
+ }
129
+ function isUserRejectedError(err) {
130
+ if (!err || typeof err !== "object") return false;
131
+ const e = err;
132
+ if (e._tag === "UserRefusedOnDevice") return true;
133
+ if (typeof e.message === "string" && /denied|rejected|refused/i.test(e.message)) return true;
134
+ if (hasStatusCode(err, USER_REJECTED_CODES)) return true;
135
+ return false;
136
+ }
137
+ function isWrongAppError(err) {
138
+ if (!err || typeof err !== "object") return false;
139
+ const e = err;
140
+ if (e._tag === "WrongAppOpenedError" || e._tag === "InvalidStatusWordError") {
141
+ if (hasStatusCode(err, WRONG_APP_CODES)) return true;
142
+ }
143
+ if (typeof e.message === "string") {
144
+ const msg = e.message.toLowerCase();
145
+ if (msg.includes("wrong app") || msg.includes("open the") || msg.includes("cla not supported"))
146
+ return true;
147
+ }
148
+ if (hasStatusCode(err, WRONG_APP_CODES)) return true;
149
+ return false;
150
+ }
151
+ function isAppNotInstalledError(err) {
152
+ if (!err || typeof err !== "object") return false;
153
+ const e = err;
154
+ if (e._tag === "OpenAppCommandError") return true;
155
+ if (typeof e.message === "string" && /unknown application/i.test(e.message)) return true;
156
+ if (hasStatusCode(err, APP_NOT_INSTALLED_CODES)) return true;
157
+ return false;
158
+ }
159
+ function isDeviceDisconnectedError(err) {
160
+ if (!err || typeof err !== "object") return false;
161
+ const e = err;
162
+ if (e._tag === "DeviceNotRecognizedError" || e._tag === "DeviceSessionNotFound") return true;
163
+ if (typeof e.message === "string") {
164
+ const msg = e.message.toLowerCase();
165
+ if (msg.includes("disconnected") || msg.includes("not found") || msg.includes("no device") || msg.includes("unplugged"))
166
+ return true;
167
+ }
168
+ return false;
169
+ }
170
+ var TIMEOUT_TAGS = /* @__PURE__ */ new Set([
171
+ "DeviceExchangeTimeoutError",
172
+ "SendApduTimeoutError",
173
+ "SendCommandTimeoutError"
174
+ ]);
175
+ function isTimeoutError(err) {
176
+ if (!err || typeof err !== "object") return false;
177
+ const e = err;
178
+ if (typeof e._tag === "string" && TIMEOUT_TAGS.has(e._tag)) return true;
179
+ if (e.code === HardwareErrorCode.OperationTimeout) return true;
180
+ return false;
181
+ }
182
+ function mapLedgerError(err, opts) {
183
+ let originalMessage = "Unknown Ledger error";
184
+ if (err instanceof Error) {
185
+ originalMessage = err.message;
186
+ } else if (err && typeof err === "object") {
187
+ const e = err;
188
+ originalMessage = String(e.message ?? e._tag ?? e.type ?? JSON.stringify(err));
189
+ }
190
+ let code;
191
+ if (isDeviceLockedError(err)) {
192
+ code = HardwareErrorCode.DeviceLocked;
193
+ } else if (isUserRejectedError(err)) {
194
+ code = HardwareErrorCode.UserRejected;
195
+ } else if (isWrongAppError(err)) {
196
+ code = HardwareErrorCode.WrongApp;
197
+ } else if (isAppNotInstalledError(err)) {
198
+ code = HardwareErrorCode.AppNotOpen;
199
+ } else if (isDeviceDisconnectedError(err)) {
200
+ code = HardwareErrorCode.DeviceDisconnected;
201
+ } else if (isTimeoutError(err)) {
202
+ code = HardwareErrorCode.OperationTimeout;
203
+ } 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;
207
+ const apduHex = ethMapped ? null : extractApduHex(err);
208
+ const chainMapped = apduHex ? mapSolanaAppError(apduHex) ?? mapTronAppError(apduHex) ?? mapBtcAppError(apduHex) : null;
209
+ code = ethMapped ?? chainMapped ?? HardwareErrorCode.UnknownError;
210
+ }
211
+ const errAppName = err && typeof err === "object" ? err.appName : void 0;
212
+ const appName = errAppName ?? opts?.defaultAppName;
213
+ return { code, message: enrichErrorMessage(code, originalMessage), appName };
214
+ }
215
+
216
+ // src/utils/sdkEventBus.ts
217
+ var listeners = /* @__PURE__ */ new Set();
218
+ function onSdkEvent(listener) {
219
+ listeners.add(listener);
220
+ return () => {
221
+ listeners.delete(listener);
222
+ };
223
+ }
224
+ function offSdkEvent(listener) {
225
+ listeners.delete(listener);
226
+ }
227
+ function emitSdkEvent(event) {
228
+ if (listeners.size === 0) return;
229
+ for (const listener of listeners) {
230
+ try {
231
+ listener(event);
232
+ } catch {
233
+ }
234
+ }
235
+ }
236
+ function emitLog(level, ...args) {
237
+ if (listeners.size === 0) return;
238
+ const message = args.map((a) => typeof a === "string" ? a : safeStringify(a)).join(" ");
239
+ emitSdkEvent({ type: "log", level, message });
240
+ }
241
+ function safeStringify(value) {
242
+ if (value instanceof Error) {
243
+ return value.stack ? `${value.message}
244
+ ${value.stack}` : value.message;
245
+ }
246
+ try {
247
+ return JSON.stringify(value);
248
+ } catch {
249
+ return String(value);
250
+ }
251
+ }
252
+
253
+ // src/utils/debugLog.ts
254
+ function debugLog(...args) {
255
+ emitLog("debug", ...args);
256
+ }
257
+ function debugError(...args) {
258
+ emitLog("error", ...args);
259
+ }
260
+
261
+ // src/adapter/LedgerAdapter.ts
262
+ function formatDeviceMismatchError(expected, actual) {
263
+ return `Wrong device: expected ${expected}, got ${actual}`;
264
+ }
265
+ var _LedgerAdapter = class _LedgerAdapter {
266
+ constructor(connector, options) {
267
+ this.vendor = "ledger";
268
+ this.emitter = new TypedEventEmitter();
269
+ // Device cache: tracks discovered devices from connector events
270
+ this._discoveredDevices = /* @__PURE__ */ new Map();
271
+ // Session tracking: maps connectId -> sessionId
272
+ this._sessions = /* @__PURE__ */ new Map();
273
+ this._uiRegistry = new UiRequestRegistry();
274
+ // Mutex for ensureConnected — prevents concurrent calls from establishing duplicate connections
275
+ this._connectingPromise = null;
276
+ // ---------------------------------------------------------------------------
277
+ // Event translation
278
+ // ---------------------------------------------------------------------------
279
+ this.deviceConnectHandler = (data) => {
280
+ const deviceInfo = this.connectorDeviceToDeviceInfo(data.device);
281
+ this._discoveredDevices.set(deviceInfo.connectId, deviceInfo);
282
+ this._sessions.delete(deviceInfo.connectId);
283
+ this.emitter.emit(DEVICE.CONNECT, {
284
+ type: DEVICE.CONNECT,
285
+ payload: deviceInfo
286
+ });
287
+ };
288
+ this.deviceDisconnectHandler = (data) => {
289
+ this._discoveredDevices.delete(data.connectId);
290
+ this._sessions.delete(data.connectId);
291
+ this.emitter.emit(DEVICE.DISCONNECT, {
292
+ type: DEVICE.DISCONNECT,
293
+ payload: { connectId: data.connectId }
294
+ });
295
+ };
296
+ // Forward low-level connector 'ui-event' (the four EConnectorInteraction values)
297
+ // to the public hw.emitter so consumers only need to subscribe in one place
298
+ // (hw.on instead of also reaching into connector.on).
299
+ this.uiEventForwarder = (event) => {
300
+ this.emitter.emit("ui-event", event);
301
+ };
302
+ this.connector = connector;
303
+ this._handleSelectDevice = options?.handleSelectDevice ?? false;
304
+ this._jobQueue = new DeviceJobQueue({
305
+ emit: (event, data) => this.emitter.emit(event, data),
306
+ uiRegistry: this._uiRegistry
307
+ });
308
+ this.registerEventListeners();
309
+ }
310
+ /**
311
+ * Classify a method's interruptibility.
312
+ * - Signing / typed data / transaction → 'confirm' (user may decide via preemption UI)
313
+ * - Read-only queries (getAddress / getPublicKey / getMasterFingerprint) → 'safe'
314
+ * (auto-cancels any pending read for the same device)
315
+ */
316
+ static _getInterruptibility(method) {
317
+ if (method.toLowerCase().includes("sign")) return "confirm";
318
+ return "safe";
319
+ }
320
+ // ---------------------------------------------------------------------------
321
+ // Transport
322
+ // ---------------------------------------------------------------------------
323
+ // Transport is decided at connector creation time. These methods
324
+ // satisfy the IHardwareWallet interface with sensible defaults.
325
+ get activeTransport() {
326
+ return this.connector.connectionType === "ble" ? "ble" : "hid";
327
+ }
328
+ getAvailableTransports() {
329
+ return this.activeTransport ? [this.activeTransport] : [];
330
+ }
331
+ async switchTransport(_type) {
332
+ }
333
+ // ---------------------------------------------------------------------------
334
+ // Lifecycle
335
+ // ---------------------------------------------------------------------------
336
+ async init(_config) {
337
+ }
338
+ /**
339
+ * Clear cached device/session state without tearing down the adapter.
340
+ * Call before retrying after errors or when the device state may be stale.
341
+ * The next operation will re-discover and re-connect automatically.
342
+ */
343
+ resetState() {
344
+ this._discoveredDevices.clear();
345
+ this._sessions.clear();
346
+ this._connectingPromise = null;
347
+ this._uiRegistry.reset();
348
+ this._jobQueue.clear();
349
+ }
350
+ async dispose() {
351
+ this._uiRegistry.reset();
352
+ this._jobQueue.clear();
353
+ this.unregisterEventListeners();
354
+ this.connector.reset();
355
+ this._discoveredDevices.clear();
356
+ this._sessions.clear();
357
+ this.emitter.removeAllListeners();
358
+ }
359
+ uiResponse(response) {
360
+ this._uiRegistry.resolve(response.type, response.payload);
361
+ }
362
+ // ---------------------------------------------------------------------------
363
+ // Device management
364
+ // ---------------------------------------------------------------------------
365
+ async searchDevices() {
366
+ await this._ensureDevicePermission();
367
+ const devices = await this.connector.searchDevices();
368
+ debugLog("[DMK] adapter.searchDevices raw:", JSON.stringify(devices));
369
+ this._discoveredDevices.clear();
370
+ for (const d of devices) {
371
+ if (d.connectId) {
372
+ this._discoveredDevices.set(d.connectId, this.connectorDeviceToDeviceInfo(d));
373
+ }
374
+ }
375
+ if (this._discoveredDevices.size === 0) {
376
+ await this._ensureDevicePermission();
377
+ }
378
+ return Array.from(this._discoveredDevices.values());
379
+ }
380
+ async connectDevice(connectId) {
381
+ await this._ensureDevicePermission(connectId);
382
+ try {
383
+ const session = await this.connector.connect(connectId);
384
+ this._sessions.set(connectId, session.sessionId);
385
+ if (session.deviceInfo) {
386
+ this._discoveredDevices.set(connectId, session.deviceInfo);
387
+ }
388
+ return success(connectId);
389
+ } catch (err) {
390
+ return this.errorToFailure(err);
391
+ }
392
+ }
393
+ async disconnectDevice(connectId) {
394
+ const sessionId = this._sessions.get(connectId);
395
+ if (sessionId) {
396
+ await this.connector.disconnect(sessionId);
397
+ this._sessions.delete(connectId);
398
+ }
399
+ }
400
+ async getDeviceInfo(connectId, deviceId) {
401
+ await this._ensureDevicePermission(connectId, deviceId);
402
+ const cached = this._discoveredDevices.get(connectId) ?? Array.from(this._discoveredDevices.values()).find((d) => d.deviceId === deviceId);
403
+ if (cached) {
404
+ return success(cached);
405
+ }
406
+ return failure(
407
+ HardwareErrorCode2.DeviceNotFound,
408
+ "Device not found in cache. Call searchDevices() or wait for a device-connected event first."
409
+ );
410
+ }
411
+ getSupportedChains() {
412
+ return ["evm", "btc", "sol", "tron"];
413
+ }
414
+ // ---------------------------------------------------------------------------
415
+ // Chain call helper
416
+ // ---------------------------------------------------------------------------
417
+ async callChain(connectId, deviceId, chain, method, params, skipFingerprint = false) {
418
+ await this._ensureDevicePermission(connectId, deviceId);
419
+ try {
420
+ const result = await this.connectorCall(connectId, method, params, {
421
+ chain,
422
+ deviceId,
423
+ skipFingerprint
424
+ });
425
+ return success(result);
426
+ } catch (err) {
427
+ return this.errorToFailure(err);
428
+ }
429
+ }
430
+ /**
431
+ * Batch version of callChain — checks permission once,
432
+ * fingerprint is verified on the first call inside connectorCall.
433
+ */
434
+ async callChainBatch(connectId, deviceId, chain, method, params, onProgress, skipFingerprint = false) {
435
+ await this._ensureDevicePermission(connectId, deviceId);
436
+ const results = [];
437
+ for (let i = 0; i < params.length; i++) {
438
+ try {
439
+ const result = await this.connectorCall(connectId, method, params[i], {
440
+ chain,
441
+ deviceId,
442
+ // Only verify fingerprint on the first call in the batch
443
+ skipFingerprint: skipFingerprint || i > 0
444
+ });
445
+ results.push(result);
446
+ onProgress?.({ index: i, total: params.length });
447
+ } catch (err) {
448
+ return this.errorToFailure(err);
449
+ }
450
+ }
451
+ return success(results);
452
+ }
453
+ // ---------------------------------------------------------------------------
454
+ // EVM chain methods
455
+ // ---------------------------------------------------------------------------
456
+ evmGetAddress(connectId, deviceId, params) {
457
+ return this.callChain(connectId, deviceId, "evm", "evmGetAddress", params);
458
+ }
459
+ evmGetAddresses(connectId, deviceId, params, onProgress) {
460
+ return this.callChainBatch(
461
+ connectId,
462
+ deviceId,
463
+ "evm",
464
+ "evmGetAddress",
465
+ params,
466
+ onProgress
467
+ );
468
+ }
469
+ evmSignTransaction(connectId, deviceId, params) {
470
+ return this.callChain(connectId, deviceId, "evm", "evmSignTransaction", params);
471
+ }
472
+ evmSignMessage(connectId, deviceId, params) {
473
+ return this.callChain(connectId, deviceId, "evm", "evmSignMessage", params);
474
+ }
475
+ evmSignTypedData(connectId, deviceId, params) {
476
+ return this.callChain(connectId, deviceId, "evm", "evmSignTypedData", params);
477
+ }
478
+ // ---------------------------------------------------------------------------
479
+ // BTC chain methods
480
+ // ---------------------------------------------------------------------------
481
+ btcGetAddress(connectId, deviceId, params) {
482
+ return this.callChain(connectId, deviceId, "btc", "btcGetAddress", params);
483
+ }
484
+ btcGetAddresses(connectId, deviceId, params, onProgress) {
485
+ return this.callChainBatch(
486
+ connectId,
487
+ deviceId,
488
+ "btc",
489
+ "btcGetAddress",
490
+ params,
491
+ onProgress
492
+ );
493
+ }
494
+ btcGetPublicKey(connectId, deviceId, params) {
495
+ return this.callChain(connectId, deviceId, "btc", "btcGetPublicKey", params);
496
+ }
497
+ btcSignTransaction(connectId, deviceId, params) {
498
+ return this.callChain(connectId, deviceId, "btc", "btcSignTransaction", params);
499
+ }
500
+ btcSignPsbt(connectId, deviceId, params) {
501
+ return this.callChain(connectId, deviceId, "btc", "btcSignPsbt", params);
502
+ }
503
+ btcSignMessage(connectId, deviceId, params) {
504
+ return this.callChain(connectId, deviceId, "btc", "btcSignMessage", params);
505
+ }
506
+ btcGetMasterFingerprint(connectId, deviceId) {
507
+ return this.callChain(
508
+ connectId,
509
+ deviceId,
510
+ "btc",
511
+ "btcGetMasterFingerprint",
512
+ {}
513
+ );
514
+ }
515
+ // ---------------------------------------------------------------------------
516
+ // SOL chain methods
517
+ // ---------------------------------------------------------------------------
518
+ solGetAddress(connectId, deviceId, params) {
519
+ return this.callChain(connectId, deviceId, "sol", "solGetAddress", params);
520
+ }
521
+ solGetAddresses(connectId, deviceId, params, onProgress) {
522
+ return this.callChainBatch(
523
+ connectId,
524
+ deviceId,
525
+ "sol",
526
+ "solGetAddress",
527
+ params,
528
+ onProgress
529
+ );
530
+ }
531
+ solSignTransaction(connectId, deviceId, params) {
532
+ return this.callChain(connectId, deviceId, "sol", "solSignTransaction", params);
533
+ }
534
+ solSignMessage(connectId, deviceId, params) {
535
+ return this.callChain(connectId, deviceId, "sol", "solSignMessage", params);
536
+ }
537
+ // ---------------------------------------------------------------------------
538
+ // TRON chain methods
539
+ // ---------------------------------------------------------------------------
540
+ tronGetAddress(connectId, deviceId, params) {
541
+ return this.callChain(connectId, deviceId, "tron", "tronGetAddress", params, true);
542
+ }
543
+ tronGetAddresses(connectId, deviceId, params, onProgress) {
544
+ return this.callChainBatch(
545
+ connectId,
546
+ deviceId,
547
+ "tron",
548
+ "tronGetAddress",
549
+ params,
550
+ onProgress,
551
+ true
552
+ );
553
+ }
554
+ tronSignTransaction(connectId, deviceId, params) {
555
+ return this.callChain(
556
+ connectId,
557
+ deviceId,
558
+ "tron",
559
+ "tronSignTransaction",
560
+ params,
561
+ true
562
+ );
563
+ }
564
+ tronSignMessage(connectId, deviceId, params) {
565
+ return this.callChain(
566
+ connectId,
567
+ deviceId,
568
+ "tron",
569
+ "tronSignMessage",
570
+ params,
571
+ true
572
+ );
573
+ }
574
+ on(event, listener) {
575
+ this.emitter.on(event, listener);
576
+ }
577
+ off(event, listener) {
578
+ this.emitter.off(event, listener);
579
+ }
580
+ cancel(connectId) {
581
+ const sessionId = this._sessions.get(connectId) ?? connectId;
582
+ this._jobQueue.forceCancelActive(connectId || "__ledger_default__");
583
+ void this.connector.cancel(sessionId);
584
+ }
585
+ // ---------------------------------------------------------------------------
586
+ // Chain fingerprint
587
+ // ---------------------------------------------------------------------------
588
+ async getChainFingerprint(connectId, deviceId, chain) {
589
+ debugLog(
590
+ "[LedgerAdapter] getChainFingerprint called, chain:",
591
+ chain,
592
+ "connectId:",
593
+ connectId || "(empty)",
594
+ "sessions:",
595
+ this._sessions.size
596
+ );
597
+ await this._ensureDevicePermission(connectId, deviceId);
598
+ debugLog("[LedgerAdapter] getChainFingerprint permission ok, computing fingerprint");
599
+ try {
600
+ const fingerprint = await this._computeChainFingerprint(
601
+ chain,
602
+ (method, params) => this.connectorCall(connectId, method, params)
603
+ );
604
+ debugLog("[LedgerAdapter] getChainFingerprint result:", fingerprint?.substring(0, 20));
605
+ return success(fingerprint);
606
+ } catch (err) {
607
+ debugError("[LedgerAdapter] getChainFingerprint error:", chain, err);
608
+ return this.errorToFailure(err);
609
+ }
610
+ }
611
+ /**
612
+ * Verify fingerprint using an existing sessionId directly.
613
+ * Safe to call inside connectorCall without causing queue deadlock.
614
+ */
615
+ async _verifyDeviceFingerprintWithSession(sessionId, deviceId, chain) {
616
+ if (!deviceId) return { success: true };
617
+ try {
618
+ const fingerprint = await this._computeChainFingerprint(
619
+ chain,
620
+ (method, params) => this.connector.call(sessionId, method, params)
621
+ );
622
+ if (fingerprint === deviceId) {
623
+ return { success: true };
624
+ }
625
+ return { success: false, expected: deviceId, actual: fingerprint };
626
+ } catch (err) {
627
+ const mapped = mapLedgerError(err);
628
+ if (mapped.code === HardwareErrorCode2.WrongApp || mapped.code === HardwareErrorCode2.DeviceLocked) {
629
+ return { success: true };
630
+ }
631
+ throw err;
632
+ }
633
+ }
634
+ /**
635
+ * Compute the chain fingerprint via a caller-supplied call strategy.
636
+ *
637
+ * Chains with a native device-side identity primitive (BTC → BIP32 master
638
+ * fingerprint) short-circuit at the top and return it verbatim, so the value
639
+ * stays reusable for higher-level use (BIP380 descriptors, PSBT signing).
640
+ *
641
+ * All other chains derive a fixed-path address and run it through
642
+ * `deriveDeviceFingerprint` to produce an opaque seed identifier.
643
+ *
644
+ * The two callers (`getChainFingerprint` / `_verifyDeviceFingerprintWithSession`)
645
+ * differ only in the underlying call mechanism, which is injected as `callMethod`
646
+ * to avoid queue deadlocks when running inside `connectorCall`.
647
+ */
648
+ async _computeChainFingerprint(chain, callMethod) {
649
+ if (chain === "btc") {
650
+ const result = await callMethod("btcGetMasterFingerprint", {});
651
+ return result.masterFingerprint;
652
+ }
653
+ const path = CHAIN_FINGERPRINT_PATHS[chain];
654
+ let address;
655
+ if (chain === "evm") {
656
+ address = (await callMethod("evmGetAddress", { path, showOnDevice: false })).address.toLowerCase();
657
+ } else if (chain === "sol") {
658
+ address = (await callMethod("solGetAddress", { path, showOnDevice: false })).address;
659
+ } else if (chain === "tron") {
660
+ address = (await callMethod("tronGetAddress", { path, showOnDevice: false })).address;
661
+ } else {
662
+ throw new Error(`Unsupported chain for fingerprint: ${chain}`);
663
+ }
664
+ return deriveDeviceFingerprint(address);
665
+ }
666
+ // Ledger WebUSB won't expose a locked device, so we can't auto-detect unlock.
667
+ // The user must press Confirm after unlocking, which triggers a search retry.
668
+ // If `signal` is provided, an abort cancels the pending UI request so the
669
+ // registry slot is released and a stale RECEIVE_DEVICE_CONNECT won't land in
670
+ // a future request.
671
+ async _waitForDeviceConnect(signal) {
672
+ if (signal?.aborted) {
673
+ _LedgerAdapter._throwIfAborted(signal);
674
+ }
675
+ this.emitter.emit(UI_REQUEST.REQUEST_DEVICE_CONNECT, {
676
+ type: UI_REQUEST.REQUEST_DEVICE_CONNECT,
677
+ payload: {
678
+ message: "Please connect and unlock your Ledger device"
679
+ }
680
+ });
681
+ const waitPromise = this._uiRegistry.wait(
682
+ UI_REQUEST.REQUEST_DEVICE_CONNECT
683
+ );
684
+ let payload;
685
+ if (signal) {
686
+ const onAbort = () => {
687
+ this._uiRegistry.cancel(UI_REQUEST.REQUEST_DEVICE_CONNECT);
688
+ };
689
+ signal.addEventListener("abort", onAbort, { once: true });
690
+ try {
691
+ payload = await waitPromise;
692
+ } finally {
693
+ signal.removeEventListener("abort", onAbort);
694
+ }
695
+ } else {
696
+ payload = await waitPromise;
697
+ }
698
+ if (!payload?.confirmed) {
699
+ throw Object.assign(new Error("User cancelled Ledger connection"), {
700
+ _tag: "DeviceNotRecognizedError"
701
+ });
702
+ }
703
+ }
704
+ async ensureConnected(connectId) {
705
+ if (connectId && this._sessions.has(connectId)) {
706
+ return connectId;
707
+ }
708
+ if (this._sessions.size > 0) {
709
+ const firstKey = this._sessions.keys().next().value;
710
+ return firstKey;
711
+ }
712
+ if (this._connectingPromise) {
713
+ return this._connectingPromise;
714
+ }
715
+ this._connectingPromise = this._doConnect();
716
+ try {
717
+ return await this._connectingPromise;
718
+ } finally {
719
+ this._connectingPromise = null;
720
+ }
721
+ }
722
+ async _doConnect() {
723
+ for (let attempt = 0; attempt < _LedgerAdapter.MAX_DEVICE_RETRY; attempt++) {
724
+ const devices = await this.searchDevices();
725
+ if (devices.length > 0) {
726
+ return this._connectFirstOrSelect(devices);
727
+ }
728
+ if (attempt < _LedgerAdapter.MAX_DEVICE_RETRY - 1) {
729
+ await this._waitForDeviceConnect();
730
+ }
731
+ }
732
+ throw Object.assign(
733
+ new Error(
734
+ "No Ledger device found after multiple attempts. Please connect and unlock your device."
735
+ ),
736
+ { _tag: "DeviceNotRecognizedError" }
737
+ );
738
+ }
739
+ async _connectFirstOrSelect(devices) {
740
+ const chosenConnectId = devices.length === 1 ? devices[0].connectId : await this._chooseDeviceFromList(devices);
741
+ const result = await this.connectDevice(chosenConnectId);
742
+ if (!result.success) {
743
+ throw Object.assign(new Error(result.payload.error), {
744
+ _tag: "DeviceNotRecognizedError"
745
+ });
746
+ }
747
+ return chosenConnectId;
748
+ }
749
+ async _chooseDeviceFromList(devices) {
750
+ if (!this._handleSelectDevice) {
751
+ debugLog(
752
+ `[DMK] Multiple Ledger devices found (${devices.length}); handleSelectDevice=false, picking first (${devices[0].connectId}).`
753
+ );
754
+ return devices[0].connectId;
755
+ }
756
+ this.emitter.emit(UI_REQUEST.REQUEST_SELECT_DEVICE, {
757
+ type: UI_REQUEST.REQUEST_SELECT_DEVICE,
758
+ payload: { devices }
759
+ });
760
+ const response = await this._uiRegistry.wait(
761
+ UI_REQUEST.REQUEST_SELECT_DEVICE
762
+ );
763
+ const chosen = devices.find((d) => d.connectId === response?.sdkConnectId);
764
+ if (!chosen) {
765
+ throw Object.assign(
766
+ new Error(`Selected sdkConnectId '${response?.sdkConnectId}' not in discovered list`),
767
+ { _tag: "DeviceNotRecognizedError" }
768
+ );
769
+ }
770
+ return chosen.connectId;
771
+ }
772
+ /**
773
+ * Call the connector with automatic session resolution and disconnect retry.
774
+ *
775
+ * 1. Resolves a valid connectId via ensureConnected()
776
+ * 2. Looks up sessionId from _sessions
777
+ * 3. Calls connector.call()
778
+ * 4. On disconnect error: clears stale session, re-connects, retries once
779
+ */
780
+ async connectorCall(connectId, method, params, fingerprint) {
781
+ debugLog("[LedgerAdapter] connectorCall:", method, "connectId:", connectId || "(empty)");
782
+ const queueKey = connectId || "__ledger_default__";
783
+ const interruptibility = _LedgerAdapter._getInterruptibility(method);
784
+ return this._jobQueue.enqueue(
785
+ queueKey,
786
+ async (signal) => this._runConnectorCall(connectId, method, params, signal, fingerprint),
787
+ { interruptibility, label: method }
788
+ );
789
+ }
790
+ /**
791
+ * Race a promise against an abort signal. If the signal fires, rejects with the
792
+ * signal's reason (or a generic Error). The underlying connector.call() cannot
793
+ * actually be cancelled on Ledger DMK, but the caller gets the abort immediately.
794
+ */
795
+ static _abortable(signal, promise) {
796
+ if (signal.aborted) {
797
+ return Promise.reject(
798
+ signal.reason ?? new Error("Aborted")
799
+ );
800
+ }
801
+ return new Promise((resolve, reject) => {
802
+ const onAbort = () => {
803
+ reject(signal.reason ?? new Error("Aborted"));
804
+ };
805
+ signal.addEventListener("abort", onAbort, { once: true });
806
+ promise.then(
807
+ (value) => {
808
+ signal.removeEventListener("abort", onAbort);
809
+ resolve(value);
810
+ },
811
+ (err) => {
812
+ signal.removeEventListener("abort", onAbort);
813
+ reject(err);
814
+ }
815
+ );
816
+ });
817
+ }
818
+ /** Throw an AbortError if signal is already aborted. */
819
+ static _throwIfAborted(signal) {
820
+ if (signal.aborted) {
821
+ throw signal.reason ?? new Error("Aborted");
822
+ }
823
+ }
824
+ /** Actual work done under the job queue — connection, fingerprint, call, and recovery. */
825
+ async _runConnectorCall(connectId, method, params, signal, fingerprint) {
826
+ _LedgerAdapter._throwIfAborted(signal);
827
+ const resolvedConnectId = await _LedgerAdapter._abortable(
828
+ signal,
829
+ this.ensureConnected(connectId)
830
+ );
831
+ _LedgerAdapter._throwIfAborted(signal);
832
+ const sessionId = this._sessions.get(resolvedConnectId);
833
+ debugLog(
834
+ "[LedgerAdapter] connectorCall resolved:",
835
+ method,
836
+ "resolvedConnectId:",
837
+ resolvedConnectId,
838
+ "sessionId:",
839
+ sessionId
840
+ );
841
+ if (!sessionId) {
842
+ throw Object.assign(new Error("Auto-connect succeeded but no session found"), {
843
+ _tag: "DeviceSessionNotFound"
844
+ });
845
+ }
846
+ if (fingerprint && !fingerprint.skipFingerprint && fingerprint.deviceId) {
847
+ const fp = await _LedgerAdapter._abortable(
848
+ signal,
849
+ this._verifyDeviceFingerprintWithSession(sessionId, fingerprint.deviceId, fingerprint.chain)
850
+ );
851
+ if (!fp.success) {
852
+ throw Object.assign(new Error(formatDeviceMismatchError(fp.expected, fp.actual)), {
853
+ code: HardwareErrorCode2.DeviceMismatch
854
+ });
855
+ }
856
+ }
857
+ try {
858
+ return await _LedgerAdapter._abortable(signal, this.connector.call(sessionId, method, params));
859
+ } catch (err) {
860
+ if (signal.aborted) throw err;
861
+ const errObj = err;
862
+ debugLog("[LedgerAdapter] connectorCall error:", method, {
863
+ message: errObj?.message,
864
+ _tag: errObj?._tag,
865
+ errorCode: errObj?.errorCode,
866
+ statusCode: errObj?.statusCode,
867
+ isDisconnected: isDeviceDisconnectedError(err),
868
+ isLocked: isDeviceLockedError(err)
869
+ });
870
+ if (isDeviceDisconnectedError(err)) {
871
+ debugLog("[LedgerAdapter] disconnected, retrying with fresh connection...");
872
+ this._discoveredDevices.clear();
873
+ return this._retryWithFreshConnection(resolvedConnectId, method, params, signal, err);
874
+ }
875
+ if (isDeviceLockedError(err)) {
876
+ await this._waitForDeviceConnect(signal);
877
+ _LedgerAdapter._throwIfAborted(signal);
878
+ return _LedgerAdapter._abortable(signal, this.connector.call(sessionId, method, params));
879
+ }
880
+ if (isTimeoutError(err)) {
881
+ debugLog("[LedgerAdapter] timeout, retrying with fresh connection...");
882
+ this._discoveredDevices.delete(resolvedConnectId);
883
+ return this._retryWithFreshConnection(resolvedConnectId, method, params, signal, err);
884
+ }
885
+ throw err;
886
+ }
887
+ }
888
+ /** Clear stale session, reconnect, and retry the call. */
889
+ async _retryWithFreshConnection(resolvedConnectId, method, params, signal, originalErr) {
890
+ this._sessions.delete(resolvedConnectId);
891
+ _LedgerAdapter._throwIfAborted(signal);
892
+ const retryConnectId = await this.ensureConnected();
893
+ _LedgerAdapter._throwIfAborted(signal);
894
+ const retrySessionId = this._sessions.get(retryConnectId);
895
+ if (!retrySessionId) {
896
+ throw originalErr;
897
+ }
898
+ return _LedgerAdapter._abortable(signal, this.connector.call(retrySessionId, method, params));
899
+ }
900
+ /**
901
+ * Ensure OS-level device permission (Bluetooth / USB) before proceeding.
902
+ *
903
+ * Emits `REQUEST_DEVICE_PERMISSION` and awaits the consumer's
904
+ * `RECEIVE_DEVICE_PERMISSION` reply (60s budget covers "probe → system
905
+ * prompt → user tap" plus a generous margin). If the consumer never wires
906
+ * a handler or never replies, the wait times out and the operation fails
907
+ * fast so scanners/callers don't hang silently.
908
+ *
909
+ * - No connectId (searchDevices): environment-level permission
910
+ * - With connectId (business methods): device-level permission
911
+ */
912
+ async _ensureDevicePermission(connectId, deviceId) {
913
+ const transportType = this.activeTransport ?? "hid";
914
+ const waitPromise = this._uiRegistry.wait(
915
+ UI_REQUEST.REQUEST_DEVICE_PERMISSION,
916
+ { timeoutMs: 6e4 }
917
+ );
918
+ this.emitter.emit(UI_REQUEST.REQUEST_DEVICE_PERMISSION, {
919
+ type: UI_REQUEST.REQUEST_DEVICE_PERMISSION,
920
+ payload: { transportType, connectId, deviceId }
921
+ });
922
+ const { granted } = await waitPromise;
923
+ if (!granted) {
924
+ throw Object.assign(new Error("Device permission denied"), {
925
+ code: HardwareErrorCode2.DevicePermissionDenied
926
+ });
927
+ }
928
+ }
929
+ /**
930
+ * Convert a thrown error to a Response failure.
931
+ * Uses mapLedgerError to parse Ledger DMK error codes into HardwareErrorCode values.
932
+ */
933
+ errorToFailure(err) {
934
+ debugError("[LedgerAdapter] error:", err);
935
+ if (err && typeof err === "object" && "code" in err && typeof err.code === "number") {
936
+ const e = err;
937
+ return ledgerFailure(e.code, e.message ?? "Unknown error", e.appName);
938
+ }
939
+ const mapped = mapLedgerError(err);
940
+ return ledgerFailure(mapped.code, mapped.message, mapped.appName);
941
+ }
942
+ registerEventListeners() {
943
+ this.connector.on("device-connect", this.deviceConnectHandler);
944
+ this.connector.on("device-disconnect", this.deviceDisconnectHandler);
945
+ this.connector.on("ui-event", this.uiEventForwarder);
946
+ }
947
+ unregisterEventListeners() {
948
+ this.connector.off("device-connect", this.deviceConnectHandler);
949
+ this.connector.off("device-disconnect", this.deviceDisconnectHandler);
950
+ this.connector.off("ui-event", this.uiEventForwarder);
951
+ }
952
+ // ---------------------------------------------------------------------------
953
+ // Device info mapping
954
+ // ---------------------------------------------------------------------------
955
+ connectorDeviceToDeviceInfo(device) {
956
+ const isBle = device.connectId && /^[0-9A-Fa-f]{4}$/.test(device.connectId);
957
+ return {
958
+ vendor: "ledger",
959
+ model: device.model ?? "unknown",
960
+ firmwareVersion: "",
961
+ deviceId: device.deviceId,
962
+ connectId: device.connectId,
963
+ label: device.name,
964
+ connectionType: isBle ? "ble" : "usb",
965
+ capabilities: device.capabilities
966
+ };
967
+ }
968
+ };
969
+ // ---------------------------------------------------------------------------
970
+ // Private helpers
971
+ // ---------------------------------------------------------------------------
972
+ /**
973
+ * Ensure at least one device is connected and return a valid connectId.
974
+ *
975
+ * - If a session already exists for the given connectId, reuse it.
976
+ * - If ANY session exists (Ledger IDs are ephemeral), reuse it.
977
+ * - Otherwise: search → 1 device: auto-connect, multiple: ask user, 0: throw.
978
+ */
979
+ _LedgerAdapter.MAX_DEVICE_RETRY = 3;
980
+ var LedgerAdapter = _LedgerAdapter;
981
+
982
+ // src/device/LedgerDeviceManager.ts
983
+ var LedgerDeviceManager = class {
984
+ constructor(dmk) {
985
+ this._discovered = /* @__PURE__ */ new Map();
986
+ this._sessions = /* @__PURE__ */ new Map();
987
+ // deviceId → sessionId
988
+ this._sessionToDevice = /* @__PURE__ */ new Map();
989
+ // sessionId → deviceId
990
+ this._listenSub = null;
991
+ /**
992
+ * Trigger browser device selection (WebHID requestDevice).
993
+ * Starts discovery for a short period, then stops.
994
+ */
995
+ /**
996
+ * Start BLE discovery if not already running.
997
+ * Does NOT stop — DMK keeps scanning in the background so that
998
+ * listenToAvailableDevices() always has fresh data.
999
+ */
1000
+ this._discoverySub = null;
1001
+ this._dmk = dmk;
1002
+ }
1003
+ /**
1004
+ * One-shot enumeration: subscribe to listenToAvailableDevices,
1005
+ * take the first emission, unsubscribe, return DeviceDescriptors.
1006
+ */
1007
+ enumerate() {
1008
+ debugLog("[DMK] enumerate() called, dmk exists:", !!this._dmk);
1009
+ return new Promise((resolve) => {
1010
+ let resolved = false;
1011
+ let syncResult = null;
1012
+ let sub = null;
1013
+ sub = this._dmk.listenToAvailableDevices({}).subscribe({
1014
+ next: (devices) => {
1015
+ if (resolved) return;
1016
+ resolved = true;
1017
+ this._discovered.clear();
1018
+ debugLog("[DMK] enumerate raw devices:", devices.length);
1019
+ for (const d of devices) {
1020
+ this._discovered.set(d.id, d);
1021
+ }
1022
+ if (sub) {
1023
+ sub.unsubscribe();
1024
+ resolve(
1025
+ devices.map((d) => ({
1026
+ path: d.id,
1027
+ type: d.deviceModel.model,
1028
+ name: d.name,
1029
+ transport: d.transport
1030
+ }))
1031
+ );
1032
+ } else {
1033
+ syncResult = devices;
1034
+ }
1035
+ },
1036
+ error: () => {
1037
+ if (!resolved) {
1038
+ resolved = true;
1039
+ resolve([]);
1040
+ }
1041
+ }
1042
+ });
1043
+ if (syncResult !== null) {
1044
+ sub.unsubscribe();
1045
+ const devices = syncResult;
1046
+ resolve(
1047
+ devices.map((d) => ({
1048
+ path: d.id,
1049
+ type: d.deviceModel.model,
1050
+ name: d.name,
1051
+ transport: d.transport
1052
+ }))
1053
+ );
1054
+ }
1055
+ });
1056
+ }
1057
+ /**
1058
+ * Continuous listening: tracks device connect/disconnect via diffing.
1059
+ */
1060
+ listen(onChange) {
1061
+ this.stopListening();
1062
+ let previousIds = /* @__PURE__ */ new Set();
1063
+ this._listenSub = this._dmk.listenToAvailableDevices({}).subscribe({
1064
+ next: (devices) => {
1065
+ const currentIds = new Set(devices.map((d) => d.id));
1066
+ for (const d of devices) {
1067
+ this._discovered.set(d.id, d);
1068
+ debugLog("[DMK] listen device:", d.id, d.name);
1069
+ if (!previousIds.has(d.id)) {
1070
+ onChange({
1071
+ type: "device-connected",
1072
+ descriptor: {
1073
+ path: d.id,
1074
+ type: d.deviceModel.model,
1075
+ name: d.name,
1076
+ transport: d.transport
1077
+ }
1078
+ });
1079
+ }
1080
+ }
1081
+ for (const id of previousIds) {
1082
+ if (!currentIds.has(id)) {
1083
+ this._discovered.delete(id);
1084
+ onChange({ type: "device-disconnected", descriptor: { path: id } });
1085
+ }
1086
+ }
1087
+ previousIds = currentIds;
1088
+ }
1089
+ });
1090
+ }
1091
+ stopListening() {
1092
+ this._listenSub?.unsubscribe();
1093
+ this._listenSub = null;
1094
+ }
1095
+ requestDevice() {
1096
+ if (this._discoverySub) {
1097
+ return Promise.resolve();
1098
+ }
1099
+ debugLog("[DMK] requestDevice() starting persistent BLE scan");
1100
+ this._discoverySub = this._dmk.startDiscovering({}).subscribe({
1101
+ next: (d) => {
1102
+ debugLog("[DMK] BLE discovered:", d.name || d.id);
1103
+ this._discovered.set(d.id, d);
1104
+ },
1105
+ error: (err) => {
1106
+ debugError("[DMK] BLE scan error:", err);
1107
+ this._discoverySub = null;
1108
+ }
1109
+ });
1110
+ return Promise.resolve();
1111
+ }
1112
+ /** Connect to a previously discovered device. Returns sessionId. */
1113
+ async connect(deviceId) {
1114
+ const device = this._discovered.get(deviceId);
1115
+ if (!device) {
1116
+ throw new Error(`Device "${deviceId}" not found. Call enumerate() or listen() first.`);
1117
+ }
1118
+ const sessionId = await this._dmk.connect({ device });
1119
+ this._sessions.set(deviceId, sessionId);
1120
+ this._sessionToDevice.set(sessionId, deviceId);
1121
+ return sessionId;
1122
+ }
1123
+ /** Disconnect a session. */
1124
+ async disconnect(sessionId) {
1125
+ await this._dmk.disconnect({ sessionId });
1126
+ const deviceId = this._sessionToDevice.get(sessionId);
1127
+ if (deviceId) this._sessions.delete(deviceId);
1128
+ this._sessionToDevice.delete(sessionId);
1129
+ }
1130
+ getSessionId(deviceId) {
1131
+ return this._sessions.get(deviceId);
1132
+ }
1133
+ getDeviceId(sessionId) {
1134
+ return this._sessionToDevice.get(sessionId);
1135
+ }
1136
+ /** Get the underlying DMK instance (needed by SignerManager). */
1137
+ getDmk() {
1138
+ return this._dmk;
1139
+ }
1140
+ stopDiscovery() {
1141
+ if (this._discoverySub) {
1142
+ this._discoverySub.unsubscribe();
1143
+ this._discoverySub = null;
1144
+ this._dmk.stopDiscovering();
1145
+ }
1146
+ }
1147
+ dispose() {
1148
+ this.stopListening();
1149
+ this.stopDiscovery();
1150
+ this._discovered.clear();
1151
+ this._sessions.clear();
1152
+ this._sessionToDevice.clear();
1153
+ this._dmk.close();
1154
+ }
1155
+ };
1156
+
1157
+ // src/signer/SignerManager.ts
1158
+ import { SignerEthBuilder } from "@ledgerhq/device-signer-kit-ethereum";
1159
+ import { ContextModuleBuilder } from "@ledgerhq/context-module";
1160
+
1161
+ // src/signer/SignerEth.ts
1162
+ import { hexToBytes } from "@onekeyfe/hwk-adapter-core";
1163
+
1164
+ // src/signer/deviceActionToPromise.ts
1165
+ import { DeviceActionStatus } from "@ledgerhq/device-management-kit";
1166
+ var DEFAULT_TIMEOUT_MS = 3e4;
1167
+ function deviceActionToPromise(action, onInteraction, timeoutMs = DEFAULT_TIMEOUT_MS, onRegisterCanceller) {
1168
+ return new Promise((resolve, reject) => {
1169
+ let settled = false;
1170
+ let lastStep;
1171
+ let sub;
1172
+ let timer = null;
1173
+ const cancelAction = () => {
1174
+ try {
1175
+ sub?.unsubscribe();
1176
+ } catch {
1177
+ }
1178
+ try {
1179
+ action.cancel();
1180
+ } catch {
1181
+ }
1182
+ };
1183
+ const resetTimer = () => {
1184
+ if (timer) clearTimeout(timer);
1185
+ if (timeoutMs > 0) {
1186
+ timer = setTimeout(() => {
1187
+ if (!settled) {
1188
+ settled = true;
1189
+ cancelAction();
1190
+ reject(new Error("Device action timed out \u2014 device may be locked or disconnected"));
1191
+ }
1192
+ }, timeoutMs);
1193
+ }
1194
+ };
1195
+ resetTimer();
1196
+ if (onRegisterCanceller) {
1197
+ onRegisterCanceller(() => {
1198
+ if (settled) return;
1199
+ settled = true;
1200
+ if (timer) clearTimeout(timer);
1201
+ cancelAction();
1202
+ reject(new Error("Device action cancelled"));
1203
+ });
1204
+ }
1205
+ debugLog("[DMK-Observable] subscribing to action.observable...");
1206
+ sub = action.observable.subscribe({
1207
+ next: (state) => {
1208
+ if (settled) return;
1209
+ resetTimer();
1210
+ const step = state?.intermediateValue?.step;
1211
+ if (step) lastStep = step;
1212
+ debugLog(
1213
+ "[DMK-Observable] state:",
1214
+ JSON.stringify({
1215
+ status: state.status,
1216
+ intermediateValue: state.status === DeviceActionStatus.Pending ? state.intermediateValue : void 0,
1217
+ hasOutput: state.status === DeviceActionStatus.Completed,
1218
+ hasError: state.status === DeviceActionStatus.Error
1219
+ })
1220
+ );
1221
+ if (state.status === DeviceActionStatus.Completed) {
1222
+ settled = true;
1223
+ if (timer) clearTimeout(timer);
1224
+ onInteraction?.("interaction-complete");
1225
+ sub?.unsubscribe();
1226
+ resolve(state.output);
1227
+ } else if (state.status === DeviceActionStatus.Error) {
1228
+ settled = true;
1229
+ if (timer) clearTimeout(timer);
1230
+ onInteraction?.("interaction-complete");
1231
+ sub?.unsubscribe();
1232
+ rejectWithLastStep(state.error, lastStep, reject);
1233
+ } else if (state.status === DeviceActionStatus.Pending && onInteraction) {
1234
+ const interaction = state.intermediateValue?.requiredUserInteraction;
1235
+ if (interaction && interaction !== "none") {
1236
+ onInteraction(String(interaction));
1237
+ }
1238
+ }
1239
+ },
1240
+ error: (err) => {
1241
+ if (!settled) {
1242
+ settled = true;
1243
+ if (timer) clearTimeout(timer);
1244
+ sub?.unsubscribe();
1245
+ rejectWithLastStep(err, lastStep, reject);
1246
+ }
1247
+ },
1248
+ complete: () => {
1249
+ if (!settled) {
1250
+ settled = true;
1251
+ if (timer) clearTimeout(timer);
1252
+ reject(new Error("Device action completed without result"));
1253
+ }
1254
+ }
1255
+ });
1256
+ });
1257
+ }
1258
+ function rejectWithLastStep(err, lastStep, reject) {
1259
+ if (err && typeof err === "object" && lastStep) {
1260
+ try {
1261
+ Object.defineProperty(err, "_lastStep", {
1262
+ value: lastStep,
1263
+ configurable: true,
1264
+ enumerable: false,
1265
+ writable: true
1266
+ });
1267
+ } catch {
1268
+ }
1269
+ }
1270
+ reject(err);
1271
+ }
1272
+
1273
+ // src/signer/SignerEth.ts
1274
+ var INTERACTIVE_TIMEOUT_MS = 5 * 6e4;
1275
+ var SignerEth = class {
1276
+ // eslint-disable-next-line no-useless-constructor, no-empty-function
1277
+ constructor(_sdk) {
1278
+ this._sdk = _sdk;
1279
+ }
1280
+ async getAddress(derivationPath, options) {
1281
+ const checkOnDevice = options?.checkOnDevice ?? false;
1282
+ debugLog("[DMK] getAddress \u2192 DMK:", { derivationPath, checkOnDevice });
1283
+ const action = this._sdk.getAddress(derivationPath, {
1284
+ checkOnDevice
1285
+ });
1286
+ const timeout = checkOnDevice ? INTERACTIVE_TIMEOUT_MS : void 0;
1287
+ return deviceActionToPromise(
1288
+ action,
1289
+ this.onInteraction,
1290
+ timeout,
1291
+ this.onRegisterCanceller
1292
+ );
1293
+ }
1294
+ async signTransaction(derivationPath, serializedTxHex) {
1295
+ const action = this._sdk.signTransaction(derivationPath, hexToBytes(serializedTxHex));
1296
+ return deviceActionToPromise(
1297
+ action,
1298
+ this.onInteraction,
1299
+ INTERACTIVE_TIMEOUT_MS,
1300
+ this.onRegisterCanceller
1301
+ );
1302
+ }
1303
+ async signMessage(derivationPath, message) {
1304
+ const action = this._sdk.signMessage(derivationPath, hexToBytes(message));
1305
+ return deviceActionToPromise(
1306
+ action,
1307
+ this.onInteraction,
1308
+ INTERACTIVE_TIMEOUT_MS,
1309
+ this.onRegisterCanceller
1310
+ );
1311
+ }
1312
+ async signTypedData(derivationPath, data) {
1313
+ const action = this._sdk.signTypedData(derivationPath, data);
1314
+ return deviceActionToPromise(
1315
+ action,
1316
+ this.onInteraction,
1317
+ INTERACTIVE_TIMEOUT_MS,
1318
+ this.onRegisterCanceller
1319
+ );
1320
+ }
1321
+ };
1322
+
1323
+ // src/signer/SignerManager.ts
1324
+ var SignerManager = class _SignerManager {
1325
+ constructor(dmk, builderFn) {
1326
+ this._dmk = dmk;
1327
+ this._builderFn = builderFn ?? _SignerManager._defaultBuilder();
1328
+ }
1329
+ async getOrCreate(sessionId) {
1330
+ debugLog("[DMK] SignerManager.getOrCreate:", { sessionId });
1331
+ const builder = await this._builderFn({ dmk: this._dmk, sessionId });
1332
+ return new SignerEth(builder.build());
1333
+ }
1334
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars, class-methods-use-this
1335
+ invalidate(_sessionId) {
1336
+ }
1337
+ // eslint-disable-next-line class-methods-use-this
1338
+ clearAll() {
1339
+ }
1340
+ static _defaultBuilder() {
1341
+ return (args) => {
1342
+ const contextModule = new ContextModuleBuilder({}).removeDefaultLoaders().build();
1343
+ return new SignerEthBuilder(args).withContextModule(contextModule);
1344
+ };
1345
+ }
1346
+ };
1347
+
1348
+ // src/connector/chains/evm.ts
1349
+ import { HardwareErrorCode as HardwareErrorCode3, padHex64, stripHex } from "@onekeyfe/hwk-adapter-core";
1350
+
1351
+ // src/connector/chains/utils.ts
1352
+ import { EConnectorInteraction } from "@onekeyfe/hwk-adapter-core";
1353
+ function normalizePath(path) {
1354
+ return path.startsWith("m/") ? path.slice(2) : path;
1355
+ }
1356
+ function collapseSignerInteraction(interaction) {
1357
+ switch (interaction) {
1358
+ case "confirm-open-app":
1359
+ return EConnectorInteraction.ConfirmOpenApp;
1360
+ case "unlock-device":
1361
+ return EConnectorInteraction.UnlockDevice;
1362
+ case "interaction-complete":
1363
+ return EConnectorInteraction.InteractionComplete;
1364
+ default:
1365
+ return EConnectorInteraction.ConfirmOnDevice;
1366
+ }
1367
+ }
1368
+
1369
+ // src/connector/chains/evm.ts
1370
+ async function evmGetAddress(ctx, sessionId, params) {
1371
+ const signer = await _getEthSigner(ctx, sessionId);
1372
+ const path = normalizePath(params.path);
1373
+ const checkOnDevice = params.showOnDevice ?? false;
1374
+ debugLog("[DMK] evmGetAddress -> signer.getAddress:", { path, checkOnDevice });
1375
+ try {
1376
+ const result = await signer.getAddress(path, { checkOnDevice });
1377
+ return { address: result.address, publicKey: result.publicKey };
1378
+ } catch (err) {
1379
+ ctx.invalidateSession(sessionId);
1380
+ throw ctx.wrapError(err);
1381
+ } finally {
1382
+ ctx.clearCanceller(sessionId);
1383
+ }
1384
+ }
1385
+ async function evmSignTransaction(ctx, sessionId, params) {
1386
+ if (!params.serializedTx) {
1387
+ throw Object.assign(
1388
+ new Error(
1389
+ "Ledger requires a pre-serialized transaction (serializedTx). Provide an RLP-encoded hex string."
1390
+ ),
1391
+ { code: HardwareErrorCode3.InvalidParams }
1392
+ );
1393
+ }
1394
+ const signer = await _getEthSigner(ctx, sessionId);
1395
+ const path = normalizePath(params.path);
1396
+ try {
1397
+ const result = await signer.signTransaction(path, params.serializedTx);
1398
+ return {
1399
+ v: `0x${result.v.toString(16)}`,
1400
+ r: padHex64(result.r),
1401
+ s: padHex64(result.s)
1402
+ };
1403
+ } catch (err) {
1404
+ ctx.invalidateSession(sessionId);
1405
+ throw ctx.wrapError(err);
1406
+ } finally {
1407
+ ctx.clearCanceller(sessionId);
1408
+ }
1409
+ }
1410
+ async function evmSignMessage(ctx, sessionId, params) {
1411
+ const signer = await _getEthSigner(ctx, sessionId);
1412
+ const path = normalizePath(params.path);
1413
+ try {
1414
+ const result = await signer.signMessage(path, params.message);
1415
+ const rHex = stripHex(result.r).padStart(64, "0");
1416
+ const sHex = stripHex(result.s).padStart(64, "0");
1417
+ const vHex = result.v.toString(16).padStart(2, "0");
1418
+ return { signature: `0x${rHex}${sHex}${vHex}` };
1419
+ } catch (err) {
1420
+ ctx.invalidateSession(sessionId);
1421
+ throw ctx.wrapError(err);
1422
+ } finally {
1423
+ ctx.clearCanceller(sessionId);
1424
+ }
1425
+ }
1426
+ async function evmSignTypedData(ctx, sessionId, params) {
1427
+ if (params.mode === "hash") {
1428
+ throw Object.assign(
1429
+ new Error(
1430
+ 'Ledger does not support hash-only EIP-712 signing. Use mode "full" with the complete typed data structure.'
1431
+ ),
1432
+ { code: HardwareErrorCode3.MethodNotSupported }
1433
+ );
1434
+ }
1435
+ const signer = await _getEthSigner(ctx, sessionId);
1436
+ const path = normalizePath(params.path);
1437
+ try {
1438
+ const result = await signer.signTypedData(path, params.data);
1439
+ const rHex = stripHex(result.r).padStart(64, "0");
1440
+ const sHex = stripHex(result.s).padStart(64, "0");
1441
+ const vHex = result.v.toString(16).padStart(2, "0");
1442
+ return { signature: `0x${rHex}${sHex}${vHex}` };
1443
+ } catch (err) {
1444
+ ctx.invalidateSession(sessionId);
1445
+ throw ctx.wrapError(err);
1446
+ } finally {
1447
+ ctx.clearCanceller(sessionId);
1448
+ }
1449
+ }
1450
+ async function _getEthSigner(ctx, sessionId) {
1451
+ const signerManager = await ctx.getSignerManager();
1452
+ const signer = await signerManager.getOrCreate(sessionId);
1453
+ signer.onInteraction = (interaction) => {
1454
+ debugLog("[LedgerConnector] evm.onInteraction:", interaction);
1455
+ ctx.emit("ui-event", {
1456
+ type: collapseSignerInteraction(interaction),
1457
+ payload: { sessionId }
1458
+ });
1459
+ };
1460
+ signer.onRegisterCanceller = (cancel) => {
1461
+ ctx.registerCanceller(sessionId, cancel);
1462
+ };
1463
+ return signer;
1464
+ }
1465
+
1466
+ // src/connector/chains/btc.ts
1467
+ import { HardwareErrorCode as HardwareErrorCode4, stripHex as stripHex2 } from "@onekeyfe/hwk-adapter-core";
1468
+ import { Psbt } from "bitcoinjs-lib";
1469
+
1470
+ // src/signer/SignerBtc.ts
1471
+ import { hexToBytes as hexToBytes2 } from "@onekeyfe/hwk-adapter-core";
1472
+ function hexToUtf8(hex) {
1473
+ return new TextDecoder().decode(hexToBytes2(hex));
1474
+ }
1475
+ var INTERACTIVE_TIMEOUT_MS2 = 5 * 6e4;
1476
+ var SignerBtc = class {
1477
+ // eslint-disable-next-line no-useless-constructor, no-empty-function
1478
+ constructor(_sdk) {
1479
+ this._sdk = _sdk;
1480
+ }
1481
+ async getWalletAddress(wallet, addressIndex, options) {
1482
+ const action = this._sdk.getWalletAddress(wallet, addressIndex, {
1483
+ checkOnDevice: options?.checkOnDevice ?? false,
1484
+ change: options?.change ?? false
1485
+ });
1486
+ return deviceActionToPromise(
1487
+ action,
1488
+ this.onInteraction,
1489
+ void 0,
1490
+ this.onRegisterCanceller
1491
+ );
1492
+ }
1493
+ async getExtendedPublicKey(derivationPath, options) {
1494
+ debugLog(
1495
+ "[SignerBtc] getExtendedPublicKey called, path:",
1496
+ derivationPath,
1497
+ "options:",
1498
+ JSON.stringify(options)
1499
+ );
1500
+ const action = this._sdk.getExtendedPublicKey(derivationPath, {
1501
+ checkOnDevice: options?.checkOnDevice ?? false
1502
+ });
1503
+ try {
1504
+ const result = await deviceActionToPromise(
1505
+ action,
1506
+ this.onInteraction,
1507
+ void 0,
1508
+ this.onRegisterCanceller
1509
+ );
1510
+ debugLog(
1511
+ "[SignerBtc] getExtendedPublicKey result type:",
1512
+ typeof result,
1513
+ "value:",
1514
+ typeof result === "string" ? `${result.substring(0, 20)}...` : JSON.stringify(result).substring(0, 50)
1515
+ );
1516
+ if (typeof result === "string") return result;
1517
+ return result.extendedPublicKey;
1518
+ } catch (err) {
1519
+ debugError("[SignerBtc] getExtendedPublicKey error:", err);
1520
+ throw err;
1521
+ }
1522
+ }
1523
+ async getMasterFingerprint(options) {
1524
+ const action = this._sdk.getMasterFingerprint(options);
1525
+ const result = await deviceActionToPromise(
1526
+ action,
1527
+ this.onInteraction,
1528
+ void 0,
1529
+ this.onRegisterCanceller
1530
+ );
1531
+ return result.masterFingerprint;
1532
+ }
1533
+ /**
1534
+ * Sign a PSBT and return the array of partial signatures.
1535
+ * The `wallet` param is a DefaultWallet or WalletPolicy instance.
1536
+ * The `psbt` param can be a hex string, base64 string, or Uint8Array.
1537
+ */
1538
+ async signPsbt(wallet, psbt, options) {
1539
+ const action = this._sdk.signPsbt(wallet, psbt, options);
1540
+ return deviceActionToPromise(
1541
+ action,
1542
+ this.onInteraction,
1543
+ INTERACTIVE_TIMEOUT_MS2,
1544
+ this.onRegisterCanceller
1545
+ );
1546
+ }
1547
+ /**
1548
+ * Sign a PSBT and return the fully extracted raw transaction as a hex string.
1549
+ * Like signPsbt, but also finalises the PSBT and extracts the transaction.
1550
+ */
1551
+ async signTransaction(wallet, psbt, options) {
1552
+ const action = this._sdk.signTransaction(wallet, psbt, options);
1553
+ return deviceActionToPromise(
1554
+ action,
1555
+ this.onInteraction,
1556
+ INTERACTIVE_TIMEOUT_MS2,
1557
+ this.onRegisterCanceller
1558
+ );
1559
+ }
1560
+ /**
1561
+ * Sign a message with the BTC app (BIP-137 / "Bitcoin Signed Message").
1562
+ * Returns `{ r, s, v }` signature object.
1563
+ */
1564
+ async signMessage(derivationPath, message, options) {
1565
+ const action = this._sdk.signMessage(derivationPath, hexToUtf8(message), options);
1566
+ return deviceActionToPromise(
1567
+ action,
1568
+ this.onInteraction,
1569
+ INTERACTIVE_TIMEOUT_MS2,
1570
+ this.onRegisterCanceller
1571
+ );
1572
+ }
1573
+ };
1574
+
1575
+ // src/connector/chains/btc.ts
1576
+ function _purposeToTemplate(purpose, DDT) {
1577
+ switch (purpose) {
1578
+ case "44":
1579
+ return DDT.LEGACY;
1580
+ case "49":
1581
+ return DDT.NESTED_SEGWIT;
1582
+ case "84":
1583
+ return DDT.NATIVE_SEGWIT;
1584
+ case "86":
1585
+ return DDT.TAPROOT;
1586
+ default:
1587
+ throw Object.assign(new Error(`Unsupported BTC purpose: m/${purpose ?? "<undefined>"}'`), {
1588
+ code: HardwareErrorCode4.InvalidParams
1589
+ });
1590
+ }
1591
+ }
1592
+ async function btcGetAddress(ctx, sessionId, params) {
1593
+ const btcSigner = await _createBtcSigner(ctx, sessionId);
1594
+ const path = normalizePath(params.path);
1595
+ try {
1596
+ const { DefaultWallet, DefaultDescriptorTemplate } = await ctx.importLedgerKit(
1597
+ "@ledgerhq/device-signer-kit-bitcoin"
1598
+ );
1599
+ const purpose = path.split("/")[0]?.replace(/'/g, "");
1600
+ const template = _purposeToTemplate(purpose, DefaultDescriptorTemplate);
1601
+ const wallet = new DefaultWallet(path, template);
1602
+ debugLog("[LedgerConnector] btcGetAddress params:", {
1603
+ path,
1604
+ purpose,
1605
+ template,
1606
+ addressIndex: params.addressIndex,
1607
+ change: params.change,
1608
+ showOnDevice: params.showOnDevice
1609
+ });
1610
+ const result = await btcSigner.getWalletAddress(wallet, params.addressIndex ?? 0, {
1611
+ checkOnDevice: params.showOnDevice ?? false,
1612
+ change: params.change ?? false
1613
+ });
1614
+ return { address: result.address, path: params.path };
1615
+ } catch (err) {
1616
+ ctx.invalidateSession(sessionId);
1617
+ throw ctx.wrapError(err);
1618
+ } finally {
1619
+ ctx.clearCanceller(sessionId);
1620
+ }
1621
+ }
1622
+ async function btcGetPublicKey(ctx, sessionId, params) {
1623
+ const btcSigner = await _createBtcSigner(ctx, sessionId);
1624
+ const path = normalizePath(params.path);
1625
+ debugLog("[LedgerConnector] btcGetPublicKey called, path:", path, "sessionId:", sessionId);
1626
+ try {
1627
+ const xpub = await btcSigner.getExtendedPublicKey(path, {
1628
+ checkOnDevice: params.showOnDevice ?? false
1629
+ });
1630
+ debugLog("[LedgerConnector] btcGetPublicKey success, xpub:", `${xpub?.substring(0, 20)}...`);
1631
+ return { xpub, path: params.path };
1632
+ } catch (err) {
1633
+ debugError("[LedgerConnector] btcGetPublicKey error, path:", path, "err:", err);
1634
+ ctx.invalidateSession(sessionId);
1635
+ throw ctx.wrapError(err);
1636
+ } finally {
1637
+ ctx.clearCanceller(sessionId);
1638
+ }
1639
+ }
1640
+ async function btcSignTransaction(ctx, sessionId, params) {
1641
+ if (!params.psbt) {
1642
+ throw Object.assign(
1643
+ new Error("Ledger requires PSBT format for BTC transaction signing. Provide params.psbt."),
1644
+ { code: HardwareErrorCode4.InvalidParams }
1645
+ );
1646
+ }
1647
+ const btcSigner = await _createBtcSigner(ctx, sessionId);
1648
+ try {
1649
+ const { DefaultWallet, DefaultDescriptorTemplate } = await ctx.importLedgerKit(
1650
+ "@ledgerhq/device-signer-kit-bitcoin"
1651
+ );
1652
+ const path = normalizePath(params.path || "84'/0'/0'");
1653
+ const purpose = path.split("/")[0]?.replace(/'/g, "");
1654
+ const template = _purposeToTemplate(purpose, DefaultDescriptorTemplate);
1655
+ debugLog("[LedgerConnector] btcSignTransaction wallet:", { path, purpose, template });
1656
+ const wallet = new DefaultWallet(path, template);
1657
+ const signedTxHex = await btcSigner.signTransaction(wallet, params.psbt);
1658
+ return { serializedTx: stripHex2(signedTxHex) };
1659
+ } catch (err) {
1660
+ ctx.invalidateSession(sessionId);
1661
+ throw ctx.wrapError(err);
1662
+ } finally {
1663
+ ctx.clearCanceller(sessionId);
1664
+ }
1665
+ }
1666
+ async function btcSignPsbt(ctx, sessionId, params) {
1667
+ if (!params.psbt) {
1668
+ throw Object.assign(new Error("btcSignPsbt requires params.psbt"), {
1669
+ code: HardwareErrorCode4.InvalidParams
1670
+ });
1671
+ }
1672
+ const btcSigner = await _createBtcSigner(ctx, sessionId);
1673
+ try {
1674
+ const { DefaultWallet, DefaultDescriptorTemplate } = await ctx.importLedgerKit(
1675
+ "@ledgerhq/device-signer-kit-bitcoin"
1676
+ );
1677
+ const path = normalizePath(params.path || "84'/0'/0'");
1678
+ const purpose = path.split("/")[0]?.replace(/'/g, "");
1679
+ const template = _purposeToTemplate(purpose, DefaultDescriptorTemplate);
1680
+ debugLog("[LedgerConnector] btcSignPsbt wallet:", { path, purpose, template });
1681
+ const wallet = new DefaultWallet(path, template);
1682
+ const signatures = await btcSigner.signPsbt(wallet, params.psbt);
1683
+ debugLog("[LedgerConnector] btcSignPsbt signatures received:", signatures.length);
1684
+ const signedPsbtHex = _applySignaturesToPsbt(params.psbt, signatures);
1685
+ return { signedPsbt: signedPsbtHex };
1686
+ } catch (err) {
1687
+ ctx.invalidateSession(sessionId);
1688
+ throw ctx.wrapError(err);
1689
+ } finally {
1690
+ ctx.clearCanceller(sessionId);
1691
+ }
1692
+ }
1693
+ async function btcSignMessage(ctx, sessionId, params) {
1694
+ const btcSigner = await _createBtcSigner(ctx, sessionId);
1695
+ const path = normalizePath(params.path);
1696
+ try {
1697
+ const result = await btcSigner.signMessage(path, params.message);
1698
+ const vHex = result.v.toString(16).padStart(2, "0");
1699
+ const rHex = stripHex2(result.r).padStart(64, "0");
1700
+ const sHex = stripHex2(result.s).padStart(64, "0");
1701
+ return { signature: `${vHex}${rHex}${sHex}` };
1702
+ } catch (err) {
1703
+ ctx.invalidateSession(sessionId);
1704
+ throw ctx.wrapError(err);
1705
+ } finally {
1706
+ ctx.clearCanceller(sessionId);
1707
+ }
1708
+ }
1709
+ async function btcGetMasterFingerprint(ctx, sessionId, params) {
1710
+ const btcSigner = await _createBtcSigner(ctx, sessionId);
1711
+ try {
1712
+ const fingerprint = await btcSigner.getMasterFingerprint({
1713
+ skipOpenApp: params?.skipOpenApp
1714
+ });
1715
+ const hex = Array.from(fingerprint).map((b) => b.toString(16).padStart(2, "0")).join("");
1716
+ return { masterFingerprint: hex };
1717
+ } catch (err) {
1718
+ ctx.invalidateSession(sessionId);
1719
+ throw ctx.wrapError(err);
1720
+ } finally {
1721
+ ctx.clearCanceller(sessionId);
1722
+ }
1723
+ }
1724
+ async function _createBtcSigner(ctx, sessionId) {
1725
+ const dmk = await ctx.getOrCreateDmk();
1726
+ const { SignerBtcBuilder } = await ctx.importLedgerKit("@ledgerhq/device-signer-kit-bitcoin");
1727
+ const sdkSigner = new SignerBtcBuilder({ dmk, sessionId }).build();
1728
+ const signer = new SignerBtc(sdkSigner);
1729
+ signer.onInteraction = (interaction) => {
1730
+ debugLog("[LedgerConnector] btc.onInteraction:", interaction);
1731
+ ctx.emit("ui-event", {
1732
+ type: collapseSignerInteraction(interaction),
1733
+ payload: { sessionId }
1734
+ });
1735
+ };
1736
+ signer.onRegisterCanceller = (cancel) => {
1737
+ ctx.registerCanceller(sessionId, cancel);
1738
+ };
1739
+ return signer;
1740
+ }
1741
+ function _isTaprootInput(input) {
1742
+ if (input.tapInternalKey || input.tapKeySig || input.tapLeafScript || input.tapMerkleRoot || input.tapBip32Derivation || input.tapScriptSig)
1743
+ return true;
1744
+ const script = input.witnessUtxo?.script;
1745
+ return !!(script && script.length === 34 && script[0] === 81 && script[1] === 32);
1746
+ }
1747
+ function _isScriptPathInput(input) {
1748
+ return !!(input.tapLeafScript && input.tapLeafScript.length > 0);
1749
+ }
1750
+ function _applySignaturesToPsbt(psbtHex, signatures) {
1751
+ const psbt = Psbt.fromHex(psbtHex);
1752
+ for (const sig of signatures) {
1753
+ const input = psbt.data.inputs[sig.inputIndex];
1754
+ if (!input) {
1755
+ throw new Error(`_applySignaturesToPsbt: no PSBT input at index ${sig.inputIndex}`);
1756
+ }
1757
+ const taproot = _isTaprootInput(input);
1758
+ const scriptPath = taproot && _isScriptPathInput(input);
1759
+ const hasLeafHash = !!(sig.tapleafHash && sig.tapleafHash.length > 0);
1760
+ if (scriptPath) {
1761
+ if (sig.pubkey.length !== 32)
1762
+ throw new Error(
1763
+ `input ${sig.inputIndex}: taproot script-path pubkey must be 32B, got ${sig.pubkey.length}`
1764
+ );
1765
+ if (!hasLeafHash)
1766
+ throw new Error(
1767
+ `input ${sig.inputIndex}: taproot script-path signature missing tapleafHash`
1768
+ );
1769
+ psbt.updateInput(sig.inputIndex, {
1770
+ tapScriptSig: [
1771
+ { pubkey: sig.pubkey, leafHash: sig.tapleafHash, signature: sig.signature }
1772
+ ]
1773
+ });
1774
+ } else if (taproot) {
1775
+ if (sig.pubkey.length !== 32)
1776
+ throw new Error(
1777
+ `input ${sig.inputIndex}: taproot key-path pubkey must be 32B, got ${sig.pubkey.length}`
1778
+ );
1779
+ if (hasLeafHash)
1780
+ throw new Error(
1781
+ `input ${sig.inputIndex}: taproot key-path signature must not carry tapleafHash`
1782
+ );
1783
+ psbt.updateInput(sig.inputIndex, { tapKeySig: sig.signature });
1784
+ } else {
1785
+ if (sig.pubkey.length !== 33 && sig.pubkey.length !== 65)
1786
+ throw new Error(
1787
+ `input ${sig.inputIndex}: ECDSA pubkey must be 33B or 65B, got ${sig.pubkey.length}`
1788
+ );
1789
+ if (hasLeafHash)
1790
+ throw new Error(`input ${sig.inputIndex}: ECDSA signature must not carry tapleafHash`);
1791
+ psbt.updateInput(sig.inputIndex, {
1792
+ partialSig: [{ pubkey: sig.pubkey, signature: sig.signature }]
1793
+ });
1794
+ }
1795
+ }
1796
+ return psbt.toHex();
1797
+ }
1798
+
1799
+ // src/connector/chains/sol.ts
1800
+ import { bytesToHex, hexToBytes as hexToBytes3 } from "@onekeyfe/hwk-adapter-core";
1801
+
1802
+ // src/signer/SignerSol.ts
1803
+ var SignerSol = class {
1804
+ // eslint-disable-next-line no-useless-constructor, no-empty-function
1805
+ constructor(_sdk) {
1806
+ this._sdk = _sdk;
1807
+ }
1808
+ /**
1809
+ * Get the Solana address (base58-encoded Ed25519 public key) at the given derivation path.
1810
+ */
1811
+ async getAddress(derivationPath, options) {
1812
+ const action = this._sdk.getAddress(derivationPath, {
1813
+ checkOnDevice: options?.checkOnDevice ?? false
1814
+ });
1815
+ return deviceActionToPromise(
1816
+ action,
1817
+ this.onInteraction,
1818
+ void 0,
1819
+ this.onRegisterCanceller
1820
+ );
1821
+ }
1822
+ /**
1823
+ * Sign a Solana transaction.
1824
+ */
1825
+ async signTransaction(derivationPath, transaction, options) {
1826
+ const action = this._sdk.signTransaction(derivationPath, transaction, options);
1827
+ return deviceActionToPromise(
1828
+ action,
1829
+ this.onInteraction,
1830
+ void 0,
1831
+ this.onRegisterCanceller
1832
+ );
1833
+ }
1834
+ /**
1835
+ * Sign a message with the Solana app.
1836
+ * DMK returns { signature: string } for signMessage (unlike signTransaction which returns Uint8Array).
1837
+ */
1838
+ async signMessage(derivationPath, message, options) {
1839
+ const action = this._sdk.signMessage(derivationPath, message, options);
1840
+ return deviceActionToPromise(
1841
+ action,
1842
+ this.onInteraction,
1843
+ void 0,
1844
+ this.onRegisterCanceller
1845
+ );
1846
+ }
1847
+ };
1848
+
1849
+ // src/connector/chains/sol.ts
1850
+ async function solGetAddress(ctx, sessionId, params) {
1851
+ const solSigner = await _createSolSigner(ctx, sessionId);
1852
+ const path = normalizePath(params.path);
1853
+ try {
1854
+ const publicKey = await solSigner.getAddress(path, {
1855
+ checkOnDevice: params.showOnDevice ?? false
1856
+ });
1857
+ return { address: publicKey, path: params.path };
1858
+ } catch (err) {
1859
+ ctx.invalidateSession(sessionId);
1860
+ throw ctx.wrapError(err);
1861
+ } finally {
1862
+ ctx.clearCanceller(sessionId);
1863
+ }
1864
+ }
1865
+ async function solSignTransaction(ctx, sessionId, params) {
1866
+ const solSigner = await _createSolSigner(ctx, sessionId);
1867
+ const path = normalizePath(params.path);
1868
+ const txBytes = hexToBytes3(params.serializedTx);
1869
+ try {
1870
+ const result = await solSigner.signTransaction(path, txBytes);
1871
+ return { signature: bytesToHex(result) };
1872
+ } catch (err) {
1873
+ ctx.invalidateSession(sessionId);
1874
+ throw ctx.wrapError(err);
1875
+ } finally {
1876
+ ctx.clearCanceller(sessionId);
1877
+ }
1878
+ }
1879
+ async function solSignMessage(ctx, sessionId, params) {
1880
+ const solSigner = await _createSolSigner(ctx, sessionId);
1881
+ const path = normalizePath(params.path);
1882
+ const messageBytes = hexToBytes3(params.message);
1883
+ try {
1884
+ const result = await solSigner.signMessage(path, messageBytes);
1885
+ return { signature: result.signature };
1886
+ } catch (err) {
1887
+ ctx.invalidateSession(sessionId);
1888
+ throw ctx.wrapError(err);
1889
+ } finally {
1890
+ ctx.clearCanceller(sessionId);
1891
+ }
1892
+ }
1893
+ async function _createSolSigner(ctx, sessionId) {
1894
+ const dmk = await ctx.getOrCreateDmk();
1895
+ const { ContextModuleBuilder: ContextModuleBuilder2 } = await ctx.importLedgerKit("@ledgerhq/context-module");
1896
+ const { SignerSolanaBuilder } = await ctx.importLedgerKit("@ledgerhq/device-signer-kit-solana");
1897
+ const contextModule = new ContextModuleBuilder2({}).removeDefaultLoaders().build();
1898
+ const sdkSigner = new SignerSolanaBuilder({ dmk, sessionId }).withContextModule(contextModule).build();
1899
+ const signer = new SignerSol(sdkSigner);
1900
+ signer.onInteraction = (interaction) => {
1901
+ debugLog("[LedgerConnector] sol.onInteraction:", interaction);
1902
+ ctx.emit("ui-event", {
1903
+ type: collapseSignerInteraction(interaction),
1904
+ payload: { sessionId }
1905
+ });
1906
+ };
1907
+ signer.onRegisterCanceller = (cancel) => {
1908
+ ctx.registerCanceller(sessionId, cancel);
1909
+ };
1910
+ return signer;
1911
+ }
1912
+
1913
+ // src/connector/chains/tron.ts
1914
+ import { HardwareErrorCode as HardwareErrorCode5 } from "@onekeyfe/hwk-adapter-core";
1915
+ import Trx from "@ledgerhq/hw-app-trx";
1916
+
1917
+ // src/connector/chains/legacyChainCall.ts
1918
+ import { EConnectorInteraction as EConnectorInteraction2 } from "@onekeyfe/hwk-adapter-core";
1919
+
1920
+ // src/app/AppManager.ts
1921
+ import {
1922
+ CloseAppCommand,
1923
+ GetAppAndVersionCommand,
1924
+ OpenAppCommand,
1925
+ isSuccessCommandResult
1926
+ } from "@ledgerhq/device-management-kit";
1927
+ var APP_NAME_MAP = {
1928
+ ETH: "Ethereum",
1929
+ BTC: "Bitcoin",
1930
+ SOL: "Solana",
1931
+ TRX: "Tron",
1932
+ XRP: "XRP",
1933
+ ADA: "Cardano",
1934
+ DOT: "Polkadot",
1935
+ ATOM: "Cosmos"
1936
+ };
1937
+ var DASHBOARD_APP_NAME = "BOLOS";
1938
+ var AppManager = class {
1939
+ constructor(dmk, options) {
1940
+ this._dmk = dmk;
1941
+ this._waitMs = options?.waitMs ?? 1e3;
1942
+ this._maxRetries = options?.maxRetries ?? 10;
1943
+ }
1944
+ /**
1945
+ * Return the Ledger app name for a given chain ticker,
1946
+ * or undefined if the chain is not supported.
1947
+ */
1948
+ static getAppName(chain) {
1949
+ return APP_NAME_MAP[chain];
1950
+ }
1951
+ /**
1952
+ * Ensure the target app is open on the device identified by `sessionId`.
1953
+ *
1954
+ * Flow:
1955
+ * 1. Check the currently running app.
1956
+ * 2. If it is already the target, return immediately.
1957
+ * 3. If a different app is running (not dashboard), close it first.
1958
+ * 4. Open the target app.
1959
+ * 5. Poll until the device confirms the target app is running.
1960
+ */
1961
+ /**
1962
+ * @param onConfirmOnDevice Called BEFORE OpenAppCommand is issued — the
1963
+ * device is about to display "Open <app>" on screen and wait for the
1964
+ * user's button press. UI consumers should show their "open app" prompt
1965
+ * in response. NOT called when the target app is already open (no user
1966
+ * interaction needed in that case).
1967
+ *
1968
+ * Important: OpenAppCommand is blocking. It does not resolve until the user
1969
+ * has physically confirmed on the device, so anything that runs AFTER
1970
+ * `await this._openApp(...)` lands AFTER the prompt is already gone.
1971
+ * Hence the callback must fire BEFORE that await.
1972
+ */
1973
+ async ensureAppOpen(sessionId, targetAppName, onConfirmOnDevice) {
1974
+ const currentApp = await this._getCurrentApp(sessionId);
1975
+ if (currentApp === targetAppName) {
1976
+ return;
1977
+ }
1978
+ if (!this._isDashboard(currentApp)) {
1979
+ await this._closeCurrentApp(sessionId);
1980
+ await this._waitForApp(sessionId, DASHBOARD_APP_NAME);
1981
+ }
1982
+ onConfirmOnDevice?.();
1983
+ await this._openApp(sessionId, targetAppName);
1984
+ await this._waitForApp(sessionId, targetAppName);
1985
+ }
1986
+ // ---------------------------------------------------------------------------
1987
+ // Private helpers
1988
+ // ---------------------------------------------------------------------------
1989
+ async _getCurrentApp(sessionId) {
1990
+ const result = await this._dmk.sendCommand({
1991
+ sessionId,
1992
+ command: new GetAppAndVersionCommand()
1993
+ });
1994
+ if (isSuccessCommandResult(result)) {
1995
+ debugLog("[AppManager] currentApp:", result.data.name);
1996
+ return result.data.name;
1997
+ }
1998
+ throw new Error("Failed to get current app from device");
1999
+ }
2000
+ async _openApp(sessionId, appName) {
2001
+ const result = await this._dmk.sendCommand({
2002
+ sessionId,
2003
+ command: new OpenAppCommand({ appName })
2004
+ });
2005
+ if (!isSuccessCommandResult(result)) {
2006
+ const { statusCode } = result;
2007
+ debugLog("[AppManager] openApp failed:", appName, "statusCode:", statusCode);
2008
+ throw Object.assign(new Error(`Failed to open "${appName}"`), {
2009
+ _tag: "OpenAppCommandError",
2010
+ errorCode: String(statusCode ?? ""),
2011
+ statusCode,
2012
+ appName
2013
+ });
2014
+ }
2015
+ }
2016
+ async _closeCurrentApp(sessionId) {
2017
+ debugLog("[AppManager] closeCurrentApp");
2018
+ await this._dmk.sendCommand({
2019
+ sessionId,
2020
+ command: new CloseAppCommand()
2021
+ });
2022
+ }
2023
+ /**
2024
+ * Poll the device until the expected app is reported as running,
2025
+ * or throw after `_maxRetries` attempts.
2026
+ */
2027
+ async _waitForApp(sessionId, expectedAppName) {
2028
+ let lastSeen = "";
2029
+ for (let i = 0; i < this._maxRetries; i++) {
2030
+ await this._wait();
2031
+ const current = await this._getCurrentApp(sessionId);
2032
+ lastSeen = current;
2033
+ if (current === expectedAppName) {
2034
+ return;
2035
+ }
2036
+ }
2037
+ debugLog(
2038
+ "[AppManager] waitForApp exhausted: expected=",
2039
+ expectedAppName,
2040
+ "lastSeen=",
2041
+ lastSeen
2042
+ );
2043
+ throw new Error(
2044
+ `Ledger: failed to open "${expectedAppName}" after ${this._maxRetries} retries (last seen: ${lastSeen})`
2045
+ );
2046
+ }
2047
+ _isDashboard(appName) {
2048
+ return appName === DASHBOARD_APP_NAME;
2049
+ }
2050
+ _wait() {
2051
+ return new Promise((resolve) => setTimeout(resolve, this._waitMs));
2052
+ }
2053
+ };
2054
+
2055
+ // src/connector/chains/legacyChainCall.ts
2056
+ function isLegacyWrongAppError(err, _appName) {
2057
+ return isWrongAppError(err);
2058
+ }
2059
+ async function withLegacyChainCall(ctx, sessionId, options, action) {
2060
+ const { appName, needsConfirmation } = options;
2061
+ let openAppPromptShown = false;
2062
+ const onAppOpenPrompt = () => {
2063
+ openAppPromptShown = true;
2064
+ ctx.emit("ui-event", {
2065
+ type: EConnectorInteraction2.ConfirmOpenApp,
2066
+ payload: { sessionId }
2067
+ });
2068
+ };
2069
+ const closeOpenAppUiIfShown = () => {
2070
+ if (openAppPromptShown) {
2071
+ ctx.emit("ui-event", {
2072
+ type: EConnectorInteraction2.InteractionComplete,
2073
+ payload: { sessionId }
2074
+ });
2075
+ openAppPromptShown = false;
2076
+ }
2077
+ };
2078
+ try {
2079
+ await _ensureAppOpen(ctx, sessionId, appName, onAppOpenPrompt);
2080
+ } catch (err) {
2081
+ debugLog(
2082
+ "[LegacyChainCall] pre-flight ensureAppOpen failed:",
2083
+ appName,
2084
+ err?.message
2085
+ );
2086
+ closeOpenAppUiIfShown();
2087
+ throw ctx.wrapError(err, { defaultAppName: appName });
2088
+ }
2089
+ const runOnce = async () => {
2090
+ let confirmEmitted = false;
2091
+ if (needsConfirmation) {
2092
+ ctx.emit("ui-event", {
2093
+ type: EConnectorInteraction2.ConfirmOnDevice,
2094
+ payload: { sessionId }
2095
+ });
2096
+ confirmEmitted = true;
2097
+ }
2098
+ try {
2099
+ return await action(sessionId);
2100
+ } finally {
2101
+ if (confirmEmitted || openAppPromptShown) {
2102
+ ctx.emit("ui-event", {
2103
+ type: EConnectorInteraction2.InteractionComplete,
2104
+ payload: { sessionId }
2105
+ });
2106
+ openAppPromptShown = false;
2107
+ }
2108
+ }
2109
+ };
2110
+ try {
2111
+ return await runOnce();
2112
+ } catch (err) {
2113
+ if (!isLegacyWrongAppError(err, appName)) {
2114
+ debugLog("[LegacyChainCall] non-wrong-app failure:", appName, err?.message);
2115
+ ctx.invalidateSession(sessionId);
2116
+ throw ctx.wrapError(err, { defaultAppName: appName });
2117
+ }
2118
+ debugLog("[LegacyChainCall] wrong-app detected, retrying:", appName);
2119
+ try {
2120
+ await _ensureAppOpen(ctx, sessionId, appName, onAppOpenPrompt);
2121
+ } catch (switchErr) {
2122
+ debugLog(
2123
+ "[LegacyChainCall] retry ensureAppOpen failed:",
2124
+ appName,
2125
+ switchErr?.message
2126
+ );
2127
+ closeOpenAppUiIfShown();
2128
+ throw ctx.wrapError(switchErr, { defaultAppName: appName });
2129
+ }
2130
+ ctx.clearAllSigners();
2131
+ const result = await runOnce();
2132
+ debugLog("[LegacyChainCall] retry succeeded:", appName);
2133
+ return result;
2134
+ }
2135
+ }
2136
+ async function _ensureAppOpen(ctx, sessionId, appName, onPrompt) {
2137
+ const dmk = await ctx.getOrCreateDmk();
2138
+ const appManager = new AppManager(dmk);
2139
+ await appManager.ensureAppOpen(sessionId, appName, onPrompt);
2140
+ }
2141
+
2142
+ // src/transport/DmkTransport.ts
2143
+ import Transport from "@ledgerhq/hw-transport";
2144
+ var DmkTransport = class extends Transport {
2145
+ constructor(dmk, sessionId) {
2146
+ super();
2147
+ this._dmk = dmk;
2148
+ this._sessionId = sessionId;
2149
+ }
2150
+ async exchange(apdu) {
2151
+ const response = await this._dmk.sendApdu({
2152
+ sessionId: this._sessionId,
2153
+ apdu: new Uint8Array(apdu)
2154
+ });
2155
+ const { data, statusCode } = response;
2156
+ const result = Buffer.alloc(data.length + 2);
2157
+ if (data.length > 0) {
2158
+ result.set(data, 0);
2159
+ }
2160
+ result.set(statusCode, data.length);
2161
+ return result;
2162
+ }
2163
+ async close() {
2164
+ }
2165
+ };
2166
+
2167
+ // src/connector/chains/tron.ts
2168
+ async function tronGetAddress(ctx, sessionId, params) {
2169
+ const path = normalizePath(params.path);
2170
+ const showOnDevice = params.showOnDevice ?? false;
2171
+ return withLegacyChainCall(
2172
+ ctx,
2173
+ sessionId,
2174
+ {
2175
+ appName: "Tron",
2176
+ // Only show "confirm on device" UI when the device is actually going
2177
+ // to display the address for the user to verify.
2178
+ needsConfirmation: showOnDevice
2179
+ },
2180
+ async (sid) => {
2181
+ const trx = await _createTrx(ctx, sid);
2182
+ const result = await trx.getAddress(path, showOnDevice);
2183
+ return { address: result.address, publicKey: result.publicKey, path: params.path };
2184
+ }
2185
+ );
2186
+ }
2187
+ async function tronSignTransaction(ctx, sessionId, params) {
2188
+ if (!params.rawTxHex) {
2189
+ throw Object.assign(
2190
+ new Error("TRON signing requires a protobuf-encoded raw transaction hex (rawTxHex)."),
2191
+ { code: HardwareErrorCode5.InvalidParams }
2192
+ );
2193
+ }
2194
+ const path = normalizePath(params.path);
2195
+ return withLegacyChainCall(
2196
+ ctx,
2197
+ sessionId,
2198
+ { appName: "Tron", needsConfirmation: true },
2199
+ async (sid) => {
2200
+ const trx = await _createTrx(ctx, sid);
2201
+ const signature = await trx.signTransaction(
2202
+ path,
2203
+ params.rawTxHex,
2204
+ params.tokenSignatures ?? []
2205
+ );
2206
+ return { signature };
2207
+ }
2208
+ );
2209
+ }
2210
+ async function tronSignMessage(ctx, sessionId, params) {
2211
+ const path = normalizePath(params.path);
2212
+ return withLegacyChainCall(
2213
+ ctx,
2214
+ sessionId,
2215
+ { appName: "Tron", needsConfirmation: true },
2216
+ async (sid) => {
2217
+ const trx = await _createTrx(ctx, sid);
2218
+ const signature = await trx.signPersonalMessage(path, params.messageHex);
2219
+ return { signature };
2220
+ }
2221
+ );
2222
+ }
2223
+ async function _createTrx(ctx, sessionId) {
2224
+ const dmk = await ctx.getOrCreateDmk();
2225
+ return new Trx(new DmkTransport(dmk, sessionId));
2226
+ }
2227
+
2228
+ // src/connector/LedgerConnectorBase.ts
2229
+ var METHOD_PREFIX_TO_APP_NAME = {
2230
+ evm: "Ethereum",
2231
+ btc: "Bitcoin",
2232
+ sol: "Solana",
2233
+ tron: "Tron"
2234
+ };
2235
+ async function defaultLedgerKitImporter(pkg) {
2236
+ switch (pkg) {
2237
+ case "@ledgerhq/device-management-kit":
2238
+ return import("@ledgerhq/device-management-kit");
2239
+ case "@ledgerhq/device-signer-kit-ethereum":
2240
+ return import("@ledgerhq/device-signer-kit-ethereum");
2241
+ case "@ledgerhq/device-signer-kit-bitcoin":
2242
+ return import("@ledgerhq/device-signer-kit-bitcoin");
2243
+ case "@ledgerhq/device-signer-kit-solana":
2244
+ return import("@ledgerhq/device-signer-kit-solana");
2245
+ case "@ledgerhq/context-module":
2246
+ return import("@ledgerhq/context-module");
2247
+ default:
2248
+ throw new Error(`Unknown Ledger kit package: ${pkg}`);
2249
+ }
2250
+ }
2251
+ var LedgerConnectorBase = class {
2252
+ constructor(createTransport, options) {
2253
+ this._deviceManager = null;
2254
+ this._signerManager = null;
2255
+ this._dmk = null;
2256
+ this._eventHandlers = /* @__PURE__ */ new Map();
2257
+ // ---------------------------------------------------------------------------
2258
+ // ConnectId <-> DMK path mapping
2259
+ //
2260
+ // DMK uses internal paths (BLE MAC, USB UUID) that may change across sessions.
2261
+ // _resolveConnectId() maps these to stable external IDs (BLE: "A58F", USB: same).
2262
+ // This bidirectional map is the SINGLE SOURCE OF TRUTH for all connectId usage.
2263
+ // ---------------------------------------------------------------------------
2264
+ this._connectIdToPath = /* @__PURE__ */ new Map();
2265
+ // "A58F" -> "D5:75:7D:4B:51:E8"
2266
+ this._pathToConnectId = /* @__PURE__ */ new Map();
2267
+ // ---------------------------------------------------------------------------
2268
+ // Per-session DeviceAction cancellers
2269
+ //
2270
+ // Each chain handler registers its active DeviceAction's canceller via
2271
+ // ctx.registerCanceller(sessionId, cancel) and clears it on completion.
2272
+ // IConnector.cancel(sessionId) invokes the registered canceller, which
2273
+ // unsubscribes the observable and releases DMK's IntentQueue slot.
2274
+ // ---------------------------------------------------------------------------
2275
+ this._cancellers = /* @__PURE__ */ new Map();
2276
+ this._createTransport = createTransport;
2277
+ this.connectionType = options?.connectionType ?? "usb";
2278
+ this._providedDmk = options?.dmk;
2279
+ this._importLedgerKit = options?.importLedgerKit ?? defaultLedgerKitImporter;
2280
+ if (this._providedDmk) {
2281
+ this._initManagers(this._providedDmk);
2282
+ }
2283
+ this._ctx = {
2284
+ emit: (event, data) => this._emit(event, data),
2285
+ invalidateSession: (sid) => this._invalidateSession(sid),
2286
+ wrapError: (err, opts) => this._wrapError(err, opts),
2287
+ getOrCreateDmk: () => this._getOrCreateDmk(),
2288
+ getDeviceManager: () => this._getDeviceManager(),
2289
+ getSignerManager: () => this._getSignerManager(),
2290
+ clearAllSigners: () => this._signerManager?.clearAll(),
2291
+ replaceSession: (oldSid, newSid) => this._replaceSession(oldSid, newSid),
2292
+ registerCanceller: (sid, cancel) => this._cancellers.set(sid, cancel),
2293
+ clearCanceller: (sid) => this._cancellers.delete(sid),
2294
+ importLedgerKit: this._importLedgerKit
2295
+ };
2296
+ }
2297
+ // "D5:75:7D:4B:51:E8" -> "A58F"
2298
+ /** Register a connectId <-> path mapping from a device descriptor. */
2299
+ _registerDeviceId(descriptor) {
2300
+ const connectId = this._resolveConnectId(descriptor);
2301
+ this._connectIdToPath.set(connectId, descriptor.path);
2302
+ this._pathToConnectId.set(descriptor.path, connectId);
2303
+ return connectId;
2304
+ }
2305
+ /** Get DMK path from external connectId. Falls back to connectId itself. */
2306
+ _getPathForConnectId(connectId) {
2307
+ return this._connectIdToPath.get(connectId) ?? connectId;
2308
+ }
2309
+ /** Get external connectId from DMK path. Falls back to path itself. */
2310
+ _getConnectIdForPath(path) {
2311
+ return this._pathToConnectId.get(path) ?? path;
2312
+ }
2313
+ // ---------------------------------------------------------------------------
2314
+ // Protected — hooks for subclasses
2315
+ // ---------------------------------------------------------------------------
2316
+ /**
2317
+ * Resolve the connectId for a discovered device descriptor.
2318
+ * Default: use the DMK path (ephemeral UUID).
2319
+ * Override in subclasses to extract stable identifiers (e.g. BLE hex ID).
2320
+ */
2321
+ _resolveConnectId(descriptor) {
2322
+ return descriptor.path;
2323
+ }
2324
+ // ---------------------------------------------------------------------------
2325
+ // IConnector -- Device discovery
2326
+ // ---------------------------------------------------------------------------
2327
+ async searchDevices() {
2328
+ const dm = await this._getDeviceManager();
2329
+ let descriptors = await dm.enumerate();
2330
+ if (descriptors.length === 0) {
2331
+ try {
2332
+ await dm.requestDevice();
2333
+ } catch {
2334
+ }
2335
+ descriptors = await dm.enumerate();
2336
+ }
2337
+ const result = descriptors.map((d) => {
2338
+ const connectId = this._registerDeviceId(d);
2339
+ return {
2340
+ connectId,
2341
+ deviceId: d.path,
2342
+ name: d.name || d.type || "Ledger",
2343
+ model: d.type
2344
+ };
2345
+ });
2346
+ return result;
2347
+ }
2348
+ // ---------------------------------------------------------------------------
2349
+ // IConnector -- Connection
2350
+ // ---------------------------------------------------------------------------
2351
+ async connect(deviceId) {
2352
+ const dm = await this._getDeviceManager();
2353
+ let discovered = await this.searchDevices();
2354
+ const dmkPath = deviceId ? this._getPathForConnectId(deviceId) : void 0;
2355
+ let targetPath = dmkPath;
2356
+ if (!targetPath) {
2357
+ const descriptors = await dm.enumerate();
2358
+ if (descriptors.length === 0) {
2359
+ throw new Error(
2360
+ `No Ledger device found. Make sure the device is connected${this.connectionType === "ble" ? " nearby with Bluetooth enabled" : " via USB"} and unlocked.`
2361
+ );
2362
+ }
2363
+ targetPath = descriptors[0].path;
2364
+ }
2365
+ const externalConnectId = this._getConnectIdForPath(targetPath);
2366
+ const doConnect = async (path) => {
2367
+ const sessionId = await dm.connect(path);
2368
+ const session = {
2369
+ sessionId,
2370
+ deviceInfo: {
2371
+ vendor: "ledger",
2372
+ model: "unknown",
2373
+ firmwareVersion: "unknown",
2374
+ deviceId: path,
2375
+ connectId: externalConnectId,
2376
+ connectionType: this.connectionType,
2377
+ capabilities: { persistentDeviceIdentity: false }
2378
+ }
2379
+ };
2380
+ const realName = discovered.find((d) => d.connectId === externalConnectId)?.name ?? "Ledger";
2381
+ this._emit("device-connect", {
2382
+ device: {
2383
+ connectId: externalConnectId,
2384
+ deviceId: path,
2385
+ name: realName
2386
+ }
2387
+ });
2388
+ return session;
2389
+ };
2390
+ try {
2391
+ return await doConnect(targetPath);
2392
+ } catch {
2393
+ this._resetSignersAndSessions();
2394
+ const dm2 = await this._getDeviceManager();
2395
+ discovered = await this.searchDevices();
2396
+ const retryPath = this._getPathForConnectId(externalConnectId);
2397
+ if (!retryPath || retryPath === externalConnectId) {
2398
+ const descriptors = await dm2.enumerate();
2399
+ if (descriptors.length === 0) {
2400
+ throw new Error(
2401
+ `No Ledger device found after retry. Make sure the device is connected${this.connectionType === "ble" ? " nearby with Bluetooth enabled" : " via USB"} and unlocked.`
2402
+ );
2403
+ }
2404
+ return doConnect(descriptors[0].path);
2405
+ }
2406
+ return doConnect(retryPath);
2407
+ }
2408
+ }
2409
+ async disconnect(sessionId) {
2410
+ if (!this._deviceManager) return;
2411
+ const deviceId = this._deviceManager.getDeviceId(sessionId);
2412
+ this._signerManager?.invalidate(sessionId);
2413
+ await this._deviceManager.disconnect(sessionId);
2414
+ if (deviceId) {
2415
+ this._emit("device-disconnect", { connectId: deviceId });
2416
+ }
2417
+ }
2418
+ // ---------------------------------------------------------------------------
2419
+ // IConnector -- Method dispatch
2420
+ // ---------------------------------------------------------------------------
2421
+ async call(sessionId, method, params) {
2422
+ debugLog("[DMK] call:", method, JSON.stringify(params));
2423
+ const ctx = this._ctxForMethod(method);
2424
+ switch (method) {
2425
+ // EVM
2426
+ case "evmGetAddress":
2427
+ return evmGetAddress(ctx, sessionId, params);
2428
+ case "evmSignTransaction":
2429
+ return evmSignTransaction(ctx, sessionId, params);
2430
+ case "evmSignMessage":
2431
+ return evmSignMessage(ctx, sessionId, params);
2432
+ case "evmSignTypedData":
2433
+ return evmSignTypedData(ctx, sessionId, params);
2434
+ // BTC
2435
+ case "btcGetAddress":
2436
+ return btcGetAddress(ctx, sessionId, params);
2437
+ case "btcGetPublicKey":
2438
+ return btcGetPublicKey(ctx, sessionId, params);
2439
+ case "btcSignTransaction":
2440
+ return btcSignTransaction(ctx, sessionId, params);
2441
+ case "btcSignPsbt":
2442
+ return btcSignPsbt(ctx, sessionId, params);
2443
+ case "btcSignMessage":
2444
+ return btcSignMessage(ctx, sessionId, params);
2445
+ case "btcGetMasterFingerprint":
2446
+ return btcGetMasterFingerprint(
2447
+ ctx,
2448
+ sessionId,
2449
+ params
2450
+ );
2451
+ // SOL
2452
+ case "solGetAddress":
2453
+ return solGetAddress(ctx, sessionId, params);
2454
+ case "solSignTransaction":
2455
+ return solSignTransaction(ctx, sessionId, params);
2456
+ case "solSignMessage":
2457
+ return solSignMessage(ctx, sessionId, params);
2458
+ // TRON
2459
+ case "tronGetAddress":
2460
+ return tronGetAddress(ctx, sessionId, params);
2461
+ case "tronSignTransaction":
2462
+ return tronSignTransaction(ctx, sessionId, params);
2463
+ case "tronSignMessage": {
2464
+ const p = params;
2465
+ const internalParams = {
2466
+ path: p.path,
2467
+ messageHex: p.messageHex
2468
+ };
2469
+ return tronSignMessage(ctx, sessionId, internalParams);
2470
+ }
2471
+ default:
2472
+ throw new Error(`LedgerConnector: unknown method "${method}"`);
2473
+ }
2474
+ }
2475
+ async cancel(sessionId) {
2476
+ const cancel = this._cancellers.get(sessionId);
2477
+ if (cancel) {
2478
+ this._cancellers.delete(sessionId);
2479
+ try {
2480
+ cancel();
2481
+ } catch {
2482
+ }
2483
+ }
2484
+ }
2485
+ uiResponse(_response) {
2486
+ }
2487
+ // ---------------------------------------------------------------------------
2488
+ // IConnector -- Events
2489
+ // ---------------------------------------------------------------------------
2490
+ on(event, handler) {
2491
+ if (!this._eventHandlers.has(event)) {
2492
+ this._eventHandlers.set(event, /* @__PURE__ */ new Set());
2493
+ }
2494
+ this._eventHandlers.get(event).add(handler);
2495
+ }
2496
+ off(event, handler) {
2497
+ this._eventHandlers.get(event)?.delete(handler);
2498
+ }
2499
+ // ---------------------------------------------------------------------------
2500
+ // IConnector -- Reset
2501
+ // ---------------------------------------------------------------------------
2502
+ reset() {
2503
+ this._resetAll();
2504
+ }
2505
+ // ---------------------------------------------------------------------------
2506
+ // Private -- DMK / Manager lifecycle
2507
+ // ---------------------------------------------------------------------------
2508
+ /**
2509
+ * Lazily create or return the DMK instance.
2510
+ * If a DMK was provided via constructor, it is used directly.
2511
+ * Otherwise, one is created via the transport factory.
2512
+ */
2513
+ async _getOrCreateDmk() {
2514
+ debugLog(
2515
+ "[DMK] _getOrCreateDmk called, _dmk exists:",
2516
+ !!this._dmk,
2517
+ "_providedDmk exists:",
2518
+ !!this._providedDmk
2519
+ );
2520
+ if (this._dmk) return this._dmk;
2521
+ if (this._providedDmk) {
2522
+ this._dmk = this._providedDmk;
2523
+ return this._dmk;
2524
+ }
2525
+ const { DeviceManagementKitBuilder } = await this._importLedgerKit(
2526
+ "@ledgerhq/device-management-kit"
2527
+ );
2528
+ const transportFactory = await this._createTransport();
2529
+ debugLog("[DMK] _getOrCreateDmk: transportFactory type:", typeof transportFactory);
2530
+ const dmk = new DeviceManagementKitBuilder().addTransport(transportFactory).build();
2531
+ this._dmk = dmk;
2532
+ debugLog("[DMK] _getOrCreateDmk: DMK created");
2533
+ return dmk;
2534
+ }
2535
+ _initManagers(dmk) {
2536
+ this._dmk = dmk;
2537
+ this._deviceManager = new LedgerDeviceManager(dmk);
2538
+ const importKit = this._importLedgerKit;
2539
+ this._signerManager = new SignerManager(dmk, async (args) => {
2540
+ const mod = await importKit("@ledgerhq/device-signer-kit-ethereum");
2541
+ return new mod.SignerEthBuilder(args);
2542
+ });
2543
+ }
2544
+ async _getDeviceManager() {
2545
+ if (this._deviceManager) return this._deviceManager;
2546
+ const dmk = await this._getOrCreateDmk();
2547
+ this._initManagers(dmk);
2548
+ return this._deviceManager;
2549
+ }
2550
+ async _getSignerManager() {
2551
+ if (!this._signerManager) {
2552
+ const dmk = await this._getOrCreateDmk();
2553
+ this._initManagers(dmk);
2554
+ }
2555
+ return this._signerManager;
2556
+ }
2557
+ _invalidateSession(sessionId) {
2558
+ this._signerManager?.invalidate(sessionId);
2559
+ }
2560
+ /**
2561
+ * Replace an old session with a new one after app switch.
2562
+ * Updates the connectId→path mapping so subsequent calls() use the new session,
2563
+ * and emits device-connect so the adapter updates its _sessions Map.
2564
+ */
2565
+ _replaceSession(oldSessionId, _newSessionId) {
2566
+ const dm = this._deviceManager;
2567
+ if (!dm) return;
2568
+ const oldDeviceId = dm.getDeviceId(oldSessionId);
2569
+ const connectId = oldDeviceId ? this._pathToConnectId.get(oldDeviceId) : void 0;
2570
+ this._signerManager?.invalidate(oldSessionId);
2571
+ if (connectId) {
2572
+ this._emit("device-connect", {
2573
+ device: {
2574
+ connectId,
2575
+ deviceId: connectId,
2576
+ name: "Ledger"
2577
+ }
2578
+ });
2579
+ }
2580
+ }
2581
+ /**
2582
+ * Light reset: clear signer/session state but keep DMK, BLE scan, and ID mapping alive.
2583
+ * Used by connect() retry — we want to re-discover with the same transport.
2584
+ */
2585
+ _resetSignersAndSessions() {
2586
+ debugLog("[DMK] _resetSignersAndSessions called");
2587
+ this._signerManager?.clearAll();
2588
+ this._signerManager = null;
2589
+ this._deviceManager = null;
2590
+ }
2591
+ _resetAll() {
2592
+ debugLog("[DMK] _resetAll called");
2593
+ for (const cancel of this._cancellers.values()) {
2594
+ try {
2595
+ cancel();
2596
+ } catch {
2597
+ }
2598
+ }
2599
+ this._cancellers.clear();
2600
+ this._signerManager?.clearAll();
2601
+ this._deviceManager?.dispose();
2602
+ this._deviceManager = null;
2603
+ this._signerManager = null;
2604
+ this._dmk = null;
2605
+ this._connectIdToPath.clear();
2606
+ this._pathToConnectId.clear();
2607
+ }
2608
+ // ---------------------------------------------------------------------------
2609
+ // Private -- Events
2610
+ // ---------------------------------------------------------------------------
2611
+ _emit(event, data) {
2612
+ const handlers = this._eventHandlers.get(event);
2613
+ if (handlers) {
2614
+ for (const handler of handlers) {
2615
+ try {
2616
+ handler(data);
2617
+ } catch {
2618
+ }
2619
+ }
2620
+ }
2621
+ }
2622
+ // ---------------------------------------------------------------------------
2623
+ // Private -- Error handling
2624
+ // ---------------------------------------------------------------------------
2625
+ /**
2626
+ * Return a per-call ctx with the chain's Ledger app name pre-bound to
2627
+ * wrapError, so chain handlers don't need to repeat `{ defaultAppName: 'X' }`
2628
+ * at every catch site. Falls through unchanged for unknown methods.
2629
+ */
2630
+ _ctxForMethod(method) {
2631
+ const prefix = /^(evm|btc|sol|tron)/.exec(method)?.[1];
2632
+ const defaultAppName = prefix ? METHOD_PREFIX_TO_APP_NAME[prefix] : void 0;
2633
+ if (!defaultAppName) return this._ctx;
2634
+ return {
2635
+ ...this._ctx,
2636
+ wrapError: (err, opts) => this._wrapError(err, { defaultAppName, ...opts })
2637
+ };
2638
+ }
2639
+ _wrapError(err, opts) {
2640
+ const mapped = mapLedgerError(err, opts);
2641
+ const error = new Error(mapped.message);
2642
+ const src = err && typeof err === "object" ? err : {};
2643
+ Object.assign(error, {
2644
+ code: mapped.code,
2645
+ appName: mapped.appName,
2646
+ _tag: src._tag,
2647
+ errorCode: src.errorCode,
2648
+ _lastStep: src._lastStep
2649
+ });
2650
+ Object.defineProperty(error, "originalError", {
2651
+ value: err,
2652
+ configurable: true,
2653
+ enumerable: false,
2654
+ writable: true
2655
+ });
2656
+ return error;
2657
+ }
2658
+ };
2659
+
2660
+ // src/utils/bleIdentity.ts
2661
+ function extractBleHexId(name) {
2662
+ if (!name) return void 0;
2663
+ const match = name.match(/\b([0-9A-Fa-f]{4})$/);
2664
+ return match ? match[1].toUpperCase() : void 0;
2665
+ }
2666
+ export {
2667
+ AppManager,
2668
+ DmkTransport,
2669
+ LedgerAdapter,
2670
+ LedgerConnectorBase,
2671
+ LedgerDeviceManager,
2672
+ SignerBtc,
2673
+ SignerEth,
2674
+ SignerManager,
2675
+ SignerSol,
2676
+ deviceActionToPromise,
2677
+ extractBleHexId,
2678
+ isDeviceLockedError,
2679
+ ledgerFailure,
2680
+ mapLedgerError,
2681
+ offSdkEvent,
2682
+ onSdkEvent
2683
+ };
2684
+ //# sourceMappingURL=index.mjs.map