@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.mjs CHANGED
@@ -13,7 +13,9 @@ import {
13
13
  HardwareErrorCode as HardwareErrorCode2,
14
14
  TypedEventEmitter,
15
15
  DEVICE,
16
- UI_REQUEST
16
+ UI_REQUEST,
17
+ CHAIN_FINGERPRINT_PATHS,
18
+ deriveDeviceFingerprint
17
19
  } from "@bytezhang/hardware-wallet-core";
18
20
 
19
21
  // src/errors.ts
@@ -135,6 +137,8 @@ var _LedgerAdapter = class _LedgerAdapter {
135
137
  this._sessions = /* @__PURE__ */ new Map();
136
138
  // Pending device-connect resolve — set by _waitForDeviceConnect, resolved by uiResponse
137
139
  this._deviceConnectResolve = null;
140
+ // Mutex for ensureConnected — prevents concurrent calls from establishing duplicate connections
141
+ this._connectingPromise = null;
138
142
  // ---------------------------------------------------------------------------
139
143
  // Event translation
140
144
  // ---------------------------------------------------------------------------
@@ -188,6 +192,8 @@ var _LedgerAdapter = class _LedgerAdapter {
188
192
  async init(_config) {
189
193
  }
190
194
  async dispose() {
195
+ this._deviceConnectResolve?.(true);
196
+ this._deviceConnectResolve = null;
191
197
  this.unregisterEventListeners();
192
198
  this.connector.reset();
193
199
  this._uiHandler = null;
@@ -259,10 +265,74 @@ var _LedgerAdapter = class _LedgerAdapter {
259
265
  void this.connector.cancel(sessionId);
260
266
  }
261
267
  // ---------------------------------------------------------------------------
268
+ // Chain fingerprint
269
+ // ---------------------------------------------------------------------------
270
+ async getChainFingerprint(connectId, deviceId, chain) {
271
+ await this._ensureDevicePermission(connectId, deviceId);
272
+ try {
273
+ const address = await this._deriveAddressForFingerprint(connectId, chain);
274
+ return success(deriveDeviceFingerprint(address));
275
+ } catch (err) {
276
+ return this.errorToFailure(err);
277
+ }
278
+ }
279
+ /**
280
+ * Verify that the connected device matches the expected fingerprint.
281
+ *
282
+ * - If deviceId is empty, verification is skipped (returns true).
283
+ * - deviceId is used here as the stored fingerprint to compare against.
284
+ */
285
+ async _verifyDeviceFingerprint(connectId, deviceId, chain) {
286
+ if (!deviceId) return true;
287
+ try {
288
+ const address = await this._deriveAddressForFingerprint(connectId, chain);
289
+ const fingerprint = deriveDeviceFingerprint(address);
290
+ return fingerprint === deviceId;
291
+ } catch (err) {
292
+ const mapped = mapLedgerError(err);
293
+ if (mapped.code === HardwareErrorCode2.WrongApp || mapped.code === HardwareErrorCode2.DeviceLocked) {
294
+ return true;
295
+ }
296
+ throw err;
297
+ }
298
+ }
299
+ /**
300
+ * Derive an address at the fixed testnet path for fingerprint generation.
301
+ */
302
+ async _deriveAddressForFingerprint(connectId, chain) {
303
+ const path = CHAIN_FINGERPRINT_PATHS[chain];
304
+ if (chain === "evm") {
305
+ const result = await this.connectorCall(connectId, "evmGetAddress", {
306
+ path,
307
+ showOnDevice: false
308
+ });
309
+ return result.address;
310
+ }
311
+ if (chain === "btc") {
312
+ const result = await this.connectorCall(connectId, "btcGetAddress", {
313
+ path,
314
+ showOnDevice: false,
315
+ coin: "Testnet"
316
+ });
317
+ return result.address;
318
+ }
319
+ if (chain === "sol") {
320
+ const result = await this.connectorCall(connectId, "solGetAddress", {
321
+ path,
322
+ showOnDevice: false
323
+ });
324
+ return result.address;
325
+ }
326
+ throw new Error(`Unsupported chain for fingerprint: ${chain}`);
327
+ }
328
+ // ---------------------------------------------------------------------------
262
329
  // EVM methods
263
330
  // ---------------------------------------------------------------------------
264
331
  async evmGetAddress(connectId, _deviceId, params) {
265
332
  await this._ensureDevicePermission(connectId, _deviceId);
333
+ if (!await this._verifyDeviceFingerprint(connectId, _deviceId, "evm")) {
334
+ return failure(HardwareErrorCode2.DeviceMismatch, "Wrong device connected");
335
+ }
266
336
  try {
267
337
  const result = await this.connectorCall(connectId, "evmGetAddress", {
268
338
  path: params.path,
@@ -286,6 +356,9 @@ var _LedgerAdapter = class _LedgerAdapter {
286
356
  }
287
357
  async evmGetPublicKey(connectId, _deviceId, params) {
288
358
  await this._ensureDevicePermission(connectId, _deviceId);
359
+ if (!await this._verifyDeviceFingerprint(connectId, _deviceId, "evm")) {
360
+ return failure(HardwareErrorCode2.DeviceMismatch, "Wrong device connected");
361
+ }
289
362
  try {
290
363
  const result = await this.connectorCall(connectId, "evmGetAddress", {
291
364
  path: params.path,
@@ -301,21 +374,19 @@ var _LedgerAdapter = class _LedgerAdapter {
301
374
  }
302
375
  async evmSignTransaction(connectId, _deviceId, params) {
303
376
  await this._ensureDevicePermission(connectId, _deviceId);
377
+ if (!await this._verifyDeviceFingerprint(connectId, _deviceId, "evm")) {
378
+ return failure(HardwareErrorCode2.DeviceMismatch, "Wrong device connected");
379
+ }
304
380
  try {
381
+ if (!params.serializedTx) {
382
+ return failure(
383
+ HardwareErrorCode2.InvalidParams,
384
+ "Ledger requires a pre-serialized transaction (serializedTx). Provide an RLP-encoded hex string."
385
+ );
386
+ }
305
387
  const result = await this.connectorCall(connectId, "evmSignTransaction", {
306
388
  path: params.path,
307
- transaction: {
308
- to: params.to,
309
- value: params.value,
310
- chainId: params.chainId,
311
- nonce: params.nonce,
312
- gasLimit: params.gasLimit,
313
- gasPrice: params.gasPrice,
314
- maxFeePerGas: params.maxFeePerGas,
315
- maxPriorityFeePerGas: params.maxPriorityFeePerGas,
316
- accessList: params.accessList,
317
- data: params.data
318
- }
389
+ serializedTx: params.serializedTx
319
390
  });
320
391
  return success({
321
392
  v: ensure0x(result.v),
@@ -328,6 +399,9 @@ var _LedgerAdapter = class _LedgerAdapter {
328
399
  }
329
400
  async evmSignMessage(connectId, _deviceId, params) {
330
401
  await this._ensureDevicePermission(connectId, _deviceId);
402
+ if (!await this._verifyDeviceFingerprint(connectId, _deviceId, "evm")) {
403
+ return failure(HardwareErrorCode2.DeviceMismatch, "Wrong device connected");
404
+ }
331
405
  try {
332
406
  const result = await this.connectorCall(connectId, "evmSignMessage", {
333
407
  path: params.path,
@@ -342,6 +416,9 @@ var _LedgerAdapter = class _LedgerAdapter {
342
416
  }
343
417
  async evmSignTypedData(connectId, _deviceId, params) {
344
418
  await this._ensureDevicePermission(connectId, _deviceId);
419
+ if (!await this._verifyDeviceFingerprint(connectId, _deviceId, "evm")) {
420
+ return failure(HardwareErrorCode2.DeviceMismatch, "Wrong device connected");
421
+ }
345
422
  if (params.mode === "hash") {
346
423
  return failure(
347
424
  HardwareErrorCode2.MethodNotSupported,
@@ -365,6 +442,9 @@ var _LedgerAdapter = class _LedgerAdapter {
365
442
  // ---------------------------------------------------------------------------
366
443
  async btcGetAddress(connectId, _deviceId, params) {
367
444
  await this._ensureDevicePermission(connectId, _deviceId);
445
+ if (!await this._verifyDeviceFingerprint(connectId, _deviceId, "btc")) {
446
+ return failure(HardwareErrorCode2.DeviceMismatch, "Wrong device connected");
447
+ }
368
448
  try {
369
449
  const result = await this.connectorCall(connectId, "btcGetAddress", {
370
450
  path: params.path,
@@ -389,6 +469,9 @@ var _LedgerAdapter = class _LedgerAdapter {
389
469
  }
390
470
  async btcGetPublicKey(connectId, _deviceId, params) {
391
471
  await this._ensureDevicePermission(connectId, _deviceId);
472
+ if (!await this._verifyDeviceFingerprint(connectId, _deviceId, "btc")) {
473
+ return failure(HardwareErrorCode2.DeviceMismatch, "Wrong device connected");
474
+ }
392
475
  try {
393
476
  const result = await this.connectorCall(connectId, "btcGetPublicKey", {
394
477
  path: params.path,
@@ -409,6 +492,9 @@ var _LedgerAdapter = class _LedgerAdapter {
409
492
  }
410
493
  async btcSignTransaction(connectId, _deviceId, params) {
411
494
  await this._ensureDevicePermission(connectId, _deviceId);
495
+ if (!await this._verifyDeviceFingerprint(connectId, _deviceId, "btc")) {
496
+ return failure(HardwareErrorCode2.DeviceMismatch, "Wrong device connected");
497
+ }
412
498
  if (!params.psbt) {
413
499
  return failure(
414
500
  HardwareErrorCode2.InvalidParams,
@@ -429,34 +515,75 @@ var _LedgerAdapter = class _LedgerAdapter {
429
515
  // ---------------------------------------------------------------------------
430
516
  // Device fingerprint
431
517
  // ---------------------------------------------------------------------------
432
- async btcGetMasterFingerprint(connectId, _deviceId, params) {
518
+ async btcGetMasterFingerprint(connectId, _deviceId) {
433
519
  await this._ensureDevicePermission(connectId, _deviceId);
520
+ if (!await this._verifyDeviceFingerprint(connectId, _deviceId, "btc")) {
521
+ return failure(HardwareErrorCode2.DeviceMismatch, "Wrong device connected");
522
+ }
434
523
  try {
435
- const result = await this.connectorCall(connectId, "btcGetMasterFingerprint", {
436
- skipOpenApp: params?.skipOpenApp
437
- });
524
+ const result = await this.connectorCall(connectId, "btcGetMasterFingerprint", {});
438
525
  return success({ masterFingerprint: result.masterFingerprint });
439
526
  } catch (err) {
440
527
  return this.errorToFailure(err);
441
528
  }
442
529
  }
443
530
  // ---------------------------------------------------------------------------
444
- // Solana methods (stubs -- not yet supported)
531
+ // Solana methods
445
532
  // ---------------------------------------------------------------------------
446
- async solGetAddress(_connectId, _deviceId, _params) {
447
- return failure(HardwareErrorCode2.MethodNotSupported, "Solana not supported on Ledger yet");
533
+ async solGetAddress(connectId, _deviceId, params) {
534
+ await this._ensureDevicePermission(connectId, _deviceId);
535
+ if (!await this._verifyDeviceFingerprint(connectId, _deviceId, "sol")) {
536
+ return failure(HardwareErrorCode2.DeviceMismatch, "Wrong device connected");
537
+ }
538
+ try {
539
+ const result = await this.connectorCall(connectId, "solGetAddress", {
540
+ path: params.path,
541
+ showOnDevice: params.showOnDevice
542
+ });
543
+ return success({
544
+ address: result.address,
545
+ path: params.path
546
+ });
547
+ } catch (err) {
548
+ return this.errorToFailure(err);
549
+ }
448
550
  }
449
- async solGetAddresses(_connectId, _deviceId, _params, _onProgress) {
450
- return failure(HardwareErrorCode2.MethodNotSupported, "Solana not supported on Ledger yet");
551
+ async solGetAddresses(connectId, deviceId, params, onProgress) {
552
+ return this.batchCall(
553
+ params,
554
+ (p) => this.solGetAddress(connectId, deviceId, p),
555
+ onProgress
556
+ );
451
557
  }
452
- async solGetPublicKey(_connectId, _deviceId, _params) {
453
- return failure(HardwareErrorCode2.MethodNotSupported, "Solana not supported on Ledger yet");
558
+ async solGetPublicKey(connectId, _deviceId, params) {
559
+ await this._ensureDevicePermission(connectId, _deviceId);
560
+ if (!await this._verifyDeviceFingerprint(connectId, _deviceId, "sol")) {
561
+ return failure(HardwareErrorCode2.DeviceMismatch, "Wrong device connected");
562
+ }
563
+ try {
564
+ const result = await this.connectorCall(connectId, "solGetAddress", {
565
+ path: params.path,
566
+ showOnDevice: params.showOnDevice
567
+ });
568
+ return success({
569
+ publicKey: result.address,
570
+ path: params.path
571
+ });
572
+ } catch (err) {
573
+ return this.errorToFailure(err);
574
+ }
454
575
  }
455
576
  async solSignTransaction(_connectId, _deviceId, _params) {
456
- return failure(HardwareErrorCode2.MethodNotSupported, "Solana not supported on Ledger yet");
577
+ return failure(
578
+ HardwareErrorCode2.MethodNotSupported,
579
+ "Solana transaction signing via Ledger is not yet implemented."
580
+ );
457
581
  }
458
582
  async solSignMessage(_connectId, _deviceId, _params) {
459
- return failure(HardwareErrorCode2.MethodNotSupported, "Solana signMessage is not supported on Ledger yet");
583
+ return failure(
584
+ HardwareErrorCode2.MethodNotSupported,
585
+ "Solana message signing via Ledger is not yet implemented."
586
+ );
460
587
  }
461
588
  /**
462
589
  * Wait for user to connect and unlock device.
@@ -481,7 +608,10 @@ var _LedgerAdapter = class _LedgerAdapter {
481
608
  clearTimeout(timer);
482
609
  this._deviceConnectResolve = null;
483
610
  if (cancelled) {
484
- reject(new Error("User cancelled Ledger connection"));
611
+ reject(Object.assign(
612
+ new Error("User cancelled Ledger connection"),
613
+ { _tag: "DeviceNotRecognizedError" }
614
+ ));
485
615
  } else {
486
616
  resolve();
487
617
  }
@@ -512,6 +642,17 @@ var _LedgerAdapter = class _LedgerAdapter {
512
642
  if (this._sessions.size > 0) {
513
643
  return this._sessions.keys().next().value;
514
644
  }
645
+ if (this._connectingPromise) {
646
+ return this._connectingPromise;
647
+ }
648
+ this._connectingPromise = this._doConnect();
649
+ try {
650
+ return await this._connectingPromise;
651
+ } finally {
652
+ this._connectingPromise = null;
653
+ }
654
+ }
655
+ async _doConnect() {
515
656
  for (let attempt = 0; attempt < _LedgerAdapter.MAX_DEVICE_RETRY; attempt++) {
516
657
  const devices = await this.searchDevices();
517
658
  if (devices.length > 0) {
@@ -745,7 +886,8 @@ var LedgerDeviceManager = class {
745
886
  return new Promise((resolve) => {
746
887
  let resolved = false;
747
888
  let syncResult = null;
748
- const sub = this._dmk.listenToAvailableDevices().subscribe({
889
+ let sub = null;
890
+ sub = this._dmk.listenToAvailableDevices().subscribe({
749
891
  next: (devices) => {
750
892
  if (resolved) return;
751
893
  resolved = true;
@@ -753,7 +895,17 @@ var LedgerDeviceManager = class {
753
895
  for (const d of devices) {
754
896
  this._discovered.set(d.id, d);
755
897
  }
756
- syncResult = devices;
898
+ if (sub) {
899
+ sub.unsubscribe();
900
+ resolve(devices.map((d) => ({
901
+ path: d.id,
902
+ type: d.deviceModel.id,
903
+ name: d.name,
904
+ transport: d.transport
905
+ })));
906
+ } else {
907
+ syncResult = devices;
908
+ }
757
909
  },
758
910
  error: () => {
759
911
  if (!resolved) {
@@ -765,7 +917,12 @@ var LedgerDeviceManager = class {
765
917
  if (syncResult !== null) {
766
918
  sub.unsubscribe();
767
919
  const devices = syncResult;
768
- resolve(devices.map((d) => ({ path: d.id, type: d.deviceModel.name })));
920
+ resolve(devices.map((d) => ({
921
+ path: d.id,
922
+ type: d.deviceModel.id,
923
+ name: d.name,
924
+ transport: d.transport
925
+ })));
769
926
  }
770
927
  });
771
928
  }
@@ -786,7 +943,12 @@ var LedgerDeviceManager = class {
786
943
  name: d.name
787
944
  }));
788
945
  if (!previousIds.has(d.id)) {
789
- onChange({ type: "device-connected", descriptor: { path: d.id, type: d.deviceModel.name } });
946
+ onChange({ type: "device-connected", descriptor: {
947
+ path: d.id,
948
+ type: d.deviceModel.id,
949
+ name: d.name,
950
+ transport: d.transport
951
+ } });
790
952
  }
791
953
  }
792
954
  for (const id of previousIds) {
@@ -996,6 +1158,36 @@ var SignerBtc = class {
996
1158
  }
997
1159
  };
998
1160
 
1161
+ // src/signer/SignerSol.ts
1162
+ var SignerSol = class {
1163
+ constructor(_sdk) {
1164
+ this._sdk = _sdk;
1165
+ }
1166
+ /**
1167
+ * Get the Solana address (base58-encoded Ed25519 public key) at the given derivation path.
1168
+ */
1169
+ async getAddress(derivationPath, options) {
1170
+ const action = this._sdk.getAddress(derivationPath, {
1171
+ checkOnDevice: options?.checkOnDevice ?? false
1172
+ });
1173
+ return deviceActionToPromise(action, this.onInteraction);
1174
+ }
1175
+ /**
1176
+ * Sign a Solana transaction.
1177
+ */
1178
+ async signTransaction(derivationPath, transaction, options) {
1179
+ const action = this._sdk.signTransaction(derivationPath, transaction, options);
1180
+ return deviceActionToPromise(action, this.onInteraction);
1181
+ }
1182
+ /**
1183
+ * Sign a message with the Solana app.
1184
+ */
1185
+ async signMessage(derivationPath, message, options) {
1186
+ const action = this._sdk.signMessage(derivationPath, message, options);
1187
+ return deviceActionToPromise(action, this.onInteraction);
1188
+ }
1189
+ };
1190
+
999
1191
  // src/app/AppManager.ts
1000
1192
  var APP_NAME_MAP = {
1001
1193
  ETH: "Ethereum",
@@ -1095,6 +1287,7 @@ export {
1095
1287
  SignerBtc,
1096
1288
  SignerEth,
1097
1289
  SignerManager,
1290
+ SignerSol,
1098
1291
  clearRegistry,
1099
1292
  deviceActionToPromise,
1100
1293
  getTransportProvider,