@bytezhang/ledger-adapter 0.0.18 → 0.0.21
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 +126 -6
- package/dist/index.d.ts +126 -6
- package/dist/index.js +501 -39
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +500 -39
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/dist/index.mjs
CHANGED
|
@@ -29,7 +29,7 @@ function isDeviceLockedError(err) {
|
|
|
29
29
|
if (e.errorCode != null && LOCKED_ERROR_CODES.has(String(e.errorCode))) return true;
|
|
30
30
|
if (e.statusCode != null && LOCKED_ERROR_CODES.has(String(e.statusCode))) return true;
|
|
31
31
|
if (e._tag === "DeviceLockedError") return true;
|
|
32
|
-
if (typeof e.message === "string" && /locked/i.test(e.message)) return true;
|
|
32
|
+
if (typeof e.message === "string" && /locked|device exchange error/i.test(e.message)) return true;
|
|
33
33
|
if (e.originalError != null && isDeviceLockedError(e.originalError)) return true;
|
|
34
34
|
if (e.error != null && e._tag && isDeviceLockedError(e.error)) return true;
|
|
35
35
|
return false;
|
|
@@ -65,7 +65,7 @@ function isDeviceDisconnectedError(err) {
|
|
|
65
65
|
if (!err || typeof err !== "object") return false;
|
|
66
66
|
const e = err;
|
|
67
67
|
if (e._tag === "DeviceNotRecognizedError" || e._tag === "DeviceSessionNotFound") return true;
|
|
68
|
-
if (typeof e.message === "string" && /disconnected|not found|no device|unplugged|session.*not.*found/i.test(e.message)) return true;
|
|
68
|
+
if (typeof e.message === "string" && /disconnected|not found|no device|unplugged|session.*not.*found|timed out.*locked/i.test(e.message)) return true;
|
|
69
69
|
return false;
|
|
70
70
|
}
|
|
71
71
|
function isTimeoutError(err) {
|
|
@@ -252,7 +252,7 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
252
252
|
);
|
|
253
253
|
}
|
|
254
254
|
getSupportedChains() {
|
|
255
|
-
return ["evm", "btc", "sol"];
|
|
255
|
+
return ["evm", "btc", "sol", "tron"];
|
|
256
256
|
}
|
|
257
257
|
on(event, listener) {
|
|
258
258
|
this.emitter.on(event, listener);
|
|
@@ -502,16 +502,40 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
502
502
|
"Ledger requires PSBT format for BTC transaction signing. Provide params.psbt."
|
|
503
503
|
);
|
|
504
504
|
}
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
505
|
+
try {
|
|
506
|
+
const accountPath = params.inputs?.[0]?.path ? params.inputs[0].path.split("/").slice(0, 3).join("/") : void 0;
|
|
507
|
+
const result = await this.connectorCall(connectId, "btcSignTransaction", {
|
|
508
|
+
psbt: params.psbt,
|
|
509
|
+
coin: params.coin,
|
|
510
|
+
path: accountPath
|
|
511
|
+
});
|
|
512
|
+
return success({
|
|
513
|
+
signatures: [],
|
|
514
|
+
serializedTx: result.signedPsbt,
|
|
515
|
+
signedPsbt: result.signedPsbt
|
|
516
|
+
});
|
|
517
|
+
} catch (err) {
|
|
518
|
+
return this.errorToFailure(err);
|
|
519
|
+
}
|
|
509
520
|
}
|
|
510
|
-
async btcSignMessage(
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
521
|
+
async btcSignMessage(connectId, _deviceId, params) {
|
|
522
|
+
await this._ensureDevicePermission(connectId, _deviceId);
|
|
523
|
+
if (!await this._verifyDeviceFingerprint(connectId, _deviceId, "btc")) {
|
|
524
|
+
return failure(HardwareErrorCode2.DeviceMismatch, "Wrong device connected");
|
|
525
|
+
}
|
|
526
|
+
try {
|
|
527
|
+
const result = await this.connectorCall(connectId, "btcSignMessage", {
|
|
528
|
+
path: params.path,
|
|
529
|
+
message: params.message,
|
|
530
|
+
coin: params.coin
|
|
531
|
+
});
|
|
532
|
+
return success({
|
|
533
|
+
signature: result.signature,
|
|
534
|
+
address: result.address || ""
|
|
535
|
+
});
|
|
536
|
+
} catch (err) {
|
|
537
|
+
return this.errorToFailure(err);
|
|
538
|
+
}
|
|
515
539
|
}
|
|
516
540
|
// ---------------------------------------------------------------------------
|
|
517
541
|
// Device fingerprint
|
|
@@ -574,18 +598,91 @@ var _LedgerAdapter = class _LedgerAdapter {
|
|
|
574
598
|
return this.errorToFailure(err);
|
|
575
599
|
}
|
|
576
600
|
}
|
|
577
|
-
async solSignTransaction(
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
601
|
+
async solSignTransaction(connectId, _deviceId, params) {
|
|
602
|
+
await this._ensureDevicePermission(connectId, _deviceId);
|
|
603
|
+
if (!await this._verifyDeviceFingerprint(connectId, _deviceId, "sol")) {
|
|
604
|
+
return failure(HardwareErrorCode2.DeviceMismatch, "Wrong device connected");
|
|
605
|
+
}
|
|
606
|
+
try {
|
|
607
|
+
const result = await this.connectorCall(connectId, "solSignTransaction", {
|
|
608
|
+
path: params.path,
|
|
609
|
+
serializedTx: params.serializedTx
|
|
610
|
+
});
|
|
611
|
+
return success({ signature: result.signature });
|
|
612
|
+
} catch (err) {
|
|
613
|
+
return this.errorToFailure(err);
|
|
614
|
+
}
|
|
582
615
|
}
|
|
583
|
-
async solSignMessage(
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
616
|
+
async solSignMessage(connectId, _deviceId, params) {
|
|
617
|
+
await this._ensureDevicePermission(connectId, _deviceId);
|
|
618
|
+
if (!await this._verifyDeviceFingerprint(connectId, _deviceId, "sol")) {
|
|
619
|
+
return failure(HardwareErrorCode2.DeviceMismatch, "Wrong device connected");
|
|
620
|
+
}
|
|
621
|
+
try {
|
|
622
|
+
const result = await this.connectorCall(connectId, "solSignMessage", {
|
|
623
|
+
path: params.path,
|
|
624
|
+
message: params.message
|
|
625
|
+
});
|
|
626
|
+
return success({ signature: result.signature });
|
|
627
|
+
} catch (err) {
|
|
628
|
+
return this.errorToFailure(err);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
// ---------------------------------------------------------------------------
|
|
632
|
+
// TRON methods
|
|
633
|
+
// ---------------------------------------------------------------------------
|
|
634
|
+
async tronGetAddress(connectId, _deviceId, params) {
|
|
635
|
+
await this._ensureDevicePermission(connectId, _deviceId);
|
|
636
|
+
try {
|
|
637
|
+
const result = await this.connectorCall(connectId, "tronGetAddress", {
|
|
638
|
+
path: params.path,
|
|
639
|
+
showOnDevice: params.showOnDevice
|
|
640
|
+
});
|
|
641
|
+
return success({
|
|
642
|
+
address: result.address,
|
|
643
|
+
path: params.path
|
|
644
|
+
});
|
|
645
|
+
} catch (err) {
|
|
646
|
+
return this.errorToFailure(err);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
async tronGetAddresses(connectId, deviceId, params, onProgress) {
|
|
650
|
+
return this.batchCall(
|
|
651
|
+
params,
|
|
652
|
+
(p) => this.tronGetAddress(connectId, deviceId, p),
|
|
653
|
+
onProgress
|
|
587
654
|
);
|
|
588
655
|
}
|
|
656
|
+
async tronSignTransaction(connectId, _deviceId, params) {
|
|
657
|
+
await this._ensureDevicePermission(connectId, _deviceId);
|
|
658
|
+
try {
|
|
659
|
+
if (!params.rawTxHex) {
|
|
660
|
+
return failure(
|
|
661
|
+
HardwareErrorCode2.InvalidParams,
|
|
662
|
+
"TRON signing requires a protobuf-encoded raw transaction hex (rawTxHex)."
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
const result = await this.connectorCall(connectId, "tronSignTransaction", {
|
|
666
|
+
path: params.path,
|
|
667
|
+
rawTxHex: params.rawTxHex
|
|
668
|
+
});
|
|
669
|
+
return success({ signature: result.signature });
|
|
670
|
+
} catch (err) {
|
|
671
|
+
return this.errorToFailure(err);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
async tronSignMessage(connectId, _deviceId, params) {
|
|
675
|
+
await this._ensureDevicePermission(connectId, _deviceId);
|
|
676
|
+
try {
|
|
677
|
+
const result = await this.connectorCall(connectId, "tronSignMessage", {
|
|
678
|
+
path: params.path,
|
|
679
|
+
messageHex: params.message
|
|
680
|
+
});
|
|
681
|
+
return success({ signature: result.signature });
|
|
682
|
+
} catch (err) {
|
|
683
|
+
return this.errorToFailure(err);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
589
686
|
/**
|
|
590
687
|
* Wait for user to connect and unlock device.
|
|
591
688
|
* Emits 'ui-request' event via the adapter's own emitter.
|
|
@@ -1035,12 +1132,30 @@ var LedgerDeviceManager = class {
|
|
|
1035
1132
|
};
|
|
1036
1133
|
|
|
1037
1134
|
// src/signer/deviceActionToPromise.ts
|
|
1038
|
-
|
|
1135
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
1136
|
+
function deviceActionToPromise(action, onInteraction, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
1039
1137
|
return new Promise((resolve, reject) => {
|
|
1040
1138
|
let settled = false;
|
|
1041
1139
|
let sub;
|
|
1140
|
+
let timer = null;
|
|
1141
|
+
const resetTimer = () => {
|
|
1142
|
+
if (timer) clearTimeout(timer);
|
|
1143
|
+
if (timeoutMs > 0) {
|
|
1144
|
+
timer = setTimeout(() => {
|
|
1145
|
+
if (!settled) {
|
|
1146
|
+
settled = true;
|
|
1147
|
+
sub?.unsubscribe();
|
|
1148
|
+
reject(new Error("Device action timed out \u2014 device may be locked or disconnected"));
|
|
1149
|
+
}
|
|
1150
|
+
}, timeoutMs);
|
|
1151
|
+
}
|
|
1152
|
+
};
|
|
1153
|
+
resetTimer();
|
|
1154
|
+
console.log("[DMK-Observable] subscribing to action.observable...");
|
|
1042
1155
|
sub = action.observable.subscribe({
|
|
1043
1156
|
next: (state) => {
|
|
1157
|
+
console.log("[DMK-Observable] next received");
|
|
1158
|
+
resetTimer();
|
|
1044
1159
|
console.log("[DMK-Observable] state:", JSON.stringify({
|
|
1045
1160
|
status: state.status,
|
|
1046
1161
|
interaction: state.intermediateValue?.requiredUserInteraction,
|
|
@@ -1051,11 +1166,13 @@ function deviceActionToPromise(action, onInteraction) {
|
|
|
1051
1166
|
if (settled) return;
|
|
1052
1167
|
if (state.status === "completed") {
|
|
1053
1168
|
settled = true;
|
|
1169
|
+
if (timer) clearTimeout(timer);
|
|
1054
1170
|
onInteraction?.("interaction-complete");
|
|
1055
1171
|
sub?.unsubscribe();
|
|
1056
1172
|
resolve(state.output);
|
|
1057
1173
|
} else if (state.status === "error") {
|
|
1058
1174
|
settled = true;
|
|
1175
|
+
if (timer) clearTimeout(timer);
|
|
1059
1176
|
onInteraction?.("interaction-complete");
|
|
1060
1177
|
sub?.unsubscribe();
|
|
1061
1178
|
reject(state.error);
|
|
@@ -1071,6 +1188,7 @@ function deviceActionToPromise(action, onInteraction) {
|
|
|
1071
1188
|
error: (err) => {
|
|
1072
1189
|
if (!settled) {
|
|
1073
1190
|
settled = true;
|
|
1191
|
+
if (timer) clearTimeout(timer);
|
|
1074
1192
|
sub?.unsubscribe();
|
|
1075
1193
|
reject(err);
|
|
1076
1194
|
}
|
|
@@ -1078,6 +1196,7 @@ function deviceActionToPromise(action, onInteraction) {
|
|
|
1078
1196
|
complete: () => {
|
|
1079
1197
|
if (!settled) {
|
|
1080
1198
|
settled = true;
|
|
1199
|
+
if (timer) clearTimeout(timer);
|
|
1081
1200
|
reject(new Error("Device action completed without result"));
|
|
1082
1201
|
}
|
|
1083
1202
|
}
|
|
@@ -1094,6 +1213,7 @@ function hexToBytes(hex) {
|
|
|
1094
1213
|
}
|
|
1095
1214
|
return bytes;
|
|
1096
1215
|
}
|
|
1216
|
+
var INTERACTIVE_TIMEOUT_MS = 5 * 6e4;
|
|
1097
1217
|
var SignerEth = class {
|
|
1098
1218
|
constructor(_sdk) {
|
|
1099
1219
|
this._sdk = _sdk;
|
|
@@ -1101,22 +1221,25 @@ var SignerEth = class {
|
|
|
1101
1221
|
async getAddress(derivationPath, options) {
|
|
1102
1222
|
const checkOnDevice = options?.checkOnDevice ?? false;
|
|
1103
1223
|
console.log("[DMK] getAddress \u2192 DMK:", { derivationPath, checkOnDevice });
|
|
1224
|
+
console.log("[DMK] signer instance id:", this._instanceId ?? "none");
|
|
1104
1225
|
const action = this._sdk.getAddress(derivationPath, {
|
|
1105
1226
|
checkOnDevice
|
|
1106
1227
|
});
|
|
1107
|
-
|
|
1228
|
+
console.log("[DMK] getAddress action created:", !!action, "hasObservable:", !!action?.observable);
|
|
1229
|
+
const timeout = checkOnDevice ? INTERACTIVE_TIMEOUT_MS : void 0;
|
|
1230
|
+
return deviceActionToPromise(action, this.onInteraction, timeout);
|
|
1108
1231
|
}
|
|
1109
1232
|
async signTransaction(derivationPath, serializedTxHex) {
|
|
1110
1233
|
const action = this._sdk.signTransaction(derivationPath, hexToBytes(serializedTxHex));
|
|
1111
|
-
return deviceActionToPromise(action, this.onInteraction);
|
|
1234
|
+
return deviceActionToPromise(action, this.onInteraction, INTERACTIVE_TIMEOUT_MS);
|
|
1112
1235
|
}
|
|
1113
1236
|
async signMessage(derivationPath, message) {
|
|
1114
1237
|
const action = this._sdk.signMessage(derivationPath, message);
|
|
1115
|
-
return deviceActionToPromise(action, this.onInteraction);
|
|
1238
|
+
return deviceActionToPromise(action, this.onInteraction, INTERACTIVE_TIMEOUT_MS);
|
|
1116
1239
|
}
|
|
1117
1240
|
async signTypedData(derivationPath, data) {
|
|
1118
1241
|
const action = this._sdk.signTypedData(derivationPath, data);
|
|
1119
|
-
return deviceActionToPromise(action, this.onInteraction);
|
|
1242
|
+
return deviceActionToPromise(action, this.onInteraction, INTERACTIVE_TIMEOUT_MS);
|
|
1120
1243
|
}
|
|
1121
1244
|
};
|
|
1122
1245
|
|
|
@@ -1128,11 +1251,13 @@ var SignerManager = class _SignerManager {
|
|
|
1128
1251
|
this._builderFn = builderFn ?? _SignerManager._defaultBuilder();
|
|
1129
1252
|
}
|
|
1130
1253
|
async getOrCreate(sessionId) {
|
|
1131
|
-
|
|
1132
|
-
|
|
1254
|
+
const hadCached = this._cache.has(sessionId);
|
|
1255
|
+
this._cache.delete(sessionId);
|
|
1256
|
+
console.log("[DMK] SignerManager.getOrCreate:", { sessionId, hadCached, creating: true });
|
|
1133
1257
|
const builder = await this._builderFn({ dmk: this._dmk, sessionId });
|
|
1134
1258
|
const sdkSigner = builder.build();
|
|
1135
|
-
|
|
1259
|
+
console.log("[DMK] SignerManager: new signer built");
|
|
1260
|
+
const signer = new SignerEth(sdkSigner);
|
|
1136
1261
|
this._cache.set(sessionId, signer);
|
|
1137
1262
|
return signer;
|
|
1138
1263
|
}
|
|
@@ -1155,6 +1280,7 @@ var SignerManager = class _SignerManager {
|
|
|
1155
1280
|
};
|
|
1156
1281
|
|
|
1157
1282
|
// src/signer/SignerBtc.ts
|
|
1283
|
+
var INTERACTIVE_TIMEOUT_MS2 = 5 * 6e4;
|
|
1158
1284
|
var SignerBtc = class {
|
|
1159
1285
|
constructor(_sdk) {
|
|
1160
1286
|
this._sdk = _sdk;
|
|
@@ -1177,6 +1303,31 @@ var SignerBtc = class {
|
|
|
1177
1303
|
const result = await deviceActionToPromise(action, this.onInteraction);
|
|
1178
1304
|
return result.masterFingerprint;
|
|
1179
1305
|
}
|
|
1306
|
+
/**
|
|
1307
|
+
* Sign a PSBT and return the array of partial signatures.
|
|
1308
|
+
* The `wallet` param is a DefaultWallet or WalletPolicy instance.
|
|
1309
|
+
* The `psbt` param can be a hex string, base64 string, or Uint8Array.
|
|
1310
|
+
*/
|
|
1311
|
+
async signPsbt(wallet, psbt, options) {
|
|
1312
|
+
const action = this._sdk.signPsbt(wallet, psbt, options);
|
|
1313
|
+
return deviceActionToPromise(action, this.onInteraction, INTERACTIVE_TIMEOUT_MS2);
|
|
1314
|
+
}
|
|
1315
|
+
/**
|
|
1316
|
+
* Sign a PSBT and return the fully extracted raw transaction as a hex string.
|
|
1317
|
+
* Like signPsbt, but also finalises the PSBT and extracts the transaction.
|
|
1318
|
+
*/
|
|
1319
|
+
async signTransaction(wallet, psbt, options) {
|
|
1320
|
+
const action = this._sdk.signTransaction(wallet, psbt, options);
|
|
1321
|
+
return deviceActionToPromise(action, this.onInteraction, INTERACTIVE_TIMEOUT_MS2);
|
|
1322
|
+
}
|
|
1323
|
+
/**
|
|
1324
|
+
* Sign a message with the BTC app (BIP-137 / "Bitcoin Signed Message").
|
|
1325
|
+
* Returns `{ r, s, v }` signature object.
|
|
1326
|
+
*/
|
|
1327
|
+
async signMessage(derivationPath, message, options) {
|
|
1328
|
+
const action = this._sdk.signMessage(derivationPath, message, options);
|
|
1329
|
+
return deviceActionToPromise(action, this.onInteraction, INTERACTIVE_TIMEOUT_MS2);
|
|
1330
|
+
}
|
|
1180
1331
|
};
|
|
1181
1332
|
|
|
1182
1333
|
// src/signer/SignerSol.ts
|
|
@@ -1209,6 +1360,200 @@ var SignerSol = class {
|
|
|
1209
1360
|
}
|
|
1210
1361
|
};
|
|
1211
1362
|
|
|
1363
|
+
// src/signer/SignerTron.ts
|
|
1364
|
+
var CLA = 224;
|
|
1365
|
+
var INS_ADDRESS = 2;
|
|
1366
|
+
var INS_SIGN = 4;
|
|
1367
|
+
var INS_SIGN_MESSAGE = 8;
|
|
1368
|
+
var CHUNK_SIZE = 250;
|
|
1369
|
+
function hexToBytes2(hex) {
|
|
1370
|
+
const h = hex.startsWith("0x") ? hex.slice(2) : hex;
|
|
1371
|
+
const bytes = new Uint8Array(h.length / 2);
|
|
1372
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
1373
|
+
bytes[i] = parseInt(h.substring(i * 2, i * 2 + 2), 16);
|
|
1374
|
+
}
|
|
1375
|
+
return bytes;
|
|
1376
|
+
}
|
|
1377
|
+
function bytesToHex(bytes) {
|
|
1378
|
+
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1379
|
+
}
|
|
1380
|
+
function splitPath(path) {
|
|
1381
|
+
const p = path.startsWith("m/") ? path.slice(2) : path;
|
|
1382
|
+
return p.split("/").map((component) => {
|
|
1383
|
+
const hardened = component.endsWith("'");
|
|
1384
|
+
const index = parseInt(hardened ? component.slice(0, -1) : component, 10);
|
|
1385
|
+
return hardened ? index + 2147483648 : index;
|
|
1386
|
+
});
|
|
1387
|
+
}
|
|
1388
|
+
function serializePath(path) {
|
|
1389
|
+
const components = splitPath(path);
|
|
1390
|
+
const buf = new Uint8Array(1 + components.length * 4);
|
|
1391
|
+
buf[0] = components.length;
|
|
1392
|
+
for (let i = 0; i < components.length; i++) {
|
|
1393
|
+
const val = components[i];
|
|
1394
|
+
const offset = 1 + i * 4;
|
|
1395
|
+
buf[offset] = val >>> 24 & 255;
|
|
1396
|
+
buf[offset + 1] = val >>> 16 & 255;
|
|
1397
|
+
buf[offset + 2] = val >>> 8 & 255;
|
|
1398
|
+
buf[offset + 3] = val & 255;
|
|
1399
|
+
}
|
|
1400
|
+
return buf;
|
|
1401
|
+
}
|
|
1402
|
+
function buildApdu(cla, ins, p1, p2, data) {
|
|
1403
|
+
const dataLen = data?.length ?? 0;
|
|
1404
|
+
const apdu = new Uint8Array(5 + dataLen);
|
|
1405
|
+
apdu[0] = cla;
|
|
1406
|
+
apdu[1] = ins;
|
|
1407
|
+
apdu[2] = p1;
|
|
1408
|
+
apdu[3] = p2;
|
|
1409
|
+
apdu[4] = dataLen;
|
|
1410
|
+
if (data && dataLen > 0) {
|
|
1411
|
+
apdu.set(data, 5);
|
|
1412
|
+
}
|
|
1413
|
+
return apdu;
|
|
1414
|
+
}
|
|
1415
|
+
function checkStatusCode(statusCode, context) {
|
|
1416
|
+
if (statusCode.length < 2) {
|
|
1417
|
+
throw new Error(`${context}: invalid status code length`);
|
|
1418
|
+
}
|
|
1419
|
+
const sw = statusCode[0] << 8 | statusCode[1];
|
|
1420
|
+
if (sw !== 36864) {
|
|
1421
|
+
throw Object.assign(
|
|
1422
|
+
new Error(`${context}: device returned error status 0x${sw.toString(16).padStart(4, "0")}`),
|
|
1423
|
+
{ statusCode: sw.toString(16), errorCode: sw.toString() }
|
|
1424
|
+
);
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
var SignerTron = class {
|
|
1428
|
+
constructor(_sendApdu) {
|
|
1429
|
+
this._sendApdu = _sendApdu;
|
|
1430
|
+
}
|
|
1431
|
+
/**
|
|
1432
|
+
* Get the TRON address at the given derivation path.
|
|
1433
|
+
*
|
|
1434
|
+
* APDU: CLA=0xE0, INS=0x02, P1=(showOnDevice?0x01:0x00), P2=0x00
|
|
1435
|
+
* Data: pathCount(1) + paths(4 bytes each, big-endian, hardened bit)
|
|
1436
|
+
* Response: pubKeyLen(1) + pubKey(pubKeyLen) + addrLen(1) + addr(addrLen, ASCII base58)
|
|
1437
|
+
*/
|
|
1438
|
+
async getAddress(path, options) {
|
|
1439
|
+
const showOnDevice = options?.checkOnDevice ?? false;
|
|
1440
|
+
const pathData = serializePath(path);
|
|
1441
|
+
const apdu = buildApdu(
|
|
1442
|
+
CLA,
|
|
1443
|
+
INS_ADDRESS,
|
|
1444
|
+
showOnDevice ? 1 : 0,
|
|
1445
|
+
0,
|
|
1446
|
+
pathData
|
|
1447
|
+
);
|
|
1448
|
+
const response = await this._sendApdu(apdu);
|
|
1449
|
+
checkStatusCode(response.statusCode, "tronGetAddress");
|
|
1450
|
+
const data = response.data;
|
|
1451
|
+
let offset = 0;
|
|
1452
|
+
const pubKeyLen = data[offset];
|
|
1453
|
+
offset += 1;
|
|
1454
|
+
const publicKey = bytesToHex(data.slice(offset, offset + pubKeyLen));
|
|
1455
|
+
offset += pubKeyLen;
|
|
1456
|
+
const addrLen = data[offset];
|
|
1457
|
+
offset += 1;
|
|
1458
|
+
const addressBytes = data.slice(offset, offset + addrLen);
|
|
1459
|
+
const address = new TextDecoder().decode(addressBytes);
|
|
1460
|
+
return { publicKey, address };
|
|
1461
|
+
}
|
|
1462
|
+
/**
|
|
1463
|
+
* Sign a TRON transaction (protobuf-encoded raw transaction).
|
|
1464
|
+
*
|
|
1465
|
+
* The transaction bytes are split into 250-byte chunks and sent sequentially.
|
|
1466
|
+
* First chunk includes the serialized derivation path prefix.
|
|
1467
|
+
*
|
|
1468
|
+
* P1 flags:
|
|
1469
|
+
* 0x10 = single chunk (entire tx fits in one APDU)
|
|
1470
|
+
* 0x00 = first chunk of multi-chunk
|
|
1471
|
+
* 0x80 = middle chunk (continuation)
|
|
1472
|
+
* 0x90 = last chunk (final continuation)
|
|
1473
|
+
*
|
|
1474
|
+
* Returns: 65-byte signature as hex string (no 0x prefix).
|
|
1475
|
+
*/
|
|
1476
|
+
async signTransaction(path, rawTxHex) {
|
|
1477
|
+
const pathData = serializePath(path);
|
|
1478
|
+
const txBytes = hexToBytes2(rawTxHex);
|
|
1479
|
+
const firstChunkMaxTx = CHUNK_SIZE - pathData.length;
|
|
1480
|
+
const txForFirst = txBytes.slice(0, firstChunkMaxTx);
|
|
1481
|
+
const firstPayload = new Uint8Array(pathData.length + txForFirst.length);
|
|
1482
|
+
firstPayload.set(pathData, 0);
|
|
1483
|
+
firstPayload.set(txForFirst, pathData.length);
|
|
1484
|
+
const remaining = txBytes.slice(firstChunkMaxTx);
|
|
1485
|
+
const chunks = [];
|
|
1486
|
+
let pos = 0;
|
|
1487
|
+
while (pos < remaining.length) {
|
|
1488
|
+
chunks.push(remaining.slice(pos, pos + CHUNK_SIZE));
|
|
1489
|
+
pos += CHUNK_SIZE;
|
|
1490
|
+
}
|
|
1491
|
+
const totalChunks = 1 + chunks.length;
|
|
1492
|
+
let response;
|
|
1493
|
+
if (totalChunks === 1) {
|
|
1494
|
+
const apdu = buildApdu(CLA, INS_SIGN, 16, 0, firstPayload);
|
|
1495
|
+
response = await this._sendApdu(apdu);
|
|
1496
|
+
checkStatusCode(response.statusCode, "tronSignTransaction");
|
|
1497
|
+
} else {
|
|
1498
|
+
const firstApdu = buildApdu(CLA, INS_SIGN, 0, 0, firstPayload);
|
|
1499
|
+
response = await this._sendApdu(firstApdu);
|
|
1500
|
+
checkStatusCode(response.statusCode, "tronSignTransaction (first)");
|
|
1501
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
1502
|
+
const isLast = i === chunks.length - 1;
|
|
1503
|
+
const p1 = isLast ? 144 : 128;
|
|
1504
|
+
const apdu = buildApdu(CLA, INS_SIGN, p1, 0, chunks[i]);
|
|
1505
|
+
response = await this._sendApdu(apdu);
|
|
1506
|
+
checkStatusCode(response.statusCode, `tronSignTransaction (chunk ${i + 1})`);
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
return bytesToHex(response.data.slice(0, 65));
|
|
1510
|
+
}
|
|
1511
|
+
/**
|
|
1512
|
+
* Sign a personal message with the TRON app.
|
|
1513
|
+
*
|
|
1514
|
+
* First chunk: pathCount(1) + paths(4 bytes BE) + messageLength(4 bytes BE) + message bytes
|
|
1515
|
+
* Subsequent: continuation bytes
|
|
1516
|
+
* P1: 0x00 = first, 0x80 = continuation
|
|
1517
|
+
*
|
|
1518
|
+
* Returns: 65-byte signature as hex string (no 0x prefix).
|
|
1519
|
+
*/
|
|
1520
|
+
async signMessage(path, messageHex) {
|
|
1521
|
+
const pathData = serializePath(path);
|
|
1522
|
+
const messageBytes = hexToBytes2(messageHex);
|
|
1523
|
+
const msgLenBuf = new Uint8Array(4);
|
|
1524
|
+
const msgLen = messageBytes.length;
|
|
1525
|
+
msgLenBuf[0] = msgLen >>> 24 & 255;
|
|
1526
|
+
msgLenBuf[1] = msgLen >>> 16 & 255;
|
|
1527
|
+
msgLenBuf[2] = msgLen >>> 8 & 255;
|
|
1528
|
+
msgLenBuf[3] = msgLen & 255;
|
|
1529
|
+
const header = new Uint8Array(pathData.length + 4);
|
|
1530
|
+
header.set(pathData, 0);
|
|
1531
|
+
header.set(msgLenBuf, pathData.length);
|
|
1532
|
+
const firstChunkMaxMsg = CHUNK_SIZE - header.length;
|
|
1533
|
+
const msgForFirst = messageBytes.slice(0, firstChunkMaxMsg);
|
|
1534
|
+
const firstPayload = new Uint8Array(header.length + msgForFirst.length);
|
|
1535
|
+
firstPayload.set(header, 0);
|
|
1536
|
+
firstPayload.set(msgForFirst, header.length);
|
|
1537
|
+
const remaining = messageBytes.slice(firstChunkMaxMsg);
|
|
1538
|
+
const chunks = [];
|
|
1539
|
+
let pos = 0;
|
|
1540
|
+
while (pos < remaining.length) {
|
|
1541
|
+
chunks.push(remaining.slice(pos, pos + CHUNK_SIZE));
|
|
1542
|
+
pos += CHUNK_SIZE;
|
|
1543
|
+
}
|
|
1544
|
+
let response;
|
|
1545
|
+
const firstApdu = buildApdu(CLA, INS_SIGN_MESSAGE, 0, 0, firstPayload);
|
|
1546
|
+
response = await this._sendApdu(firstApdu);
|
|
1547
|
+
checkStatusCode(response.statusCode, "tronSignMessage (first)");
|
|
1548
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
1549
|
+
const apdu = buildApdu(CLA, INS_SIGN_MESSAGE, 128, 0, chunks[i]);
|
|
1550
|
+
response = await this._sendApdu(apdu);
|
|
1551
|
+
checkStatusCode(response.statusCode, `tronSignMessage (chunk ${i + 1})`);
|
|
1552
|
+
}
|
|
1553
|
+
return bytesToHex(response.data.slice(0, 65));
|
|
1554
|
+
}
|
|
1555
|
+
};
|
|
1556
|
+
|
|
1212
1557
|
// src/connector/LedgerConnectorBase.ts
|
|
1213
1558
|
function normalizePath(path) {
|
|
1214
1559
|
return path.startsWith("m/") ? path.slice(2) : path;
|
|
@@ -1219,6 +1564,17 @@ function stripHex2(hex) {
|
|
|
1219
1564
|
function padHex642(hex) {
|
|
1220
1565
|
return `0x${stripHex2(hex).padStart(64, "0")}`;
|
|
1221
1566
|
}
|
|
1567
|
+
function hexToBytes3(hex) {
|
|
1568
|
+
const clean = stripHex2(hex);
|
|
1569
|
+
const bytes = new Uint8Array(clean.length / 2);
|
|
1570
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
1571
|
+
bytes[i] = parseInt(clean.slice(i * 2, i * 2 + 2), 16);
|
|
1572
|
+
}
|
|
1573
|
+
return bytes;
|
|
1574
|
+
}
|
|
1575
|
+
function bytesToHex2(bytes) {
|
|
1576
|
+
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1577
|
+
}
|
|
1222
1578
|
var LedgerConnectorBase = class {
|
|
1223
1579
|
constructor(createTransport, options) {
|
|
1224
1580
|
this._deviceManager = null;
|
|
@@ -1379,6 +1735,12 @@ var LedgerConnectorBase = class {
|
|
|
1379
1735
|
return this._solSignTransaction(sessionId, params);
|
|
1380
1736
|
case "solSignMessage":
|
|
1381
1737
|
return this._solSignMessage(sessionId, params);
|
|
1738
|
+
case "tronGetAddress":
|
|
1739
|
+
return this._tronGetAddress(sessionId, params);
|
|
1740
|
+
case "tronSignTransaction":
|
|
1741
|
+
return this._tronSignTransaction(sessionId, params);
|
|
1742
|
+
case "tronSignMessage":
|
|
1743
|
+
return this._tronSignMessage(sessionId, params);
|
|
1382
1744
|
default:
|
|
1383
1745
|
throw new Error(`LedgerConnector: unknown method "${method}"`);
|
|
1384
1746
|
}
|
|
@@ -1513,11 +1875,38 @@ var LedgerConnectorBase = class {
|
|
|
1513
1875
|
throw this._wrapError(err);
|
|
1514
1876
|
}
|
|
1515
1877
|
}
|
|
1516
|
-
async _btcSignTransaction(
|
|
1517
|
-
|
|
1878
|
+
async _btcSignTransaction(sessionId, params) {
|
|
1879
|
+
const btcSigner = await this._createBtcSigner(sessionId);
|
|
1880
|
+
try {
|
|
1881
|
+
const { DefaultWallet, DefaultDescriptorTemplate } = await import("@ledgerhq/device-signer-kit-bitcoin");
|
|
1882
|
+
const path = normalizePath(params.path || "84'/0'/0'");
|
|
1883
|
+
const purpose = path.split("/")[0]?.replace("'", "");
|
|
1884
|
+
let template = DefaultDescriptorTemplate.NATIVE_SEGWIT;
|
|
1885
|
+
if (purpose === "44") template = DefaultDescriptorTemplate.LEGACY;
|
|
1886
|
+
else if (purpose === "49")
|
|
1887
|
+
template = DefaultDescriptorTemplate.NESTED_SEGWIT;
|
|
1888
|
+
else if (purpose === "86") template = DefaultDescriptorTemplate.TAPROOT;
|
|
1889
|
+
const wallet = new DefaultWallet(path, template);
|
|
1890
|
+
const signedTxHex = await btcSigner.signTransaction(wallet, params.psbt);
|
|
1891
|
+
return { signedPsbt: stripHex2(signedTxHex) };
|
|
1892
|
+
} catch (err) {
|
|
1893
|
+
this._invalidateSession(sessionId);
|
|
1894
|
+
throw this._wrapError(err);
|
|
1895
|
+
}
|
|
1518
1896
|
}
|
|
1519
|
-
async _btcSignMessage(
|
|
1520
|
-
|
|
1897
|
+
async _btcSignMessage(sessionId, params) {
|
|
1898
|
+
const btcSigner = await this._createBtcSigner(sessionId);
|
|
1899
|
+
const path = normalizePath(params.path);
|
|
1900
|
+
try {
|
|
1901
|
+
const result = await btcSigner.signMessage(path, params.message);
|
|
1902
|
+
const rHex = stripHex2(result.r).padStart(64, "0");
|
|
1903
|
+
const sHex = stripHex2(result.s).padStart(64, "0");
|
|
1904
|
+
const vHex = result.v.toString(16).padStart(2, "0");
|
|
1905
|
+
return { signature: `${rHex}${sHex}${vHex}`, address: "" };
|
|
1906
|
+
} catch (err) {
|
|
1907
|
+
this._invalidateSession(sessionId);
|
|
1908
|
+
throw this._wrapError(err);
|
|
1909
|
+
}
|
|
1521
1910
|
}
|
|
1522
1911
|
async _btcGetMasterFingerprint(sessionId, params) {
|
|
1523
1912
|
const btcSigner = await this._createBtcSigner(sessionId);
|
|
@@ -1548,11 +1937,67 @@ var LedgerConnectorBase = class {
|
|
|
1548
1937
|
throw this._wrapError(err);
|
|
1549
1938
|
}
|
|
1550
1939
|
}
|
|
1551
|
-
async _solSignTransaction(
|
|
1552
|
-
|
|
1940
|
+
async _solSignTransaction(sessionId, params) {
|
|
1941
|
+
const solSigner = await this._createSolSigner(sessionId);
|
|
1942
|
+
const path = normalizePath(params.path);
|
|
1943
|
+
const txBytes = hexToBytes3(params.serializedTx);
|
|
1944
|
+
try {
|
|
1945
|
+
const result = await solSigner.signTransaction(path, txBytes);
|
|
1946
|
+
return { signature: bytesToHex2(result) };
|
|
1947
|
+
} catch (err) {
|
|
1948
|
+
this._invalidateSession(sessionId);
|
|
1949
|
+
throw this._wrapError(err);
|
|
1950
|
+
}
|
|
1553
1951
|
}
|
|
1554
|
-
async _solSignMessage(
|
|
1555
|
-
|
|
1952
|
+
async _solSignMessage(sessionId, params) {
|
|
1953
|
+
const solSigner = await this._createSolSigner(sessionId);
|
|
1954
|
+
const path = normalizePath(params.path);
|
|
1955
|
+
const messageBytes = hexToBytes3(params.message);
|
|
1956
|
+
try {
|
|
1957
|
+
const result = await solSigner.signMessage(path, messageBytes);
|
|
1958
|
+
return { signature: bytesToHex2(result) };
|
|
1959
|
+
} catch (err) {
|
|
1960
|
+
this._invalidateSession(sessionId);
|
|
1961
|
+
throw this._wrapError(err);
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
// ---------------------------------------------------------------------------
|
|
1965
|
+
// Private -- TRON methods
|
|
1966
|
+
// ---------------------------------------------------------------------------
|
|
1967
|
+
async _tronGetAddress(sessionId, params) {
|
|
1968
|
+
const tronSigner = await this._createTronSigner(sessionId);
|
|
1969
|
+
const path = normalizePath(params.path);
|
|
1970
|
+
try {
|
|
1971
|
+
const result = await tronSigner.getAddress(path, {
|
|
1972
|
+
checkOnDevice: params.showOnDevice ?? false
|
|
1973
|
+
});
|
|
1974
|
+
return { address: result.address, publicKey: result.publicKey, path: params.path };
|
|
1975
|
+
} catch (err) {
|
|
1976
|
+
this._invalidateSession(sessionId);
|
|
1977
|
+
throw this._wrapError(err);
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
async _tronSignTransaction(sessionId, params) {
|
|
1981
|
+
const tronSigner = await this._createTronSigner(sessionId);
|
|
1982
|
+
const path = normalizePath(params.path);
|
|
1983
|
+
try {
|
|
1984
|
+
const signature = await tronSigner.signTransaction(path, params.rawTxHex);
|
|
1985
|
+
return { signature };
|
|
1986
|
+
} catch (err) {
|
|
1987
|
+
this._invalidateSession(sessionId);
|
|
1988
|
+
throw this._wrapError(err);
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
async _tronSignMessage(sessionId, params) {
|
|
1992
|
+
const tronSigner = await this._createTronSigner(sessionId);
|
|
1993
|
+
const path = normalizePath(params.path);
|
|
1994
|
+
try {
|
|
1995
|
+
const signature = await tronSigner.signMessage(path, params.messageHex);
|
|
1996
|
+
return { signature };
|
|
1997
|
+
} catch (err) {
|
|
1998
|
+
this._invalidateSession(sessionId);
|
|
1999
|
+
throw this._wrapError(err);
|
|
2000
|
+
}
|
|
1556
2001
|
}
|
|
1557
2002
|
// ---------------------------------------------------------------------------
|
|
1558
2003
|
// Private -- DMK / Manager lifecycle
|
|
@@ -1605,17 +2050,32 @@ var LedgerConnectorBase = class {
|
|
|
1605
2050
|
dmk,
|
|
1606
2051
|
sessionId
|
|
1607
2052
|
}).build();
|
|
1608
|
-
|
|
2053
|
+
const signer = new SignerBtc(sdkSigner);
|
|
2054
|
+
signer.onInteraction = (interaction) => {
|
|
2055
|
+
this._emit("ui-event", {
|
|
2056
|
+
type: interaction,
|
|
2057
|
+
payload: { sessionId }
|
|
2058
|
+
});
|
|
2059
|
+
};
|
|
2060
|
+
return signer;
|
|
1609
2061
|
}
|
|
1610
2062
|
async _createSolSigner(sessionId) {
|
|
1611
2063
|
const dmk = await this._getOrCreateDmk();
|
|
1612
|
-
const { SignerSolanaBuilder } = await import("@ledgerhq/device-signer-kit-solana
|
|
2064
|
+
const { SignerSolanaBuilder } = await import("@ledgerhq/device-signer-kit-solana");
|
|
1613
2065
|
const sdkSigner = new SignerSolanaBuilder({
|
|
1614
2066
|
dmk,
|
|
1615
2067
|
sessionId
|
|
1616
2068
|
}).build();
|
|
1617
2069
|
return new SignerSol(sdkSigner);
|
|
1618
2070
|
}
|
|
2071
|
+
async _createTronSigner(sessionId) {
|
|
2072
|
+
const dmk = await this._getOrCreateDmk();
|
|
2073
|
+
const sendApdu = async (rawApdu) => {
|
|
2074
|
+
const response = await dmk.sendApdu({ sessionId, apdu: rawApdu });
|
|
2075
|
+
return response;
|
|
2076
|
+
};
|
|
2077
|
+
return new SignerTron(sendApdu);
|
|
2078
|
+
}
|
|
1619
2079
|
_invalidateSession(sessionId) {
|
|
1620
2080
|
this._signerManager?.invalidate(sessionId);
|
|
1621
2081
|
}
|
|
@@ -1752,6 +2212,7 @@ export {
|
|
|
1752
2212
|
SignerEth,
|
|
1753
2213
|
SignerManager,
|
|
1754
2214
|
SignerSol,
|
|
2215
|
+
SignerTron,
|
|
1755
2216
|
clearRegistry,
|
|
1756
2217
|
deviceActionToPromise,
|
|
1757
2218
|
getTransportProvider,
|