@caravan/psbt 2.0.7 → 2.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.
package/README.md CHANGED
@@ -28,6 +28,7 @@ A set of utilities for working with PSBTs.
28
28
  - [`public addPartialSig`](#public-addpartialsig)
29
29
  - [`public removePartialSig`](#public-removepartialsig)
30
30
  - [`public setProprietaryValue`](#public-setproprietaryvalue)
31
+ - [`public combine`](#public-combine)
31
32
  - [`static PsbtV2.FromV0`](#static-psbtv2fromv0)
32
33
  - [`function getPsbtVersionNumber`](#function-getpsbtversionnumber)
33
34
  - [Concepts](#concepts)
@@ -197,6 +198,23 @@ Args:
197
198
 
198
199
  From the provided args, a key with the following format will be generated: `0xFC<compact uint identifier length><bytes identifier><bytes subtype><bytes subkeydata>`
199
200
 
201
+ ##### `public combine`
202
+
203
+ Combines multiple PSBTs into this PsbtV2. This implements the Combiner role as defined in [BIP 174](https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki#user-content-Roles).
204
+
205
+ Args:
206
+
207
+ - `psbts` - An array of `PsbtV2` instances to combine into this PSBT.
208
+
209
+ Before combining, this method validates that:
210
+ 1. This PsbtV2 is ready for the Combiner role (`isReadyForCombiner` returns `true`).
211
+ 2. Each PSBT in the provided array is also ready for the Combiner role.
212
+ 3. All PSBTs represent the same unsigned transaction. This is determined by comparing transaction IDs after setting all input sequence numbers to 0 (per [BIP 370](https://github.com/bitcoin/bips/blob/master/bip-0370.mediawiki#unique-identification) unique identification rules).
213
+
214
+ When combining, later PSBTs in the array take precedence over earlier ones. The combination merges all global, input, and output map key-value pairs. This operation is atomic—if any error occurs during combination, the original state is preserved.
215
+
216
+ **Warning:** This method could potentially produce a PSBT in a bad state. For example, if a later PSBT has an input sequence without a signature, it could potentially invalidate signatures existing on this or earlier PSBTs in the list if the sequence numbers do not agree.
217
+
200
218
  ##### `static PsbtV2.FromV0`
201
219
 
202
220
  Attempts to return a `PsbtV2` by converting from a PSBTv0 string or Buffer
package/dist/index.d.ts CHANGED
@@ -69,6 +69,21 @@ declare abstract class PsbtV2Maps {
69
69
  copy(to: PsbtV2Maps): void;
70
70
  private copyMaps;
71
71
  private copyMap;
72
+ /**
73
+ * For a given map, set the value to the given key if the value is not empty.
74
+ * This overrides the value at that key as long as the value will not be
75
+ * empty.
76
+ */
77
+ private combineValue;
78
+ private validateCombineMaps;
79
+ /**
80
+ * Combines the maps in the provided PsbtV2Maps object into this one. Using
81
+ * this without validation may produce a PSBT in an invalid state.
82
+ *
83
+ * This operation is atomic - if any error occurs, the original state is
84
+ * preserved.
85
+ */
86
+ protected combineMaps(from: PsbtV2Maps): void;
72
87
  }
73
88
 
74
89
  /**
@@ -408,6 +423,23 @@ declare class PsbtV2 extends PsbtV2Maps {
408
423
  * Updates the PSBT_GLOBAL_OUTPUT_COUNT field in the global map.
409
424
  */
410
425
  private updateGlobalOutputCount;
426
+ /**
427
+ * Validates this PsbtV2 and the one it's being combined with are ready for
428
+ * the Combiner role. Also, the calculated unsigned transaction IDs of the two
429
+ * must be the same.
430
+ */
431
+ private validateCombine;
432
+ /**
433
+ * Combines multiple PsbtV2 objects into this one with the latest index taking
434
+ * precedence over earlier indices of the provided list. This action is
435
+ * atomic.
436
+ *
437
+ * WARNING: This method could potentially produce a PSBT in an bad state. For
438
+ * example, if a later PSBT has an input sequence without a signature, it
439
+ * could potentially invalidate signatures existing on this or earlier PSBTs
440
+ * in the list if the sequence numbers do not agree.
441
+ */
442
+ combine(psbts: PsbtV2[]): void;
411
443
  }
412
444
 
413
445
  interface PsbtInput {
package/dist/index.js CHANGED
@@ -295,6 +295,63 @@ var PsbtV2Maps = class {
295
295
  copyMap(from, to) {
296
296
  from.forEach((v, k) => to.set(k, Buffer.from(v)));
297
297
  }
298
+ /**
299
+ * For a given map, set the value to the given key if the value is not empty.
300
+ * This overrides the value at that key as long as the value will not be
301
+ * empty.
302
+ */
303
+ combineValue(map, value, toKey) {
304
+ if (value && value.length > 0) {
305
+ map.set(toKey, value);
306
+ }
307
+ }
308
+ validateCombineMaps(from) {
309
+ if (from.inputMaps.length !== this.inputMaps.length) {
310
+ throw new Error(
311
+ `Cannot combine PSBTs with different input counts: this has ${this.inputMaps.length}, other has ${from.inputMaps.length}`
312
+ );
313
+ }
314
+ if (from.outputMaps.length !== this.outputMaps.length) {
315
+ throw new Error(
316
+ `Cannot combine PSBTs with different output counts: this has ${this.outputMaps.length}, other has ${from.outputMaps.length}`
317
+ );
318
+ }
319
+ }
320
+ /**
321
+ * Combines the maps in the provided PsbtV2Maps object into this one. Using
322
+ * this without validation may produce a PSBT in an invalid state.
323
+ *
324
+ * This operation is atomic - if any error occurs, the original state is
325
+ * preserved.
326
+ */
327
+ combineMaps(from) {
328
+ this.validateCombineMaps(from);
329
+ const newGlobalMap = new Map(this.globalMap);
330
+ const newInputMaps = this.inputMaps.map((m) => new Map(m));
331
+ const newOutputMaps = this.outputMaps.map((m) => new Map(m));
332
+ for (const [key, value] of from.globalMap) {
333
+ this.combineValue(newGlobalMap, value, key);
334
+ }
335
+ for (let i = 0; i < from.inputMaps.length; i++) {
336
+ if (!newInputMaps[i]) {
337
+ newInputMaps[i] = /* @__PURE__ */ new Map();
338
+ }
339
+ for (const [key, value] of from.inputMaps[i]) {
340
+ this.combineValue(newInputMaps[i], value, key);
341
+ }
342
+ }
343
+ for (let i = 0; i < from.outputMaps.length; i++) {
344
+ if (!newOutputMaps[i]) {
345
+ newOutputMaps[i] = /* @__PURE__ */ new Map();
346
+ }
347
+ for (const [key, value] of from.outputMaps[i]) {
348
+ this.combineValue(newOutputMaps[i], value, key);
349
+ }
350
+ }
351
+ this.globalMap = newGlobalMap;
352
+ this.inputMaps = newInputMaps;
353
+ this.outputMaps = newOutputMaps;
354
+ }
298
355
  };
299
356
  var PsbtConversionMaps = class extends PsbtV2Maps {
300
357
  /**
@@ -375,6 +432,29 @@ var PsbtConversionMaps = class extends PsbtV2Maps {
375
432
  this.v0delete(outputMap, "04" /* PSBT_OUT_SCRIPT */);
376
433
  }
377
434
  };
435
+ getTransactionId() {
436
+ const txBuf = this.buildUnsignedTx();
437
+ try {
438
+ return import_bitcoinjs_lib_v6.Transaction.fromBuffer(txBuf).getId();
439
+ } catch (e) {
440
+ const numInputs = this.inputMaps.length;
441
+ const numOutputs = this.outputMaps.length;
442
+ if (numInputs === 0 && numOutputs === 1) {
443
+ console.error(
444
+ `Cannot compute transaction ID for PSBT with 0 inputs and 1 output. The serialized transaction (${txBuf.toString("hex")}) is ambiguous with SegWit format and cannot be parsed.`
445
+ );
446
+ throw new Error(
447
+ "Cannot compute transaction ID: PSBT has 0 inputs and 1 output, which produces an ambiguous transaction format. "
448
+ );
449
+ }
450
+ console.error(
451
+ `Failed to parse transaction buffer: ${txBuf.toString("hex")}`
452
+ );
453
+ throw new Error(
454
+ `Failed to compute transaction ID (${numInputs} inputs, ${numOutputs} outputs): ${e instanceof Error ? e.message : e}`
455
+ );
456
+ }
457
+ }
378
458
  };
379
459
 
380
460
  // src/psbtv2/psbtv2.ts
@@ -961,9 +1041,9 @@ var PsbtV2 = class _PsbtV2 extends PsbtV2Maps {
961
1041
  * https://github.com/bitcoin/bips/blob/master/bip-0068.mediawiki
962
1042
  */
963
1043
  setInputSequence(inputIndex, sequence) {
964
- if (!this.isReadyForUpdater) {
1044
+ if (!this.isReadyForUpdater && !this.isReadyForCombiner) {
965
1045
  throw new Error(
966
- "PSBT is not ready for the Updater role. Sequence cannot be changed."
1046
+ "PSBT is not ready for the Updater or Combiner role. Sequence cannot be changed."
967
1047
  );
968
1048
  }
969
1049
  if (inputIndex < 0 || inputIndex >= this.PSBT_GLOBAL_INPUT_COUNT) {
@@ -1454,6 +1534,67 @@ var PsbtV2 = class _PsbtV2 extends PsbtV2Maps {
1454
1534
  bw.writeU8(this.outputMaps.length);
1455
1535
  this.globalMap.set("05" /* PSBT_GLOBAL_OUTPUT_COUNT */, bw.render());
1456
1536
  }
1537
+ /**
1538
+ * Validates this PsbtV2 and the one it's being combined with are ready for
1539
+ * the Combiner role. Also, the calculated unsigned transaction IDs of the two
1540
+ * must be the same.
1541
+ */
1542
+ validateCombine(psbt) {
1543
+ if (!this.isReadyForCombiner) {
1544
+ throw Error("This PsbtV2 is not ready for Combiner role.");
1545
+ }
1546
+ if (!psbt.isReadyForCombiner) {
1547
+ throw Error("Provided PsbtV2 is not ready for Combiner role.");
1548
+ }
1549
+ const tempThis = new _PsbtV2(this.serialize(), true);
1550
+ const tempOther = new _PsbtV2(psbt.serialize(), true);
1551
+ for (const [index, sequence] of tempThis.PSBT_IN_SEQUENCE.entries()) {
1552
+ if (sequence !== 0) {
1553
+ tempThis.setInputSequence(index, 0);
1554
+ }
1555
+ }
1556
+ for (const [index, sequence] of tempOther.PSBT_IN_SEQUENCE.entries()) {
1557
+ if (sequence !== 0) {
1558
+ tempOther.setInputSequence(index, 0);
1559
+ }
1560
+ }
1561
+ const checkThis = new PsbtConversionMaps(tempThis.serialize());
1562
+ const checkOther = new PsbtConversionMaps(tempOther.serialize());
1563
+ if (checkThis.getTransactionId() !== checkOther.getTransactionId()) {
1564
+ throw Error("Cannot combine PSBTs for different unsigned transactions.");
1565
+ }
1566
+ for (const [index, sequence] of this.PSBT_IN_SEQUENCE.entries()) {
1567
+ const otherSequence = psbt.PSBT_IN_SEQUENCE[index];
1568
+ if (otherSequence !== sequence && this.PSBT_IN_PARTIAL_SIG[index].some((sig) => sig !== null)) {
1569
+ console.warn(
1570
+ `Combined PSBT updated sequence on signed input ${index}. This may invalidate the existing signature.`
1571
+ );
1572
+ }
1573
+ }
1574
+ }
1575
+ /**
1576
+ * Combines multiple PsbtV2 objects into this one with the latest index taking
1577
+ * precedence over earlier indices of the provided list. This action is
1578
+ * atomic.
1579
+ *
1580
+ * WARNING: This method could potentially produce a PSBT in an bad state. For
1581
+ * example, if a later PSBT has an input sequence without a signature, it
1582
+ * could potentially invalidate signatures existing on this or earlier PSBTs
1583
+ * in the list if the sequence numbers do not agree.
1584
+ */
1585
+ combine(psbts) {
1586
+ for (const psbt of psbts) {
1587
+ this.validateCombine(psbt);
1588
+ }
1589
+ try {
1590
+ for (const psbt of psbts) {
1591
+ this.combineMaps(psbt);
1592
+ }
1593
+ } catch (error) {
1594
+ console.error(error);
1595
+ throw Error("Failed to combine PSBTs.");
1596
+ }
1597
+ }
1457
1598
  };
1458
1599
 
1459
1600
  // src/psbtv0/psbt.ts
package/dist/index.mjs CHANGED
@@ -248,6 +248,63 @@ var PsbtV2Maps = class {
248
248
  copyMap(from, to) {
249
249
  from.forEach((v, k) => to.set(k, Buffer.from(v)));
250
250
  }
251
+ /**
252
+ * For a given map, set the value to the given key if the value is not empty.
253
+ * This overrides the value at that key as long as the value will not be
254
+ * empty.
255
+ */
256
+ combineValue(map, value, toKey) {
257
+ if (value && value.length > 0) {
258
+ map.set(toKey, value);
259
+ }
260
+ }
261
+ validateCombineMaps(from) {
262
+ if (from.inputMaps.length !== this.inputMaps.length) {
263
+ throw new Error(
264
+ `Cannot combine PSBTs with different input counts: this has ${this.inputMaps.length}, other has ${from.inputMaps.length}`
265
+ );
266
+ }
267
+ if (from.outputMaps.length !== this.outputMaps.length) {
268
+ throw new Error(
269
+ `Cannot combine PSBTs with different output counts: this has ${this.outputMaps.length}, other has ${from.outputMaps.length}`
270
+ );
271
+ }
272
+ }
273
+ /**
274
+ * Combines the maps in the provided PsbtV2Maps object into this one. Using
275
+ * this without validation may produce a PSBT in an invalid state.
276
+ *
277
+ * This operation is atomic - if any error occurs, the original state is
278
+ * preserved.
279
+ */
280
+ combineMaps(from) {
281
+ this.validateCombineMaps(from);
282
+ const newGlobalMap = new Map(this.globalMap);
283
+ const newInputMaps = this.inputMaps.map((m) => new Map(m));
284
+ const newOutputMaps = this.outputMaps.map((m) => new Map(m));
285
+ for (const [key, value] of from.globalMap) {
286
+ this.combineValue(newGlobalMap, value, key);
287
+ }
288
+ for (let i = 0; i < from.inputMaps.length; i++) {
289
+ if (!newInputMaps[i]) {
290
+ newInputMaps[i] = /* @__PURE__ */ new Map();
291
+ }
292
+ for (const [key, value] of from.inputMaps[i]) {
293
+ this.combineValue(newInputMaps[i], value, key);
294
+ }
295
+ }
296
+ for (let i = 0; i < from.outputMaps.length; i++) {
297
+ if (!newOutputMaps[i]) {
298
+ newOutputMaps[i] = /* @__PURE__ */ new Map();
299
+ }
300
+ for (const [key, value] of from.outputMaps[i]) {
301
+ this.combineValue(newOutputMaps[i], value, key);
302
+ }
303
+ }
304
+ this.globalMap = newGlobalMap;
305
+ this.inputMaps = newInputMaps;
306
+ this.outputMaps = newOutputMaps;
307
+ }
251
308
  };
252
309
  var PsbtConversionMaps = class extends PsbtV2Maps {
253
310
  /**
@@ -328,6 +385,29 @@ var PsbtConversionMaps = class extends PsbtV2Maps {
328
385
  this.v0delete(outputMap, "04" /* PSBT_OUT_SCRIPT */);
329
386
  }
330
387
  };
388
+ getTransactionId() {
389
+ const txBuf = this.buildUnsignedTx();
390
+ try {
391
+ return Transaction.fromBuffer(txBuf).getId();
392
+ } catch (e) {
393
+ const numInputs = this.inputMaps.length;
394
+ const numOutputs = this.outputMaps.length;
395
+ if (numInputs === 0 && numOutputs === 1) {
396
+ console.error(
397
+ `Cannot compute transaction ID for PSBT with 0 inputs and 1 output. The serialized transaction (${txBuf.toString("hex")}) is ambiguous with SegWit format and cannot be parsed.`
398
+ );
399
+ throw new Error(
400
+ "Cannot compute transaction ID: PSBT has 0 inputs and 1 output, which produces an ambiguous transaction format. "
401
+ );
402
+ }
403
+ console.error(
404
+ `Failed to parse transaction buffer: ${txBuf.toString("hex")}`
405
+ );
406
+ throw new Error(
407
+ `Failed to compute transaction ID (${numInputs} inputs, ${numOutputs} outputs): ${e instanceof Error ? e.message : e}`
408
+ );
409
+ }
410
+ }
331
411
  };
332
412
 
333
413
  // src/psbtv2/psbtv2.ts
@@ -914,9 +994,9 @@ var PsbtV2 = class _PsbtV2 extends PsbtV2Maps {
914
994
  * https://github.com/bitcoin/bips/blob/master/bip-0068.mediawiki
915
995
  */
916
996
  setInputSequence(inputIndex, sequence) {
917
- if (!this.isReadyForUpdater) {
997
+ if (!this.isReadyForUpdater && !this.isReadyForCombiner) {
918
998
  throw new Error(
919
- "PSBT is not ready for the Updater role. Sequence cannot be changed."
999
+ "PSBT is not ready for the Updater or Combiner role. Sequence cannot be changed."
920
1000
  );
921
1001
  }
922
1002
  if (inputIndex < 0 || inputIndex >= this.PSBT_GLOBAL_INPUT_COUNT) {
@@ -1407,6 +1487,67 @@ var PsbtV2 = class _PsbtV2 extends PsbtV2Maps {
1407
1487
  bw.writeU8(this.outputMaps.length);
1408
1488
  this.globalMap.set("05" /* PSBT_GLOBAL_OUTPUT_COUNT */, bw.render());
1409
1489
  }
1490
+ /**
1491
+ * Validates this PsbtV2 and the one it's being combined with are ready for
1492
+ * the Combiner role. Also, the calculated unsigned transaction IDs of the two
1493
+ * must be the same.
1494
+ */
1495
+ validateCombine(psbt) {
1496
+ if (!this.isReadyForCombiner) {
1497
+ throw Error("This PsbtV2 is not ready for Combiner role.");
1498
+ }
1499
+ if (!psbt.isReadyForCombiner) {
1500
+ throw Error("Provided PsbtV2 is not ready for Combiner role.");
1501
+ }
1502
+ const tempThis = new _PsbtV2(this.serialize(), true);
1503
+ const tempOther = new _PsbtV2(psbt.serialize(), true);
1504
+ for (const [index, sequence] of tempThis.PSBT_IN_SEQUENCE.entries()) {
1505
+ if (sequence !== 0) {
1506
+ tempThis.setInputSequence(index, 0);
1507
+ }
1508
+ }
1509
+ for (const [index, sequence] of tempOther.PSBT_IN_SEQUENCE.entries()) {
1510
+ if (sequence !== 0) {
1511
+ tempOther.setInputSequence(index, 0);
1512
+ }
1513
+ }
1514
+ const checkThis = new PsbtConversionMaps(tempThis.serialize());
1515
+ const checkOther = new PsbtConversionMaps(tempOther.serialize());
1516
+ if (checkThis.getTransactionId() !== checkOther.getTransactionId()) {
1517
+ throw Error("Cannot combine PSBTs for different unsigned transactions.");
1518
+ }
1519
+ for (const [index, sequence] of this.PSBT_IN_SEQUENCE.entries()) {
1520
+ const otherSequence = psbt.PSBT_IN_SEQUENCE[index];
1521
+ if (otherSequence !== sequence && this.PSBT_IN_PARTIAL_SIG[index].some((sig) => sig !== null)) {
1522
+ console.warn(
1523
+ `Combined PSBT updated sequence on signed input ${index}. This may invalidate the existing signature.`
1524
+ );
1525
+ }
1526
+ }
1527
+ }
1528
+ /**
1529
+ * Combines multiple PsbtV2 objects into this one with the latest index taking
1530
+ * precedence over earlier indices of the provided list. This action is
1531
+ * atomic.
1532
+ *
1533
+ * WARNING: This method could potentially produce a PSBT in an bad state. For
1534
+ * example, if a later PSBT has an input sequence without a signature, it
1535
+ * could potentially invalidate signatures existing on this or earlier PSBTs
1536
+ * in the list if the sequence numbers do not agree.
1537
+ */
1538
+ combine(psbts) {
1539
+ for (const psbt of psbts) {
1540
+ this.validateCombine(psbt);
1541
+ }
1542
+ try {
1543
+ for (const psbt of psbts) {
1544
+ this.combineMaps(psbt);
1545
+ }
1546
+ } catch (error) {
1547
+ console.error(error);
1548
+ throw Error("Failed to combine PSBTs.");
1549
+ }
1550
+ }
1410
1551
  };
1411
1552
 
1412
1553
  // src/psbtv0/psbt.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@caravan/psbt",
3
- "version": "2.0.7",
3
+ "version": "2.1.0",
4
4
  "description": "typescript library for working with PSBTs",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -66,6 +66,11 @@
66
66
  "typescript": "^5.3.3"
67
67
  },
68
68
  "peerDependencies": {
69
- "@caravan/bitcoin": "0.4.4"
69
+ "@caravan/bitcoin": "0.4.5"
70
+ },
71
+ "repository": {
72
+ "type": "git",
73
+ "url": "https://github.com/caravan-bitcoin/caravan",
74
+ "directory": "packages/caravan-psbt"
70
75
  }
71
76
  }