@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.d.mts +16 -4
- package/dist/index.d.ts +16 -4
- package/dist/index.js +131 -29
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +134 -30
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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)
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
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.
|
|
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
|
},
|