@swapkit/wallet-hardware 5.0.0 → 6.0.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 (167) hide show
  1. package/dist/THIRD_PARTY_LICENSES.md +2344 -0
  2. package/dist/chunk-09e51wma.js +4 -0
  3. package/dist/chunk-09e51wma.js.map +167 -0
  4. package/dist/chunk-0arpsm5f.js +4 -0
  5. package/dist/chunk-0arpsm5f.js.map +10 -0
  6. package/dist/chunk-1amf58aq.js +4 -0
  7. package/dist/{chunk-vwv6z2mk.js.map → chunk-1amf58aq.js.map} +2 -2
  8. package/dist/chunk-3t17wpjg.js +17 -0
  9. package/dist/chunk-3t17wpjg.js.map +29 -0
  10. package/dist/chunk-5c7b7z2d.js +4 -0
  11. package/dist/{chunk-9gy0xve4.js.map → chunk-5c7b7z2d.js.map} +2 -2
  12. package/dist/chunk-5p3ttw6y.js +4 -0
  13. package/dist/{chunk-n05bv2n5.js.map → chunk-5p3ttw6y.js.map} +1 -1
  14. package/dist/chunk-dptbnkzm.js +64 -0
  15. package/dist/chunk-dptbnkzm.js.map +260 -0
  16. package/dist/chunk-ds2m83jp.js +5 -0
  17. package/dist/chunk-ds2m83jp.js.map +11 -0
  18. package/dist/chunk-eenx3cv6.js +4 -0
  19. package/dist/chunk-eenx3cv6.js.map +33 -0
  20. package/dist/chunk-gr3eswf1.js +4 -0
  21. package/dist/chunk-gr3eswf1.js.map +73 -0
  22. package/dist/{chunk-7fsaymh4.js → chunk-hg71hp4m.js} +3 -3
  23. package/dist/{chunk-7fsaymh4.js.map → chunk-hg71hp4m.js.map} +1 -1
  24. package/dist/chunk-jewm2ag3.js +5 -0
  25. package/dist/chunk-jewm2ag3.js.map +10 -0
  26. package/dist/chunk-jzrytcrf.js +4 -0
  27. package/dist/chunk-jzrytcrf.js.map +12 -0
  28. package/dist/chunk-k3thje6a.js +4 -0
  29. package/dist/chunk-k3thje6a.js.map +9 -0
  30. package/dist/chunk-nhva847d.js +5 -0
  31. package/dist/chunk-nhva847d.js.map +9 -0
  32. package/dist/chunk-tm47brht.js +4 -0
  33. package/dist/chunk-tm47brht.js.map +140 -0
  34. package/dist/chunk-xmvg4cds.js +4 -0
  35. package/dist/chunk-xmvg4cds.js.map +127 -0
  36. package/dist/chunk-xx7cwpzq.js +4 -0
  37. package/dist/chunk-xx7cwpzq.js.map +9 -0
  38. package/dist/chunk-yrxe491r.js +20 -0
  39. package/dist/chunk-yrxe491r.js.map +17 -0
  40. package/dist/index.cjs +2 -2
  41. package/dist/index.cjs.map +1 -1
  42. package/dist/keepkey/index.cjs +2 -2
  43. package/dist/keepkey/index.cjs.map +2 -2
  44. package/dist/keepkey/index.js +2 -2
  45. package/dist/keepkey/index.js.map +2 -2
  46. package/dist/ledger/index.cjs +90 -2
  47. package/dist/ledger/index.cjs.map +830 -23
  48. package/dist/ledger/index.js +2 -2
  49. package/dist/ledger/index.js.map +21 -19
  50. package/dist/trezor/index.cjs +2 -2
  51. package/dist/trezor/index.cjs.map +5 -4
  52. package/dist/trezor/index.js +2 -2
  53. package/dist/trezor/index.js.map +3 -3
  54. package/dist/types/helpers/psbt.d.ts +9 -0
  55. package/dist/types/helpers/psbt.d.ts.map +1 -0
  56. package/dist/types/ledger/clients/bitcoin.d.ts +36 -0
  57. package/dist/types/ledger/clients/bitcoin.d.ts.map +1 -0
  58. package/dist/types/ledger/clients/cosmos.d.ts +38 -10
  59. package/dist/types/ledger/clients/cosmos.d.ts.map +1 -1
  60. package/dist/types/ledger/clients/evm.d.ts +37 -46
  61. package/dist/types/ledger/clients/evm.d.ts.map +1 -1
  62. package/dist/types/ledger/clients/near.d.ts +5 -2
  63. package/dist/types/ledger/clients/near.d.ts.map +1 -1
  64. package/dist/types/ledger/clients/sui.d.ts +8 -12
  65. package/dist/types/ledger/clients/sui.d.ts.map +1 -1
  66. package/dist/types/ledger/clients/thorchain/index.d.ts +34 -8
  67. package/dist/types/ledger/clients/thorchain/index.d.ts.map +1 -1
  68. package/dist/types/ledger/clients/thorchain/protocol.d.ts +58 -0
  69. package/dist/types/ledger/clients/thorchain/protocol.d.ts.map +1 -0
  70. package/dist/types/ledger/clients/tron.d.ts +11 -8
  71. package/dist/types/ledger/clients/tron.d.ts.map +1 -1
  72. package/dist/types/ledger/clients/utxo-legacy-adapter.d.ts +3 -2
  73. package/dist/types/ledger/clients/utxo-legacy-adapter.d.ts.map +1 -1
  74. package/dist/types/ledger/clients/utxo-legacy-outputs.d.ts +18 -0
  75. package/dist/types/ledger/clients/utxo-legacy-outputs.d.ts.map +1 -0
  76. package/dist/types/ledger/clients/utxo.d.ts +10 -13
  77. package/dist/types/ledger/clients/utxo.d.ts.map +1 -1
  78. package/dist/types/ledger/clients/xrp.d.ts +4 -1
  79. package/dist/types/ledger/clients/xrp.d.ts.map +1 -1
  80. package/dist/types/ledger/clients/zcash.d.ts +53 -0
  81. package/dist/types/ledger/clients/zcash.d.ts.map +1 -0
  82. package/dist/types/ledger/helpers/dmk.d.ts +24 -0
  83. package/dist/types/ledger/helpers/dmk.d.ts.map +1 -0
  84. package/dist/types/ledger/helpers/executeDeviceAction.d.ts +34 -0
  85. package/dist/types/ledger/helpers/executeDeviceAction.d.ts.map +1 -0
  86. package/dist/types/ledger/helpers/getLedgerAddress.d.ts +1 -1
  87. package/dist/types/ledger/helpers/getLedgerAddress.d.ts.map +1 -1
  88. package/dist/types/ledger/helpers/getLedgerClient.d.ts +10 -3
  89. package/dist/types/ledger/helpers/getLedgerClient.d.ts.map +1 -1
  90. package/dist/types/ledger/helpers/index.d.ts +2 -1
  91. package/dist/types/ledger/helpers/index.d.ts.map +1 -1
  92. package/dist/types/ledger/helpers/ledgerJsDmkBridge.d.ts +26 -0
  93. package/dist/types/ledger/helpers/ledgerJsDmkBridge.d.ts.map +1 -0
  94. package/dist/types/ledger/helpers/ledgerJsDmkTransport.d.ts +13 -0
  95. package/dist/types/ledger/helpers/ledgerJsDmkTransport.d.ts.map +1 -0
  96. package/dist/types/ledger/helpers/lowS.d.ts +6 -0
  97. package/dist/types/ledger/helpers/lowS.d.ts.map +1 -0
  98. package/dist/types/ledger/helpers/rawTx.d.ts +2 -0
  99. package/dist/types/ledger/helpers/rawTx.d.ts.map +1 -0
  100. package/dist/types/ledger/index.d.ts +19 -7
  101. package/dist/types/ledger/index.d.ts.map +1 -1
  102. package/dist/types/ledger/types.d.ts +6 -10
  103. package/dist/types/ledger/types.d.ts.map +1 -1
  104. package/dist/types/trezor/index.d.ts +2 -2
  105. package/dist/types/trezor/index.d.ts.map +1 -1
  106. package/package.json +36 -36
  107. package/src/helpers/psbt.ts +26 -0
  108. package/src/ledger/clients/bitcoin.ts +575 -0
  109. package/src/ledger/clients/cosmos.ts +180 -51
  110. package/src/ledger/clients/evm.ts +238 -195
  111. package/src/ledger/clients/near.ts +43 -13
  112. package/src/ledger/clients/sui.ts +55 -68
  113. package/src/ledger/clients/thorchain/index.ts +210 -86
  114. package/src/ledger/clients/thorchain/protocol.ts +166 -0
  115. package/src/ledger/clients/tron.ts +49 -37
  116. package/src/ledger/clients/utxo-legacy-adapter.ts +35 -32
  117. package/src/ledger/clients/utxo-legacy-outputs.ts +122 -0
  118. package/src/ledger/clients/utxo.ts +108 -206
  119. package/src/ledger/clients/xrp.ts +39 -23
  120. package/src/ledger/clients/zcash.ts +440 -0
  121. package/src/ledger/helpers/dmk.ts +231 -0
  122. package/src/ledger/helpers/executeDeviceAction.ts +201 -0
  123. package/src/ledger/helpers/getLedgerClient.ts +101 -72
  124. package/src/ledger/helpers/index.ts +6 -1
  125. package/src/ledger/helpers/ledgerJsDmkBridge.ts +109 -0
  126. package/src/ledger/helpers/ledgerJsDmkTransport.ts +88 -0
  127. package/src/ledger/helpers/lowS.ts +30 -0
  128. package/src/ledger/helpers/rawTx.ts +12 -0
  129. package/src/ledger/index.ts +564 -369
  130. package/src/ledger/types.ts +15 -10
  131. package/src/trezor/index.ts +117 -25
  132. package/dist/chunk-3yr76n9s.js +0 -5
  133. package/dist/chunk-3yr76n9s.js.map +0 -10
  134. package/dist/chunk-9gy0xve4.js +0 -4
  135. package/dist/chunk-bwf31zxr.js +0 -4
  136. package/dist/chunk-bwf31zxr.js.map +0 -10
  137. package/dist/chunk-egtwbe6q.js +0 -4
  138. package/dist/chunk-egtwbe6q.js.map +0 -10
  139. package/dist/chunk-n05bv2n5.js +0 -5
  140. package/dist/chunk-vwv6z2mk.js +0 -4
  141. package/dist/types/ledger/clients/thorchain/common.d.ts +0 -28
  142. package/dist/types/ledger/clients/thorchain/common.d.ts.map +0 -1
  143. package/dist/types/ledger/clients/thorchain/helpers.d.ts +0 -7
  144. package/dist/types/ledger/clients/thorchain/helpers.d.ts.map +0 -1
  145. package/dist/types/ledger/clients/thorchain/lib.d.ts +0 -22
  146. package/dist/types/ledger/clients/thorchain/lib.d.ts.map +0 -1
  147. package/dist/types/ledger/clients/thorchain/utils.d.ts +0 -2
  148. package/dist/types/ledger/clients/thorchain/utils.d.ts.map +0 -1
  149. package/dist/types/ledger/clients/utxo-psbt.d.ts +0 -18
  150. package/dist/types/ledger/clients/utxo-psbt.d.ts.map +0 -1
  151. package/dist/types/ledger/cosmosTypes.d.ts +0 -40
  152. package/dist/types/ledger/cosmosTypes.d.ts.map +0 -1
  153. package/dist/types/ledger/helpers/getLedgerTransport.d.ts +0 -3
  154. package/dist/types/ledger/helpers/getLedgerTransport.d.ts.map +0 -1
  155. package/dist/types/ledger/helpers/ledgerNetworkInfo.d.ts +0 -14
  156. package/dist/types/ledger/helpers/ledgerNetworkInfo.d.ts.map +0 -1
  157. package/dist/types/ledger/interfaces/CosmosLedgerInterface.d.ts +0 -14
  158. package/dist/types/ledger/interfaces/CosmosLedgerInterface.d.ts.map +0 -1
  159. package/src/ledger/clients/thorchain/common.ts +0 -93
  160. package/src/ledger/clients/thorchain/helpers.ts +0 -120
  161. package/src/ledger/clients/thorchain/lib.ts +0 -258
  162. package/src/ledger/clients/thorchain/utils.ts +0 -69
  163. package/src/ledger/clients/utxo-psbt.ts +0 -172
  164. package/src/ledger/cosmosTypes.ts +0 -98
  165. package/src/ledger/helpers/getLedgerTransport.ts +0 -102
  166. package/src/ledger/helpers/ledgerNetworkInfo.ts +0 -127
  167. package/src/ledger/interfaces/CosmosLedgerInterface.ts +0 -67
@@ -0,0 +1,166 @@
1
+ import type Transport from "@ledgerhq/hw-transport";
2
+ import { SwapKitError } from "@swapkit/helpers";
3
+
4
+ const CLA = 0x55;
5
+ const INS_GET_ADDRESS = 0x04;
6
+ const INS_GET_VERSION = 0x00;
7
+ const INS_SIGN = 0x02;
8
+ const P1_INIT = 0x00;
9
+ const P1_ADD = 0x01;
10
+ const P1_LAST = 0x02;
11
+ const STATUS_OK = 0x9000;
12
+ const SIGN_CHUNK_SIZE = 250;
13
+
14
+ interface ThorCommandParams {
15
+ data?: Uint8Array;
16
+ ins: number;
17
+ p1?: number;
18
+ p2?: number;
19
+ }
20
+
21
+ interface ThorApduResponse {
22
+ data: Uint8Array;
23
+ statusCode: Uint8Array;
24
+ }
25
+
26
+ export class ThorCommand {
27
+ readonly name = "THORChainCommand";
28
+ private readonly params: ThorCommandParams;
29
+
30
+ constructor(params: ThorCommandParams) {
31
+ this.params = params;
32
+ }
33
+
34
+ getApdu() {
35
+ const { data = new Uint8Array(), ins, p1 = 0, p2 = 0 } = this.params;
36
+ return { cla: CLA, data, ins, p1, p2 };
37
+ }
38
+
39
+ parseResponse(response: ThorApduResponse) {
40
+ const status = (response.statusCode[0] ?? 0) * 0x100 + (response.statusCode[1] ?? 0);
41
+ if (status !== STATUS_OK) {
42
+ throw new Error(`THORChain app returned status 0x${status.toString(16).padStart(4, "0")}`);
43
+ }
44
+
45
+ return response.data;
46
+ }
47
+ }
48
+
49
+ export function serializeThorPath({ path }: { path: readonly (number | undefined)[] }) {
50
+ if (path.length !== 5) {
51
+ throw new SwapKitError("wallet_ledger_invalid_params", { reason: "THORChain path must contain five elements" });
52
+ }
53
+
54
+ const serialized = new Uint8Array(20);
55
+ const view = new DataView(serialized.buffer);
56
+
57
+ path.forEach((segment, index) => {
58
+ if (typeof segment !== "number" || !Number.isInteger(segment) || segment < 0 || segment > 0xffffffff) {
59
+ throw new SwapKitError("wallet_ledger_invalid_params", { reason: `Invalid THORChain path segment ${index}` });
60
+ }
61
+
62
+ // The app hardens purpose, coin type and account; a hardened change or index would silently derive
63
+ // a different key if its bit were cleared, so reject it instead.
64
+ if (index >= 3 && segment >= 0x80000000) {
65
+ throw new SwapKitError("wallet_ledger_invalid_params", {
66
+ reason: `THORChain path segment ${index} must not be hardened`,
67
+ });
68
+ }
69
+
70
+ const normalized = segment & 0x7fffffff;
71
+ const value = index < 3 ? (normalized | 0x80000000) >>> 0 : normalized;
72
+ view.setUint32(index * 4, value, true);
73
+ });
74
+
75
+ return serialized;
76
+ }
77
+
78
+ export function serializeThorAddressPayload({ hrp, path }: { hrp: string; path: readonly (number | undefined)[] }) {
79
+ const encodedHrp = new TextEncoder().encode(hrp);
80
+ if (encodedHrp.length < 1 || encodedHrp.length > 83) {
81
+ throw new SwapKitError("wallet_ledger_invalid_params", { reason: "Invalid THORChain address prefix" });
82
+ }
83
+
84
+ const serializedPath = serializeThorPath({ path });
85
+ return Uint8Array.from([encodedHrp.length, ...encodedHrp, ...serializedPath]);
86
+ }
87
+
88
+ export function getThorAddressCommand({
89
+ checkOnDevice,
90
+ hrp,
91
+ path,
92
+ }: {
93
+ checkOnDevice: boolean;
94
+ hrp: string;
95
+ path: readonly (number | undefined)[];
96
+ }) {
97
+ return new ThorCommand({
98
+ data: serializeThorAddressPayload({ hrp, path }),
99
+ ins: INS_GET_ADDRESS,
100
+ p1: checkOnDevice ? 1 : 0,
101
+ });
102
+ }
103
+
104
+ export function getThorSignCommands({ message, path }: { message: Uint8Array; path: readonly (number | undefined)[] }) {
105
+ const commands = [new ThorCommand({ data: serializeThorPath({ path }), ins: INS_SIGN, p1: P1_INIT })];
106
+
107
+ if (message.length === 0) {
108
+ commands.push(new ThorCommand({ data: new Uint8Array(), ins: INS_SIGN, p1: P1_LAST }));
109
+ return commands;
110
+ }
111
+
112
+ for (let offset = 0; offset < message.length; offset += SIGN_CHUNK_SIZE) {
113
+ const end = Math.min(offset + SIGN_CHUNK_SIZE, message.length);
114
+ commands.push(
115
+ new ThorCommand({
116
+ data: message.slice(offset, end),
117
+ ins: INS_SIGN,
118
+ p1: end === message.length ? P1_LAST : P1_ADD,
119
+ }),
120
+ );
121
+ }
122
+
123
+ return commands;
124
+ }
125
+
126
+ function parseTransportResponse({ response }: { response: Uint8Array }) {
127
+ if (response.length < 2) {
128
+ throw new SwapKitError("wallet_ledger_invalid_response", { reason: "THORChain response is missing status bytes" });
129
+ }
130
+
131
+ return response.slice(0, -2);
132
+ }
133
+
134
+ export async function getThorLegacyVersion({ transport }: { transport: Transport }) {
135
+ const response = await transport.send(CLA, INS_GET_VERSION, 0, 0, Buffer.alloc(0), [STATUS_OK]);
136
+ const data = parseTransportResponse({ response });
137
+ if (data.length < 4) {
138
+ throw new SwapKitError("wallet_ledger_invalid_response", { reason: "Invalid THORChain version response" });
139
+ }
140
+
141
+ return `${data[1]}.${data[2]}.${data[3]}`;
142
+ }
143
+
144
+ export async function sendThorLegacyCommand({ command, transport }: { command: ThorCommand; transport: Transport }) {
145
+ const apdu = command.getApdu();
146
+ const response = await transport.send(apdu.cla, apdu.ins, apdu.p1, apdu.p2, Buffer.from(apdu.data), [STATUS_OK]);
147
+ return parseTransportResponse({ response });
148
+ }
149
+
150
+ export function parseThorAddressResponse({ response }: { response: Uint8Array }) {
151
+ if (response.length < 34) {
152
+ throw new SwapKitError("wallet_ledger_invalid_response", { reason: "Invalid THORChain address response" });
153
+ }
154
+
155
+ const publicKey = response.slice(0, 33);
156
+ const address = new TextDecoder().decode(response.slice(33));
157
+ if (!address) {
158
+ throw new SwapKitError("wallet_ledger_invalid_response", { reason: "THORChain address is empty" });
159
+ }
160
+
161
+ return { address, publicKey };
162
+ }
163
+
164
+ export function invalidThorAppVersionMessage({ version }: { version?: string }) {
165
+ return `THORChain Ledger app major 2 is required, received ${version || "unknown"}`;
166
+ }
@@ -1,3 +1,4 @@
1
+ import type { UserInteractionRequired } from "@ledgerhq/device-management-kit";
1
2
  import type TronApp from "@ledgerhq/hw-app-trx";
2
3
  import type Transport from "@ledgerhq/hw-transport";
3
4
  import {
@@ -8,79 +9,90 @@ import {
8
9
  } from "@swapkit/helpers";
9
10
  import type { TronSignedTransaction, TronSigner, TronTransaction } from "@swapkit/toolboxes/tron";
10
11
 
11
- import { getLedgerTransport } from "../helpers/getLedgerTransport";
12
+ import {
13
+ LEDGER_USER_INTERACTION_REQUIRED,
14
+ type LedgerJsClientParams,
15
+ normalizeLedgerJsClientParams,
16
+ runLedgerJsOperation,
17
+ } from "../helpers/ledgerJsDmkBridge";
18
+
19
+ type TronLedgerParams = LedgerJsClientParams<DerivationPathArray | string>;
12
20
 
13
21
  export class TronLedgerInterface implements TronSigner {
14
- derivationPath: string;
15
- ledgerApp: InstanceType<typeof TronApp> | null = null;
16
- ledgerTimeout = 50000;
17
- private readonly injectedTransport?: Transport;
22
+ readonly derivationPath: string;
23
+ readonly ledgerTimeout = 50000;
24
+ private readonly connection: Omit<TronLedgerParams, "derivationPath">;
18
25
 
19
- constructor(derivationPath?: DerivationPathArray | string, transport?: Transport) {
26
+ constructor(paramsOrPath?: TronLedgerParams | DerivationPathArray | string, transport?: Transport) {
27
+ const { derivationPath, ...connection } = normalizeLedgerJsClientParams({ paramsOrPath, transport });
20
28
  this.derivationPath =
21
29
  typeof derivationPath === "string"
22
30
  ? derivationPath
23
31
  : derivationPathToString(derivationPath || NetworkDerivationPath.TRON);
24
- this.injectedTransport = transport;
32
+ this.connection = connection;
33
+ }
34
+
35
+ private async runTronOperation<Output>({
36
+ operation,
37
+ requiredUserInteraction,
38
+ }: {
39
+ operation: (app: InstanceType<typeof TronApp>) => Promise<Output>;
40
+ requiredUserInteraction?: UserInteractionRequired;
41
+ }) {
42
+ const TronApp = (await import("@ledgerhq/hw-app-trx")).default;
43
+ return runLedgerJsOperation({
44
+ appName: "Tron",
45
+ connection: this.connection,
46
+ createApp: (ledgerTransport) => new TronApp(ledgerTransport),
47
+ operation,
48
+ requiredUserInteraction,
49
+ });
25
50
  }
26
51
 
27
52
  checkOrCreateTransportAndLedger = async () => {
28
- if (this.ledgerApp) return;
29
- await this.createTransportAndLedger();
53
+ await this.runTronOperation({ operation: async () => true });
30
54
  };
31
55
 
32
56
  createTransportAndLedger = async () => {
33
- const transport = this.injectedTransport ?? (await getLedgerTransport());
34
- const TronApp = (await import("@ledgerhq/hw-app-trx")).default;
35
-
36
- this.ledgerApp = new TronApp(transport);
57
+ await this.runTronOperation({ operation: async () => true });
37
58
  };
38
59
 
39
- getAddress = async (): Promise<string> => {
60
+ getAddress = async () => {
40
61
  const response = await this.getAddressAndPubKey();
41
62
  if (!response) throw new SwapKitError("wallet_ledger_failed_to_get_address");
42
63
  return response.address;
43
64
  };
44
65
 
45
66
  getAddressAndPubKey = async () => {
46
- await this.createTransportAndLedger();
47
- const result = await this.ledgerApp?.getAddress(this.derivationPath);
48
-
67
+ const result = await this.runTronOperation({ operation: (app) => app.getAddress(this.derivationPath) });
49
68
  if (!result) throw new SwapKitError("wallet_ledger_failed_to_get_address");
50
69
 
51
70
  return { address: result.address, publicKey: result.publicKey };
52
71
  };
53
72
 
54
73
  showAddressAndPubKey = async () => {
55
- await this.createTransportAndLedger();
56
- return this.ledgerApp?.getAddress(this.derivationPath, true);
74
+ return await this.runTronOperation({
75
+ operation: (app) => app.getAddress(this.derivationPath, true),
76
+ requiredUserInteraction: LEDGER_USER_INTERACTION_REQUIRED.VerifyAddress,
77
+ });
57
78
  };
58
79
 
59
80
  signTransaction = async (transaction: TronTransaction): Promise<TronSignedTransaction> => {
60
- await this.createTransportAndLedger();
61
-
62
- if (!this.ledgerApp) {
63
- throw new SwapKitError("wallet_ledger_transport_error");
64
- }
65
-
66
81
  try {
67
- const signature = await this.ledgerApp.signTransaction(
68
- this.derivationPath,
69
- transaction.raw_data_hex,
70
- [], // Token signatures array - empty for native TRX transfers
71
- );
72
-
73
- if (!signature) {
74
- throw new SwapKitError("wallet_ledger_signing_error");
75
- }
82
+ const signature = await this.runTronOperation({
83
+ operation: (app) => app.signTransaction(this.derivationPath, transaction.raw_data_hex, []),
84
+ requiredUserInteraction: LEDGER_USER_INTERACTION_REQUIRED.SignTransaction,
85
+ });
76
86
 
77
- // Return the signed transaction in Tron's expected format
87
+ if (!signature) throw new SwapKitError("wallet_ledger_signing_error");
78
88
  return { ...transaction, signature: [signature] };
79
89
  } catch (error) {
90
+ if (error instanceof SwapKitError) throw error;
80
91
  throw new SwapKitError("wallet_ledger_signing_error", { error });
81
92
  }
82
93
  };
83
94
  }
84
95
 
85
- export const TronLedger = (derivationPath?: DerivationPathArray, transport?: Transport) =>
86
- new TronLedgerInterface(derivationPath, transport);
96
+ export function TronLedger(paramsOrPath?: TronLedgerParams | DerivationPathArray, transport?: Transport) {
97
+ return new TronLedgerInterface(paramsOrPath, transport);
98
+ }
@@ -2,6 +2,7 @@ import { hex } from "@scure/base";
2
2
  import { SwapKitError, type UTXOChain } from "@swapkit/helpers";
3
3
  import type { UTXOType } from "@swapkit/toolboxes/utxo";
4
4
  import type { Transaction } from "@swapkit/utxo-signer";
5
+ import { createCachedRawTxResolver } from "../helpers/rawTx";
5
6
 
6
7
  /**
7
8
  * Extract per-input metadata from a V3 PSBT in the shape the legacy
@@ -16,50 +17,51 @@ import type { Transaction } from "@swapkit/utxo-signer";
16
17
  export async function extractInputsFromPsbt(tx: Transaction, chain: UTXOChain): Promise<UTXOType[]> {
17
18
  const { RawTx } = await import("@swapkit/utxo-signer");
18
19
  const { getUtxoApi } = await import("@swapkit/toolboxes/utxo");
19
- const inputs: UTXOType[] = [];
20
+ const utxoApi = getUtxoApi(chain);
21
+ const getRawTx = createCachedRawTxResolver((txid) => utxoApi.getRawTx(txid));
20
22
 
21
- for (let i = 0; i < tx.inputsLength; i++) {
22
- const input = tx.getInput(i);
23
+ return Promise.all(
24
+ Array.from({ length: tx.inputsLength }, async (_, inputIndex) => {
25
+ const input = tx.getInput(inputIndex);
23
26
 
24
- if (!input.txid || input.index === undefined) {
25
- throw new Error(`PSBT input ${i} is missing txid/index`);
26
- }
27
+ if (!input.txid || input.index === undefined) {
28
+ throw new Error(`PSBT input ${inputIndex} is missing txid/index`);
29
+ }
27
30
 
28
- const txid = hex.encode(input.txid);
29
- const txHex = input.nonWitnessUtxo
30
- ? hex.encode(RawTx.encode(input.nonWitnessUtxo))
31
- : await getUtxoApi(chain).getRawTx(txid);
32
- if (!txHex) {
33
- throw new SwapKitError("wallet_ledger_invalid_params", {
34
- chain,
35
- inputIndex: i,
36
- reason: "Unable to resolve previous transaction hex for Ledger signing",
37
- txid,
38
- });
39
- }
40
- const witnessUtxo = input.witnessUtxo
41
- ? { script: input.witnessUtxo.script, value: Number(input.witnessUtxo.amount) }
42
- : undefined;
43
- const nonWitnessPrevout = input.index !== undefined ? input.nonWitnessUtxo?.outputs?.[input.index] : undefined;
44
- const value = witnessUtxo?.value ?? (nonWitnessPrevout ? Number(nonWitnessPrevout.amount) : 0);
31
+ const txid = hex.encode(input.txid);
32
+ const txHex = input.nonWitnessUtxo ? hex.encode(RawTx.encode(input.nonWitnessUtxo)) : await getRawTx(txid);
33
+ if (!txHex) {
34
+ throw new SwapKitError("wallet_ledger_invalid_params", {
35
+ chain,
36
+ inputIndex,
37
+ reason: "Unable to resolve previous transaction hex for Ledger signing",
38
+ txid,
39
+ });
40
+ }
41
+ const witnessUtxo = input.witnessUtxo
42
+ ? { script: input.witnessUtxo.script, value: Number(input.witnessUtxo.amount) }
43
+ : undefined;
44
+ const nonWitnessPrevout = input.nonWitnessUtxo?.outputs?.[input.index];
45
+ const value = witnessUtxo?.value ?? (nonWitnessPrevout ? Number(nonWitnessPrevout.amount) : 0);
45
46
 
46
- inputs.push({ hash: txid, index: input.index, txHex, value, witnessUtxo } as UTXOType);
47
- }
48
-
49
- return inputs;
47
+ return { hash: txid, index: input.index, txHex, value, witnessUtxo } as UTXOType;
48
+ }),
49
+ );
50
50
  }
51
51
 
52
52
  export async function signLegacyPsbtTransaction({
53
- legacyClient,
54
53
  chain,
54
+ inputUtxos,
55
+ legacyClient,
55
56
  tx,
56
57
  }: {
57
- legacyClient: { signTransaction: (tx: Transaction, inputUtxos: UTXOType[]) => Promise<string> };
58
58
  chain: UTXOChain;
59
+ inputUtxos?: UTXOType[];
60
+ legacyClient: { signTransaction: (tx: Transaction, inputUtxos: UTXOType[]) => Promise<string> };
59
61
  tx: Transaction;
60
62
  }): Promise<string> {
61
- const inputUtxos = await extractInputsFromPsbt(tx, chain);
62
- return legacyClient.signTransaction(tx, inputUtxos);
63
+ const resolvedInputUtxos = inputUtxos ?? (await extractInputsFromPsbt(tx, chain));
64
+ return legacyClient.signTransaction(tx, resolvedInputUtxos);
63
65
  }
64
66
 
65
67
  /**
@@ -86,7 +88,8 @@ export function createLegacyPsbtSigner({
86
88
  const { Transaction: TxClass } = await import("@swapkit/utxo-signer");
87
89
  // `Transaction.fromRaw` parses a serialised tx (no PSBT envelope) — exactly
88
90
  // what `createPaymentTransaction` returns.
89
- return TxClass.fromRaw(hex.decode(signedTxHex));
91
+ // Memo transactions carry an OP_RETURN output, which the parser treats as unknown.
92
+ return TxClass.fromRaw(hex.decode(signedTxHex), { allowUnknownOutputs: true });
90
93
  },
91
94
  };
92
95
  }
@@ -0,0 +1,122 @@
1
+ import type Transport from "@ledgerhq/hw-transport";
2
+ import { SwapKitError } from "@swapkit/helpers";
3
+
4
+ /**
5
+ * lib-app-bitcoin, behind the Bitcoin Cash, Dash, Dogecoin and Litecoin apps, collects the output it is checking in a
6
+ * 100-byte buffer (MAX_OUTPUT_TO_CHECK) while signing the first input. It tests every HASH_INPUT_FINALIZE_FULL payload
7
+ * against the room left in that buffer before appending it, and rejects one that does not fit with 0x6a80 ("Output is
8
+ * too long to be checked"); a complete output leaves the buffer once the user has approved it.
9
+ */
10
+ const LEGACY_APP_MAX_OUTPUT_LENGTH = 100;
11
+
12
+ const HASH_INPUT_FINALIZE_FULL = { cla: 0xe0, ins: 0x4a } as const;
13
+ const FINALIZE_P1_MORE = 0x00;
14
+ const FINALIZE_P1_LAST = 0x80;
15
+ // The app's own reply to a FINALIZE_P1_MORE APDU: a zero byte and 0x9000.
16
+ const FINALIZE_MORE_RESPONSE = [0x00, 0x90, 0x00];
17
+
18
+ // Bitcoin CompactSize. The 8-byte form describes nothing the app accepts, so it reads as malformed.
19
+ function readCompactSize(bytes: Uint8Array, offset: number): { size: number; value: number } | undefined {
20
+ const marker = bytes[offset];
21
+ if (marker === undefined || marker === 0xff) return undefined;
22
+ if (marker < 0xfd) return { size: 1, value: marker };
23
+
24
+ const width = marker === 0xfd ? 2 : 4;
25
+ if (offset + 1 + width > bytes.length) return undefined;
26
+ const view = new DataView(bytes.buffer, bytes.byteOffset + offset + 1, width);
27
+ return { size: 1 + width, value: width === 2 ? view.getUint16(0, true) : view.getUint32(0, true) };
28
+ }
29
+
30
+ /**
31
+ * Splits serialised transaction outputs (the output count, then each output) into one piece per output, the first
32
+ * carrying the count, so that every piece the legacy app receives ends on an output boundary. Rejects a malformed
33
+ * stream and any piece the app cannot buffer.
34
+ */
35
+ export function splitOutputsForLegacyApp(outputs: Uint8Array, chain: string): Uint8Array[] {
36
+ const invalidOutputs = (reason: string, info: Record<string, number> = {}) =>
37
+ new SwapKitError({
38
+ errorKey: "wallet_ledger_invalid_params",
39
+ info: {
40
+ chain,
41
+ maxLength: LEGACY_APP_MAX_OUTPUT_LENGTH,
42
+ operation: "splitOutputsForLegacyApp",
43
+ outputsLength: outputs.length,
44
+ reason,
45
+ ...info,
46
+ },
47
+ });
48
+
49
+ const count = readCompactSize(outputs, 0);
50
+ if (!count?.value) throw invalidOutputs("Transaction outputs do not start with a readable non-zero output count");
51
+
52
+ const pieces: Uint8Array[] = [];
53
+ let offset = count.size;
54
+ for (let outputIndex = 0; outputIndex < count.value; outputIndex += 1) {
55
+ // An output is an 8-byte amount, the script length and the script; the first piece also carries the count.
56
+ const pieceStart = outputIndex === 0 ? 0 : offset;
57
+ const scriptLength = readCompactSize(outputs, offset + 8);
58
+ const outputEnd = scriptLength ? offset + 8 + scriptLength.size + scriptLength.value : undefined;
59
+ if (!outputEnd || outputEnd > outputs.length) {
60
+ throw invalidOutputs("Transaction output is truncated", { outputIndex });
61
+ }
62
+
63
+ const pieceLength = outputEnd - pieceStart;
64
+ if (pieceLength > LEGACY_APP_MAX_OUTPUT_LENGTH) {
65
+ throw invalidOutputs("Output is longer than the Ledger app can check; shorten the memo", {
66
+ outputIndex,
67
+ pieceLength,
68
+ });
69
+ }
70
+
71
+ pieces.push(outputs.subarray(pieceStart, outputEnd));
72
+ offset = outputEnd;
73
+ }
74
+
75
+ if (offset !== outputs.length) {
76
+ throw invalidOutputs("Transaction outputs are followed by trailing bytes", {
77
+ trailingBytes: outputs.length - offset,
78
+ });
79
+ }
80
+
81
+ return pieces;
82
+ }
83
+
84
+ /**
85
+ * hw-app-btc streams the outputs in fixed 50-byte HASH_INPUT_FINALIZE_FULL chunks. Each chunk has to fit the app's
86
+ * buffer beside whatever unfinished output earlier chunks left there, which fails for longer memo outputs (in the
87
+ * SDK's [vault, memo, change] order, from a 56-byte memo with P2PKH outputs and a 59-byte memo on Litecoin). Collect
88
+ * the chunks of each stream and resend it one output per APDU: each APDU then meets an empty buffer, so any output of
89
+ * up to 100 bytes (the first together with the output count) fits, and the bytes, and so the hashes, stay the same.
90
+ * hw-app-btc ignores every response to these APDUs, so a collected chunk gets the app's own reply and the last one
91
+ * returns the device's. The change-path APDU (P1 0xff) and every other APDU pass straight through.
92
+ */
93
+ export function withOutputAlignedFinalize(transport: Transport, chain: string): Transport {
94
+ let chunks: Buffer[] = [];
95
+
96
+ const send: Transport["send"] = async (cla, ins, p1, p2, data = Buffer.alloc(0), ...rest) => {
97
+ const isOutputChunk =
98
+ cla === HASH_INPUT_FINALIZE_FULL.cla &&
99
+ ins === HASH_INPUT_FINALIZE_FULL.ins &&
100
+ (p1 === FINALIZE_P1_MORE || p1 === FINALIZE_P1_LAST);
101
+ if (!isOutputChunk) return transport.send(cla, ins, p1, p2, data, ...rest);
102
+
103
+ chunks.push(data);
104
+ if (p1 === FINALIZE_P1_MORE) return Buffer.from(FINALIZE_MORE_RESPONSE);
105
+
106
+ const outputs = Buffer.concat(chunks);
107
+ chunks = [];
108
+ const pieces = splitOutputsForLegacyApp(outputs, chain);
109
+
110
+ let response: Buffer = Buffer.alloc(0);
111
+ for (const [index, piece] of pieces.entries()) {
112
+ const pieceP1 = index === pieces.length - 1 ? FINALIZE_P1_LAST : FINALIZE_P1_MORE;
113
+ response = await transport.send(cla, ins, pieceP1, p2, Buffer.from(piece), ...rest);
114
+ }
115
+ return response;
116
+ };
117
+
118
+ // A Proxy rather than a copy, so the app-API lock and every other field stay on the caller's transport.
119
+ return new Proxy(transport, {
120
+ get: (target, property, receiver) => (property === "send" ? send : Reflect.get(target, property, receiver)),
121
+ });
122
+ }