@scure/btc-signer 2.2.0 → 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.
@@ -1,4 +1,5 @@
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, type CustomScript, OutScript, checkScript, tapLeafHash } from './payment.ts';
4
5
  import * as psbt from './psbt.ts';
@@ -9,7 +10,6 @@ import {
9
10
  RawInput,
10
11
  RawOutput,
11
12
  RawTx,
12
- RawWitness,
13
13
  Script,
14
14
  scriptPushLen,
15
15
  VarBytes,
@@ -18,16 +18,22 @@ import * as u from './utils.ts';
18
18
  import {
19
19
  type Bytes,
20
20
  NETWORK,
21
+ abigint,
21
22
  concatBytes,
22
23
  equalBytes,
23
24
  isBytes,
24
25
  type TArg,
25
26
  type TRet,
27
+ validateObject,
26
28
  } from './utils.ts';
27
29
 
30
+ // Be friendly to bad ECMAScript parsers by not using bigint literals.
31
+ // prettier-ignore
32
+ const _0n = /* @__PURE__ */ BigInt(0), _1n = /* @__PURE__ */ BigInt(1);
33
+ const U64_MAX = /* @__PURE__ */ BigInt('0xffffffffffffffff');
28
34
  const EMPTY32: Uint8Array = /* @__PURE__ */ new Uint8Array(32);
29
35
  const EMPTY_OUTPUT: P.UnwrapCoder<typeof RawOutput> = {
30
- amount: 0xffffffffffffffffn,
36
+ amount: U64_MAX,
31
37
  script: P.EMPTY,
32
38
  };
33
39
  /**
@@ -301,6 +307,7 @@ function outputBeforeSign(i: TArg<psbt.TransactionOutput>): TRet<psbt.Transactio
301
307
  * ```
302
308
  */
303
309
  export function inputBeforeSign(i: TArg<psbt.TransactionInput>): TRet<TransactionInputRequired> {
310
+ validateObject(i as Record<string, any>, {}, {}, 'i');
304
311
  if (i.txid === undefined || i.index === undefined)
305
312
  throw new Error('Transaction/input: txid and index required');
306
313
  const res = {
@@ -344,8 +351,7 @@ function unpackSighash(hashType: number) {
344
351
  }
345
352
 
346
353
  function validateOpts(opts: TArg<TxOpts>): TRet<Readonly<TxOpts>> {
347
- if (opts !== undefined && {}.toString.call(opts) !== '[object Object]')
348
- throw new Error(`Wrong object type for transaction options: ${opts}`);
354
+ if (opts !== undefined) validateObject(opts as Record<string, any>, {}, {}, 'opts');
349
355
 
350
356
  const _opts = {
351
357
  ...opts,
@@ -382,12 +388,15 @@ function validateOpts(opts: TArg<TxOpts>): TRet<Readonly<TxOpts>> {
382
388
  throw new Error(`Transation options wrong type: ${k}=${v} (${typeof v})`);
383
389
  }
384
390
  // 0 and -1 happens in tests
391
+ // With allowUnknownVersion any numeric version is fine; the ternary was inverted
392
+ // before 2026-07 (audit), which made the option throw for every numeric version.
385
393
  if (
386
394
  _opts.allowUnknownVersion
387
- ? typeof _opts.version === 'number'
395
+ ? typeof _opts.version !== 'number'
388
396
  : ![-1, 0, 1, 2, 3].includes(_opts.version)
389
397
  )
390
398
  throw new Error(`Unknown version: ${_opts.version}`);
399
+ P.I32LE.encode(_opts.version); // Validate the signed transaction-version wire domain.
391
400
  if (_opts.customScripts !== undefined) {
392
401
  const cs = _opts.customScripts;
393
402
  if (!Array.isArray(cs)) {
@@ -407,6 +416,7 @@ function validateOpts(opts: TArg<TxOpts>): TRet<Readonly<TxOpts>> {
407
416
 
408
417
  // NOTE: we cannot do this inside PSBTInput coder, because there is no index/txid at this point!
409
418
  function validateInput(i: TArg<psbt.TransactionInput>): TRet<PSBTInputs> {
419
+ validateObject(i as Record<string, any>, {}, {}, 'i');
410
420
  const _i = i as PSBTInputs;
411
421
  if (_i.nonWitnessUtxo && _i.index !== undefined) {
412
422
  const last = _i.nonWitnessUtxo.outputs.length - 1;
@@ -433,6 +443,9 @@ function validateInput(i: TArg<psbt.TransactionInput>): TRet<PSBTInputs> {
433
443
  allowUnknownOutputs: true,
434
444
  disableScriptCheck: true,
435
445
  allowUnknownInputs: true,
446
+ // Consensus does not restrict nVersion; a previous tx with a non-standard
447
+ // version is still spendable and its txid must still be verifiable.
448
+ allowUnknownVersion: true,
436
449
  });
437
450
  const txid = hex.encode(_i.txid);
438
451
  // BIP174 requires the provided nonWitnessUtxo to hash to the prevout txid even when the
@@ -465,6 +478,7 @@ export type PSBTOutputs = psbt.PSBTKeyMapKeys<typeof psbt.PSBTOutput>;
465
478
  * ```
466
479
  */
467
480
  export function getPrevOut(input: TArg<psbt.TransactionInput>): P.UnwrapCoder<typeof RawOutput> {
481
+ validateObject(input as Record<string, any>, {}, {}, 'input');
468
482
  const _input = input as PSBTInputs;
469
483
  if (_input.nonWitnessUtxo) {
470
484
  if (_input.index === undefined) throw new Error('Unknown input index');
@@ -478,8 +492,17 @@ export function getPrevOut(input: TArg<psbt.TransactionInput>): P.UnwrapCoder<ty
478
492
  )
479
493
  throw new Error(`Wrong input index=${_input.index}`);
480
494
  return _input.nonWitnessUtxo.outputs[_input.index];
481
- } else if (_input.witnessUtxo) return _input.witnessUtxo;
482
- else throw new Error('Cannot find previous output info');
495
+ } else if ('witnessUtxo' in _input) {
496
+ // The presence check catches malformed provided values; narrow after the guard for TS.
497
+ const prev = _input.witnessUtxo as P.UnwrapCoder<typeof RawOutput>;
498
+ validateObject(prev as Record<string, any>, {}, {}, 'input.witnessUtxo');
499
+ abigint(prev.amount, 'input.witnessUtxo.amount');
500
+ if (!isBytes(prev.script))
501
+ throw new TypeError(
502
+ '"input.witnessUtxo.script" expected Uint8Array, got type=' + typeof prev.script
503
+ );
504
+ return prev;
505
+ } else throw new Error('Cannot find previous output info');
483
506
  }
484
507
 
485
508
  /**
@@ -510,6 +533,9 @@ export function normalizeInput(
510
533
  disableScriptCheck = false,
511
534
  allowUnknown = false
512
535
  ): TRet<PSBTInputs> {
536
+ validateObject(i as Record<string, any>, {}, {}, 'i');
537
+ if (cur !== undefined) validateObject(cur as Record<string, any>, {}, {}, 'cur');
538
+ if (allowedFields !== undefined) u.aarray(allowedFields, 'allowedFields');
513
539
  const _i = i as psbt.TransactionInputUpdate;
514
540
  const _cur = cur as PSBTInputs | undefined;
515
541
  const _allowedFields = allowedFields as readonly (keyof PSBTInputs)[] | undefined;
@@ -718,6 +744,7 @@ export class Transaction {
718
744
  toPSBT(
719
745
  PSBTVersion: number | undefined = this.global.version || this.opts.PSBTVersion
720
746
  ): Uint8Array {
747
+ if (PSBTVersion !== undefined) anumber(PSBTVersion, 'PSBTVersion');
721
748
  if (PSBTVersion !== 0 && PSBTVersion !== 2)
722
749
  throw new Error(`Wrong PSBT version=${PSBTVersion}`);
723
750
  // if (PSBTVersion === 0 && this.inputs.length === 0) {
@@ -878,27 +905,33 @@ export class Transaction {
878
905
 
879
906
  // Info utils
880
907
  get hasWitnesses(): boolean {
881
- let out = false;
882
908
  for (const i of this.inputs)
883
- if (i.finalScriptWitness && i.finalScriptWitness.length) out = true;
884
- return out;
909
+ if (i.finalScriptWitness && i.finalScriptWitness.length) return true;
910
+ return false;
885
911
  }
886
912
  // https://en.bitcoin.it/wiki/Weight_units
887
913
  get weight(): number {
888
914
  if (!this.isFinal) throw new Error('Transaction is not finalized');
915
+ // Serialized length of VarBytes(data) without allocating the encoded copy
916
+ const varLen = (dataLen: number) => CompactSizeLen.encode(dataLen).length + dataLen;
917
+ const hasWitnesses = this.hasWitnesses;
889
918
  let out = 32;
890
919
  // Outputs
891
920
  const outputs = this.outputs.map(outputBeforeSign);
892
921
  out += 4 * CompactSizeLen.encode(this.outputs.length).length;
893
- for (const o of outputs) out += 32 + 4 * VarBytes.encode(o.script).length;
922
+ for (const o of outputs) out += 32 + 4 * varLen(o.script.length);
894
923
  // Inputs
895
- if (this.hasWitnesses) out += 2;
924
+ if (hasWitnesses) out += 2;
896
925
  out += 4 * CompactSizeLen.encode(this.inputs.length).length;
897
926
  for (const i of this.inputs) {
898
- out += 160 + 4 * VarBytes.encode(i.finalScriptSig || P.EMPTY).length;
927
+ out += 160 + 4 * varLen((i.finalScriptSig || P.EMPTY).length);
899
928
  // Once segwit serialization is active, every input contributes one witness vector, including
900
929
  // legacy inputs whose empty vector still encodes as a single zero-item-count byte.
901
- if (this.hasWitnesses) out += RawWitness.encode(i.finalScriptWitness || []).length;
930
+ if (hasWitnesses) {
931
+ const witness = i.finalScriptWitness || [];
932
+ out += CompactSizeLen.encode(witness.length).length;
933
+ for (const w of witness) out += varLen(w.length);
934
+ }
902
935
  }
903
936
  return out;
904
937
  }
@@ -933,8 +966,8 @@ export class Transaction {
933
966
  }
934
967
  // Input stuff
935
968
  private checkInputIdx(idx: number) {
936
- if (!Number.isSafeInteger(idx) || 0 > idx || idx >= this.inputs.length)
937
- throw new Error(`Wrong input index=${idx}`);
969
+ anumber(idx, 'idx');
970
+ if (idx >= this.inputs.length) throw new Error(`Wrong input index=${idx}`);
938
971
  }
939
972
  getInput(idx: number): psbt.TransactionInput {
940
973
  this.checkInputIdx(idx);
@@ -945,6 +978,7 @@ export class Transaction {
945
978
  }
946
979
  // Modification
947
980
  addInput(input: TArg<psbt.TransactionInputUpdate>, _ignoreSignStatus = false): number {
981
+ validateObject(input as Record<string, any>, {}, {}, 'input');
948
982
  if (!_ignoreSignStatus && !this.signStatus().addInput)
949
983
  throw new Error('Tx has signed inputs, cannot add new one');
950
984
  // normalizeInput preserves nested caller-owned byte arrays, so detach them here before the
@@ -982,8 +1016,8 @@ export class Transaction {
982
1016
  }
983
1017
  // Output stuff
984
1018
  private checkOutputIdx(idx: number) {
985
- if (!Number.isSafeInteger(idx) || 0 > idx || idx >= this.outputs.length)
986
- throw new Error(`Wrong output index=${idx}`);
1019
+ anumber(idx, 'idx');
1020
+ if (idx >= this.outputs.length) throw new Error(`Wrong output index=${idx}`);
987
1021
  }
988
1022
  getOutput(idx: number): psbt.TransactionOutput {
989
1023
  this.checkOutputIdx(idx);
@@ -1005,12 +1039,10 @@ export class Transaction {
1005
1039
  cur?: PSBTOutputs,
1006
1040
  allowedFields?: readonly (keyof typeof psbt.PSBTOutput)[]
1007
1041
  ): PSBTOutputs {
1042
+ validateObject(o as Record<string, any>, {}, {}, 'o');
1008
1043
  let { amount, script } = o;
1009
1044
  if (amount === undefined) amount = cur?.amount;
1010
- if (typeof amount !== 'bigint')
1011
- throw new Error(
1012
- `Wrong amount type, should be of type bigint in sats, but got ${amount} of type ${typeof amount}`
1013
- );
1045
+ amount = abigint(amount, 'o.amount');
1014
1046
  if (typeof script === 'string') script = hex.decode(script);
1015
1047
  if (script === undefined) script = cur?.script;
1016
1048
  let res: PSBTOutputs = { ...cur, ...(o as PSBTOutputs & { script?: string }), amount, script };
@@ -1065,7 +1097,7 @@ export class Transaction {
1065
1097
  }
1066
1098
  // Utils
1067
1099
  get fee(): bigint {
1068
- let res = 0n;
1100
+ let res = _0n;
1069
1101
  for (const i of this.inputs) {
1070
1102
  const prevOut = getPrevOut(i);
1071
1103
  if (!prevOut) throw new Error('Empty input amount');
@@ -1084,7 +1116,7 @@ export class Transaction {
1084
1116
  const { isAny, isNone, isSingle } = unpackSighash(hashType);
1085
1117
  if (idx < 0 || !Number.isSafeInteger(idx)) throw new Error(`Invalid input idx=${idx}`);
1086
1118
  if ((isSingle && idx >= this.outputs.length) || idx >= this.inputs.length)
1087
- return P.U256BE.encode(1n);
1119
+ return P.U256BE.encode(_1n);
1088
1120
  prevOutScript = stripCodeSeparator(prevOutScript);
1089
1121
  let inputs: TransactionInputRequired[] = this.inputs
1090
1122
  .map(inputBeforeSign)
@@ -1124,8 +1156,8 @@ export class Transaction {
1124
1156
  ): Uint8Array {
1125
1157
  // BIP143 serializes txTo.vin[nIn].prevout and txTo.vin[nIn].nSequence, so reject an invalid
1126
1158
  // nIn explicitly instead of leaking a later undefined-input TypeError from inputs[idx].
1127
- if (idx < 0 || !Number.isSafeInteger(idx) || idx >= this.inputs.length)
1128
- throw new Error(`Invalid input idx=${idx}`);
1159
+ anumber(idx, 'idx');
1160
+ if (idx >= this.inputs.length) throw new Error(`Invalid input idx=${idx}`);
1129
1161
  const { isAny, isNone, isSingle } = unpackSighash(hashType);
1130
1162
  let inputHash = EMPTY32;
1131
1163
  let sequenceHash = EMPTY32;
@@ -1164,15 +1196,16 @@ export class Transaction {
1164
1196
  leafVer = 0xc0,
1165
1197
  annex?: Bytes
1166
1198
  ): Uint8Array {
1167
- if (!Array.isArray(amount) || this.inputs.length !== amount.length)
1168
- throw new Error(`Invalid amounts array=${amount}`);
1169
- if (!Array.isArray(prevOutScript) || this.inputs.length !== prevOutScript.length)
1170
- throw new Error(`Invalid prevOutScript array=${prevOutScript}`);
1171
1199
  // BIP341 SigMsg commits either to input_index or to the selected input's outpoint/amount/script/
1172
1200
  // sequence under ANYONECANPAY, so reject an invalid index explicitly instead of hashing a
1173
1201
  // nonexistent input or leaking a later integer-encoding RangeError for negative idx.
1174
- if (idx < 0 || !Number.isSafeInteger(idx) || idx >= this.inputs.length)
1175
- throw new Error(`Invalid input idx=${idx}`);
1202
+ anumber(idx, 'idx');
1203
+ if (idx >= this.inputs.length) throw new Error(`Invalid input idx=${idx}`);
1204
+ u.aarray(amount, 'amount');
1205
+ u.aarray(prevOutScript, 'prevOutScript');
1206
+ if (this.inputs.length !== amount.length) throw new Error(`Invalid amounts array=${amount}`);
1207
+ if (this.inputs.length !== prevOutScript.length)
1208
+ throw new Error(`Invalid prevOutScript array=${prevOutScript}`);
1176
1209
  const out: Bytes[] = [
1177
1210
  P.U8.encode(0),
1178
1211
  P.U8.encode(hashType), // U8 sigHash
@@ -1216,6 +1249,18 @@ export class Transaction {
1216
1249
  }
1217
1250
  // Signer can be privateKey OR instance of bip32 HD stuff
1218
1251
  signIdx(privateKey: Signer, idx: number, allowedSighash?: SigHash[], _auxRand?: Bytes): boolean {
1252
+ if (!isBytes(privateKey)) {
1253
+ // HDKey is a structural external instance, so plain-object validation would
1254
+ // reject valid signers.
1255
+ if (
1256
+ !privateKey ||
1257
+ typeof privateKey !== 'object' ||
1258
+ typeof (privateKey as HDKey).deriveChild !== 'function'
1259
+ )
1260
+ throw new TypeError(
1261
+ '"privateKey" expected Uint8Array or HDKey, got type=' + typeof privateKey
1262
+ );
1263
+ }
1219
1264
  this.checkInputIdx(idx);
1220
1265
  const input = this.inputs[idx];
1221
1266
  const inputType = getInputType(
@@ -1438,7 +1483,7 @@ export class Transaction {
1438
1483
 
1439
1484
  finalizeIdx(idx: number): void {
1440
1485
  this.checkInputIdx(idx);
1441
- if (this.fee < 0n) throw new Error('Outputs spends more than inputs amount');
1486
+ if (this.fee < _0n) throw new Error('Outputs spends more than inputs amount');
1442
1487
  const input = this.inputs[idx];
1443
1488
  const inputType = getInputType(input, this.opts.allowLegacyWitnessUtxo);
1444
1489
  // Taproot finalize
@@ -1592,10 +1637,12 @@ export class Transaction {
1592
1637
  extract(): Uint8Array {
1593
1638
  if (!this.isFinal) throw new Error('Transaction has unfinalized inputs');
1594
1639
  if (!this.outputs.length) throw new Error('Transaction has no outputs');
1595
- if (this.fee < 0n) throw new Error('Outputs spends more than inputs amount');
1640
+ if (this.fee < _0n) throw new Error('Outputs spends more than inputs amount');
1596
1641
  return this.toBytes(true, true);
1597
1642
  }
1598
1643
  combine(other: Transaction): this {
1644
+ if (!(other instanceof Transaction))
1645
+ throw new TypeError('"other" expected Transaction, got type=' + typeof other);
1599
1646
  // BIP174 combiners merge same-transaction PSBTs across versions and emit the highest required
1600
1647
  // version, so PSBTVersion mismatches are normalized below instead of treated as conflicts.
1601
1648
  const PSBTVersion = Math.max(this.opts.PSBTVersion || 0, other.opts.PSBTVersion || 0);
package/src/utils.ts CHANGED
@@ -4,6 +4,7 @@ import { ripemd160 } from '@noble/hashes/legacy.js';
4
4
  import { sha256 as nobleSha256 } from '@noble/hashes/sha2.js';
5
5
  import { type TArg, type TRet } from '@noble/hashes/utils.js';
6
6
  import { utils as packedUtils, U32LE } from 'micro-packed';
7
+ export { abytes, validateObject as vld } from '@noble/curves/utils.js';
7
8
  export { type TArg, type TRet } from '@noble/hashes/utils.js';
8
9
 
9
10
  /** Hex-like input accepted by helpers in this module. */
@@ -11,9 +12,71 @@ export type Hex = string | Uint8Array;
11
12
  /** Byte array alias used across the library. */
12
13
  export type Bytes = Uint8Array;
13
14
 
15
+ /**
16
+ * Validates that a value is a non-negative bigint.
17
+ * @param n - Value to validate.
18
+ * @param title - Label included in thrown errors.
19
+ * @returns The same bigint.
20
+ * @throws On wrong argument types. {@link TypeError}
21
+ * @example
22
+ * Validate a satoshi amount before transaction encoding.
23
+ * ```ts
24
+ * abigint(1n, 'amount');
25
+ * ```
26
+ */
27
+ export function abigint(n: unknown, title: string = 'value'): bigint {
28
+ if (typeof n !== 'bigint')
29
+ throw new TypeError(`"${title}" expected bigint, got type=${typeof n}`);
30
+ if (n < _0n) throw new RangeError(`"${title}" expected non-negative bigint, got ${n}`);
31
+ return n;
32
+ }
33
+
34
+ import { validateObject as vld } from '@noble/curves/utils.js';
35
+
36
+ export function aarray<T>(
37
+ item: unknown,
38
+ title: string,
39
+ inner: (elm: T, title: string) => void = () => {}
40
+ ): T[] {
41
+ if (!Array.isArray(item))
42
+ throw new TypeError(`"${title}" expected array, got type=${typeof item}`);
43
+ for (let i = 0; i < item.length; i++) inner(item[i], `${title}[${i}]`);
44
+ return item;
45
+ }
46
+ /**
47
+ * Asserts something is a string.
48
+ * @param value - Value to validate.
49
+ * @param title - Label included in thrown errors.
50
+ * @returns The validated string.
51
+ * @throws On wrong argument types. {@link TypeError}
52
+ * @example
53
+ * Validate a label string.
54
+ *
55
+ * ```ts
56
+ * astring('example', 'label');
57
+ * ```
58
+ */
59
+ export function astring(value: unknown, title: string = ''): string {
60
+ if (typeof value !== 'string') {
61
+ const prefix = title && `"${title}" `;
62
+ throw new TypeError(prefix + 'expected string, got type=' + typeof value);
63
+ }
64
+ return value;
65
+ }
66
+ export function validateObject(
67
+ object: Record<string, any>,
68
+ fields: Record<string, string> = {},
69
+ optFields: Record<string, string> = {},
70
+ _title = 'object'
71
+ ) {
72
+ return vld(object, fields, optFields);
73
+ }
14
74
  const Point = /* @__PURE__ */ (() => secp.Point)();
15
75
  const Fn = /* @__PURE__ */ (() => Point.Fn)();
16
76
  const CURVE_ORDER = /* @__PURE__ */ (() => Point.Fn.ORDER)();
77
+ // Be friendly to bad ECMAScript parsers by not using bigint literals.
78
+ // prettier-ignore
79
+ const _0n = /* @__PURE__ */ BigInt(0), _2n = /* @__PURE__ */ BigInt(2);
17
80
  /**
18
81
  * Checks whether a curve y-coordinate is even.
19
82
  * @param y - y-coordinate to inspect
@@ -24,7 +87,7 @@ const CURVE_ORDER = /* @__PURE__ */ (() => Point.Fn.ORDER)();
24
87
  * hasEven(2n);
25
88
  * ```
26
89
  */
27
- export const hasEven = (y: bigint) => y % 2n === 0n;
90
+ export const hasEven = (y: bigint) => y % _2n === _0n;
28
91
 
29
92
  /**
30
93
  * Checks whether a value is a Uint8Array.
@@ -144,7 +207,10 @@ export const pubECDSA = (privateKey: TArg<Uint8Array>, isCompressed?: boolean):
144
207
  // noble/secp256k1 does not support the feature: it is not used outside of BTC.
145
208
  // We implement it manually, because in BTC it's common.
146
209
  // Not best way, but closest to bitcoin implementation (easier to check)
147
- const hasLowR = (sig: { r: bigint; s: bigint }) => sig.r < CURVE_ORDER / 2n;
210
+ // Hoisted: the bound is constant; no need to redo the bigint division on every
211
+ // grinding-loop iteration. n/2 < 2^255, so r < n/2 guarantees the 32-byte DER r.
212
+ const LOW_R_BOUND = /* @__PURE__ */ (() => CURVE_ORDER / _2n)();
213
+ const hasLowR = (sig: { r: bigint; s: bigint }) => sig.r < LOW_R_BOUND;
148
214
  /**
149
215
  * Signs a 32-byte hash with ECDSA and returns DER encoding.
150
216
  * @param hash - message hash to sign
package/src/utxo.ts CHANGED
@@ -2,7 +2,7 @@ import { hex } from '@scure/base';
2
2
  import * as P from 'micro-packed';
3
3
  import { Address, type CustomScript, OutScript, checkScript, tapLeafHash } from './payment.ts';
4
4
  import * as psbt from './psbt.ts';
5
- import { CompactSizeLen, RawWitness, Script, VarBytes } from './script.ts';
5
+ import { CompactSizeLen, Script } from './script.ts';
6
6
  import {
7
7
  SignatureHash,
8
8
  Transaction,
@@ -14,6 +14,9 @@ import {
14
14
  toVsize,
15
15
  } from './transaction.ts';
16
16
  import {
17
+ abigint,
18
+ aarray,
19
+ astring,
17
20
  type Bytes,
18
21
  NETWORK,
19
22
  PubT,
@@ -24,6 +27,7 @@ import {
24
27
  isBytes,
25
28
  sha256,
26
29
  validatePubkey,
30
+ validateObject,
27
31
  } from './utils.ts';
28
32
 
29
33
  // UTXO Select
@@ -41,6 +45,11 @@ export type Accumulated =
41
45
  type TapLeafScript = psbt.TransactionInput['tapLeafScript'];
42
46
  type TB = Parameters<typeof psbt.TaprootControlBlock.encode>[0];
43
47
  const encodeTapBlock = (item: TB) => psbt.TaprootControlBlock.encode(item);
48
+ // Be friendly to bad ECMAScript parsers by not using bigint literals.
49
+ // prettier-ignore
50
+ const _0n = /* @__PURE__ */ BigInt(0), _3n = /* @__PURE__ */ BigInt(3);
51
+ // Serialized length of VarBytes(data) without allocating the encoded copy
52
+ const varLen = (dataLen: number) => CompactSizeLen.encode(dataLen).length + dataLen;
44
53
 
45
54
  function iterLeafs(
46
55
  tapLeafScript: TArg<TapLeafScript>,
@@ -175,10 +184,11 @@ function estimateInput(
175
184
  } else if (inputType.type.startsWith('wsh-')) {
176
185
  } else if (inputType.txType !== 'segwit') script = inputScript;
177
186
  }
178
- let weight = 160 + 4 * VarBytes.encode(script).length;
187
+ let weight = 160 + 4 * varLen(script.length);
179
188
  let hasWitnesses = false;
180
189
  if (witness) {
181
- weight += RawWitness.encode(witness).length;
190
+ weight += CompactSizeLen.encode(witness.length).length;
191
+ for (const w of witness) weight += varLen(w.length);
182
192
  hasWitnesses = true;
183
193
  }
184
194
  return { weight, hasWitnesses };
@@ -189,8 +199,8 @@ export const _cmpBig = (a: bigint, b: bigint): 0 | 1 | -1 => {
189
199
  // Array.sort comparators must return a number, so normalize bigint comparisons to -1/0/1
190
200
  // instead of coercing large differences through Number(...) and losing ordering precision.
191
201
  const n = a - b;
192
- if (n < 0n) return -1;
193
- else if (n > 0n) return 1;
202
+ if (n < _0n) return -1;
203
+ else if (n > _0n) return 1;
194
204
  return 0;
195
205
  };
196
206
 
@@ -212,6 +222,7 @@ export type EstimatorOpts = TxOpts & {
212
222
  };
213
223
 
214
224
  function getScript(o: TArg<Output>, opts: TArg<TxOpts> = {}, network = NETWORK) {
225
+ validateObject(o as Record<string, any>, {}, {}, 'output');
215
226
  const _o = o as Output;
216
227
  const _opts = opts as TxOpts;
217
228
  let script;
@@ -219,8 +230,7 @@ function getScript(o: TArg<Output>, opts: TArg<TxOpts> = {}, network = NETWORK)
219
230
  script = _o.script;
220
231
  }
221
232
  if ('address' in _o) {
222
- if (typeof _o.address !== 'string')
223
- throw new Error(`Estimator: wrong output address=${_o.address}`);
233
+ astring(_o.address, 'output.address');
224
234
  // Address.decode() only yields known descriptors for valid output addresses, but the wrapped
225
235
  // coder type still includes `undefined`, so narrow before re-encoding the script template.
226
236
  script = OutScript.encode(
@@ -228,15 +238,9 @@ function getScript(o: TArg<Output>, opts: TArg<TxOpts> = {}, network = NETWORK)
228
238
  );
229
239
  }
230
240
  if (!script) throw new Error('Estimator: wrong output script');
231
- if (typeof _o.amount !== 'bigint')
232
- throw new Error(
233
- `Estimator: wrong output amount=${
234
- _o.amount
235
- }, should be of type bigint but got ${typeof _o.amount}.`
236
- );
237
241
  // Keep selector-only `createTx: false` flows aligned with the transaction/PSBT output boundary:
238
242
  // satoshi-denominated outputs are not allowed to go negative.
239
- if (_o.amount < 0n) throw new Error(`Estimator: wrong output amount=${_o.amount}`);
243
+ abigint(_o.amount, 'output.amount');
240
244
  if (script && !_opts.allowUnknownOutputs && OutScript.decode(script).type === 'unknown') {
241
245
  throw new Error(
242
246
  'Estimator: unknown output script type, there is a chance that input is unspendable. Pass allowUnknownOutputs=true, if you sure'
@@ -282,16 +286,9 @@ export class _Estimator {
282
286
  constructor(inputs: psbt.TransactionInputUpdate[], outputs: Output[], opts: EstimatorOpts) {
283
287
  this.outputs = outputs;
284
288
  this.opts = opts;
285
- if (typeof opts.feePerByte !== 'bigint')
286
- throw new Error(
287
- `Estimator: wrong feePerByte=${
288
- opts.feePerByte
289
- }, should be of type bigint but got ${typeof opts.feePerByte}.`
290
- );
291
289
  // Zero-fee estimation is useful on regtest/in tests, but negative fee rates would make
292
290
  // `getSatoshi(...)` produce nonsensical negative fees throughout selection.
293
- if (opts.feePerByte < 0n)
294
- throw new Error(`Estimator: feePerByte must be >= 0 satoshi per vbyte`);
291
+ abigint(opts.feePerByte, 'opts.feePerByte');
295
292
  // Dust stuff
296
293
  // TODO: think about this more:
297
294
  // - current dust filters tx which cannot be relayed by core
@@ -304,46 +301,37 @@ export class _Estimator {
304
301
  const inputsDust = 32 + 4 + 1 + 107 + 4; // NOTE: can be smaller for segwit tx?
305
302
  const outputDust = 34; // NOTE: 'nSize = GetSerializeSize(txout)'
306
303
  const dustBytes = opts.dust === undefined ? BigInt(inputsDust + outputDust) : opts.dust;
307
- if (typeof dustBytes !== 'bigint') {
308
- throw new Error(
309
- `Estimator: wrong dust=${opts.dust}, should be of type bigint but got ${typeof opts.dust}.`
310
- );
311
- }
304
+ abigint(dustBytes, 'opts.dust');
312
305
  // 3 sat/vb is the default minimum fee rate used to calculate dust thresholds by bitcoin core.
313
306
  // 3000 sat/kvb -> 3 sat/vb.
314
307
  // https://github.com/bitcoin/bitcoin/blob/27a770b34b8f1dbb84760f442edb3e23a0c2420b/src/policy/policy.h#L55
315
- const dustFee = opts.dustRelayFeeRate === undefined ? 3n : opts.dustRelayFeeRate;
316
- if (typeof dustFee !== 'bigint') {
317
- throw new Error(
318
- `Estimator: wrong dustRelayFeeRate=${opts.dustRelayFeeRate}, should be of type bigint but got ${typeof opts.dustRelayFeeRate}.`
319
- );
320
- }
308
+ const dustFee = opts.dustRelayFeeRate === undefined ? _3n : opts.dustRelayFeeRate;
309
+ abigint(dustFee, 'opts.dustRelayFeeRate');
321
310
  // Dust uses feePerbyte by default, but we allow separate dust fee if needed
322
311
  this.dust = dustBytes * dustFee;
323
312
  if (opts.requiredInputs !== undefined && !Array.isArray(opts.requiredInputs))
324
313
  throw new Error(`Estimator: wrong required inputs=${opts.requiredInputs}`);
325
314
  const network = opts.network || NETWORK;
326
- let amount = 0n;
315
+ let amount = _0n;
327
316
  // Base weight: tx with outputs, no inputs
328
317
  let baseWeight = 32;
329
318
  for (const o of outputs) {
330
319
  const script = getScript(o, opts, opts.network);
331
- baseWeight += 32 + 4 * VarBytes.encode(script).length;
320
+ baseWeight += 32 + 4 * varLen(script.length);
332
321
  amount += o.amount;
333
322
  }
334
- if (typeof opts.changeAddress !== 'string')
335
- throw new Error(`Estimator: wrong change address=${opts.changeAddress}`);
323
+ astring(opts.changeAddress, 'opts.changeAddress');
336
324
  let changeWeight =
337
325
  baseWeight +
338
326
  32 +
339
327
  // Same Address.decode() narrowing as above: the estimator only reaches this path for a
340
328
  // concrete change output address, not an unknown descriptor.
341
329
  4 *
342
- VarBytes.encode(
330
+ varLen(
343
331
  OutScript.encode(
344
332
  Address(network).decode(opts.changeAddress) as Parameters<typeof OutScript.encode>[0]
345
- )
346
- ).length;
333
+ ).length
334
+ );
347
335
  baseWeight += 4 * CompactSizeLen.encode(outputs.length).length;
348
336
  // If there a lot of outputs change can change fee
349
337
  changeWeight += 4 * CompactSizeLen.encode(outputs.length + 1).length;
@@ -441,7 +429,7 @@ export class _Estimator {
441
429
  let weight = this.opts.alwaysChange ? this.changeWeight : this.baseWeight;
442
430
  let hasWitnesses = false;
443
431
  let num = 0;
444
- let inputsAmount = 0n;
432
+ let inputsAmount = _0n;
445
433
  const targetAmount = this.amount;
446
434
  const res: Set<number> = new Set();
447
435
  let fee;
@@ -483,7 +471,7 @@ export class _Estimator {
483
471
  // Negative: cost of using input is more than value provided (negative)
484
472
  // By default 'blackjack' mode in coinselect doesn't use that, which means
485
473
  // it will use negative output if sorted by 'smallest'
486
- if (skipNegative && value <= 0n) continue;
474
+ if (skipNegative && value <= _0n) continue;
487
475
  weight = newWeight;
488
476
  if (estimate.hasWitnesses) hasWitnesses = true;
489
477
  num = newNum;
@@ -495,6 +483,10 @@ export class _Estimator {
495
483
  }
496
484
  if (all) {
497
485
  const total = getTotal(weight, num);
486
+ // 'all' accumulates unconditionally, so sufficiency must be checked here; otherwise
487
+ // result() would report a negative fee (or throw its internal negative-change error).
488
+ // Insufficient funds are a selection failure, same as for accumulation strategies.
489
+ if (targetAmount + total.fee > inputsAmount) return undefined;
498
490
  return {
499
491
  indices: Array.from(res),
500
492
  fee: total.fee,
@@ -572,7 +564,7 @@ export class _Estimator {
572
564
  if (needChange) {
573
565
  fee = changeFee;
574
566
  // this shouldn't happen!
575
- if (change < 0n) throw new Error(`Estimator.result: negative change=${change}`);
567
+ if (change < _0n) throw new Error(`Estimator.result: negative change=${change}`);
576
568
  outputs.push({ address: this.opts.changeAddress, amount: change });
577
569
  }
578
570
  if (this.opts.bip69) {
@@ -634,6 +626,10 @@ export function selectUTXO(
634
626
  strategy: SelectionStrategy,
635
627
  opts: TArg<EstimatorOpts>
636
628
  ) {
629
+ aarray(inputs, 'inputs');
630
+ aarray(outputs, 'outputs');
631
+ validateObject(opts as Record<string, any>, {}, {}, 'opts');
632
+ astring(strategy, 'strategy');
637
633
  // Public wrapper defaults to BIP69 ordering and tx construction unless callers override them.
638
634
  const _opts = { createTx: true, bip69: true, ...(opts as EstimatorOpts) };
639
635
  const est = new _Estimator(inputs as psbt.TransactionInputUpdate[], outputs as Output[], _opts);
package/transaction.d.ts CHANGED
@@ -374,4 +374,3 @@ export declare function PSBTCombine(psbts: TArg<Bytes[]>): TRet<Bytes>;
374
374
  */
375
375
  export declare function bip32Path(path: string): number[];
376
376
  export {};
377
- //# sourceMappingURL=transaction.d.ts.map