@scure/btc-signer 2.0.1 → 2.3.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,23 +1,116 @@
1
1
  import { hex } from '@scure/base';
2
+ import { anumber } from '@noble/hashes/utils.js';
2
3
  import * as P from 'micro-packed';
3
4
  import { Address, OutScript, checkScript, tapLeafHash } from "./payment.js";
4
5
  import * as psbt from "./psbt.js";
5
- import { CompactSizeLen, RawOldTx, RawOutput, RawTx, RawWitness, Script, VarBytes, } from "./script.js";
6
+ import { CompactSizeLen, OP, RawOldTx, RawInput, RawOutput, RawTx, Script, scriptPushLen, VarBytes, } from "./script.js";
6
7
  import * as u from "./utils.js";
7
- import { NETWORK, concatBytes, equalBytes, isBytes } from "./utils.js";
8
- const EMPTY32 = new Uint8Array(32);
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');
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
  };
18
+ /**
19
+ * Converts transaction weight units into virtual bytes.
20
+ * @param weight - transaction weight
21
+ * @returns Rounded-up virtual size.
22
+ * @example
23
+ * Convert transaction weight units into virtual bytes.
24
+ * ```ts
25
+ * toVsize(4);
26
+ * ```
27
+ */
13
28
  export const toVsize = (weight) => Math.ceil(weight / 4);
29
+ const stripCodeSeparator = (script) => {
30
+ // Reuse Script's raw pushdata-length parser here. Legacy sighash must remove
31
+ // only actual OP_CODESEPARATOR opcodes while preserving every other original
32
+ // byte, because semantic decode/re-encode would change the signed digest.
33
+ let start = 0;
34
+ const out = [];
35
+ for (let i = 0; i < script.length;) {
36
+ const pos = i;
37
+ const op = script[i++];
38
+ if (op === OP.CODESEPARATOR) {
39
+ if (start < pos)
40
+ out.push(script.subarray(start, pos));
41
+ start = i;
42
+ continue;
43
+ }
44
+ const len = scriptPushLen(op, (bytes) => {
45
+ if (i + bytes > script.length)
46
+ throw new Error('Unexpected end of script');
47
+ let len = 0;
48
+ for (let j = 0; j < bytes; j++)
49
+ len |= script[i + j] << (8 * j);
50
+ i += bytes;
51
+ return len;
52
+ });
53
+ if (len === undefined)
54
+ continue;
55
+ i += len;
56
+ if (i > script.length)
57
+ throw new Error('Unexpected end of script');
58
+ }
59
+ if (start === 0)
60
+ return script;
61
+ if (start < script.length)
62
+ out.push(script.subarray(start));
63
+ return (out.length ? concatBytes(...out) : P.EMPTY);
64
+ };
65
+ /** Decimal precision used for BTC string formatting. */
14
66
  export const PRECISION = 8;
67
+ /** Default transaction version used for newly created transactions. */
15
68
  export const DEFAULT_VERSION = 2;
69
+ /** Default transaction locktime. */
16
70
  export const DEFAULT_LOCKTIME = 0;
71
+ /** Default input sequence number.
72
+ * Final (`0xffffffff`): matches the PSBT omission default and disables nLockTime/CLTV semantics
73
+ * unless callers choose a lower sequence explicitly (for example `0xfffffffe` with lockTime).
74
+ */
17
75
  export const DEFAULT_SEQUENCE = 4294967295;
18
- export const Decimal = P.coders.decimal(PRECISION);
76
+ /**
77
+ * Decimal coder for BTC-denominated strings.
78
+ * This is a fixed-precision BTC-string to satoshi-bigint helper, not a validator
79
+ * for transaction/PSBT output amounts. Signed values are intentional here, so
80
+ * callers can reuse the helper for display/history-style deltas as well as
81
+ * unsigned transfer amounts. It keeps the BTC scale at 8 fractional digits and
82
+ * rejects over-precise inputs instead of rounding.
83
+ * @example
84
+ * Convert between satoshi bigint values and BTC-denominated decimal strings.
85
+ * ```ts
86
+ * Decimal.encode(1n);
87
+ * ```
88
+ */
89
+ export const Decimal = /* @__PURE__ */ (() => Object.freeze(P.coders.decimal(PRECISION)))();
19
90
  // Same as value || def, but doesn't overwrites zero ('0', 0, 0n, etc)
91
+ /**
92
+ * Returns a fallback only when the value is `undefined`.
93
+ * @param value - optional value
94
+ * @param def - fallback value
95
+ * @returns `value` when defined, otherwise `def`.
96
+ * @example
97
+ * Keep zero-like values but replace `undefined` with a fallback.
98
+ * ```ts
99
+ * def(undefined, 1);
100
+ * ```
101
+ */
20
102
  export const def = (value, def) => (value === undefined ? def : value);
103
+ /**
104
+ * Deep-clones plain transaction data structures.
105
+ * @param obj - value to clone
106
+ * @returns Deep copy of the input value.
107
+ * @throws If the value contains an unsupported runtime type. {@link Error}
108
+ * @example
109
+ * Clone plain transaction data structures before mutating them.
110
+ * ```ts
111
+ * cloneDeep({ a: [new Uint8Array([1])] });
112
+ * ```
113
+ */
21
114
  export function cloneDeep(obj) {
22
115
  if (Array.isArray(obj))
23
116
  return obj.map((i) => cloneDeep(i));
@@ -34,30 +127,48 @@ export function cloneDeep(obj) {
34
127
  else if (typeof obj === 'object') {
35
128
  return Object.fromEntries(Object.entries(obj).map(([k, v]) => [k, cloneDeep(v)]));
36
129
  }
37
- throw new Error(`cloneDeep: unknown type=${obj} (${typeof obj})`);
130
+ // Don't interpolate unsupported values here: Symbol string coercion would
131
+ // throw before cloneDeep can surface its own stable helper error.
132
+ throw new Error(`cloneDeep: unknown type=${typeof obj}`);
38
133
  }
39
134
  /**
40
135
  * Internal, exported only for backwards-compat. Use `SigHash` instead.
41
- * @deprecated
136
+ * @deprecated Use {@link SigHash} instead.
137
+ * @example
138
+ * Combine the legacy bit flags when interoperating with older code.
139
+ * ```ts
140
+ * SignatureHash.ALL | SignatureHash.ANYONECANPAY;
141
+ * ```
42
142
  */
43
- export const SignatureHash = {
143
+ export const SignatureHash = /* @__PURE__ */ (() => Object.freeze({
44
144
  DEFAULT: 0,
45
145
  ALL: 1,
46
146
  NONE: 2,
47
147
  SINGLE: 3,
48
148
  ANYONECANPAY: 0x80,
49
- };
50
- export const SigHash = {
149
+ }))();
150
+ /**
151
+ * Common signature hash flag combinations.
152
+ * @example
153
+ * Use the predefined signature-hash combinations exported by the library.
154
+ * ```ts
155
+ * SigHash.SINGLE_ANYONECANPAY;
156
+ * ```
157
+ */
158
+ export const SigHash = /* @__PURE__ */ (() => Object.freeze({
51
159
  DEFAULT: SignatureHash.DEFAULT,
52
160
  ALL: SignatureHash.ALL,
53
161
  NONE: SignatureHash.NONE,
54
162
  SINGLE: SignatureHash.SINGLE,
55
- DEFAULT_ANYONECANPAY: SignatureHash.DEFAULT | SignatureHash.ANYONECANPAY,
163
+ // BIP341 only permits 0x00, 0x01, 0x02, 0x03, 0x81, 0x82, and 0x83 for taproot, so
164
+ // the mechanical `DEFAULT | ANYONECANPAY` combination (0x80) is invalid and not exported.
165
+ // DEFAULT_ANYONECANPAY: SignatureHash.DEFAULT | SignatureHash.ANYONECANPAY,
56
166
  ALL_ANYONECANPAY: SignatureHash.ALL | SignatureHash.ANYONECANPAY,
57
167
  NONE_ANYONECANPAY: SignatureHash.NONE | SignatureHash.ANYONECANPAY,
58
168
  SINGLE_ANYONECANPAY: SignatureHash.SINGLE | SignatureHash.ANYONECANPAY,
59
- };
60
- export const SigHashNames = u.reverseObject(SigHash);
169
+ }))();
170
+ /** Reverse lookup table for signature hash flag names. */
171
+ export const SigHashNames = /* @__PURE__ */ (() => Object.freeze(u.reverseObject(SigHash)))();
61
172
  function getTaprootKeys(privKey, pubKey, internalKey, merkleRoot = P.EMPTY) {
62
173
  if (equalBytes(internalKey, pubKey)) {
63
174
  privKey = u.taprootTweakPrivKey(privKey, merkleRoot);
@@ -72,25 +183,50 @@ function outputBeforeSign(i) {
72
183
  return { script: i.script, amount: i.amount };
73
184
  }
74
185
  // Force check index/txid/sequence
186
+ /**
187
+ * Normalizes a PSBT input into the fields needed for signing.
188
+ * @param i - PSBT input to validate
189
+ * @returns Input fields required for signing.
190
+ * @throws If the input is missing `txid` or `index`. {@link Error}
191
+ * @example
192
+ * Fill in defaults for the fields the signer expects to see.
193
+ * ```ts
194
+ * import { hex } from '@scure/base';
195
+ * import { inputBeforeSign } from '@scure/btc-signer/transaction.js';
196
+ * inputBeforeSign({
197
+ * txid: hex.decode('0000000000000000000000000000000000000000000000000000000000000001'),
198
+ * index: 0,
199
+ * });
200
+ * ```
201
+ */
75
202
  export function inputBeforeSign(i) {
203
+ validateObject(i, {}, {}, 'i');
76
204
  if (i.txid === undefined || i.index === undefined)
77
205
  throw new Error('Transaction/input: txid and index required');
78
- return {
206
+ const res = {
79
207
  txid: i.txid,
80
208
  index: i.index,
81
209
  sequence: def(i.sequence, DEFAULT_SEQUENCE),
82
210
  finalScriptSig: def(i.finalScriptSig, P.EMPTY),
83
211
  };
212
+ // This helper is the public "normalize for signing" boundary, so reuse RawInput's existing
213
+ // wire-shape checks here instead of letting malformed runtime field types fail much later.
214
+ RawInput.encode(res);
215
+ return res;
84
216
  }
85
217
  function cleanFinalInput(i) {
86
- for (const _k in i) {
218
+ const _i = i;
219
+ // BIP174 finalizers clear non-final input metadata after constructing final scripts/witnesses.
220
+ // That intentionally drops sighashType here, so post-finalize mutation becomes conservative
221
+ // until callers explicitly reopen the input by removing finalScriptSig/finalScriptWitness.
222
+ for (const _k in _i) {
87
223
  const k = _k;
88
224
  if (!psbt.PSBTInputFinalKeys.includes(k))
89
- delete i[k];
225
+ delete _i[k];
90
226
  }
91
227
  }
92
228
  // (TxHash, Idx)
93
- const TxHashIdx = P.struct({ txid: P.bytes(32, true), index: P.U32LE });
229
+ const TxHashIdx = /* @__PURE__ */ (() => P.struct({ txid: P.bytes(32, true), index: P.U32LE }))();
94
230
  function validateSigHash(s) {
95
231
  if (typeof s !== 'number' || typeof SigHashNames[s] !== 'string')
96
232
  throw new Error(`Invalid SigHash=${s}`);
@@ -105,8 +241,8 @@ function unpackSighash(hashType) {
105
241
  };
106
242
  }
107
243
  function validateOpts(opts) {
108
- if (opts !== undefined && {}.toString.call(opts) !== '[object Object]')
109
- throw new Error(`Wrong object type for transaction options: ${opts}`);
244
+ if (opts !== undefined)
245
+ validateObject(opts, {}, {}, 'opts');
110
246
  const _opts = {
111
247
  ...opts,
112
248
  // Defaults
@@ -114,10 +250,12 @@ function validateOpts(opts) {
114
250
  lockTime: def(opts.lockTime, 0),
115
251
  PSBTVersion: def(opts.PSBTVersion, 0),
116
252
  };
253
+ // Normalize deprecated aliases on the owned copy so they still affect tx.opts without rewriting the
254
+ // caller-owned options object passed to the constructor.
117
255
  if (typeof _opts.allowUnknowInput !== 'undefined')
118
- opts.allowUnknownInputs = _opts.allowUnknowInput;
256
+ _opts.allowUnknownInputs = _opts.allowUnknowInput;
119
257
  if (typeof _opts.allowUnknowOutput !== 'undefined')
120
- opts.allowUnknownOutputs = _opts.allowUnknowOutput;
258
+ _opts.allowUnknownOutputs = _opts.allowUnknowOutput;
121
259
  if (typeof _opts.lockTime !== 'number')
122
260
  throw new Error('Transaction lock time should be number');
123
261
  P.U32LE.encode(_opts.lockTime); // Additional range checks that lockTime
@@ -142,10 +280,13 @@ function validateOpts(opts) {
142
280
  throw new Error(`Transation options wrong type: ${k}=${v} (${typeof v})`);
143
281
  }
144
282
  // 0 and -1 happens in tests
283
+ // With allowUnknownVersion any numeric version is fine; the ternary was inverted
284
+ // before 2026-07 (audit), which made the option throw for every numeric version.
145
285
  if (_opts.allowUnknownVersion
146
- ? typeof _opts.version === 'number'
286
+ ? typeof _opts.version !== 'number'
147
287
  : ![-1, 0, 1, 2, 3].includes(_opts.version))
148
288
  throw new Error(`Unknown version: ${_opts.version}`);
289
+ P.I32LE.encode(_opts.version); // Validate the signed transaction-version wire domain.
149
290
  if (_opts.customScripts !== undefined) {
150
291
  const cs = _opts.customScripts;
151
292
  if (!Array.isArray(cs)) {
@@ -162,17 +303,20 @@ function validateOpts(opts) {
162
303
  }
163
304
  // NOTE: we cannot do this inside PSBTInput coder, because there is no index/txid at this point!
164
305
  function validateInput(i) {
165
- if (i.nonWitnessUtxo && i.index !== undefined) {
166
- const last = i.nonWitnessUtxo.outputs.length - 1;
167
- if (i.index > last)
168
- throw new Error(`validateInput: index(${i.index}) not in nonWitnessUtxo`);
169
- const prevOut = i.nonWitnessUtxo.outputs[i.index];
170
- if (i.witnessUtxo &&
171
- (!equalBytes(i.witnessUtxo.script, prevOut.script) || i.witnessUtxo.amount !== prevOut.amount))
306
+ validateObject(i, {}, {}, 'i');
307
+ const _i = i;
308
+ if (_i.nonWitnessUtxo && _i.index !== undefined) {
309
+ const last = _i.nonWitnessUtxo.outputs.length - 1;
310
+ if (_i.index > last)
311
+ throw new Error(`validateInput: index(${_i.index}) not in nonWitnessUtxo`);
312
+ const prevOut = _i.nonWitnessUtxo.outputs[_i.index];
313
+ if (_i.witnessUtxo &&
314
+ (!equalBytes(_i.witnessUtxo.script, prevOut.script) ||
315
+ _i.witnessUtxo.amount !== prevOut.amount))
172
316
  throw new Error('validateInput: witnessUtxo different from nonWitnessUtxo');
173
- if (i.txid) {
174
- const outputs = i.nonWitnessUtxo.outputs;
175
- if (outputs.length - 1 < i.index)
317
+ if (_i.txid) {
318
+ const outputs = _i.nonWitnessUtxo.outputs;
319
+ if (outputs.length - 1 < _i.index)
176
320
  throw new Error('nonWitnessUtxo: incorect output index');
177
321
  // At this point, we are using previous tx output to create new input.
178
322
  // Script safety checks are unnecessary:
@@ -182,33 +326,96 @@ function validateInput(i) {
182
326
  // in case user wants to use wrong input by mistake
183
327
  // - Worst case: tx will be rejected by nodes. Still better than disallowing user
184
328
  // to spend real input, no matter how broken it looks
185
- const tx = Transaction.fromRaw(RawTx.encode(i.nonWitnessUtxo), {
329
+ const tx = Transaction.fromRaw(RawTx.encode(_i.nonWitnessUtxo), {
186
330
  allowUnknownOutputs: true,
187
331
  disableScriptCheck: true,
188
332
  allowUnknownInputs: true,
333
+ // Consensus does not restrict nVersion; a previous tx with a non-standard
334
+ // version is still spendable and its txid must still be verifiable.
335
+ allowUnknownVersion: true,
189
336
  });
190
- const txid = hex.encode(i.txid);
191
- // PSBTv2 vectors have non-final tx in inputs
192
- if (tx.isFinal && tx.id !== txid)
337
+ const txid = hex.encode(_i.txid);
338
+ // BIP174 requires the provided nonWitnessUtxo to hash to the prevout txid even when the
339
+ // previous transaction is otherwise non-final; finality does not make its serialized txid optional.
340
+ // Keep the historical TransactionInput.txid convention here: internal txid bytes match
341
+ // `Transaction.id` (display-order hex), while raw-tx / PSBT boundary coders are responsible
342
+ // for any byte-order conversions required by their wire formats.
343
+ if (tx.id !== txid)
193
344
  throw new Error(`nonWitnessUtxo: wrong txid, exp=${txid} got=${tx.id}`);
194
345
  }
195
346
  }
196
- return i;
347
+ return _i;
197
348
  }
198
349
  // Normalizes input
350
+ /**
351
+ * Extracts the previous output referenced by an input.
352
+ * @param input - PSBT input with previous output data
353
+ * @returns Previous output information.
354
+ * @throws If the input does not contain usable previous-output information. {@link Error}
355
+ * @example
356
+ * Read the previous output from either `witnessUtxo` or `nonWitnessUtxo`.
357
+ * ```ts
358
+ * getPrevOut({ witnessUtxo: { amount: 1n, script: new Uint8Array([0x51]) } });
359
+ * ```
360
+ */
199
361
  export function getPrevOut(input) {
200
- if (input.nonWitnessUtxo) {
201
- if (input.index === undefined)
362
+ validateObject(input, {}, {}, 'input');
363
+ const _input = input;
364
+ if (_input.nonWitnessUtxo) {
365
+ if (_input.index === undefined)
202
366
  throw new Error('Unknown input index');
203
- return input.nonWitnessUtxo.outputs[input.index];
367
+ // BIP174 `PSBT_IN_NON_WITNESS_UTXO` is the full spent transaction, so the
368
+ // input outpoint index must name an existing output instead of leaking a
369
+ // synthetic `undefined` prevout into later signing / estimation callers.
370
+ if (!Number.isSafeInteger(_input.index) ||
371
+ _input.index < 0 ||
372
+ _input.index >= _input.nonWitnessUtxo.outputs.length)
373
+ throw new Error(`Wrong input index=${_input.index}`);
374
+ return _input.nonWitnessUtxo.outputs[_input.index];
375
+ }
376
+ else if ('witnessUtxo' in _input) {
377
+ // The presence check catches malformed provided values; narrow after the guard for TS.
378
+ const prev = _input.witnessUtxo;
379
+ validateObject(prev, {}, {}, 'input.witnessUtxo');
380
+ abigint(prev.amount, 'input.witnessUtxo.amount');
381
+ if (!isBytes(prev.script))
382
+ throw new TypeError('"input.witnessUtxo.script" expected Uint8Array, got type=' + typeof prev.script);
383
+ return prev;
204
384
  }
205
- else if (input.witnessUtxo)
206
- return input.witnessUtxo;
207
385
  else
208
386
  throw new Error('Cannot find previous output info');
209
387
  }
388
+ /**
389
+ * Normalizes a transaction input update into canonical PSBT form.
390
+ * @param i - input update to normalize
391
+ * @param cur - existing input value to merge with
392
+ * @param allowedFields - fields that may still change on signed inputs
393
+ * @param disableScriptCheck - whether to skip redeem/witness script sanity checks
394
+ * @param allowUnknown - whether to keep unknown PSBT fields
395
+ * @returns Normalized PSBT input.
396
+ * @example
397
+ * Accept hex txids from callers in the same display-order form used by `Transaction.id`, then
398
+ * normalize them into the repo's internal `TransactionInput` shape.
399
+ * ```ts
400
+ * import { hex } from '@scure/base';
401
+ * import { normalizeInput } from '@scure/btc-signer/transaction.js';
402
+ * normalizeInput({
403
+ * txid: '0000000000000000000000000000000000000000000000000000000000000001',
404
+ * index: 0,
405
+ * witnessUtxo: { amount: 1n, script: new Uint8Array([0x51]) },
406
+ * });
407
+ * ```
408
+ */
210
409
  export function normalizeInput(i, cur, allowedFields, disableScriptCheck = false, allowUnknown = false) {
211
- let { nonWitnessUtxo, txid } = i;
410
+ validateObject(i, {}, {}, 'i');
411
+ if (cur !== undefined)
412
+ validateObject(cur, {}, {}, 'cur');
413
+ if (allowedFields !== undefined)
414
+ u.aarray(allowedFields, 'allowedFields');
415
+ const _i = i;
416
+ const _cur = cur;
417
+ const _allowedFields = allowedFields;
418
+ let { nonWitnessUtxo, txid } = _i;
212
419
  // String support for common fields. We usually prefer Uint8Array to avoid errors
213
420
  // like hex looking string accidentally passed, however, in case of nonWitnessUtxo
214
421
  // it is better to expect string, since constructing this complex object will be
@@ -217,21 +424,23 @@ export function normalizeInput(i, cur, allowedFields, disableScriptCheck = false
217
424
  nonWitnessUtxo = hex.decode(nonWitnessUtxo);
218
425
  if (isBytes(nonWitnessUtxo))
219
426
  nonWitnessUtxo = RawTx.decode(nonWitnessUtxo);
220
- if (!('nonWitnessUtxo' in i) && nonWitnessUtxo === undefined)
221
- nonWitnessUtxo = cur?.nonWitnessUtxo;
427
+ if (!('nonWitnessUtxo' in _i) && nonWitnessUtxo === undefined)
428
+ nonWitnessUtxo = _cur?.nonWitnessUtxo;
222
429
  if (typeof txid === 'string')
223
430
  txid = hex.decode(txid);
224
431
  // TODO: if we have nonWitnessUtxo, we can extract txId from here
225
432
  if (txid === undefined)
226
- txid = cur?.txid;
227
- let res = { ...cur, ...i, nonWitnessUtxo, txid };
228
- if (!('nonWitnessUtxo' in i) && res.nonWitnessUtxo === undefined)
433
+ txid = _cur?.txid;
434
+ let res = { ..._cur, ..._i, nonWitnessUtxo, txid };
435
+ if (!('nonWitnessUtxo' in _i) && res.nonWitnessUtxo === undefined)
229
436
  delete res.nonWitnessUtxo;
230
437
  if (res.sequence === undefined)
231
438
  res.sequence = DEFAULT_SEQUENCE;
232
439
  if (res.tapMerkleRoot === null)
233
440
  delete res.tapMerkleRoot;
234
- res = psbt.mergeKeyMap(psbt.PSBTInput, res, cur, allowedFields, allowUnknown);
441
+ res = psbt.mergeKeyMap(psbt.PSBTInput, res, _cur, _allowedFields, allowUnknown);
442
+ // Public PSBT coder surface is wrapped with TArg/TRet for TS compatibility; normalizeInput keeps
443
+ // the repo's historical raw internal shape and casts only at the validation boundary here.
235
444
  psbt.PSBTInputCoder.encode(res); // Validates that everything is correct at this point
236
445
  let prevOut;
237
446
  if (res.nonWitnessUtxo && res.index !== undefined)
@@ -242,15 +451,42 @@ export function normalizeInput(i, cur, allowedFields, disableScriptCheck = false
242
451
  checkScript(prevOut && prevOut.script, res.redeemScript, res.witnessScript);
243
452
  return res;
244
453
  }
454
+ /**
455
+ * Determines how an input should be signed and finalized.
456
+ * Wrapper consistency is expected to be validated earlier by {@link normalizeInput}
457
+ * and {@link checkScript}; this helper classifies already-normalized inputs and is
458
+ * not a standalone redeemScript/witnessScript correctness gate for raw caller input.
459
+ * @param input - PSBT input to inspect
460
+ * @param allowLegacyWitnessUtxo - whether legacy inputs may rely on witness UTXO data only
461
+ * @returns Input classification including transaction type and sighash defaults.
462
+ * @throws If a documented runtime validation or state check fails. {@link Error}
463
+ * @example
464
+ * Detect how the signer should treat a SegWit input from its previous output script.
465
+ * ```ts
466
+ * import { hex } from '@scure/base';
467
+ * import { p2wpkh } from '@scure/btc-signer/payment.js';
468
+ * import { getInputType } from '@scure/btc-signer/transaction.js';
469
+ * import { pubECDSA, randomPrivateKeyBytes } from '@scure/btc-signer/utils.js';
470
+ * getInputType({
471
+ * witnessUtxo: {
472
+ * amount: 1n,
473
+ * script: p2wpkh(pubECDSA(randomPrivateKeyBytes())).script,
474
+ * },
475
+ * });
476
+ * ```
477
+ */
245
478
  export function getInputType(input, allowLegacyWitnessUtxo = false) {
479
+ const _input = input;
246
480
  let txType = 'legacy';
247
481
  let defaultSighash = SignatureHash.ALL;
248
- const prevOut = getPrevOut(input);
482
+ const prevOut = getPrevOut(_input);
249
483
  const first = OutScript.decode(prevOut.script);
250
484
  let type = first.type;
251
485
  let cur = first;
252
486
  const stack = [first];
253
487
  if (first.type === 'tr') {
488
+ // Expected invariant: taproot inputs use PSBT_IN_TAP_* metadata only;
489
+ // legacy redeemScript/witnessScript fields belong to P2SH/P2WSH paths.
254
490
  defaultSighash = SignatureHash.DEFAULT;
255
491
  return {
256
492
  txType: 'taproot',
@@ -258,16 +494,16 @@ export function getInputType(input, allowLegacyWitnessUtxo = false) {
258
494
  last: first,
259
495
  lastScript: prevOut.script,
260
496
  defaultSighash,
261
- sighash: input.sighashType || defaultSighash,
497
+ sighash: _input.sighashType || defaultSighash,
262
498
  };
263
499
  }
264
500
  else {
265
501
  if (first.type === 'wpkh' || first.type === 'wsh')
266
502
  txType = 'segwit';
267
503
  if (first.type === 'sh') {
268
- if (!input.redeemScript)
504
+ if (!_input.redeemScript)
269
505
  throw new Error('inputType: sh without redeemScript');
270
- let child = OutScript.decode(input.redeemScript);
506
+ let child = OutScript.decode(_input.redeemScript);
271
507
  if (child.type === 'wpkh' || child.type === 'wsh')
272
508
  txType = 'segwit';
273
509
  stack.push(child);
@@ -276,9 +512,9 @@ export function getInputType(input, allowLegacyWitnessUtxo = false) {
276
512
  }
277
513
  // wsh can be inside sh
278
514
  if (cur.type === 'wsh') {
279
- if (!input.witnessScript)
515
+ if (!_input.witnessScript)
280
516
  throw new Error('inputType: wsh without witnessScript');
281
- let child = OutScript.decode(input.witnessScript);
517
+ let child = OutScript.decode(_input.witnessScript);
282
518
  if (child.type === 'wsh')
283
519
  txType = 'segwit';
284
520
  stack.push(child);
@@ -295,14 +531,35 @@ export function getInputType(input, allowLegacyWitnessUtxo = false) {
295
531
  last,
296
532
  lastScript,
297
533
  defaultSighash,
298
- sighash: input.sighashType || defaultSighash,
534
+ sighash: _input.sighashType || defaultSighash,
299
535
  };
300
- if (txType === 'legacy' && !allowLegacyWitnessUtxo && !input.nonWitnessUtxo) {
536
+ if (txType === 'legacy' && !allowLegacyWitnessUtxo && !_input.nonWitnessUtxo) {
301
537
  throw new Error(`Transaction/sign: legacy input without nonWitnessUtxo, can result in attack that forces paying higher fees. Pass allowLegacyWitnessUtxo=true, if you sure`);
302
538
  }
303
539
  return res;
304
540
  }
305
541
  }
542
+ /**
543
+ * Mutable Bitcoin transaction and PSBT helper.
544
+ * @param opts - Transaction construction and PSBT serialization options. See {@link TxOpts}.
545
+ * @example
546
+ * Create a transaction, add one spend, and export it as PSBT.
547
+ * ```ts
548
+ * import { hex } from '@scure/base';
549
+ * import { p2wpkh } from '@scure/btc-signer/payment.js';
550
+ * import { Transaction } from '@scure/btc-signer/transaction.js';
551
+ * import { pubECDSA, randomPrivateKeyBytes } from '@scure/btc-signer/utils.js';
552
+ * const spend = p2wpkh(pubECDSA(randomPrivateKeyBytes()));
553
+ * const tx = new Transaction();
554
+ * tx.addInput({
555
+ * txid: hex.decode('0000000000000000000000000000000000000000000000000000000000000001'),
556
+ * index: 0,
557
+ * witnessUtxo: { amount: 2n, script: spend.script },
558
+ * });
559
+ * tx.addOutput({ script: spend.script, amount: 1n });
560
+ * tx.toPSBT();
561
+ * ```
562
+ */
306
563
  export class Transaction {
307
564
  global = {};
308
565
  inputs = []; // use getInput()
@@ -368,7 +625,11 @@ export class Transaction {
368
625
  tx.global.fallbackLocktime = lockTime;
369
626
  return tx;
370
627
  }
371
- toPSBT(PSBTVersion = this.opts.PSBTVersion) {
628
+ // Prefer `global.version` when present so cross-version combiners can serialize at the highest
629
+ // required PSBT version without mutating the frozen transaction options object.
630
+ toPSBT(PSBTVersion = this.global.version || this.opts.PSBTVersion) {
631
+ if (PSBTVersion !== undefined)
632
+ anumber(PSBTVersion, 'PSBTVersion');
372
633
  if (PSBTVersion !== 0 && PSBTVersion !== 2)
373
634
  throw new Error(`Wrong PSBT version=${PSBTVersion}`);
374
635
  // if (PSBTVersion === 0 && this.inputs.length === 0) {
@@ -376,7 +637,10 @@ export class Transaction {
376
637
  // 'PSBT version=0 export for transaction without inputs disabled, please use version=2. Please check `toPSBT` method for explanation.'
377
638
  // );
378
639
  // }
379
- const inputs = this.inputs.map((i) => validateInput(psbt.cleanPSBTFields(PSBTVersion, psbt.PSBTInput, i)));
640
+ const inputs = this.inputs.map((i) =>
641
+ // For PSBTv0 the prevout txid/index live in global.unsignedTx rather than the input map, so
642
+ // validate the full transaction input before version filtering drops those fields.
643
+ psbt.cleanPSBTFields(PSBTVersion, psbt.PSBTInput, validateInput(i)));
380
644
  for (const inp of inputs) {
381
645
  // Don't serialize empty fields
382
646
  if (inp.partialSig && !inp.partialSig.length)
@@ -398,16 +662,28 @@ export class Transaction {
398
662
  global.unsignedTx = RawOldTx.decode(RawOldTx.encode({
399
663
  version: this.version,
400
664
  lockTime: this.lockTime,
401
- inputs: this.inputs.map(inputBeforeSign).map((i) => ({
665
+ inputs: this.inputs
666
+ .map((i) => inputBeforeSign(i))
667
+ .map((i) => ({
402
668
  ...i,
403
669
  finalScriptSig: P.EMPTY,
404
670
  })),
405
- outputs: this.outputs.map(outputBeforeSign),
671
+ outputs: this.outputs.map((o) => outputBeforeSign(o)),
406
672
  }));
407
673
  delete global.fallbackLocktime;
408
674
  delete global.txVersion;
675
+ // PSBTv0 carries the unsigned transaction as one blob, so the PSBTv2 framing fields must be
676
+ // removed here. Keeping `global.version` would make validation treat this rebuilt v0 map as
677
+ // PSBTv2 and reject the required `unsignedTx` field.
678
+ delete global.inputCount;
679
+ delete global.outputCount;
680
+ delete global.version;
409
681
  }
410
682
  else {
683
+ // Cross-version merges and v0->v2 re-exports can still carry the PSBTv0 unsignedTx blob in
684
+ // `this.global`, but PSBTv2 serializes the transaction through split global/input/output
685
+ // fields instead, so drop the stale v0-only field before PSBTv2 validation/encoding.
686
+ delete global.unsignedTx;
411
687
  global.version = PSBTVersion;
412
688
  global.txVersion = this.version;
413
689
  global.inputCount = this.inputs.length;
@@ -421,11 +697,10 @@ export class Transaction {
421
697
  if (!outputs.length)
422
698
  outputs.push({});
423
699
  }
424
- return (PSBTVersion === 0 ? psbt.RawPSBTV0 : psbt.RawPSBTV2).encode({
425
- global,
426
- inputs,
427
- outputs,
428
- });
700
+ const raw = { global, inputs, outputs };
701
+ return PSBTVersion === 0
702
+ ? psbt.RawPSBTV0.encode(raw)
703
+ : psbt.RawPSBTV2.encode(raw);
429
704
  }
430
705
  // BIP370 lockTime (https://github.com/bitcoin/bips/blob/master/bip-0370.mediawiki#determining-lock-time)
431
706
  get lockTime() {
@@ -527,30 +802,38 @@ export class Transaction {
527
802
  }
528
803
  // Info utils
529
804
  get hasWitnesses() {
530
- let out = false;
531
805
  for (const i of this.inputs)
532
806
  if (i.finalScriptWitness && i.finalScriptWitness.length)
533
- out = true;
534
- return out;
807
+ return true;
808
+ return false;
535
809
  }
536
810
  // https://en.bitcoin.it/wiki/Weight_units
537
811
  get weight() {
538
812
  if (!this.isFinal)
539
813
  throw new Error('Transaction is not finalized');
814
+ // Serialized length of VarBytes(data) without allocating the encoded copy
815
+ const varLen = (dataLen) => CompactSizeLen.encode(dataLen).length + dataLen;
816
+ const hasWitnesses = this.hasWitnesses;
540
817
  let out = 32;
541
818
  // Outputs
542
819
  const outputs = this.outputs.map(outputBeforeSign);
543
820
  out += 4 * CompactSizeLen.encode(this.outputs.length).length;
544
821
  for (const o of outputs)
545
- out += 32 + 4 * VarBytes.encode(o.script).length;
822
+ out += 32 + 4 * varLen(o.script.length);
546
823
  // Inputs
547
- if (this.hasWitnesses)
824
+ if (hasWitnesses)
548
825
  out += 2;
549
826
  out += 4 * CompactSizeLen.encode(this.inputs.length).length;
550
827
  for (const i of this.inputs) {
551
- out += 160 + 4 * VarBytes.encode(i.finalScriptSig || P.EMPTY).length;
552
- if (this.hasWitnesses && i.finalScriptWitness)
553
- out += RawWitness.encode(i.finalScriptWitness).length;
828
+ out += 160 + 4 * varLen((i.finalScriptSig || P.EMPTY).length);
829
+ // Once segwit serialization is active, every input contributes one witness vector, including
830
+ // legacy inputs whose empty vector still encodes as a single zero-item-count byte.
831
+ if (hasWitnesses) {
832
+ const witness = i.finalScriptWitness || [];
833
+ out += CompactSizeLen.encode(witness.length).length;
834
+ for (const w of witness)
835
+ out += varLen(w.length);
836
+ }
554
837
  }
555
838
  return out;
556
839
  }
@@ -584,7 +867,8 @@ export class Transaction {
584
867
  }
585
868
  // Input stuff
586
869
  checkInputIdx(idx) {
587
- if (!Number.isSafeInteger(idx) || 0 > idx || idx >= this.inputs.length)
870
+ anumber(idx, 'idx');
871
+ if (idx >= this.inputs.length)
588
872
  throw new Error(`Wrong input index=${idx}`);
589
873
  }
590
874
  getInput(idx) {
@@ -596,9 +880,12 @@ export class Transaction {
596
880
  }
597
881
  // Modification
598
882
  addInput(input, _ignoreSignStatus = false) {
883
+ validateObject(input, {}, {}, 'input');
599
884
  if (!_ignoreSignStatus && !this.signStatus().addInput)
600
885
  throw new Error('Tx has signed inputs, cannot add new one');
601
- this.inputs.push(normalizeInput(input, undefined, undefined, this.opts.disableScriptCheck));
886
+ // normalizeInput preserves nested caller-owned byte arrays, so detach them here before the
887
+ // new input becomes transaction state and later caller mutation can rewrite it by aliasing.
888
+ this.inputs.push(cloneDeep(normalizeInput(input, undefined, undefined, this.opts.disableScriptCheck)));
602
889
  return this.inputs.length - 1;
603
890
  }
604
891
  updateInput(idx, input, _ignoreSignStatus = false) {
@@ -609,11 +896,14 @@ export class Transaction {
609
896
  if (!status.addInput || status.inputs.includes(idx))
610
897
  allowedFields = psbt.PSBTInputUnsignedKeys;
611
898
  }
612
- this.inputs[idx] = normalizeInput(input, this.inputs[idx], allowedFields, this.opts.disableScriptCheck, this.opts.allowUnknown);
899
+ // normalizeInput preserves nested caller-owned byte arrays, so detach the merged result here
900
+ // before the updated input becomes transaction state and later caller mutation can rewrite it.
901
+ this.inputs[idx] = cloneDeep(normalizeInput(input, this.inputs[idx], allowedFields, this.opts.disableScriptCheck, this.opts.allowUnknown));
613
902
  }
614
903
  // Output stuff
615
904
  checkOutputIdx(idx) {
616
- if (!Number.isSafeInteger(idx) || 0 > idx || idx >= this.outputs.length)
905
+ anumber(idx, 'idx');
906
+ if (idx >= this.outputs.length)
617
907
  throw new Error(`Wrong output index=${idx}`);
618
908
  }
619
909
  getOutput(idx) {
@@ -630,11 +920,11 @@ export class Transaction {
630
920
  return this.outputs.length;
631
921
  }
632
922
  normalizeOutput(o, cur, allowedFields) {
923
+ validateObject(o, {}, {}, 'o');
633
924
  let { amount, script } = o;
634
925
  if (amount === undefined)
635
926
  amount = cur?.amount;
636
- if (typeof amount !== 'bigint')
637
- throw new Error(`Wrong amount type, should be of type bigint in sats, but got ${amount} of type ${typeof amount}`);
927
+ amount = abigint(amount, 'o.amount');
638
928
  if (typeof script === 'string')
639
929
  script = hex.decode(script);
640
930
  if (script === undefined)
@@ -656,7 +946,9 @@ export class Transaction {
656
946
  addOutput(o, _ignoreSignStatus = false) {
657
947
  if (!_ignoreSignStatus && !this.signStatus().addOutput)
658
948
  throw new Error('Tx has signed outputs, cannot add new one');
659
- this.outputs.push(this.normalizeOutput(o));
949
+ // normalizeOutput preserves nested caller-owned script bytes, so detach them here before the
950
+ // new output becomes transaction state and later caller mutation can rewrite it by aliasing.
951
+ this.outputs.push(cloneDeep(this.normalizeOutput(o)));
660
952
  return this.outputs.length - 1;
661
953
  }
662
954
  updateOutput(idx, output, _ignoreSignStatus = false) {
@@ -667,14 +959,21 @@ export class Transaction {
667
959
  if (!status.addOutput || status.outputs.includes(idx))
668
960
  allowedFields = psbt.PSBTOutputUnsignedKeys;
669
961
  }
670
- this.outputs[idx] = this.normalizeOutput(output, this.outputs[idx], allowedFields);
962
+ // updateOutput replaces stored state with normalizeOutput(...) directly, so detach the result
963
+ // before storing it or later caller mutation of `output.script` will rewrite transaction state.
964
+ this.outputs[idx] = cloneDeep(this.normalizeOutput(output, this.outputs[idx], allowedFields));
671
965
  }
672
966
  addOutputAddress(address, amount, network = NETWORK) {
673
- return this.addOutput({ script: OutScript.encode(Address(network).decode(address)), amount });
967
+ return this.addOutput({
968
+ // Address.decode() only returns recognized descriptors here, but its wrapped output type
969
+ // still carries `undefined` for coder parity, so narrow before feeding OutScript.encode().
970
+ script: OutScript.encode(Address(network).decode(address)),
971
+ amount,
972
+ });
674
973
  }
675
974
  // Utils
676
975
  get fee() {
677
- let res = 0n;
976
+ let res = _0n;
678
977
  for (const i of this.inputs) {
679
978
  const prevOut = getPrevOut(i);
680
979
  if (!prevOut)
@@ -695,8 +994,8 @@ export class Transaction {
695
994
  if (idx < 0 || !Number.isSafeInteger(idx))
696
995
  throw new Error(`Invalid input idx=${idx}`);
697
996
  if ((isSingle && idx >= this.outputs.length) || idx >= this.inputs.length)
698
- return P.U256BE.encode(1n);
699
- prevOutScript = Script.encode(Script.decode(prevOutScript).filter((i) => i !== 'CODESEPARATOR'));
997
+ return P.U256BE.encode(_1n);
998
+ prevOutScript = stripCodeSeparator(prevOutScript);
700
999
  let inputs = this.inputs
701
1000
  .map(inputBeforeSign)
702
1001
  .map((input, inputIdx) => ({
@@ -715,7 +1014,10 @@ export class Transaction {
715
1014
  if (isNone)
716
1015
  outputs = [];
717
1016
  else if (isSingle) {
718
- outputs = outputs.slice(0, idx).fill(EMPTY_OUTPUT).concat([outputs[idx]]);
1017
+ outputs = outputs
1018
+ .slice(0, idx)
1019
+ .fill(EMPTY_OUTPUT)
1020
+ .concat([outputs[idx]]);
719
1021
  }
720
1022
  const tmpTx = RawTx.encode({
721
1023
  lockTime: this.lockTime,
@@ -727,6 +1029,11 @@ export class Transaction {
727
1029
  return u.sha256x2(tmpTx, P.I32LE.encode(hashType));
728
1030
  }
729
1031
  preimageWitnessV0(idx, prevOutScript, hashType, amount) {
1032
+ // BIP143 serializes txTo.vin[nIn].prevout and txTo.vin[nIn].nSequence, so reject an invalid
1033
+ // nIn explicitly instead of leaking a later undefined-input TypeError from inputs[idx].
1034
+ anumber(idx, 'idx');
1035
+ if (idx >= this.inputs.length)
1036
+ throw new Error(`Invalid input idx=${idx}`);
730
1037
  const { isAny, isNone, isSingle } = unpackSighash(hashType);
731
1038
  let inputHash = EMPTY32;
732
1039
  let sequenceHash = EMPTY32;
@@ -746,9 +1053,17 @@ export class Transaction {
746
1053
  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));
747
1054
  }
748
1055
  preimageWitnessV1(idx, prevOutScript, hashType, amount, codeSeparator = -1, leafScript, leafVer = 0xc0, annex) {
749
- if (!Array.isArray(amount) || this.inputs.length !== amount.length)
1056
+ // BIP341 SigMsg commits either to input_index or to the selected input's outpoint/amount/script/
1057
+ // sequence under ANYONECANPAY, so reject an invalid index explicitly instead of hashing a
1058
+ // nonexistent input or leaking a later integer-encoding RangeError for negative idx.
1059
+ anumber(idx, 'idx');
1060
+ if (idx >= this.inputs.length)
1061
+ throw new Error(`Invalid input idx=${idx}`);
1062
+ u.aarray(amount, 'amount');
1063
+ u.aarray(prevOutScript, 'prevOutScript');
1064
+ if (this.inputs.length !== amount.length)
750
1065
  throw new Error(`Invalid amounts array=${amount}`);
751
- if (!Array.isArray(prevOutScript) || this.inputs.length !== prevOutScript.length)
1066
+ if (this.inputs.length !== prevOutScript.length)
752
1067
  throw new Error(`Invalid prevOutScript array=${prevOutScript}`);
753
1068
  const out = [
754
1069
  P.U8.encode(0),
@@ -789,32 +1104,91 @@ export class Transaction {
789
1104
  }
790
1105
  // Signer can be privateKey OR instance of bip32 HD stuff
791
1106
  signIdx(privateKey, idx, allowedSighash, _auxRand) {
1107
+ if (!isBytes(privateKey)) {
1108
+ // HDKey is a structural external instance, so plain-object validation would
1109
+ // reject valid signers.
1110
+ if (!privateKey ||
1111
+ typeof privateKey !== 'object' ||
1112
+ typeof privateKey.deriveChild !== 'function')
1113
+ throw new TypeError('"privateKey" expected Uint8Array or HDKey, got type=' + typeof privateKey);
1114
+ }
792
1115
  this.checkInputIdx(idx);
793
1116
  const input = this.inputs[idx];
794
1117
  const inputType = getInputType(input, this.opts.allowLegacyWitnessUtxo);
795
- // Handle BIP32 HDKey
1118
+ const canSign = (privateKey) => {
1119
+ if (inputType.txType === 'taproot') {
1120
+ const pubKey = u.pubSchnorr(privateKey);
1121
+ if (input.tapInternalKey && equalBytes(pubKey, input.tapInternalKey))
1122
+ return true;
1123
+ if (!input.tapLeafScript)
1124
+ return false;
1125
+ for (const [_, leaf] of input.tapLeafScript) {
1126
+ for (const op of Script.decode(leaf.subarray(0, -1))) {
1127
+ if (isBytes(op) && equalBytes(op, pubKey))
1128
+ return true;
1129
+ }
1130
+ }
1131
+ return false;
1132
+ }
1133
+ const pubKey = u.pubECDSA(privateKey);
1134
+ const pubKeyHash = u.hash160(pubKey);
1135
+ for (const op of Script.decode(inputType.lastScript)) {
1136
+ if (isBytes(op) && (equalBytes(op, pubKey) || equalBytes(op, pubKeyHash)))
1137
+ return true;
1138
+ }
1139
+ return false;
1140
+ };
1141
+ // Expected invariant: HD signing should use bip32Derivation for legacy/segwit inputs,
1142
+ // tapBip32Derivation for taproot inputs, and preserve caller sighash/auxRand constraints.
796
1143
  if (!isBytes(privateKey)) {
797
- if (!input.bip32Derivation || !input.bip32Derivation.length)
798
- throw new Error('bip32Derivation: empty');
799
- const signers = input.bip32Derivation
800
- .filter((i) => i[1].fingerprint == privateKey.fingerprint)
801
- .map(([pubKey, { path }]) => {
802
- let s = privateKey;
803
- for (const i of path)
804
- s = s.deriveChild(i);
805
- if (!equalBytes(s.publicKey, pubKey))
806
- throw new Error('bip32Derivation: wrong pubKey');
807
- if (!s.privateKey)
808
- throw new Error('bip32Derivation: no privateKey');
809
- return s;
810
- });
811
- if (!signers.length)
812
- throw new Error(`bip32Derivation: no items with fingerprint=${privateKey.fingerprint}`);
1144
+ const root = privateKey;
1145
+ const deriveSigners = (label, rows, pubKey) => {
1146
+ if (!rows || !rows.length)
1147
+ throw new Error(`${label}: empty`);
1148
+ const signers = rows
1149
+ .filter((row) => row.fingerprint == root.fingerprint)
1150
+ .map((row) => {
1151
+ let s = root;
1152
+ for (const i of row.path)
1153
+ s = s.deriveChild(i);
1154
+ if (!equalBytes(pubKey(s), row.pubKey))
1155
+ throw new Error(`${label}: wrong pubKey`);
1156
+ if (!s.privateKey)
1157
+ throw new Error(`${label}: no privateKey`);
1158
+ return s;
1159
+ });
1160
+ if (!signers.length)
1161
+ throw new Error(`${label}: no items with fingerprint=${root.fingerprint}`);
1162
+ return signers;
1163
+ };
1164
+ const signers = inputType.txType === 'taproot'
1165
+ ? // BIP371 PSBT_IN_TAP_BIP32_DERIVATION stores x-only pubkeys plus `der`, so taproot HD
1166
+ // signing must derive against that map instead of legacy bip32Derivation.
1167
+ deriveSigners('tapBip32Derivation', input.tapBip32Derivation?.map(([pubKey, { der }]) => ({
1168
+ pubKey,
1169
+ fingerprint: der.fingerprint,
1170
+ path: der.path,
1171
+ })), (s) => s.publicKey.slice(1))
1172
+ : deriveSigners('bip32Derivation', input.bip32Derivation?.map(([pubKey, der]) => ({
1173
+ pubKey,
1174
+ fingerprint: der.fingerprint,
1175
+ path: der.path,
1176
+ })), (s) => s.publicKey);
813
1177
  let signed = false;
814
- for (const s of signers)
815
- if (this.signIdx(s.privateKey, idx))
1178
+ for (const s of signers) {
1179
+ // PSBT may legitimately carry multiple same-fingerprint derivation entries (multisig or
1180
+ // taproot internal/script-path keys). Skip unrelated derived children instead of aborting
1181
+ // the whole HD signing attempt on the first non-applicable candidate.
1182
+ if (!canSign(s.privateKey))
1183
+ continue;
1184
+ if (this.signIdx(s.privateKey, idx, allowedSighash, _auxRand))
816
1185
  signed = true;
817
- return signed;
1186
+ }
1187
+ if (signed)
1188
+ return true;
1189
+ if (inputType.txType === 'taproot')
1190
+ throw new Error('No taproot scripts signed');
1191
+ throw new Error(`Input script doesn't have pubKey: ${inputType.lastScript}`);
818
1192
  }
819
1193
  // Sighash checks
820
1194
  // Just for compat with bitcoinjs-lib, so users won't face unexpected behaviour.
@@ -935,7 +1309,7 @@ export class Transaction {
935
1309
  }
936
1310
  finalizeIdx(idx) {
937
1311
  this.checkInputIdx(idx);
938
- if (this.fee < 0n)
1312
+ if (this.fee < _0n)
939
1313
  throw new Error('Outputs spends more than inputs amount');
940
1314
  const input = this.inputs[idx];
941
1315
  const inputType = getInputType(input, this.opts.allowLegacyWitnessUtxo);
@@ -1013,7 +1387,7 @@ export class Transaction {
1013
1387
  if (!finalized)
1014
1388
  continue;
1015
1389
  input.finalScriptWitness = finalized.concat(psbt.TaprootControlBlock.encode(cb));
1016
- input.finalScriptSig = P.EMPTY;
1390
+ delete input.finalScriptSig;
1017
1391
  cleanFinalInput(input);
1018
1392
  return;
1019
1393
  }
@@ -1031,7 +1405,8 @@ export class Transaction {
1031
1405
  }
1032
1406
  else
1033
1407
  throw new Error('finalize/taproot: unknown input');
1034
- input.finalScriptSig = P.EMPTY;
1408
+ // BIP174 Input Finalizer: if scriptSig is empty for an input, 0x07 remains unset.
1409
+ delete input.finalScriptSig;
1035
1410
  cleanFinalInput(input);
1036
1411
  return;
1037
1412
  }
@@ -1114,12 +1489,17 @@ export class Transaction {
1114
1489
  throw new Error('Transaction has unfinalized inputs');
1115
1490
  if (!this.outputs.length)
1116
1491
  throw new Error('Transaction has no outputs');
1117
- if (this.fee < 0n)
1492
+ if (this.fee < _0n)
1118
1493
  throw new Error('Outputs spends more than inputs amount');
1119
1494
  return this.toBytes(true, true);
1120
1495
  }
1121
1496
  combine(other) {
1122
- for (const k of ['PSBTVersion', 'version', 'lockTime']) {
1497
+ if (!(other instanceof Transaction))
1498
+ throw new TypeError('"other" expected Transaction, got type=' + typeof other);
1499
+ // BIP174 combiners merge same-transaction PSBTs across versions and emit the highest required
1500
+ // version, so PSBTVersion mismatches are normalized below instead of treated as conflicts.
1501
+ const PSBTVersion = Math.max(this.opts.PSBTVersion || 0, other.opts.PSBTVersion || 0);
1502
+ for (const k of ['version', 'lockTime']) {
1123
1503
  if (this.opts[k] !== other.opts[k]) {
1124
1504
  throw new Error(`Transaction/combine: different ${k} this=${this.opts[k]} other=${other.opts[k]}`);
1125
1505
  }
@@ -1129,13 +1509,13 @@ export class Transaction {
1129
1509
  throw new Error(`Transaction/combine: different ${k} length this=${this[k].length} other=${other[k].length}`);
1130
1510
  }
1131
1511
  }
1132
- const thisUnsigned = this.global.unsignedTx ? RawOldTx.encode(this.global.unsignedTx) : P.EMPTY;
1133
- const otherUnsigned = other.global.unsignedTx
1134
- ? RawOldTx.encode(other.global.unsignedTx)
1135
- : P.EMPTY;
1136
- if (!equalBytes(thisUnsigned, otherUnsigned))
1512
+ // Same-transaction checks must compare the normalized unsigned tx bytes here: PSBTv0 stores
1513
+ // `global.unsignedTx`, while PSBTv2 reconstructs the same transaction from split fields.
1514
+ if (!equalBytes(this.unsignedTx, other.unsignedTx))
1137
1515
  throw new Error(`Transaction/combine: different unsigned tx`);
1138
1516
  this.global = psbt.mergeKeyMap(psbt.PSBTGlobal, this.global, other.global, undefined, this.opts.allowUnknown);
1517
+ if (PSBTVersion)
1518
+ this.global.version = PSBTVersion;
1139
1519
  for (let i = 0; i < this.inputs.length; i++)
1140
1520
  this.updateInput(i, other.inputs[i], true);
1141
1521
  for (let i = 0; i < this.outputs.length; i++)
@@ -1144,9 +1524,22 @@ export class Transaction {
1144
1524
  }
1145
1525
  clone() {
1146
1526
  // deepClone probably faster, but this enforces that encoding is valid
1147
- return Transaction.fromPSBT(this.toPSBT(this.opts.PSBTVersion), this.opts);
1527
+ return Transaction.fromPSBT(this.toPSBT(), this.opts);
1148
1528
  }
1149
1529
  }
1530
+ /**
1531
+ * Merges multiple PSBT blobs into one.
1532
+ * @param psbts - PSBT byte arrays to combine
1533
+ * @returns Combined PSBT bytes.
1534
+ * @throws If the PSBT list is empty or the partial transactions cannot be combined. {@link Error}
1535
+ * @example
1536
+ * Merge separate partially signed PSBTs that share the same unsigned transaction.
1537
+ * ```ts
1538
+ * import { PSBTCombine, Transaction } from '@scure/btc-signer/transaction.js';
1539
+ * const psbt = new Transaction().toPSBT();
1540
+ * PSBTCombine([psbt, psbt]);
1541
+ * ```
1542
+ */
1150
1543
  export function PSBTCombine(psbts) {
1151
1544
  if (!psbts || !Array.isArray(psbts) || !psbts.length)
1152
1545
  throw new Error('PSBTCombine: wrong PSBT list');
@@ -1157,13 +1550,31 @@ export function PSBTCombine(psbts) {
1157
1550
  }
1158
1551
  // Copy-pasted from bip32 derive, maybe do something like 'bip32.parsePath'?
1159
1552
  const HARDENED_OFFSET = 0x80000000;
1553
+ /**
1554
+ * Parses a BIP32 path string into child indices.
1555
+ * @param path - derivation path such as `m/0'/1`
1556
+ * @returns Array of encoded child indices.
1557
+ * @throws If the derivation path syntax or child indices are invalid. {@link Error}
1558
+ * @example
1559
+ * Parse a BIP32 derivation path into hardened and unhardened indices.
1560
+ * ```ts
1561
+ * bip32Path("m/0'/1");
1562
+ * ```
1563
+ */
1160
1564
  export function bip32Path(path) {
1161
1565
  const out = [];
1566
+ // PSBT key-origin records only carry raw child indices, so this convenience
1567
+ // parser normalizes textual BIP32 roots into the same integer path array and
1568
+ // uses apostrophe suffixes for hardening.
1162
1569
  if (!/^[mM]'?/.test(path))
1163
1570
  throw new Error('Path must start with "m" or "M"');
1164
1571
  if (/^[mM]'?$/.test(path))
1165
1572
  return out;
1166
1573
  const parts = path.replace(/^[mM]'?\//, '').split('/');
1574
+ // BIP32 Serialization format `* 1 byte: depth`: extended keys cap depth at
1575
+ // 255, so deeper text paths cannot roundtrip.
1576
+ if (parts.length > 255)
1577
+ throw new Error('Path depth exceeds 255');
1167
1578
  for (const c of parts) {
1168
1579
  const m = /^(\d+)('?)$/.exec(c);
1169
1580
  if (!m || m.length !== 3)
@@ -1178,4 +1589,3 @@ export function bip32Path(path) {
1178
1589
  }
1179
1590
  return out;
1180
1591
  }
1181
- //# sourceMappingURL=transaction.js.map