@swapkit/wallet-hardware 6.1.0 → 6.2.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.
Files changed (101) hide show
  1. package/dist/chunk-035dayv6.js +4 -0
  2. package/dist/chunk-035dayv6.js.map +10 -0
  3. package/dist/{chunk-yrxe491r.js → chunk-2jddegc1.js} +3 -3
  4. package/dist/{chunk-yrxe491r.js.map → chunk-2jddegc1.js.map} +1 -1
  5. package/dist/{chunk-gr3eswf1.js → chunk-42xrtarj.js} +3 -3
  6. package/dist/{chunk-gr3eswf1.js.map → chunk-42xrtarj.js.map} +1 -1
  7. package/dist/{chunk-3t17wpjg.js → chunk-545s1tsc.js} +3 -3
  8. package/dist/{chunk-3t17wpjg.js.map → chunk-545s1tsc.js.map} +1 -1
  9. package/dist/{chunk-1amf58aq.js → chunk-5xg0xc0g.js} +3 -3
  10. package/dist/{chunk-1amf58aq.js.map → chunk-5xg0xc0g.js.map} +1 -1
  11. package/dist/{chunk-xn0fnag5.js → chunk-71kfnwdp.js} +3 -3
  12. package/dist/{chunk-xn0fnag5.js.map → chunk-71kfnwdp.js.map} +1 -1
  13. package/dist/{chunk-xmvg4cds.js → chunk-7kwtpk70.js} +3 -3
  14. package/dist/{chunk-xmvg4cds.js.map → chunk-7kwtpk70.js.map} +1 -1
  15. package/dist/{chunk-0arpsm5f.js → chunk-7rbvcts9.js} +3 -3
  16. package/dist/{chunk-0arpsm5f.js.map → chunk-7rbvcts9.js.map} +1 -1
  17. package/dist/{chunk-k3thje6a.js → chunk-a47dwwv6.js} +3 -3
  18. package/dist/{chunk-k3thje6a.js.map → chunk-a47dwwv6.js.map} +1 -1
  19. package/dist/{chunk-5c7b7z2d.js → chunk-a6vcbcyd.js} +3 -3
  20. package/dist/{chunk-5c7b7z2d.js.map → chunk-a6vcbcyd.js.map} +1 -1
  21. package/dist/{chunk-wfbg89d3.js → chunk-b093ajcq.js} +3 -3
  22. package/dist/{chunk-wfbg89d3.js.map → chunk-b093ajcq.js.map} +1 -1
  23. package/dist/{chunk-ds2m83jp.js → chunk-b7htg03t.js} +3 -3
  24. package/dist/{chunk-ds2m83jp.js.map → chunk-b7htg03t.js.map} +1 -1
  25. package/dist/chunk-g6rrzxcx.js +5 -0
  26. package/dist/chunk-g6rrzxcx.js.map +10 -0
  27. package/dist/{chunk-jewm2ag3.js → chunk-j3n6fbqh.js} +4 -4
  28. package/dist/{chunk-jewm2ag3.js.map → chunk-j3n6fbqh.js.map} +1 -1
  29. package/dist/{chunk-nhva847d.js → chunk-kaw35ae2.js} +3 -3
  30. package/dist/{chunk-nhva847d.js.map → chunk-kaw35ae2.js.map} +1 -1
  31. package/dist/{chunk-dptbnkzm.js → chunk-mzan0m09.js} +4 -4
  32. package/dist/{chunk-dptbnkzm.js.map → chunk-mzan0m09.js.map} +1 -1
  33. package/dist/{chunk-jzrytcrf.js → chunk-rydhx4se.js} +3 -3
  34. package/dist/{chunk-jzrytcrf.js.map → chunk-rydhx4se.js.map} +1 -1
  35. package/dist/{chunk-09e51wma.js → chunk-s2akss77.js} +3 -3
  36. package/dist/{chunk-09e51wma.js.map → chunk-s2akss77.js.map} +1 -1
  37. package/dist/{chunk-tm47brht.js → chunk-s5a5v0m1.js} +3 -3
  38. package/dist/{chunk-tm47brht.js.map → chunk-s5a5v0m1.js.map} +1 -1
  39. package/dist/{chunk-8m2sn76p.js → chunk-sbebfr50.js} +3 -3
  40. package/dist/{chunk-8m2sn76p.js.map → chunk-sbebfr50.js.map} +1 -1
  41. package/dist/{chunk-xx7cwpzq.js → chunk-sp3dq5hr.js} +2 -2
  42. package/dist/{chunk-hg71hp4m.js → chunk-tecse0bj.js} +3 -3
  43. package/dist/{chunk-hg71hp4m.js.map → chunk-tecse0bj.js.map} +1 -1
  44. package/dist/chunk-tm5gk51g.js +4 -0
  45. package/dist/{chunk-eenx3cv6.js → chunk-x28a37pz.js} +3 -3
  46. package/dist/{chunk-eenx3cv6.js.map → chunk-x28a37pz.js.map} +1 -1
  47. package/dist/{chunk-3h1f3hyp.js → chunk-ys51wj1c.js} +3 -3
  48. package/dist/{chunk-3h1f3hyp.js.map → chunk-ys51wj1c.js.map} +1 -1
  49. package/dist/keepkey/index.cjs +2 -2
  50. package/dist/keepkey/index.cjs.map +3 -3
  51. package/dist/keepkey/index.js +2 -2
  52. package/dist/keepkey/index.js.map +3 -3
  53. package/dist/ledger/index.cjs +30 -30
  54. package/dist/ledger/index.cjs.map +11 -7
  55. package/dist/ledger/index.js +2 -2
  56. package/dist/ledger/index.js.map +10 -7
  57. package/dist/trezor/index.cjs +2 -2
  58. package/dist/trezor/index.cjs.map +6 -5
  59. package/dist/trezor/index.js +2 -2
  60. package/dist/trezor/index.js.map +3 -3
  61. package/dist/types/helpers/cardano.d.ts +35 -0
  62. package/dist/types/helpers/cardano.d.ts.map +1 -0
  63. package/dist/types/keepkey/index.d.ts +1 -0
  64. package/dist/types/keepkey/index.d.ts.map +1 -1
  65. package/dist/types/ledger/clients/cardano.d.ts +24 -0
  66. package/dist/types/ledger/clients/cardano.d.ts.map +1 -0
  67. package/dist/types/ledger/clients/evm.d.ts +1 -0
  68. package/dist/types/ledger/clients/evm.d.ts.map +1 -1
  69. package/dist/types/ledger/clients/stellar.d.ts +13 -0
  70. package/dist/types/ledger/clients/stellar.d.ts.map +1 -0
  71. package/dist/types/ledger/clients/ton.d.ts +13 -0
  72. package/dist/types/ledger/clients/ton.d.ts.map +1 -0
  73. package/dist/types/ledger/helpers/getLedgerAddress.d.ts.map +1 -1
  74. package/dist/types/ledger/helpers/getLedgerClient.d.ts +8 -1
  75. package/dist/types/ledger/helpers/getLedgerClient.d.ts.map +1 -1
  76. package/dist/types/ledger/index.d.ts +3 -2
  77. package/dist/types/ledger/index.d.ts.map +1 -1
  78. package/dist/types/ledger/types.d.ts +2 -2
  79. package/dist/types/ledger/types.d.ts.map +1 -1
  80. package/dist/types/trezor/cardanoSigner.d.ts.map +1 -1
  81. package/dist/types/trezor/index.d.ts +1 -0
  82. package/dist/types/trezor/index.d.ts.map +1 -1
  83. package/dist/types/trezor/stellarSigner.d.ts +1 -1
  84. package/package.json +17 -5
  85. package/src/helpers/cardano.ts +112 -0
  86. package/src/keepkey/index.ts +14 -14
  87. package/src/ledger/clients/cardano.ts +204 -0
  88. package/src/ledger/clients/evm.ts +4 -0
  89. package/src/ledger/clients/stellar.ts +83 -0
  90. package/src/ledger/clients/ton.ts +282 -0
  91. package/src/ledger/helpers/getLedgerAddress.ts +16 -0
  92. package/src/ledger/helpers/getLedgerClient.ts +13 -0
  93. package/src/ledger/index.ts +100 -28
  94. package/src/ledger/types.ts +2 -0
  95. package/src/trezor/cardanoSigner.ts +24 -80
  96. package/src/trezor/index.ts +86 -64
  97. package/dist/chunk-5p3ttw6y.js +0 -4
  98. package/dist/chunk-qjq7gvyx.js +0 -4
  99. package/dist/chunk-qjq7gvyx.js.map +0 -10
  100. /package/dist/{chunk-xx7cwpzq.js.map → chunk-sp3dq5hr.js.map} +0 -0
  101. /package/dist/{chunk-5p3ttw6y.js.map → chunk-tm5gk51g.js.map} +0 -0
@@ -0,0 +1,204 @@
1
+ import type {
2
+ Ada,
3
+ AssetGroup,
4
+ BIP32Path,
5
+ ExtendedPublicKey,
6
+ SignedTransactionData,
7
+ SignTransactionRequest,
8
+ TxOutput,
9
+ TxOutputDestination,
10
+ } from "@cardano-foundation/ledgerjs-hw-app-cardano";
11
+ import type { UserInteractionRequired } from "@ledgerhq/device-management-kit";
12
+ import type Transport from "@ledgerhq/hw-transport";
13
+ import { hex } from "@scure/base";
14
+ import { Chain, type DerivationPathArray, NetworkDerivationPath, SwapKitError } from "@swapkit/helpers";
15
+
16
+ import { type CardanoOutputFields, readCardanoBody } from "../../helpers/cardano";
17
+ import {
18
+ LEDGER_USER_INTERACTION_REQUIRED,
19
+ type LedgerJsClientParams,
20
+ normalizeLedgerJsClientParams,
21
+ runLedgerJsOperation,
22
+ } from "../helpers/ledgerJsDmkBridge";
23
+
24
+ export type CardanoLedgerModule = typeof import("@cardano-foundation/ledgerjs-hw-app-cardano");
25
+ export type CardanoLedgerAddressParams = { address: string; paymentPath: BIP32Path; stakingPath: BIP32Path };
26
+
27
+ type CardanoLedgerParams = LedgerJsClientParams<DerivationPathArray>;
28
+
29
+ function notSupported(reason: string) {
30
+ return new SwapKitError("wallet_ledger_method_not_supported", { chain: Chain.Cardano, reason });
31
+ }
32
+
33
+ async function toLedgerOutput(
34
+ { address, amount, format, tokenGroups }: CardanoOutputFields,
35
+ own: CardanoLedgerAddressParams,
36
+ { AddressType, TxOutputDestinationType, TxOutputFormat }: CardanoLedgerModule,
37
+ ): Promise<TxOutput> {
38
+ const { getCardanoAddressFromBytes } = await import("@swapkit/toolboxes/cardano");
39
+ const outputAddress = await getCardanoAddressFromBytes(address);
40
+ const destination: TxOutputDestination =
41
+ outputAddress === own.address
42
+ ? {
43
+ params: {
44
+ params: { spendingPath: own.paymentPath, stakingPath: own.stakingPath },
45
+ type: AddressType.BASE_PAYMENT_KEY_STAKE_KEY,
46
+ },
47
+ type: TxOutputDestinationType.DEVICE_OWNED,
48
+ }
49
+ : { params: { addressHex: hex.encode(address) }, type: TxOutputDestinationType.THIRD_PARTY };
50
+ const tokenBundle: AssetGroup[] | undefined = tokenGroups?.map(({ policyId, tokens }) => ({
51
+ policyIdHex: hex.encode(policyId),
52
+ tokens: tokens.map(({ amount, assetName }) => ({ amount, assetNameHex: hex.encode(assetName) })),
53
+ }));
54
+
55
+ return {
56
+ amount,
57
+ destination,
58
+ format: format === "legacy" ? TxOutputFormat.ARRAY_LEGACY : TxOutputFormat.MAP_BABBAGE,
59
+ ...(tokenBundle === undefined ? {} : { tokenBundle }),
60
+ } as TxOutput;
61
+ }
62
+
63
+ export async function toLedgerCardanoTransaction(
64
+ txHex: string,
65
+ own: CardanoLedgerAddressParams,
66
+ ledgerModule: CardanoLedgerModule,
67
+ ): Promise<SignTransactionRequest> {
68
+ const { decodeCardanoTransaction } = await import("@swapkit/toolboxes/cardano");
69
+ const { Networks, TransactionSigningMode, TxAuxiliaryDataType } = ledgerModule;
70
+ const { body } = await decodeCardanoTransaction(txHex);
71
+ const { auxiliaryDataHash, fee, inputs, outputs, tagCborSets, ttl, validityIntervalStart } = readCardanoBody(
72
+ body,
73
+ "Ledger",
74
+ notSupported,
75
+ );
76
+
77
+ return {
78
+ additionalWitnessPaths: [],
79
+ options: { tagCborSets },
80
+ signingMode: TransactionSigningMode.ORDINARY_TRANSACTION,
81
+ tx: {
82
+ fee,
83
+ inputs: inputs.map(({ index, txHash }) => ({
84
+ outputIndex: index,
85
+ path: own.paymentPath,
86
+ txHashHex: hex.encode(txHash),
87
+ })),
88
+ network: Networks.Mainnet,
89
+ outputs: await Promise.all(outputs.map((output) => toLedgerOutput(output, own, ledgerModule))),
90
+ ...(auxiliaryDataHash
91
+ ? {
92
+ auxiliaryData: {
93
+ params: { hashHex: hex.encode(auxiliaryDataHash) },
94
+ type: TxAuxiliaryDataType.ARBITRARY_HASH,
95
+ },
96
+ }
97
+ : {}),
98
+ ...(ttl === undefined ? {} : { ttl }),
99
+ ...(validityIntervalStart === undefined ? {} : { validityIntervalStart }),
100
+ },
101
+ };
102
+ }
103
+
104
+ export async function addLedgerWitnesses(
105
+ txHex: string,
106
+ { txHashHex, witnesses }: Pick<SignedTransactionData, "txHashHex" | "witnesses">,
107
+ { paymentPath, publicKey }: { paymentPath: BIP32Path; publicKey: Uint8Array },
108
+ ) {
109
+ const { addCardanoVkeyWitnesses, decodeCardanoTransaction } = await import("@swapkit/toolboxes/cardano");
110
+ const { bodyHash } = await decodeCardanoTransaction(txHex);
111
+
112
+ if (txHashHex.toLowerCase() !== bodyHash) {
113
+ throw new SwapKitError("wallet_ledger_invalid_signature", {
114
+ chain: Chain.Cardano,
115
+ error: "Ledger signed a different transaction body",
116
+ });
117
+ }
118
+ if (witnesses.length === 0) {
119
+ throw new SwapKitError("wallet_ledger_signing_error", {
120
+ chain: Chain.Cardano,
121
+ error: "Ledger returned no witness",
122
+ });
123
+ }
124
+
125
+ const vkeyWitnesses = witnesses.map(({ path, witnessSignatureHex }) => {
126
+ if (path.length !== paymentPath.length || path.some((level, i) => level !== paymentPath[i])) {
127
+ throw new SwapKitError("wallet_ledger_signing_error", {
128
+ chain: Chain.Cardano,
129
+ error: "Ledger returned a witness for an unexpected key",
130
+ });
131
+ }
132
+ return { publicKey, signature: hex.decode(witnessSignatureHex) };
133
+ });
134
+
135
+ return addCardanoVkeyWitnesses(txHex, vkeyWitnesses);
136
+ }
137
+
138
+ export async function CardanoLedger(paramsOrPath?: CardanoLedgerParams | DerivationPathArray, transport?: Transport) {
139
+ const { derivationPath, ...connection } = normalizeLedgerJsClientParams({ paramsOrPath, transport });
140
+ const path = derivationPath || NetworkDerivationPath[Chain.Cardano];
141
+ const [purpose, coinType, account, role, index] = path;
142
+ if (path.length !== 5 || role !== 0 || index === undefined) {
143
+ throw new SwapKitError("wallet_ledger_derivation_path_error", { chain: Chain.Cardano, derivationPath: path });
144
+ }
145
+
146
+ const ledgerModule = await import("@cardano-foundation/ledgerjs-hw-app-cardano");
147
+ const { getCardanoAddressFromBytes, getCardanoPublicKeyHash } = await import("@swapkit/toolboxes/cardano");
148
+ const { HARDENED, Networks } = ledgerModule;
149
+ const accountPath = [purpose + HARDENED, coinType + HARDENED, account + HARDENED];
150
+ const paymentPath = [...accountPath, 0, index];
151
+ const stakingPath = [...accountPath, 2, 0];
152
+
153
+ function runCardanoOperation<Output>({
154
+ operation,
155
+ requiredUserInteraction,
156
+ }: {
157
+ operation: (app: Ada) => Promise<Output>;
158
+ requiredUserInteraction?: UserInteractionRequired;
159
+ }) {
160
+ return runLedgerJsOperation({
161
+ appName: "Cardano ADA",
162
+ connection,
163
+ // Named export: the package is CommonJS only, so `default` is `module.exports` under Node ESM.
164
+ createApp: (ledgerTransport) => new ledgerModule.Ada(ledgerTransport),
165
+ operation,
166
+ requiredUserInteraction,
167
+ });
168
+ }
169
+
170
+ function toPublicKey(key: ExtendedPublicKey | undefined) {
171
+ const publicKey = Buffer.from(key?.publicKeyHex ?? "", "hex");
172
+ if (publicKey.length !== 32) {
173
+ throw new SwapKitError("wallet_ledger_failed_to_get_address", { chain: Chain.Cardano, derivationPath: path });
174
+ }
175
+ return publicKey;
176
+ }
177
+
178
+ const keys = await runCardanoOperation({
179
+ operation: (app) => app.getExtendedPublicKeys({ paths: [paymentPath, stakingPath] }),
180
+ });
181
+ const paymentKey = toPublicKey(keys[0]);
182
+ const stakingKey = toPublicKey(keys[1]);
183
+
184
+ const publicKeyHash = await getCardanoPublicKeyHash(paymentKey);
185
+ const address = await getCardanoAddressFromBytes(
186
+ Buffer.concat([
187
+ Buffer.from([Networks.Mainnet.networkId]),
188
+ publicKeyHash,
189
+ await getCardanoPublicKeyHash(stakingKey),
190
+ ]),
191
+ );
192
+
193
+ async function signTransaction(txHex: string) {
194
+ const request = await toLedgerCardanoTransaction(txHex, { address, paymentPath, stakingPath }, ledgerModule);
195
+ const result = await runCardanoOperation({
196
+ operation: (app) => app.signTransaction(request),
197
+ requiredUserInteraction: LEDGER_USER_INTERACTION_REQUIRED.SignTransaction,
198
+ });
199
+
200
+ return addLedgerWitnesses(txHex, result, { paymentPath, publicKey: paymentKey });
201
+ }
202
+
203
+ return { getAddress: () => address, publicKey: paymentKey, publicKeyHash, signTransaction };
204
+ }
@@ -316,6 +316,10 @@ export function PolygonLedger(params: LedgerParams) {
316
316
  return new EVMLedgerInterface({ ...params, chainId: ChainId.Polygon });
317
317
  }
318
318
 
319
+ export function RobinhoodLedger(params: LedgerParams) {
320
+ return new EVMLedgerInterface({ ...params, chainId: ChainId.Robinhood });
321
+ }
322
+
319
323
  export function BinanceSmartChainLedger(params: LedgerParams) {
320
324
  return new EVMLedgerInterface({ ...params, chainId: ChainId.BinanceSmartChain });
321
325
  }
@@ -0,0 +1,83 @@
1
+ import type { UserInteractionRequired } from "@ledgerhq/device-management-kit";
2
+ import type Str from "@ledgerhq/hw-app-str";
3
+ import type Transport from "@ledgerhq/hw-transport";
4
+ import {
5
+ Chain,
6
+ type DerivationPathArray,
7
+ derivationPathToString,
8
+ NetworkDerivationPath,
9
+ SwapKitError,
10
+ } from "@swapkit/helpers";
11
+ import type { StellarSigner, StellarTransaction } from "@swapkit/toolboxes/stellar";
12
+
13
+ import {
14
+ LEDGER_USER_INTERACTION_REQUIRED,
15
+ type LedgerJsClientParams,
16
+ normalizeLedgerJsClientParams,
17
+ runLedgerJsOperation,
18
+ } from "../helpers/ledgerJsDmkBridge";
19
+
20
+ type StellarLedgerParams = LedgerJsClientParams<DerivationPathArray>;
21
+
22
+ // The Stellar app derives accounts from m/44'/148'/x' only, like Trezor and the SwapKit keystore.
23
+ export function getStellarLedgerPath(derivationPath: DerivationPathArray) {
24
+ const [purpose, coinType, account, change, index] = derivationPath;
25
+ if (purpose !== 44 || coinType !== 148 || account === undefined || change || index) {
26
+ throw new SwapKitError("wallet_ledger_derivation_path_error", {
27
+ chain: Chain.Stellar,
28
+ derivationPath,
29
+ reason: "Stellar only uses m/44'/148'/x'",
30
+ });
31
+ }
32
+
33
+ // hw-app-str hardens every segment, so a leading "m" would become "m'" and fail to parse.
34
+ return derivationPathToString([purpose, coinType, account]).replace(/^m\//, "");
35
+ }
36
+
37
+ export async function StellarLedger(paramsOrPath?: StellarLedgerParams | DerivationPathArray, transport?: Transport) {
38
+ const { derivationPath, ...connection } = normalizeLedgerJsClientParams({ paramsOrPath, transport });
39
+ const path = getStellarLedgerPath(derivationPath || NetworkDerivationPath[Chain.Stellar]);
40
+
41
+ async function runStellarOperation<Output>({
42
+ operation,
43
+ requiredUserInteraction,
44
+ }: {
45
+ operation: (app: Str) => Promise<Output>;
46
+ requiredUserInteraction?: UserInteractionRequired;
47
+ }) {
48
+ const StrApp = (await import("@ledgerhq/hw-app-str")).default;
49
+ return runLedgerJsOperation({
50
+ appName: "Stellar",
51
+ connection,
52
+ createApp: (ledgerTransport) => new StrApp(ledgerTransport),
53
+ operation,
54
+ requiredUserInteraction,
55
+ });
56
+ }
57
+
58
+ const [{ rawPublicKey }, { StrKey }] = await Promise.all([
59
+ runStellarOperation({ operation: (app) => app.getPublicKey(path) }),
60
+ import("@stellar/stellar-sdk"),
61
+ ]);
62
+ const address = StrKey.encodeEd25519PublicKey(rawPublicKey);
63
+
64
+ // The device parses the signature base itself, so it displays and signs the transaction exactly as received.
65
+ async function signTransaction(transaction: StellarTransaction) {
66
+ const { signature } = await runStellarOperation({
67
+ operation: (app) => app.signTransaction(path, transaction.signatureBase()),
68
+ requiredUserInteraction: LEDGER_USER_INTERACTION_REQUIRED.SignTransaction,
69
+ });
70
+
71
+ try {
72
+ transaction.addSignature(address, signature.toString("base64"));
73
+ } catch (error) {
74
+ throw new SwapKitError("wallet_ledger_invalid_signature", { chain: Chain.Stellar, error });
75
+ }
76
+
77
+ return transaction;
78
+ }
79
+
80
+ return { getAddress: () => address, publicKey: rawPublicKey, signTransaction } satisfies StellarSigner & {
81
+ publicKey: Buffer;
82
+ };
83
+ }
@@ -0,0 +1,282 @@
1
+ import type { UserInteractionRequired } from "@ledgerhq/device-management-kit";
2
+ import type Transport from "@ledgerhq/hw-transport";
3
+ import {
4
+ Chain,
5
+ type DerivationPathArray,
6
+ getRPCUrl,
7
+ NetworkDerivationPath,
8
+ SwapKitError,
9
+ WalletOption,
10
+ } from "@swapkit/helpers";
11
+ import type { TONTransactionInput, TONTransactionMessage } from "@swapkit/toolboxes/ton";
12
+ import type { Address, Cell, MessageRelaxed, StateInit } from "@ton/core";
13
+ import type { TonClient } from "@ton/ton";
14
+ import type { TonPayloadFormat, TonTransport } from "@ton-community/ton-ledger";
15
+
16
+ import {
17
+ LEDGER_USER_INTERACTION_REQUIRED,
18
+ type LedgerJsClientParams,
19
+ normalizeLedgerJsClientParams,
20
+ runLedgerJsOperation,
21
+ } from "../helpers/ledgerJsDmkBridge";
22
+
23
+ // SendMode.PAY_GAS_SEPARATELY + SendMode.IGNORE_ERRORS, the TON toolbox default.
24
+ const DEFAULT_SEND_MODE = 3;
25
+ const TRANSACTION_VALID_SECONDS = 300;
26
+ const TON_WORKCHAIN = 0;
27
+
28
+ type TONLedgerParams = LedgerJsClientParams<DerivationPathArray>;
29
+
30
+ interface TONLedgerOrder {
31
+ amount: bigint;
32
+ bounce: boolean;
33
+ payload?: Cell;
34
+ payloadFormat?: TonPayloadFormat;
35
+ stateInit?: StateInit;
36
+ to: Address;
37
+ }
38
+
39
+ interface SignedTransferExpectation extends TONLedgerOrder {
40
+ sendMode: number;
41
+ seqno: number;
42
+ timeout: number;
43
+ walletId: number;
44
+ }
45
+
46
+ function toLedgerPath(derivationPath: DerivationPathArray) {
47
+ const [purpose, coinType, network, workchain, account, ...rest] = derivationPath;
48
+ const isTonPath =
49
+ purpose === 44 &&
50
+ coinType === 607 &&
51
+ network === 0 &&
52
+ workchain === 0 &&
53
+ typeof account === "number" &&
54
+ Number.isInteger(account) &&
55
+ account >= 0 &&
56
+ rest.length === 0;
57
+
58
+ if (!isTonPath) {
59
+ throw new SwapKitError({
60
+ errorKey: "wallet_ledger_derivation_path_error",
61
+ info: { derivationPath, reason: "Ledger TON accounts derive from m/44'/607'/0'/0'/{account}'" },
62
+ });
63
+ }
64
+
65
+ return [44, 607, 0, 0, account, 0];
66
+ }
67
+
68
+ function invalidParams(reason: string, info: Record<string, unknown> = {}) {
69
+ return new SwapKitError({
70
+ errorKey: "wallet_ledger_invalid_params",
71
+ info: { ...info, reason, wallet: WalletOption.LEDGER },
72
+ });
73
+ }
74
+
75
+ function resolveSingleMessage(input: TONTransactionInput) {
76
+ const messages = Array.isArray(input) ? input : input.messages;
77
+ const [message] = messages;
78
+
79
+ if (!message) throw invalidParams("A TON transaction needs one message");
80
+ if (messages.length > 1) {
81
+ throw new SwapKitError({
82
+ errorKey: "wallet_ledger_method_not_supported",
83
+ info: {
84
+ messageCount: messages.length,
85
+ method: "signTransaction",
86
+ reason: "Ledger signs one TON message per transaction",
87
+ wallet: WalletOption.LEDGER,
88
+ },
89
+ });
90
+ }
91
+
92
+ const sendMode =
93
+ (Array.isArray(input) ? message.sendMode : (input.sendMode ?? message.sendMode)) ?? DEFAULT_SEND_MODE;
94
+ if (!Array.isArray(input) && message.sendMode !== undefined && message.sendMode !== sendMode) {
95
+ throw invalidParams("The message send mode differs from the transaction send mode", {
96
+ messageSendMode: message.sendMode,
97
+ sendMode,
98
+ });
99
+ }
100
+
101
+ if (!Number.isInteger(sendMode) || sendMode < 0 || sendMode > 255) {
102
+ throw invalidParams("The TON send mode must be an integer between 0 and 255", { sendMode });
103
+ }
104
+
105
+ return { message, sendMode };
106
+ }
107
+
108
+ async function toLedgerOrder(message: TONTransactionMessage): Promise<TONLedgerOrder> {
109
+ const [{ Address, Cell, loadStateInit }, { parseMessage }] = await Promise.all([
110
+ import("@ton/core"),
111
+ import("@ton-community/ton-ledger"),
112
+ ]);
113
+
114
+ if (!/^\d+$/.test(message.amount)) {
115
+ throw invalidParams("The TON amount must be an integer number of nanotons", { amount: message.amount });
116
+ }
117
+ const amount = BigInt(message.amount);
118
+
119
+ let to: Address;
120
+ try {
121
+ to = Address.parse(message.address);
122
+ } catch {
123
+ throw invalidParams("Invalid TON destination address", { address: message.address });
124
+ }
125
+ // Raw `0:<hex>` destinations default to bounceable, the toolbox convention; friendly ones carry their own flag.
126
+ const bounce = Address.isFriendly(message.address) ? Address.parseFriendly(message.address).isBounceable : true;
127
+
128
+ let stateInit: StateInit | undefined;
129
+ if (message.stateInit !== undefined) {
130
+ try {
131
+ stateInit = loadStateInit(Cell.fromBase64(message.stateInit).beginParse());
132
+ } catch {
133
+ throw invalidParams("The TON state init is not a valid BOC", { stateInit: message.stateInit });
134
+ }
135
+ }
136
+
137
+ let payload: Cell | undefined;
138
+ if (message.payload !== undefined) {
139
+ try {
140
+ payload = Cell.fromBase64(message.payload);
141
+ } catch {
142
+ throw invalidParams("The TON payload is not a valid BOC", { payload: message.payload });
143
+ }
144
+ }
145
+
146
+ // A payload the app cannot display is kept as the untouched `unsafe` cell, which the device signs blind or refuses.
147
+ const payloadFormat = payload ? parseMessage(payload, { disallowModification: true }) : undefined;
148
+
149
+ return { amount, bounce, payload, payloadFormat, stateInit, to };
150
+ }
151
+
152
+ function sameCell(a: Cell | null | undefined, b: Cell | null | undefined) {
153
+ return a && b ? a.hash().equals(b.hash()) : a == null && b == null;
154
+ }
155
+
156
+ function sameStateInit(signed: StateInit | null | undefined, requested: StateInit | undefined) {
157
+ if (!(signed && requested)) return signed == null && requested == null;
158
+ return sameCell(signed.code, requested.code) && sameCell(signed.data, requested.data);
159
+ }
160
+
161
+ // The library re-encodes structured payloads before the device signs them, so the body it returns is only
162
+ // broadcast when the signed order is the requested one.
163
+ async function assertSignedTransfer(body: Cell, expected: SignedTransferExpectation) {
164
+ const { Cell, loadMessageRelaxed } = await import("@ton/core");
165
+
166
+ let signed: { walletId: number; timeout: number; seqno: number; sendMode: number; order: MessageRelaxed };
167
+ try {
168
+ const transfer = body.beginParse().skip(512);
169
+ const walletId = transfer.loadUint(32);
170
+ const timeout = transfer.loadUint(32);
171
+ const seqno = transfer.loadUint(32);
172
+ transfer.skip(8);
173
+ const sendMode = transfer.loadUint(8);
174
+ const order = loadMessageRelaxed(transfer.loadRef().beginParse());
175
+ transfer.endParse();
176
+ signed = { order, sendMode, seqno, timeout, walletId };
177
+ } catch (error) {
178
+ throw new SwapKitError(
179
+ { errorKey: "wallet_ledger_signing_error", info: { reason: "Unreadable signed TON transfer" } },
180
+ error,
181
+ );
182
+ }
183
+
184
+ const { order } = signed;
185
+ const matches =
186
+ signed.walletId === expected.walletId &&
187
+ signed.timeout === expected.timeout &&
188
+ signed.seqno === expected.seqno &&
189
+ signed.sendMode === expected.sendMode &&
190
+ order.info.type === "internal" &&
191
+ order.info.dest.equals(expected.to) &&
192
+ order.info.value.coins === expected.amount &&
193
+ order.info.bounce === expected.bounce &&
194
+ order.body.hash().equals((expected.payload ?? Cell.EMPTY).hash()) &&
195
+ sameStateInit(order.init, expected.stateInit);
196
+ if (!matches) {
197
+ throw new SwapKitError({
198
+ errorKey: "wallet_ledger_signing_error",
199
+ info: { reason: "The signed TON transfer does not match the requested message" },
200
+ });
201
+ }
202
+ }
203
+
204
+ export async function TONLedger(paramsOrPath?: TONLedgerParams | DerivationPathArray, transport?: Transport) {
205
+ const { derivationPath, ...connection } = normalizeLedgerJsClientParams({ paramsOrPath, transport });
206
+ const path = toLedgerPath(derivationPath || NetworkDerivationPath[Chain.Ton]);
207
+
208
+ async function runTonOperation<Output>({
209
+ operation,
210
+ requiredUserInteraction,
211
+ }: {
212
+ operation: (app: TonTransport) => Promise<Output>;
213
+ requiredUserInteraction?: UserInteractionRequired;
214
+ }) {
215
+ const { TonTransport } = await import("@ton-community/ton-ledger");
216
+ return runLedgerJsOperation({
217
+ appName: "TON",
218
+ connection,
219
+ createApp: (ledgerTransport) => new TonTransport(ledgerTransport),
220
+ operation,
221
+ requiredUserInteraction,
222
+ });
223
+ }
224
+
225
+ const [{ Address }, { WalletContractV4 }] = await Promise.all([import("@ton/core"), import("@ton/ton")]);
226
+ const { address: deviceAddress, publicKey } = await runTonOperation({
227
+ operation: (app) =>
228
+ app.getAddress(path, { bounceable: false, chain: TON_WORKCHAIN, testOnly: false, walletVersion: "v4" }),
229
+ });
230
+
231
+ const wallet = WalletContractV4.create({ publicKey, workchain: TON_WORKCHAIN });
232
+ if (!wallet.address.equals(Address.parse(deviceAddress))) {
233
+ throw new SwapKitError({
234
+ errorKey: "wallet_ledger_failed_to_get_address",
235
+ info: { deviceAddress, reason: "The Ledger TON address is not the wallet v4 address of its public key" },
236
+ });
237
+ }
238
+ const address = wallet.address.toString({ bounceable: false, urlSafe: true });
239
+
240
+ let client: TonClient | undefined;
241
+ async function getWalletState() {
242
+ if (!client) {
243
+ const { TonClient } = await import("@ton/ton");
244
+ client = new TonClient({ endpoint: await getRPCUrl(Chain.Ton) });
245
+ }
246
+ const { state } = await client.getContractState(wallet.address);
247
+ const deployed = state === "active";
248
+ const seqno = deployed ? await client.open(wallet).getSeqno() : 0;
249
+
250
+ return { deployed, seqno };
251
+ }
252
+
253
+ async function signTransaction(input: TONTransactionInput): Promise<Cell> {
254
+ const { message, sendMode } = resolveSingleMessage(input);
255
+ const order = await toLedgerOrder(message);
256
+ const { deployed, seqno } = await getWalletState();
257
+ const timeout = Math.floor(Date.now() / 1000) + TRANSACTION_VALID_SECONDS;
258
+
259
+ const body = await runTonOperation({
260
+ operation: (app) =>
261
+ app.signTransaction(path, {
262
+ amount: order.amount,
263
+ bounce: order.bounce,
264
+ payload: order.payloadFormat,
265
+ sendMode,
266
+ seqno,
267
+ stateInit: order.stateInit,
268
+ timeout,
269
+ to: order.to,
270
+ }),
271
+ requiredUserInteraction: LEDGER_USER_INTERACTION_REQUIRED.SignTransaction,
272
+ });
273
+ await assertSignedTransfer(body, { ...order, sendMode, seqno, timeout, walletId: wallet.walletId });
274
+
275
+ const { beginCell, external, storeMessage } = await import("@ton/core");
276
+ return beginCell()
277
+ .store(storeMessage(external({ body, init: deployed ? undefined : wallet.init, to: wallet.address })))
278
+ .endCell();
279
+ }
280
+
281
+ return { getAddress: () => address, publicKey, signTransaction };
282
+ }
@@ -1,7 +1,10 @@
1
1
  import { Chain, SwapKitError, WalletOption } from "@swapkit/helpers";
2
2
 
3
+ import type { CardanoLedger } from "../clients/cardano";
3
4
  import type { getNearLedgerClient } from "../clients/near";
5
+ import type { StellarLedger } from "../clients/stellar";
4
6
  import type { SuiLedger } from "../clients/sui";
7
+ import type { TONLedger } from "../clients/ton";
5
8
  import type { TronLedger } from "../clients/tron";
6
9
  import type { XRPLedger } from "../clients/xrp";
7
10
  import type { LEDGER_SUPPORTED_CHAINS } from "../index";
@@ -38,6 +41,7 @@ export const getLedgerAddress = async <
38
41
  case Chain.Aurora:
39
42
  case Chain.Gnosis:
40
43
  case Chain.Monad:
44
+ case Chain.Robinhood:
41
45
  case Chain.XLayer: {
42
46
  return (ledgerClient as EVMLedgerClients).getAddress();
43
47
  }
@@ -63,6 +67,14 @@ export const getLedgerAddress = async <
63
67
  return (ledgerClient as Awaited<ReturnType<typeof XRPLedger>>).getAddress();
64
68
  }
65
69
 
70
+ case Chain.Stellar: {
71
+ return (ledgerClient as Awaited<ReturnType<typeof StellarLedger>>).getAddress();
72
+ }
73
+
74
+ case Chain.Cardano: {
75
+ return (ledgerClient as Awaited<ReturnType<typeof CardanoLedger>>).getAddress();
76
+ }
77
+
66
78
  case Chain.Tron: {
67
79
  return (ledgerClient as Awaited<ReturnType<typeof TronLedger>>).getAddress();
68
80
  }
@@ -71,6 +83,10 @@ export const getLedgerAddress = async <
71
83
  return (ledgerClient as Awaited<ReturnType<typeof SuiLedger>>).connect();
72
84
  }
73
85
 
86
+ case Chain.Ton: {
87
+ return (ledgerClient as Awaited<ReturnType<typeof TONLedger>>).getAddress();
88
+ }
89
+
74
90
  default:
75
91
  throw new SwapKitError("wallet_chain_not_supported", { chain, wallet: WalletOption.LEDGER });
76
92
  }
@@ -9,6 +9,7 @@ import {
9
9
  WalletOption,
10
10
  } from "@swapkit/helpers";
11
11
  import { BitcoinLedger } from "../clients/bitcoin";
12
+ import { CardanoLedger } from "../clients/cardano";
12
13
  import { CosmosLedger } from "../clients/cosmos";
13
14
  import {
14
15
  ArbitrumLedger,
@@ -23,11 +24,14 @@ import {
23
24
  MonadLedger,
24
25
  OptimismLedger,
25
26
  PolygonLedger,
27
+ RobinhoodLedger,
26
28
  XLayerLedger,
27
29
  } from "../clients/evm";
28
30
  import { getNearLedgerClient } from "../clients/near";
31
+ import { StellarLedger } from "../clients/stellar";
29
32
  import { SuiLedger } from "../clients/sui";
30
33
  import { THORChainLedger } from "../clients/thorchain";
34
+ import { TONLedger } from "../clients/ton";
31
35
  import { TronLedger } from "../clients/tron";
32
36
  import { BitcoinCashLedger, DashLedger, DogecoinLedger, LitecoinLedger } from "../clients/utxo";
33
37
  import { XRPLedger } from "../clients/xrp";
@@ -45,6 +49,7 @@ type LedgerSignerMap = {
45
49
  [Chain.BinanceSmartChain]: ReturnType<typeof BinanceSmartChainLedger>;
46
50
  [Chain.BitcoinCash]: ReturnType<typeof BitcoinCashLedger>;
47
51
  [Chain.Bitcoin]: ReturnType<typeof BitcoinLedger>;
52
+ [Chain.Cardano]: ReturnType<typeof CardanoLedger>;
48
53
  [Chain.Cosmos]: CosmosLedger;
49
54
  [Chain.Dash]: ReturnType<typeof DashLedger>;
50
55
  [Chain.Dogecoin]: ReturnType<typeof DogecoinLedger>;
@@ -56,8 +61,11 @@ type LedgerSignerMap = {
56
61
  [Chain.Optimism]: ReturnType<typeof OptimismLedger>;
57
62
  [Chain.Polygon]: ReturnType<typeof PolygonLedger>;
58
63
  [Chain.Ripple]: ReturnType<typeof XRPLedger>;
64
+ [Chain.Robinhood]: ReturnType<typeof RobinhoodLedger>;
65
+ [Chain.Stellar]: ReturnType<typeof StellarLedger>;
59
66
  [Chain.Sui]: ReturnType<typeof SuiLedger>;
60
67
  [Chain.THORChain]: THORChainLedger;
68
+ [Chain.Ton]: ReturnType<typeof TONLedger>;
61
69
  [Chain.Tron]: ReturnType<typeof TronLedger>;
62
70
  [Chain.XLayer]: ReturnType<typeof XLayerLedger>;
63
71
  [Chain.Zcash]: ReturnType<typeof ZcashLedger>;
@@ -114,8 +122,11 @@ export async function getLedgerClient<T extends LedgerSupportedChain>({
114
122
  .with(Chain.Litecoin, () => Promise.resolve(LitecoinLedger(clientParams) as LedgerSignerMap[T]))
115
123
  .with(Chain.Zcash, () => Promise.resolve(ZcashLedger(clientParams) as LedgerSignerMap[T]))
116
124
  .with(Chain.Ripple, () => Promise.resolve(XRPLedger(clientParams) as LedgerSignerMap[T]))
125
+ .with(Chain.Stellar, () => Promise.resolve(StellarLedger(clientParams) as LedgerSignerMap[T]))
126
+ .with(Chain.Cardano, () => Promise.resolve(CardanoLedger(clientParams) as LedgerSignerMap[T]))
117
127
  .with(Chain.Tron, () => Promise.resolve(TronLedger(clientParams) as LedgerSignerMap[T]))
118
128
  .with(Chain.Sui, () => Promise.resolve(SuiLedger(clientParams) as LedgerSignerMap[T]))
129
+ .with(Chain.Ton, () => Promise.resolve(TONLedger(clientParams) as LedgerSignerMap[T]))
119
130
  .with(Chain.Near, () => Promise.resolve(getNearLedgerClient(clientParams) as LedgerSignerMap[T]))
120
131
  .with(
121
132
  Chain.Arbitrum,
@@ -130,6 +141,7 @@ export async function getLedgerClient<T extends LedgerSupportedChain>({
130
141
  Chain.Optimism,
131
142
  Chain.Polygon,
132
143
  Chain.Base,
144
+ Chain.Robinhood,
133
145
  Chain.XLayer,
134
146
  async () => {
135
147
  const { getProvider } = await import("@swapkit/toolboxes/evm");
@@ -154,6 +166,7 @@ export async function getLedgerClient<T extends LedgerSupportedChain>({
154
166
  .with(Chain.Aurora, () => AuroraLedger(params) as LedgerSignerMap[T])
155
167
  .with(Chain.Gnosis, () => GnosisLedger(params) as LedgerSignerMap[T])
156
168
  .with(Chain.Monad, () => MonadLedger(params) as LedgerSignerMap[T])
169
+ .with(Chain.Robinhood, () => RobinhoodLedger(params) as LedgerSignerMap[T])
157
170
  .with(Chain.XLayer, () => XLayerLedger(params) as LedgerSignerMap[T])
158
171
  .otherwise(() => EthereumLedger(params) as LedgerSignerMap[T]);
159
172
  },