@ledgerhq/psbtv2 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/LICENSE.txt +21 -0
  3. package/README.md +91 -0
  4. package/jest.config.js +26 -0
  5. package/lib/buffertools.d.ts +29 -0
  6. package/lib/buffertools.d.ts.map +1 -0
  7. package/lib/buffertools.js +129 -0
  8. package/lib/buffertools.js.map +1 -0
  9. package/lib/index.d.ts +4 -0
  10. package/lib/index.d.ts.map +1 -0
  11. package/lib/index.js +18 -0
  12. package/lib/index.js.map +1 -0
  13. package/lib/psbtParsing.d.ts +15 -0
  14. package/lib/psbtParsing.d.ts.map +1 -0
  15. package/lib/psbtParsing.js +52 -0
  16. package/lib/psbtParsing.js.map +1 -0
  17. package/lib/psbtv2.d.ts +200 -0
  18. package/lib/psbtv2.d.ts.map +1 -0
  19. package/lib/psbtv2.js +647 -0
  20. package/lib/psbtv2.js.map +1 -0
  21. package/lib-es/buffertools.d.ts +29 -0
  22. package/lib-es/buffertools.d.ts.map +1 -0
  23. package/lib-es/buffertools.js +119 -0
  24. package/lib-es/buffertools.js.map +1 -0
  25. package/lib-es/index.d.ts +4 -0
  26. package/lib-es/index.d.ts.map +1 -0
  27. package/lib-es/index.js +4 -0
  28. package/lib-es/index.js.map +1 -0
  29. package/lib-es/psbtParsing.d.ts +15 -0
  30. package/lib-es/psbtParsing.d.ts.map +1 -0
  31. package/lib-es/psbtParsing.js +48 -0
  32. package/lib-es/psbtParsing.js.map +1 -0
  33. package/lib-es/psbtv2.d.ts +200 -0
  34. package/lib-es/psbtv2.d.ts.map +1 -0
  35. package/lib-es/psbtv2.js +641 -0
  36. package/lib-es/psbtv2.js.map +1 -0
  37. package/package.json +78 -0
  38. package/src/buffertools.test.ts +116 -0
  39. package/src/buffertools.ts +137 -0
  40. package/src/fromV0.test.ts +577 -0
  41. package/src/index.ts +3 -0
  42. package/src/psbtParsing.test.ts +86 -0
  43. package/src/psbtParsing.ts +51 -0
  44. package/src/psbtv2.test.ts +441 -0
  45. package/src/psbtv2.ts +740 -0
  46. package/tsconfig.json +9 -0
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Normalize possible PSBT inputs: base64 (with whitespace/URL-safe), or raw hex.
3
+ *
4
+ * This is a low-level helper; callers that need strict validation and
5
+ * user-facing error messages should use `parsePsbt` instead.
6
+ */
7
+ export function normalizeToBuffer(psbtMaybe: string): Buffer | null {
8
+ if (!psbtMaybe) return null;
9
+ const s = psbtMaybe.trim();
10
+
11
+ // If hex (even length, only [0-9a-fA-F])  treat as hex
12
+ if (/^[0-9a-fA-F]+$/.test(s) && s.length % 2 === 0) {
13
+ try {
14
+ return Buffer.from(s, "hex");
15
+ } catch {
16
+ /* ignore and fall through to base64 */
17
+ }
18
+ }
19
+
20
+ // Treat as base64: strip whitespace and convert URL-safe to standard
21
+ const b64 = s.replaceAll(/\s+/g, "").replaceAll("-", "+").replaceAll("_", "/");
22
+ // pad base64
23
+ const pad = b64.length % 4;
24
+ const padded = pad ? b64 + "=".repeat(4 - pad) : b64;
25
+
26
+ try {
27
+ return Buffer.from(padded, "base64");
28
+ } catch {
29
+ return null;
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Parse a PSBT string into a Buffer, throwing on clearly invalid inputs.
35
+ *
36
+ * This wraps `normalizeToBuffer` but preserves existing error messages used in
37
+ * ledger-live when encountering invalid PSBT payloads.
38
+ */
39
+ export function parsePsbt(psbt: string): Buffer {
40
+ const buf = normalizeToBuffer(psbt);
41
+
42
+ if (!buf) {
43
+ throw new Error("Invalid PSBT: not valid base64");
44
+ }
45
+
46
+ if (!buf.length) {
47
+ throw new Error("Invalid PSBT: empty buffer");
48
+ }
49
+
50
+ return buf;
51
+ }
@@ -0,0 +1,441 @@
1
+ import { NoSuchEntry, PsbtV2, psbtGlobal, psbtIn, parseBip32Path } from "./psbtv2";
2
+ import { Psbt, networks } from "bitcoinjs-lib";
3
+
4
+ describe("PsbtV2", () => {
5
+ it("deserializes a psbt and reserializes it unchanged", async () => {
6
+ const psbtBuf = Buffer.from(
7
+ "cHNidP8BAAoBAAAAAAAAAAAAAQIEAgAAAAEDBAAAAAABBAECAQUBAgH7BAIAAAAAAQBxAgAAAAGTarLgEHL3k8/kyXdU3hth/gPn22U2yLLyHdC1dCxIRQEAAAAA/v///wLe4ccAAAAAABYAFOt418QL8QY7Dj/OKcNWW2ichVmrECcAAAAAAAAWABQjGNZvhP71xIdfkzsDjcY4MfjaE/mXHgABAR8QJwAAAAAAABYAFCMY1m+E/vXEh1+TOwONxjgx+NoTIgYDRV7nztyXsLpDW4AGb8ksljo0xgAxeYHRNTMMTuQ6x6MY9azC/VQAAIABAACAAAAAgAAAAAABAAAAAQ4gniz+J/Cth7eKI31ddAXUowZmyjYdWFpGew3+QiYrTbQBDwQBAAAAARAE/f///wESBAAAAAAAAQBxAQAAAAEORx706Sway1HvyGYPjT9pk26pybK/9y/5vIHFHvz0ZAEAAAAAAAAAAAJgrgoAAAAAABYAFDXG4N1tPISxa6iF3Kc6yGPQtZPsrwYyAAAAAAAWABTcKG4M0ua9N86+nsNJ+18IkFZy/AAAAAABAR9grgoAAAAAABYAFDXG4N1tPISxa6iF3Kc6yGPQtZPsIgYCcbW3ea2HCDhYd5e89vDHrsWr52pwnXJPSNLibPh08KAY9azC/VQAAIABAACAAAAAgAEAAAAAAAAAAQ4gr7+uBlkPdB/xr1m2rEYRJjNqTEqC21U99v76tzesM/MBDwQAAAAAARAE/f///wESBAAAAAAAIgICKexHcnEx7SWIogxG7amrt9qm9J/VC6/nC5xappYcTswY9azC/VQAAIABAACAAAAAgAEAAAAKAAAAAQMIqDoGAAAAAAABBBYAFOs4+puBKPgfJule2wxf+uqDaQ/kAAEDCOCTBAAAAAAAAQQiACA/qWbJ3c3C/ZbkpeG8dlufr2zos+tPEQSq1r33cyTlvgA=",
8
+ "base64",
9
+ );
10
+
11
+ const psbt = new PsbtV2();
12
+ psbt.deserialize(psbtBuf);
13
+
14
+ expect(psbt.serialize()).toEqual(psbtBuf);
15
+ });
16
+
17
+ describe("getPsbtVersionNumber", () => {
18
+ it("should return 2 for a PSBTv2 buffer", () => {
19
+ // Create a PSBTv2
20
+ const psbtv2 = new PsbtV2();
21
+ psbtv2.setGlobalTxVersion(2);
22
+ psbtv2.setGlobalInputCount(0);
23
+ psbtv2.setGlobalOutputCount(0);
24
+ psbtv2.setGlobalPsbtVersion(2);
25
+
26
+ const buffer = psbtv2.serialize();
27
+ const version = PsbtV2.getPsbtVersionNumber(buffer);
28
+
29
+ expect(version).toBe(2);
30
+ });
31
+
32
+ it("should return 0 for a PSBTv0 buffer (no version field)", () => {
33
+ // Create a PSBTv0 using bitcoinjs-lib
34
+ const psbtv0 = new Psbt({ network: networks.testnet });
35
+ psbtv0.addInput({
36
+ //@ts-expect-error TransactionInput interface is not declared correctly in bip174 lib
37
+ hash: Buffer.alloc(32, 0),
38
+ index: 0,
39
+ witnessUtxo: {
40
+ script: Buffer.from("0014" + "00".repeat(20), "hex"),
41
+ value: 100000,
42
+ },
43
+ });
44
+ psbtv0.addOutput({
45
+ script: Buffer.from("0014" + "11".repeat(20), "hex"),
46
+ value: 90000,
47
+ });
48
+
49
+ const buffer = psbtv0.toBuffer();
50
+ const version = PsbtV2.getPsbtVersionNumber(buffer);
51
+
52
+ expect(version).toBe(0);
53
+ });
54
+
55
+ it("should return 0 for a minimal PSBTv0", () => {
56
+ // Minimal PSBTv0: magic bytes + empty global map (0x00) + empty input/output
57
+ const psbtv0 = new Psbt({ network: networks.testnet });
58
+ psbtv0.addInput({
59
+ //@ts-expect-error TransactionInput interface is not declared correctly in bip174 lib
60
+ hash: Buffer.alloc(32, 1),
61
+ index: 0,
62
+ witnessUtxo: {
63
+ script: Buffer.from("001400000000000000000000000000000000000000000000", "hex"),
64
+ value: 1000,
65
+ },
66
+ });
67
+ psbtv0.addOutput({
68
+ script: Buffer.from("001400000000000000000000000000000000000000000000", "hex"),
69
+ value: 900,
70
+ });
71
+
72
+ const buffer = psbtv0.toBuffer();
73
+ const version = PsbtV2.getPsbtVersionNumber(buffer);
74
+
75
+ expect(version).toBe(0);
76
+ });
77
+
78
+ it("should correctly identify version in a converted PSBTv2", () => {
79
+ // Create a PSBTv0 and convert it to PSBTv2
80
+ const psbtv0 = new Psbt({ network: networks.testnet });
81
+ psbtv0.addInput({
82
+ //@ts-expect-error TransactionInput interface is not declared correctly in bip174 lib
83
+ hash: Buffer.alloc(32, 0),
84
+ index: 0,
85
+ witnessUtxo: {
86
+ script: Buffer.from("0014" + "00".repeat(20), "hex"),
87
+ value: 100000,
88
+ },
89
+ });
90
+ psbtv0.addOutput({
91
+ script: Buffer.from("0014" + "11".repeat(20), "hex"),
92
+ value: 90000,
93
+ });
94
+
95
+ const psbtv2 = PsbtV2.fromV0(psbtv0.toBuffer(), true);
96
+ const buffer = psbtv2.serialize();
97
+ const version = PsbtV2.getPsbtVersionNumber(buffer);
98
+
99
+ expect(version).toBe(2);
100
+ });
101
+
102
+ it("should handle the version field from the test PSBT", () => {
103
+ const psbtBuf = Buffer.from(
104
+ "cHNidP8BAAoBAAAAAAAAAAAAAQIEAgAAAAEDBAAAAAABBAECAQUBAgH7BAIAAAAAAQBxAgAAAAGTarLgEHL3k8/kyXdU3hth/gPn22U2yLLyHdC1dCxIRQEAAAAA/v///wLe4ccAAAAAABYAFOt418QL8QY7Dj/OKcNWW2ichVmrECcAAAAAAAAWABQjGNZvhP71xIdfkzsDjcY4MfjaE/mXHgABAR8QJwAAAAAAABYAFCMY1m+E/vXEh1+TOwONxjgx+NoTIgYDRV7nztyXsLpDW4AGb8ksljo0xgAxeYHRNTMMTuQ6x6MY9azC/VQAAIABAACAAAAAgAAAAAABAAAAAQ4gniz+J/Cth7eKI31ddAXUowZmyjYdWFpGew3+QiYrTbQBDwQBAAAAARAE/f///wESBAAAAAAAAQBxAQAAAAEORx706Sway1HvyGYPjT9pk26pybK/9y/5vIHFHvz0ZAEAAAAAAAAAAAJgrgoAAAAAABYAFDXG4N1tPISxa6iF3Kc6yGPQtZPsrwYyAAAAAAAWABTcKG4M0ua9N86+nsNJ+18IkFZy/AAAAAABAR9grgoAAAAAABYAFDXG4N1tPISxa6iF3Kc6yGPQtZPsIgYCcbW3ea2HCDhYd5e89vDHrsWr52pwnXJPSNLibPh08KAY9azC/VQAAIABAACAAAAAgAEAAAAAAAAAAQ4gr7+uBlkPdB/xr1m2rEYRJjNqTEqC21U99v76tzesM/MBDwQAAAAAARAE/f///wESBAAAAAAAIgICKexHcnEx7SWIogxG7amrt9qm9J/VC6/nC5xappYcTswY9azC/VQAAIABAACAAAAAgAEAAAAKAAAAAQMIqDoGAAAAAAABBBYAFOs4+puBKPgfJule2wxf+uqDaQ/kAAEDCOCTBAAAAAAAAQQiACA/qWbJ3c3C/ZbkpeG8dlufr2zos+tPEQSq1r33cyTlvgA=",
105
+ "base64",
106
+ );
107
+
108
+ const version = PsbtV2.getPsbtVersionNumber(psbtBuf);
109
+ expect(version).toBe(2);
110
+ });
111
+
112
+ it("should return 0 when PSBTv2 has no explicit VERSION field", () => {
113
+ const psbtv2 = new PsbtV2();
114
+ const buffer = psbtv2.serialize();
115
+
116
+ const version = PsbtV2.getPsbtVersionNumber(buffer);
117
+
118
+ expect(version).toBe(0);
119
+ });
120
+
121
+ it("should handle global keys with key data and empty values", () => {
122
+ const magic = Buffer.from([0x70, 0x73, 0x62, 0x74, 0xff]);
123
+
124
+ // Manually craft a minimal PSBT-like buffer:
125
+ // - One global key with keyLen = 2 (type + 1 byte keyData)
126
+ // - valueLen = 0 (empty value)
127
+ // - followed by keyLen = 0 as global-map terminator
128
+ const body = Buffer.from([
129
+ 0x02, // keyLen = 2
130
+ psbtGlobal.TX_VERSION, // keyType
131
+ 0x99, // keyData (ignored by version parser)
132
+ 0x00, // valueLen = 0
133
+ 0x00, // next keyLen = 0 -> end of global map
134
+ ]);
135
+
136
+ const buffer = Buffer.concat([magic, body]);
137
+
138
+ const version = PsbtV2.getPsbtVersionNumber(buffer);
139
+
140
+ expect(version).toBe(0);
141
+ });
142
+ });
143
+
144
+ describe("optional getters and taproot helpers", () => {
145
+ it("should return undefined for optional globals and inputs when not set", () => {
146
+ const psbtv2 = new PsbtV2();
147
+
148
+ expect(psbtv2.getGlobalFallbackLocktime()).toBeUndefined();
149
+
150
+ // Ensure input map exists but no optional fields are set
151
+ psbtv2.setInputSequence(0, 0xfffffffe);
152
+
153
+ expect(psbtv2.getInputSighashType(0)).toBeUndefined();
154
+ expect(psbtv2.getInputTapKeySig(0)).toBeUndefined();
155
+
156
+ const witnessUtxo = psbtv2.getInputWitnessUtxo(0);
157
+ expect(witnessUtxo).toBeUndefined();
158
+
159
+ const bip32Deriv = psbtv2.getInputBip32Derivation(0, Buffer.alloc(33, 2));
160
+ expect(bip32Deriv).toBeUndefined();
161
+ });
162
+
163
+ it("should encode and decode taproot BIP32 derivation for inputs and outputs", () => {
164
+ const psbtv2 = new PsbtV2();
165
+
166
+ const pubkey = Buffer.alloc(32, 1);
167
+ const hashes = [Buffer.alloc(32, 2), Buffer.alloc(32, 3)];
168
+ const masterFingerprint = Buffer.from("01020304", "hex");
169
+ const path = [0x8000002c, 0x80000001, 0x80000000, 0, 5];
170
+
171
+ psbtv2.setInputTapBip32Derivation(0, pubkey, hashes, masterFingerprint, path);
172
+ psbtv2.setOutputTapBip32Derivation(0, pubkey, hashes, masterFingerprint, path);
173
+
174
+ const inputDeriv = psbtv2.getInputTapBip32Derivation(0, pubkey);
175
+ expect(inputDeriv.hashes).toHaveLength(2);
176
+ expect(inputDeriv.hashes[0]).toEqual(hashes[0]);
177
+ expect(inputDeriv.hashes[1]).toEqual(hashes[1]);
178
+ expect(inputDeriv.masterFingerprint).toEqual(masterFingerprint);
179
+ expect(inputDeriv.path).toEqual(path);
180
+
181
+ const outputDeriv = psbtv2.getOutputTapBip32Derivation(0, pubkey);
182
+ expect(outputDeriv.hashes).toHaveLength(2);
183
+ expect(outputDeriv.hashes[0]).toEqual(hashes[0]);
184
+ expect(outputDeriv.hashes[1]).toEqual(hashes[1]);
185
+ expect(outputDeriv.masterFingerprint).toEqual(masterFingerprint);
186
+ expect(outputDeriv.path).toEqual(path);
187
+
188
+ const keyDatas = psbtv2.getInputKeyDatas(0, psbtIn.TAP_BIP32_DERIVATION);
189
+ expect(keyDatas).toHaveLength(1);
190
+ expect(keyDatas[0]).toEqual(pubkey);
191
+ });
192
+
193
+ it("should delete specified input entries and throw when accessing them afterwards", () => {
194
+ const psbtv2 = new PsbtV2();
195
+
196
+ const pubkey = Buffer.alloc(32, 9);
197
+ const hashes = [Buffer.alloc(32, 4)];
198
+ const masterFingerprint = Buffer.from("0a0b0c0d", "hex");
199
+ const path = [0x8000002c, 0x80000001, 0x80000000, 1, 0];
200
+
201
+ psbtv2.setInputTapBip32Derivation(0, pubkey, hashes, masterFingerprint, path);
202
+
203
+ expect(() => psbtv2.getInputTapBip32Derivation(0, pubkey)).not.toThrow();
204
+
205
+ psbtv2.deleteInputEntries(0, [psbtIn.TAP_BIP32_DERIVATION]);
206
+
207
+ expect(() => psbtv2.getInputTapBip32Derivation(0, pubkey)).toThrow(NoSuchEntry);
208
+ });
209
+
210
+ it("should set and get txModifiable global flag", () => {
211
+ const psbtv2 = new PsbtV2();
212
+
213
+ expect(psbtv2.getGlobalTxModifiable()).toBeUndefined();
214
+
215
+ const flag = Buffer.from([0x01]);
216
+ psbtv2.setGlobalTxModifiable(flag);
217
+
218
+ expect(psbtv2.getGlobalTxModifiable()).toEqual(flag);
219
+ });
220
+
221
+ it("should default input sequence to 0xffffffff when not set", () => {
222
+ const psbtv2 = new PsbtV2();
223
+
224
+ const prevTxId = Buffer.alloc(32, 0);
225
+ psbtv2.setInputPreviousTxId(0, prevTxId);
226
+
227
+ expect(psbtv2.getInputSequence(0)).toBe(0xffffffff);
228
+ });
229
+
230
+ it("should throw when accessing required input data for a non-existent map", () => {
231
+ const psbtv2 = new PsbtV2();
232
+
233
+ expect(() => psbtv2.getInputFinalScriptwitness(0)).toThrow("No such map");
234
+ });
235
+
236
+ it("should support taproot derivation with empty hashes vector", () => {
237
+ const psbtv2 = new PsbtV2();
238
+
239
+ const pubkey = Buffer.alloc(32, 7);
240
+ const hashes: Buffer[] = [];
241
+ const masterFingerprint = Buffer.from("01020304", "hex");
242
+ const path = [0x8000002c, 0x80000001];
243
+
244
+ psbtv2.setInputTapBip32Derivation(0, pubkey, hashes, masterFingerprint, path);
245
+
246
+ const deriv = psbtv2.getInputTapBip32Derivation(0, pubkey);
247
+ expect(deriv.hashes).toHaveLength(0);
248
+ expect(deriv.masterFingerprint).toEqual(masterFingerprint);
249
+ expect(deriv.path).toEqual(path);
250
+ });
251
+
252
+ it("should validate BIP32 input pubkey length", () => {
253
+ const psbtv2 = new PsbtV2();
254
+
255
+ const invalidPubkey = Buffer.alloc(32, 1); // must be 33 bytes
256
+ const masterFingerprint = Buffer.from("01020304", "hex");
257
+
258
+ expect(() =>
259
+ psbtv2.setInputBip32Derivation(0, invalidPubkey, masterFingerprint, [0x8000002c]),
260
+ ).toThrow("Invalid pubkey length: 32");
261
+ });
262
+
263
+ it("should validate taproot BIP32 pubkey length", () => {
264
+ const psbtv2 = new PsbtV2();
265
+
266
+ const invalidPubkey = Buffer.alloc(33, 1); // must be 32 bytes
267
+ const masterFingerprint = Buffer.from("01020304", "hex");
268
+
269
+ expect(() =>
270
+ psbtv2.setInputTapBip32Derivation(0, invalidPubkey, [], masterFingerprint, [0]),
271
+ ).toThrow("Invalid pubkey length: 33");
272
+ });
273
+
274
+ it("should parse and validate BIP32 string paths", () => {
275
+ const validPath = "m/84'/1'/0'/0/0";
276
+ expect(parseBip32Path(validPath)).toEqual([
277
+ 0x80000054, // 84'
278
+ 0x80000001, // 1'
279
+ 0x80000000, // 0'
280
+ 0, // 0
281
+ 0, // 0
282
+ ]);
283
+
284
+ const invalidPath = "m/84'/1'/foo/0/0";
285
+ expect(() => parseBip32Path(invalidPath)).toThrow(/Invalid BIP32 path segment/);
286
+ });
287
+
288
+ it("should set and get taproot key signature", () => {
289
+ const psbtv2 = new PsbtV2();
290
+
291
+ const sig = Buffer.from("aa", "hex");
292
+ psbtv2.setInputTapKeySig(0, sig);
293
+
294
+ const storedSig = psbtv2.getInputTapKeySig(0);
295
+ expect(storedSig).toEqual(sig);
296
+ });
297
+
298
+ it("should set and get output redeem script", () => {
299
+ const psbtv2 = new PsbtV2();
300
+
301
+ const redeemScript = Buffer.from("0014" + "22".repeat(20), "hex");
302
+
303
+ psbtv2.setOutputRedeemScript(0, redeemScript);
304
+
305
+ const outRedeemScript = psbtv2.getOutputRedeemScript(0);
306
+ expect(outRedeemScript).toEqual(redeemScript);
307
+ });
308
+
309
+ it("should copy psbtv2 state deeply", () => {
310
+ const source = new PsbtV2();
311
+ source.setGlobalTxVersion(2);
312
+ source.setGlobalInputCount(1);
313
+ source.setGlobalOutputCount(1);
314
+ source.setGlobalPsbtVersion(2);
315
+
316
+ const prevTxId = Buffer.alloc(32, 1);
317
+ const script = Buffer.from("0014" + "00".repeat(20), "hex");
318
+
319
+ source.setInputPreviousTxId(0, prevTxId);
320
+ source.setInputSequence(0, 0xfffffffe);
321
+ source.setInputWitnessUtxo(0, Buffer.alloc(8, 1), script);
322
+
323
+ source.setOutputAmount(0, 100000);
324
+ source.setOutputScript(0, script);
325
+
326
+ const target = new PsbtV2();
327
+ source.copy(target);
328
+
329
+ expect(target.getGlobalTxVersion()).toBe(2);
330
+ expect(target.getGlobalInputCount()).toBe(1);
331
+ expect(target.getGlobalOutputCount()).toBe(1);
332
+ expect(target.getGlobalPsbtVersion()).toBe(2);
333
+
334
+ expect(target.getInputPreviousTxid(0)).toEqual(prevTxId);
335
+ expect(target.getInputSequence(0)).toBe(0xfffffffe);
336
+
337
+ const witness = target.getInputWitnessUtxo(0);
338
+ expect(witness).toBeDefined();
339
+ expect(witness!.scriptPubKey).toEqual(script);
340
+
341
+ expect(target.getOutputAmount(0)).toBe(100000);
342
+ expect(target.getOutputScript(0)).toEqual(script);
343
+
344
+ // Mutate source and ensure target is unchanged (deep copy)
345
+ source.setOutputAmount(0, 1);
346
+ expect(target.getOutputAmount(0)).toBe(100000);
347
+ });
348
+
349
+ it("should throw when deserializing buffer with invalid magic bytes", () => {
350
+ const psbtv2 = new PsbtV2();
351
+
352
+ const invalid = Buffer.alloc(5, 0);
353
+
354
+ expect(() => psbtv2.deserialize(invalid)).toThrow("Invalid magic bytes");
355
+ });
356
+
357
+ it("should delete only matching input entries and preserve others", () => {
358
+ const psbtv2 = new PsbtV2();
359
+
360
+ // Add multiple different entry types to input 0
361
+ const pubkey1 = Buffer.alloc(32, 1);
362
+ const pubkey2 = Buffer.alloc(33, 2);
363
+ const hashes = [Buffer.alloc(32, 3)];
364
+ const masterFingerprint = Buffer.from("01020304", "hex");
365
+ const path = [0x8000002c, 0x80000001, 0x80000000, 0, 0];
366
+
367
+ // Set TAP_BIP32_DERIVATION (will be deleted)
368
+ psbtv2.setInputTapBip32Derivation(0, pubkey1, hashes, masterFingerprint, path);
369
+
370
+ // Set BIP32_DERIVATION (will be preserved)
371
+ psbtv2.setInputBip32Derivation(0, pubkey2, masterFingerprint, path);
372
+
373
+ // Set TAP_KEY_SIG (will be deleted)
374
+ psbtv2.setInputTapKeySig(0, Buffer.from("aa", "hex"));
375
+
376
+ // Verify both entries exist before deletion
377
+ expect(() => psbtv2.getInputTapBip32Derivation(0, pubkey1)).not.toThrow();
378
+ expect(() => psbtv2.getInputBip32Derivation(0, pubkey2)).not.toThrow();
379
+ expect(psbtv2.getInputTapKeySig(0)).toBeDefined();
380
+
381
+ // Delete only TAP-related entries
382
+ psbtv2.deleteInputEntries(0, [psbtIn.TAP_BIP32_DERIVATION, psbtIn.TAP_KEY_SIG]);
383
+
384
+ // TAP entries should be gone
385
+ expect(() => psbtv2.getInputTapBip32Derivation(0, pubkey1)).toThrow(NoSuchEntry);
386
+ expect(psbtv2.getInputTapKeySig(0)).toBeUndefined();
387
+
388
+ // BIP32_DERIVATION should still exist
389
+ expect(() => psbtv2.getInputBip32Derivation(0, pubkey2)).not.toThrow();
390
+ const deriv = psbtv2.getInputBip32Derivation(0, pubkey2);
391
+ expect(deriv).toBeDefined();
392
+ expect(deriv!.masterFingerprint).toEqual(masterFingerprint);
393
+ expect(deriv!.path).toEqual(path);
394
+ });
395
+
396
+ it("should return multiple key datas when multiple entries of same type exist", () => {
397
+ const psbtv2 = new PsbtV2();
398
+
399
+ // Add multiple TAP_BIP32_DERIVATION entries with different pubkeys
400
+ const pubkey1 = Buffer.alloc(32, 1);
401
+ const pubkey2 = Buffer.alloc(32, 2);
402
+ const pubkey3 = Buffer.alloc(32, 3);
403
+ const hashes = [Buffer.alloc(32, 4)];
404
+ const masterFingerprint = Buffer.from("0a0b0c0d", "hex");
405
+ const path = [0x8000002c, 0x80000001, 0x80000000, 0, 0];
406
+
407
+ psbtv2.setInputTapBip32Derivation(0, pubkey1, hashes, masterFingerprint, path);
408
+ psbtv2.setInputTapBip32Derivation(0, pubkey2, hashes, masterFingerprint, path);
409
+ psbtv2.setInputTapBip32Derivation(0, pubkey3, hashes, masterFingerprint, path);
410
+
411
+ // Get all key datas for TAP_BIP32_DERIVATION
412
+ const keyDatas = psbtv2.getInputKeyDatas(0, psbtIn.TAP_BIP32_DERIVATION);
413
+
414
+ expect(keyDatas).toHaveLength(3);
415
+ expect(keyDatas).toContainEqual(pubkey1);
416
+ expect(keyDatas).toContainEqual(pubkey2);
417
+ expect(keyDatas).toContainEqual(pubkey3);
418
+
419
+ // Verify each one can be retrieved
420
+ expect(() => psbtv2.getInputTapBip32Derivation(0, pubkey1)).not.toThrow();
421
+ expect(() => psbtv2.getInputTapBip32Derivation(0, pubkey2)).not.toThrow();
422
+ expect(() => psbtv2.getInputTapBip32Derivation(0, pubkey3)).not.toThrow();
423
+ });
424
+
425
+ it("should return empty array when no keys match the requested type", () => {
426
+ const psbtv2 = new PsbtV2();
427
+
428
+ // Set some input data that's not TAP_BIP32_DERIVATION
429
+ const pubkey = Buffer.alloc(33, 1);
430
+ const masterFingerprint = Buffer.from("01020304", "hex");
431
+ const path = [0x8000002c];
432
+
433
+ psbtv2.setInputBip32Derivation(0, pubkey, masterFingerprint, path);
434
+
435
+ // Try to get TAP_BIP32_DERIVATION keys (should be empty)
436
+ const keyDatas = psbtv2.getInputKeyDatas(0, psbtIn.TAP_BIP32_DERIVATION);
437
+
438
+ expect(keyDatas).toHaveLength(0);
439
+ });
440
+ });
441
+ });