@scure/btc-signer 2.2.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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
@@ -345,10 +411,32 @@ export function taprootTweakPubkey(pubKey: TArg<Bytes>, h: TArg<Bytes>): TRet<[B
345
411
  // This is the fixed BIP 341 H example, not the privacy-preserving H + rG variant.
346
412
  // Downstream helpers use exact-byte equality with it to recognize
347
413
  // library-generated script-only outputs.
348
- /** Standard unspendable internal key used for script-only Taproot outputs. */
349
- export const TAPROOT_UNSPENDABLE_KEY: TRet<Bytes> = /* @__PURE__ */ (() =>
414
+ // Keep the value used by library internals private: exported Uint8Arrays are mutable, so the
415
+ // public compatibility export below cannot safely be a source of cryptographic key material.
416
+ const INTERNAL_TAPROOT_NUMS: TRet<Bytes> = /* @__PURE__ */ (() =>
350
417
  sha256(Point.BASE.toBytes(false)) as TRet<Bytes>)();
351
418
 
419
+ /**
420
+ * Standard unspendable internal key used for script-only Taproot outputs.
421
+ * @deprecated Use {@link taprootNumsKey} to receive an owned copy.
422
+ */
423
+ export const TAPROOT_UNSPENDABLE_KEY: TRet<Bytes> = /* @__PURE__ */ (() =>
424
+ Uint8Array.from(INTERNAL_TAPROOT_NUMS) as TRet<Bytes>)();
425
+
426
+ /**
427
+ * Returns an owned copy of the library's stable Taproot NUMS key.
428
+ * @returns A new 32-byte NUMS key copy.
429
+ * @example
430
+ * Obtain an internal key without sharing mutable exported storage.
431
+ * ```ts
432
+ * import { taprootNumsKey } from '@scure/btc-signer/utils.js';
433
+ * const internalKey = taprootNumsKey();
434
+ * ```
435
+ */
436
+ export function taprootNumsKey(): TRet<Bytes> {
437
+ return Uint8Array.from(INTERNAL_TAPROOT_NUMS) as TRet<Bytes>;
438
+ }
439
+
352
440
  /** Bitcoin network parameters. */
353
441
  export type BTC_NETWORK = {
354
442
  /** Human-readable prefix used by Bech32 and Bech32m addresses. */
package/src/utxo.ts CHANGED
@@ -1,8 +1,15 @@
1
1
  import { hex } from '@scure/base';
2
2
  import * as P from 'micro-packed';
3
- import { Address, type CustomScript, OutScript, checkScript, tapLeafHash } from './payment.ts';
3
+ import {
4
+ Address,
5
+ type CustomScript,
6
+ OutScript,
7
+ _WitnessOutScript,
8
+ checkScript,
9
+ tapLeafHash,
10
+ } from './payment.ts';
4
11
  import * as psbt from './psbt.ts';
5
- import { CompactSizeLen, RawWitness, Script, VarBytes } from './script.ts';
12
+ import { CompactSizeLen, RawWitness, Script } from './script.ts';
6
13
  import {
7
14
  SignatureHash,
8
15
  Transaction,
@@ -14,16 +21,22 @@ import {
14
21
  toVsize,
15
22
  } from './transaction.ts';
16
23
  import {
24
+ abigint,
25
+ aarray,
26
+ astring,
17
27
  type Bytes,
18
28
  NETWORK,
19
29
  PubT,
20
- TAPROOT_UNSPENDABLE_KEY,
21
30
  type TArg,
31
+ type TRet,
22
32
  compareBytes,
23
33
  equalBytes,
24
34
  isBytes,
25
35
  sha256,
36
+ taprootNumsKey,
37
+ taprootTweakPubkey,
26
38
  validatePubkey,
39
+ validateObject,
27
40
  } from './utils.ts';
28
41
 
29
42
  // UTXO Select
@@ -39,73 +52,174 @@ export type Accumulated =
39
52
  }
40
53
  | undefined;
41
54
  type TapLeafScript = psbt.TransactionInput['tapLeafScript'];
55
+ type TapLeaf = NonNullable<TapLeafScript>[number];
42
56
  type TB = Parameters<typeof psbt.TaprootControlBlock.encode>[0];
43
57
  const encodeTapBlock = (item: TB) => psbt.TaprootControlBlock.encode(item);
58
+ // Be friendly to bad ECMAScript parsers by not using bigint literals.
59
+ // prettier-ignore
60
+ const _0n = /* @__PURE__ */ BigInt(0), _3n = /* @__PURE__ */ BigInt(3);
61
+ // Serialized length of VarBytes(data) without allocating the encoded copy
62
+ const varLen = (dataLen: number) => CompactSizeLen.encode(dataLen).length + dataLen;
63
+
64
+ const tapLeafWitness = (
65
+ leaf: TArg<TapLeaf>,
66
+ sigSize: number,
67
+ customScripts?: TArg<CustomScript[]>,
68
+ pubkeys?: Set<string>,
69
+ unknownError = 'Finalize: Unknown tapLeafScript'
70
+ ): TRet<Bytes[] | undefined> => {
71
+ const [cb, _script] = leaf as TapLeaf;
72
+ const _customScripts = customScripts as CustomScript[] | undefined;
73
+ // Last byte is version
74
+ const script = _script.slice(0, -1);
75
+ const ver = _script[_script.length - 1];
76
+ const outs = OutScript.decode(script);
77
+ const available = (pubkey: TArg<Bytes>) => !pubkeys || pubkeys.has(hex.encode(pubkey as Bytes));
78
+ const empty = () => new Uint8Array(sigSize);
79
+ let signatures: Bytes[] = [];
80
+ if (outs.type === 'tr_ms') {
81
+ let added = 0;
82
+ for (const pubkey of outs.pubkeys) {
83
+ if (added === outs.m || !available(pubkey)) signatures.push(P.EMPTY);
84
+ else {
85
+ signatures.push(empty());
86
+ added++;
87
+ }
88
+ }
89
+ if (added !== outs.m) return;
90
+ } else if (outs.type === 'tr_ns') {
91
+ for (const pubkey of outs.pubkeys) {
92
+ if (!available(pubkey)) return;
93
+ signatures.push(empty());
94
+ }
95
+ } else {
96
+ if (!_customScripts) throw new Error(unknownError);
97
+ const leafHash = tapLeafHash(script, ver);
98
+ const scriptDecoded = Script.decode(script);
99
+ const scriptPubkeys = scriptDecoded.filter((i) => {
100
+ if (!isBytes(i)) return false;
101
+ try {
102
+ validatePubkey(i, PubT.schnorr);
103
+ return true;
104
+ } catch (e) {
105
+ return false;
106
+ }
107
+ }) as Bytes[];
108
+ const availablePubkeys = scriptPubkeys.filter(available);
109
+ // A custom finalizer may treat an unexpected empty signature list as malformed input. If the
110
+ // script embeds keys but none are owned, the path is unavailable without consulting the hook.
111
+ if (pubkeys && scriptPubkeys.length && !availablePubkeys.length) return;
112
+ let recognized = false;
113
+ for (const c of _customScripts) {
114
+ if (!c.finalizeTaproot) continue;
115
+ const csEncoded = c.encode(scriptDecoded);
116
+ if (csEncoded === undefined) continue;
117
+ recognized = true;
118
+ const finalized = c.finalizeTaproot(
119
+ script,
120
+ csEncoded,
121
+ availablePubkeys.map((pubKey) => [{ pubKey, leafHash }, empty()])
122
+ );
123
+ if (finalized) return finalized.concat(encodeTapBlock(cb)) as TRet<Bytes[]>;
124
+ }
125
+ if (recognized && pubkeys) return;
126
+ throw new Error(unknownError);
127
+ }
128
+ // Witness is stack, so last element will be used first
129
+ return signatures.reverse().concat([script, encodeTapBlock(cb)]) as TRet<Bytes[]>;
130
+ };
44
131
 
45
132
  function iterLeafs(
46
133
  tapLeafScript: TArg<TapLeafScript>,
47
134
  sigSize: number,
48
135
  customScripts?: TArg<CustomScript[]>
49
- ) {
136
+ ): TRet<Bytes[]> {
50
137
  const _tapLeafScript = tapLeafScript as TapLeafScript;
51
138
  const _customScripts = customScripts as CustomScript[] | undefined;
52
139
  if (!_tapLeafScript || !_tapLeafScript.length) throw new Error('no leafs');
53
- // Dummy non-empty Schnorr signature bytes for weight estimation.
54
- // Unsigned tr_ms slots use P.EMPTY below.
55
- const empty = () => new Uint8Array(sigSize);
56
- // If user want to select specific leaf, which can signed,
57
- // it is possible to remove all other leafs manually.
58
- // Sort leafs by control block length.
59
- const leafs = _tapLeafScript.sort(
60
- (a, b) => encodeTapBlock(a[0]).length - encodeTapBlock(b[0]).length
61
- );
62
- for (const [cb, _script] of leafs) {
63
- // Last byte is version
64
- const script = _script.slice(0, -1);
65
- const ver = _script[_script.length - 1];
66
- const outs = OutScript.decode(script);
140
+ // Start with the old shallowest-path order for stable equal-weight ties. Full witness size can
141
+ // reverse that order when a shallow leaf needs a larger script or more signatures.
142
+ const leafs = _tapLeafScript
143
+ .slice()
144
+ .sort((a, b) => encodeTapBlock(a[0]).length - encodeTapBlock(b[0]).length);
145
+ let smallest: Bytes[] | undefined;
146
+ let smallestSize = Number.POSITIVE_INFINITY;
147
+ for (const leaf of leafs) {
148
+ const witness = tapLeafWitness(leaf, sigSize, _customScripts);
149
+ if (!witness) continue;
150
+ const size = RawWitness.encode(witness).length;
151
+ if (size >= smallestSize) continue;
152
+ smallest = witness;
153
+ smallestSize = size;
154
+ }
155
+ if (!smallest) throw new Error('there was no witness');
156
+ return smallest as TRet<Bytes[]>;
157
+ }
67
158
 
68
- let signatures: Bytes[] = [];
69
- if (outs.type === 'tr_ms') {
70
- const m = outs.m;
71
- const n = outs.pubkeys.length - m;
72
- for (let i = 0; i < m; i++) signatures.push(empty());
73
- for (let i = 0; i < n; i++) signatures.push(P.EMPTY);
74
- } else if (outs.type === 'tr_ns') {
75
- for (const _pub of outs.pubkeys) signatures.push(empty());
76
- } else {
77
- if (!_customScripts) throw new Error('Finalize: Unknown tapLeafScript');
78
- const leafHash = tapLeafHash(script, ver);
79
- for (const c of _customScripts) {
80
- if (!c.finalizeTaproot) continue;
81
- const scriptDecoded = Script.decode(script);
82
- const csEncoded = c.encode(scriptDecoded);
83
- if (csEncoded === undefined) continue;
84
- const pubKeys = scriptDecoded.filter((i) => {
85
- if (!isBytes(i)) return false;
86
- try {
87
- validatePubkey(i, PubT.schnorr);
88
- return true;
89
- } catch (e) {
90
- return false;
91
- }
92
- }) as Bytes[];
93
- const finalized = c.finalizeTaproot(
94
- script,
95
- csEncoded,
96
- pubKeys.map((pubKey) => [{ pubKey, leafHash }, empty()])
97
- );
98
- if (!finalized) continue;
99
- return finalized.concat(encodeTapBlock(cb));
100
- }
101
- // UTXO selection may run without the real signer/finalizer process. When no matching local
102
- // finalizeTaproot hook exists, keep a minimal script-path witness lower bound here instead of
103
- // failing selection; callers that need exact fee estimates must provide the matching hook.
159
+ /**
160
+ * Removes Taproot spend paths that cannot be satisfied by the supplied Schnorr public keys.
161
+ * Non-Taproot inputs are retained, and caller-owned input metadata is never mutated.
162
+ * @param inputs - candidate PSBT input records to filter
163
+ * @param pubkeys - available x-only Schnorr public keys
164
+ * @returns Copies of inputs that retain at least one available path
165
+ * @example
166
+ * Filter wallet UTXOs before selecting coins.
167
+ * ```ts
168
+ * import { filterTaproot } from '@scure/btc-signer/utxo.js';
169
+ * import { pubSchnorr, randomPrivateKeyBytes } from '@scure/btc-signer/utils.js';
170
+ * const spendable = filterTaproot([], [pubSchnorr(randomPrivateKeyBytes())]);
171
+ * ```
172
+ */
173
+ export function filterTaproot(
174
+ inputs: TArg<psbt.TransactionInputUpdate[]>,
175
+ pubkeys: TArg<Bytes[]>
176
+ ): TRet<psbt.TransactionInputUpdate[]> {
177
+ aarray(inputs, 'inputs');
178
+ aarray(pubkeys, 'pubkeys');
179
+ const _inputs = inputs as psbt.TransactionInputUpdate[];
180
+ const _pubkeys = pubkeys as Bytes[];
181
+ const keys = new Set<string>();
182
+ for (const pubkey of _pubkeys) keys.add(hex.encode(validatePubkey(pubkey, PubT.schnorr)));
183
+ const res: psbt.TransactionInputUpdate[] = [];
184
+ for (const input of _inputs) {
185
+ const filtered = { ...input };
186
+ // BIP371 path fields are optional and empty keyed lists encode as absence, so the committed
187
+ // previous-output script is the authoritative input type.
188
+ const prevScript = _WitnessOutScript.decode(
189
+ getPrevOut(filtered as TArg<psbt.TransactionInput>).script
190
+ );
191
+ if (prevScript.type !== 'tr') {
192
+ res.push(filtered);
193
+ continue;
194
+ }
195
+ if (filtered.tapInternalKey) {
196
+ // A control block can reconstruct the root, but BIP371 does not require that inference.
197
+ // Omission may be broken data or an earlier filter deliberately disabling the key path.
198
+ // Recognize root omission only when the prevout proves this is an empty-tree commitment.
199
+ const hasRoot =
200
+ filtered.tapMerkleRoot !== undefined ||
201
+ equalBytes(taprootTweakPubkey(filtered.tapInternalKey, P.EMPTY)[0], prevScript.pubkey);
202
+ if (
203
+ !hasRoot ||
204
+ equalBytes(filtered.tapInternalKey, taprootNumsKey()) ||
205
+ (!keys.has(hex.encode(filtered.tapInternalKey)) && !keys.has(hex.encode(prevScript.pubkey)))
206
+ )
207
+ delete filtered.tapInternalKey;
208
+ }
209
+ if (filtered.tapLeafScript) {
210
+ const sigSize =
211
+ filtered.sighashType !== undefined && filtered.sighashType !== SignatureHash.DEFAULT
212
+ ? 65
213
+ : 64;
214
+ const leafs = filtered.tapLeafScript.filter((leaf) =>
215
+ tapLeafWitness(leaf, sigSize, undefined, keys, 'filterTaproot: unknown Taproot leaf')
216
+ );
217
+ if (leafs.length) filtered.tapLeafScript = leafs;
218
+ else delete filtered.tapLeafScript;
104
219
  }
105
- // Witness is stack, so last element will be used first
106
- return signatures.reverse().concat([script, encodeTapBlock(cb)]);
220
+ if (filtered.tapInternalKey || filtered.tapLeafScript) res.push(filtered);
107
221
  }
108
- throw new Error('there was no witness');
222
+ return res as TRet<psbt.TransactionInputUpdate[]>;
109
223
  }
110
224
 
111
225
  function estimateInput(
@@ -121,13 +235,9 @@ function estimateInput(
121
235
  // schnorr sig is always 64 bytes. except for cases when sighash is not default!
122
236
  if (inputType.txType === 'taproot') {
123
237
  const SCHNORR_SIG_SIZE = inputType.sighash !== SignatureHash.DEFAULT ? 65 : 64;
124
- // BIP371 `PSBT_IN_TAP_INTERNAL_KEY` is signer metadata, but UTXO selection
125
- // runs before signer availability is known. We intentionally treat a
126
- // present internal key as a key-path hint here to avoid overestimating fees
127
- // on the online side. Callers that know only script-path signing is
128
- // possible should omit `tapInternalKey` or pre-filter `tapLeafScript`
129
- // before estimation.
130
- if (_input.tapInternalKey && !equalBytes(_input.tapInternalKey, TAPROOT_UNSPENDABLE_KEY)) {
238
+ // A real internal key and every retained leaf declare paths available to this caller. Use
239
+ // filterTaproot before estimation when the supplied input contains unavailable paths.
240
+ if (_input.tapInternalKey && !equalBytes(_input.tapInternalKey, taprootNumsKey())) {
131
241
  witness = [new Uint8Array(SCHNORR_SIG_SIZE)];
132
242
  } else if (_input.tapLeafScript) {
133
243
  witness = iterLeafs(_input.tapLeafScript, SCHNORR_SIG_SIZE, _opts.customScripts);
@@ -175,10 +285,11 @@ function estimateInput(
175
285
  } else if (inputType.type.startsWith('wsh-')) {
176
286
  } else if (inputType.txType !== 'segwit') script = inputScript;
177
287
  }
178
- let weight = 160 + 4 * VarBytes.encode(script).length;
288
+ let weight = 160 + 4 * varLen(script.length);
179
289
  let hasWitnesses = false;
180
290
  if (witness) {
181
- weight += RawWitness.encode(witness).length;
291
+ weight += CompactSizeLen.encode(witness.length).length;
292
+ for (const w of witness) weight += varLen(w.length);
182
293
  hasWitnesses = true;
183
294
  }
184
295
  return { weight, hasWitnesses };
@@ -189,8 +300,8 @@ export const _cmpBig = (a: bigint, b: bigint): 0 | 1 | -1 => {
189
300
  // Array.sort comparators must return a number, so normalize bigint comparisons to -1/0/1
190
301
  // instead of coercing large differences through Number(...) and losing ordering precision.
191
302
  const n = a - b;
192
- if (n < 0n) return -1;
193
- else if (n > 0n) return 1;
303
+ if (n < _0n) return -1;
304
+ else if (n > _0n) return 1;
194
305
  return 0;
195
306
  };
196
307
 
@@ -209,9 +320,12 @@ export type EstimatorOpts = TxOpts & {
209
320
  createTx?: boolean; // Create tx inside selection
210
321
  requiredInputs?: psbt.TransactionInputUpdate[]; // these inputs always will be used
211
322
  allowSameUtxo?: boolean; // allow using UTXO multiple times (for test purposes)
323
+ /** Filter Taproot candidate paths to those satisfiable by these Schnorr public keys. */
324
+ filterTaproot?: Bytes[];
212
325
  };
213
326
 
214
327
  function getScript(o: TArg<Output>, opts: TArg<TxOpts> = {}, network = NETWORK) {
328
+ validateObject(o as Record<string, any>, {}, {}, 'output');
215
329
  const _o = o as Output;
216
330
  const _opts = opts as TxOpts;
217
331
  let script;
@@ -219,8 +333,7 @@ function getScript(o: TArg<Output>, opts: TArg<TxOpts> = {}, network = NETWORK)
219
333
  script = _o.script;
220
334
  }
221
335
  if ('address' in _o) {
222
- if (typeof _o.address !== 'string')
223
- throw new Error(`Estimator: wrong output address=${_o.address}`);
336
+ astring(_o.address, 'output.address');
224
337
  // Address.decode() only yields known descriptors for valid output addresses, but the wrapped
225
338
  // coder type still includes `undefined`, so narrow before re-encoding the script template.
226
339
  script = OutScript.encode(
@@ -228,16 +341,10 @@ function getScript(o: TArg<Output>, opts: TArg<TxOpts> = {}, network = NETWORK)
228
341
  );
229
342
  }
230
343
  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
344
  // Keep selector-only `createTx: false` flows aligned with the transaction/PSBT output boundary:
238
345
  // satoshi-denominated outputs are not allowed to go negative.
239
- if (_o.amount < 0n) throw new Error(`Estimator: wrong output amount=${_o.amount}`);
240
- if (script && !_opts.allowUnknownOutputs && OutScript.decode(script).type === 'unknown') {
346
+ abigint(_o.amount, 'output.amount');
347
+ if (script && !_opts.allowUnknownOutputs && _WitnessOutScript.decode(script).type === 'unknown') {
241
348
  throw new Error(
242
349
  'Estimator: unknown output script type, there is a chance that input is unspendable. Pass allowUnknownOutputs=true, if you sure'
243
350
  );
@@ -254,10 +361,7 @@ type AccumStrategy = `accum${SortStrategy}`;
254
361
 
255
362
  /** Supported UTXO selection strategies. */
256
363
  export type SelectionStrategy =
257
- | 'all'
258
- | 'default'
259
- | AccumStrategy
260
- | `${ExactStrategy}/${AccumStrategy}`;
364
+ 'all' | 'default' | AccumStrategy | `${ExactStrategy}/${AccumStrategy}`;
261
365
 
262
366
  // class, because we need to re-use normalized inputs, instead of parsing each time
263
367
  // internal stuff, exported for tests only
@@ -280,18 +384,14 @@ export class _Estimator {
280
384
  private outputs: Output[];
281
385
  private opts: EstimatorOpts;
282
386
  constructor(inputs: psbt.TransactionInputUpdate[], outputs: Output[], opts: EstimatorOpts) {
387
+ // EstimatorOpts extends TxOpts, so resolve the complete transaction policy once even when
388
+ // createTx=false. This keeps selection normalization and a returned Transaction identical.
389
+ opts = new Transaction(opts).opts as EstimatorOpts;
283
390
  this.outputs = outputs;
284
391
  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
392
  // Zero-fee estimation is useful on regtest/in tests, but negative fee rates would make
292
393
  // `getSatoshi(...)` produce nonsensical negative fees throughout selection.
293
- if (opts.feePerByte < 0n)
294
- throw new Error(`Estimator: feePerByte must be >= 0 satoshi per vbyte`);
394
+ abigint(opts.feePerByte, 'opts.feePerByte');
295
395
  // Dust stuff
296
396
  // TODO: think about this more:
297
397
  // - current dust filters tx which cannot be relayed by core
@@ -304,46 +404,37 @@ export class _Estimator {
304
404
  const inputsDust = 32 + 4 + 1 + 107 + 4; // NOTE: can be smaller for segwit tx?
305
405
  const outputDust = 34; // NOTE: 'nSize = GetSerializeSize(txout)'
306
406
  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
- }
407
+ abigint(dustBytes, 'opts.dust');
312
408
  // 3 sat/vb is the default minimum fee rate used to calculate dust thresholds by bitcoin core.
313
409
  // 3000 sat/kvb -> 3 sat/vb.
314
410
  // 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
- }
411
+ const dustFee = opts.dustRelayFeeRate === undefined ? _3n : opts.dustRelayFeeRate;
412
+ abigint(dustFee, 'opts.dustRelayFeeRate');
321
413
  // Dust uses feePerbyte by default, but we allow separate dust fee if needed
322
414
  this.dust = dustBytes * dustFee;
323
415
  if (opts.requiredInputs !== undefined && !Array.isArray(opts.requiredInputs))
324
416
  throw new Error(`Estimator: wrong required inputs=${opts.requiredInputs}`);
325
417
  const network = opts.network || NETWORK;
326
- let amount = 0n;
418
+ let amount = _0n;
327
419
  // Base weight: tx with outputs, no inputs
328
420
  let baseWeight = 32;
329
421
  for (const o of outputs) {
330
422
  const script = getScript(o, opts, opts.network);
331
- baseWeight += 32 + 4 * VarBytes.encode(script).length;
423
+ baseWeight += 32 + 4 * varLen(script.length);
332
424
  amount += o.amount;
333
425
  }
334
- if (typeof opts.changeAddress !== 'string')
335
- throw new Error(`Estimator: wrong change address=${opts.changeAddress}`);
426
+ astring(opts.changeAddress, 'opts.changeAddress');
336
427
  let changeWeight =
337
428
  baseWeight +
338
429
  32 +
339
430
  // Same Address.decode() narrowing as above: the estimator only reaches this path for a
340
431
  // concrete change output address, not an unknown descriptor.
341
432
  4 *
342
- VarBytes.encode(
433
+ varLen(
343
434
  OutScript.encode(
344
435
  Address(network).decode(opts.changeAddress) as Parameters<typeof OutScript.encode>[0]
345
- )
346
- ).length;
436
+ ).length
437
+ );
347
438
  baseWeight += 4 * CompactSizeLen.encode(outputs.length).length;
348
439
  // If there a lot of outputs change can change fee
349
440
  changeWeight += 4 * CompactSizeLen.encode(outputs.length + 1).length;
@@ -362,7 +453,8 @@ export class _Estimator {
362
453
  undefined,
363
454
  undefined,
364
455
  opts.disableScriptCheck,
365
- opts.allowUnknown
456
+ opts.unknown!,
457
+ opts.proprietary!
366
458
  );
367
459
  inputBeforeSign(normalized as TArg<psbt.TransactionInput>); // check fields
368
460
  const key = `${hex.encode(normalized.txid!)}:${normalized.index}`;
@@ -441,7 +533,7 @@ export class _Estimator {
441
533
  let weight = this.opts.alwaysChange ? this.changeWeight : this.baseWeight;
442
534
  let hasWitnesses = false;
443
535
  let num = 0;
444
- let inputsAmount = 0n;
536
+ let inputsAmount = _0n;
445
537
  const targetAmount = this.amount;
446
538
  const res: Set<number> = new Set();
447
539
  let fee;
@@ -483,7 +575,7 @@ export class _Estimator {
483
575
  // Negative: cost of using input is more than value provided (negative)
484
576
  // By default 'blackjack' mode in coinselect doesn't use that, which means
485
577
  // it will use negative output if sorted by 'smallest'
486
- if (skipNegative && value <= 0n) continue;
578
+ if (skipNegative && value <= _0n) continue;
487
579
  weight = newWeight;
488
580
  if (estimate.hasWitnesses) hasWitnesses = true;
489
581
  num = newNum;
@@ -495,6 +587,10 @@ export class _Estimator {
495
587
  }
496
588
  if (all) {
497
589
  const total = getTotal(weight, num);
590
+ // 'all' accumulates unconditionally, so sufficiency must be checked here; otherwise
591
+ // result() would report a negative fee (or throw its internal negative-change error).
592
+ // Insufficient funds are a selection failure, same as for accumulation strategies.
593
+ if (targetAmount + total.fee > inputsAmount) return undefined;
498
594
  return {
499
595
  indices: Array.from(res),
500
596
  fee: total.fee,
@@ -572,7 +668,7 @@ export class _Estimator {
572
668
  if (needChange) {
573
669
  fee = changeFee;
574
670
  // this shouldn't happen!
575
- if (change < 0n) throw new Error(`Estimator.result: negative change=${change}`);
671
+ if (change < _0n) throw new Error(`Estimator.result: negative change=${change}`);
576
672
  outputs.push({ address: this.opts.changeAddress, amount: change });
577
673
  }
578
674
  if (this.opts.bip69) {
@@ -634,8 +730,22 @@ export function selectUTXO(
634
730
  strategy: SelectionStrategy,
635
731
  opts: TArg<EstimatorOpts>
636
732
  ) {
733
+ aarray(inputs, 'inputs');
734
+ aarray(outputs, 'outputs');
735
+ validateObject(opts as Record<string, any>, {}, {}, 'opts');
736
+ astring(strategy, 'strategy');
637
737
  // Public wrapper defaults to BIP69 ordering and tx construction unless callers override them.
638
738
  const _opts = { createTx: true, bip69: true, ...(opts as EstimatorOpts) };
639
- const est = new _Estimator(inputs as psbt.TransactionInputUpdate[], outputs as Output[], _opts);
739
+ let candidates = inputs as psbt.TransactionInputUpdate[];
740
+ if (_opts.filterTaproot !== undefined) {
741
+ candidates = filterTaproot(candidates, _opts.filterTaproot);
742
+ if (_opts.requiredInputs) {
743
+ const requiredInputs = filterTaproot(_opts.requiredInputs, _opts.filterTaproot);
744
+ if (requiredInputs.length !== _opts.requiredInputs.length)
745
+ throw new Error('filterTaproot: required input has no available Taproot path');
746
+ _opts.requiredInputs = requiredInputs;
747
+ }
748
+ }
749
+ const est = new _Estimator(candidates, outputs as Output[], _opts);
640
750
  return est.result(strategy);
641
751
  }