@bytezhang/ledger-adapter 0.0.6 → 0.0.8

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.js CHANGED
@@ -36,6 +36,7 @@ __export(index_exports, {
36
36
  SignerBtc: () => SignerBtc,
37
37
  SignerEth: () => SignerEth,
38
38
  SignerManager: () => SignerManager,
39
+ SignerSol: () => SignerSol,
39
40
  clearRegistry: () => clearRegistry,
40
41
  deviceActionToPromise: () => deviceActionToPromise,
41
42
  getTransportProvider: () => getTransportProvider,
@@ -169,6 +170,8 @@ var _LedgerAdapter = class _LedgerAdapter {
169
170
  this._sessions = /* @__PURE__ */ new Map();
170
171
  // Pending device-connect resolve — set by _waitForDeviceConnect, resolved by uiResponse
171
172
  this._deviceConnectResolve = null;
173
+ // Mutex for ensureConnected — prevents concurrent calls from establishing duplicate connections
174
+ this._connectingPromise = null;
172
175
  // ---------------------------------------------------------------------------
173
176
  // Event translation
174
177
  // ---------------------------------------------------------------------------
@@ -222,6 +225,8 @@ var _LedgerAdapter = class _LedgerAdapter {
222
225
  async init(_config) {
223
226
  }
224
227
  async dispose() {
228
+ this._deviceConnectResolve?.(true);
229
+ this._deviceConnectResolve = null;
225
230
  this.unregisterEventListeners();
226
231
  this.connector.reset();
227
232
  this._uiHandler = null;
@@ -293,10 +298,74 @@ var _LedgerAdapter = class _LedgerAdapter {
293
298
  void this.connector.cancel(sessionId);
294
299
  }
295
300
  // ---------------------------------------------------------------------------
301
+ // Chain fingerprint
302
+ // ---------------------------------------------------------------------------
303
+ async getChainFingerprint(connectId, deviceId, chain) {
304
+ await this._ensureDevicePermission(connectId, deviceId);
305
+ try {
306
+ const address = await this._deriveAddressForFingerprint(connectId, chain);
307
+ return (0, import_hardware_wallet_core2.success)((0, import_hardware_wallet_core2.deriveDeviceFingerprint)(address));
308
+ } catch (err) {
309
+ return this.errorToFailure(err);
310
+ }
311
+ }
312
+ /**
313
+ * Verify that the connected device matches the expected fingerprint.
314
+ *
315
+ * - If deviceId is empty, verification is skipped (returns true).
316
+ * - deviceId is used here as the stored fingerprint to compare against.
317
+ */
318
+ async _verifyDeviceFingerprint(connectId, deviceId, chain) {
319
+ if (!deviceId) return true;
320
+ try {
321
+ const address = await this._deriveAddressForFingerprint(connectId, chain);
322
+ const fingerprint = (0, import_hardware_wallet_core2.deriveDeviceFingerprint)(address);
323
+ return fingerprint === deviceId;
324
+ } catch (err) {
325
+ const mapped = mapLedgerError(err);
326
+ if (mapped.code === import_hardware_wallet_core2.HardwareErrorCode.WrongApp || mapped.code === import_hardware_wallet_core2.HardwareErrorCode.DeviceLocked) {
327
+ return true;
328
+ }
329
+ throw err;
330
+ }
331
+ }
332
+ /**
333
+ * Derive an address at the fixed testnet path for fingerprint generation.
334
+ */
335
+ async _deriveAddressForFingerprint(connectId, chain) {
336
+ const path = import_hardware_wallet_core2.CHAIN_FINGERPRINT_PATHS[chain];
337
+ if (chain === "evm") {
338
+ const result = await this.connectorCall(connectId, "evmGetAddress", {
339
+ path,
340
+ showOnDevice: false
341
+ });
342
+ return result.address;
343
+ }
344
+ if (chain === "btc") {
345
+ const result = await this.connectorCall(connectId, "btcGetAddress", {
346
+ path,
347
+ showOnDevice: false,
348
+ coin: "Testnet"
349
+ });
350
+ return result.address;
351
+ }
352
+ if (chain === "sol") {
353
+ const result = await this.connectorCall(connectId, "solGetAddress", {
354
+ path,
355
+ showOnDevice: false
356
+ });
357
+ return result.address;
358
+ }
359
+ throw new Error(`Unsupported chain for fingerprint: ${chain}`);
360
+ }
361
+ // ---------------------------------------------------------------------------
296
362
  // EVM methods
297
363
  // ---------------------------------------------------------------------------
298
364
  async evmGetAddress(connectId, _deviceId, params) {
299
365
  await this._ensureDevicePermission(connectId, _deviceId);
366
+ if (!await this._verifyDeviceFingerprint(connectId, _deviceId, "evm")) {
367
+ return (0, import_hardware_wallet_core2.failure)(import_hardware_wallet_core2.HardwareErrorCode.DeviceMismatch, "Wrong device connected");
368
+ }
300
369
  try {
301
370
  const result = await this.connectorCall(connectId, "evmGetAddress", {
302
371
  path: params.path,
@@ -320,6 +389,9 @@ var _LedgerAdapter = class _LedgerAdapter {
320
389
  }
321
390
  async evmGetPublicKey(connectId, _deviceId, params) {
322
391
  await this._ensureDevicePermission(connectId, _deviceId);
392
+ if (!await this._verifyDeviceFingerprint(connectId, _deviceId, "evm")) {
393
+ return (0, import_hardware_wallet_core2.failure)(import_hardware_wallet_core2.HardwareErrorCode.DeviceMismatch, "Wrong device connected");
394
+ }
323
395
  try {
324
396
  const result = await this.connectorCall(connectId, "evmGetAddress", {
325
397
  path: params.path,
@@ -335,21 +407,19 @@ var _LedgerAdapter = class _LedgerAdapter {
335
407
  }
336
408
  async evmSignTransaction(connectId, _deviceId, params) {
337
409
  await this._ensureDevicePermission(connectId, _deviceId);
410
+ if (!await this._verifyDeviceFingerprint(connectId, _deviceId, "evm")) {
411
+ return (0, import_hardware_wallet_core2.failure)(import_hardware_wallet_core2.HardwareErrorCode.DeviceMismatch, "Wrong device connected");
412
+ }
338
413
  try {
414
+ if (!params.serializedTx) {
415
+ return (0, import_hardware_wallet_core2.failure)(
416
+ import_hardware_wallet_core2.HardwareErrorCode.InvalidParams,
417
+ "Ledger requires a pre-serialized transaction (serializedTx). Provide an RLP-encoded hex string."
418
+ );
419
+ }
339
420
  const result = await this.connectorCall(connectId, "evmSignTransaction", {
340
421
  path: params.path,
341
- transaction: {
342
- to: params.to,
343
- value: params.value,
344
- chainId: params.chainId,
345
- nonce: params.nonce,
346
- gasLimit: params.gasLimit,
347
- gasPrice: params.gasPrice,
348
- maxFeePerGas: params.maxFeePerGas,
349
- maxPriorityFeePerGas: params.maxPriorityFeePerGas,
350
- accessList: params.accessList,
351
- data: params.data
352
- }
422
+ serializedTx: params.serializedTx
353
423
  });
354
424
  return (0, import_hardware_wallet_core2.success)({
355
425
  v: ensure0x(result.v),
@@ -362,6 +432,9 @@ var _LedgerAdapter = class _LedgerAdapter {
362
432
  }
363
433
  async evmSignMessage(connectId, _deviceId, params) {
364
434
  await this._ensureDevicePermission(connectId, _deviceId);
435
+ if (!await this._verifyDeviceFingerprint(connectId, _deviceId, "evm")) {
436
+ return (0, import_hardware_wallet_core2.failure)(import_hardware_wallet_core2.HardwareErrorCode.DeviceMismatch, "Wrong device connected");
437
+ }
365
438
  try {
366
439
  const result = await this.connectorCall(connectId, "evmSignMessage", {
367
440
  path: params.path,
@@ -376,6 +449,9 @@ var _LedgerAdapter = class _LedgerAdapter {
376
449
  }
377
450
  async evmSignTypedData(connectId, _deviceId, params) {
378
451
  await this._ensureDevicePermission(connectId, _deviceId);
452
+ if (!await this._verifyDeviceFingerprint(connectId, _deviceId, "evm")) {
453
+ return (0, import_hardware_wallet_core2.failure)(import_hardware_wallet_core2.HardwareErrorCode.DeviceMismatch, "Wrong device connected");
454
+ }
379
455
  if (params.mode === "hash") {
380
456
  return (0, import_hardware_wallet_core2.failure)(
381
457
  import_hardware_wallet_core2.HardwareErrorCode.MethodNotSupported,
@@ -399,6 +475,9 @@ var _LedgerAdapter = class _LedgerAdapter {
399
475
  // ---------------------------------------------------------------------------
400
476
  async btcGetAddress(connectId, _deviceId, params) {
401
477
  await this._ensureDevicePermission(connectId, _deviceId);
478
+ if (!await this._verifyDeviceFingerprint(connectId, _deviceId, "btc")) {
479
+ return (0, import_hardware_wallet_core2.failure)(import_hardware_wallet_core2.HardwareErrorCode.DeviceMismatch, "Wrong device connected");
480
+ }
402
481
  try {
403
482
  const result = await this.connectorCall(connectId, "btcGetAddress", {
404
483
  path: params.path,
@@ -423,6 +502,9 @@ var _LedgerAdapter = class _LedgerAdapter {
423
502
  }
424
503
  async btcGetPublicKey(connectId, _deviceId, params) {
425
504
  await this._ensureDevicePermission(connectId, _deviceId);
505
+ if (!await this._verifyDeviceFingerprint(connectId, _deviceId, "btc")) {
506
+ return (0, import_hardware_wallet_core2.failure)(import_hardware_wallet_core2.HardwareErrorCode.DeviceMismatch, "Wrong device connected");
507
+ }
426
508
  try {
427
509
  const result = await this.connectorCall(connectId, "btcGetPublicKey", {
428
510
  path: params.path,
@@ -443,6 +525,9 @@ var _LedgerAdapter = class _LedgerAdapter {
443
525
  }
444
526
  async btcSignTransaction(connectId, _deviceId, params) {
445
527
  await this._ensureDevicePermission(connectId, _deviceId);
528
+ if (!await this._verifyDeviceFingerprint(connectId, _deviceId, "btc")) {
529
+ return (0, import_hardware_wallet_core2.failure)(import_hardware_wallet_core2.HardwareErrorCode.DeviceMismatch, "Wrong device connected");
530
+ }
446
531
  if (!params.psbt) {
447
532
  return (0, import_hardware_wallet_core2.failure)(
448
533
  import_hardware_wallet_core2.HardwareErrorCode.InvalidParams,
@@ -463,34 +548,75 @@ var _LedgerAdapter = class _LedgerAdapter {
463
548
  // ---------------------------------------------------------------------------
464
549
  // Device fingerprint
465
550
  // ---------------------------------------------------------------------------
466
- async btcGetMasterFingerprint(connectId, _deviceId, params) {
551
+ async btcGetMasterFingerprint(connectId, _deviceId) {
467
552
  await this._ensureDevicePermission(connectId, _deviceId);
553
+ if (!await this._verifyDeviceFingerprint(connectId, _deviceId, "btc")) {
554
+ return (0, import_hardware_wallet_core2.failure)(import_hardware_wallet_core2.HardwareErrorCode.DeviceMismatch, "Wrong device connected");
555
+ }
468
556
  try {
469
- const result = await this.connectorCall(connectId, "btcGetMasterFingerprint", {
470
- skipOpenApp: params?.skipOpenApp
471
- });
557
+ const result = await this.connectorCall(connectId, "btcGetMasterFingerprint", {});
472
558
  return (0, import_hardware_wallet_core2.success)({ masterFingerprint: result.masterFingerprint });
473
559
  } catch (err) {
474
560
  return this.errorToFailure(err);
475
561
  }
476
562
  }
477
563
  // ---------------------------------------------------------------------------
478
- // Solana methods (stubs -- not yet supported)
564
+ // Solana methods
479
565
  // ---------------------------------------------------------------------------
480
- async solGetAddress(_connectId, _deviceId, _params) {
481
- return (0, import_hardware_wallet_core2.failure)(import_hardware_wallet_core2.HardwareErrorCode.MethodNotSupported, "Solana not supported on Ledger yet");
566
+ async solGetAddress(connectId, _deviceId, params) {
567
+ await this._ensureDevicePermission(connectId, _deviceId);
568
+ if (!await this._verifyDeviceFingerprint(connectId, _deviceId, "sol")) {
569
+ return (0, import_hardware_wallet_core2.failure)(import_hardware_wallet_core2.HardwareErrorCode.DeviceMismatch, "Wrong device connected");
570
+ }
571
+ try {
572
+ const result = await this.connectorCall(connectId, "solGetAddress", {
573
+ path: params.path,
574
+ showOnDevice: params.showOnDevice
575
+ });
576
+ return (0, import_hardware_wallet_core2.success)({
577
+ address: result.address,
578
+ path: params.path
579
+ });
580
+ } catch (err) {
581
+ return this.errorToFailure(err);
582
+ }
482
583
  }
483
- async solGetAddresses(_connectId, _deviceId, _params, _onProgress) {
484
- return (0, import_hardware_wallet_core2.failure)(import_hardware_wallet_core2.HardwareErrorCode.MethodNotSupported, "Solana not supported on Ledger yet");
584
+ async solGetAddresses(connectId, deviceId, params, onProgress) {
585
+ return this.batchCall(
586
+ params,
587
+ (p) => this.solGetAddress(connectId, deviceId, p),
588
+ onProgress
589
+ );
485
590
  }
486
- async solGetPublicKey(_connectId, _deviceId, _params) {
487
- return (0, import_hardware_wallet_core2.failure)(import_hardware_wallet_core2.HardwareErrorCode.MethodNotSupported, "Solana not supported on Ledger yet");
591
+ async solGetPublicKey(connectId, _deviceId, params) {
592
+ await this._ensureDevicePermission(connectId, _deviceId);
593
+ if (!await this._verifyDeviceFingerprint(connectId, _deviceId, "sol")) {
594
+ return (0, import_hardware_wallet_core2.failure)(import_hardware_wallet_core2.HardwareErrorCode.DeviceMismatch, "Wrong device connected");
595
+ }
596
+ try {
597
+ const result = await this.connectorCall(connectId, "solGetAddress", {
598
+ path: params.path,
599
+ showOnDevice: params.showOnDevice
600
+ });
601
+ return (0, import_hardware_wallet_core2.success)({
602
+ publicKey: result.address,
603
+ path: params.path
604
+ });
605
+ } catch (err) {
606
+ return this.errorToFailure(err);
607
+ }
488
608
  }
489
609
  async solSignTransaction(_connectId, _deviceId, _params) {
490
- return (0, import_hardware_wallet_core2.failure)(import_hardware_wallet_core2.HardwareErrorCode.MethodNotSupported, "Solana not supported on Ledger yet");
610
+ return (0, import_hardware_wallet_core2.failure)(
611
+ import_hardware_wallet_core2.HardwareErrorCode.MethodNotSupported,
612
+ "Solana transaction signing via Ledger is not yet implemented."
613
+ );
491
614
  }
492
615
  async solSignMessage(_connectId, _deviceId, _params) {
493
- return (0, import_hardware_wallet_core2.failure)(import_hardware_wallet_core2.HardwareErrorCode.MethodNotSupported, "Solana signMessage is not supported on Ledger yet");
616
+ return (0, import_hardware_wallet_core2.failure)(
617
+ import_hardware_wallet_core2.HardwareErrorCode.MethodNotSupported,
618
+ "Solana message signing via Ledger is not yet implemented."
619
+ );
494
620
  }
495
621
  /**
496
622
  * Wait for user to connect and unlock device.
@@ -515,7 +641,10 @@ var _LedgerAdapter = class _LedgerAdapter {
515
641
  clearTimeout(timer);
516
642
  this._deviceConnectResolve = null;
517
643
  if (cancelled) {
518
- reject(new Error("User cancelled Ledger connection"));
644
+ reject(Object.assign(
645
+ new Error("User cancelled Ledger connection"),
646
+ { _tag: "DeviceNotRecognizedError" }
647
+ ));
519
648
  } else {
520
649
  resolve();
521
650
  }
@@ -546,6 +675,17 @@ var _LedgerAdapter = class _LedgerAdapter {
546
675
  if (this._sessions.size > 0) {
547
676
  return this._sessions.keys().next().value;
548
677
  }
678
+ if (this._connectingPromise) {
679
+ return this._connectingPromise;
680
+ }
681
+ this._connectingPromise = this._doConnect();
682
+ try {
683
+ return await this._connectingPromise;
684
+ } finally {
685
+ this._connectingPromise = null;
686
+ }
687
+ }
688
+ async _doConnect() {
549
689
  for (let attempt = 0; attempt < _LedgerAdapter.MAX_DEVICE_RETRY; attempt++) {
550
690
  const devices = await this.searchDevices();
551
691
  if (devices.length > 0) {
@@ -779,7 +919,8 @@ var LedgerDeviceManager = class {
779
919
  return new Promise((resolve) => {
780
920
  let resolved = false;
781
921
  let syncResult = null;
782
- const sub = this._dmk.listenToAvailableDevices().subscribe({
922
+ let sub = null;
923
+ sub = this._dmk.listenToAvailableDevices().subscribe({
783
924
  next: (devices) => {
784
925
  if (resolved) return;
785
926
  resolved = true;
@@ -787,7 +928,17 @@ var LedgerDeviceManager = class {
787
928
  for (const d of devices) {
788
929
  this._discovered.set(d.id, d);
789
930
  }
790
- syncResult = devices;
931
+ if (sub) {
932
+ sub.unsubscribe();
933
+ resolve(devices.map((d) => ({
934
+ path: d.id,
935
+ type: d.deviceModel.id,
936
+ name: d.name,
937
+ transport: d.transport
938
+ })));
939
+ } else {
940
+ syncResult = devices;
941
+ }
791
942
  },
792
943
  error: () => {
793
944
  if (!resolved) {
@@ -799,7 +950,12 @@ var LedgerDeviceManager = class {
799
950
  if (syncResult !== null) {
800
951
  sub.unsubscribe();
801
952
  const devices = syncResult;
802
- resolve(devices.map((d) => ({ path: d.id, type: d.deviceModel.name })));
953
+ resolve(devices.map((d) => ({
954
+ path: d.id,
955
+ type: d.deviceModel.id,
956
+ name: d.name,
957
+ transport: d.transport
958
+ })));
803
959
  }
804
960
  });
805
961
  }
@@ -820,7 +976,12 @@ var LedgerDeviceManager = class {
820
976
  name: d.name
821
977
  }));
822
978
  if (!previousIds.has(d.id)) {
823
- onChange({ type: "device-connected", descriptor: { path: d.id, type: d.deviceModel.name } });
979
+ onChange({ type: "device-connected", descriptor: {
980
+ path: d.id,
981
+ type: d.deviceModel.id,
982
+ name: d.name,
983
+ transport: d.transport
984
+ } });
824
985
  }
825
986
  }
826
987
  for (const id of previousIds) {
@@ -1030,6 +1191,36 @@ var SignerBtc = class {
1030
1191
  }
1031
1192
  };
1032
1193
 
1194
+ // src/signer/SignerSol.ts
1195
+ var SignerSol = class {
1196
+ constructor(_sdk) {
1197
+ this._sdk = _sdk;
1198
+ }
1199
+ /**
1200
+ * Get the Solana address (base58-encoded Ed25519 public key) at the given derivation path.
1201
+ */
1202
+ async getAddress(derivationPath, options) {
1203
+ const action = this._sdk.getAddress(derivationPath, {
1204
+ checkOnDevice: options?.checkOnDevice ?? false
1205
+ });
1206
+ return deviceActionToPromise(action, this.onInteraction);
1207
+ }
1208
+ /**
1209
+ * Sign a Solana transaction.
1210
+ */
1211
+ async signTransaction(derivationPath, transaction, options) {
1212
+ const action = this._sdk.signTransaction(derivationPath, transaction, options);
1213
+ return deviceActionToPromise(action, this.onInteraction);
1214
+ }
1215
+ /**
1216
+ * Sign a message with the Solana app.
1217
+ */
1218
+ async signMessage(derivationPath, message, options) {
1219
+ const action = this._sdk.signMessage(derivationPath, message, options);
1220
+ return deviceActionToPromise(action, this.onInteraction);
1221
+ }
1222
+ };
1223
+
1033
1224
  // src/transport/registry.ts
1034
1225
  var registry = /* @__PURE__ */ new Map();
1035
1226
  function normalizeType(type) {
@@ -1153,6 +1344,7 @@ var AppManager = class {
1153
1344
  SignerBtc,
1154
1345
  SignerEth,
1155
1346
  SignerManager,
1347
+ SignerSol,
1156
1348
  clearRegistry,
1157
1349
  deviceActionToPromise,
1158
1350
  getTransportProvider,