@scure/btc-signer 2.2.0 → 2.4.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/transaction.js CHANGED
@@ -1,13 +1,18 @@
1
1
  import { hex } from '@scure/base';
2
+ import { anumber } from '@noble/hashes/utils.js';
2
3
  import * as P from 'micro-packed';
3
- import { Address, OutScript, checkScript, tapLeafHash } from "./payment.js";
4
+ import { Address, OutScript, _WitnessOutScript, checkScript, tapLeafHash, } from "./payment.js";
4
5
  import * as psbt from "./psbt.js";
5
6
  import { CompactSizeLen, OP, RawOldTx, RawInput, RawOutput, RawTx, RawWitness, Script, scriptPushLen, VarBytes, } from "./script.js";
6
7
  import * as u from "./utils.js";
7
- import { NETWORK, concatBytes, equalBytes, isBytes, } from "./utils.js";
8
+ import { NETWORK, abigint, concatBytes, equalBytes, isBytes, validateObject, } from "./utils.js";
9
+ // Be friendly to bad ECMAScript parsers by not using bigint literals.
10
+ // prettier-ignore
11
+ const _0n = /* @__PURE__ */ BigInt(0), _1n = /* @__PURE__ */ BigInt(1);
12
+ const U64_MAX = /* @__PURE__ */ BigInt('0xffffffffffffffff');
8
13
  const EMPTY32 = /* @__PURE__ */ new Uint8Array(32);
9
14
  const EMPTY_OUTPUT = {
10
- amount: 0xffffffffffffffffn,
15
+ amount: U64_MAX,
11
16
  script: P.EMPTY,
12
17
  };
13
18
  /**
@@ -195,6 +200,7 @@ function outputBeforeSign(i) {
195
200
  * ```
196
201
  */
197
202
  export function inputBeforeSign(i) {
203
+ validateObject(i, {}, {}, 'i');
198
204
  if (i.txid === undefined || i.index === undefined)
199
205
  throw new Error('Transaction/input: txid and index required');
200
206
  const res = {
@@ -208,13 +214,61 @@ export function inputBeforeSign(i) {
208
214
  RawInput.encode(res);
209
215
  return res;
210
216
  }
211
- function cleanFinalInput(i) {
217
+ const cleanExtensions = (map, unknownMode, proprietaryMode, rejectStrip = false) => {
218
+ const out = { ...map };
219
+ for (const [name, mode] of [
220
+ ['unknown', unknownMode],
221
+ ['proprietary', proprietaryMode],
222
+ ]) {
223
+ const value = out[name];
224
+ // Policy cleanup must not reinterpret malformed caller metadata as an empty keyed map.
225
+ if (value !== undefined)
226
+ u.aarray(value, `${name} PSBT field`);
227
+ const rows = value;
228
+ if (!rows?.length) {
229
+ delete out[name];
230
+ continue;
231
+ }
232
+ if (mode === 'strict')
233
+ throw new Error(`PSBT: ${name} PSBT field is not allowed in strict mode`);
234
+ if (mode === 'strip') {
235
+ // Silent stripping is appropriate at relay/cleanup boundaries. On direct mutation it would
236
+ // hide a caller bug by accepting metadata that can never become transaction state.
237
+ if (rejectStrip)
238
+ throw new Error(`PSBT: ${name} PSBT field cannot be supplied when policy is strip`);
239
+ delete out[name];
240
+ }
241
+ }
242
+ return out;
243
+ };
244
+ const cleanTxModifiable = (value, mode) => {
245
+ if (value === undefined || !(value & ~0b111))
246
+ return value;
247
+ if (mode === 'strict')
248
+ throw new Error('PSBT: unknown txModifiable bits in strict mode');
249
+ return mode === 'strip' ? value & 0b111 : value;
250
+ };
251
+ function cleanFinalInput(i, unknownMode = 'strip', proprietaryMode = 'strip') {
212
252
  const _i = i;
253
+ const extensions = cleanExtensions(_i, unknownMode, proprietaryMode);
254
+ if (extensions.unknown)
255
+ _i.unknown = extensions.unknown;
256
+ else
257
+ delete _i.unknown;
258
+ if (extensions.proprietary)
259
+ _i.proprietary = extensions.proprietary;
260
+ else
261
+ delete _i.proprietary;
213
262
  // BIP174 finalizers clear non-final input metadata after constructing final scripts/witnesses.
214
263
  // That intentionally drops sighashType here, so post-finalize mutation becomes conservative
215
- // until callers explicitly reopen the input by removing finalScriptSig/finalScriptWitness.
264
+ // until callers explicitly clear satisfaction by removing finalScriptSig/finalScriptWitness.
216
265
  for (const _k in _i) {
217
266
  const k = _k;
267
+ // Proprietary records are cleanup metadata too, but callers may need their opaque protocol
268
+ // state after finalization for PSBT coordination outside transaction extraction. An empty
269
+ // keyed list encodes no records, so canonicalize it to absence like its serialized clone.
270
+ if (proprietaryMode === 'ignore' && k === 'proprietary' && _i.proprietary?.length)
271
+ continue;
218
272
  if (!psbt.PSBTInputFinalKeys.includes(k))
219
273
  delete _i[k];
220
274
  }
@@ -234,9 +288,21 @@ function unpackSighash(hashType) {
234
288
  isSingle: masked === SignatureHash.SINGLE,
235
289
  };
236
290
  }
291
+ const sighashScope = (sighash) => ({
292
+ sigInputs: sighash & SignatureHash.ANYONECANPAY,
293
+ sigOutputs: sighash === SignatureHash.DEFAULT ? SignatureHash.ALL : sighash & 0b11,
294
+ });
295
+ const normalizeUnknowns = (name, mode, legacy, fallback = 'strip') => {
296
+ const alias = legacy === undefined ? undefined : legacy ? 'ignore' : 'strip';
297
+ if (mode !== undefined && mode !== 'ignore' && mode !== 'strip' && mode !== 'strict')
298
+ throw new Error(`Transaction options wrong value: ${name}=${mode}`);
299
+ if (mode !== undefined && alias !== undefined && mode !== alias)
300
+ throw new Error(`Transaction options: conflicting ${name} options`);
301
+ return mode || alias || fallback;
302
+ };
237
303
  function validateOpts(opts) {
238
- if (opts !== undefined && {}.toString.call(opts) !== '[object Object]')
239
- throw new Error(`Wrong object type for transaction options: ${opts}`);
304
+ if (opts !== undefined)
305
+ validateObject(opts, {}, {}, 'opts');
240
306
  const _opts = {
241
307
  ...opts,
242
308
  // Defaults
@@ -250,6 +316,8 @@ function validateOpts(opts) {
250
316
  _opts.allowUnknownInputs = _opts.allowUnknowInput;
251
317
  if (typeof _opts.allowUnknowOutput !== 'undefined')
252
318
  _opts.allowUnknownOutputs = _opts.allowUnknowOutput;
319
+ if (_opts.allowMissingTxModifiable === undefined)
320
+ _opts.allowMissingTxModifiable = true;
253
321
  if (typeof _opts.lockTime !== 'number')
254
322
  throw new Error('Transaction lock time should be number');
255
323
  P.U32LE.encode(_opts.lockTime); // Additional range checks that lockTime
@@ -265,7 +333,10 @@ function validateOpts(opts) {
265
333
  'disableScriptCheck',
266
334
  'bip174jsCompat',
267
335
  'allowLegacyWitnessUtxo',
336
+ 'strictPrevoutValidation',
268
337
  'lowR',
338
+ 'allowUnknown',
339
+ 'allowMissingTxModifiable',
269
340
  ]) {
270
341
  const v = _opts[k];
271
342
  if (v === undefined)
@@ -273,11 +344,16 @@ function validateOpts(opts) {
273
344
  if (typeof v !== 'boolean')
274
345
  throw new Error(`Transation options wrong type: ${k}=${v} (${typeof v})`);
275
346
  }
347
+ _opts.unknown = normalizeUnknowns('unknown', _opts.unknown, _opts.allowUnknown);
348
+ _opts.proprietary = normalizeUnknowns('proprietary', _opts.proprietary, undefined, _opts.unknown);
276
349
  // 0 and -1 happens in tests
350
+ // With allowUnknownVersion any numeric version is fine; the ternary was inverted
351
+ // before 2026-07 (audit), which made the option throw for every numeric version.
277
352
  if (_opts.allowUnknownVersion
278
- ? typeof _opts.version === 'number'
353
+ ? typeof _opts.version !== 'number'
279
354
  : ![-1, 0, 1, 2, 3].includes(_opts.version))
280
355
  throw new Error(`Unknown version: ${_opts.version}`);
356
+ P.I32LE.encode(_opts.version); // Validate the signed transaction-version wire domain.
281
357
  if (_opts.customScripts !== undefined) {
282
358
  const cs = _opts.customScripts;
283
359
  if (!Array.isArray(cs)) {
@@ -292,14 +368,105 @@ function validateOpts(opts) {
292
368
  }
293
369
  return Object.freeze(_opts);
294
370
  }
371
+ function checkTaprootInputCommitments(input, prevScript) {
372
+ const output = _WitnessOutScript.decode(prevScript);
373
+ const hasTaprootCommitments = input.tapInternalKey !== undefined ||
374
+ input.tapMerkleRoot !== undefined ||
375
+ // Repeated keyed fields only exist on the PSBT wire when at least one entry is encoded.
376
+ !!input.tapLeafScript?.length;
377
+ if (output.type !== 'tr') {
378
+ if (hasTaprootCommitments)
379
+ throw new Error('validateInput: Taproot metadata without P2TR previous output');
380
+ return;
381
+ }
382
+ const checkOutputKey = (internalKey, merkleRoot, parity) => {
383
+ const [outputKey, outputParity] = u.taprootTweakPubkey(internalKey, merkleRoot);
384
+ if (!equalBytes(outputKey, output.pubkey))
385
+ throw new Error('validateInput: Taproot commitment does not match previous output');
386
+ if (parity !== undefined && outputParity !== parity)
387
+ throw new Error('validateInput: Taproot control-block parity does not match previous output');
388
+ };
389
+ if (input.tapLeafScript) {
390
+ for (const [controlBlock, scriptWithVersion] of input.tapLeafScript) {
391
+ const leafVersion = scriptWithVersion[scriptWithVersion.length - 1];
392
+ const script = scriptWithVersion.subarray(0, -1);
393
+ let merkleRoot = tapLeafHash(script, leafVersion);
394
+ for (const sibling of controlBlock.merklePath) {
395
+ merkleRoot =
396
+ u.compareBytes(sibling, merkleRoot) === -1
397
+ ? u.tagSchnorr('TapBranch', sibling, merkleRoot)
398
+ : u.tagSchnorr('TapBranch', merkleRoot, sibling);
399
+ }
400
+ checkOutputKey(controlBlock.internalKey, merkleRoot, controlBlock.version & 1);
401
+ if (input.tapInternalKey && !equalBytes(input.tapInternalKey, controlBlock.internalKey))
402
+ throw new Error('validateInput: tapInternalKey does not match Taproot control block');
403
+ if (input.tapMerkleRoot && !equalBytes(input.tapMerkleRoot, merkleRoot))
404
+ throw new Error('validateInput: tapMerkleRoot does not match Taproot control block');
405
+ }
406
+ }
407
+ // A tree-bearing input can omit the aggregate root while still providing independently
408
+ // verifiable control blocks. With no leaves, an internal key without a root describes the
409
+ // standard key-only (empty-root) commitment.
410
+ if (input.tapInternalKey && (input.tapMerkleRoot || !input.tapLeafScript?.length))
411
+ checkOutputKey(input.tapInternalKey, input.tapMerkleRoot || P.EMPTY);
412
+ }
413
+ const LOCKTIME_THRESHOLD = 500_000_000;
414
+ function validateRequiredLocktimes(input) {
415
+ const height = input.requiredHeightLocktime;
416
+ if (height !== undefined) {
417
+ anumber(height, 'requiredHeightLocktime');
418
+ if (height === 0 || height >= LOCKTIME_THRESHOLD)
419
+ throw new RangeError(`requiredHeightLocktime must be between 1 and ${LOCKTIME_THRESHOLD - 1}, got ${height}`);
420
+ }
421
+ const time = input.requiredTimeLocktime;
422
+ if (time !== undefined) {
423
+ anumber(time, 'requiredTimeLocktime');
424
+ if (time < LOCKTIME_THRESHOLD || time > 0xffffffff)
425
+ throw new RangeError(`requiredTimeLocktime must be between ${LOCKTIME_THRESHOLD} and 4294967295, got ${time}`);
426
+ }
427
+ }
428
+ function resolvePSBTLocktime(inputs, fallback = DEFAULT_LOCKTIME) {
429
+ let height = DEFAULT_LOCKTIME;
430
+ let time = DEFAULT_LOCKTIME;
431
+ let hasRequirements = false;
432
+ let heightSupported = true;
433
+ let timeSupported = true;
434
+ for (const input of inputs) {
435
+ validateRequiredLocktimes(input);
436
+ const hasHeight = input.requiredHeightLocktime !== undefined;
437
+ const hasTime = input.requiredTimeLocktime !== undefined;
438
+ if (!hasHeight && !hasTime)
439
+ continue;
440
+ hasRequirements = true;
441
+ if (hasHeight)
442
+ height = Math.max(height, input.requiredHeightLocktime);
443
+ else
444
+ heightSupported = false;
445
+ if (hasTime)
446
+ time = Math.max(time, input.requiredTimeLocktime);
447
+ else
448
+ timeSupported = false;
449
+ }
450
+ if (!hasRequirements)
451
+ return fallback;
452
+ // BIP370 requires height when every relevant input supports both domains.
453
+ if (heightSupported)
454
+ return height;
455
+ if (timeSupported)
456
+ return time;
457
+ throw new Error('PSBTv2: incompatible height-based and time-based locktime requirements');
458
+ }
295
459
  // NOTE: we cannot do this inside PSBTInput coder, because there is no index/txid at this point!
296
- function validateInput(i) {
460
+ function validateInput(i, disableScriptCheck = false) {
461
+ validateObject(i, {}, {}, 'i');
297
462
  const _i = i;
463
+ validateRequiredLocktimes(_i);
464
+ let prevOut;
298
465
  if (_i.nonWitnessUtxo && _i.index !== undefined) {
299
466
  const last = _i.nonWitnessUtxo.outputs.length - 1;
300
467
  if (_i.index > last)
301
468
  throw new Error(`validateInput: index(${_i.index}) not in nonWitnessUtxo`);
302
- const prevOut = _i.nonWitnessUtxo.outputs[_i.index];
469
+ prevOut = _i.nonWitnessUtxo.outputs[_i.index];
303
470
  if (_i.witnessUtxo &&
304
471
  (!equalBytes(_i.witnessUtxo.script, prevOut.script) ||
305
472
  _i.witnessUtxo.amount !== prevOut.amount))
@@ -320,6 +487,9 @@ function validateInput(i) {
320
487
  allowUnknownOutputs: true,
321
488
  disableScriptCheck: true,
322
489
  allowUnknownInputs: true,
490
+ // Consensus does not restrict nVersion; a previous tx with a non-standard
491
+ // version is still spendable and its txid must still be verifiable.
492
+ allowUnknownVersion: true,
323
493
  });
324
494
  const txid = hex.encode(_i.txid);
325
495
  // BIP174 requires the provided nonWitnessUtxo to hash to the prevout txid even when the
@@ -331,8 +501,43 @@ function validateInput(i) {
331
501
  throw new Error(`nonWitnessUtxo: wrong txid, exp=${txid} got=${tx.id}`);
332
502
  }
333
503
  }
504
+ else if (_i.witnessUtxo)
505
+ prevOut = _i.witnessUtxo;
506
+ if (prevOut && !disableScriptCheck) {
507
+ checkScript(prevOut.script, _i.redeemScript, _i.witnessScript);
508
+ checkTaprootInputCommitments(_i, prevOut.script);
509
+ }
334
510
  return _i;
335
511
  }
512
+ const inputSignatures = (input) => {
513
+ const _input = input;
514
+ const res = [];
515
+ const add = (signature, taproot, scriptPath = false) => {
516
+ const sig = signature;
517
+ if (!sig.length)
518
+ return;
519
+ // Taproot's 64-byte encoding omits the SIGHASH_DEFAULT byte; every other PSBT signature
520
+ // carries its sighash in the final byte, including signatures imported from another signer.
521
+ const sighash = taproot && sig.length === 64 ? SignatureHash.DEFAULT : sig[sig.length - 1];
522
+ res.push({ sighash, taproot, scriptPath });
523
+ };
524
+ for (const [, signature] of _input.partialSig || [])
525
+ add(signature, false);
526
+ if (_input.tapKeySig)
527
+ add(_input.tapKeySig, true);
528
+ for (const [, signature] of _input.tapScriptSig || [])
529
+ add(signature, true, true);
530
+ return res;
531
+ };
532
+ const inputSignedKeys = {
533
+ // sighashType is signer policy rather than a digest byte, but changing it after one signature
534
+ // exists would make later signers interpret the same input under a different policy.
535
+ self: ['txid', 'index', 'sequence', 'nonWitnessUtxo', 'witnessUtxo', 'sighashType'],
536
+ ecdsa: ['redeemScript', 'witnessScript'],
537
+ tapscript: ['tapLeafScript'],
538
+ cross: ['txid', 'index'],
539
+ prevout: ['nonWitnessUtxo', 'witnessUtxo'],
540
+ };
336
541
  // Normalizes input
337
542
  /**
338
543
  * Extracts the previous output referenced by an input.
@@ -346,6 +551,7 @@ function validateInput(i) {
346
551
  * ```
347
552
  */
348
553
  export function getPrevOut(input) {
554
+ validateObject(input, {}, {}, 'input');
349
555
  const _input = input;
350
556
  if (_input.nonWitnessUtxo) {
351
557
  if (_input.index === undefined)
@@ -359,8 +565,15 @@ export function getPrevOut(input) {
359
565
  throw new Error(`Wrong input index=${_input.index}`);
360
566
  return _input.nonWitnessUtxo.outputs[_input.index];
361
567
  }
362
- else if (_input.witnessUtxo)
363
- return _input.witnessUtxo;
568
+ else if ('witnessUtxo' in _input) {
569
+ // The presence check catches malformed provided values; narrow after the guard for TS.
570
+ const prev = _input.witnessUtxo;
571
+ validateObject(prev, {}, {}, 'input.witnessUtxo');
572
+ abigint(prev.amount, 'input.witnessUtxo.amount');
573
+ if (!isBytes(prev.script))
574
+ throw new TypeError('"input.witnessUtxo.script" expected Uint8Array, got type=' + typeof prev.script);
575
+ return prev;
576
+ }
364
577
  else
365
578
  throw new Error('Cannot find previous output info');
366
579
  }
@@ -369,9 +582,12 @@ export function getPrevOut(input) {
369
582
  * @param i - input update to normalize
370
583
  * @param cur - existing input value to merge with
371
584
  * @param allowedFields - fields that may still change on signed inputs
372
- * @param disableScriptCheck - whether to skip redeem/witness script sanity checks
373
- * @param allowUnknown - whether to keep unknown PSBT fields
585
+ * @param disableScriptCheck - whether to skip wrapper and Taproot commitment sanity checks
586
+ * @param unknown - handling policy for unknown PSBT fields
587
+ * @param proprietary - handling policy for proprietary PSBT fields
374
588
  * @returns Normalized PSBT input.
589
+ * @throws If the update conflicts with the existing input or its signatures. {@link Error}
590
+ * @throws If a numeric input field is outside its wire or protocol range. {@link RangeError}
375
591
  * @example
376
592
  * Accept hex txids from callers in the same display-order form used by `Transaction.id`, then
377
593
  * normalize them into the repo's internal `TransactionInput` shape.
@@ -385,7 +601,12 @@ export function getPrevOut(input) {
385
601
  * });
386
602
  * ```
387
603
  */
388
- export function normalizeInput(i, cur, allowedFields, disableScriptCheck = false, allowUnknown = false) {
604
+ export function normalizeInput(i, cur, allowedFields, disableScriptCheck = false, unknown = 'strip', proprietary = 'strip') {
605
+ validateObject(i, {}, {}, 'i');
606
+ if (cur !== undefined)
607
+ validateObject(cur, {}, {}, 'cur');
608
+ if (allowedFields !== undefined)
609
+ u.aarray(allowedFields, 'allowedFields');
389
610
  const _i = i;
390
611
  const _cur = cur;
391
612
  const _allowedFields = allowedFields;
@@ -412,18 +633,19 @@ export function normalizeInput(i, cur, allowedFields, disableScriptCheck = false
412
633
  res.sequence = DEFAULT_SEQUENCE;
413
634
  if (res.tapMerkleRoot === null)
414
635
  delete res.tapMerkleRoot;
415
- res = psbt.mergeKeyMap(psbt.PSBTInput, res, _cur, _allowedFields, allowUnknown);
636
+ res = psbt.mergeKeyMap(psbt.PSBTInput, res, _cur, _allowedFields, unknown, proprietary);
637
+ // An actual empty repeated field emits no PSBT keypairs. Canonicalize only arrays so malformed
638
+ // falsy values still reach the PSBT coder's validation instead of becoming valid absence.
639
+ if (Array.isArray(res.tapLeafScript) && !res.tapLeafScript.length)
640
+ delete res.tapLeafScript;
641
+ validateRequiredLocktimes(res);
416
642
  // Public PSBT coder surface is wrapped with TArg/TRet for TS compatibility; normalizeInput keeps
417
643
  // the repo's historical raw internal shape and casts only at the validation boundary here.
418
644
  psbt.PSBTInputCoder.encode(res); // Validates that everything is correct at this point
419
- let prevOut;
420
- if (res.nonWitnessUtxo && res.index !== undefined)
421
- prevOut = res.nonWitnessUtxo.outputs[res.index];
422
- else if (res.witnessUtxo)
423
- prevOut = res.witnessUtxo;
424
- if (prevOut && !disableScriptCheck)
425
- checkScript(prevOut && prevOut.script, res.redeemScript, res.witnessScript);
426
- return res;
645
+ // Direct construction and UTXO selection consume nonWitnessUtxo amounts without crossing a
646
+ // PSBT serialization boundary. Enforce the same outpoint binding here so a mismatched previous
647
+ // transaction cannot understate a legacy input amount and turn the difference into mining fees.
648
+ return validateInput(res, disableScriptCheck);
427
649
  }
428
650
  /**
429
651
  * Determines how an input should be signed and finalized.
@@ -454,10 +676,13 @@ export function getInputType(input, allowLegacyWitnessUtxo = false) {
454
676
  let txType = 'legacy';
455
677
  let defaultSighash = SignatureHash.ALL;
456
678
  const prevOut = getPrevOut(_input);
457
- const first = OutScript.decode(prevOut.script);
679
+ const first = _WitnessOutScript.decode(prevOut.script);
458
680
  let type = first.type;
459
681
  let cur = first;
460
682
  const stack = [first];
683
+ // Classification is semantic, but legacy/BIP143 scriptCode and finalization must retain the
684
+ // exact committed spelling (including consensus-valid non-minimal pushes).
685
+ let lastScript = prevOut.script;
461
686
  if (first.type === 'tr') {
462
687
  // Expected invariant: taproot inputs use PSBT_IN_TAP_* metadata only;
463
688
  // legacy redeemScript/witnessScript fields belong to P2SH/P2WSH paths.
@@ -477,11 +702,12 @@ export function getInputType(input, allowLegacyWitnessUtxo = false) {
477
702
  if (first.type === 'sh') {
478
703
  if (!_input.redeemScript)
479
704
  throw new Error('inputType: sh without redeemScript');
480
- let child = OutScript.decode(_input.redeemScript);
705
+ let child = _WitnessOutScript.decode(_input.redeemScript);
481
706
  if (child.type === 'wpkh' || child.type === 'wsh')
482
707
  txType = 'segwit';
483
708
  stack.push(child);
484
709
  cur = child;
710
+ lastScript = _input.redeemScript;
485
711
  type += `-${child.type}`;
486
712
  }
487
713
  // wsh can be inside sh
@@ -493,12 +719,12 @@ export function getInputType(input, allowLegacyWitnessUtxo = false) {
493
719
  txType = 'segwit';
494
720
  stack.push(child);
495
721
  cur = child;
722
+ lastScript = _input.witnessScript;
496
723
  type += `-${child.type}`;
497
724
  }
498
725
  const last = stack[stack.length - 1];
499
726
  if (last.type === 'sh' || last.type === 'wsh')
500
727
  throw new Error('inputType: sh/wsh cannot be terminal type');
501
- const lastScript = OutScript.encode(last);
502
728
  const res = {
503
729
  type,
504
730
  txType,
@@ -542,9 +768,61 @@ export class Transaction {
542
768
  constructor(opts = {}) {
543
769
  const _opts = (this.opts = validateOpts(opts));
544
770
  // Merge with global structure of PSBTv2
545
- if (_opts.lockTime !== DEFAULT_LOCKTIME)
546
- this.global.fallbackLocktime = _opts.lockTime;
771
+ // Bitcoin Core sets fallback even when it is zero. Matching its common encoding reduces the
772
+ // fingerprint of locally created PSBTv2s; imported PSBTs replace this map and retain omission.
773
+ this.global.fallbackLocktime = _opts.lockTime;
547
774
  this.global.txVersion = _opts.version;
775
+ // A locally-created PSBTv2 is still under construction. Imported PSBTs replace this global
776
+ // map below, so an omitted field there retains BIP370's immutable meaning.
777
+ if (_opts.PSBTVersion === 2)
778
+ this.global.txModifiable = 0b011;
779
+ }
780
+ isPSBTv2() {
781
+ return (this.global.version ?? this.opts.PSBTVersion) === 2;
782
+ }
783
+ requireTxModifiable(bit, kind) {
784
+ if (!this.isPSBTv2())
785
+ return;
786
+ if (!(this.txModifiablePolicy() & bit))
787
+ throw new Error(`PSBTv2: ${kind} are not modifiable`);
788
+ }
789
+ txModifiablePolicy(allowMissing = this.opts.allowMissingTxModifiable, unknownMode = this.opts.unknown) {
790
+ if (!this.isPSBTv2())
791
+ return 0b011;
792
+ if (this.global.txModifiable !== undefined)
793
+ return cleanTxModifiable(this.global.txModifiable, unknownMode);
794
+ return allowMissing ? 0b011 : 0;
795
+ }
796
+ modifiable(allowMissing = this.opts.allowMissingTxModifiable, unknownMode = this.opts.unknown) {
797
+ let flags = this.txModifiablePolicy(allowMissing, unknownMode);
798
+ let hasOpaqueFinal = false;
799
+ let hasSingle = false;
800
+ for (let idx = 0; idx < this.inputs.length; idx++) {
801
+ const signatures = inputSignatures(this.inputs[idx]);
802
+ if (!signatures.length && this.inputStatus(idx) === 'finalized')
803
+ hasOpaqueFinal = true;
804
+ for (const { sighash } of signatures) {
805
+ const { isAny, isNone, isSingle } = unpackSighash(sighash);
806
+ if (!isAny)
807
+ flags &= ~0b001;
808
+ if (!isNone)
809
+ flags &= ~0b010;
810
+ if (isSingle)
811
+ hasSingle = true;
812
+ }
813
+ }
814
+ // Bit 2 summarizes signatures rather than granting policy. Preserve an imported summary for
815
+ // opaque or externally managed state, and union in every signature visible to this object.
816
+ if (hasSingle)
817
+ flags |= 0b100;
818
+ // PSBTv0 and legacy field-less PSBTv2 cannot describe an opaque finalized sighash. Promotion
819
+ // must therefore deny both mutations instead of manufacturing permissions from absence.
820
+ if (hasOpaqueFinal && this.global.txModifiable === undefined)
821
+ flags &= ~0b011;
822
+ return flags;
823
+ }
824
+ get txModifiable() {
825
+ return this.modifiable();
548
826
  }
549
827
  // Import
550
828
  static fromRaw(raw, opts = {}) {
@@ -584,24 +862,58 @@ export class Transaction {
584
862
  const tx = new Transaction({ ...opts, version, lockTime, PSBTVersion });
585
863
  // We need slice here, because otherwise
586
864
  const inputCount = PSBTVersion === 0 ? unsigned?.inputs.length : parsed.global.inputCount;
587
- tx.inputs = parsed.inputs.slice(0, inputCount).map((i, j) => validateInput({
588
- finalScriptSig: P.EMPTY,
589
- ...parsed.global.unsignedTx?.inputs[j],
590
- ...i,
591
- }));
865
+ tx.inputs = parsed.inputs.slice(0, inputCount).map((i, j) => {
866
+ const input = {
867
+ ...parsed.global.unsignedTx?.inputs[j],
868
+ ...i,
869
+ };
870
+ // The unsigned transaction's empty scriptSig is framing, not a PSBT_IN_FINAL_SCRIPTSIG
871
+ // record. Keeping it makes combination conflict with an otherwise identical finalized PSBT.
872
+ if (!i.finalScriptSig?.length)
873
+ delete input.finalScriptSig;
874
+ return validateInput(input, tx.opts.disableScriptCheck);
875
+ });
592
876
  const outputCount = PSBTVersion === 0 ? unsigned?.outputs.length : parsed.global.outputCount;
593
- tx.outputs = parsed.outputs.slice(0, outputCount).map((i, j) => ({
877
+ // bip174js writes a phantom empty input map when a PSBTv0 transaction has zero inputs. Raw v0
878
+ // framing necessarily reads it as the first output map, so skip it before pairing real maps
879
+ // with the unsigned transaction's declared outputs.
880
+ const hasBip174InputMap = PSBTVersion === 0 &&
881
+ inputCount === 0 &&
882
+ Object.keys(parsed.outputs[0] || {}).length === 0 &&
883
+ ((outputCount > 0 && parsed.outputs.length === outputCount + 1) ||
884
+ (outputCount === 0 &&
885
+ parsed.outputs.length === 2 &&
886
+ Object.keys(parsed.outputs[1]).length === 0));
887
+ const outputStart = hasBip174InputMap ? 1 : 0;
888
+ tx.outputs = parsed.outputs.slice(outputStart, outputStart + outputCount).map((i, j) => ({
594
889
  ...i,
595
890
  ...parsed.global.unsignedTx?.outputs[j],
596
891
  }));
597
- tx.global = { ...parsed.global, txVersion: version }; // just in case proprietary/unknown fields
598
- if (lockTime !== DEFAULT_LOCKTIME)
599
- tx.global.fallbackLocktime = lockTime;
892
+ const unknownMode = tx.opts.unknown;
893
+ const proprietaryMode = tx.opts.proprietary;
894
+ // Unknown PSBT rows can carry opaque metadata between participants. The documented default is
895
+ // to strip them; callers that need forward compatibility must opt in explicitly. Proprietary
896
+ // (0xfc) rows follow the same explicit policy, which defaults to the resolved unknown mode.
897
+ tx.global = cleanExtensions({ ...parsed.global, txVersion: version }, unknownMode, proprietaryMode);
898
+ tx.inputs = tx.inputs.map((input) => cleanExtensions(input, unknownMode, proprietaryMode));
899
+ tx.outputs = tx.outputs.map((output) => cleanExtensions(output, unknownMode, proprietaryMode));
900
+ if (tx.global.txModifiable !== undefined)
901
+ tx.global.txModifiable = cleanTxModifiable(tx.global.txModifiable, unknownMode);
902
+ // A high-level Transaction must have a determinable nLockTime. Raw PSBT coders can still be
903
+ // used by callers that need to inspect or relay a structurally valid but incompatible PSBT.
904
+ resolvePSBTLocktime(tx.inputs, tx.global.fallbackLocktime ?? DEFAULT_LOCKTIME);
905
+ // PSBTv0 always provides nLockTime in its unsigned transaction. Retain zero internally too so
906
+ // promotion to v2 matches fresh construction and Bitcoin Core rather than gaining a
907
+ // fingerprint.
908
+ if (PSBTVersion === 0)
909
+ tx.global.fallbackLocktime = def(lockTime, DEFAULT_LOCKTIME);
600
910
  return tx;
601
911
  }
602
912
  // Prefer `global.version` when present so cross-version combiners can serialize at the highest
603
913
  // required PSBT version without mutating the frozen transaction options object.
604
914
  toPSBT(PSBTVersion = this.global.version || this.opts.PSBTVersion) {
915
+ if (PSBTVersion !== undefined)
916
+ anumber(PSBTVersion, 'PSBTVersion');
605
917
  if (PSBTVersion !== 0 && PSBTVersion !== 2)
606
918
  throw new Error(`Wrong PSBT version=${PSBTVersion}`);
607
919
  // if (PSBTVersion === 0 && this.inputs.length === 0) {
@@ -609,10 +921,10 @@ export class Transaction {
609
921
  // 'PSBT version=0 export for transaction without inputs disabled, please use version=2. Please check `toPSBT` method for explanation.'
610
922
  // );
611
923
  // }
612
- const inputs = this.inputs.map((i) =>
924
+ const inputs = this.inputs.map((i) => cleanExtensions(
613
925
  // For PSBTv0 the prevout txid/index live in global.unsignedTx rather than the input map, so
614
926
  // validate the full transaction input before version filtering drops those fields.
615
- psbt.cleanPSBTFields(PSBTVersion, psbt.PSBTInput, validateInput(i)));
927
+ psbt.cleanPSBTFields(PSBTVersion, psbt.PSBTInput, validateInput(i, this.opts.disableScriptCheck)), this.opts.unknown, this.opts.proprietary));
616
928
  for (const inp of inputs) {
617
929
  // Don't serialize empty fields
618
930
  if (inp.partialSig && !inp.partialSig.length)
@@ -622,8 +934,10 @@ export class Transaction {
622
934
  if (inp.finalScriptWitness && !inp.finalScriptWitness.length)
623
935
  delete inp.finalScriptWitness;
624
936
  }
625
- const outputs = this.outputs.map((i) => psbt.cleanPSBTFields(PSBTVersion, psbt.PSBTOutput, i));
626
- const global = { ...this.global };
937
+ const outputs = this.outputs.map((i) => cleanExtensions(psbt.cleanPSBTFields(PSBTVersion, psbt.PSBTOutput, i), this.opts.unknown, this.opts.proprietary));
938
+ const global = cleanExtensions({ ...this.global }, this.opts.unknown, this.opts.proprietary);
939
+ if (global.txModifiable !== undefined)
940
+ global.txModifiable = cleanTxModifiable(global.txModifiable, this.opts.unknown);
627
941
  if (PSBTVersion === 0) {
628
942
  /*
629
943
  - Bitcoin raw transaction expects to have at least 1 input because it uses case with zero inputs as marker for SegWit
@@ -646,10 +960,12 @@ export class Transaction {
646
960
  delete global.txVersion;
647
961
  // PSBTv0 carries the unsigned transaction as one blob, so the PSBTv2 framing fields must be
648
962
  // removed here. Keeping `global.version` would make validation treat this rebuilt v0 map as
649
- // PSBTv2 and reject the required `unsignedTx` field.
963
+ // PSBTv2 and reject the required `unsignedTx` field. Transaction-modifiable is also v2-only;
964
+ // its restrictions remain represented by the signatures when explicitly converting to v0.
650
965
  delete global.inputCount;
651
966
  delete global.outputCount;
652
967
  delete global.version;
968
+ delete global.txModifiable;
653
969
  }
654
970
  else {
655
971
  // Cross-version merges and v0->v2 re-exports can still carry the PSBTv0 unsignedTx blob in
@@ -660,15 +976,18 @@ export class Transaction {
660
976
  global.txVersion = this.version;
661
977
  global.inputCount = this.inputs.length;
662
978
  global.outputCount = this.outputs.length;
663
- if (global.fallbackLocktime && global.fallbackLocktime === DEFAULT_LOCKTIME)
664
- delete global.fallbackLocktime;
665
- }
666
- if (this.opts.bip174jsCompat) {
667
- if (!inputs.length)
668
- inputs.push({});
669
- if (!outputs.length)
670
- outputs.push({});
979
+ // Core serializes this optional field exactly as stored. Preserve no-op v2 round-trips;
980
+ // only v0 promotion and the explicit legacy-omission compatibility mode materialize policy.
981
+ if (!this.isPSBTv2() ||
982
+ (global.txModifiable === undefined && this.opts.allowMissingTxModifiable))
983
+ global.txModifiable = this.txModifiable;
671
984
  }
985
+ // bip174js historically emits one empty output map for a PSBTv0 transaction with no outputs.
986
+ // Input maps are count-framed by the unsigned transaction, so a phantom input map cannot be
987
+ // represented: with zero inputs it would be decoded as an output map instead. PSBTv2 has
988
+ // explicit counts for both map arrays and does not use this compatibility encoding.
989
+ if (this.opts.bip174jsCompat && PSBTVersion === 0 && !outputs.length)
990
+ outputs.push({});
672
991
  const raw = { global, inputs, outputs };
673
992
  return PSBTVersion === 0
674
993
  ? psbt.RawPSBTV0.encode(raw)
@@ -676,25 +995,7 @@ export class Transaction {
676
995
  }
677
996
  // BIP370 lockTime (https://github.com/bitcoin/bips/blob/master/bip-0370.mediawiki#determining-lock-time)
678
997
  get lockTime() {
679
- let height = DEFAULT_LOCKTIME;
680
- let heightCnt = 0;
681
- let time = DEFAULT_LOCKTIME;
682
- let timeCnt = 0;
683
- for (const i of this.inputs) {
684
- if (i.requiredHeightLocktime) {
685
- height = Math.max(height, i.requiredHeightLocktime);
686
- heightCnt++;
687
- }
688
- if (i.requiredTimeLocktime) {
689
- time = Math.max(time, i.requiredTimeLocktime);
690
- timeCnt++;
691
- }
692
- }
693
- if (heightCnt && heightCnt >= timeCnt)
694
- return height;
695
- if (time !== DEFAULT_LOCKTIME)
696
- return time;
697
- return this.global.fallbackLocktime || DEFAULT_LOCKTIME;
998
+ return resolvePSBTLocktime(this.inputs, this.global.fallbackLocktime ?? DEFAULT_LOCKTIME);
698
999
  }
699
1000
  get version() {
700
1001
  // Should be not possible
@@ -720,6 +1021,13 @@ export class Transaction {
720
1021
  return 'signed';
721
1022
  return 'unsigned';
722
1023
  }
1024
+ cleanFinalInput(input) {
1025
+ // Core preserves producer policy during finalization. Once cleanup makes signatures opaque,
1026
+ // signStatus conservatively locks transaction mutation until the input is explicitly reopened.
1027
+ cleanFinalInput(input, this.opts.unknown, this.opts.proprietary);
1028
+ if (this.global.txModifiable !== undefined)
1029
+ this.global.txModifiable = cleanTxModifiable(this.global.txModifiable, this.opts.unknown);
1030
+ }
723
1031
  // Cannot replace unpackSighash, tests rely on very generic implemenetation with signing inputs outside of range
724
1032
  // We will lose some vectors -> smaller test coverage of preimages (very important!)
725
1033
  inputSighash(idx) {
@@ -732,32 +1040,81 @@ export class Transaction {
732
1040
  // ALL + ANYONE -- specific input + all outputs
733
1041
  // NONE + ANYONE -- specific input + no outputs
734
1042
  // SINGLE -- specific inputs + output with same index
735
- const sigOutputs = sighash === SignatureHash.DEFAULT ? SignatureHash.ALL : sighash & 0b11;
736
- const sigInputs = sighash & SignatureHash.ANYONECANPAY;
737
- return { sigInputs, sigOutputs };
1043
+ return sighashScope(sighash);
738
1044
  }
739
1045
  // Very nice for debug purposes, but slow. If there is too much inputs/outputs to add, will be quadratic.
740
1046
  // Some cache will be nice, but there chance to have bugs with cache invalidation
741
- signStatus() {
1047
+ signatures() {
1048
+ const res = [];
1049
+ for (let idx = 0; idx < this.inputs.length; idx++) {
1050
+ const actual = inputSignatures(this.inputs[idx]);
1051
+ for (const signature of actual)
1052
+ res.push({ idx, ...signature });
1053
+ if (actual.length || this.inputStatus(idx) !== 'finalized')
1054
+ continue;
1055
+ let taproot = true;
1056
+ try {
1057
+ taproot = _WitnessOutScript.decode(getPrevOut(this.inputs[idx]).script).type === 'tr';
1058
+ }
1059
+ catch {
1060
+ // Finalization may remove the data needed to classify an imported opaque signature.
1061
+ // Treating it as Taproot conservatively protects the larger all-prevout commitment.
1062
+ }
1063
+ const declared = this.inputs[idx].sighashType;
1064
+ const sighash = declared === undefined ? SignatureHash.DEFAULT : declared;
1065
+ res.push({ idx, sighash, taproot, scriptPath: taproot });
1066
+ }
1067
+ return res;
1068
+ }
1069
+ signedInputKeys(idx, signatures = this.signatures()) {
1070
+ const res = new Set();
1071
+ const add = (keys) => {
1072
+ for (const key of keys)
1073
+ res.add(key);
1074
+ };
1075
+ for (const signature of signatures) {
1076
+ const { isAny, isNone, isSingle } = unpackSighash(signature.sighash);
1077
+ if (signature.idx === idx) {
1078
+ add(inputSignedKeys.self);
1079
+ if (signature.taproot) {
1080
+ if (signature.scriptPath)
1081
+ add(inputSignedKeys.tapscript);
1082
+ }
1083
+ else
1084
+ add(inputSignedKeys.ecdsa);
1085
+ }
1086
+ else if (!isAny) {
1087
+ add(inputSignedKeys.cross);
1088
+ // Legacy and BIP143 omit other sequences for NONE/SINGLE. BIP341 commits every sequence
1089
+ // whenever ANYONECANPAY is absent, independently of the output sighash mode.
1090
+ if (signature.taproot || (!isNone && !isSingle))
1091
+ res.add('sequence');
1092
+ if (signature.taproot)
1093
+ add(inputSignedKeys.prevout);
1094
+ }
1095
+ }
1096
+ return [...res];
1097
+ }
1098
+ signStatus(signatures = this.signatures()) {
742
1099
  // if addInput or addOutput is not possible, then all inputs or outputs are signed
743
1100
  let addInput = true, addOutput = true;
744
1101
  let inputs = [], outputs = [];
745
- for (let idx = 0; idx < this.inputs.length; idx++) {
746
- const status = this.inputStatus(idx);
747
- // Unsigned input doesn't affect anything
748
- if (status === 'unsigned')
749
- continue;
750
- const { sigInputs, sigOutputs } = this.inputSighash(idx);
1102
+ for (const { idx, sighash } of signatures) {
1103
+ const { sigInputs, sigOutputs } = sighashScope(sighash);
751
1104
  // Input type
752
- if (sigInputs === SignatureHash.ANYONECANPAY)
753
- inputs.push(idx);
1105
+ if (sigInputs === SignatureHash.ANYONECANPAY) {
1106
+ if (!inputs.includes(idx))
1107
+ inputs.push(idx);
1108
+ }
754
1109
  else
755
1110
  addInput = false;
756
1111
  // Output type
757
1112
  if (sigOutputs === SignatureHash.ALL)
758
1113
  addOutput = false;
759
- else if (sigOutputs === SignatureHash.SINGLE)
760
- outputs.push(idx);
1114
+ else if (sigOutputs === SignatureHash.SINGLE) {
1115
+ if (!outputs.includes(idx))
1116
+ outputs.push(idx);
1117
+ }
761
1118
  else if (sigOutputs === SignatureHash.NONE) {
762
1119
  // Doesn't affect any outputs at all
763
1120
  }
@@ -774,32 +1131,38 @@ export class Transaction {
774
1131
  }
775
1132
  // Info utils
776
1133
  get hasWitnesses() {
777
- let out = false;
778
1134
  for (const i of this.inputs)
779
1135
  if (i.finalScriptWitness && i.finalScriptWitness.length)
780
- out = true;
781
- return out;
1136
+ return true;
1137
+ return false;
782
1138
  }
783
1139
  // https://en.bitcoin.it/wiki/Weight_units
784
1140
  get weight() {
785
1141
  if (!this.isFinal)
786
1142
  throw new Error('Transaction is not finalized');
1143
+ // Serialized length of VarBytes(data) without allocating the encoded copy
1144
+ const varLen = (dataLen) => CompactSizeLen.encode(dataLen).length + dataLen;
1145
+ const hasWitnesses = this.hasWitnesses;
787
1146
  let out = 32;
788
1147
  // Outputs
789
1148
  const outputs = this.outputs.map(outputBeforeSign);
790
1149
  out += 4 * CompactSizeLen.encode(this.outputs.length).length;
791
1150
  for (const o of outputs)
792
- out += 32 + 4 * VarBytes.encode(o.script).length;
1151
+ out += 32 + 4 * varLen(o.script.length);
793
1152
  // Inputs
794
- if (this.hasWitnesses)
1153
+ if (hasWitnesses)
795
1154
  out += 2;
796
1155
  out += 4 * CompactSizeLen.encode(this.inputs.length).length;
797
1156
  for (const i of this.inputs) {
798
- out += 160 + 4 * VarBytes.encode(i.finalScriptSig || P.EMPTY).length;
1157
+ out += 160 + 4 * varLen((i.finalScriptSig || P.EMPTY).length);
799
1158
  // Once segwit serialization is active, every input contributes one witness vector, including
800
1159
  // legacy inputs whose empty vector still encodes as a single zero-item-count byte.
801
- if (this.hasWitnesses)
802
- out += RawWitness.encode(i.finalScriptWitness || []).length;
1160
+ if (hasWitnesses) {
1161
+ const witness = i.finalScriptWitness || [];
1162
+ out += CompactSizeLen.encode(witness.length).length;
1163
+ for (const w of witness)
1164
+ out += varLen(w.length);
1165
+ }
803
1166
  }
804
1167
  return out;
805
1168
  }
@@ -833,9 +1196,27 @@ export class Transaction {
833
1196
  }
834
1197
  // Input stuff
835
1198
  checkInputIdx(idx) {
836
- if (!Number.isSafeInteger(idx) || 0 > idx || idx >= this.inputs.length)
1199
+ anumber(idx, 'idx');
1200
+ if (idx >= this.inputs.length)
837
1201
  throw new Error(`Wrong input index=${idx}`);
838
1202
  }
1203
+ validatePrevoutsForSigning() {
1204
+ if (!this.opts.strictPrevoutValidation)
1205
+ return;
1206
+ for (let i = 0; i < this.inputs.length; i++) {
1207
+ const input = this.inputs[i];
1208
+ if (!input.nonWitnessUtxo) {
1209
+ throw new Error(`Transaction/sign: strictPrevoutValidation requires nonWitnessUtxo for input=${i}`);
1210
+ }
1211
+ if (input.txid === undefined || input.index === undefined) {
1212
+ throw new Error(`Transaction/sign: strictPrevoutValidation requires an outpoint for input=${i}`);
1213
+ }
1214
+ // A full previous transaction is only a trusted amount/script commitment after its txid and
1215
+ // selected output have been checked against the unsigned transaction. Also cross-check a
1216
+ // redundant witnessUtxo when one is present.
1217
+ validateInput(input, this.opts.disableScriptCheck);
1218
+ }
1219
+ }
839
1220
  getInput(idx) {
840
1221
  this.checkInputIdx(idx);
841
1222
  return cloneDeep(this.inputs[idx]);
@@ -845,28 +1226,63 @@ export class Transaction {
845
1226
  }
846
1227
  // Modification
847
1228
  addInput(input, _ignoreSignStatus = false) {
848
- if (!_ignoreSignStatus && !this.signStatus().addInput)
1229
+ validateObject(input, {}, {}, 'input');
1230
+ cleanExtensions(input, this.opts.unknown, this.opts.proprietary, true);
1231
+ this.requireTxModifiable(0b001, 'inputs');
1232
+ const signatures = _ignoreSignStatus ? undefined : this.signatures();
1233
+ const status = signatures && this.signStatus(signatures);
1234
+ if (status && !status.addInput)
849
1235
  throw new Error('Tx has signed inputs, cannot add new one');
850
1236
  // normalizeInput preserves nested caller-owned byte arrays, so detach them here before the
851
1237
  // new input becomes transaction state and later caller mutation can rewrite it by aliasing.
852
- this.inputs.push(cloneDeep(normalizeInput(input, undefined, undefined, this.opts.disableScriptCheck)));
1238
+ const normalized = cloneDeep(normalizeInput(input, undefined, undefined, this.opts.disableScriptCheck, this.opts.unknown, this.opts.proprietary));
1239
+ const nextLockTime = resolvePSBTLocktime([...this.inputs, normalized], this.global.fallbackLocktime ?? DEFAULT_LOCKTIME);
1240
+ // ANYONECANPAY permits adding an outpoint, but every signature still commits to nLockTime.
1241
+ if (signatures?.length && nextLockTime !== this.lockTime)
1242
+ throw new Error('Tx has signed inputs, cannot change lockTime');
1243
+ this.inputs.push(normalized);
853
1244
  return this.inputs.length - 1;
854
1245
  }
855
1246
  updateInput(idx, input, _ignoreSignStatus = false) {
856
1247
  this.checkInputIdx(idx);
857
- let allowedFields = undefined;
858
- if (!_ignoreSignStatus) {
859
- const status = this.signStatus();
860
- if (!status.addInput || status.inputs.includes(idx))
861
- allowedFields = psbt.PSBTInputUnsignedKeys;
1248
+ cleanExtensions(input, this.opts.unknown, this.opts.proprietary, true);
1249
+ let allowedFields;
1250
+ const signatures = _ignoreSignStatus ? undefined : this.signatures();
1251
+ if (signatures?.length) {
1252
+ if (this.inputStatus(idx) === 'finalized') {
1253
+ // Once finalized, only already-present signature/final fields may be repeated or removed.
1254
+ // In particular, do not let a native-SegWit final witness gain a stray finalScriptSig.
1255
+ allowedFields = psbt.PSBTInputSignatureKeys.filter((key) => this.inputs[idx][key] !== undefined);
1256
+ }
1257
+ else {
1258
+ const signed = new Set(this.signedInputKeys(idx, signatures));
1259
+ if (signed.size)
1260
+ allowedFields = Object.keys(psbt.PSBTInput).filter((key) => !signed.has(key));
1261
+ }
862
1262
  }
863
1263
  // normalizeInput preserves nested caller-owned byte arrays, so detach the merged result here
864
1264
  // before the updated input becomes transaction state and later caller mutation can rewrite it.
865
- this.inputs[idx] = cloneDeep(normalizeInput(input, this.inputs[idx], allowedFields, this.opts.disableScriptCheck, this.opts.allowUnknown));
1265
+ const normalized = cloneDeep(normalizeInput(input, this.inputs[idx], allowedFields, this.opts.disableScriptCheck, this.opts.unknown, this.opts.proprietary));
1266
+ const inputs = this.inputs.slice();
1267
+ inputs[idx] = normalized;
1268
+ const nextLockTime = resolvePSBTLocktime(inputs, this.global.fallbackLocktime ?? DEFAULT_LOCKTIME);
1269
+ if (signatures?.length && nextLockTime !== this.lockTime)
1270
+ throw new Error('Tx has signed inputs, cannot change lockTime');
1271
+ const current = this.inputs[idx];
1272
+ const transactionChanged = current.index !== normalized.index ||
1273
+ def(current.sequence, DEFAULT_SEQUENCE) !== def(normalized.sequence, DEFAULT_SEQUENCE) ||
1274
+ (current.txid === undefined
1275
+ ? normalized.txid !== undefined
1276
+ : normalized.txid === undefined || !equalBytes(current.txid, normalized.txid)) ||
1277
+ nextLockTime !== this.lockTime;
1278
+ if (transactionChanged)
1279
+ this.requireTxModifiable(0b001, 'inputs');
1280
+ this.inputs[idx] = normalized;
866
1281
  }
867
1282
  // Output stuff
868
1283
  checkOutputIdx(idx) {
869
- if (!Number.isSafeInteger(idx) || 0 > idx || idx >= this.outputs.length)
1284
+ anumber(idx, 'idx');
1285
+ if (idx >= this.outputs.length)
870
1286
  throw new Error(`Wrong output index=${idx}`);
871
1287
  }
872
1288
  getOutput(idx) {
@@ -877,17 +1293,17 @@ export class Transaction {
877
1293
  const out = this.getOutput(idx);
878
1294
  if (!out.script)
879
1295
  return;
880
- return Address(network).encode(OutScript.decode(out.script));
1296
+ return Address(network).encode(_WitnessOutScript.decode(out.script));
881
1297
  }
882
1298
  get outputsLength() {
883
1299
  return this.outputs.length;
884
1300
  }
885
1301
  normalizeOutput(o, cur, allowedFields) {
1302
+ validateObject(o, {}, {}, 'o');
886
1303
  let { amount, script } = o;
887
1304
  if (amount === undefined)
888
1305
  amount = cur?.amount;
889
- if (typeof amount !== 'bigint')
890
- throw new Error(`Wrong amount type, should be of type bigint in sats, but got ${amount} of type ${typeof amount}`);
1306
+ amount = abigint(amount, 'o.amount');
891
1307
  if (typeof script === 'string')
892
1308
  script = hex.decode(script);
893
1309
  if (script === undefined)
@@ -895,11 +1311,11 @@ export class Transaction {
895
1311
  let res = { ...cur, ...o, amount, script };
896
1312
  if (res.amount === undefined)
897
1313
  delete res.amount;
898
- res = psbt.mergeKeyMap(psbt.PSBTOutput, res, cur, allowedFields, this.opts.allowUnknown);
1314
+ res = psbt.mergeKeyMap(psbt.PSBTOutput, res, cur, allowedFields, this.opts.unknown, this.opts.proprietary);
899
1315
  psbt.PSBTOutputCoder.encode(res);
900
1316
  if (res.script &&
901
1317
  !this.opts.allowUnknownOutputs &&
902
- OutScript.decode(res.script).type === 'unknown') {
1318
+ _WitnessOutScript.decode(res.script).type === 'unknown') {
903
1319
  throw new Error('Transaction/output: unknown output script type, there is a chance that input is unspendable. Pass allowUnknownOutputs=true, if you sure');
904
1320
  }
905
1321
  if (!this.opts.disableScriptCheck)
@@ -907,7 +1323,11 @@ export class Transaction {
907
1323
  return res;
908
1324
  }
909
1325
  addOutput(o, _ignoreSignStatus = false) {
910
- if (!_ignoreSignStatus && !this.signStatus().addOutput)
1326
+ cleanExtensions(o, this.opts.unknown, this.opts.proprietary, true);
1327
+ this.requireTxModifiable(0b010, 'outputs');
1328
+ const status = _ignoreSignStatus ? undefined : this.signStatus();
1329
+ // Appending the previously missing same-index output changes a SIGHASH_SINGLE digest.
1330
+ if (status && (!status.addOutput || status.outputs.includes(this.outputs.length)))
911
1331
  throw new Error('Tx has signed outputs, cannot add new one');
912
1332
  // normalizeOutput preserves nested caller-owned script bytes, so detach them here before the
913
1333
  // new output becomes transaction state and later caller mutation can rewrite it by aliasing.
@@ -916,6 +1336,7 @@ export class Transaction {
916
1336
  }
917
1337
  updateOutput(idx, output, _ignoreSignStatus = false) {
918
1338
  this.checkOutputIdx(idx);
1339
+ cleanExtensions(output, this.opts.unknown, this.opts.proprietary, true);
919
1340
  let allowedFields = undefined;
920
1341
  if (!_ignoreSignStatus) {
921
1342
  const status = this.signStatus();
@@ -924,7 +1345,15 @@ export class Transaction {
924
1345
  }
925
1346
  // updateOutput replaces stored state with normalizeOutput(...) directly, so detach the result
926
1347
  // before storing it or later caller mutation of `output.script` will rewrite transaction state.
927
- this.outputs[idx] = cloneDeep(this.normalizeOutput(output, this.outputs[idx], allowedFields));
1348
+ const current = this.outputs[idx];
1349
+ const normalized = cloneDeep(this.normalizeOutput(output, current, allowedFields));
1350
+ const transactionChanged = current.amount !== normalized.amount ||
1351
+ (current.script === undefined
1352
+ ? normalized.script !== undefined
1353
+ : normalized.script === undefined || !equalBytes(current.script, normalized.script));
1354
+ if (transactionChanged)
1355
+ this.requireTxModifiable(0b010, 'outputs');
1356
+ this.outputs[idx] = normalized;
928
1357
  }
929
1358
  addOutputAddress(address, amount, network = NETWORK) {
930
1359
  return this.addOutput({
@@ -936,7 +1365,7 @@ export class Transaction {
936
1365
  }
937
1366
  // Utils
938
1367
  get fee() {
939
- let res = 0n;
1368
+ let res = _0n;
940
1369
  for (const i of this.inputs) {
941
1370
  const prevOut = getPrevOut(i);
942
1371
  if (!prevOut)
@@ -957,7 +1386,8 @@ export class Transaction {
957
1386
  if (idx < 0 || !Number.isSafeInteger(idx))
958
1387
  throw new Error(`Invalid input idx=${idx}`);
959
1388
  if ((isSingle && idx >= this.outputs.length) || idx >= this.inputs.length)
960
- return P.U256BE.encode(1n);
1389
+ // Bitcoin Core passes uint256::ONE's internal little-endian bytes directly to ECDSA.
1390
+ return P.U256LE.encode(_1n);
961
1391
  prevOutScript = stripCodeSeparator(prevOutScript);
962
1392
  let inputs = this.inputs
963
1393
  .map(inputBeforeSign)
@@ -994,7 +1424,8 @@ export class Transaction {
994
1424
  preimageWitnessV0(idx, prevOutScript, hashType, amount) {
995
1425
  // BIP143 serializes txTo.vin[nIn].prevout and txTo.vin[nIn].nSequence, so reject an invalid
996
1426
  // nIn explicitly instead of leaking a later undefined-input TypeError from inputs[idx].
997
- if (idx < 0 || !Number.isSafeInteger(idx) || idx >= this.inputs.length)
1427
+ anumber(idx, 'idx');
1428
+ if (idx >= this.inputs.length)
998
1429
  throw new Error(`Invalid input idx=${idx}`);
999
1430
  const { isAny, isNone, isSingle } = unpackSighash(hashType);
1000
1431
  let inputHash = EMPTY32;
@@ -1015,15 +1446,18 @@ export class Transaction {
1015
1446
  return u.sha256x2(P.I32LE.encode(this.version), inputHash, sequenceHash, P.bytes(32, true).encode(input.txid), P.U32LE.encode(input.index), VarBytes.encode(prevOutScript), P.U64LE.encode(amount), P.U32LE.encode(input.sequence), outputHash, P.U32LE.encode(this.lockTime), P.U32LE.encode(hashType));
1016
1447
  }
1017
1448
  preimageWitnessV1(idx, prevOutScript, hashType, amount, codeSeparator = -1, leafScript, leafVer = 0xc0, annex) {
1018
- if (!Array.isArray(amount) || this.inputs.length !== amount.length)
1019
- throw new Error(`Invalid amounts array=${amount}`);
1020
- if (!Array.isArray(prevOutScript) || this.inputs.length !== prevOutScript.length)
1021
- throw new Error(`Invalid prevOutScript array=${prevOutScript}`);
1022
1449
  // BIP341 SigMsg commits either to input_index or to the selected input's outpoint/amount/script/
1023
1450
  // sequence under ANYONECANPAY, so reject an invalid index explicitly instead of hashing a
1024
1451
  // nonexistent input or leaking a later integer-encoding RangeError for negative idx.
1025
- if (idx < 0 || !Number.isSafeInteger(idx) || idx >= this.inputs.length)
1452
+ anumber(idx, 'idx');
1453
+ if (idx >= this.inputs.length)
1026
1454
  throw new Error(`Invalid input idx=${idx}`);
1455
+ u.aarray(amount, 'amount');
1456
+ u.aarray(prevOutScript, 'prevOutScript');
1457
+ if (this.inputs.length !== amount.length)
1458
+ throw new Error(`Invalid amounts array=${amount}`);
1459
+ if (this.inputs.length !== prevOutScript.length)
1460
+ throw new Error(`Invalid prevOutScript array=${prevOutScript}`);
1027
1461
  const out = [
1028
1462
  P.U8.encode(0),
1029
1463
  P.U8.encode(hashType), // U8 sigHash
@@ -1034,6 +1468,12 @@ export class Transaction {
1034
1468
  const inType = hashType & SignatureHash.ANYONECANPAY;
1035
1469
  const inputs = this.inputs.map(inputBeforeSign);
1036
1470
  const outputs = this.outputs.map(outputBeforeSign);
1471
+ // Unlike legacy and segwit v0, BIP341 defines no digest for SINGLE when the
1472
+ // corresponding output does not exist. Returning a digest here would produce
1473
+ // signatures that consensus can never accept.
1474
+ if (outType === SignatureHash.SINGLE && idx >= outputs.length) {
1475
+ throw new Error(`Input with sighash SINGLE, but there is no output with corresponding index=${idx}`);
1476
+ }
1037
1477
  if (inType !== SignatureHash.ANYONECANPAY) {
1038
1478
  out.push(...[
1039
1479
  inputs.map(TxHashIdx.encode),
@@ -1056,15 +1496,24 @@ export class Transaction {
1056
1496
  if (spendType & 1)
1057
1497
  out.push(u.sha256(VarBytes.encode(annex || P.EMPTY)));
1058
1498
  if (outType === SignatureHash.SINGLE)
1059
- out.push(idx < outputs.length ? u.sha256(RawOutput.encode(outputs[idx])) : EMPTY32);
1499
+ out.push(u.sha256(RawOutput.encode(outputs[idx])));
1060
1500
  if (leafScript)
1061
1501
  out.push(tapLeafHash(leafScript, leafVer), P.U8.encode(0), P.I32LE.encode(codeSeparator));
1062
1502
  return u.tagSchnorr('TapSighash', ...out);
1063
1503
  }
1064
1504
  // Signer can be privateKey OR instance of bip32 HD stuff
1065
1505
  signIdx(privateKey, idx, allowedSighash, _auxRand) {
1506
+ if (!isBytes(privateKey)) {
1507
+ // HDKey is a structural external instance, so plain-object validation would
1508
+ // reject valid signers.
1509
+ if (!privateKey ||
1510
+ typeof privateKey !== 'object' ||
1511
+ typeof privateKey.deriveChild !== 'function')
1512
+ throw new TypeError('"privateKey" expected Uint8Array or HDKey, got type=' + typeof privateKey);
1513
+ }
1066
1514
  this.checkInputIdx(idx);
1067
- const input = this.inputs[idx];
1515
+ this.validatePrevoutsForSigning();
1516
+ const input = validateInput(this.inputs[idx], this.opts.disableScriptCheck);
1068
1517
  const inputType = getInputType(input, this.opts.allowLegacyWitnessUtxo);
1069
1518
  const canSign = (privateKey) => {
1070
1519
  if (inputType.txType === 'taproot') {
@@ -1246,6 +1695,9 @@ export class Transaction {
1246
1695
  // Even worse: another user can add bip32 derivation, and spend money from different address.
1247
1696
  // Better api: signIdx
1248
1697
  sign(privateKey, allowedSighash, _auxRand) {
1698
+ // Check transaction-wide strict requirements outside the per-input catch below so callers get
1699
+ // the actionable validation error instead of the generic "No inputs signed" result.
1700
+ this.validatePrevoutsForSigning();
1249
1701
  let num = 0;
1250
1702
  for (let i = 0; i < this.inputs.length; i++) {
1251
1703
  try {
@@ -1260,18 +1712,26 @@ export class Transaction {
1260
1712
  }
1261
1713
  finalizeIdx(idx) {
1262
1714
  this.checkInputIdx(idx);
1263
- if (this.fee < 0n)
1715
+ if (this.fee < _0n)
1264
1716
  throw new Error('Outputs spends more than inputs amount');
1265
1717
  const input = this.inputs[idx];
1718
+ // Validate strict extension policy before constructing satisfaction so a rejection is atomic.
1719
+ cleanExtensions(input, this.opts.unknown, this.opts.proprietary);
1720
+ cleanTxModifiable(this.global.txModifiable, this.opts.unknown);
1266
1721
  const inputType = getInputType(input, this.opts.allowLegacyWitnessUtxo);
1267
1722
  // Taproot finalize
1268
1723
  if (inputType.txType === 'taproot') {
1269
1724
  if (input.tapKeySig)
1270
1725
  input.finalScriptWitness = [input.tapKeySig];
1271
1726
  else if (input.tapLeafScript && input.tapScriptSig) {
1272
- // Sort leafs by control block length.
1273
- const leafs = input.tapLeafScript.sort((a, b) => psbt.TaprootControlBlock.encode(a[0]).length -
1727
+ // Preserve the old shallowest-path tie-break without mutating caller-visible leaf order.
1728
+ const leafs = input.tapLeafScript
1729
+ .slice()
1730
+ .sort((a, b) => psbt.TaprootControlBlock.encode(a[0]).length -
1274
1731
  psbt.TaprootControlBlock.encode(b[0]).length);
1732
+ let smallest;
1733
+ let smallestSize = Number.POSITIVE_INFINITY;
1734
+ let unsupported = false;
1275
1735
  for (const [cb, _script] of leafs) {
1276
1736
  // Last byte is version
1277
1737
  const script = _script.slice(0, -1);
@@ -1280,6 +1740,7 @@ export class Transaction {
1280
1740
  const hash = tapLeafHash(script, ver);
1281
1741
  const scriptSig = input.tapScriptSig.filter((i) => equalBytes(i[0].leafHash, hash));
1282
1742
  let signatures = [];
1743
+ let witness;
1283
1744
  if (outScript.type === 'tr_ms') {
1284
1745
  const m = outScript.m;
1285
1746
  const pubkeys = outScript.pubkeys;
@@ -1326,6 +1787,7 @@ export class Transaction {
1326
1787
  }
1327
1788
  else {
1328
1789
  const custom = this.opts.customScripts;
1790
+ let recognized = false;
1329
1791
  if (custom) {
1330
1792
  for (const c of custom) {
1331
1793
  if (!c.finalizeTaproot)
@@ -1334,31 +1796,44 @@ export class Transaction {
1334
1796
  const csEncoded = c.encode(scriptDecoded);
1335
1797
  if (csEncoded === undefined)
1336
1798
  continue;
1799
+ recognized = true;
1800
+ // Do not catch hook errors here. `undefined` means no satisfaction, while a throw
1801
+ // from a matching custom finalizer reports broken leaf/signature data and must
1802
+ // abort even when another leaf has already produced a valid witness candidate.
1337
1803
  const finalized = c.finalizeTaproot(script, csEncoded, scriptSig);
1338
1804
  if (!finalized)
1339
1805
  continue;
1340
- input.finalScriptWitness = finalized.concat(psbt.TaprootControlBlock.encode(cb));
1341
- delete input.finalScriptSig;
1342
- cleanFinalInput(input);
1343
- return;
1806
+ witness = finalized.concat(psbt.TaprootControlBlock.encode(cb));
1807
+ break;
1344
1808
  }
1345
1809
  }
1346
- throw new Error('Finalize: Unknown tapLeafScript');
1810
+ // Minimum search inspects every leaf, so an unsupported path cannot block an already
1811
+ // complete known path merely because it appears later. Retain the old error when no
1812
+ // supported satisfaction exists at all.
1813
+ if (!witness && !recognized && scriptSig.length)
1814
+ unsupported = true;
1815
+ if (!witness)
1816
+ continue;
1347
1817
  }
1348
1818
  // Witness is stack, so last element will be used first
1349
- input.finalScriptWitness = signatures
1350
- .reverse()
1351
- .concat([script, psbt.TaprootControlBlock.encode(cb)]);
1352
- break;
1819
+ witness ||= signatures.reverse().concat([script, psbt.TaprootControlBlock.encode(cb)]);
1820
+ const size = RawWitness.encode(witness).length;
1821
+ if (size >= smallestSize)
1822
+ continue;
1823
+ smallest = witness;
1824
+ smallestSize = size;
1353
1825
  }
1354
- if (!input.finalScriptWitness)
1826
+ if (!smallest && unsupported)
1827
+ throw new Error('Finalize: Unknown tapLeafScript');
1828
+ if (!smallest)
1355
1829
  throw new Error('finalize/taproot: empty witness');
1830
+ input.finalScriptWitness = smallest;
1356
1831
  }
1357
1832
  else
1358
1833
  throw new Error('finalize/taproot: unknown input');
1359
1834
  // BIP174 Input Finalizer: if scriptSig is empty for an input, 0x07 remains unset.
1360
1835
  delete input.finalScriptSig;
1361
- cleanFinalInput(input);
1836
+ this.cleanFinalInput(input);
1362
1837
  return;
1363
1838
  }
1364
1839
  if (!input.partialSig || !input.partialSig.length)
@@ -1429,7 +1904,7 @@ export class Transaction {
1429
1904
  input.finalScriptSig = finalScriptSig;
1430
1905
  if (finalScriptWitness)
1431
1906
  input.finalScriptWitness = finalScriptWitness;
1432
- cleanFinalInput(input);
1907
+ this.cleanFinalInput(input);
1433
1908
  }
1434
1909
  finalize() {
1435
1910
  for (let i = 0; i < this.inputs.length; i++)
@@ -1440,18 +1915,28 @@ export class Transaction {
1440
1915
  throw new Error('Transaction has unfinalized inputs');
1441
1916
  if (!this.outputs.length)
1442
1917
  throw new Error('Transaction has no outputs');
1443
- if (this.fee < 0n)
1918
+ if (this.fee < _0n)
1444
1919
  throw new Error('Outputs spends more than inputs amount');
1445
1920
  return this.toBytes(true, true);
1446
1921
  }
1447
1922
  combine(other) {
1923
+ if (!(other instanceof Transaction))
1924
+ throw new TypeError('"other" expected Transaction, got type=' + typeof other);
1925
+ // Match main's accumulator model: operation policy belongs to the receiver that is mutated.
1926
+ const opts = this.opts;
1448
1927
  // BIP174 combiners merge same-transaction PSBTs across versions and emit the highest required
1449
1928
  // version, so PSBTVersion mismatches are normalized below instead of treated as conflicts.
1450
1929
  const PSBTVersion = Math.max(this.opts.PSBTVersion || 0, other.opts.PSBTVersion || 0);
1451
- for (const k of ['version', 'lockTime']) {
1452
- if (this.opts[k] !== other.opts[k]) {
1453
- throw new Error(`Transaction/combine: different ${k} this=${this.opts[k]} other=${other.opts[k]}`);
1454
- }
1930
+ if (this.opts.version !== other.opts.version)
1931
+ throw new Error(`Transaction/combine: different version this=${this.opts.version} ` +
1932
+ `other=${other.opts.version}`);
1933
+ const thisV2 = this.isPSBTv2();
1934
+ const otherV2 = other.isPSBTv2();
1935
+ if (!thisV2 || !otherV2) {
1936
+ const thisLockTime = this.lockTime;
1937
+ const otherLockTime = other.lockTime;
1938
+ if (thisLockTime !== otherLockTime)
1939
+ throw new Error(`Transaction/combine: different lockTime this=${thisLockTime} other=${otherLockTime}`);
1455
1940
  }
1456
1941
  for (const k of ['inputs', 'outputs']) {
1457
1942
  if (this[k].length !== other[k].length) {
@@ -1460,15 +1945,122 @@ export class Transaction {
1460
1945
  }
1461
1946
  // Same-transaction checks must compare the normalized unsigned tx bytes here: PSBTv0 stores
1462
1947
  // `global.unsignedTx`, while PSBTv2 reconstructs the same transaction from split fields.
1463
- if (!equalBytes(this.unsignedTx, other.unsignedTx))
1948
+ const unsignedTx = this.unsignedTx;
1949
+ if (!equalBytes(unsignedTx, other.unsignedTx))
1464
1950
  throw new Error(`Transaction/combine: different unsigned tx`);
1465
- this.global = psbt.mergeKeyMap(psbt.PSBTGlobal, this.global, other.global, undefined, this.opts.allowUnknown);
1951
+ let txModifiable;
1952
+ if (thisV2 && otherV2) {
1953
+ // Core combines the stored optional bytes without deriving replacements from signatures.
1954
+ // Only explicit legacy-omission compatibility gives an absent field an effective value.
1955
+ const policy = (tx) => {
1956
+ if (tx.global.txModifiable !== undefined)
1957
+ return cleanTxModifiable(tx.global.txModifiable, opts.unknown);
1958
+ return opts.allowMissingTxModifiable ? tx.modifiable(true, opts.unknown) : 0;
1959
+ };
1960
+ const a = policy(this);
1961
+ const b = policy(other);
1962
+ // Known mutability permissions use intersection and SIGHASH_SINGLE presence uses union.
1963
+ // Future flag bits must agree because this implementation does not know how to merge them.
1964
+ if ((a & ~0b111) !== (b & ~0b111))
1965
+ throw new Error('Transaction/combine: conflicting unknown txModifiable flags');
1966
+ txModifiable = (a & ~0b111) | (a & b & 0b011) | ((a | b) & 0b100);
1967
+ // Preserve Core's optional-field semantics when neither participant supplied policy.
1968
+ if (txModifiable === 0 &&
1969
+ this.global.txModifiable === undefined &&
1970
+ other.global.txModifiable === undefined &&
1971
+ !opts.allowMissingTxModifiable)
1972
+ txModifiable = undefined;
1973
+ }
1974
+ else if (thisV2)
1975
+ txModifiable = this.modifiable(opts.allowMissingTxModifiable, opts.unknown);
1976
+ else if (otherV2)
1977
+ txModifiable = other.modifiable(opts.allowMissingTxModifiable, opts.unknown);
1978
+ const thisGlobal = { ...this.global };
1979
+ const otherGlobal = { ...other.global };
1980
+ // PSBTv0 has no fallback-locktime field: fromPSBT caches unsignedTx.nLockTime there only for
1981
+ // effective-locktime resolution and v2 promotion. Do not merge that cache as a v2 wire value.
1982
+ if (thisV2 !== otherV2) {
1983
+ if (!thisV2)
1984
+ delete thisGlobal.fallbackLocktime;
1985
+ if (!otherV2)
1986
+ delete otherGlobal.fallbackLocktime;
1987
+ // BIP174 permits v0 to encode version zero explicitly or omit it. Remove only that v0
1988
+ // spelling before scalar conflicts; retaining the v2 field anchors repeated promotion when
1989
+ // the accumulator's original options still target v0.
1990
+ if (!thisV2)
1991
+ delete thisGlobal.version;
1992
+ if (!otherV2)
1993
+ delete otherGlobal.version;
1994
+ }
1995
+ // Transaction-modifiable has dedicated bitwise merge rules above. Fallback locktime is only
1996
+ // one input to the effective locktime resolved from the combined input maps, so retain receiver
1997
+ // precedence and validate the resulting unsigned transaction after those maps merge below.
1998
+ const fallbackLocktime = thisGlobal.fallbackLocktime !== undefined
1999
+ ? thisGlobal.fallbackLocktime
2000
+ : otherGlobal.fallbackLocktime;
2001
+ delete thisGlobal.txModifiable;
2002
+ delete otherGlobal.txModifiable;
2003
+ delete thisGlobal.fallbackLocktime;
2004
+ delete otherGlobal.fallbackLocktime;
2005
+ // Every ordinary global scalar must agree when both participants provide it; silently choosing
2006
+ // either value can detach extension metadata such as a BIP322 message from its signatures.
2007
+ const global = psbt.combineKeyMap(psbt.PSBTGlobal, thisGlobal, otherGlobal, opts.unknown, opts.proprietary);
2008
+ if (fallbackLocktime !== undefined)
2009
+ global.fallbackLocktime = fallbackLocktime;
1466
2010
  if (PSBTVersion)
1467
- this.global.version = PSBTVersion;
1468
- for (let i = 0; i < this.inputs.length; i++)
1469
- this.updateInput(i, other.inputs[i], true);
1470
- for (let i = 0; i < this.outputs.length; i++)
1471
- this.updateOutput(i, other.outputs[i], true);
2011
+ global.version = PSBTVersion;
2012
+ if (txModifiable === undefined)
2013
+ delete global.txModifiable;
2014
+ else
2015
+ global.txModifiable = txModifiable;
2016
+ let hasOpaqueFinalizedV0 = false;
2017
+ const inputs = this.inputs.map((current, i) => {
2018
+ const currentFinal = this.inputStatus(i) === 'finalized';
2019
+ const otherFinal = other.inputStatus(i) === 'finalized';
2020
+ // Finalized v0 maps no longer contain the partial signatures needed to derive v2 flags.
2021
+ if ((!thisV2 && currentFinal) || (!otherV2 && otherFinal))
2022
+ hasOpaqueFinalizedV0 = true;
2023
+ if (currentFinal && otherFinal) {
2024
+ // Two finalized PSBTs must describe the same complete satisfaction. Requiring matching
2025
+ // presence as well as matching values prevents combining witness-only and scriptSig-only
2026
+ // final states into a third, unreviewed satisfaction.
2027
+ for (const k of ['finalScriptSig', 'finalScriptWitness']) {
2028
+ const currentHas = !!this.inputs[i][k]?.length;
2029
+ const otherHas = !!other.inputs[i][k]?.length;
2030
+ if (currentHas !== otherHas)
2031
+ throw new Error(`Transaction/combine: different finalized field=${k} input=${i}`);
2032
+ }
2033
+ }
2034
+ const combined = psbt.combineKeyMap(psbt.PSBTInput, current, other.inputs[i], opts.unknown, opts.proprietary);
2035
+ // A final satisfaction supersedes partial signatures and transient signing metadata. This
2036
+ // also avoids manufacturing a contradictory final+partial input from two valid PSBTs.
2037
+ if (currentFinal || otherFinal)
2038
+ cleanFinalInput(combined, opts.unknown, opts.proprietary);
2039
+ return cloneDeep(normalizeInput(combined, undefined, undefined, opts.disableScriptCheck, opts.unknown, opts.proprietary));
2040
+ });
2041
+ // A promoted opaque satisfaction may commit to both transaction halves. Clear only known
2042
+ // permissions; retain a v2 participant's SIGHASH_SINGLE indicator and any future flag bits.
2043
+ if (hasOpaqueFinalizedV0 && global.txModifiable !== undefined)
2044
+ global.txModifiable &= ~0b011;
2045
+ const candidate = new Transaction({ ...opts, PSBTVersion });
2046
+ const outputs = this.outputs.map((current, i) => {
2047
+ const combined = psbt.combineKeyMap(psbt.PSBTOutput, current, other.outputs[i], opts.unknown, opts.proprietary);
2048
+ return cloneDeep(candidate.normalizeOutput(combined));
2049
+ });
2050
+ // Build and validate a detached candidate before touching the receiver. Combining
2051
+ // complementary v2 locktime fields can otherwise create a different unsigned transaction.
2052
+ candidate.global = global;
2053
+ candidate.inputs = inputs;
2054
+ candidate.outputs = outputs;
2055
+ // A v0 input map can contribute signatures while the combined transaction is promoted to v2.
2056
+ // All-v2 restrictions were already intersected above, preserving mutual field omission.
2057
+ if ((!thisV2 || !otherV2) && candidate.isPSBTv2())
2058
+ candidate.global.txModifiable = candidate.txModifiable;
2059
+ if (!equalBytes(candidate.unsignedTx, unsignedTx))
2060
+ throw new Error('Transaction/combine: combined unsigned tx differs');
2061
+ this.global = candidate.global;
2062
+ this.inputs = candidate.inputs;
2063
+ this.outputs = candidate.outputs;
1472
2064
  return this;
1473
2065
  }
1474
2066
  clone() {
@@ -1479,6 +2071,7 @@ export class Transaction {
1479
2071
  /**
1480
2072
  * Merges multiple PSBT blobs into one.
1481
2073
  * @param psbts - PSBT byte arrays to combine
2074
+ * @param opts - Transaction parsing, combination, and serialization options. See {@link TxOpts}.
1482
2075
  * @returns Combined PSBT bytes.
1483
2076
  * @throws If the PSBT list is empty or the partial transactions cannot be combined. {@link Error}
1484
2077
  * @example
@@ -1489,13 +2082,15 @@ export class Transaction {
1489
2082
  * PSBTCombine([psbt, psbt]);
1490
2083
  * ```
1491
2084
  */
1492
- export function PSBTCombine(psbts) {
2085
+ export function PSBTCombine(psbts, opts = {}) {
1493
2086
  if (!psbts || !Array.isArray(psbts) || !psbts.length)
1494
2087
  throw new Error('PSBTCombine: wrong PSBT list');
1495
- const tx = Transaction.fromPSBT(psbts[0]);
2088
+ // Options affect both map cleanup during combination and the encoding of the returned PSBT.
2089
+ const combineOpts = opts;
2090
+ const tx = Transaction.fromPSBT(psbts[0], combineOpts);
1496
2091
  for (let i = 1; i < psbts.length; i++)
1497
- tx.combine(Transaction.fromPSBT(psbts[i]));
1498
- return tx.toPSBT();
2092
+ tx.combine(Transaction.fromPSBT(psbts[i], tx.opts));
2093
+ return tx.toPSBT(combineOpts.PSBTVersion);
1499
2094
  }
1500
2095
  // Copy-pasted from bip32 derive, maybe do something like 'bip32.parsePath'?
1501
2096
  const HARDENED_OFFSET = 0x80000000;
@@ -1538,4 +2133,3 @@ export function bip32Path(path) {
1538
2133
  }
1539
2134
  return out;
1540
2135
  }
1541
- //# sourceMappingURL=transaction.js.map