@swapkit/wallet-hardware 6.0.0 → 6.1.0
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/chunk-3h1f3hyp.js +4 -0
- package/dist/chunk-3h1f3hyp.js.map +10 -0
- package/dist/chunk-8m2sn76p.js +4 -0
- package/dist/chunk-8m2sn76p.js.map +10 -0
- package/dist/chunk-qjq7gvyx.js +4 -0
- package/dist/chunk-qjq7gvyx.js.map +10 -0
- package/dist/chunk-wfbg89d3.js +4 -0
- package/dist/chunk-wfbg89d3.js.map +10 -0
- package/dist/chunk-xn0fnag5.js +4 -0
- package/dist/chunk-xn0fnag5.js.map +10 -0
- package/dist/trezor/index.cjs +2 -2
- package/dist/trezor/index.cjs.map +9 -4
- package/dist/trezor/index.js +2 -2
- package/dist/trezor/index.js.map +3 -3
- package/dist/types/keepkey/index.d.ts +1 -1
- package/dist/types/ledger/index.d.ts +1 -1
- package/dist/types/trezor/cardanoSigner.d.ts +21 -0
- package/dist/types/trezor/cardanoSigner.d.ts.map +1 -0
- package/dist/types/trezor/index.d.ts +3 -3
- package/dist/types/trezor/index.d.ts.map +1 -1
- package/dist/types/trezor/rippleSigner.d.ts +23 -0
- package/dist/types/trezor/rippleSigner.d.ts.map +1 -0
- package/dist/types/trezor/solanaSigner.d.ts +17 -0
- package/dist/types/trezor/solanaSigner.d.ts.map +1 -0
- package/dist/types/trezor/stellarSigner.d.ts +99 -0
- package/dist/types/trezor/stellarSigner.d.ts.map +1 -0
- package/dist/types/trezor/tronSigner.d.ts +30 -0
- package/dist/types/trezor/tronSigner.d.ts.map +1 -0
- package/package.json +7 -3
- package/src/trezor/cardanoSigner.ts +226 -0
- package/src/trezor/index.ts +136 -43
- package/src/trezor/rippleSigner.ts +100 -0
- package/src/trezor/solanaSigner.ts +129 -0
- package/src/trezor/stellarSigner.ts +186 -0
- package/src/trezor/tronSigner.ts +148 -0
package/src/trezor/index.ts
CHANGED
|
@@ -199,11 +199,55 @@ function tryNormalizeTrezorExtendedPublicKey(xpub: string | undefined, chain: UT
|
|
|
199
199
|
}
|
|
200
200
|
}
|
|
201
201
|
|
|
202
|
-
function decodeOpReturnData(script: Uint8Array)
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
202
|
+
async function decodeOpReturnData(script: Uint8Array) {
|
|
203
|
+
const { Script } = await import("@swapkit/utxo-signer");
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
const [opcode, data] = Script.decode(script);
|
|
207
|
+
const bytes = data === 0 ? new Uint8Array() : data;
|
|
208
|
+
if (opcode !== "RETURN" || !(bytes instanceof Uint8Array)) return null;
|
|
209
|
+
|
|
210
|
+
// Trezor rebuilds the script from the data, so only accept scripts it reproduces byte for byte
|
|
211
|
+
return hex.encode(Script.encode(["RETURN", bytes])) === hex.encode(script) ? hex.encode(bytes) : null;
|
|
212
|
+
} catch {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function toTrezorOpReturnOutput(script: Uint8Array, amount: bigint | undefined, chain: Chain) {
|
|
218
|
+
if (amount !== 0n) {
|
|
219
|
+
throw new SwapKitError({
|
|
220
|
+
errorKey: "wallet_trezor_failed_to_sign_transaction",
|
|
221
|
+
info: { chain, error: "OP_RETURN output carrying value cannot be signed" },
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const opReturnData = await decodeOpReturnData(script);
|
|
226
|
+
|
|
227
|
+
if (opReturnData === null) {
|
|
228
|
+
throw new SwapKitError({
|
|
229
|
+
errorKey: "wallet_trezor_failed_to_sign_transaction",
|
|
230
|
+
info: { chain, error: "Malformed OP_RETURN output cannot be signed" },
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return { amount: "0", op_return_data: opReturnData, script_type: "PAYTOOPRETURN" as const };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function assertTransactionCarriesMemo(tx: Transaction, memo: string, chain: Chain) {
|
|
238
|
+
if (!memo) return;
|
|
239
|
+
|
|
240
|
+
const expected = hex.encode(new TextEncoder().encode(memo));
|
|
241
|
+
|
|
242
|
+
for (let i = 0; i < tx.outputsLength; i++) {
|
|
243
|
+
const script = tx.getOutput(i).script;
|
|
244
|
+
if (script?.[0] === 0x6a && (await decodeOpReturnData(script)) === expected) return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
throw new SwapKitError({
|
|
248
|
+
errorKey: "wallet_trezor_failed_to_sign_transaction",
|
|
249
|
+
info: { chain, error: "Transaction does not carry the provided memo" },
|
|
250
|
+
});
|
|
207
251
|
}
|
|
208
252
|
|
|
209
253
|
function getScriptType(chain: Chain, derivationPath: DerivationPathArray) {
|
|
@@ -327,16 +371,9 @@ async function buildPCZTOutputsForTrezor(pczt: PCZT, address_n: number[], myAddr
|
|
|
327
371
|
const output = pczt.getOutput(i);
|
|
328
372
|
const script = output.scriptPubkey;
|
|
329
373
|
|
|
330
|
-
if (
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
outputs.push({ amount: "0", op_return_data: opReturnData, script_type: "PAYTOOPRETURN" as const });
|
|
334
|
-
continue;
|
|
335
|
-
}
|
|
336
|
-
throw new SwapKitError({
|
|
337
|
-
errorKey: "wallet_trezor_failed_to_sign_transaction",
|
|
338
|
-
info: { chain, error: "Malformed OP_RETURN output cannot be signed" },
|
|
339
|
-
});
|
|
374
|
+
if (script?.[0] === 0x6a) {
|
|
375
|
+
outputs.push(await toTrezorOpReturnOutput(script, output.value, chain));
|
|
376
|
+
continue;
|
|
340
377
|
}
|
|
341
378
|
|
|
342
379
|
const outputAddress = await decodeOutputAddress(script);
|
|
@@ -411,7 +448,7 @@ function buildZcashTxInputsForTrezor(
|
|
|
411
448
|
return inputs;
|
|
412
449
|
}
|
|
413
450
|
|
|
414
|
-
function buildZcashTxOutputsForTrezor({
|
|
451
|
+
async function buildZcashTxOutputsForTrezor({
|
|
415
452
|
addressN,
|
|
416
453
|
chain,
|
|
417
454
|
myAddress,
|
|
@@ -430,12 +467,8 @@ function buildZcashTxOutputsForTrezor({
|
|
|
430
467
|
const outputAddress = tx.getOutputAddress(i, network);
|
|
431
468
|
const script = output.script;
|
|
432
469
|
|
|
433
|
-
if (
|
|
434
|
-
|
|
435
|
-
if (opReturnData) {
|
|
436
|
-
outputs.push({ amount: "0", op_return_data: opReturnData, script_type: "PAYTOOPRETURN" as const });
|
|
437
|
-
continue;
|
|
438
|
-
}
|
|
470
|
+
if (script?.[0] === 0x6a) {
|
|
471
|
+
outputs.push(await toTrezorOpReturnOutput(script, output.amount, chain));
|
|
439
472
|
continue;
|
|
440
473
|
}
|
|
441
474
|
|
|
@@ -453,12 +486,11 @@ function buildZcashTxOutputsForTrezor({
|
|
|
453
486
|
return outputs;
|
|
454
487
|
}
|
|
455
488
|
|
|
456
|
-
function buildUtxoOutputsForTrezor(
|
|
489
|
+
async function buildUtxoOutputsForTrezor(
|
|
457
490
|
tx: Transaction,
|
|
458
491
|
network: BTCNetwork,
|
|
459
492
|
address_n: number[],
|
|
460
493
|
myAddress: string,
|
|
461
|
-
memo: string,
|
|
462
494
|
chain: Chain,
|
|
463
495
|
scriptType: { input: string; output: string },
|
|
464
496
|
toCashAddress: (addr: string) => string,
|
|
@@ -469,17 +501,12 @@ function buildUtxoOutputsForTrezor(
|
|
|
469
501
|
const output = tx.getOutput(i);
|
|
470
502
|
const outputAddress = tx.getOutputAddress(i, network);
|
|
471
503
|
|
|
472
|
-
if (
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
amount: "0",
|
|
477
|
-
op_return_data: opReturnData ?? Buffer.from(memo).toString("hex"),
|
|
478
|
-
script_type: "PAYTOOPRETURN",
|
|
479
|
-
});
|
|
480
|
-
continue;
|
|
481
|
-
}
|
|
504
|
+
if (output.script?.[0] === 0x6a) {
|
|
505
|
+
outputs.push(await toTrezorOpReturnOutput(output.script, output.amount, chain));
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
482
508
|
|
|
509
|
+
if (!outputAddress) {
|
|
483
510
|
throw new SwapKitError({
|
|
484
511
|
errorKey: "wallet_trezor_failed_to_sign_transaction",
|
|
485
512
|
info: { chain, error: "Unable to decode output address from scriptPubkey" },
|
|
@@ -571,6 +598,62 @@ async function getTrezorWallet<T extends Chain>({
|
|
|
571
598
|
return { ...toolbox, address };
|
|
572
599
|
}
|
|
573
600
|
|
|
601
|
+
case Chain.Cardano: {
|
|
602
|
+
const { getCardanoToolbox } = await import("@swapkit/toolboxes/cardano");
|
|
603
|
+
const { getCardanoSigner } = await import("./cardanoSigner");
|
|
604
|
+
|
|
605
|
+
const connect = await import("@trezor/connect-web");
|
|
606
|
+
const signer = await getCardanoSigner({ connect, derivationPath });
|
|
607
|
+
const address = signer.getAddress();
|
|
608
|
+
const toolbox = getCardanoToolbox({ signer });
|
|
609
|
+
|
|
610
|
+
return { ...toolbox, address };
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
case Chain.Ripple: {
|
|
614
|
+
const { getRippleToolbox } = await import("@swapkit/toolboxes/ripple");
|
|
615
|
+
const { getRippleSigner } = await import("./rippleSigner");
|
|
616
|
+
|
|
617
|
+
const signer = getRippleSigner({ derivationPath });
|
|
618
|
+
const address = providedAddress ?? (await signer.getAddress());
|
|
619
|
+
const toolbox = getRippleToolbox({ signer });
|
|
620
|
+
|
|
621
|
+
return { ...toolbox, address };
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
case Chain.Solana: {
|
|
625
|
+
const { getSolanaToolbox } = await import("@swapkit/toolboxes/solana");
|
|
626
|
+
const { getSolanaSigner } = await import("./solanaSigner");
|
|
627
|
+
|
|
628
|
+
const signer = getSolanaSigner({ address: providedAddress, derivationPath });
|
|
629
|
+
const address = providedAddress ?? (await signer.getAddress());
|
|
630
|
+
const toolbox = getSolanaToolbox({ signer });
|
|
631
|
+
|
|
632
|
+
return { ...toolbox, address };
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
case Chain.Stellar: {
|
|
636
|
+
const { getStellarToolbox } = await import("@swapkit/toolboxes/stellar");
|
|
637
|
+
const { getStellarSigner } = await import("./stellarSigner");
|
|
638
|
+
|
|
639
|
+
const signer = getStellarSigner({ derivationPath });
|
|
640
|
+
const address = providedAddress ?? (await signer.getAddress());
|
|
641
|
+
const toolbox = getStellarToolbox({ signer });
|
|
642
|
+
|
|
643
|
+
return { ...toolbox, address };
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
case Chain.Tron: {
|
|
647
|
+
const { getTronToolbox } = await import("@swapkit/toolboxes/tron");
|
|
648
|
+
const { getTronSigner } = await import("./tronSigner");
|
|
649
|
+
|
|
650
|
+
const signer = getTronSigner({ address: providedAddress, derivationPath });
|
|
651
|
+
const address = await signer.getAddress();
|
|
652
|
+
const toolbox = getTronToolbox({ signer });
|
|
653
|
+
|
|
654
|
+
return { ...toolbox, address };
|
|
655
|
+
}
|
|
656
|
+
|
|
574
657
|
case Chain.Zcash: {
|
|
575
658
|
const { getUtxoToolbox } = await import("@swapkit/toolboxes/utxo");
|
|
576
659
|
|
|
@@ -637,7 +720,7 @@ async function getTrezorWallet<T extends Chain>({
|
|
|
637
720
|
const address_n = hardenDerivationPath(derivationPath);
|
|
638
721
|
|
|
639
722
|
const inputs = buildZcashTxInputsForTrezor(tx, [], address_n, hexEncode);
|
|
640
|
-
const outputs = buildZcashTxOutputsForTrezor({
|
|
723
|
+
const outputs = await buildZcashTxOutputsForTrezor({
|
|
641
724
|
addressN: address_n,
|
|
642
725
|
chain,
|
|
643
726
|
myAddress: address,
|
|
@@ -779,16 +862,17 @@ async function getTrezorWallet<T extends Chain>({
|
|
|
779
862
|
const baseToolbox = getUtxoToolbox(chain, toolboxParams);
|
|
780
863
|
|
|
781
864
|
const signTransaction = async (tx: Transaction, inputs: UTXOType[], memo = "") => {
|
|
865
|
+
await assertTransactionCarriesMemo(tx, memo, chain);
|
|
866
|
+
|
|
782
867
|
const TrezorConnect = (await import("@trezor/connect-web")).default;
|
|
783
868
|
const address_n = hardenDerivationPath(derivationPath);
|
|
784
869
|
const network = getNetworkForChain(chain as UTXOChain);
|
|
785
870
|
|
|
786
|
-
const outputs = buildUtxoOutputsForTrezor(
|
|
871
|
+
const outputs = await buildUtxoOutputsForTrezor(
|
|
787
872
|
tx,
|
|
788
873
|
network,
|
|
789
874
|
address_n,
|
|
790
875
|
address,
|
|
791
|
-
memo,
|
|
792
876
|
chain,
|
|
793
877
|
resolvedScriptType,
|
|
794
878
|
toCashAddress,
|
|
@@ -902,12 +986,11 @@ async function getTrezorWallet<T extends Chain>({
|
|
|
902
986
|
});
|
|
903
987
|
}
|
|
904
988
|
|
|
905
|
-
const outputs = buildUtxoOutputsForTrezor(
|
|
989
|
+
const outputs = await buildUtxoOutputsForTrezor(
|
|
906
990
|
tx,
|
|
907
991
|
network,
|
|
908
992
|
address_n,
|
|
909
993
|
address,
|
|
910
|
-
"",
|
|
911
994
|
chain,
|
|
912
995
|
resolvedScriptType,
|
|
913
996
|
toCashAddress,
|
|
@@ -977,12 +1060,11 @@ async function getTrezorWallet<T extends Chain>({
|
|
|
977
1060
|
});
|
|
978
1061
|
}
|
|
979
1062
|
|
|
980
|
-
const outputs = buildUtxoOutputsForTrezor(
|
|
1063
|
+
const outputs = await buildUtxoOutputsForTrezor(
|
|
981
1064
|
tx,
|
|
982
1065
|
network,
|
|
983
1066
|
address_n,
|
|
984
1067
|
address,
|
|
985
|
-
"",
|
|
986
1068
|
chain,
|
|
987
1069
|
resolvedScriptType,
|
|
988
1070
|
toCashAddress,
|
|
@@ -1020,16 +1102,17 @@ async function getTrezorWallet<T extends Chain>({
|
|
|
1020
1102
|
}>,
|
|
1021
1103
|
memo = "",
|
|
1022
1104
|
) => {
|
|
1105
|
+
await assertTransactionCarriesMemo(tx, memo, chain);
|
|
1106
|
+
|
|
1023
1107
|
const TrezorConnect = (await import("@trezor/connect-web")).default;
|
|
1024
1108
|
const network = getNetworkForChain(chain as UTXOChain);
|
|
1025
1109
|
const baseAddressN = hardenDerivationPath(derivationPath.slice(0, 3) as unknown as DerivationPathArray);
|
|
1026
1110
|
|
|
1027
|
-
const outputs = buildUtxoOutputsForTrezor(
|
|
1111
|
+
const outputs = await buildUtxoOutputsForTrezor(
|
|
1028
1112
|
tx,
|
|
1029
1113
|
network,
|
|
1030
1114
|
baseAddressN,
|
|
1031
1115
|
address,
|
|
1032
|
-
memo,
|
|
1033
1116
|
chain,
|
|
1034
1117
|
resolvedScriptType,
|
|
1035
1118
|
toCashAddress,
|
|
@@ -1458,6 +1541,7 @@ export const trezorWallet = createWallet({
|
|
|
1458
1541
|
[Chain.BinanceSmartChain]: true,
|
|
1459
1542
|
[Chain.Bitcoin]: true,
|
|
1460
1543
|
[Chain.BitcoinCash]: true,
|
|
1544
|
+
[Chain.Cardano]: true,
|
|
1461
1545
|
[Chain.Dash]: true,
|
|
1462
1546
|
[Chain.Ethereum]: true,
|
|
1463
1547
|
[Chain.Gnosis]: true,
|
|
@@ -1467,7 +1551,11 @@ export const trezorWallet = createWallet({
|
|
|
1467
1551
|
[Chain.Monad]: true,
|
|
1468
1552
|
[Chain.Optimism]: true,
|
|
1469
1553
|
[Chain.Polygon]: true,
|
|
1554
|
+
[Chain.Ripple]: true,
|
|
1470
1555
|
[Chain.Robinhood]: true,
|
|
1556
|
+
[Chain.Solana]: true,
|
|
1557
|
+
[Chain.Stellar]: true,
|
|
1558
|
+
[Chain.Tron]: true,
|
|
1471
1559
|
[Chain.XLayer]: true,
|
|
1472
1560
|
[Chain.Zcash]: true,
|
|
1473
1561
|
},
|
|
@@ -1483,6 +1571,7 @@ export const trezorWallet = createWallet({
|
|
|
1483
1571
|
Chain.BinanceSmartChain,
|
|
1484
1572
|
Chain.Bitcoin,
|
|
1485
1573
|
Chain.BitcoinCash,
|
|
1574
|
+
Chain.Cardano,
|
|
1486
1575
|
Chain.Dash,
|
|
1487
1576
|
Chain.Dogecoin,
|
|
1488
1577
|
Chain.Ethereum,
|
|
@@ -1492,7 +1581,11 @@ export const trezorWallet = createWallet({
|
|
|
1492
1581
|
Chain.Monad,
|
|
1493
1582
|
Chain.Optimism,
|
|
1494
1583
|
Chain.Polygon,
|
|
1584
|
+
Chain.Ripple,
|
|
1495
1585
|
Chain.Robinhood,
|
|
1586
|
+
Chain.Solana,
|
|
1587
|
+
Chain.Stellar,
|
|
1588
|
+
Chain.Tron,
|
|
1496
1589
|
Chain.XLayer,
|
|
1497
1590
|
Chain.Zcash,
|
|
1498
1591
|
],
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { Chain, type DerivationPathArray, derivationPathToString, SwapKitError } from "@swapkit/helpers";
|
|
2
|
+
import type { RippleTransaction } from "@swapkit/toolboxes/ripple";
|
|
3
|
+
import { GlobalFlags, hashes, isValidClassicAddress } from "xrpl";
|
|
4
|
+
|
|
5
|
+
// Fields Trezor serializes itself; anything else would be dropped from the signed transaction
|
|
6
|
+
const TREZOR_RIPPLE_FIELDS = new Set([
|
|
7
|
+
"Account",
|
|
8
|
+
"Amount",
|
|
9
|
+
"Destination",
|
|
10
|
+
"DestinationTag",
|
|
11
|
+
"Fee",
|
|
12
|
+
"Flags",
|
|
13
|
+
"LastLedgerSequence",
|
|
14
|
+
"Sequence",
|
|
15
|
+
"SigningPubKey",
|
|
16
|
+
"TransactionType",
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
function notSupported(reason: string) {
|
|
20
|
+
return new SwapKitError({ errorKey: "wallet_trezor_method_not_supported", info: { chain: Chain.Ripple, reason } });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function toTrezorRippleTransaction(transaction: RippleTransaction) {
|
|
24
|
+
const tx = transaction as unknown as Record<string, unknown>;
|
|
25
|
+
|
|
26
|
+
if (tx.TransactionType !== "Payment") {
|
|
27
|
+
throw notSupported(`Trezor only signs XRP payments, got ${String(tx.TransactionType)}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const unsupportedFields = Object.keys(tx).filter(
|
|
31
|
+
(field) => tx[field] !== undefined && tx[field] !== null && !TREZOR_RIPPLE_FIELDS.has(field),
|
|
32
|
+
);
|
|
33
|
+
if (unsupportedFields.length > 0) {
|
|
34
|
+
throw notSupported(`Trezor cannot sign XRP payments with ${unsupportedFields.join(", ")}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const { Amount, Destination, DestinationTag, Fee, Flags, LastLedgerSequence, Sequence } = tx;
|
|
38
|
+
|
|
39
|
+
if (typeof Amount !== "string" || !/^\d+$/.test(Amount)) throw notSupported("Trezor only signs native XRP amounts");
|
|
40
|
+
if (typeof Destination !== "string") throw notSupported("Missing destination");
|
|
41
|
+
if (!isValidClassicAddress(Destination)) throw notSupported("Destination must be a classic XRP address");
|
|
42
|
+
if (typeof Fee !== "string" || typeof Sequence !== "number") throw notSupported("Transaction is not autofilled");
|
|
43
|
+
if (Flags !== undefined && typeof Flags !== "number") throw notSupported("Flags must be numeric");
|
|
44
|
+
if (((Flags ?? 0) & GlobalFlags.tfInnerBatchTxn) !== 0) {
|
|
45
|
+
throw notSupported("Trezor cannot sign Batch inner XRP transactions");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
fee: Fee,
|
|
50
|
+
flags: Flags as number | undefined,
|
|
51
|
+
maxLedgerVersion: LastLedgerSequence as number | undefined,
|
|
52
|
+
payment: { amount: Amount, destination: Destination, destinationTag: DestinationTag as number | undefined },
|
|
53
|
+
sequence: Sequence,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function getRippleSigner({ derivationPath }: { derivationPath: DerivationPathArray }) {
|
|
58
|
+
const path = derivationPathToString(derivationPath);
|
|
59
|
+
let address = "";
|
|
60
|
+
|
|
61
|
+
async function getAddress() {
|
|
62
|
+
if (address) return address;
|
|
63
|
+
|
|
64
|
+
const TrezorConnect = (await import("@trezor/connect-web")).default;
|
|
65
|
+
const result = await TrezorConnect.rippleGetAddress({ path, showOnTrezor: true });
|
|
66
|
+
|
|
67
|
+
if (!result.success) {
|
|
68
|
+
throw new SwapKitError({
|
|
69
|
+
errorKey: "wallet_trezor_failed_to_get_address",
|
|
70
|
+
info: { chain: Chain.Ripple, derivationPath, error: result.payload.error },
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
address = result.payload.address;
|
|
75
|
+
return address;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function signTransaction(transaction: RippleTransaction) {
|
|
79
|
+
const trezorTransaction = toTrezorRippleTransaction(transaction);
|
|
80
|
+
if (transaction.Account !== (await getAddress())) {
|
|
81
|
+
throw notSupported("Transaction account does not match the Trezor address");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const TrezorConnect = (await import("@trezor/connect-web")).default;
|
|
85
|
+
|
|
86
|
+
const result = await TrezorConnect.rippleSignTransaction({ path, transaction: trezorTransaction });
|
|
87
|
+
|
|
88
|
+
if (!result.success) {
|
|
89
|
+
throw new SwapKitError({
|
|
90
|
+
errorKey: "wallet_trezor_failed_to_sign_transaction",
|
|
91
|
+
info: { chain: Chain.Ripple, error: result.payload.error },
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const tx_blob = result.payload.serializedTx.toUpperCase();
|
|
96
|
+
return { hash: hashes.hashSignedTx(tx_blob), tx_blob };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return { getAddress, signTransaction };
|
|
100
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { ed25519 } from "@noble/curves/ed25519.js";
|
|
2
|
+
import { hex } from "@scure/base";
|
|
3
|
+
import { PublicKey, type Transaction, type VersionedTransaction } from "@solana/web3.js";
|
|
4
|
+
import { Chain, type DerivationPathArray, derivationPathToString, SwapKitError } from "@swapkit/helpers";
|
|
5
|
+
|
|
6
|
+
type SolanaTransaction = Transaction | VersionedTransaction;
|
|
7
|
+
|
|
8
|
+
function notSupported(reason: string) {
|
|
9
|
+
return new SwapKitError({ errorKey: "wallet_trezor_method_not_supported", info: { chain: Chain.Solana, reason } });
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function getSigners(transaction: SolanaTransaction) {
|
|
13
|
+
if ("version" in transaction) {
|
|
14
|
+
const { header, staticAccountKeys } = transaction.message;
|
|
15
|
+
return {
|
|
16
|
+
message: transaction.message.serialize(),
|
|
17
|
+
signerKeys: staticAccountKeys.slice(0, header.numRequiredSignatures),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
message: transaction.serializeMessage(),
|
|
23
|
+
signerKeys: transaction.signatures.map(({ publicKey }) => publicKey),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getSolanaSigningPayload(transaction: SolanaTransaction, address: string) {
|
|
28
|
+
const { message, signerKeys } = getSigners(transaction);
|
|
29
|
+
const signerKey = signerKeys.find((key) => key.toBase58() === address);
|
|
30
|
+
|
|
31
|
+
if (!signerKey) throw notSupported("Trezor account is not a required signer of this transaction");
|
|
32
|
+
|
|
33
|
+
return { message, signerKey };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function addSolanaSignature<T extends SolanaTransaction>(
|
|
37
|
+
transaction: T,
|
|
38
|
+
{ message, signature, signerKey }: { message: Uint8Array; signature: Uint8Array; signerKey: PublicKey },
|
|
39
|
+
) {
|
|
40
|
+
if (signature.length !== 64) {
|
|
41
|
+
throw new SwapKitError({
|
|
42
|
+
errorKey: "wallet_trezor_failed_to_sign_transaction",
|
|
43
|
+
info: { chain: Chain.Solana, error: `Invalid signature length: ${signature.length}` },
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!ed25519.verify(signature, message, signerKey.toBytes())) {
|
|
48
|
+
throw new SwapKitError({
|
|
49
|
+
errorKey: "wallet_trezor_failed_to_sign_transaction",
|
|
50
|
+
info: { chain: Chain.Solana, error: "Signature does not match the Trezor account" },
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
transaction.addSignature(signerKey, Buffer.from(signature));
|
|
55
|
+
return transaction;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function getSolanaSigner({
|
|
59
|
+
address,
|
|
60
|
+
derivationPath,
|
|
61
|
+
}: {
|
|
62
|
+
address?: string;
|
|
63
|
+
derivationPath: DerivationPathArray;
|
|
64
|
+
}) {
|
|
65
|
+
// Trezor firmware only accepts m/44'/501' plus up to two more levels.
|
|
66
|
+
const [purpose, coinType] = derivationPath;
|
|
67
|
+
if (purpose !== 44 || coinType !== 501 || derivationPath.length > 4) {
|
|
68
|
+
throw new SwapKitError({
|
|
69
|
+
errorKey: "wallet_trezor_derivation_path_not_supported",
|
|
70
|
+
info: { chain: Chain.Solana, derivationPath },
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const path = derivationPathToString(derivationPath, { allHardened: true });
|
|
75
|
+
const knownKey = address ? new PublicKey(address) : null;
|
|
76
|
+
let publicKey: PublicKey | null = null;
|
|
77
|
+
|
|
78
|
+
async function loadAddress(showOnTrezor: boolean) {
|
|
79
|
+
if (publicKey) return publicKey.toBase58();
|
|
80
|
+
|
|
81
|
+
const TrezorConnect = (await import("@trezor/connect-web")).default;
|
|
82
|
+
const result = await TrezorConnect.solanaGetAddress({ path, showOnTrezor });
|
|
83
|
+
|
|
84
|
+
if (!result.success) {
|
|
85
|
+
throw new SwapKitError({
|
|
86
|
+
errorKey: "wallet_trezor_failed_to_get_address",
|
|
87
|
+
info: { chain: Chain.Solana, derivationPath, error: result.payload.error },
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const deviceKey = new PublicKey(result.payload.address);
|
|
92
|
+
if (knownKey && !deviceKey.equals(knownKey)) {
|
|
93
|
+
throw new SwapKitError({
|
|
94
|
+
errorKey: "wallet_trezor_failed_to_get_address",
|
|
95
|
+
info: { chain: Chain.Solana, derivationPath, error: "Trezor account does not match the restored address" },
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
publicKey = deviceKey;
|
|
100
|
+
return result.payload.address;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function signTransaction<T extends SolanaTransaction>(transaction: T) {
|
|
104
|
+
const { message, signerKey } = getSolanaSigningPayload(transaction, await loadAddress(false));
|
|
105
|
+
const TrezorConnect = (await import("@trezor/connect-web")).default;
|
|
106
|
+
|
|
107
|
+
const result = await TrezorConnect.solanaSignTransaction({ path, serializedTx: hex.encode(message) });
|
|
108
|
+
|
|
109
|
+
if (!result.success) {
|
|
110
|
+
throw new SwapKitError({
|
|
111
|
+
errorKey: "wallet_trezor_failed_to_sign_transaction",
|
|
112
|
+
info: { chain: Chain.Solana, error: result.payload.error },
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const signature = hex.decode(result.payload.signature.replace(/^0x/, ""));
|
|
117
|
+
return addSolanaSignature(transaction, { message, signature, signerKey });
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
connect: async () => ({ publicKey: new PublicKey(await loadAddress(true)) }),
|
|
122
|
+
disconnect: () => Promise.resolve(),
|
|
123
|
+
getAddress: () => loadAddress(true),
|
|
124
|
+
get publicKey() {
|
|
125
|
+
return publicKey ?? knownKey;
|
|
126
|
+
},
|
|
127
|
+
signTransaction,
|
|
128
|
+
};
|
|
129
|
+
}
|