@bytezhang/ledger-adapter 0.0.5 → 0.0.7

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,12 +515,13 @@ 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);
@@ -481,7 +568,10 @@ var _LedgerAdapter = class _LedgerAdapter {
481
568
  clearTimeout(timer);
482
569
  this._deviceConnectResolve = null;
483
570
  if (cancelled) {
484
- reject(new Error("User cancelled Ledger connection"));
571
+ reject(Object.assign(
572
+ new Error("User cancelled Ledger connection"),
573
+ { _tag: "DeviceNotRecognizedError" }
574
+ ));
485
575
  } else {
486
576
  resolve();
487
577
  }
@@ -512,6 +602,17 @@ var _LedgerAdapter = class _LedgerAdapter {
512
602
  if (this._sessions.size > 0) {
513
603
  return this._sessions.keys().next().value;
514
604
  }
605
+ if (this._connectingPromise) {
606
+ return this._connectingPromise;
607
+ }
608
+ this._connectingPromise = this._doConnect();
609
+ try {
610
+ return await this._connectingPromise;
611
+ } finally {
612
+ this._connectingPromise = null;
613
+ }
614
+ }
615
+ async _doConnect() {
515
616
  for (let attempt = 0; attempt < _LedgerAdapter.MAX_DEVICE_RETRY; attempt++) {
516
617
  const devices = await this.searchDevices();
517
618
  if (devices.length > 0) {
@@ -744,6 +845,7 @@ var LedgerDeviceManager = class {
744
845
  enumerate() {
745
846
  return new Promise((resolve) => {
746
847
  let resolved = false;
848
+ let syncResult = null;
747
849
  let sub = null;
748
850
  sub = this._dmk.listenToAvailableDevices().subscribe({
749
851
  next: (devices) => {
@@ -755,23 +857,23 @@ var LedgerDeviceManager = class {
755
857
  }
756
858
  if (sub) {
757
859
  sub.unsubscribe();
860
+ resolve(devices.map((d) => ({ path: d.id, type: d.deviceModel.id })));
758
861
  } else {
759
- Promise.resolve().then(() => sub?.unsubscribe());
862
+ syncResult = devices;
760
863
  }
761
- console.log("[LedgerDeviceManager] enumerate devices:", JSON.stringify(devices.map((d) => ({
762
- id: d.id,
763
- deviceModel: d.deviceModel,
764
- name: d.name
765
- }))));
766
- resolve(devices.map((d) => ({ path: d.id, type: d.deviceModel.name })));
767
864
  },
768
865
  error: () => {
769
- if (resolved) return;
770
- resolved = true;
771
- sub?.unsubscribe();
772
- resolve([]);
866
+ if (!resolved) {
867
+ resolved = true;
868
+ resolve([]);
869
+ }
773
870
  }
774
871
  });
872
+ if (syncResult !== null) {
873
+ sub.unsubscribe();
874
+ const devices = syncResult;
875
+ resolve(devices.map((d) => ({ path: d.id, type: d.deviceModel.id })));
876
+ }
775
877
  });
776
878
  }
777
879
  /**
@@ -791,7 +893,7 @@ var LedgerDeviceManager = class {
791
893
  name: d.name
792
894
  }));
793
895
  if (!previousIds.has(d.id)) {
794
- onChange({ type: "device-connected", descriptor: { path: d.id, type: d.deviceModel.name } });
896
+ onChange({ type: "device-connected", descriptor: { path: d.id, type: d.deviceModel.id } });
795
897
  }
796
898
  }
797
899
  for (const id of previousIds) {
@@ -887,6 +989,8 @@ function deviceActionToPromise(action, onInteraction) {
887
989
  const interaction = state.intermediateValue?.requiredUserInteraction;
888
990
  if (interaction && interaction !== "none") {
889
991
  onInteraction(interaction);
992
+ } else if (interaction === "none") {
993
+ onInteraction("interaction-complete");
890
994
  }
891
995
  }
892
996
  },