@swapkit/wallet-hardware 5.0.1 → 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 (165) 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-xkh15edk.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-20wx60bn.js.map → chunk-5c7b7z2d.js.map} +2 -2
  12. package/dist/chunk-5p3ttw6y.js +4 -0
  13. package/dist/{chunk-1jpcb30j.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-4tbtxdxa.js.map → chunk-jewm2ag3.js.map} +2 -2
  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 -24
  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 -5
  52. package/dist/trezor/index.js +2 -2
  53. package/dist/trezor/index.js.map +3 -3
  54. package/dist/types/ledger/clients/bitcoin.d.ts +36 -0
  55. package/dist/types/ledger/clients/bitcoin.d.ts.map +1 -0
  56. package/dist/types/ledger/clients/cosmos.d.ts +38 -10
  57. package/dist/types/ledger/clients/cosmos.d.ts.map +1 -1
  58. package/dist/types/ledger/clients/evm.d.ts +37 -46
  59. package/dist/types/ledger/clients/evm.d.ts.map +1 -1
  60. package/dist/types/ledger/clients/near.d.ts +5 -2
  61. package/dist/types/ledger/clients/near.d.ts.map +1 -1
  62. package/dist/types/ledger/clients/sui.d.ts +8 -12
  63. package/dist/types/ledger/clients/sui.d.ts.map +1 -1
  64. package/dist/types/ledger/clients/thorchain/index.d.ts +34 -8
  65. package/dist/types/ledger/clients/thorchain/index.d.ts.map +1 -1
  66. package/dist/types/ledger/clients/thorchain/protocol.d.ts +58 -0
  67. package/dist/types/ledger/clients/thorchain/protocol.d.ts.map +1 -0
  68. package/dist/types/ledger/clients/tron.d.ts +11 -8
  69. package/dist/types/ledger/clients/tron.d.ts.map +1 -1
  70. package/dist/types/ledger/clients/utxo-legacy-adapter.d.ts +3 -2
  71. package/dist/types/ledger/clients/utxo-legacy-adapter.d.ts.map +1 -1
  72. package/dist/types/ledger/clients/utxo-legacy-outputs.d.ts +18 -0
  73. package/dist/types/ledger/clients/utxo-legacy-outputs.d.ts.map +1 -0
  74. package/dist/types/ledger/clients/utxo.d.ts +10 -13
  75. package/dist/types/ledger/clients/utxo.d.ts.map +1 -1
  76. package/dist/types/ledger/clients/xrp.d.ts +4 -1
  77. package/dist/types/ledger/clients/xrp.d.ts.map +1 -1
  78. package/dist/types/ledger/clients/zcash.d.ts +53 -0
  79. package/dist/types/ledger/clients/zcash.d.ts.map +1 -0
  80. package/dist/types/ledger/helpers/dmk.d.ts +24 -0
  81. package/dist/types/ledger/helpers/dmk.d.ts.map +1 -0
  82. package/dist/types/ledger/helpers/executeDeviceAction.d.ts +34 -0
  83. package/dist/types/ledger/helpers/executeDeviceAction.d.ts.map +1 -0
  84. package/dist/types/ledger/helpers/getLedgerAddress.d.ts +1 -1
  85. package/dist/types/ledger/helpers/getLedgerAddress.d.ts.map +1 -1
  86. package/dist/types/ledger/helpers/getLedgerClient.d.ts +10 -3
  87. package/dist/types/ledger/helpers/getLedgerClient.d.ts.map +1 -1
  88. package/dist/types/ledger/helpers/index.d.ts +2 -1
  89. package/dist/types/ledger/helpers/index.d.ts.map +1 -1
  90. package/dist/types/ledger/helpers/ledgerJsDmkBridge.d.ts +26 -0
  91. package/dist/types/ledger/helpers/ledgerJsDmkBridge.d.ts.map +1 -0
  92. package/dist/types/ledger/helpers/ledgerJsDmkTransport.d.ts +13 -0
  93. package/dist/types/ledger/helpers/ledgerJsDmkTransport.d.ts.map +1 -0
  94. package/dist/types/ledger/helpers/lowS.d.ts +6 -0
  95. package/dist/types/ledger/helpers/lowS.d.ts.map +1 -0
  96. package/dist/types/ledger/helpers/rawTx.d.ts +2 -0
  97. package/dist/types/ledger/helpers/rawTx.d.ts.map +1 -0
  98. package/dist/types/ledger/index.d.ts +19 -7
  99. package/dist/types/ledger/index.d.ts.map +1 -1
  100. package/dist/types/ledger/types.d.ts +6 -10
  101. package/dist/types/ledger/types.d.ts.map +1 -1
  102. package/dist/types/trezor/index.d.ts +2 -2
  103. package/dist/types/trezor/index.d.ts.map +1 -1
  104. package/package.json +36 -36
  105. package/src/ledger/clients/bitcoin.ts +575 -0
  106. package/src/ledger/clients/cosmos.ts +180 -51
  107. package/src/ledger/clients/evm.ts +238 -195
  108. package/src/ledger/clients/near.ts +43 -13
  109. package/src/ledger/clients/sui.ts +55 -68
  110. package/src/ledger/clients/thorchain/index.ts +210 -86
  111. package/src/ledger/clients/thorchain/protocol.ts +166 -0
  112. package/src/ledger/clients/tron.ts +49 -37
  113. package/src/ledger/clients/utxo-legacy-adapter.ts +35 -32
  114. package/src/ledger/clients/utxo-legacy-outputs.ts +122 -0
  115. package/src/ledger/clients/utxo.ts +108 -206
  116. package/src/ledger/clients/xrp.ts +39 -23
  117. package/src/ledger/clients/zcash.ts +440 -0
  118. package/src/ledger/helpers/dmk.ts +231 -0
  119. package/src/ledger/helpers/executeDeviceAction.ts +201 -0
  120. package/src/ledger/helpers/getLedgerClient.ts +101 -72
  121. package/src/ledger/helpers/index.ts +6 -1
  122. package/src/ledger/helpers/ledgerJsDmkBridge.ts +109 -0
  123. package/src/ledger/helpers/ledgerJsDmkTransport.ts +88 -0
  124. package/src/ledger/helpers/lowS.ts +30 -0
  125. package/src/ledger/helpers/rawTx.ts +12 -0
  126. package/src/ledger/index.ts +564 -369
  127. package/src/ledger/types.ts +15 -10
  128. package/src/trezor/index.ts +115 -27
  129. package/dist/chunk-1jpcb30j.js +0 -5
  130. package/dist/chunk-20wx60bn.js +0 -4
  131. package/dist/chunk-4tbtxdxa.js +0 -5
  132. package/dist/chunk-7v7cgm69.js +0 -4
  133. package/dist/chunk-7v7cgm69.js.map +0 -10
  134. package/dist/chunk-83g13ck6.js +0 -5
  135. package/dist/chunk-83g13ck6.js.map +0 -10
  136. package/dist/chunk-h2r568rz.js +0 -4
  137. package/dist/chunk-h2r568rz.js.map +0 -10
  138. package/dist/chunk-xkh15edk.js +0 -4
  139. package/dist/types/ledger/clients/thorchain/common.d.ts +0 -28
  140. package/dist/types/ledger/clients/thorchain/common.d.ts.map +0 -1
  141. package/dist/types/ledger/clients/thorchain/helpers.d.ts +0 -7
  142. package/dist/types/ledger/clients/thorchain/helpers.d.ts.map +0 -1
  143. package/dist/types/ledger/clients/thorchain/lib.d.ts +0 -22
  144. package/dist/types/ledger/clients/thorchain/lib.d.ts.map +0 -1
  145. package/dist/types/ledger/clients/thorchain/utils.d.ts +0 -2
  146. package/dist/types/ledger/clients/thorchain/utils.d.ts.map +0 -1
  147. package/dist/types/ledger/clients/utxo-psbt.d.ts +0 -18
  148. package/dist/types/ledger/clients/utxo-psbt.d.ts.map +0 -1
  149. package/dist/types/ledger/cosmosTypes.d.ts +0 -40
  150. package/dist/types/ledger/cosmosTypes.d.ts.map +0 -1
  151. package/dist/types/ledger/helpers/getLedgerTransport.d.ts +0 -3
  152. package/dist/types/ledger/helpers/getLedgerTransport.d.ts.map +0 -1
  153. package/dist/types/ledger/helpers/ledgerNetworkInfo.d.ts +0 -14
  154. package/dist/types/ledger/helpers/ledgerNetworkInfo.d.ts.map +0 -1
  155. package/dist/types/ledger/interfaces/CosmosLedgerInterface.d.ts +0 -14
  156. package/dist/types/ledger/interfaces/CosmosLedgerInterface.d.ts.map +0 -1
  157. package/src/ledger/clients/thorchain/common.ts +0 -93
  158. package/src/ledger/clients/thorchain/helpers.ts +0 -120
  159. package/src/ledger/clients/thorchain/lib.ts +0 -258
  160. package/src/ledger/clients/thorchain/utils.ts +0 -69
  161. package/src/ledger/clients/utxo-psbt.ts +0 -204
  162. package/src/ledger/cosmosTypes.ts +0 -98
  163. package/src/ledger/helpers/getLedgerTransport.ts +0 -102
  164. package/src/ledger/helpers/ledgerNetworkInfo.ts +0 -127
  165. package/src/ledger/interfaces/CosmosLedgerInterface.ts +0 -67
@@ -0,0 +1,575 @@
1
+ import type { SignerBtc } from "@ledgerhq/device-signer-kit-bitcoin";
2
+ import type Transport from "@ledgerhq/hw-transport";
3
+ import { hex } from "@scure/base";
4
+ import { HARDENED_OFFSET, HDKey } from "@scure/bip32";
5
+ import {
6
+ Chain,
7
+ type DerivationPathArray,
8
+ derivationPathToString,
9
+ NetworkDerivationPath,
10
+ SwapKitError,
11
+ } from "@swapkit/helpers";
12
+ import type { UTXOType } from "@swapkit/toolboxes/utxo";
13
+ import type { Transaction } from "@swapkit/utxo-signer";
14
+ import { match } from "ts-pattern";
15
+
16
+ import { applyMissingSpendingMetadata } from "../../helpers/psbt";
17
+ import type { LedgerDMKSession } from "../helpers/dmk";
18
+ import { createLedgerSessionSigner } from "../helpers/dmk";
19
+ import { executeLedgerDeviceAction, type LedgerDeviceActionStateHandler } from "../helpers/executeDeviceAction";
20
+ import { createCachedRawTxResolver } from "../helpers/rawTx";
21
+ import { BitcoinLedger as LegacyBitcoinLedger } from "./utxo";
22
+ import { extractInputsFromPsbt, signLegacyPsbtTransaction } from "./utxo-legacy-adapter";
23
+
24
+ interface BitcoinLedgerParams {
25
+ derivationPath?: DerivationPathArray | string;
26
+ dmkSession?: LedgerDMKSession;
27
+ onDeviceActionState?: LedgerDeviceActionStateHandler;
28
+ transport?: Transport;
29
+ }
30
+
31
+ interface GetExtendedPublicKeyParams {
32
+ checkOnDevice?: boolean;
33
+ path?: string;
34
+ }
35
+
36
+ interface SignBitcoinTransactionParams {
37
+ inputUtxos?: UTXOType[];
38
+ tx: Transaction;
39
+ }
40
+
41
+ interface SignBitcoinTransactionWithMultiplePathsParams extends SignBitcoinTransactionParams {
42
+ derivationPaths: string[];
43
+ }
44
+
45
+ interface ParsedBitcoinPath {
46
+ accountPath: string;
47
+ addressIndex: number;
48
+ change: number;
49
+ fullPath: string;
50
+ purpose: 44 | 49 | 84 | 86;
51
+ }
52
+
53
+ function normalizePath(path: DerivationPathArray | string) {
54
+ return (typeof path === "string" ? path : derivationPathToString(path)).replace(/^m\//, "").replace(/^\/+/, "");
55
+ }
56
+
57
+ function invalidPath({ path, reason }: { path: string; reason: string }) {
58
+ return new SwapKitError("wallet_ledger_invalid_params", { path, reason });
59
+ }
60
+
61
+ function parseBitcoinPath(path: DerivationPathArray | string): ParsedBitcoinPath {
62
+ const normalized = normalizePath(path);
63
+ const segments = /^(44|49|84|86)'\/0'\/(\d+)'(?:\/(0|1)\/(\d+))?$/.exec(normalized);
64
+
65
+ if (!segments) {
66
+ throw invalidPath({
67
+ path: normalized,
68
+ reason: "Expected a Bitcoin path with purpose 44, 49, 84, or 86 and coin type 0",
69
+ });
70
+ }
71
+
72
+ const purpose = Number(segments[1]) as ParsedBitcoinPath["purpose"];
73
+ const account = Number(segments[2]);
74
+ const change = Number(segments[3] ?? 0);
75
+ const addressIndex = Number(segments[4] ?? 0);
76
+
77
+ // A larger index would collide with the hardened range and name another key.
78
+ if (account >= HARDENED_OFFSET || addressIndex >= HARDENED_OFFSET) {
79
+ throw invalidPath({ path: normalized, reason: "Account and address index must be below 2^31" });
80
+ }
81
+
82
+ const accountPath = `${purpose}'/0'/${account}'`;
83
+ return { accountPath, addressIndex, change, fullPath: `${accountPath}/${change}/${addressIndex}`, purpose };
84
+ }
85
+
86
+ function fingerprintToNumber(fingerprint: Uint8Array) {
87
+ if (fingerprint.length !== 4) {
88
+ throw new SwapKitError("wallet_ledger_invalid_response", {
89
+ reason: `Expected a 4-byte master fingerprint, received ${fingerprint.length}`,
90
+ });
91
+ }
92
+
93
+ return new DataView(fingerprint.buffer, fingerprint.byteOffset, fingerprint.byteLength).getUint32(0, false);
94
+ }
95
+
96
+ function validateInputCount({ count, tx }: { count: number; tx: Transaction }) {
97
+ if (count !== tx.inputsLength) {
98
+ throw new SwapKitError("wallet_ledger_invalid_params", {
99
+ reason: `Input metadata count (${count}) must match transaction inputs count (${tx.inputsLength})`,
100
+ });
101
+ }
102
+ }
103
+
104
+ function validateAccountPaths({
105
+ accountPath,
106
+ derivationPaths,
107
+ tx,
108
+ }: {
109
+ accountPath: string;
110
+ derivationPaths: string[];
111
+ tx: Transaction;
112
+ }) {
113
+ validateInputCount({ count: derivationPaths.length, tx });
114
+ const parsedPaths = derivationPaths.map(parseBitcoinPath);
115
+ const mismatchedPath = parsedPaths.find((path) => path.accountPath !== accountPath);
116
+
117
+ if (mismatchedPath) {
118
+ throw new SwapKitError("wallet_ledger_invalid_params", {
119
+ accountPath,
120
+ path: mismatchedPath.fullPath,
121
+ reason: "All input derivation paths must belong to the configured Bitcoin account",
122
+ });
123
+ }
124
+
125
+ return parsedPaths;
126
+ }
127
+
128
+ function descriptorTemplate(purpose: ParsedBitcoinPath["purpose"]) {
129
+ return import("@ledgerhq/device-signer-kit-bitcoin").then(({ DefaultDescriptorTemplate }) =>
130
+ match(purpose)
131
+ .with(44, () => DefaultDescriptorTemplate.LEGACY)
132
+ .with(49, () => DefaultDescriptorTemplate.NESTED_SEGWIT)
133
+ .with(84, () => DefaultDescriptorTemplate.NATIVE_SEGWIT)
134
+ .with(86, () => DefaultDescriptorTemplate.TAPROOT)
135
+ .exhaustive(),
136
+ );
137
+ }
138
+
139
+ // Mirrors the signer kit's `PartialSignature`, which its package entry point does not export.
140
+ interface PartialSignature {
141
+ inputIndex: number;
142
+ pubkey: Uint8Array;
143
+ signature: Uint8Array;
144
+ tapleafHash?: Uint8Array;
145
+ }
146
+
147
+ function isPartialSignature(signature: object): signature is PartialSignature {
148
+ return "signature" in signature && "pubkey" in signature;
149
+ }
150
+
151
+ function signatureError({ inputIndex, reason }: { inputIndex?: number; reason: string }) {
152
+ return new SwapKitError("wallet_ledger_invalid_response", { inputIndex, reason });
153
+ }
154
+
155
+ type PreviousTransaction = NonNullable<ReturnType<Transaction["getInput"]>["nonWitnessUtxo"]>;
156
+ type SpentOutput = PreviousTransaction["outputs"][number];
157
+
158
+ // Returns the output an input spends, after checking the previous transaction hashes to the input's
159
+ // txid and that a witnessUtxo the input carries agrees with it: amounts shown on the device come from
160
+ // them, so neither may be substitutable.
161
+ async function verifiedSpentOutput({
162
+ input,
163
+ inputIndex,
164
+ previousTransaction,
165
+ }: {
166
+ input: ReturnType<Transaction["getInput"]>;
167
+ inputIndex: number;
168
+ previousTransaction?: PreviousTransaction;
169
+ }) {
170
+ if (!(previousTransaction && input.txid && input.index !== undefined)) {
171
+ throw signatureError({ inputIndex, reason: "PSBT input is missing its previous output" });
172
+ }
173
+
174
+ const { RawTx, utils } = await import("@swapkit/utxo-signer");
175
+ // The txid commits to the legacy serialisation; raw SegWit transactions also carry the marker, flag and witnesses.
176
+ const legacySerialisation = RawTx.encode({ ...previousTransaction, segwitFlag: false, witnesses: undefined });
177
+ const previousTxid = utils.sha256x2(legacySerialisation).reverse();
178
+ if (hex.encode(previousTxid) !== hex.encode(input.txid)) {
179
+ throw new SwapKitError("wallet_ledger_invalid_params", {
180
+ inputIndex,
181
+ reason: "Previous transaction does not match the input txid",
182
+ });
183
+ }
184
+
185
+ const spentOutput = previousTransaction.outputs[input.index];
186
+ if (!spentOutput) throw signatureError({ inputIndex, reason: "Previous transaction has no spent output" });
187
+
188
+ const { witnessUtxo } = input;
189
+ if (
190
+ witnessUtxo &&
191
+ (witnessUtxo.amount !== spentOutput.amount || hex.encode(witnessUtxo.script) !== hex.encode(spentOutput.script))
192
+ ) {
193
+ throw new SwapKitError("wallet_ledger_invalid_params", {
194
+ inputIndex,
195
+ reason: "Input witnessUtxo does not match the previous transaction output",
196
+ });
197
+ }
198
+
199
+ return spentOutput;
200
+ }
201
+
202
+ // Fails closed unless the input spends the script its key path derives, so the key origin the device signs
203
+ // with and the spending metadata derived from that key describe the output actually being spent.
204
+ async function assertSpendsPathScript({
205
+ inputIndex,
206
+ path,
207
+ publicKey,
208
+ spentOutput,
209
+ }: {
210
+ inputIndex: number;
211
+ path: ParsedBitcoinPath;
212
+ publicKey: Uint8Array;
213
+ spentOutput: SpentOutput;
214
+ }) {
215
+ const { p2pkh, p2sh, p2tr, p2wpkh } = await import("@swapkit/utxo-signer");
216
+ const pathScript = match(path.purpose)
217
+ .with(44, () => p2pkh(publicKey).script)
218
+ .with(49, () => p2sh(p2wpkh(publicKey)).script)
219
+ .with(84, () => p2wpkh(publicKey).script)
220
+ .with(86, () => p2tr(publicKey.slice(1)).script)
221
+ .exhaustive();
222
+
223
+ if (hex.encode(pathScript) === hex.encode(spentOutput.script)) return;
224
+
225
+ throw new SwapKitError("wallet_ledger_invalid_params", {
226
+ inputIndex,
227
+ path: path.fullPath,
228
+ reason: "Input does not spend the script derived from its derivation path",
229
+ });
230
+ }
231
+
232
+ export function BitcoinLedger({
233
+ derivationPath = NetworkDerivationPath.BTC,
234
+ dmkSession,
235
+ onDeviceActionState,
236
+ transport,
237
+ }: BitcoinLedgerParams = {}) {
238
+ const configuredPath = parseBitcoinPath(derivationPath);
239
+ const legacyClient = transport ? LegacyBitcoinLedger(derivationPath, transport) : undefined;
240
+ const getSessionSigner = createLedgerSessionSigner<SignerBtc>({
241
+ build: async (session) => {
242
+ const { SignerBtcBuilder } = await import("@ledgerhq/device-signer-kit-bitcoin");
243
+ return new SignerBtcBuilder(session).build();
244
+ },
245
+ dmkSession,
246
+ });
247
+ let accountXpubPromise: Promise<string> | undefined;
248
+ let fingerprintPromise: Promise<number> | undefined;
249
+
250
+ function getSigner() {
251
+ if (legacyClient) {
252
+ throw new SwapKitError("wallet_ledger_invalid_params", {
253
+ reason: "A Device Signer Kit operation is unavailable when a legacy transport is supplied",
254
+ });
255
+ }
256
+
257
+ return getSessionSigner();
258
+ }
259
+
260
+ function getAccountXpub() {
261
+ if (!accountXpubPromise) {
262
+ accountXpubPromise = getSigner().then(async (signer) => {
263
+ const { extendedPublicKey } = await executeLedgerDeviceAction({
264
+ action: () => signer.getExtendedPublicKey(configuredPath.accountPath),
265
+ onDeviceActionState,
266
+ });
267
+ return extendedPublicKey;
268
+ });
269
+ const pendingXpub = accountXpubPromise;
270
+ void pendingXpub.catch(() => {
271
+ if (accountXpubPromise === pendingXpub) accountXpubPromise = undefined;
272
+ });
273
+ }
274
+ return accountXpubPromise;
275
+ }
276
+
277
+ function getMasterFingerprint() {
278
+ if (!fingerprintPromise) {
279
+ fingerprintPromise = getSigner().then(async (signer) => {
280
+ const { masterFingerprint } = await executeLedgerDeviceAction({
281
+ action: () => signer.getMasterFingerprint(),
282
+ onDeviceActionState,
283
+ });
284
+ return fingerprintToNumber(masterFingerprint);
285
+ });
286
+ const pendingFingerprint = fingerprintPromise;
287
+ void pendingFingerprint.catch(() => {
288
+ if (fingerprintPromise === pendingFingerprint) fingerprintPromise = undefined;
289
+ });
290
+ }
291
+ return fingerprintPromise;
292
+ }
293
+
294
+ /**
295
+ * The configured address's compressed public key, derived from the account xpub. The toolbox needs it to
296
+ * attach a nested SegWit input's redeemScript and a taproot input's internal key when it builds a transaction.
297
+ */
298
+ async function getPublicKey() {
299
+ const accountXpub = legacyClient
300
+ ? await legacyClient.getExtendedPublicKey(configuredPath.accountPath)
301
+ : await getAccountXpub();
302
+ const { publicKey } = HDKey.fromExtendedKey(accountXpub).derive(
303
+ `m/${configuredPath.change}/${configuredPath.addressIndex}`,
304
+ );
305
+ if (!publicKey) {
306
+ throw new SwapKitError("wallet_ledger_invalid_response", {
307
+ path: configuredPath.fullPath,
308
+ reason: "Could not derive a public key from the Ledger account xpub",
309
+ });
310
+ }
311
+ return publicKey;
312
+ }
313
+
314
+ async function getWallet() {
315
+ const [{ DefaultWallet }, template] = await Promise.all([
316
+ import("@ledgerhq/device-signer-kit-bitcoin"),
317
+ descriptorTemplate(configuredPath.purpose),
318
+ ]);
319
+ return new DefaultWallet(configuredPath.accountPath, template);
320
+ }
321
+
322
+ async function resolvePreviousTransactions({ inputUtxos, tx }: { inputUtxos?: UTXOType[]; tx: Transaction }) {
323
+ const { RawTx } = await import("@swapkit/utxo-signer");
324
+ const needsLookup = Array.from({ length: tx.inputsLength }, (_, index) => index).some(
325
+ (index) => !(tx.getInput(index).nonWitnessUtxo || inputUtxos?.[index]?.txHex),
326
+ );
327
+ const getRawTx = needsLookup
328
+ ? await import("@swapkit/toolboxes/utxo").then(({ getUtxoApi }) => {
329
+ const utxoApi = getUtxoApi(Chain.Bitcoin);
330
+ return createCachedRawTxResolver((txid) => utxoApi.getRawTx(txid));
331
+ })
332
+ : undefined;
333
+
334
+ return Promise.all(
335
+ Array.from({ length: tx.inputsLength }, async (_, inputIndex) => {
336
+ const input = tx.getInput(inputIndex);
337
+ if (input.nonWitnessUtxo) return input.nonWitnessUtxo;
338
+ if (!input.txid) throw signatureError({ inputIndex, reason: "PSBT input is missing its previous txid" });
339
+
340
+ const txHex = inputUtxos?.[inputIndex]?.txHex ?? (await getRawTx?.(hex.encode(input.txid)));
341
+ if (!txHex) {
342
+ throw new SwapKitError("wallet_ledger_invalid_params", {
343
+ inputIndex,
344
+ reason: "Unable to resolve the previous transaction for Ledger signing",
345
+ });
346
+ }
347
+ return RawTx.decode(hex.decode(txHex));
348
+ }),
349
+ );
350
+ }
351
+
352
+ /**
353
+ * Attach what the Bitcoin app needs to verify and sign each input: key origins, for pre-taproot inputs the
354
+ * full previous transaction (their amounts are only verifiable from it), for SegWit the spent output and, for
355
+ * nested SegWit and taproot, the redeemScript or internal key. The previous transaction is checked against the
356
+ * input's txid so it cannot misstate the amount, and the spent script against the input's key path.
357
+ */
358
+ async function addInputDerivations({
359
+ inputUtxos,
360
+ paths,
361
+ tx,
362
+ }: {
363
+ inputUtxos?: UTXOType[];
364
+ paths: ParsedBitcoinPath[];
365
+ tx: Transaction;
366
+ }) {
367
+ const [accountXpub, fingerprint, previousTransactions] = await Promise.all([
368
+ getAccountXpub(),
369
+ getMasterFingerprint(),
370
+ resolvePreviousTransactions({ inputUtxos, tx }),
371
+ ]);
372
+ const { bip32Path } = await import("@swapkit/utxo-signer");
373
+ const accountKey = HDKey.fromExtendedKey(accountXpub);
374
+ const psbt = tx.clone();
375
+ const publicKeys: Uint8Array[] = [];
376
+
377
+ for (const [inputIndex, path] of paths.entries()) {
378
+ const publicKey = accountKey.derive(`m/${path.change}/${path.addressIndex}`).publicKey;
379
+ if (!publicKey) {
380
+ throw new SwapKitError("wallet_ledger_invalid_response", {
381
+ inputIndex,
382
+ reason: "Could not derive a public key from the Ledger account xpub",
383
+ });
384
+ }
385
+
386
+ publicKeys[inputIndex] = publicKey;
387
+
388
+ const input = psbt.getInput(inputIndex);
389
+ const previousTransaction = previousTransactions[inputIndex];
390
+ const spentOutput = await verifiedSpentOutput({ input, inputIndex, previousTransaction });
391
+ await assertSpendsPathScript({ inputIndex, path, publicKey, spentOutput });
392
+
393
+ const derivation = { fingerprint, path: bip32Path(`m/${path.fullPath}`) };
394
+ if (path.purpose === 86) {
395
+ const xOnlyPublicKey = publicKey.slice(1);
396
+ // Set rather than filled in: the metadata helper keeps an existing internal key, which nothing checks
397
+ // against the spent script, while the key path has just been checked to derive that script.
398
+ psbt.updateInput(inputIndex, {
399
+ tapBip32Derivation: [[xOnlyPublicKey, { der: derivation, hashes: [] }]],
400
+ tapInternalKey: xOnlyPublicKey,
401
+ witnessUtxo: spentOutput,
402
+ });
403
+ } else {
404
+ psbt.updateInput(inputIndex, {
405
+ bip32Derivation: [[publicKey, derivation]],
406
+ nonWitnessUtxo: previousTransaction,
407
+ // The app reads witnessUtxo as a SegWit claim, so a legacy input sent with one (the API attaches it
408
+ // to every input) contradicts the pkh policy and fails with 0x6a80.
409
+ witnessUtxo: path.purpose === 44 ? undefined : spentOutput,
410
+ });
411
+ }
412
+
413
+ // Adds a nested SegWit input's redeemScript; an existing one already hashes to the checked script.
414
+ await applyMissingSpendingMetadata({ chain: Chain.Bitcoin, indexes: [inputIndex], publicKey, tx: psbt });
415
+ }
416
+
417
+ return { psbt, publicKeys };
418
+ }
419
+
420
+ /**
421
+ * Ask the device for signatures only and apply them to our own PSBT. The signer kit's own
422
+ * transaction extraction writes the PSBT format version as the transaction version and drops
423
+ * the witness of nested SegWit inputs, which invalidates the signatures.
424
+ */
425
+ async function signWithPaths({
426
+ inputUtxos,
427
+ paths,
428
+ tx,
429
+ }: {
430
+ inputUtxos?: UTXOType[];
431
+ paths: ParsedBitcoinPath[];
432
+ tx: Transaction;
433
+ }) {
434
+ const [signer, wallet, { psbt, publicKeys }] = await Promise.all([
435
+ getSigner(),
436
+ getWallet(),
437
+ addInputDerivations({ inputUtxos, paths, tx }),
438
+ ]);
439
+ const psbtV0 = psbt.toPSBT(0);
440
+ const signatures = await executeLedgerDeviceAction({
441
+ action: () => signer.signPsbt(wallet, psbtV0),
442
+ onDeviceActionState,
443
+ });
444
+
445
+ const signedInputs = new Set<number>();
446
+ for (const signature of signatures) {
447
+ if (!isPartialSignature(signature)) throw signatureError({ reason: "Unexpected MuSig2 signature" });
448
+
449
+ const { inputIndex, pubkey } = signature;
450
+ const path = paths[inputIndex];
451
+ const publicKey = publicKeys[inputIndex];
452
+ if (!(path && publicKey) || signedInputs.has(inputIndex)) {
453
+ throw signatureError({ inputIndex, reason: "Unexpected or duplicate input signature" });
454
+ }
455
+ signedInputs.add(inputIndex);
456
+
457
+ if (path.purpose === 86) {
458
+ if (signature.tapleafHash) throw signatureError({ inputIndex, reason: "Unexpected taproot script signature" });
459
+ psbt.updateInput(inputIndex, { tapKeySig: signature.signature });
460
+ continue;
461
+ }
462
+
463
+ if (hex.encode(pubkey) !== hex.encode(publicKey)) {
464
+ throw signatureError({ inputIndex, reason: "Signature public key does not match the input derivation" });
465
+ }
466
+ psbt.updateInput(inputIndex, { partialSig: [[publicKey, signature.signature]] });
467
+ }
468
+
469
+ if (signedInputs.size !== psbt.inputsLength) {
470
+ throw signatureError({ reason: `Ledger signed ${signedInputs.size} of ${psbt.inputsLength} inputs` });
471
+ }
472
+
473
+ return psbt;
474
+ }
475
+
476
+ function finalizeToHex(psbt: Transaction) {
477
+ psbt.finalize();
478
+ return psbt.hex;
479
+ }
480
+
481
+ function signTransactionHex({ tx, inputUtxos }: SignBitcoinTransactionParams) {
482
+ if (inputUtxos) validateInputCount({ count: inputUtxos.length, tx });
483
+
484
+ if (legacyClient) {
485
+ return inputUtxos
486
+ ? legacyClient.signTransaction(tx, inputUtxos)
487
+ : signLegacyPsbtTransaction({ chain: Chain.Bitcoin, legacyClient, tx });
488
+ }
489
+
490
+ const paths = Array.from({ length: tx.inputsLength }, () => configuredPath);
491
+ return signWithPaths({ inputUtxos, paths, tx }).then(finalizeToHex);
492
+ }
493
+
494
+ return {
495
+ connect: async () => {
496
+ if (legacyClient) return legacyClient.connect();
497
+ await getSigner();
498
+ },
499
+ disconnect: async () => legacyClient?.disconnect(),
500
+ getAddress: async () => {
501
+ if (legacyClient) return legacyClient.getAddress();
502
+ const signer = await getSigner();
503
+ const wallet = await getWallet();
504
+ const { address } = await executeLedgerDeviceAction({
505
+ action: () =>
506
+ signer.getWalletAddress(wallet, configuredPath.addressIndex, { change: configuredPath.change === 1 }),
507
+ onDeviceActionState,
508
+ });
509
+ if (!address) {
510
+ throw new SwapKitError("wallet_ledger_get_address_error", { path: configuredPath.fullPath });
511
+ }
512
+ return address;
513
+ },
514
+ getExtendedPublicKey: async ({
515
+ checkOnDevice = false,
516
+ path = configuredPath.accountPath,
517
+ }: GetExtendedPublicKeyParams = {}) => {
518
+ if (legacyClient) return legacyClient.getExtendedPublicKey(path);
519
+ const signer = await getSigner();
520
+ const normalizedPath = normalizePath(path);
521
+ const { extendedPublicKey } = await executeLedgerDeviceAction({
522
+ action: () => signer.getExtendedPublicKey(normalizedPath, { checkOnDevice }),
523
+ onDeviceActionState,
524
+ });
525
+ return extendedPublicKey;
526
+ },
527
+ getPublicKey,
528
+ showAddressAndPubKey: async () => {
529
+ if (legacyClient) {
530
+ const address = await legacyClient.getAddress();
531
+ return { address };
532
+ }
533
+ const signer = await getSigner();
534
+ const wallet = await getWallet();
535
+ return executeLedgerDeviceAction({
536
+ action: () =>
537
+ signer.getWalletAddress(wallet, configuredPath.addressIndex, {
538
+ change: configuredPath.change === 1,
539
+ checkOnDevice: true,
540
+ }),
541
+ onDeviceActionState,
542
+ });
543
+ },
544
+ signTransaction: async (tx: Transaction) => {
545
+ if (legacyClient) {
546
+ // The legacy app returns a finalised transaction, as the other legacy UTXO chains do.
547
+ const signedTxHex = await signLegacyPsbtTransaction({ chain: Chain.Bitcoin, legacyClient, tx });
548
+ const { Transaction: TransactionClass } = await import("@swapkit/utxo-signer");
549
+ return TransactionClass.fromRaw(hex.decode(signedTxHex), { allowUnknownOutputs: true });
550
+ }
551
+ const paths = Array.from({ length: tx.inputsLength }, () => configuredPath);
552
+ return await signWithPaths({ paths, tx });
553
+ },
554
+ signTransactionHex,
555
+ signTransactionWithMultiplePaths: async ({
556
+ derivationPaths,
557
+ inputUtxos,
558
+ tx,
559
+ }: SignBitcoinTransactionWithMultiplePathsParams) => {
560
+ if (inputUtxos) validateInputCount({ count: inputUtxos.length, tx });
561
+ const paths = validateAccountPaths({ accountPath: configuredPath.accountPath, derivationPaths, tx });
562
+
563
+ if (legacyClient) {
564
+ const inputs = inputUtxos ?? (await extractInputsFromPsbt(tx, Chain.Bitcoin));
565
+ return legacyClient.signTransactionWithMultiplePaths(
566
+ tx,
567
+ inputs,
568
+ paths.map((path) => `m/${path.fullPath}`),
569
+ );
570
+ }
571
+
572
+ return signWithPaths({ inputUtxos, paths, tx }).then(finalizeToHex);
573
+ },
574
+ };
575
+ }