@scure/btc-signer 2.0.1 → 2.2.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
@@ -2,22 +2,110 @@ import { hex } from '@scure/base';
2
2
  import * as P from 'micro-packed';
3
3
  import { Address, OutScript, checkScript, tapLeafHash } from "./payment.js";
4
4
  import * as psbt from "./psbt.js";
5
- import { CompactSizeLen, RawOldTx, RawOutput, RawTx, RawWitness, Script, VarBytes, } from "./script.js";
5
+ import { CompactSizeLen, OP, RawOldTx, RawInput, RawOutput, RawTx, RawWitness, Script, scriptPushLen, VarBytes, } from "./script.js";
6
6
  import * as u from "./utils.js";
7
- import { NETWORK, concatBytes, equalBytes, isBytes } from "./utils.js";
8
- const EMPTY32 = new Uint8Array(32);
7
+ import { NETWORK, concatBytes, equalBytes, isBytes, } from "./utils.js";
8
+ const EMPTY32 = /* @__PURE__ */ new Uint8Array(32);
9
9
  const EMPTY_OUTPUT = {
10
10
  amount: 0xffffffffffffffffn,
11
11
  script: P.EMPTY,
12
12
  };
13
+ /**
14
+ * Converts transaction weight units into virtual bytes.
15
+ * @param weight - transaction weight
16
+ * @returns Rounded-up virtual size.
17
+ * @example
18
+ * Convert transaction weight units into virtual bytes.
19
+ * ```ts
20
+ * toVsize(4);
21
+ * ```
22
+ */
13
23
  export const toVsize = (weight) => Math.ceil(weight / 4);
24
+ const stripCodeSeparator = (script) => {
25
+ // Reuse Script's raw pushdata-length parser here. Legacy sighash must remove
26
+ // only actual OP_CODESEPARATOR opcodes while preserving every other original
27
+ // byte, because semantic decode/re-encode would change the signed digest.
28
+ let start = 0;
29
+ const out = [];
30
+ for (let i = 0; i < script.length;) {
31
+ const pos = i;
32
+ const op = script[i++];
33
+ if (op === OP.CODESEPARATOR) {
34
+ if (start < pos)
35
+ out.push(script.subarray(start, pos));
36
+ start = i;
37
+ continue;
38
+ }
39
+ const len = scriptPushLen(op, (bytes) => {
40
+ if (i + bytes > script.length)
41
+ throw new Error('Unexpected end of script');
42
+ let len = 0;
43
+ for (let j = 0; j < bytes; j++)
44
+ len |= script[i + j] << (8 * j);
45
+ i += bytes;
46
+ return len;
47
+ });
48
+ if (len === undefined)
49
+ continue;
50
+ i += len;
51
+ if (i > script.length)
52
+ throw new Error('Unexpected end of script');
53
+ }
54
+ if (start === 0)
55
+ return script;
56
+ if (start < script.length)
57
+ out.push(script.subarray(start));
58
+ return (out.length ? concatBytes(...out) : P.EMPTY);
59
+ };
60
+ /** Decimal precision used for BTC string formatting. */
14
61
  export const PRECISION = 8;
62
+ /** Default transaction version used for newly created transactions. */
15
63
  export const DEFAULT_VERSION = 2;
64
+ /** Default transaction locktime. */
16
65
  export const DEFAULT_LOCKTIME = 0;
66
+ /** Default input sequence number.
67
+ * Final (`0xffffffff`): matches the PSBT omission default and disables nLockTime/CLTV semantics
68
+ * unless callers choose a lower sequence explicitly (for example `0xfffffffe` with lockTime).
69
+ */
17
70
  export const DEFAULT_SEQUENCE = 4294967295;
18
- export const Decimal = P.coders.decimal(PRECISION);
71
+ /**
72
+ * Decimal coder for BTC-denominated strings.
73
+ * This is a fixed-precision BTC-string to satoshi-bigint helper, not a validator
74
+ * for transaction/PSBT output amounts. Signed values are intentional here, so
75
+ * callers can reuse the helper for display/history-style deltas as well as
76
+ * unsigned transfer amounts. It keeps the BTC scale at 8 fractional digits and
77
+ * rejects over-precise inputs instead of rounding.
78
+ * @example
79
+ * Convert between satoshi bigint values and BTC-denominated decimal strings.
80
+ * ```ts
81
+ * Decimal.encode(1n);
82
+ * ```
83
+ */
84
+ export const Decimal = /* @__PURE__ */ (() => Object.freeze(P.coders.decimal(PRECISION)))();
19
85
  // Same as value || def, but doesn't overwrites zero ('0', 0, 0n, etc)
86
+ /**
87
+ * Returns a fallback only when the value is `undefined`.
88
+ * @param value - optional value
89
+ * @param def - fallback value
90
+ * @returns `value` when defined, otherwise `def`.
91
+ * @example
92
+ * Keep zero-like values but replace `undefined` with a fallback.
93
+ * ```ts
94
+ * def(undefined, 1);
95
+ * ```
96
+ */
20
97
  export const def = (value, def) => (value === undefined ? def : value);
98
+ /**
99
+ * Deep-clones plain transaction data structures.
100
+ * @param obj - value to clone
101
+ * @returns Deep copy of the input value.
102
+ * @throws If the value contains an unsupported runtime type. {@link Error}
103
+ * @example
104
+ * Clone plain transaction data structures before mutating them.
105
+ * ```ts
106
+ * cloneDeep({ a: [new Uint8Array([1])] });
107
+ * ```
108
+ */
21
109
  export function cloneDeep(obj) {
22
110
  if (Array.isArray(obj))
23
111
  return obj.map((i) => cloneDeep(i));
@@ -34,30 +122,48 @@ export function cloneDeep(obj) {
34
122
  else if (typeof obj === 'object') {
35
123
  return Object.fromEntries(Object.entries(obj).map(([k, v]) => [k, cloneDeep(v)]));
36
124
  }
37
- throw new Error(`cloneDeep: unknown type=${obj} (${typeof obj})`);
125
+ // Don't interpolate unsupported values here: Symbol string coercion would
126
+ // throw before cloneDeep can surface its own stable helper error.
127
+ throw new Error(`cloneDeep: unknown type=${typeof obj}`);
38
128
  }
39
129
  /**
40
130
  * Internal, exported only for backwards-compat. Use `SigHash` instead.
41
- * @deprecated
131
+ * @deprecated Use {@link SigHash} instead.
132
+ * @example
133
+ * Combine the legacy bit flags when interoperating with older code.
134
+ * ```ts
135
+ * SignatureHash.ALL | SignatureHash.ANYONECANPAY;
136
+ * ```
42
137
  */
43
- export const SignatureHash = {
138
+ export const SignatureHash = /* @__PURE__ */ (() => Object.freeze({
44
139
  DEFAULT: 0,
45
140
  ALL: 1,
46
141
  NONE: 2,
47
142
  SINGLE: 3,
48
143
  ANYONECANPAY: 0x80,
49
- };
50
- export const SigHash = {
144
+ }))();
145
+ /**
146
+ * Common signature hash flag combinations.
147
+ * @example
148
+ * Use the predefined signature-hash combinations exported by the library.
149
+ * ```ts
150
+ * SigHash.SINGLE_ANYONECANPAY;
151
+ * ```
152
+ */
153
+ export const SigHash = /* @__PURE__ */ (() => Object.freeze({
51
154
  DEFAULT: SignatureHash.DEFAULT,
52
155
  ALL: SignatureHash.ALL,
53
156
  NONE: SignatureHash.NONE,
54
157
  SINGLE: SignatureHash.SINGLE,
55
- DEFAULT_ANYONECANPAY: SignatureHash.DEFAULT | SignatureHash.ANYONECANPAY,
158
+ // BIP341 only permits 0x00, 0x01, 0x02, 0x03, 0x81, 0x82, and 0x83 for taproot, so
159
+ // the mechanical `DEFAULT | ANYONECANPAY` combination (0x80) is invalid and not exported.
160
+ // DEFAULT_ANYONECANPAY: SignatureHash.DEFAULT | SignatureHash.ANYONECANPAY,
56
161
  ALL_ANYONECANPAY: SignatureHash.ALL | SignatureHash.ANYONECANPAY,
57
162
  NONE_ANYONECANPAY: SignatureHash.NONE | SignatureHash.ANYONECANPAY,
58
163
  SINGLE_ANYONECANPAY: SignatureHash.SINGLE | SignatureHash.ANYONECANPAY,
59
- };
60
- export const SigHashNames = u.reverseObject(SigHash);
164
+ }))();
165
+ /** Reverse lookup table for signature hash flag names. */
166
+ export const SigHashNames = /* @__PURE__ */ (() => Object.freeze(u.reverseObject(SigHash)))();
61
167
  function getTaprootKeys(privKey, pubKey, internalKey, merkleRoot = P.EMPTY) {
62
168
  if (equalBytes(internalKey, pubKey)) {
63
169
  privKey = u.taprootTweakPrivKey(privKey, merkleRoot);
@@ -72,25 +178,49 @@ function outputBeforeSign(i) {
72
178
  return { script: i.script, amount: i.amount };
73
179
  }
74
180
  // Force check index/txid/sequence
181
+ /**
182
+ * Normalizes a PSBT input into the fields needed for signing.
183
+ * @param i - PSBT input to validate
184
+ * @returns Input fields required for signing.
185
+ * @throws If the input is missing `txid` or `index`. {@link Error}
186
+ * @example
187
+ * Fill in defaults for the fields the signer expects to see.
188
+ * ```ts
189
+ * import { hex } from '@scure/base';
190
+ * import { inputBeforeSign } from '@scure/btc-signer/transaction.js';
191
+ * inputBeforeSign({
192
+ * txid: hex.decode('0000000000000000000000000000000000000000000000000000000000000001'),
193
+ * index: 0,
194
+ * });
195
+ * ```
196
+ */
75
197
  export function inputBeforeSign(i) {
76
198
  if (i.txid === undefined || i.index === undefined)
77
199
  throw new Error('Transaction/input: txid and index required');
78
- return {
200
+ const res = {
79
201
  txid: i.txid,
80
202
  index: i.index,
81
203
  sequence: def(i.sequence, DEFAULT_SEQUENCE),
82
204
  finalScriptSig: def(i.finalScriptSig, P.EMPTY),
83
205
  };
206
+ // This helper is the public "normalize for signing" boundary, so reuse RawInput's existing
207
+ // wire-shape checks here instead of letting malformed runtime field types fail much later.
208
+ RawInput.encode(res);
209
+ return res;
84
210
  }
85
211
  function cleanFinalInput(i) {
86
- for (const _k in i) {
212
+ const _i = i;
213
+ // BIP174 finalizers clear non-final input metadata after constructing final scripts/witnesses.
214
+ // That intentionally drops sighashType here, so post-finalize mutation becomes conservative
215
+ // until callers explicitly reopen the input by removing finalScriptSig/finalScriptWitness.
216
+ for (const _k in _i) {
87
217
  const k = _k;
88
218
  if (!psbt.PSBTInputFinalKeys.includes(k))
89
- delete i[k];
219
+ delete _i[k];
90
220
  }
91
221
  }
92
222
  // (TxHash, Idx)
93
- const TxHashIdx = P.struct({ txid: P.bytes(32, true), index: P.U32LE });
223
+ const TxHashIdx = /* @__PURE__ */ (() => P.struct({ txid: P.bytes(32, true), index: P.U32LE }))();
94
224
  function validateSigHash(s) {
95
225
  if (typeof s !== 'number' || typeof SigHashNames[s] !== 'string')
96
226
  throw new Error(`Invalid SigHash=${s}`);
@@ -114,10 +244,12 @@ function validateOpts(opts) {
114
244
  lockTime: def(opts.lockTime, 0),
115
245
  PSBTVersion: def(opts.PSBTVersion, 0),
116
246
  };
247
+ // Normalize deprecated aliases on the owned copy so they still affect tx.opts without rewriting the
248
+ // caller-owned options object passed to the constructor.
117
249
  if (typeof _opts.allowUnknowInput !== 'undefined')
118
- opts.allowUnknownInputs = _opts.allowUnknowInput;
250
+ _opts.allowUnknownInputs = _opts.allowUnknowInput;
119
251
  if (typeof _opts.allowUnknowOutput !== 'undefined')
120
- opts.allowUnknownOutputs = _opts.allowUnknowOutput;
252
+ _opts.allowUnknownOutputs = _opts.allowUnknowOutput;
121
253
  if (typeof _opts.lockTime !== 'number')
122
254
  throw new Error('Transaction lock time should be number');
123
255
  P.U32LE.encode(_opts.lockTime); // Additional range checks that lockTime
@@ -162,17 +294,19 @@ function validateOpts(opts) {
162
294
  }
163
295
  // NOTE: we cannot do this inside PSBTInput coder, because there is no index/txid at this point!
164
296
  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))
297
+ const _i = i;
298
+ if (_i.nonWitnessUtxo && _i.index !== undefined) {
299
+ const last = _i.nonWitnessUtxo.outputs.length - 1;
300
+ if (_i.index > last)
301
+ throw new Error(`validateInput: index(${_i.index}) not in nonWitnessUtxo`);
302
+ const prevOut = _i.nonWitnessUtxo.outputs[_i.index];
303
+ if (_i.witnessUtxo &&
304
+ (!equalBytes(_i.witnessUtxo.script, prevOut.script) ||
305
+ _i.witnessUtxo.amount !== prevOut.amount))
172
306
  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)
307
+ if (_i.txid) {
308
+ const outputs = _i.nonWitnessUtxo.outputs;
309
+ if (outputs.length - 1 < _i.index)
176
310
  throw new Error('nonWitnessUtxo: incorect output index');
177
311
  // At this point, we are using previous tx output to create new input.
178
312
  // Script safety checks are unnecessary:
@@ -182,33 +316,80 @@ function validateInput(i) {
182
316
  // in case user wants to use wrong input by mistake
183
317
  // - Worst case: tx will be rejected by nodes. Still better than disallowing user
184
318
  // to spend real input, no matter how broken it looks
185
- const tx = Transaction.fromRaw(RawTx.encode(i.nonWitnessUtxo), {
319
+ const tx = Transaction.fromRaw(RawTx.encode(_i.nonWitnessUtxo), {
186
320
  allowUnknownOutputs: true,
187
321
  disableScriptCheck: true,
188
322
  allowUnknownInputs: true,
189
323
  });
190
- const txid = hex.encode(i.txid);
191
- // PSBTv2 vectors have non-final tx in inputs
192
- if (tx.isFinal && tx.id !== txid)
324
+ const txid = hex.encode(_i.txid);
325
+ // BIP174 requires the provided nonWitnessUtxo to hash to the prevout txid even when the
326
+ // previous transaction is otherwise non-final; finality does not make its serialized txid optional.
327
+ // Keep the historical TransactionInput.txid convention here: internal txid bytes match
328
+ // `Transaction.id` (display-order hex), while raw-tx / PSBT boundary coders are responsible
329
+ // for any byte-order conversions required by their wire formats.
330
+ if (tx.id !== txid)
193
331
  throw new Error(`nonWitnessUtxo: wrong txid, exp=${txid} got=${tx.id}`);
194
332
  }
195
333
  }
196
- return i;
334
+ return _i;
197
335
  }
198
336
  // Normalizes input
337
+ /**
338
+ * Extracts the previous output referenced by an input.
339
+ * @param input - PSBT input with previous output data
340
+ * @returns Previous output information.
341
+ * @throws If the input does not contain usable previous-output information. {@link Error}
342
+ * @example
343
+ * Read the previous output from either `witnessUtxo` or `nonWitnessUtxo`.
344
+ * ```ts
345
+ * getPrevOut({ witnessUtxo: { amount: 1n, script: new Uint8Array([0x51]) } });
346
+ * ```
347
+ */
199
348
  export function getPrevOut(input) {
200
- if (input.nonWitnessUtxo) {
201
- if (input.index === undefined)
349
+ const _input = input;
350
+ if (_input.nonWitnessUtxo) {
351
+ if (_input.index === undefined)
202
352
  throw new Error('Unknown input index');
203
- return input.nonWitnessUtxo.outputs[input.index];
353
+ // BIP174 `PSBT_IN_NON_WITNESS_UTXO` is the full spent transaction, so the
354
+ // input outpoint index must name an existing output instead of leaking a
355
+ // synthetic `undefined` prevout into later signing / estimation callers.
356
+ if (!Number.isSafeInteger(_input.index) ||
357
+ _input.index < 0 ||
358
+ _input.index >= _input.nonWitnessUtxo.outputs.length)
359
+ throw new Error(`Wrong input index=${_input.index}`);
360
+ return _input.nonWitnessUtxo.outputs[_input.index];
204
361
  }
205
- else if (input.witnessUtxo)
206
- return input.witnessUtxo;
362
+ else if (_input.witnessUtxo)
363
+ return _input.witnessUtxo;
207
364
  else
208
365
  throw new Error('Cannot find previous output info');
209
366
  }
367
+ /**
368
+ * Normalizes a transaction input update into canonical PSBT form.
369
+ * @param i - input update to normalize
370
+ * @param cur - existing input value to merge with
371
+ * @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
374
+ * @returns Normalized PSBT input.
375
+ * @example
376
+ * Accept hex txids from callers in the same display-order form used by `Transaction.id`, then
377
+ * normalize them into the repo's internal `TransactionInput` shape.
378
+ * ```ts
379
+ * import { hex } from '@scure/base';
380
+ * import { normalizeInput } from '@scure/btc-signer/transaction.js';
381
+ * normalizeInput({
382
+ * txid: '0000000000000000000000000000000000000000000000000000000000000001',
383
+ * index: 0,
384
+ * witnessUtxo: { amount: 1n, script: new Uint8Array([0x51]) },
385
+ * });
386
+ * ```
387
+ */
210
388
  export function normalizeInput(i, cur, allowedFields, disableScriptCheck = false, allowUnknown = false) {
211
- let { nonWitnessUtxo, txid } = i;
389
+ const _i = i;
390
+ const _cur = cur;
391
+ const _allowedFields = allowedFields;
392
+ let { nonWitnessUtxo, txid } = _i;
212
393
  // String support for common fields. We usually prefer Uint8Array to avoid errors
213
394
  // like hex looking string accidentally passed, however, in case of nonWitnessUtxo
214
395
  // it is better to expect string, since constructing this complex object will be
@@ -217,21 +398,23 @@ export function normalizeInput(i, cur, allowedFields, disableScriptCheck = false
217
398
  nonWitnessUtxo = hex.decode(nonWitnessUtxo);
218
399
  if (isBytes(nonWitnessUtxo))
219
400
  nonWitnessUtxo = RawTx.decode(nonWitnessUtxo);
220
- if (!('nonWitnessUtxo' in i) && nonWitnessUtxo === undefined)
221
- nonWitnessUtxo = cur?.nonWitnessUtxo;
401
+ if (!('nonWitnessUtxo' in _i) && nonWitnessUtxo === undefined)
402
+ nonWitnessUtxo = _cur?.nonWitnessUtxo;
222
403
  if (typeof txid === 'string')
223
404
  txid = hex.decode(txid);
224
405
  // TODO: if we have nonWitnessUtxo, we can extract txId from here
225
406
  if (txid === undefined)
226
- txid = cur?.txid;
227
- let res = { ...cur, ...i, nonWitnessUtxo, txid };
228
- if (!('nonWitnessUtxo' in i) && res.nonWitnessUtxo === undefined)
407
+ txid = _cur?.txid;
408
+ let res = { ..._cur, ..._i, nonWitnessUtxo, txid };
409
+ if (!('nonWitnessUtxo' in _i) && res.nonWitnessUtxo === undefined)
229
410
  delete res.nonWitnessUtxo;
230
411
  if (res.sequence === undefined)
231
412
  res.sequence = DEFAULT_SEQUENCE;
232
413
  if (res.tapMerkleRoot === null)
233
414
  delete res.tapMerkleRoot;
234
- res = psbt.mergeKeyMap(psbt.PSBTInput, res, cur, allowedFields, allowUnknown);
415
+ res = psbt.mergeKeyMap(psbt.PSBTInput, res, _cur, _allowedFields, allowUnknown);
416
+ // Public PSBT coder surface is wrapped with TArg/TRet for TS compatibility; normalizeInput keeps
417
+ // the repo's historical raw internal shape and casts only at the validation boundary here.
235
418
  psbt.PSBTInputCoder.encode(res); // Validates that everything is correct at this point
236
419
  let prevOut;
237
420
  if (res.nonWitnessUtxo && res.index !== undefined)
@@ -242,15 +425,42 @@ export function normalizeInput(i, cur, allowedFields, disableScriptCheck = false
242
425
  checkScript(prevOut && prevOut.script, res.redeemScript, res.witnessScript);
243
426
  return res;
244
427
  }
428
+ /**
429
+ * Determines how an input should be signed and finalized.
430
+ * Wrapper consistency is expected to be validated earlier by {@link normalizeInput}
431
+ * and {@link checkScript}; this helper classifies already-normalized inputs and is
432
+ * not a standalone redeemScript/witnessScript correctness gate for raw caller input.
433
+ * @param input - PSBT input to inspect
434
+ * @param allowLegacyWitnessUtxo - whether legacy inputs may rely on witness UTXO data only
435
+ * @returns Input classification including transaction type and sighash defaults.
436
+ * @throws If a documented runtime validation or state check fails. {@link Error}
437
+ * @example
438
+ * Detect how the signer should treat a SegWit input from its previous output script.
439
+ * ```ts
440
+ * import { hex } from '@scure/base';
441
+ * import { p2wpkh } from '@scure/btc-signer/payment.js';
442
+ * import { getInputType } from '@scure/btc-signer/transaction.js';
443
+ * import { pubECDSA, randomPrivateKeyBytes } from '@scure/btc-signer/utils.js';
444
+ * getInputType({
445
+ * witnessUtxo: {
446
+ * amount: 1n,
447
+ * script: p2wpkh(pubECDSA(randomPrivateKeyBytes())).script,
448
+ * },
449
+ * });
450
+ * ```
451
+ */
245
452
  export function getInputType(input, allowLegacyWitnessUtxo = false) {
453
+ const _input = input;
246
454
  let txType = 'legacy';
247
455
  let defaultSighash = SignatureHash.ALL;
248
- const prevOut = getPrevOut(input);
456
+ const prevOut = getPrevOut(_input);
249
457
  const first = OutScript.decode(prevOut.script);
250
458
  let type = first.type;
251
459
  let cur = first;
252
460
  const stack = [first];
253
461
  if (first.type === 'tr') {
462
+ // Expected invariant: taproot inputs use PSBT_IN_TAP_* metadata only;
463
+ // legacy redeemScript/witnessScript fields belong to P2SH/P2WSH paths.
254
464
  defaultSighash = SignatureHash.DEFAULT;
255
465
  return {
256
466
  txType: 'taproot',
@@ -258,16 +468,16 @@ export function getInputType(input, allowLegacyWitnessUtxo = false) {
258
468
  last: first,
259
469
  lastScript: prevOut.script,
260
470
  defaultSighash,
261
- sighash: input.sighashType || defaultSighash,
471
+ sighash: _input.sighashType || defaultSighash,
262
472
  };
263
473
  }
264
474
  else {
265
475
  if (first.type === 'wpkh' || first.type === 'wsh')
266
476
  txType = 'segwit';
267
477
  if (first.type === 'sh') {
268
- if (!input.redeemScript)
478
+ if (!_input.redeemScript)
269
479
  throw new Error('inputType: sh without redeemScript');
270
- let child = OutScript.decode(input.redeemScript);
480
+ let child = OutScript.decode(_input.redeemScript);
271
481
  if (child.type === 'wpkh' || child.type === 'wsh')
272
482
  txType = 'segwit';
273
483
  stack.push(child);
@@ -276,9 +486,9 @@ export function getInputType(input, allowLegacyWitnessUtxo = false) {
276
486
  }
277
487
  // wsh can be inside sh
278
488
  if (cur.type === 'wsh') {
279
- if (!input.witnessScript)
489
+ if (!_input.witnessScript)
280
490
  throw new Error('inputType: wsh without witnessScript');
281
- let child = OutScript.decode(input.witnessScript);
491
+ let child = OutScript.decode(_input.witnessScript);
282
492
  if (child.type === 'wsh')
283
493
  txType = 'segwit';
284
494
  stack.push(child);
@@ -295,14 +505,35 @@ export function getInputType(input, allowLegacyWitnessUtxo = false) {
295
505
  last,
296
506
  lastScript,
297
507
  defaultSighash,
298
- sighash: input.sighashType || defaultSighash,
508
+ sighash: _input.sighashType || defaultSighash,
299
509
  };
300
- if (txType === 'legacy' && !allowLegacyWitnessUtxo && !input.nonWitnessUtxo) {
510
+ if (txType === 'legacy' && !allowLegacyWitnessUtxo && !_input.nonWitnessUtxo) {
301
511
  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
512
  }
303
513
  return res;
304
514
  }
305
515
  }
516
+ /**
517
+ * Mutable Bitcoin transaction and PSBT helper.
518
+ * @param opts - Transaction construction and PSBT serialization options. See {@link TxOpts}.
519
+ * @example
520
+ * Create a transaction, add one spend, and export it as PSBT.
521
+ * ```ts
522
+ * import { hex } from '@scure/base';
523
+ * import { p2wpkh } from '@scure/btc-signer/payment.js';
524
+ * import { Transaction } from '@scure/btc-signer/transaction.js';
525
+ * import { pubECDSA, randomPrivateKeyBytes } from '@scure/btc-signer/utils.js';
526
+ * const spend = p2wpkh(pubECDSA(randomPrivateKeyBytes()));
527
+ * const tx = new Transaction();
528
+ * tx.addInput({
529
+ * txid: hex.decode('0000000000000000000000000000000000000000000000000000000000000001'),
530
+ * index: 0,
531
+ * witnessUtxo: { amount: 2n, script: spend.script },
532
+ * });
533
+ * tx.addOutput({ script: spend.script, amount: 1n });
534
+ * tx.toPSBT();
535
+ * ```
536
+ */
306
537
  export class Transaction {
307
538
  global = {};
308
539
  inputs = []; // use getInput()
@@ -368,7 +599,9 @@ export class Transaction {
368
599
  tx.global.fallbackLocktime = lockTime;
369
600
  return tx;
370
601
  }
371
- toPSBT(PSBTVersion = this.opts.PSBTVersion) {
602
+ // Prefer `global.version` when present so cross-version combiners can serialize at the highest
603
+ // required PSBT version without mutating the frozen transaction options object.
604
+ toPSBT(PSBTVersion = this.global.version || this.opts.PSBTVersion) {
372
605
  if (PSBTVersion !== 0 && PSBTVersion !== 2)
373
606
  throw new Error(`Wrong PSBT version=${PSBTVersion}`);
374
607
  // if (PSBTVersion === 0 && this.inputs.length === 0) {
@@ -376,7 +609,10 @@ export class Transaction {
376
609
  // 'PSBT version=0 export for transaction without inputs disabled, please use version=2. Please check `toPSBT` method for explanation.'
377
610
  // );
378
611
  // }
379
- const inputs = this.inputs.map((i) => validateInput(psbt.cleanPSBTFields(PSBTVersion, psbt.PSBTInput, i)));
612
+ const inputs = this.inputs.map((i) =>
613
+ // For PSBTv0 the prevout txid/index live in global.unsignedTx rather than the input map, so
614
+ // validate the full transaction input before version filtering drops those fields.
615
+ psbt.cleanPSBTFields(PSBTVersion, psbt.PSBTInput, validateInput(i)));
380
616
  for (const inp of inputs) {
381
617
  // Don't serialize empty fields
382
618
  if (inp.partialSig && !inp.partialSig.length)
@@ -398,16 +634,28 @@ export class Transaction {
398
634
  global.unsignedTx = RawOldTx.decode(RawOldTx.encode({
399
635
  version: this.version,
400
636
  lockTime: this.lockTime,
401
- inputs: this.inputs.map(inputBeforeSign).map((i) => ({
637
+ inputs: this.inputs
638
+ .map((i) => inputBeforeSign(i))
639
+ .map((i) => ({
402
640
  ...i,
403
641
  finalScriptSig: P.EMPTY,
404
642
  })),
405
- outputs: this.outputs.map(outputBeforeSign),
643
+ outputs: this.outputs.map((o) => outputBeforeSign(o)),
406
644
  }));
407
645
  delete global.fallbackLocktime;
408
646
  delete global.txVersion;
647
+ // PSBTv0 carries the unsigned transaction as one blob, so the PSBTv2 framing fields must be
648
+ // removed here. Keeping `global.version` would make validation treat this rebuilt v0 map as
649
+ // PSBTv2 and reject the required `unsignedTx` field.
650
+ delete global.inputCount;
651
+ delete global.outputCount;
652
+ delete global.version;
409
653
  }
410
654
  else {
655
+ // Cross-version merges and v0->v2 re-exports can still carry the PSBTv0 unsignedTx blob in
656
+ // `this.global`, but PSBTv2 serializes the transaction through split global/input/output
657
+ // fields instead, so drop the stale v0-only field before PSBTv2 validation/encoding.
658
+ delete global.unsignedTx;
411
659
  global.version = PSBTVersion;
412
660
  global.txVersion = this.version;
413
661
  global.inputCount = this.inputs.length;
@@ -421,11 +669,10 @@ export class Transaction {
421
669
  if (!outputs.length)
422
670
  outputs.push({});
423
671
  }
424
- return (PSBTVersion === 0 ? psbt.RawPSBTV0 : psbt.RawPSBTV2).encode({
425
- global,
426
- inputs,
427
- outputs,
428
- });
672
+ const raw = { global, inputs, outputs };
673
+ return PSBTVersion === 0
674
+ ? psbt.RawPSBTV0.encode(raw)
675
+ : psbt.RawPSBTV2.encode(raw);
429
676
  }
430
677
  // BIP370 lockTime (https://github.com/bitcoin/bips/blob/master/bip-0370.mediawiki#determining-lock-time)
431
678
  get lockTime() {
@@ -549,8 +796,10 @@ export class Transaction {
549
796
  out += 4 * CompactSizeLen.encode(this.inputs.length).length;
550
797
  for (const i of this.inputs) {
551
798
  out += 160 + 4 * VarBytes.encode(i.finalScriptSig || P.EMPTY).length;
552
- if (this.hasWitnesses && i.finalScriptWitness)
553
- out += RawWitness.encode(i.finalScriptWitness).length;
799
+ // Once segwit serialization is active, every input contributes one witness vector, including
800
+ // 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;
554
803
  }
555
804
  return out;
556
805
  }
@@ -598,7 +847,9 @@ export class Transaction {
598
847
  addInput(input, _ignoreSignStatus = false) {
599
848
  if (!_ignoreSignStatus && !this.signStatus().addInput)
600
849
  throw new Error('Tx has signed inputs, cannot add new one');
601
- this.inputs.push(normalizeInput(input, undefined, undefined, this.opts.disableScriptCheck));
850
+ // normalizeInput preserves nested caller-owned byte arrays, so detach them here before the
851
+ // 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)));
602
853
  return this.inputs.length - 1;
603
854
  }
604
855
  updateInput(idx, input, _ignoreSignStatus = false) {
@@ -609,7 +860,9 @@ export class Transaction {
609
860
  if (!status.addInput || status.inputs.includes(idx))
610
861
  allowedFields = psbt.PSBTInputUnsignedKeys;
611
862
  }
612
- this.inputs[idx] = normalizeInput(input, this.inputs[idx], allowedFields, this.opts.disableScriptCheck, this.opts.allowUnknown);
863
+ // normalizeInput preserves nested caller-owned byte arrays, so detach the merged result here
864
+ // 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));
613
866
  }
614
867
  // Output stuff
615
868
  checkOutputIdx(idx) {
@@ -656,7 +909,9 @@ export class Transaction {
656
909
  addOutput(o, _ignoreSignStatus = false) {
657
910
  if (!_ignoreSignStatus && !this.signStatus().addOutput)
658
911
  throw new Error('Tx has signed outputs, cannot add new one');
659
- this.outputs.push(this.normalizeOutput(o));
912
+ // normalizeOutput preserves nested caller-owned script bytes, so detach them here before the
913
+ // new output becomes transaction state and later caller mutation can rewrite it by aliasing.
914
+ this.outputs.push(cloneDeep(this.normalizeOutput(o)));
660
915
  return this.outputs.length - 1;
661
916
  }
662
917
  updateOutput(idx, output, _ignoreSignStatus = false) {
@@ -667,10 +922,17 @@ export class Transaction {
667
922
  if (!status.addOutput || status.outputs.includes(idx))
668
923
  allowedFields = psbt.PSBTOutputUnsignedKeys;
669
924
  }
670
- this.outputs[idx] = this.normalizeOutput(output, this.outputs[idx], allowedFields);
925
+ // updateOutput replaces stored state with normalizeOutput(...) directly, so detach the result
926
+ // 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));
671
928
  }
672
929
  addOutputAddress(address, amount, network = NETWORK) {
673
- return this.addOutput({ script: OutScript.encode(Address(network).decode(address)), amount });
930
+ return this.addOutput({
931
+ // Address.decode() only returns recognized descriptors here, but its wrapped output type
932
+ // still carries `undefined` for coder parity, so narrow before feeding OutScript.encode().
933
+ script: OutScript.encode(Address(network).decode(address)),
934
+ amount,
935
+ });
674
936
  }
675
937
  // Utils
676
938
  get fee() {
@@ -696,7 +958,7 @@ export class Transaction {
696
958
  throw new Error(`Invalid input idx=${idx}`);
697
959
  if ((isSingle && idx >= this.outputs.length) || idx >= this.inputs.length)
698
960
  return P.U256BE.encode(1n);
699
- prevOutScript = Script.encode(Script.decode(prevOutScript).filter((i) => i !== 'CODESEPARATOR'));
961
+ prevOutScript = stripCodeSeparator(prevOutScript);
700
962
  let inputs = this.inputs
701
963
  .map(inputBeforeSign)
702
964
  .map((input, inputIdx) => ({
@@ -715,7 +977,10 @@ export class Transaction {
715
977
  if (isNone)
716
978
  outputs = [];
717
979
  else if (isSingle) {
718
- outputs = outputs.slice(0, idx).fill(EMPTY_OUTPUT).concat([outputs[idx]]);
980
+ outputs = outputs
981
+ .slice(0, idx)
982
+ .fill(EMPTY_OUTPUT)
983
+ .concat([outputs[idx]]);
719
984
  }
720
985
  const tmpTx = RawTx.encode({
721
986
  lockTime: this.lockTime,
@@ -727,6 +992,10 @@ export class Transaction {
727
992
  return u.sha256x2(tmpTx, P.I32LE.encode(hashType));
728
993
  }
729
994
  preimageWitnessV0(idx, prevOutScript, hashType, amount) {
995
+ // BIP143 serializes txTo.vin[nIn].prevout and txTo.vin[nIn].nSequence, so reject an invalid
996
+ // nIn explicitly instead of leaking a later undefined-input TypeError from inputs[idx].
997
+ if (idx < 0 || !Number.isSafeInteger(idx) || idx >= this.inputs.length)
998
+ throw new Error(`Invalid input idx=${idx}`);
730
999
  const { isAny, isNone, isSingle } = unpackSighash(hashType);
731
1000
  let inputHash = EMPTY32;
732
1001
  let sequenceHash = EMPTY32;
@@ -750,6 +1019,11 @@ export class Transaction {
750
1019
  throw new Error(`Invalid amounts array=${amount}`);
751
1020
  if (!Array.isArray(prevOutScript) || this.inputs.length !== prevOutScript.length)
752
1021
  throw new Error(`Invalid prevOutScript array=${prevOutScript}`);
1022
+ // BIP341 SigMsg commits either to input_index or to the selected input's outpoint/amount/script/
1023
+ // sequence under ANYONECANPAY, so reject an invalid index explicitly instead of hashing a
1024
+ // nonexistent input or leaking a later integer-encoding RangeError for negative idx.
1025
+ if (idx < 0 || !Number.isSafeInteger(idx) || idx >= this.inputs.length)
1026
+ throw new Error(`Invalid input idx=${idx}`);
753
1027
  const out = [
754
1028
  P.U8.encode(0),
755
1029
  P.U8.encode(hashType), // U8 sigHash
@@ -792,29 +1066,80 @@ export class Transaction {
792
1066
  this.checkInputIdx(idx);
793
1067
  const input = this.inputs[idx];
794
1068
  const inputType = getInputType(input, this.opts.allowLegacyWitnessUtxo);
795
- // Handle BIP32 HDKey
1069
+ const canSign = (privateKey) => {
1070
+ if (inputType.txType === 'taproot') {
1071
+ const pubKey = u.pubSchnorr(privateKey);
1072
+ if (input.tapInternalKey && equalBytes(pubKey, input.tapInternalKey))
1073
+ return true;
1074
+ if (!input.tapLeafScript)
1075
+ return false;
1076
+ for (const [_, leaf] of input.tapLeafScript) {
1077
+ for (const op of Script.decode(leaf.subarray(0, -1))) {
1078
+ if (isBytes(op) && equalBytes(op, pubKey))
1079
+ return true;
1080
+ }
1081
+ }
1082
+ return false;
1083
+ }
1084
+ const pubKey = u.pubECDSA(privateKey);
1085
+ const pubKeyHash = u.hash160(pubKey);
1086
+ for (const op of Script.decode(inputType.lastScript)) {
1087
+ if (isBytes(op) && (equalBytes(op, pubKey) || equalBytes(op, pubKeyHash)))
1088
+ return true;
1089
+ }
1090
+ return false;
1091
+ };
1092
+ // Expected invariant: HD signing should use bip32Derivation for legacy/segwit inputs,
1093
+ // tapBip32Derivation for taproot inputs, and preserve caller sighash/auxRand constraints.
796
1094
  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}`);
1095
+ const root = privateKey;
1096
+ const deriveSigners = (label, rows, pubKey) => {
1097
+ if (!rows || !rows.length)
1098
+ throw new Error(`${label}: empty`);
1099
+ const signers = rows
1100
+ .filter((row) => row.fingerprint == root.fingerprint)
1101
+ .map((row) => {
1102
+ let s = root;
1103
+ for (const i of row.path)
1104
+ s = s.deriveChild(i);
1105
+ if (!equalBytes(pubKey(s), row.pubKey))
1106
+ throw new Error(`${label}: wrong pubKey`);
1107
+ if (!s.privateKey)
1108
+ throw new Error(`${label}: no privateKey`);
1109
+ return s;
1110
+ });
1111
+ if (!signers.length)
1112
+ throw new Error(`${label}: no items with fingerprint=${root.fingerprint}`);
1113
+ return signers;
1114
+ };
1115
+ const signers = inputType.txType === 'taproot'
1116
+ ? // BIP371 PSBT_IN_TAP_BIP32_DERIVATION stores x-only pubkeys plus `der`, so taproot HD
1117
+ // signing must derive against that map instead of legacy bip32Derivation.
1118
+ deriveSigners('tapBip32Derivation', input.tapBip32Derivation?.map(([pubKey, { der }]) => ({
1119
+ pubKey,
1120
+ fingerprint: der.fingerprint,
1121
+ path: der.path,
1122
+ })), (s) => s.publicKey.slice(1))
1123
+ : deriveSigners('bip32Derivation', input.bip32Derivation?.map(([pubKey, der]) => ({
1124
+ pubKey,
1125
+ fingerprint: der.fingerprint,
1126
+ path: der.path,
1127
+ })), (s) => s.publicKey);
813
1128
  let signed = false;
814
- for (const s of signers)
815
- if (this.signIdx(s.privateKey, idx))
1129
+ for (const s of signers) {
1130
+ // PSBT may legitimately carry multiple same-fingerprint derivation entries (multisig or
1131
+ // taproot internal/script-path keys). Skip unrelated derived children instead of aborting
1132
+ // the whole HD signing attempt on the first non-applicable candidate.
1133
+ if (!canSign(s.privateKey))
1134
+ continue;
1135
+ if (this.signIdx(s.privateKey, idx, allowedSighash, _auxRand))
816
1136
  signed = true;
817
- return signed;
1137
+ }
1138
+ if (signed)
1139
+ return true;
1140
+ if (inputType.txType === 'taproot')
1141
+ throw new Error('No taproot scripts signed');
1142
+ throw new Error(`Input script doesn't have pubKey: ${inputType.lastScript}`);
818
1143
  }
819
1144
  // Sighash checks
820
1145
  // Just for compat with bitcoinjs-lib, so users won't face unexpected behaviour.
@@ -1013,7 +1338,7 @@ export class Transaction {
1013
1338
  if (!finalized)
1014
1339
  continue;
1015
1340
  input.finalScriptWitness = finalized.concat(psbt.TaprootControlBlock.encode(cb));
1016
- input.finalScriptSig = P.EMPTY;
1341
+ delete input.finalScriptSig;
1017
1342
  cleanFinalInput(input);
1018
1343
  return;
1019
1344
  }
@@ -1031,7 +1356,8 @@ export class Transaction {
1031
1356
  }
1032
1357
  else
1033
1358
  throw new Error('finalize/taproot: unknown input');
1034
- input.finalScriptSig = P.EMPTY;
1359
+ // BIP174 Input Finalizer: if scriptSig is empty for an input, 0x07 remains unset.
1360
+ delete input.finalScriptSig;
1035
1361
  cleanFinalInput(input);
1036
1362
  return;
1037
1363
  }
@@ -1119,7 +1445,10 @@ export class Transaction {
1119
1445
  return this.toBytes(true, true);
1120
1446
  }
1121
1447
  combine(other) {
1122
- for (const k of ['PSBTVersion', 'version', 'lockTime']) {
1448
+ // BIP174 combiners merge same-transaction PSBTs across versions and emit the highest required
1449
+ // version, so PSBTVersion mismatches are normalized below instead of treated as conflicts.
1450
+ const PSBTVersion = Math.max(this.opts.PSBTVersion || 0, other.opts.PSBTVersion || 0);
1451
+ for (const k of ['version', 'lockTime']) {
1123
1452
  if (this.opts[k] !== other.opts[k]) {
1124
1453
  throw new Error(`Transaction/combine: different ${k} this=${this.opts[k]} other=${other.opts[k]}`);
1125
1454
  }
@@ -1129,13 +1458,13 @@ export class Transaction {
1129
1458
  throw new Error(`Transaction/combine: different ${k} length this=${this[k].length} other=${other[k].length}`);
1130
1459
  }
1131
1460
  }
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))
1461
+ // Same-transaction checks must compare the normalized unsigned tx bytes here: PSBTv0 stores
1462
+ // `global.unsignedTx`, while PSBTv2 reconstructs the same transaction from split fields.
1463
+ if (!equalBytes(this.unsignedTx, other.unsignedTx))
1137
1464
  throw new Error(`Transaction/combine: different unsigned tx`);
1138
1465
  this.global = psbt.mergeKeyMap(psbt.PSBTGlobal, this.global, other.global, undefined, this.opts.allowUnknown);
1466
+ if (PSBTVersion)
1467
+ this.global.version = PSBTVersion;
1139
1468
  for (let i = 0; i < this.inputs.length; i++)
1140
1469
  this.updateInput(i, other.inputs[i], true);
1141
1470
  for (let i = 0; i < this.outputs.length; i++)
@@ -1144,9 +1473,22 @@ export class Transaction {
1144
1473
  }
1145
1474
  clone() {
1146
1475
  // deepClone probably faster, but this enforces that encoding is valid
1147
- return Transaction.fromPSBT(this.toPSBT(this.opts.PSBTVersion), this.opts);
1476
+ return Transaction.fromPSBT(this.toPSBT(), this.opts);
1148
1477
  }
1149
1478
  }
1479
+ /**
1480
+ * Merges multiple PSBT blobs into one.
1481
+ * @param psbts - PSBT byte arrays to combine
1482
+ * @returns Combined PSBT bytes.
1483
+ * @throws If the PSBT list is empty or the partial transactions cannot be combined. {@link Error}
1484
+ * @example
1485
+ * Merge separate partially signed PSBTs that share the same unsigned transaction.
1486
+ * ```ts
1487
+ * import { PSBTCombine, Transaction } from '@scure/btc-signer/transaction.js';
1488
+ * const psbt = new Transaction().toPSBT();
1489
+ * PSBTCombine([psbt, psbt]);
1490
+ * ```
1491
+ */
1150
1492
  export function PSBTCombine(psbts) {
1151
1493
  if (!psbts || !Array.isArray(psbts) || !psbts.length)
1152
1494
  throw new Error('PSBTCombine: wrong PSBT list');
@@ -1157,13 +1499,31 @@ export function PSBTCombine(psbts) {
1157
1499
  }
1158
1500
  // Copy-pasted from bip32 derive, maybe do something like 'bip32.parsePath'?
1159
1501
  const HARDENED_OFFSET = 0x80000000;
1502
+ /**
1503
+ * Parses a BIP32 path string into child indices.
1504
+ * @param path - derivation path such as `m/0'/1`
1505
+ * @returns Array of encoded child indices.
1506
+ * @throws If the derivation path syntax or child indices are invalid. {@link Error}
1507
+ * @example
1508
+ * Parse a BIP32 derivation path into hardened and unhardened indices.
1509
+ * ```ts
1510
+ * bip32Path("m/0'/1");
1511
+ * ```
1512
+ */
1160
1513
  export function bip32Path(path) {
1161
1514
  const out = [];
1515
+ // PSBT key-origin records only carry raw child indices, so this convenience
1516
+ // parser normalizes textual BIP32 roots into the same integer path array and
1517
+ // uses apostrophe suffixes for hardening.
1162
1518
  if (!/^[mM]'?/.test(path))
1163
1519
  throw new Error('Path must start with "m" or "M"');
1164
1520
  if (/^[mM]'?$/.test(path))
1165
1521
  return out;
1166
1522
  const parts = path.replace(/^[mM]'?\//, '').split('/');
1523
+ // BIP32 Serialization format `* 1 byte: depth`: extended keys cap depth at
1524
+ // 255, so deeper text paths cannot roundtrip.
1525
+ if (parts.length > 255)
1526
+ throw new Error('Path depth exceeds 255');
1167
1527
  for (const c of parts) {
1168
1528
  const m = /^(\d+)('?)$/.exec(c);
1169
1529
  if (!m || m.length !== 3)