@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
package/src/psbtv2.ts ADDED
@@ -0,0 +1,740 @@
1
+ /* eslint-disable @typescript-eslint/no-non-null-assertion */
2
+ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */
3
+ import { Psbt, Transaction } from "bitcoinjs-lib";
4
+ import { BufferReader, BufferWriter, unsafeFrom64bitLE, unsafeTo64bitLE } from "./buffertools";
5
+
6
+ export enum psbtGlobal {
7
+ TX_VERSION = 0x02,
8
+ FALLBACK_LOCKTIME = 0x03,
9
+ INPUT_COUNT = 0x04,
10
+ OUTPUT_COUNT = 0x05,
11
+ TX_MODIFIABLE = 0x06,
12
+ VERSION = 0xfb,
13
+ }
14
+ export enum psbtIn {
15
+ NON_WITNESS_UTXO = 0x00,
16
+ WITNESS_UTXO = 0x01,
17
+ PARTIAL_SIG = 0x02,
18
+ SIGHASH_TYPE = 0x03,
19
+ REDEEM_SCRIPT = 0x04,
20
+ BIP32_DERIVATION = 0x06,
21
+ FINAL_SCRIPTSIG = 0x07,
22
+ FINAL_SCRIPTWITNESS = 0x08,
23
+ PREVIOUS_TXID = 0x0e,
24
+ OUTPUT_INDEX = 0x0f,
25
+ SEQUENCE = 0x10,
26
+ TAP_KEY_SIG = 0x13,
27
+ TAP_BIP32_DERIVATION = 0x16,
28
+ }
29
+ export enum psbtOut {
30
+ REDEEM_SCRIPT = 0x00,
31
+ BIP_32_DERIVATION = 0x02,
32
+ AMOUNT = 0x03,
33
+ SCRIPT = 0x04,
34
+ TAP_BIP32_DERIVATION = 0x07,
35
+ }
36
+
37
+ const PSBT_MAGIC_BYTES = Buffer.from([0x70, 0x73, 0x62, 0x74, 0xff]);
38
+
39
+ export class NoSuchEntry extends Error {}
40
+
41
+ /**
42
+ * Implements Partially Signed Bitcoin Transaction version 2, BIP370, as
43
+ * documented at https://github.com/bitcoin/bips/blob/master/bip-0370.mediawiki
44
+ * and https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki
45
+ *
46
+ * A psbt is a data structure that can carry all relevant information about a
47
+ * transaction through all stages of the signing process. From constructing an
48
+ * unsigned transaction to extracting the final serialized transaction ready for
49
+ * broadcast.
50
+ *
51
+ * This implementation is limited to what's needed in ledgerjs to carry out its
52
+ * duties, which means that support for features like multisig or taproot script
53
+ * path spending are not implemented. Specifically, it supports p2pkh,
54
+ * p2wpkhWrappedInP2sh, p2wpkh and p2tr key path spending.
55
+ *
56
+ * This class is made purposefully dumb, so it's easy to add support for
57
+ * complemantary fields as needed in the future.
58
+ */
59
+ export class PsbtV2 {
60
+ protected globalMap: Map<string, Buffer> = new Map();
61
+ protected inputMaps: Map<string, Buffer>[] = [];
62
+ protected outputMaps: Map<string, Buffer>[] = [];
63
+
64
+ setGlobalTxVersion(version: number) {
65
+ this.setGlobal(psbtGlobal.TX_VERSION, uint32LE(version));
66
+ }
67
+ getGlobalTxVersion(): number {
68
+ return this.getGlobal(psbtGlobal.TX_VERSION).readUInt32LE(0);
69
+ }
70
+ setGlobalFallbackLocktime(locktime: number) {
71
+ this.setGlobal(psbtGlobal.FALLBACK_LOCKTIME, uint32LE(locktime));
72
+ }
73
+ getGlobalFallbackLocktime(): number | undefined {
74
+ return this.getGlobalOptional(psbtGlobal.FALLBACK_LOCKTIME)?.readUInt32LE(0);
75
+ }
76
+ setGlobalInputCount(inputCount: number) {
77
+ this.setGlobal(psbtGlobal.INPUT_COUNT, varint(inputCount));
78
+ }
79
+ getGlobalInputCount(): number {
80
+ return fromVarint(this.getGlobal(psbtGlobal.INPUT_COUNT));
81
+ }
82
+ setGlobalOutputCount(outputCount: number) {
83
+ this.setGlobal(psbtGlobal.OUTPUT_COUNT, varint(outputCount));
84
+ }
85
+ getGlobalOutputCount(): number {
86
+ return fromVarint(this.getGlobal(psbtGlobal.OUTPUT_COUNT));
87
+ }
88
+ setGlobalTxModifiable(byte: Buffer) {
89
+ this.setGlobal(psbtGlobal.TX_MODIFIABLE, byte);
90
+ }
91
+ getGlobalTxModifiable(): Buffer | undefined {
92
+ return this.getGlobalOptional(psbtGlobal.TX_MODIFIABLE);
93
+ }
94
+ setGlobalPsbtVersion(psbtVersion: number) {
95
+ this.setGlobal(psbtGlobal.VERSION, uint32LE(psbtVersion));
96
+ }
97
+ getGlobalPsbtVersion(): number {
98
+ return this.getGlobal(psbtGlobal.VERSION).readUInt32LE(0);
99
+ }
100
+
101
+ setInputNonWitnessUtxo(inputIndex: number, transaction: Buffer) {
102
+ this.setInput(inputIndex, psbtIn.NON_WITNESS_UTXO, b(), transaction);
103
+ }
104
+ getInputNonWitnessUtxo(inputIndex: number): Buffer | undefined {
105
+ return this.getInputOptional(inputIndex, psbtIn.NON_WITNESS_UTXO, b());
106
+ }
107
+ setInputWitnessUtxo(inputIndex: number, amount: Buffer, scriptPubKey: Buffer) {
108
+ const buf = new BufferWriter();
109
+ buf.writeSlice(amount);
110
+ buf.writeVarSlice(scriptPubKey);
111
+ this.setInput(inputIndex, psbtIn.WITNESS_UTXO, b(), buf.buffer());
112
+ }
113
+ getInputWitnessUtxo(inputIndex: number): { amount: Buffer; scriptPubKey: Buffer } | undefined {
114
+ const utxo = this.getInputOptional(inputIndex, psbtIn.WITNESS_UTXO, b());
115
+ if (!utxo) return undefined;
116
+ const buf = new BufferReader(utxo);
117
+ return { amount: buf.readSlice(8), scriptPubKey: buf.readVarSlice() };
118
+ }
119
+ setInputPartialSig(inputIndex: number, pubkey: Buffer, signature: Buffer) {
120
+ this.setInput(inputIndex, psbtIn.PARTIAL_SIG, pubkey, signature);
121
+ }
122
+ getInputPartialSig(inputIndex: number, pubkey: Buffer): Buffer | undefined {
123
+ return this.getInputOptional(inputIndex, psbtIn.PARTIAL_SIG, pubkey);
124
+ }
125
+ setInputSighashType(inputIndex: number, sigHashtype: number) {
126
+ this.setInput(inputIndex, psbtIn.SIGHASH_TYPE, b(), uint32LE(sigHashtype));
127
+ }
128
+ getInputSighashType(inputIndex: number): number | undefined {
129
+ const result = this.getInputOptional(inputIndex, psbtIn.SIGHASH_TYPE, b());
130
+ if (!result) return undefined;
131
+ return result.readUInt32LE(0);
132
+ }
133
+ setInputRedeemScript(inputIndex: number, redeemScript: Buffer) {
134
+ this.setInput(inputIndex, psbtIn.REDEEM_SCRIPT, b(), redeemScript);
135
+ }
136
+ getInputRedeemScript(inputIndex: number): Buffer | undefined {
137
+ return this.getInputOptional(inputIndex, psbtIn.REDEEM_SCRIPT, b());
138
+ }
139
+ setInputBip32Derivation(
140
+ inputIndex: number,
141
+ pubkey: Buffer,
142
+ masterFingerprint: Buffer,
143
+ path: number[],
144
+ ) {
145
+ if (pubkey.length != 33) throw new Error("Invalid pubkey length: " + pubkey.length);
146
+ this.setInput(
147
+ inputIndex,
148
+ psbtIn.BIP32_DERIVATION,
149
+ pubkey,
150
+ this.encodeBip32Derivation(masterFingerprint, path),
151
+ );
152
+ }
153
+ getInputBip32Derivation(
154
+ inputIndex: number,
155
+ pubkey: Buffer,
156
+ ): { masterFingerprint: Buffer; path: number[] } | undefined {
157
+ const buf = this.getInputOptional(inputIndex, psbtIn.BIP32_DERIVATION, pubkey);
158
+ if (!buf) return undefined;
159
+ return this.decodeBip32Derivation(buf);
160
+ }
161
+ setInputFinalScriptsig(inputIndex: number, scriptSig: Buffer) {
162
+ this.setInput(inputIndex, psbtIn.FINAL_SCRIPTSIG, b(), scriptSig);
163
+ }
164
+ getInputFinalScriptsig(inputIndex: number): Buffer | undefined {
165
+ return this.getInputOptional(inputIndex, psbtIn.FINAL_SCRIPTSIG, b());
166
+ }
167
+ setInputFinalScriptwitness(inputIndex: number, scriptWitness: Buffer) {
168
+ this.setInput(inputIndex, psbtIn.FINAL_SCRIPTWITNESS, b(), scriptWitness);
169
+ }
170
+ getInputFinalScriptwitness(inputIndex: number): Buffer {
171
+ return this.getInput(inputIndex, psbtIn.FINAL_SCRIPTWITNESS, b());
172
+ }
173
+ setInputPreviousTxId(inputIndex: number, txid: Buffer) {
174
+ this.setInput(inputIndex, psbtIn.PREVIOUS_TXID, b(), txid);
175
+ }
176
+ getInputPreviousTxid(inputIndex: number): Buffer {
177
+ return this.getInput(inputIndex, psbtIn.PREVIOUS_TXID, b());
178
+ }
179
+ setInputOutputIndex(inputIndex: number, outputIndex: number) {
180
+ this.setInput(inputIndex, psbtIn.OUTPUT_INDEX, b(), uint32LE(outputIndex));
181
+ }
182
+ getInputOutputIndex(inputIndex: number): number {
183
+ return this.getInput(inputIndex, psbtIn.OUTPUT_INDEX, b()).readUInt32LE(0);
184
+ }
185
+ setInputSequence(inputIndex: number, sequence: number) {
186
+ this.setInput(inputIndex, psbtIn.SEQUENCE, b(), uint32LE(sequence));
187
+ }
188
+ getInputSequence(inputIndex: number): number {
189
+ return this.getInputOptional(inputIndex, psbtIn.SEQUENCE, b())?.readUInt32LE(0) ?? 0xffffffff;
190
+ }
191
+ setInputTapKeySig(inputIndex: number, sig: Buffer) {
192
+ this.setInput(inputIndex, psbtIn.TAP_KEY_SIG, b(), sig);
193
+ }
194
+ getInputTapKeySig(inputIndex: number): Buffer | undefined {
195
+ return this.getInputOptional(inputIndex, psbtIn.TAP_KEY_SIG, b());
196
+ }
197
+ setInputTapBip32Derivation(
198
+ inputIndex: number,
199
+ pubkey: Buffer,
200
+ hashes: Buffer[],
201
+ masterFingerprint: Buffer,
202
+ path: number[],
203
+ ) {
204
+ if (pubkey.length != 32) throw new Error("Invalid pubkey length: " + pubkey.length);
205
+ const buf = this.encodeTapBip32Derivation(hashes, masterFingerprint, path);
206
+ this.setInput(inputIndex, psbtIn.TAP_BIP32_DERIVATION, pubkey, buf);
207
+ }
208
+ getInputTapBip32Derivation(
209
+ inputIndex: number,
210
+ pubkey: Buffer,
211
+ ): { hashes: Buffer[]; masterFingerprint: Buffer; path: number[] } {
212
+ const buf = this.getInput(inputIndex, psbtIn.TAP_BIP32_DERIVATION, pubkey);
213
+ return this.decodeTapBip32Derivation(buf);
214
+ }
215
+ getInputKeyDatas(inputIndex: number, keyType: number): Buffer[] {
216
+ return this.getKeyDatas(this.inputMaps[inputIndex], keyType);
217
+ }
218
+
219
+ setOutputRedeemScript(outputIndex: number, redeemScript: Buffer) {
220
+ this.setOutput(outputIndex, psbtOut.REDEEM_SCRIPT, b(), redeemScript);
221
+ }
222
+ getOutputRedeemScript(outputIndex: number): Buffer {
223
+ return this.getOutput(outputIndex, psbtOut.REDEEM_SCRIPT, b());
224
+ }
225
+ setOutputBip32Derivation(
226
+ outputIndex: number,
227
+ pubkey: Buffer,
228
+ masterFingerprint: Buffer,
229
+ path: number[],
230
+ ) {
231
+ this.setOutput(
232
+ outputIndex,
233
+ psbtOut.BIP_32_DERIVATION,
234
+ pubkey,
235
+ this.encodeBip32Derivation(masterFingerprint, path),
236
+ );
237
+ }
238
+ getOutputBip32Derivation(
239
+ outputIndex: number,
240
+ pubkey: Buffer,
241
+ ): { masterFingerprint: Buffer; path: number[] } {
242
+ const buf = this.getOutput(outputIndex, psbtOut.BIP_32_DERIVATION, pubkey);
243
+ return this.decodeBip32Derivation(buf);
244
+ }
245
+ setOutputAmount(outputIndex: number, amount: number) {
246
+ this.setOutput(outputIndex, psbtOut.AMOUNT, b(), uint64LE(amount));
247
+ }
248
+ getOutputAmount(outputIndex: number): number {
249
+ const buf = this.getOutput(outputIndex, psbtOut.AMOUNT, b());
250
+ return unsafeFrom64bitLE(buf);
251
+ }
252
+ setOutputScript(outputIndex: number, scriptPubKey: Buffer) {
253
+ this.setOutput(outputIndex, psbtOut.SCRIPT, b(), scriptPubKey);
254
+ }
255
+ getOutputScript(outputIndex: number): Buffer {
256
+ return this.getOutput(outputIndex, psbtOut.SCRIPT, b());
257
+ }
258
+ setOutputTapBip32Derivation(
259
+ outputIndex: number,
260
+ pubkey: Buffer,
261
+ hashes: Buffer[],
262
+ fingerprint: Buffer,
263
+ path: number[],
264
+ ) {
265
+ const buf = this.encodeTapBip32Derivation(hashes, fingerprint, path);
266
+ this.setOutput(outputIndex, psbtOut.TAP_BIP32_DERIVATION, pubkey, buf);
267
+ }
268
+ getOutputTapBip32Derivation(
269
+ outputIndex: number,
270
+ pubkey: Buffer,
271
+ ): { hashes: Buffer[]; masterFingerprint: Buffer; path: number[] } {
272
+ const buf = this.getOutput(outputIndex, psbtOut.TAP_BIP32_DERIVATION, pubkey);
273
+ return this.decodeTapBip32Derivation(buf);
274
+ }
275
+
276
+ deleteInputEntries(inputIndex: number, keyTypes: psbtIn[]) {
277
+ const map = this.inputMaps[inputIndex];
278
+ map.forEach((_v, k, m) => {
279
+ if (this.isKeyType(k, keyTypes)) {
280
+ m.delete(k);
281
+ }
282
+ });
283
+ }
284
+
285
+ copy(to: PsbtV2) {
286
+ this.copyMap(this.globalMap, to.globalMap);
287
+ this.copyMaps(this.inputMaps, to.inputMaps);
288
+ this.copyMaps(this.outputMaps, to.outputMaps);
289
+ }
290
+ copyMaps(from: Map<string, Buffer>[], to: Map<string, Buffer>[]) {
291
+ from.forEach((m, index) => {
292
+ const to_index = new Map();
293
+ this.copyMap(m, to_index);
294
+ to[index] = to_index;
295
+ });
296
+ }
297
+ copyMap(from: Map<string, Buffer>, to: Map<string, Buffer>) {
298
+ from.forEach((v, k) => to.set(k, Buffer.from(v)));
299
+ }
300
+ serialize(): Buffer {
301
+ const buf = new BufferWriter();
302
+ buf.writeSlice(PSBT_MAGIC_BYTES);
303
+ serializeMap(buf, this.globalMap);
304
+ this.inputMaps.forEach(map => {
305
+ serializeMap(buf, map);
306
+ });
307
+ this.outputMaps.forEach(map => {
308
+ serializeMap(buf, map);
309
+ });
310
+ return buf.buffer();
311
+ }
312
+ deserialize(psbt: Buffer) {
313
+ const buf = new BufferReader(psbt);
314
+ if (!buf.readSlice(5).equals(PSBT_MAGIC_BYTES)) {
315
+ throw new Error("Invalid magic bytes");
316
+ }
317
+ while (this.readKeyPair(this.globalMap, buf));
318
+ for (let i = 0; i < this.getGlobalInputCount(); i++) {
319
+ this.inputMaps[i] = new Map();
320
+ while (this.readKeyPair(this.inputMaps[i], buf));
321
+ }
322
+ for (let i = 0; i < this.getGlobalOutputCount(); i++) {
323
+ this.outputMaps[i] = new Map();
324
+ while (this.readKeyPair(this.outputMaps[i], buf));
325
+ }
326
+ }
327
+
328
+ /**
329
+ * Attempts to extract the version number as uint32LE from raw psbt regardless
330
+ * of psbt validity.
331
+ *
332
+ * @param psbt - PSBT buffer to extract version from
333
+ * @returns The PSBT version number, or 0 if no version field is found (indicating PSBTv0)
334
+ *
335
+ * @example
336
+ * ```typescript
337
+ * const psbtBuffer = Buffer.from('cHNidP8BAH...', 'base64');
338
+ * const version = PsbtV2.getPsbtVersionNumber(psbtBuffer);
339
+ * if (version === 2) {
340
+ * // Handle PSBTv2
341
+ * } else {
342
+ * // Handle PSBTv0
343
+ * }
344
+ * ```
345
+ */
346
+ static getPsbtVersionNumber(psbt: Buffer): number {
347
+ const map = new Map<string, Buffer>();
348
+ const buf = new BufferReader(psbt.subarray(PSBT_MAGIC_BYTES.length));
349
+
350
+ // Read global map key-value pairs
351
+ while (buf.available() > 0) {
352
+ const keyLen = buf.readVarInt();
353
+ if (keyLen === 0) break; // End of global map
354
+
355
+ const keyType = buf.readUInt8();
356
+ const keyData = keyLen > 1 ? buf.readSlice(keyLen - 1) : Buffer.alloc(0);
357
+ const key = Buffer.concat([Buffer.from([keyType]), keyData]).toString("hex");
358
+
359
+ const valueLen = buf.readVarInt();
360
+ const value = valueLen > 0 ? buf.readSlice(valueLen) : Buffer.alloc(0);
361
+
362
+ map.set(key, value);
363
+ }
364
+
365
+ // Look for PSBT version field (0xfb)
366
+ const versionKey = Buffer.from([psbtGlobal.VERSION]).toString("hex");
367
+ const versionValue = map.get(versionKey);
368
+ return versionValue ? versionValue.readUInt32LE(0) : 0;
369
+ }
370
+
371
+ /**
372
+ * Converts a PSBTv0 (from bitcoinjs-lib) to PSBTv2.
373
+ *
374
+ * This method deserializes a PSBTv0 buffer and converts it
375
+ * to the PSBTv2 format, preserving all relevant fields including:
376
+ * - Transaction version and locktime
377
+ * - Inputs (UTXOs, derivation paths, sequences, signatures)
378
+ * - Outputs (amounts, scripts, derivation paths)
379
+ * - Finalized scripts (if present)
380
+ *
381
+ * The method follows the PSBT role saga defined in BIP174:
382
+ * 1. Creator Role - Initialize PSBTv2 with version and counts
383
+ * 2. Constructor Role - Add inputs and outputs
384
+ * 3. Signer Role - Transfer partial signatures
385
+ * 4. Input Finalizer - Transfer finalized scripts
386
+ *
387
+ * @param psbt - PSBTv0 as Buffer
388
+ * @param allowTxnVersion1 - Allow transaction version 1 (default: false).
389
+ * Version 2 is recommended per BIP68.
390
+ * @returns A new PsbtV2 instance with converted data
391
+ * @throws Error if PSBT is invalid or contains unsupported features
392
+ *
393
+ * @example
394
+ * ```typescript
395
+ * const psbtV0Buffer = Buffer.from('cHNidP8BAH...', 'base64');
396
+ * const psbtV2 = PsbtV2.fromV0(psbtV0Buffer);
397
+ * ```
398
+ */
399
+ static fromV0(psbt: Buffer, allowTxnVersion1 = false): PsbtV2 {
400
+ const psbtv0 = Psbt.fromBuffer(psbt);
401
+
402
+ // Creator Role - Initialize PSBTv2
403
+ const psbtv2 = new PsbtV2();
404
+ PsbtV2.initializeFromV0(psbtv2, psbtv0, allowTxnVersion1);
405
+
406
+ // Constructor Role - Add inputs and outputs
407
+ const txBuffer = psbtv0.data.getTransaction();
408
+ const tx = Transaction.fromBuffer(txBuffer);
409
+ PsbtV2.addInputsFromV0(psbtv2, psbtv0, tx);
410
+ PsbtV2.addOutputsFromV0(psbtv2, psbtv0, tx);
411
+
412
+ // Signer Role - Transfer partial signatures
413
+ PsbtV2.transferPartialSignatures(psbtv2, psbtv0);
414
+
415
+ // Input Finalizer - Transfer finalized scripts
416
+ PsbtV2.transferFinalizedScripts(psbtv2, psbtv0);
417
+
418
+ return psbtv2;
419
+ }
420
+
421
+ private static initializeFromV0(psbtv2: PsbtV2, psbtv0: Psbt, allowTxnVersion1: boolean) {
422
+ const txVersion = psbtv0.data.getTransaction().readInt32LE(0);
423
+
424
+ if (txVersion === 1 && !allowTxnVersion1) {
425
+ throw new Error(
426
+ "Transaction version 1 detected. PSBTv2 recommends version 2 for BIP68 sequence support. " +
427
+ "Pass allowTxnVersion1=true to override.",
428
+ );
429
+ }
430
+
431
+ psbtv2.setGlobalTxVersion(txVersion);
432
+ psbtv2.setGlobalFallbackLocktime(psbtv0.locktime);
433
+ psbtv2.setGlobalInputCount(psbtv0.data.inputs.length);
434
+ psbtv2.setGlobalOutputCount(psbtv0.data.outputs.length);
435
+ psbtv2.setGlobalPsbtVersion(2);
436
+ }
437
+
438
+ private static addInputsFromV0(psbtv2: PsbtV2, psbtv0: Psbt, tx: Transaction) {
439
+ for (const [index, input] of psbtv0.data.inputs.entries()) {
440
+ // Required fields for PSBTv2 - get from the embedded transaction
441
+ psbtv2.setInputPreviousTxId(index, Buffer.from(tx.ins[index].hash).reverse());
442
+ psbtv2.setInputOutputIndex(index, tx.ins[index].index);
443
+ psbtv2.setInputSequence(index, tx.ins[index].sequence);
444
+
445
+ // Optional UTXO information
446
+ if (input.nonWitnessUtxo) {
447
+ psbtv2.setInputNonWitnessUtxo(index, input.nonWitnessUtxo);
448
+ }
449
+
450
+ if (input.witnessUtxo) {
451
+ // Convert bitcoinjs-lib format {value, script} to PSBTv2 format {amount, scriptPubKey}
452
+ const amount = unsafeTo64bitLE(input.witnessUtxo.value);
453
+ psbtv2.setInputWitnessUtxo(index, amount, input.witnessUtxo.script);
454
+ }
455
+
456
+ // Optional scripts and derivation
457
+ if (input.redeemScript) {
458
+ psbtv2.setInputRedeemScript(index, input.redeemScript);
459
+ }
460
+
461
+ if (input.sighashType !== undefined) {
462
+ psbtv2.setInputSighashType(index, input.sighashType);
463
+ }
464
+
465
+ if (input.bip32Derivation) {
466
+ for (const deriv of input.bip32Derivation) {
467
+ psbtv2.setInputBip32Derivation(
468
+ index,
469
+ deriv.pubkey,
470
+ deriv.masterFingerprint,
471
+ parseBip32Path(deriv.path),
472
+ );
473
+ }
474
+ }
475
+ }
476
+ }
477
+
478
+ private static addOutputsFromV0(psbtv2: PsbtV2, psbtv0: Psbt, tx: Transaction) {
479
+ // Constructor Role - Add outputs
480
+ for (const [index, output] of psbtv0.data.outputs.entries()) {
481
+ // Required fields for PSBTv2 - get from the embedded transaction
482
+ psbtv2.setOutputAmount(index, tx.outs[index].value);
483
+ psbtv2.setOutputScript(index, tx.outs[index].script);
484
+
485
+ // Optional fields
486
+ if (output.redeemScript) {
487
+ psbtv2.setOutputRedeemScript(index, output.redeemScript);
488
+ }
489
+
490
+ if (output.bip32Derivation) {
491
+ for (const deriv of output.bip32Derivation) {
492
+ psbtv2.setOutputBip32Derivation(
493
+ index,
494
+ deriv.pubkey,
495
+ deriv.masterFingerprint,
496
+ parseBip32Path(deriv.path),
497
+ );
498
+ }
499
+ }
500
+ }
501
+ }
502
+
503
+ private static transferPartialSignatures(psbtv2: PsbtV2, psbtv0: Psbt) {
504
+ for (const [index, input] of psbtv0.data.inputs.entries()) {
505
+ if (input.partialSig) {
506
+ for (const sig of input.partialSig) {
507
+ psbtv2.setInputPartialSig(index, sig.pubkey, sig.signature);
508
+ }
509
+ }
510
+ }
511
+ }
512
+
513
+ private static transferFinalizedScripts(psbtv2: PsbtV2, psbtv0: Psbt) {
514
+ // Input Finalizer - Transfer finalized scripts
515
+ // Note: Per BIP174, the Input Finalizer should remove other fields after
516
+ // finalization, but we preserve them for compatibility with the source PSBTv0
517
+ for (const [index, input] of psbtv0.data.inputs.entries()) {
518
+ if (input.finalScriptSig) {
519
+ psbtv2.setInputFinalScriptsig(index, input.finalScriptSig);
520
+ }
521
+
522
+ if (input.finalScriptWitness) {
523
+ psbtv2.setInputFinalScriptwitness(index, input.finalScriptWitness);
524
+ }
525
+ }
526
+ }
527
+
528
+ private readKeyPair(map: Map<string, Buffer>, buf: BufferReader): boolean {
529
+ const keyLen = buf.readVarInt();
530
+ if (keyLen == 0) {
531
+ return false;
532
+ }
533
+ const keyType = buf.readUInt8();
534
+ const keyData = buf.readSlice(keyLen - 1);
535
+ const value = buf.readVarSlice();
536
+ set(map, keyType, keyData, value);
537
+ return true;
538
+ }
539
+ private getKeyDatas(map: Map<string, Buffer>, keyType: number): Buffer[] {
540
+ const result: Buffer[] = [];
541
+ map.forEach((_v, k) => {
542
+ if (this.isKeyType(k, [keyType])) {
543
+ result.push(Buffer.from(k.substring(2), "hex"));
544
+ }
545
+ });
546
+ return result;
547
+ }
548
+ private isKeyType(hexKey: string, keyTypes: number[]): boolean {
549
+ const keyType = Buffer.from(hexKey.substring(0, 2), "hex").readUInt8(0);
550
+ return keyTypes.some(k => k == keyType);
551
+ }
552
+ private setGlobal(keyType: number, value: Buffer) {
553
+ const key = new Key(keyType, Buffer.from([]));
554
+ this.globalMap.set(key.toString(), value);
555
+ }
556
+ private getGlobal(keyType: number): Buffer {
557
+ return get(this.globalMap, keyType, b(), false)!;
558
+ }
559
+ private getGlobalOptional(keyType: number): Buffer | undefined {
560
+ return get(this.globalMap, keyType, b(), true);
561
+ }
562
+ private setInput(index: number, keyType: number, keyData: Buffer, value: Buffer) {
563
+ set(this.getMap(index, this.inputMaps), keyType, keyData, value);
564
+ }
565
+ private getInput(index: number, keyType: number, keyData: Buffer): Buffer {
566
+ return get(this.inputMaps[index], keyType, keyData, false)!;
567
+ }
568
+ private getInputOptional(index: number, keyType: number, keyData: Buffer): Buffer | undefined {
569
+ return get(this.inputMaps[index], keyType, keyData, true);
570
+ }
571
+ private setOutput(index: number, keyType: number, keyData: Buffer, value: Buffer) {
572
+ set(this.getMap(index, this.outputMaps), keyType, keyData, value);
573
+ }
574
+ private getOutput(index: number, keyType: number, keyData: Buffer): Buffer {
575
+ return get(this.outputMaps[index], keyType, keyData, false)!;
576
+ }
577
+ private getMap(index: number, maps: Map<string, Buffer>[]): Map<string, Buffer> {
578
+ if (!maps[index]) {
579
+ maps[index] = new Map();
580
+ }
581
+ return maps[index];
582
+ }
583
+ private encodeBip32Derivation(masterFingerprint: Buffer, path: number[]) {
584
+ const buf = new BufferWriter();
585
+ this.writeBip32Derivation(buf, masterFingerprint, path);
586
+ return buf.buffer();
587
+ }
588
+ private decodeBip32Derivation(buffer: Buffer): {
589
+ masterFingerprint: Buffer;
590
+ path: number[];
591
+ } {
592
+ const buf = new BufferReader(buffer);
593
+ return this.readBip32Derivation(buf);
594
+ }
595
+ private writeBip32Derivation(buf: BufferWriter, masterFingerprint: Buffer, path: number[]) {
596
+ buf.writeSlice(masterFingerprint);
597
+ path.forEach(element => {
598
+ buf.writeUInt32(element);
599
+ });
600
+ }
601
+ private readBip32Derivation(buf: BufferReader): {
602
+ masterFingerprint: Buffer;
603
+ path: number[];
604
+ } {
605
+ const masterFingerprint = buf.readSlice(4);
606
+ const path: number[] = [];
607
+ while (buf.offset < buf.buffer.length) {
608
+ path.push(buf.readUInt32());
609
+ }
610
+ return { masterFingerprint, path };
611
+ }
612
+ private encodeTapBip32Derivation(
613
+ hashes: Buffer[],
614
+ masterFingerprint: Buffer,
615
+ path: number[],
616
+ ): Buffer {
617
+ const buf = new BufferWriter();
618
+ buf.writeVarInt(hashes.length);
619
+ hashes.forEach(h => {
620
+ buf.writeSlice(h);
621
+ });
622
+ this.writeBip32Derivation(buf, masterFingerprint, path);
623
+ return buf.buffer();
624
+ }
625
+ private decodeTapBip32Derivation(buffer: Buffer): {
626
+ hashes: Buffer[];
627
+ masterFingerprint: Buffer;
628
+ path: number[];
629
+ } {
630
+ const buf = new BufferReader(buffer);
631
+ const hashCount = buf.readVarInt();
632
+ const hashes: Buffer[] = [];
633
+ for (let i = 0; i < hashCount; i++) {
634
+ hashes.push(buf.readSlice(32));
635
+ }
636
+ const deriv = this.readBip32Derivation(buf);
637
+ return { hashes, ...deriv };
638
+ }
639
+ }
640
+ function get(
641
+ map: Map<string, Buffer>,
642
+ keyType: number,
643
+ keyData: Buffer,
644
+ acceptUndefined: boolean,
645
+ ): Buffer | undefined {
646
+ if (!map) throw new Error("No such map");
647
+ const key = new Key(keyType, keyData);
648
+ const value = map.get(key.toString());
649
+ if (!value) {
650
+ if (acceptUndefined) {
651
+ return undefined;
652
+ }
653
+ throw new NoSuchEntry(key.toString());
654
+ }
655
+ // Make sure to return a copy, to protect the underlying data.
656
+ return Buffer.from(value);
657
+ }
658
+ class Key {
659
+ keyType: number;
660
+ keyData: Buffer;
661
+ constructor(keyType: number, keyData: Buffer) {
662
+ this.keyType = keyType;
663
+ this.keyData = keyData;
664
+ }
665
+ toString(): string {
666
+ const buf = new BufferWriter();
667
+ this.toBuffer(buf);
668
+ return buf.buffer().toString("hex");
669
+ }
670
+ serialize(buf: BufferWriter) {
671
+ buf.writeVarInt(1 + this.keyData.length);
672
+ this.toBuffer(buf);
673
+ }
674
+ private toBuffer(buf: BufferWriter) {
675
+ buf.writeUInt8(this.keyType);
676
+ buf.writeSlice(this.keyData);
677
+ }
678
+ }
679
+ class KeyPair {
680
+ key: Key;
681
+ value: Buffer;
682
+ constructor(key: Key, value: Buffer) {
683
+ this.key = key;
684
+ this.value = value;
685
+ }
686
+ serialize(buf: BufferWriter) {
687
+ this.key.serialize(buf);
688
+ buf.writeVarSlice(this.value);
689
+ }
690
+ }
691
+ function createKey(buf: Buffer): Key {
692
+ return new Key(buf.readUInt8(0), buf.subarray(1));
693
+ }
694
+ function serializeMap(buf: BufferWriter, map: Map<string, Buffer>) {
695
+ for (const k of map.keys()) {
696
+ const value = map.get(k)!;
697
+ const keyPair = new KeyPair(createKey(Buffer.from(k, "hex")), value);
698
+ keyPair.serialize(buf);
699
+ }
700
+ buf.writeUInt8(0);
701
+ }
702
+
703
+ function b(): Buffer {
704
+ return Buffer.from([]);
705
+ }
706
+ function set(map: Map<string, Buffer>, keyType: number, keyData: Buffer, value: Buffer) {
707
+ const key = new Key(keyType, keyData);
708
+ map.set(key.toString(), value);
709
+ }
710
+ function uint32LE(n: number): Buffer {
711
+ const b = Buffer.alloc(4);
712
+ b.writeUInt32LE(n, 0);
713
+ return b;
714
+ }
715
+ function uint64LE(n: number): Buffer {
716
+ return unsafeTo64bitLE(n);
717
+ }
718
+ function varint(n: number): Buffer {
719
+ const b = new BufferWriter();
720
+ b.writeVarInt(n);
721
+ return b.buffer();
722
+ }
723
+ function fromVarint(buf: Buffer): number {
724
+ return new BufferReader(buf).readVarInt();
725
+ }
726
+
727
+ export function parseBip32Path(path: string): number[] {
728
+ return path
729
+ .split("/")
730
+ .slice(1)
731
+ .map(s => {
732
+ const hardened = s.endsWith("'") || s.endsWith("h");
733
+ const base = hardened ? s.slice(0, -1) : s;
734
+ const num = Number(base);
735
+ if (Number.isNaN(num)) {
736
+ throw new TypeError(`Invalid BIP32 path segment: ${path}`);
737
+ }
738
+ return hardened ? num + 0x80000000 : num;
739
+ });
740
+ }