@bytezhang/ledger-adapter 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,952 @@
1
+ import {
2
+ clearRegistry,
3
+ getTransportProvider,
4
+ listRegisteredTransports,
5
+ registerTransport,
6
+ unregisterTransport
7
+ } from "./chunk-NFZDZ4OG.mjs";
8
+
9
+ // src/adapter/LedgerAdapter.ts
10
+ import {
11
+ success,
12
+ failure,
13
+ HardwareErrorCode as HardwareErrorCode2,
14
+ TypedEventEmitter,
15
+ DEVICE,
16
+ UI_REQUEST
17
+ } from "@bytezhang/hardware-wallet-core";
18
+
19
+ // src/errors.ts
20
+ import { HardwareErrorCode } from "@bytezhang/hardware-wallet-core";
21
+ var LOCKED_ERROR_CODES = /* @__PURE__ */ new Set(["5515", "21781", "6982", "27010", "5303", "21251"]);
22
+ var USER_REJECTED_CODES = /* @__PURE__ */ new Set(["6985", "27013"]);
23
+ var WRONG_APP_CODES = /* @__PURE__ */ new Set(["6e00", "28160", "6d00", "27904"]);
24
+ function isDeviceLockedError(err) {
25
+ if (!err || typeof err !== "object") return false;
26
+ const e = err;
27
+ if (e.errorCode != null && LOCKED_ERROR_CODES.has(String(e.errorCode))) return true;
28
+ if (e.statusCode != null && LOCKED_ERROR_CODES.has(String(e.statusCode))) return true;
29
+ if (e._tag === "DeviceLockedError") return true;
30
+ if (typeof e.message === "string" && /locked/i.test(e.message)) return true;
31
+ if (e.originalError != null && isDeviceLockedError(e.originalError)) return true;
32
+ if (e.error != null && e._tag && isDeviceLockedError(e.error)) return true;
33
+ return false;
34
+ }
35
+ function hasStatusCode(err, codeSet) {
36
+ if (!err || typeof err !== "object") return false;
37
+ const e = err;
38
+ if (e.errorCode != null && codeSet.has(String(e.errorCode))) return true;
39
+ if (e.statusCode != null && codeSet.has(String(e.statusCode))) return true;
40
+ if (e.originalError != null && hasStatusCode(e.originalError, codeSet)) return true;
41
+ if (e.error != null && e._tag && hasStatusCode(e.error, codeSet)) return true;
42
+ return false;
43
+ }
44
+ function isUserRejectedError(err) {
45
+ if (!err || typeof err !== "object") return false;
46
+ const e = err;
47
+ if (e._tag === "UserRefusedOnDevice") return true;
48
+ if (typeof e.message === "string" && /denied|rejected|refused/i.test(e.message)) return true;
49
+ if (hasStatusCode(err, USER_REJECTED_CODES)) return true;
50
+ return false;
51
+ }
52
+ function isWrongAppError(err) {
53
+ if (!err || typeof err !== "object") return false;
54
+ const e = err;
55
+ if (e._tag === "WrongAppOpenedError" || e._tag === "InvalidStatusWordError") {
56
+ if (hasStatusCode(err, WRONG_APP_CODES)) return true;
57
+ }
58
+ if (typeof e.message === "string" && /wrong app|open the .* app|CLA not supported/i.test(e.message)) return true;
59
+ if (hasStatusCode(err, WRONG_APP_CODES)) return true;
60
+ return false;
61
+ }
62
+ function isDeviceDisconnectedError(err) {
63
+ if (!err || typeof err !== "object") return false;
64
+ const e = err;
65
+ if (e._tag === "DeviceNotRecognizedError" || e._tag === "DeviceSessionNotFound") return true;
66
+ if (typeof e.message === "string" && /disconnected|not found|no device|unplugged|session.*not.*found/i.test(e.message)) return true;
67
+ return false;
68
+ }
69
+ function isTimeoutError(err) {
70
+ if (!err || typeof err !== "object") return false;
71
+ const e = err;
72
+ if (typeof e.message === "string" && /timeout|timed?\s*out/i.test(e.message)) return true;
73
+ if (e._tag === "DeviceExchangeTimeoutError") return true;
74
+ return false;
75
+ }
76
+ function mapLedgerError(err) {
77
+ if (isDeviceLockedError(err)) {
78
+ return {
79
+ code: HardwareErrorCode.DeviceLocked,
80
+ message: "Device is locked. Please unlock your Ledger device and try again."
81
+ };
82
+ }
83
+ if (isUserRejectedError(err)) {
84
+ return {
85
+ code: HardwareErrorCode.UserRejected,
86
+ message: "User rejected the request on the device."
87
+ };
88
+ }
89
+ if (isWrongAppError(err)) {
90
+ return {
91
+ code: HardwareErrorCode.WrongApp,
92
+ message: "Wrong app is open on the Ledger device. Please open the correct app (e.g. Ethereum) and try again."
93
+ };
94
+ }
95
+ if (isDeviceDisconnectedError(err)) {
96
+ return {
97
+ code: HardwareErrorCode.DeviceDisconnected,
98
+ message: "Ledger device was disconnected. Please reconnect the device and try again."
99
+ };
100
+ }
101
+ if (isTimeoutError(err)) {
102
+ return {
103
+ code: HardwareErrorCode.OperationTimeout,
104
+ message: "Operation timed out. Please ensure the Ledger device is connected and responsive."
105
+ };
106
+ }
107
+ let message = "Unknown Ledger error";
108
+ if (err instanceof Error) {
109
+ message = err.message;
110
+ } else if (err && typeof err === "object") {
111
+ const e = err;
112
+ message = String(e.message ?? e._tag ?? e.type ?? JSON.stringify(err));
113
+ }
114
+ return { code: HardwareErrorCode.UnknownError, message };
115
+ }
116
+
117
+ // src/adapter/LedgerAdapter.ts
118
+ function ensure0x(hex) {
119
+ return hex.startsWith("0x") ? hex : `0x${hex}`;
120
+ }
121
+ function stripHex(hex) {
122
+ return hex.startsWith("0x") ? hex.slice(2) : hex;
123
+ }
124
+ function padHex64(hex) {
125
+ return `0x${stripHex(hex).padStart(64, "0")}`;
126
+ }
127
+ var LedgerAdapter = class {
128
+ constructor(connector) {
129
+ this.vendor = "ledger";
130
+ this.emitter = new TypedEventEmitter();
131
+ this._uiHandler = null;
132
+ // Device cache: tracks discovered devices from connector events
133
+ this._discoveredDevices = /* @__PURE__ */ new Map();
134
+ // Session tracking: maps connectId -> sessionId
135
+ this._sessions = /* @__PURE__ */ new Map();
136
+ // ---------------------------------------------------------------------------
137
+ // Event translation
138
+ // ---------------------------------------------------------------------------
139
+ this.deviceConnectHandler = (data) => {
140
+ const deviceInfo = this.connectorDeviceToDeviceInfo(data.device);
141
+ this._discoveredDevices.set(deviceInfo.connectId, deviceInfo);
142
+ this.emitter.emit(DEVICE.CONNECT, {
143
+ type: DEVICE.CONNECT,
144
+ payload: deviceInfo
145
+ });
146
+ };
147
+ this.deviceDisconnectHandler = (data) => {
148
+ this._discoveredDevices.delete(data.connectId);
149
+ this._sessions.delete(data.connectId);
150
+ this.emitter.emit(DEVICE.DISCONNECT, {
151
+ type: DEVICE.DISCONNECT,
152
+ payload: { connectId: data.connectId }
153
+ });
154
+ };
155
+ this.uiRequestHandler = (data) => {
156
+ this.handleUiEvent(data);
157
+ };
158
+ this.uiEventHandler = (data) => {
159
+ this.handleUiEvent(data);
160
+ };
161
+ this.connector = connector;
162
+ this.registerEventListeners();
163
+ }
164
+ // ---------------------------------------------------------------------------
165
+ // Transport
166
+ // ---------------------------------------------------------------------------
167
+ // Transport is decided at connector creation time. These methods
168
+ // satisfy the IHardwareWallet interface with sensible defaults.
169
+ get activeTransport() {
170
+ return "hid";
171
+ }
172
+ getAvailableTransports() {
173
+ return ["hid"];
174
+ }
175
+ async switchTransport(_type) {
176
+ }
177
+ // ---------------------------------------------------------------------------
178
+ // UI handler
179
+ // ---------------------------------------------------------------------------
180
+ setUiHandler(handler) {
181
+ this._uiHandler = handler;
182
+ }
183
+ // ---------------------------------------------------------------------------
184
+ // Lifecycle
185
+ // ---------------------------------------------------------------------------
186
+ async init(_config) {
187
+ }
188
+ async dispose() {
189
+ this.unregisterEventListeners();
190
+ this.connector.reset();
191
+ this._uiHandler = null;
192
+ this._discoveredDevices.clear();
193
+ this._sessions.clear();
194
+ this.emitter.removeAllListeners();
195
+ }
196
+ // ---------------------------------------------------------------------------
197
+ // Device management
198
+ // ---------------------------------------------------------------------------
199
+ async searchDevices() {
200
+ await this._ensureDevicePermission();
201
+ const devices = await this.connector.searchDevices();
202
+ for (const d of devices) {
203
+ if (d.connectId && !this._discoveredDevices.has(d.connectId)) {
204
+ this._discoveredDevices.set(d.connectId, this.connectorDeviceToDeviceInfo(d));
205
+ }
206
+ }
207
+ if (this._discoveredDevices.size === 0) {
208
+ await this._ensureDevicePermission();
209
+ }
210
+ return Array.from(this._discoveredDevices.values());
211
+ }
212
+ async connectDevice(connectId) {
213
+ await this._ensureDevicePermission(connectId);
214
+ try {
215
+ const session = await this.connector.connect(connectId);
216
+ this._sessions.set(connectId, session.sessionId);
217
+ if (session.deviceInfo) {
218
+ this._discoveredDevices.set(connectId, session.deviceInfo);
219
+ }
220
+ return success(connectId);
221
+ } catch (err) {
222
+ return this.errorToFailure(err);
223
+ }
224
+ }
225
+ async disconnectDevice(connectId) {
226
+ const sessionId = this._sessions.get(connectId);
227
+ if (sessionId) {
228
+ await this.connector.disconnect(sessionId);
229
+ this._sessions.delete(connectId);
230
+ }
231
+ }
232
+ async getDeviceInfo(connectId, deviceId) {
233
+ await this._ensureDevicePermission(connectId, deviceId);
234
+ const cached = this._discoveredDevices.get(connectId) ?? Array.from(this._discoveredDevices.values()).find(
235
+ (d) => d.deviceId === deviceId
236
+ );
237
+ if (cached) {
238
+ return success(cached);
239
+ }
240
+ return failure(
241
+ HardwareErrorCode2.DeviceNotFound,
242
+ "Device not found in cache. Call searchDevices() or wait for a device-connected event first."
243
+ );
244
+ }
245
+ getSupportedChains() {
246
+ return ["evm", "btc", "sol"];
247
+ }
248
+ on(event, listener) {
249
+ this.emitter.on(event, listener);
250
+ }
251
+ off(event, listener) {
252
+ this.emitter.off(event, listener);
253
+ }
254
+ cancel(connectId) {
255
+ const sessionId = this._sessions.get(connectId) ?? connectId;
256
+ void this.connector.cancel(sessionId);
257
+ }
258
+ // ---------------------------------------------------------------------------
259
+ // EVM methods
260
+ // ---------------------------------------------------------------------------
261
+ async evmGetAddress(connectId, _deviceId, params) {
262
+ await this._ensureDevicePermission(connectId, _deviceId);
263
+ try {
264
+ const result = await this.connectorCall(connectId, "evmGetAddress", {
265
+ path: params.path,
266
+ showOnDevice: params.showOnDevice,
267
+ chainId: params.chainId
268
+ });
269
+ return success({
270
+ address: result.address,
271
+ path: params.path
272
+ });
273
+ } catch (err) {
274
+ return this.errorToFailure(err);
275
+ }
276
+ }
277
+ async evmGetAddresses(connectId, deviceId, params, onProgress) {
278
+ return this.batchCall(
279
+ params,
280
+ (p) => this.evmGetAddress(connectId, deviceId, p),
281
+ onProgress
282
+ );
283
+ }
284
+ async evmGetPublicKey(connectId, _deviceId, params) {
285
+ await this._ensureDevicePermission(connectId, _deviceId);
286
+ try {
287
+ const result = await this.connectorCall(connectId, "evmGetAddress", {
288
+ path: params.path,
289
+ showOnDevice: params.showOnDevice
290
+ });
291
+ return success({
292
+ publicKey: result.publicKey,
293
+ path: params.path
294
+ });
295
+ } catch (err) {
296
+ return this.errorToFailure(err);
297
+ }
298
+ }
299
+ async evmSignTransaction(connectId, _deviceId, params) {
300
+ await this._ensureDevicePermission(connectId, _deviceId);
301
+ try {
302
+ const result = await this.connectorCall(connectId, "evmSignTransaction", {
303
+ path: params.path,
304
+ transaction: {
305
+ to: params.to,
306
+ value: params.value,
307
+ chainId: params.chainId,
308
+ nonce: params.nonce,
309
+ gasLimit: params.gasLimit,
310
+ gasPrice: params.gasPrice,
311
+ maxFeePerGas: params.maxFeePerGas,
312
+ maxPriorityFeePerGas: params.maxPriorityFeePerGas,
313
+ accessList: params.accessList,
314
+ data: params.data
315
+ }
316
+ });
317
+ return success({
318
+ v: ensure0x(result.v),
319
+ r: padHex64(result.r),
320
+ s: padHex64(result.s)
321
+ });
322
+ } catch (err) {
323
+ return this.errorToFailure(err);
324
+ }
325
+ }
326
+ async evmSignMessage(connectId, _deviceId, params) {
327
+ await this._ensureDevicePermission(connectId, _deviceId);
328
+ try {
329
+ const result = await this.connectorCall(connectId, "evmSignMessage", {
330
+ path: params.path,
331
+ message: params.message
332
+ });
333
+ return success({
334
+ signature: ensure0x(result.signature)
335
+ });
336
+ } catch (err) {
337
+ return this.errorToFailure(err);
338
+ }
339
+ }
340
+ async evmSignTypedData(connectId, _deviceId, params) {
341
+ await this._ensureDevicePermission(connectId, _deviceId);
342
+ if (params.mode === "hash") {
343
+ return failure(
344
+ HardwareErrorCode2.MethodNotSupported,
345
+ 'Ledger does not support hash-only EIP-712 signing. Use mode "full" with the complete typed data structure.'
346
+ );
347
+ }
348
+ try {
349
+ const result = await this.connectorCall(connectId, "evmSignTypedData", {
350
+ path: params.path,
351
+ data: params.data
352
+ });
353
+ return success({
354
+ signature: ensure0x(result.signature)
355
+ });
356
+ } catch (err) {
357
+ return this.errorToFailure(err);
358
+ }
359
+ }
360
+ // ---------------------------------------------------------------------------
361
+ // BTC methods
362
+ // ---------------------------------------------------------------------------
363
+ async btcGetAddress(connectId, _deviceId, params) {
364
+ await this._ensureDevicePermission(connectId, _deviceId);
365
+ try {
366
+ const result = await this.connectorCall(connectId, "btcGetAddress", {
367
+ path: params.path,
368
+ coin: params.coin,
369
+ showOnDevice: params.showOnDevice,
370
+ scriptType: params.scriptType
371
+ });
372
+ return success({
373
+ address: result.address,
374
+ path: params.path
375
+ });
376
+ } catch (err) {
377
+ return this.errorToFailure(err);
378
+ }
379
+ }
380
+ async btcGetAddresses(connectId, deviceId, params, onProgress) {
381
+ return this.batchCall(
382
+ params,
383
+ (p) => this.btcGetAddress(connectId, deviceId, p),
384
+ onProgress
385
+ );
386
+ }
387
+ async btcGetPublicKey(connectId, _deviceId, params) {
388
+ await this._ensureDevicePermission(connectId, _deviceId);
389
+ try {
390
+ const result = await this.connectorCall(connectId, "btcGetPublicKey", {
391
+ path: params.path,
392
+ coin: params.coin,
393
+ showOnDevice: params.showOnDevice
394
+ });
395
+ return success({
396
+ xpub: result.xpub,
397
+ publicKey: result.publicKey ?? "",
398
+ fingerprint: result.fingerprint ?? 0,
399
+ chainCode: result.chainCode ?? "",
400
+ path: params.path,
401
+ depth: result.depth ?? 0
402
+ });
403
+ } catch (err) {
404
+ return this.errorToFailure(err);
405
+ }
406
+ }
407
+ async btcSignTransaction(connectId, _deviceId, params) {
408
+ await this._ensureDevicePermission(connectId, _deviceId);
409
+ if (!params.psbt) {
410
+ return failure(
411
+ HardwareErrorCode2.InvalidParams,
412
+ "Ledger requires PSBT format for BTC transaction signing. Provide params.psbt."
413
+ );
414
+ }
415
+ return failure(
416
+ HardwareErrorCode2.MethodNotSupported,
417
+ "BTC transaction signing via PSBT is not yet implemented for Ledger."
418
+ );
419
+ }
420
+ async btcSignMessage(_connectId, _deviceId, _params) {
421
+ return failure(
422
+ HardwareErrorCode2.MethodNotSupported,
423
+ "BTC message signing is not yet supported on Ledger."
424
+ );
425
+ }
426
+ // ---------------------------------------------------------------------------
427
+ // Device fingerprint
428
+ // ---------------------------------------------------------------------------
429
+ async btcGetMasterFingerprint(connectId, _deviceId, params) {
430
+ await this._ensureDevicePermission(connectId, _deviceId);
431
+ try {
432
+ const result = await this.connectorCall(connectId, "btcGetMasterFingerprint", {
433
+ skipOpenApp: params?.skipOpenApp
434
+ });
435
+ return success({ masterFingerprint: result.masterFingerprint });
436
+ } catch (err) {
437
+ return this.errorToFailure(err);
438
+ }
439
+ }
440
+ // ---------------------------------------------------------------------------
441
+ // Solana methods (stubs -- not yet supported)
442
+ // ---------------------------------------------------------------------------
443
+ async solGetAddress(_connectId, _deviceId, _params) {
444
+ return failure(HardwareErrorCode2.MethodNotSupported, "Solana not supported on Ledger yet");
445
+ }
446
+ async solGetAddresses(_connectId, _deviceId, _params, _onProgress) {
447
+ return failure(HardwareErrorCode2.MethodNotSupported, "Solana not supported on Ledger yet");
448
+ }
449
+ async solGetPublicKey(_connectId, _deviceId, _params) {
450
+ return failure(HardwareErrorCode2.MethodNotSupported, "Solana not supported on Ledger yet");
451
+ }
452
+ async solSignTransaction(_connectId, _deviceId, _params) {
453
+ return failure(HardwareErrorCode2.MethodNotSupported, "Solana not supported on Ledger yet");
454
+ }
455
+ async solSignMessage(_connectId, _deviceId, _params) {
456
+ return failure(HardwareErrorCode2.MethodNotSupported, "Solana signMessage is not supported on Ledger yet");
457
+ }
458
+ // ---------------------------------------------------------------------------
459
+ // Private helpers
460
+ // ---------------------------------------------------------------------------
461
+ /**
462
+ * Call the connector with session resolution.
463
+ * Looks up sessionId from connectId, falls back to connectId itself.
464
+ */
465
+ async connectorCall(connectId, method, params) {
466
+ const sessionId = this._sessions.get(connectId) ?? connectId;
467
+ return this.connector.call(sessionId, method, params);
468
+ }
469
+ /**
470
+ * Ensure device permission before proceeding.
471
+ * - No connectId (searchDevices): check environment-level permission
472
+ * - With connectId (business methods): check device-level permission
473
+ * If not granted, calls onDevicePermission so the consumer can request access.
474
+ */
475
+ async _ensureDevicePermission(connectId, deviceId) {
476
+ const transportType = "hid";
477
+ let granted = false;
478
+ let context;
479
+ if (this._uiHandler?.checkDevicePermission) {
480
+ try {
481
+ const result = await this._uiHandler.checkDevicePermission({ transportType, connectId, deviceId });
482
+ granted = result.granted;
483
+ context = result.context;
484
+ } catch {
485
+ granted = false;
486
+ }
487
+ }
488
+ if (!granted) {
489
+ try {
490
+ await this._uiHandler?.onDevicePermission?.({ transportType, context });
491
+ } catch {
492
+ }
493
+ }
494
+ }
495
+ /**
496
+ * Convert a thrown error to a Response failure.
497
+ * Uses mapLedgerError to parse Ledger DMK error codes into HardwareErrorCode values.
498
+ */
499
+ errorToFailure(err) {
500
+ console.error("[LedgerAdapter] error:", err);
501
+ const mapped = mapLedgerError(err);
502
+ if (mapped.code === HardwareErrorCode2.DeviceLocked) {
503
+ this.emitter.emit(UI_REQUEST.REQUEST_BUTTON, {
504
+ type: UI_REQUEST.REQUEST_BUTTON,
505
+ payload: {
506
+ device: this.unknownDevice(),
507
+ code: "ButtonRequest_Other"
508
+ }
509
+ });
510
+ }
511
+ return failure(mapped.code, mapped.message);
512
+ }
513
+ /**
514
+ * Generic batch call with progress reporting.
515
+ * If any single call fails, returns the failure immediately.
516
+ */
517
+ async batchCall(params, callFn, onProgress) {
518
+ const results = [];
519
+ for (let i = 0; i < params.length; i++) {
520
+ const result = await callFn(params[i]);
521
+ if (!result.success) {
522
+ return result;
523
+ }
524
+ results.push(result.payload);
525
+ onProgress?.({ index: i, total: params.length });
526
+ }
527
+ return success(results);
528
+ }
529
+ registerEventListeners() {
530
+ this.connector.on("device-connect", this.deviceConnectHandler);
531
+ this.connector.on("device-disconnect", this.deviceDisconnectHandler);
532
+ this.connector.on("ui-request", this.uiRequestHandler);
533
+ this.connector.on("ui-event", this.uiEventHandler);
534
+ }
535
+ unregisterEventListeners() {
536
+ this.connector.off("device-connect", this.deviceConnectHandler);
537
+ this.connector.off("device-disconnect", this.deviceDisconnectHandler);
538
+ this.connector.off("ui-request", this.uiRequestHandler);
539
+ this.connector.off("ui-event", this.uiEventHandler);
540
+ }
541
+ handleUiEvent(event) {
542
+ if (!event.type) return;
543
+ const payload = event.payload;
544
+ const deviceInfo = payload ? this.extractDeviceInfoFromPayload(payload) : this.unknownDevice();
545
+ switch (event.type) {
546
+ case "ui-request_confirmation":
547
+ this.emitter.emit(UI_REQUEST.REQUEST_BUTTON, {
548
+ type: UI_REQUEST.REQUEST_BUTTON,
549
+ payload: { device: deviceInfo }
550
+ });
551
+ break;
552
+ }
553
+ }
554
+ // ---------------------------------------------------------------------------
555
+ // Device info mapping
556
+ // ---------------------------------------------------------------------------
557
+ connectorDeviceToDeviceInfo(device) {
558
+ return {
559
+ vendor: "ledger",
560
+ model: device.model ?? "unknown",
561
+ firmwareVersion: "",
562
+ deviceId: device.deviceId,
563
+ connectId: device.connectId,
564
+ label: device.name,
565
+ connectionType: "usb"
566
+ };
567
+ }
568
+ extractDeviceInfoFromPayload(payload) {
569
+ return {
570
+ vendor: "ledger",
571
+ model: payload["model"] ?? "unknown",
572
+ firmwareVersion: "",
573
+ deviceId: payload["deviceId"] ?? payload["id"] ?? "",
574
+ connectId: payload["connectId"] ?? payload["path"] ?? "",
575
+ label: payload["label"],
576
+ connectionType: "usb"
577
+ };
578
+ }
579
+ unknownDevice() {
580
+ return {
581
+ vendor: "ledger",
582
+ model: "unknown",
583
+ firmwareVersion: "",
584
+ deviceId: "",
585
+ connectId: "",
586
+ connectionType: "usb"
587
+ };
588
+ }
589
+ };
590
+
591
+ // src/device/LedgerDeviceManager.ts
592
+ var LedgerDeviceManager = class {
593
+ constructor(dmk) {
594
+ this._discovered = /* @__PURE__ */ new Map();
595
+ this._sessions = /* @__PURE__ */ new Map();
596
+ // deviceId → sessionId
597
+ this._sessionToDevice = /* @__PURE__ */ new Map();
598
+ // sessionId → deviceId
599
+ this._listenSub = null;
600
+ this._dmk = dmk;
601
+ }
602
+ /**
603
+ * One-shot enumeration: subscribe to listenToAvailableDevices,
604
+ * take the first emission, unsubscribe, return DeviceDescriptors.
605
+ */
606
+ enumerate() {
607
+ return new Promise((resolve) => {
608
+ const sub = this._dmk.listenToAvailableDevices().subscribe({
609
+ next: (devices) => {
610
+ this._discovered.clear();
611
+ for (const d of devices) {
612
+ this._discovered.set(d.id, d);
613
+ }
614
+ sub.unsubscribe();
615
+ resolve(devices.map((d) => ({ path: d.id, type: d.deviceModel.id })));
616
+ },
617
+ error: () => {
618
+ sub.unsubscribe();
619
+ resolve([]);
620
+ }
621
+ });
622
+ });
623
+ }
624
+ /**
625
+ * Continuous listening: tracks device connect/disconnect via diffing.
626
+ */
627
+ listen(onChange) {
628
+ this.stopListening();
629
+ let previousIds = /* @__PURE__ */ new Set();
630
+ this._listenSub = this._dmk.listenToAvailableDevices().subscribe({
631
+ next: (devices) => {
632
+ const currentIds = new Set(devices.map((d) => d.id));
633
+ for (const d of devices) {
634
+ this._discovered.set(d.id, d);
635
+ if (!previousIds.has(d.id)) {
636
+ onChange({ type: "device-connected", descriptor: { path: d.id, type: d.deviceModel.id } });
637
+ }
638
+ }
639
+ for (const id of previousIds) {
640
+ if (!currentIds.has(id)) {
641
+ this._discovered.delete(id);
642
+ onChange({ type: "device-disconnected", descriptor: { path: id } });
643
+ }
644
+ }
645
+ previousIds = currentIds;
646
+ }
647
+ });
648
+ }
649
+ stopListening() {
650
+ this._listenSub?.unsubscribe();
651
+ this._listenSub = null;
652
+ }
653
+ /**
654
+ * Trigger browser device selection (WebHID requestDevice).
655
+ * Starts discovery for a short period, then stops.
656
+ */
657
+ requestDevice(timeoutMs = 3e3) {
658
+ return new Promise((resolve) => {
659
+ const sub = this._dmk.startDiscovering().subscribe({
660
+ next: (d) => {
661
+ this._discovered.set(d.id, d);
662
+ },
663
+ error: () => {
664
+ sub.unsubscribe();
665
+ resolve();
666
+ }
667
+ });
668
+ setTimeout(() => {
669
+ sub.unsubscribe();
670
+ this._dmk.stopDiscovering();
671
+ resolve();
672
+ }, timeoutMs);
673
+ });
674
+ }
675
+ /** Connect to a previously discovered device. Returns sessionId. */
676
+ async connect(deviceId) {
677
+ const device = this._discovered.get(deviceId);
678
+ if (!device) {
679
+ throw new Error(`Device "${deviceId}" not found. Call enumerate() or listen() first.`);
680
+ }
681
+ const sessionId = await this._dmk.connect({ device });
682
+ this._sessions.set(deviceId, sessionId);
683
+ this._sessionToDevice.set(sessionId, deviceId);
684
+ return sessionId;
685
+ }
686
+ /** Disconnect a session. */
687
+ async disconnect(sessionId) {
688
+ await this._dmk.disconnect({ sessionId });
689
+ const deviceId = this._sessionToDevice.get(sessionId);
690
+ if (deviceId) this._sessions.delete(deviceId);
691
+ this._sessionToDevice.delete(sessionId);
692
+ }
693
+ getSessionId(deviceId) {
694
+ return this._sessions.get(deviceId);
695
+ }
696
+ getDeviceId(sessionId) {
697
+ return this._sessionToDevice.get(sessionId);
698
+ }
699
+ /** Get the underlying DMK instance (needed by SignerManager). */
700
+ getDmk() {
701
+ return this._dmk;
702
+ }
703
+ dispose() {
704
+ this.stopListening();
705
+ this._discovered.clear();
706
+ this._sessions.clear();
707
+ this._sessionToDevice.clear();
708
+ this._dmk.close?.();
709
+ }
710
+ };
711
+
712
+ // src/signer/deviceActionToPromise.ts
713
+ function deviceActionToPromise(action, onInteraction) {
714
+ return new Promise((resolve, reject) => {
715
+ let settled = false;
716
+ let sub;
717
+ sub = action.observable.subscribe({
718
+ next: (state) => {
719
+ if (settled) return;
720
+ if (state.status === "completed") {
721
+ settled = true;
722
+ sub?.unsubscribe();
723
+ resolve(state.output);
724
+ } else if (state.status === "error") {
725
+ settled = true;
726
+ sub?.unsubscribe();
727
+ reject(state.error);
728
+ } else if (state.status === "pending" && onInteraction) {
729
+ const interaction = state.intermediateValue?.requiredUserInteraction;
730
+ if (interaction && interaction !== "none") {
731
+ onInteraction(interaction);
732
+ }
733
+ }
734
+ },
735
+ error: (err) => {
736
+ if (!settled) {
737
+ settled = true;
738
+ sub?.unsubscribe();
739
+ reject(err);
740
+ }
741
+ },
742
+ complete: () => {
743
+ if (!settled) {
744
+ settled = true;
745
+ reject(new Error("Device action completed without result"));
746
+ }
747
+ }
748
+ });
749
+ });
750
+ }
751
+
752
+ // src/signer/SignerEth.ts
753
+ function hexToBytes(hex) {
754
+ const h = hex.startsWith("0x") ? hex.slice(2) : hex;
755
+ const bytes = new Uint8Array(h.length / 2);
756
+ for (let i = 0; i < bytes.length; i++) {
757
+ bytes[i] = parseInt(h.substring(i * 2, i * 2 + 2), 16);
758
+ }
759
+ return bytes;
760
+ }
761
+ var SignerEth = class {
762
+ constructor(_sdk) {
763
+ this._sdk = _sdk;
764
+ }
765
+ async getAddress(derivationPath, options) {
766
+ const action = this._sdk.getAddress(derivationPath, {
767
+ checkOnDevice: options?.checkOnDevice ?? false
768
+ });
769
+ return deviceActionToPromise(action, this.onInteraction);
770
+ }
771
+ async signTransaction(derivationPath, serializedTxHex) {
772
+ const action = this._sdk.signTransaction(derivationPath, hexToBytes(serializedTxHex));
773
+ return deviceActionToPromise(action, this.onInteraction);
774
+ }
775
+ async signMessage(derivationPath, message) {
776
+ const action = this._sdk.signMessage(derivationPath, message);
777
+ return deviceActionToPromise(action, this.onInteraction);
778
+ }
779
+ async signTypedData(derivationPath, data) {
780
+ const action = this._sdk.signTypedData(derivationPath, data);
781
+ return deviceActionToPromise(action, this.onInteraction);
782
+ }
783
+ };
784
+
785
+ // src/signer/SignerManager.ts
786
+ var SignerManager = class _SignerManager {
787
+ constructor(dmk, builderFn) {
788
+ this._cache = /* @__PURE__ */ new Map();
789
+ this._dmk = dmk;
790
+ this._builderFn = builderFn ?? _SignerManager._defaultBuilder();
791
+ }
792
+ async getOrCreate(sessionId) {
793
+ let signer = this._cache.get(sessionId);
794
+ if (signer) return signer;
795
+ const builder = await this._builderFn({ dmk: this._dmk, sessionId });
796
+ const sdkSigner = builder.build();
797
+ signer = new SignerEth(sdkSigner);
798
+ this._cache.set(sessionId, signer);
799
+ return signer;
800
+ }
801
+ invalidate(sessionId) {
802
+ this._cache.delete(sessionId);
803
+ }
804
+ clearAll() {
805
+ this._cache.clear();
806
+ }
807
+ static _defaultBuilder() {
808
+ let BuilderClass = null;
809
+ return async (args) => {
810
+ if (!BuilderClass) {
811
+ const mod = await import("@ledgerhq/device-signer-kit-ethereum");
812
+ BuilderClass = mod.SignerEthBuilder;
813
+ }
814
+ return new BuilderClass(args);
815
+ };
816
+ }
817
+ };
818
+
819
+ // src/signer/SignerBtc.ts
820
+ var SignerBtc = class {
821
+ constructor(_sdk) {
822
+ this._sdk = _sdk;
823
+ }
824
+ async getWalletAddress(wallet, addressIndex, options) {
825
+ const action = this._sdk.getWalletAddress(wallet, addressIndex, {
826
+ checkOnDevice: options?.checkOnDevice ?? false,
827
+ change: options?.change ?? false
828
+ });
829
+ return deviceActionToPromise(action, this.onInteraction);
830
+ }
831
+ async getExtendedPublicKey(derivationPath, options) {
832
+ const action = this._sdk.getExtendedPublicKey(derivationPath, {
833
+ checkOnDevice: options?.checkOnDevice ?? false
834
+ });
835
+ return deviceActionToPromise(action, this.onInteraction);
836
+ }
837
+ async getMasterFingerprint(options) {
838
+ const action = this._sdk.getMasterFingerprint(options);
839
+ const result = await deviceActionToPromise(action, this.onInteraction);
840
+ return result.masterFingerprint;
841
+ }
842
+ };
843
+
844
+ // src/app/AppManager.ts
845
+ var APP_NAME_MAP = {
846
+ ETH: "Ethereum",
847
+ BTC: "Bitcoin",
848
+ SOL: "Solana",
849
+ TRX: "Tron",
850
+ XRP: "XRP",
851
+ ADA: "Cardano",
852
+ DOT: "Polkadot",
853
+ ATOM: "Cosmos"
854
+ };
855
+ var DASHBOARD_APP_NAME = "BOLOS";
856
+ var AppManager = class {
857
+ constructor(dmk, options) {
858
+ this._dmk = dmk;
859
+ this._waitMs = options?.waitMs ?? 1e3;
860
+ this._maxRetries = options?.maxRetries ?? 10;
861
+ }
862
+ /**
863
+ * Return the Ledger app name for a given chain ticker,
864
+ * or undefined if the chain is not supported.
865
+ */
866
+ static getAppName(chain) {
867
+ return APP_NAME_MAP[chain];
868
+ }
869
+ /**
870
+ * Ensure the target app is open on the device identified by `sessionId`.
871
+ *
872
+ * Flow:
873
+ * 1. Check the currently running app.
874
+ * 2. If it is already the target, return immediately.
875
+ * 3. If a different app is running (not dashboard), close it first.
876
+ * 4. Open the target app.
877
+ * 5. Poll until the device confirms the target app is running.
878
+ */
879
+ async ensureAppOpen(sessionId, targetAppName) {
880
+ const currentApp = await this._getCurrentApp(sessionId);
881
+ if (currentApp === targetAppName) {
882
+ return;
883
+ }
884
+ if (!this._isDashboard(currentApp)) {
885
+ await this._closeCurrentApp(sessionId);
886
+ await this._waitForApp(sessionId, DASHBOARD_APP_NAME);
887
+ }
888
+ await this._openApp(sessionId, targetAppName);
889
+ await this._waitForApp(sessionId, targetAppName);
890
+ }
891
+ // ---------------------------------------------------------------------------
892
+ // Private helpers
893
+ // ---------------------------------------------------------------------------
894
+ async _getCurrentApp(sessionId) {
895
+ const result = await this._dmk.sendCommand({
896
+ sessionId,
897
+ command: { type: "get-app-and-version" }
898
+ });
899
+ return result.name;
900
+ }
901
+ async _openApp(sessionId, appName) {
902
+ await this._dmk.sendCommand({
903
+ sessionId,
904
+ command: { type: "open-app", appName }
905
+ });
906
+ }
907
+ async _closeCurrentApp(sessionId) {
908
+ await this._dmk.sendCommand({
909
+ sessionId,
910
+ command: { type: "close-app" }
911
+ });
912
+ }
913
+ /**
914
+ * Poll the device until the expected app is reported as running,
915
+ * or throw after `_maxRetries` attempts.
916
+ */
917
+ async _waitForApp(sessionId, expectedAppName) {
918
+ for (let i = 0; i < this._maxRetries; i++) {
919
+ await this._wait();
920
+ const current = await this._getCurrentApp(sessionId);
921
+ if (current === expectedAppName) {
922
+ return;
923
+ }
924
+ }
925
+ throw new Error(
926
+ `Ledger: failed to open "${expectedAppName}" after ${this._maxRetries} retries`
927
+ );
928
+ }
929
+ _isDashboard(appName) {
930
+ return appName === DASHBOARD_APP_NAME;
931
+ }
932
+ _wait() {
933
+ return new Promise((resolve) => setTimeout(resolve, this._waitMs));
934
+ }
935
+ };
936
+ export {
937
+ AppManager,
938
+ LedgerAdapter,
939
+ LedgerDeviceManager,
940
+ SignerBtc,
941
+ SignerEth,
942
+ SignerManager,
943
+ clearRegistry,
944
+ deviceActionToPromise,
945
+ getTransportProvider,
946
+ isDeviceLockedError,
947
+ listRegisteredTransports,
948
+ mapLedgerError,
949
+ registerTransport,
950
+ unregisterTransport
951
+ };
952
+ //# sourceMappingURL=index.mjs.map