@scure/btc-signer 1.1.0 → 1.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/index.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  import { secp256k1 as _secp, schnorr } from '@noble/curves/secp256k1';
3
3
  import { sha256 } from '@noble/hashes/sha256';
4
4
  import { ripemd160 } from '@noble/hashes/ripemd160';
5
- import { hex, base58check as _b58, bech32, bech32m } from '@scure/base';
5
+ import { hex, createBase58check, bech32, bech32m } from '@scure/base';
6
6
  import type { Coder } from '@scure/base';
7
7
  import * as P from 'micro-packed';
8
8
 
@@ -24,7 +24,7 @@ const hash160 = (msg: Bytes) => ripemd160(sha256(msg));
24
24
  const sha256x2 = (...msgs: Bytes[]) => sha256(sha256(concat(...msgs)));
25
25
  const concat = P.concatBytes;
26
26
  // Make base58check work
27
- export const base58check = _b58(sha256);
27
+ export const base58check = createBase58check(sha256);
28
28
 
29
29
  export function cloneDeep<T>(obj: T): T {
30
30
  if (Array.isArray(obj)) return obj.map((i) => cloneDeep(i)) as unknown as T;
@@ -416,7 +416,7 @@ type PSBTKeyMapInfo = Readonly<
416
416
  any,
417
417
  readonly number[], // versionsRequiringInclusion
418
418
  readonly number[], // versionsAllowsInclusion
419
- boolean // silentIgnore
419
+ boolean, // silentIgnore
420
420
  ]
421
421
  >;
422
422
 
@@ -1705,6 +1705,109 @@ function validateOpts(opts: TxOpts) {
1705
1705
  return Object.freeze(_opts);
1706
1706
  }
1707
1707
 
1708
+ // Normalizes input
1709
+ function getPrevOut(input: TransactionInput): P.UnwrapCoder<typeof RawOutput> {
1710
+ if (input.nonWitnessUtxo) {
1711
+ if (input.index === undefined) throw new Error('Unknown input index');
1712
+ return input.nonWitnessUtxo.outputs[input.index];
1713
+ } else if (input.witnessUtxo) return input.witnessUtxo;
1714
+ else throw new Error('Cannot find previous output info');
1715
+ }
1716
+
1717
+ function normalizeInput(
1718
+ i: TransactionInputUpdate,
1719
+ cur?: TransactionInput,
1720
+ allowedFields?: (keyof TransactionInput)[],
1721
+ disableScriptCheck = false
1722
+ ): TransactionInput {
1723
+ let { nonWitnessUtxo, txid } = i;
1724
+ // String support for common fields. We usually prefer Uint8Array to avoid errors
1725
+ // like hex looking string accidentally passed, however, in case of nonWitnessUtxo
1726
+ // it is better to expect string, since constructing this complex object will be
1727
+ // difficult for user
1728
+ if (typeof nonWitnessUtxo === 'string') nonWitnessUtxo = hex.decode(nonWitnessUtxo);
1729
+ if (isBytes(nonWitnessUtxo)) nonWitnessUtxo = RawTx.decode(nonWitnessUtxo);
1730
+ if (!('nonWitnessUtxo' in i) && nonWitnessUtxo === undefined)
1731
+ nonWitnessUtxo = cur?.nonWitnessUtxo;
1732
+ if (typeof txid === 'string') txid = hex.decode(txid);
1733
+ // TODO: if we have nonWitnessUtxo, we can extract txId from here
1734
+ if (txid === undefined) txid = cur?.txid;
1735
+ let res: PSBTKeyMapKeys<typeof PSBTInput> = { ...cur, ...i, nonWitnessUtxo, txid };
1736
+ if (!('nonWitnessUtxo' in i) && res.nonWitnessUtxo === undefined) delete res.nonWitnessUtxo;
1737
+ if (res.sequence === undefined) res.sequence = DEFAULT_SEQUENCE;
1738
+ if (res.tapMerkleRoot === null) delete res.tapMerkleRoot;
1739
+ res = mergeKeyMap(PSBTInput, res, cur, allowedFields);
1740
+ PSBTInputCoder.encode(res); // Validates that everything is correct at this point
1741
+
1742
+ let prevOut;
1743
+ if (res.nonWitnessUtxo && res.index !== undefined)
1744
+ prevOut = res.nonWitnessUtxo.outputs[res.index];
1745
+ else if (res.witnessUtxo) prevOut = res.witnessUtxo;
1746
+ if (prevOut && !disableScriptCheck)
1747
+ checkScript(prevOut && prevOut.script, res.redeemScript, res.witnessScript);
1748
+ return res;
1749
+ }
1750
+
1751
+ function getInputType(input: TransactionInput, allowLegacyWitnessUtxo = false) {
1752
+ let txType = 'legacy';
1753
+ let defaultSighash = SignatureHash.ALL;
1754
+ const prevOut = getPrevOut(input);
1755
+ const first = OutScript.decode(prevOut.script);
1756
+ let type = first.type;
1757
+ let cur = first;
1758
+ const stack = [first];
1759
+ if (first.type === 'tr') {
1760
+ defaultSighash = SignatureHash.DEFAULT;
1761
+ return {
1762
+ txType: 'taproot',
1763
+ type: 'tr',
1764
+ last: first,
1765
+ lastScript: prevOut.script,
1766
+ defaultSighash,
1767
+ sighash: input.sighashType || defaultSighash,
1768
+ };
1769
+ } else {
1770
+ if (first.type === 'wpkh' || first.type === 'wsh') txType = 'segwit';
1771
+ if (first.type === 'sh') {
1772
+ if (!input.redeemScript) throw new Error('inputType: sh without redeemScript');
1773
+ let child = OutScript.decode(input.redeemScript);
1774
+ if (child.type === 'wpkh' || child.type === 'wsh') txType = 'segwit';
1775
+ stack.push(child);
1776
+ cur = child;
1777
+ type += `-${child.type}`;
1778
+ }
1779
+ // wsh can be inside sh
1780
+ if (cur.type === 'wsh') {
1781
+ if (!input.witnessScript) throw new Error('inputType: wsh without witnessScript');
1782
+ let child = OutScript.decode(input.witnessScript);
1783
+ if (child.type === 'wsh') txType = 'segwit';
1784
+ stack.push(child);
1785
+ cur = child;
1786
+ type += `-${child.type}`;
1787
+ }
1788
+ const last = stack[stack.length - 1];
1789
+ if (last.type === 'sh' || last.type === 'wsh')
1790
+ throw new Error('inputType: sh/wsh cannot be terminal type');
1791
+ const lastScript = OutScript.encode(last);
1792
+ const res = {
1793
+ type,
1794
+ txType,
1795
+ last,
1796
+ lastScript,
1797
+ defaultSighash,
1798
+ sighash: input.sighashType || defaultSighash,
1799
+ };
1800
+ if (txType === 'legacy' && !allowLegacyWitnessUtxo && !input.nonWitnessUtxo) {
1801
+ throw new Error(
1802
+ `Transaction/sign: legacy input without nonWitnessUtxo, can result in attack that forces paying higher fees. Pass allowLegacyWitnessUtxo=true, if you sure`
1803
+ );
1804
+ }
1805
+ return res;
1806
+ }
1807
+ }
1808
+
1809
+ const toVsize = (weight: number) => Math.ceil(weight / 4);
1810
+
1708
1811
  export class Transaction {
1709
1812
  private global: PSBTKeyMapKeys<typeof PSBTGlobal> = {};
1710
1813
  private inputs: TransactionInput[] = []; // use getInput()
@@ -1845,7 +1948,7 @@ export class Transaction {
1845
1948
  // We will lose some vectors -> smaller test coverage of preimages (very important!)
1846
1949
  private inputSighash(idx: number) {
1847
1950
  this.checkInputIdx(idx);
1848
- const sighash = this.inputType(this.inputs[idx]).sighash;
1951
+ const sighash = getInputType(this.inputs[idx], this.opts.allowLegacyWitnessUtxo).sighash;
1849
1952
  // ALL or DEFAULT -- everything signed
1850
1953
  // NONE -- all inputs + no outputs
1851
1954
  // SINGLE -- all inputs + output with same index
@@ -1898,23 +2001,23 @@ export class Transaction {
1898
2001
  // https://en.bitcoin.it/wiki/Weight_units
1899
2002
  get weight(): number {
1900
2003
  if (!this.isFinal) throw new Error('Transaction is not finalized');
1901
- // TODO: Can we find out how much witnesses/script will be used before signing?
1902
2004
  let out = 32;
2005
+ // Outputs
1903
2006
  const outputs = this.outputs.map(outputBeforeSign);
2007
+ out += 4 * CompactSizeLen.encode(this.outputs.length).length;
2008
+ for (const o of outputs) out += 32 + 4 * VarBytes.encode(o.script).length;
2009
+ // Inputs
1904
2010
  if (this.hasWitnesses) out += 2;
1905
2011
  out += 4 * CompactSizeLen.encode(this.inputs.length).length;
1906
- out += 4 * CompactSizeLen.encode(this.outputs.length).length;
1907
- for (const i of this.inputs)
2012
+ for (const i of this.inputs) {
1908
2013
  out += 160 + 4 * VarBytes.encode(i.finalScriptSig || P.EMPTY).length;
1909
- for (const o of outputs) out += 32 + 4 * VarBytes.encode(o.script).length;
1910
- if (this.hasWitnesses) {
1911
- for (const i of this.inputs)
1912
- if (i.finalScriptWitness) out += RawWitness.encode(i.finalScriptWitness).length;
2014
+ if (this.hasWitnesses && i.finalScriptWitness)
2015
+ out += RawWitness.encode(i.finalScriptWitness).length;
1913
2016
  }
1914
2017
  return out;
1915
2018
  }
1916
2019
  get vsize(): number {
1917
- return Math.ceil(this.weight / 4);
2020
+ return toVsize(this.weight);
1918
2021
  }
1919
2022
  toBytes(withScriptSig = false, withWitness = false) {
1920
2023
  return RawTx.encode({
@@ -1957,39 +2060,10 @@ export class Transaction {
1957
2060
  return this.inputs.length;
1958
2061
  }
1959
2062
  // Modification
1960
- private normalizeInput(
1961
- i: TransactionInputUpdate,
1962
- cur?: TransactionInput,
1963
- allowedFields?: (keyof TransactionInput)[]
1964
- ): TransactionInput {
1965
- let { nonWitnessUtxo, txid } = i;
1966
- // String support for common fields. We usually prefer Uint8Array to avoid errors (like hex looking string accidentally passed),
1967
- // however in case of nonWitnessUtxo it is better to expect string, since constructing this complex object will be difficult for user
1968
- if (typeof nonWitnessUtxo === 'string') nonWitnessUtxo = hex.decode(nonWitnessUtxo);
1969
- if (isBytes(nonWitnessUtxo)) nonWitnessUtxo = RawTx.decode(nonWitnessUtxo);
1970
- if (nonWitnessUtxo === undefined) nonWitnessUtxo = cur?.nonWitnessUtxo;
1971
- if (typeof txid === 'string') txid = hex.decode(txid);
1972
- if (txid === undefined) txid = cur?.txid;
1973
- let res: PSBTKeyMapKeys<typeof PSBTInput> = { ...cur, ...i, nonWitnessUtxo, txid };
1974
- if (res.nonWitnessUtxo === undefined) delete res.nonWitnessUtxo;
1975
- if (res.sequence === undefined) res.sequence = DEFAULT_SEQUENCE;
1976
- if (res.tapMerkleRoot === null) delete res.tapMerkleRoot;
1977
- res = mergeKeyMap(PSBTInput, res, cur, allowedFields);
1978
- PSBTInputCoder.encode(res); // Validates that everything is correct at this point
1979
-
1980
- let prevOut;
1981
- if (res.nonWitnessUtxo && res.index !== undefined)
1982
- prevOut = res.nonWitnessUtxo.outputs[res.index];
1983
- else if (res.witnessUtxo) prevOut = res.witnessUtxo;
1984
- if (prevOut && !this.opts.disableScriptCheck)
1985
- checkScript(prevOut && prevOut.script, res.redeemScript, res.witnessScript);
1986
-
1987
- return res;
1988
- }
1989
2063
  addInput(input: TransactionInputUpdate, _ignoreSignStatus = false): number {
1990
2064
  if (!_ignoreSignStatus && !this.signStatus().addInput)
1991
2065
  throw new Error('Tx has signed inputs, cannot add new one');
1992
- this.inputs.push(this.normalizeInput(input));
2066
+ this.inputs.push(normalizeInput(input, undefined, undefined, this.opts.disableScriptCheck));
1993
2067
  return this.inputs.length - 1;
1994
2068
  }
1995
2069
  updateInput(idx: number, input: TransactionInputUpdate, _ignoreSignStatus = false) {
@@ -1999,7 +2073,12 @@ export class Transaction {
1999
2073
  const status = this.signStatus();
2000
2074
  if (!status.addInput || status.inputs.includes(idx)) allowedFields = PSBTInputUnsignedKeys;
2001
2075
  }
2002
- this.inputs[idx] = this.normalizeInput(input, this.inputs[idx], allowedFields);
2076
+ this.inputs[idx] = normalizeInput(
2077
+ input,
2078
+ this.inputs[idx],
2079
+ allowedFields,
2080
+ this.opts.disableScriptCheck
2081
+ );
2003
2082
  }
2004
2083
  // Output stuff
2005
2084
  private checkOutputIdx(idx: number) {
@@ -2033,7 +2112,7 @@ export class Transaction {
2033
2112
  OutScript.decode(res.script).type === 'unknown'
2034
2113
  ) {
2035
2114
  throw new Error(
2036
- 'Transaction/output: unknown output script type, there is a chance that input is unspendable. Pass allowUnknownScript=true, if you sure'
2115
+ 'Transaction/output: unknown output script type, there is a chance that input is unspendable. Pass allowUnknownOutputs=true, if you sure'
2037
2116
  );
2038
2117
  }
2039
2118
  if (!this.opts.disableScriptCheck) checkScript(res.script, res.redeemScript, res.witnessScript);
@@ -2061,7 +2140,7 @@ export class Transaction {
2061
2140
  get fee(): bigint {
2062
2141
  let res = 0n;
2063
2142
  for (const i of this.inputs) {
2064
- const prevOut = this.prevOut(i);
2143
+ const prevOut = getPrevOut(i);
2065
2144
  if (!prevOut) throw new Error('Empty input amount');
2066
2145
  res += prevOut.amount;
2067
2146
  }
@@ -2109,7 +2188,7 @@ export class Transaction {
2109
2188
  });
2110
2189
  return sha256x2(tmpTx, P.I32LE.encode(hashType));
2111
2190
  }
2112
- private preimageWitnessV0(idx: number, prevOutScript: Bytes, hashType: number, amount: bigint) {
2191
+ preimageWitnessV0(idx: number, prevOutScript: Bytes, hashType: number, amount: bigint) {
2113
2192
  const { isAny, isNone, isSingle } = unpackSighash(hashType);
2114
2193
  let inputHash = EMPTY32;
2115
2194
  let sequenceHash = EMPTY32;
@@ -2138,7 +2217,7 @@ export class Transaction {
2138
2217
  P.U32LE.encode(hashType)
2139
2218
  );
2140
2219
  }
2141
- private preimageWitnessV1(
2220
+ preimageWitnessV1(
2142
2221
  idx: number,
2143
2222
  prevOutScript: Bytes[],
2144
2223
  hashType: number,
@@ -2193,78 +2272,11 @@ export class Transaction {
2193
2272
  out.push(tapLeafHash(leafScript, leafVer), P.U8.encode(0), P.I32LE.encode(codeSeparator));
2194
2273
  return schnorr.utils.taggedHash('TapSighash', ...out);
2195
2274
  }
2196
- // Utils for sign/finalize
2197
- // Used pretty often, should be fast
2198
- private prevOut(input: TransactionInput): P.UnwrapCoder<typeof RawOutput> {
2199
- if (input.nonWitnessUtxo) {
2200
- if (input.index === undefined) throw new Error('Unknown input index');
2201
- return input.nonWitnessUtxo.outputs[input.index];
2202
- } else if (input.witnessUtxo) return input.witnessUtxo;
2203
- else throw new Error('Cannot find previous output info');
2204
- }
2205
- private inputType(input: TransactionInput) {
2206
- let txType = 'legacy';
2207
- let defaultSighash = SignatureHash.ALL;
2208
- const prevOut = this.prevOut(input);
2209
- const first = OutScript.decode(prevOut.script);
2210
- let type = first.type;
2211
- let cur = first;
2212
- const stack = [first];
2213
- if (first.type === 'tr') {
2214
- defaultSighash = SignatureHash.DEFAULT;
2215
- return {
2216
- txType: 'taproot',
2217
- type: 'tr',
2218
- last: first,
2219
- lastScript: prevOut.script,
2220
- defaultSighash,
2221
- sighash: input.sighashType || defaultSighash,
2222
- };
2223
- } else {
2224
- if (first.type === 'wpkh' || first.type === 'wsh') txType = 'segwit';
2225
- if (first.type === 'sh') {
2226
- if (!input.redeemScript) throw new Error('inputType: sh without redeemScript');
2227
- let child = OutScript.decode(input.redeemScript);
2228
- if (child.type === 'wpkh' || child.type === 'wsh') txType = 'segwit';
2229
- stack.push(child);
2230
- cur = child;
2231
- type += `-${child.type}`;
2232
- }
2233
- // wsh can be inside sh
2234
- if (cur.type === 'wsh') {
2235
- if (!input.witnessScript) throw new Error('inputType: wsh without witnessScript');
2236
- let child = OutScript.decode(input.witnessScript);
2237
- if (child.type === 'wsh') txType = 'segwit';
2238
- stack.push(child);
2239
- cur = child;
2240
- type += `-${child.type}`;
2241
- }
2242
- const last = stack[stack.length - 1];
2243
- if (last.type === 'sh' || last.type === 'wsh')
2244
- throw new Error('inputType: sh/wsh cannot be terminal type');
2245
- const lastScript = OutScript.encode(last);
2246
- const res = {
2247
- type,
2248
- txType,
2249
- last,
2250
- lastScript,
2251
- defaultSighash,
2252
- sighash: input.sighashType || defaultSighash,
2253
- };
2254
- if (txType === 'legacy' && !this.opts.allowLegacyWitnessUtxo && !input.nonWitnessUtxo) {
2255
- throw new Error(
2256
- `Transaction/sign: legacy input without nonWitnessUtxo, can result in attack that forces paying higher fees. Pass allowLegacyWitnessUtxo=true, if you sure`
2257
- );
2258
- }
2259
- return res;
2260
- }
2261
- }
2262
-
2263
2275
  // Signer can be privateKey OR instance of bip32 HD stuff
2264
2276
  signIdx(privateKey: Signer, idx: number, allowedSighash?: SigHash[], _auxRand?: Bytes): boolean {
2265
2277
  this.checkInputIdx(idx);
2266
2278
  const input = this.inputs[idx];
2267
- const inputType = this.inputType(input);
2279
+ const inputType = getInputType(input, this.opts.allowLegacyWitnessUtxo);
2268
2280
  // Handle BIP32 HDKey
2269
2281
  if (!isBytes(privateKey)) {
2270
2282
  if (!input.bip32Derivation || !input.bip32Derivation.length)
@@ -2307,10 +2319,10 @@ export class Transaction {
2307
2319
 
2308
2320
  // Actual signing
2309
2321
  // Taproot
2310
- const prevOut = this.prevOut(input);
2322
+ const prevOut = getPrevOut(input);
2311
2323
  if (inputType.txType === 'taproot') {
2312
2324
  if (input.tapBip32Derivation) throw new Error('tapBip32Derivation unsupported');
2313
- const prevOuts = this.inputs.map(this.prevOut);
2325
+ const prevOuts = this.inputs.map(getPrevOut);
2314
2326
  const prevOutScript = prevOuts.map((i) => i.script);
2315
2327
  const amount = prevOuts.map((i) => i.amount);
2316
2328
  let signed = false;
@@ -2427,7 +2439,7 @@ export class Transaction {
2427
2439
  this.checkInputIdx(idx);
2428
2440
  if (this.fee < 0n) throw new Error('Outputs spends more than inputs amount');
2429
2441
  const input = this.inputs[idx];
2430
- const inputType = this.inputType(input);
2442
+ const inputType = getInputType(input, this.opts.allowLegacyWitnessUtxo);
2431
2443
  // Taproot finalize
2432
2444
  if (inputType.txType === 'taproot') {
2433
2445
  if (input.tapKeySig) input.finalScriptWitness = [input.tapKeySig];
@@ -2589,7 +2601,7 @@ export class Transaction {
2589
2601
  }
2590
2602
  clone() {
2591
2603
  // deepClone probably faster, but this enforces that encoding is valid
2592
- return Transaction.fromPSBT(this.toPSBT(2), this.opts);
2604
+ return Transaction.fromPSBT(this.toPSBT(this.opts.PSBTVersion), this.opts);
2593
2605
  }
2594
2606
  }
2595
2607
  // User facing API?
@@ -2639,3 +2651,403 @@ export function PSBTCombine(psbts: Bytes[]): Bytes {
2639
2651
  for (let i = 1; i < psbts.length; i++) tx.combine(Transaction.fromPSBT(psbts[i]));
2640
2652
  return tx.toPSBT();
2641
2653
  }
2654
+
2655
+ // UTXO Select
2656
+ type Output = { address: string; amount: bigint } | { script: Uint8Array; amount: bigint };
2657
+
2658
+ function estimateInput(
2659
+ inputType: ReturnType<typeof getInputType>,
2660
+ input: TransactionInput,
2661
+ opts: TxOpts
2662
+ ) {
2663
+ let script: Bytes = P.EMPTY,
2664
+ witness: Bytes[] | undefined;
2665
+
2666
+ // schnorr sig is always 64 bytes. except for cases when sighash is not default!
2667
+ if (inputType.txType === 'taproot') {
2668
+ const SCHNORR_SIG_SIZE = inputType.sighash !== SignatureHash.DEFAULT ? 65 : 64;
2669
+ if (input.tapInternalKey && !P.equalBytes(input.tapInternalKey, TAPROOT_UNSPENDABLE_KEY)) {
2670
+ witness = [new Uint8Array(SCHNORR_SIG_SIZE)];
2671
+ } else if (input.tapLeafScript) {
2672
+ // If user want to select specific leaf (which can signed, it is possible to remove all other leafs manually);
2673
+ // Sort leafs by control block length.
2674
+ const leafs = input.tapLeafScript.sort(
2675
+ (a, b) => TaprootControlBlock.encode(a[0]).length - TaprootControlBlock.encode(b[0]).length
2676
+ );
2677
+ for (const [cb, _script] of leafs) {
2678
+ // Last byte is version
2679
+ const script = _script.slice(0, -1);
2680
+ const outScript = OutScript.decode(script);
2681
+ let signatures: Bytes[] = [];
2682
+ if (outScript.type === 'tr_ms') {
2683
+ const m = outScript.m;
2684
+ for (let i = 0; i < m; i++) signatures.push(new Uint8Array(SCHNORR_SIG_SIZE));
2685
+ const n = outScript.pubkeys.length - m;
2686
+ for (let i = 0; i < n; i++) signatures.push(P.EMPTY);
2687
+ } else if (outScript.type === 'tr_ns') {
2688
+ for (const _pub of outScript.pubkeys) signatures.push(new Uint8Array(SCHNORR_SIG_SIZE));
2689
+ } else throw new Error('Finalize: Unknown tapLeafScript');
2690
+ // Witness is stack, so last element will be used first
2691
+ witness = signatures.reverse().concat([script, TaprootControlBlock.encode(cb)]);
2692
+ break;
2693
+ }
2694
+ } else throw new Error('estimateInput/taproot: unknown input');
2695
+ } else {
2696
+ // It is possible to grind signatures until it has minimal size (but changing fee value +N satoshi),
2697
+ // which will make estimations exact. But will be very hard for multi sig (need to make sure all signatures has small size).
2698
+ const SIG_SIZE = 72; // Maximum size of signatures
2699
+ const PUB_KEY_SIZE = 33;
2700
+ let inputScript = P.EMPTY;
2701
+ let inputWitness: Uint8Array[] = [];
2702
+ if (inputType.last.type === 'ms') {
2703
+ const m = inputType.last.m;
2704
+ const sig: (number | Uint8Array)[] = [0];
2705
+ for (let i = 0; i < m; i++) sig.push(new Uint8Array(SIG_SIZE));
2706
+ inputScript = Script.encode(sig);
2707
+ } else if (inputType.last.type === 'pk') {
2708
+ // 71 sig + 1 sighash
2709
+ inputScript = Script.encode([new Uint8Array(SIG_SIZE)]);
2710
+ } else if (inputType.last.type === 'pkh') {
2711
+ inputScript = Script.encode([new Uint8Array(SIG_SIZE), new Uint8Array(PUB_KEY_SIZE)]);
2712
+ } else if (inputType.last.type === 'wpkh') {
2713
+ inputScript = P.EMPTY;
2714
+ inputWitness = [new Uint8Array(SIG_SIZE), new Uint8Array(PUB_KEY_SIZE)];
2715
+ } else if (inputType.last.type === 'unknown' && !opts.allowUnknownInputs)
2716
+ throw new Error('Unknown inputs not allowed');
2717
+ if (inputType.type.includes('wsh-')) {
2718
+ // P2WSH
2719
+ if (inputScript.length && inputType.lastScript.length) {
2720
+ inputWitness = Script.decode(inputScript).map((i) => {
2721
+ if (i === 0) return P.EMPTY;
2722
+ if (isBytes(i)) return i;
2723
+ throw new Error(`Wrong witness op=${i}`);
2724
+ });
2725
+ }
2726
+ inputWitness = inputWitness.concat(inputType.lastScript);
2727
+ }
2728
+ if (inputType.txType === 'segwit') witness = inputWitness;
2729
+ if (inputType.type.startsWith('sh-wsh-')) {
2730
+ script = Script.encode([Script.encode([0, new Uint8Array(sha256.outputLen)])]);
2731
+ } else if (inputType.type.startsWith('sh-')) {
2732
+ script = Script.encode([...Script.decode(inputScript), inputType.lastScript]);
2733
+ } else if (inputType.type.startsWith('wsh-')) {
2734
+ } else if (inputType.txType !== 'segwit') script = inputScript;
2735
+ }
2736
+ let weight = 160 + 4 * VarBytes.encode(script).length;
2737
+ let hasWitnesses = false;
2738
+ if (witness) {
2739
+ weight += RawWitness.encode(witness).length;
2740
+ hasWitnesses = true;
2741
+ }
2742
+ return { weight, hasWitnesses };
2743
+ }
2744
+
2745
+ // Exported for tests, internal method
2746
+ export const _cmpBig = (a: bigint, b: bigint) => {
2747
+ const n = a - b;
2748
+ if (n < 0n) return -1;
2749
+ else if (n > 0n) return 1;
2750
+ return 0;
2751
+ };
2752
+
2753
+ export type EstimatorOpts = TxOpts & {
2754
+ // NOTE: fees less than 1 satoshi per vbyte is not supported. Please create issue if you have valid use case for that.
2755
+ feePerByte: bigint; // satoshi per vbyte
2756
+ changeAddress: string; // address where change will be sent
2757
+ // Optional
2758
+ alwaysChange?: boolean; // always create change, even if less than dust threshold
2759
+ bip69?: boolean; // https://github.com/bitcoin/bips/blob/master/bip-0069.mediawiki
2760
+ network?: typeof NETWORK;
2761
+ dust?: number; // how much vbytes considered dust?
2762
+ createTx?: boolean; // Create tx inside selection
2763
+ };
2764
+
2765
+ function getScript(o: Output, opts: TxOpts = {}, network = NETWORK) {
2766
+ let script;
2767
+ if ('script' in o && o.script instanceof Uint8Array) {
2768
+ script = o.script;
2769
+ }
2770
+ if ('address' in o) {
2771
+ if (typeof o.address !== 'string')
2772
+ throw new Error(`Estimator: wrong output address=${o.address}`);
2773
+ script = OutScript.encode(Address(network).decode(o.address));
2774
+ }
2775
+ if (!script) throw new Error('Estimator: wrong output script');
2776
+ if (typeof o.amount !== 'bigint') throw new Error(`Estimator: wrong output amount=${o.amount}`);
2777
+ if (script && !opts.allowUnknownOutputs && OutScript.decode(script).type === 'unknown') {
2778
+ throw new Error(
2779
+ 'Estimator: unknown output script type, there is a chance that input is unspendable. Pass allowUnknownOutputs=true, if you sure'
2780
+ );
2781
+ }
2782
+ if (!opts.disableScriptCheck) checkScript(script);
2783
+ return script;
2784
+ }
2785
+
2786
+ // exact is meaningless without additional accum (will often fail if not possible to find right utxo)
2787
+ // -> we support only exact+accum or accum
2788
+ type SortStrategy = 'Newest' | 'Oldest' | 'Smallest' | 'Biggest';
2789
+ type ExactStrategy = `exact${SortStrategy}`;
2790
+ type AccumStrategy = `accum${SortStrategy}`;
2791
+
2792
+ export type SelectionStrategy =
2793
+ | 'all'
2794
+ | 'default'
2795
+ | AccumStrategy
2796
+ | `${ExactStrategy}/${AccumStrategy}`;
2797
+
2798
+ // class, because we need to re-use normalized inputs, instead of parsing each time
2799
+ // internal stuff, exported for tests only
2800
+ export class _Estimator {
2801
+ private baseWeight: number;
2802
+ private changeWeight: number;
2803
+ private amount: bigint;
2804
+ private normalizedInputs: {
2805
+ inputType: ReturnType<typeof getInputType>;
2806
+ normalized: ReturnType<typeof normalizeInput>;
2807
+ amount: bigint;
2808
+ value: bigint;
2809
+ estimate: { weight: number; hasWitnesses: boolean };
2810
+ }[];
2811
+ // https://github.com/bitcoin/bitcoin/blob/f90603ac6d24f5263649675d51233f1fce8b2ecd/src/policy/policy.cpp#L44
2812
+ // 32 + 4 + 1 + 107 + 4
2813
+ // Dust used in accumExact + change address algo
2814
+ // - change address: can be smaller for segwit
2815
+ // - accumExact: ???
2816
+ private dust = 148n; // compat with coinselect
2817
+
2818
+ constructor(
2819
+ private inputs: TransactionInputUpdate[],
2820
+ private outputs: Output[],
2821
+ private opts: EstimatorOpts
2822
+ ) {
2823
+ if (typeof opts.feePerByte !== 'bigint')
2824
+ throw new Error(`Estimator: wrong feePerByte=${opts.feePerByte}`);
2825
+ if (opts.dust) {
2826
+ if (typeof opts.dust !== 'bigint') throw new Error(`Estimator: wrong dust=${opts.dust}`);
2827
+ this.dust = opts.dust;
2828
+ }
2829
+ const network = opts.network || NETWORK;
2830
+ let amount = 0n;
2831
+ // Base weight: tx with outputs, no inputs
2832
+ let baseWeight = 32;
2833
+ for (const o of outputs) {
2834
+ const script = getScript(o, opts, opts.network);
2835
+ baseWeight += 32 + 4 * VarBytes.encode(script).length;
2836
+ amount += o.amount;
2837
+ }
2838
+ if (typeof opts.changeAddress !== 'string')
2839
+ throw new Error(`Estimator: wrong change address=${opts.changeAddress}`);
2840
+ let changeWeight =
2841
+ baseWeight +
2842
+ 32 +
2843
+ 4 * VarBytes.encode(OutScript.encode(Address(network).decode(opts.changeAddress))).length;
2844
+ baseWeight += 4 * CompactSizeLen.encode(outputs.length).length;
2845
+ // If there a lot of outputs change can change fee
2846
+ changeWeight += 4 * CompactSizeLen.encode(outputs.length + 1).length;
2847
+ this.baseWeight = baseWeight;
2848
+ this.changeWeight = changeWeight;
2849
+ this.amount = amount;
2850
+ this.normalizedInputs = this.inputs.map((i) => {
2851
+ const normalized = normalizeInput(i, undefined, undefined, opts.disableScriptCheck);
2852
+ inputBeforeSign(normalized); // check fields
2853
+ const inputType = getInputType(normalized, opts.allowLegacyWitnessUtxo);
2854
+ const prev = getPrevOut(normalized);
2855
+ const estimate = estimateInput(inputType, normalized, this.opts);
2856
+ const value = prev.amount - opts.feePerByte * BigInt(toVsize(estimate.weight)); // value = amount-fee
2857
+ return { inputType, normalized, amount: prev.amount, value, estimate };
2858
+ });
2859
+ }
2860
+ private checkInputIdx(idx: number) {
2861
+ if (!Number.isSafeInteger(idx) || 0 > idx || idx >= this.inputs.length)
2862
+ throw new Error(`Wrong input index=${idx}`);
2863
+ return idx;
2864
+ }
2865
+ private sortIndices(indices: number[]) {
2866
+ return indices.slice().sort((a, b) => {
2867
+ const ai = this.normalizedInputs[this.checkInputIdx(a)];
2868
+ const bi = this.normalizedInputs[this.checkInputIdx(b)];
2869
+ const out = _cmpBytes(ai.normalized.txid!, bi.normalized.txid!);
2870
+ if (out !== 0) return out;
2871
+ return ai.normalized.index! - bi.normalized.index!;
2872
+ });
2873
+ }
2874
+ private sortOutputs(outputs: Output[]) {
2875
+ const scripts = outputs.map((o) => getScript(o, this.opts, this.opts.network));
2876
+ const indices = outputs.map((_, j) => j);
2877
+ return indices.sort((a, b) => {
2878
+ const aa = outputs[a].amount;
2879
+ const ba = outputs[b].amount;
2880
+ const out = _cmpBig(aa, ba);
2881
+ if (out !== 0) return out;
2882
+ return _cmpBytes(scripts[a], scripts[b]);
2883
+ });
2884
+ }
2885
+ private getSatoshi(weigth: number) {
2886
+ return this.opts.feePerByte * BigInt(toVsize(weigth));
2887
+ }
2888
+
2889
+ // Sort by value instead of amount
2890
+ get biggest() {
2891
+ return this.inputs
2892
+ .map((_i, j) => j)
2893
+ .sort((a, b) => _cmpBig(this.normalizedInputs[b].value, this.normalizedInputs[a].value));
2894
+ }
2895
+ get smallest() {
2896
+ return this.biggest.reverse();
2897
+ }
2898
+ // These assume that UTXO array has historical order.
2899
+ // Otherwise, we have no way to know which tx is oldest
2900
+ // Explorers usually give UTXO in this order.
2901
+ get oldest() {
2902
+ return this.inputs.map((_i, j) => j);
2903
+ }
2904
+ get newest() {
2905
+ return this.oldest.reverse();
2906
+ }
2907
+ // exact - like blackjack from coinselect.
2908
+ // exact(biggest) will select one big utxo which is closer to targetValue+dust, if possible.
2909
+ // If not, it will accumulate largest utxo until value is close to targetValue+dust.
2910
+ accumulate(indices: number[], exact = false, skipNegative = true, all = false) {
2911
+ const { feePerByte } = this.opts;
2912
+ // TODO: how to handle change addresses?
2913
+ // - cost of input
2914
+ // - cost of change output (if input requires change)
2915
+ // - cost of output spending
2916
+ // Dust threshold should be significantly bigger, no point in
2917
+ // creating an output, which cannot be spent.
2918
+ // coinselect doesn't consider cost of output address for dust.
2919
+ // Changing that can actually reduce privacy
2920
+ let weight = this.opts.alwaysChange ? this.changeWeight : this.baseWeight;
2921
+ let hasWitnesses = false;
2922
+ let num = 0;
2923
+ let inputsAmount = 0n;
2924
+ const targetAmount = this.amount;
2925
+ const res = [];
2926
+ let fee;
2927
+ for (const idx of indices) {
2928
+ this.checkInputIdx(idx);
2929
+ const { estimate, amount, value } = this.normalizedInputs[idx];
2930
+ let newWeight = weight + estimate.weight;
2931
+ if (!hasWitnesses && estimate.hasWitnesses) newWeight += 2; // enable witness if needed
2932
+ const totalWeight = newWeight + 4 * CompactSizeLen.encode(num).length; // number of outputs can change weight
2933
+ fee = this.getSatoshi(totalWeight);
2934
+ // Best case scenario exact(biggest) -> we find biggest output, less than target+threshold
2935
+ if (exact) {
2936
+ const dust = this.dust * feePerByte;
2937
+ // skip if added value is bigger than dust
2938
+ if (amount + inputsAmount > targetAmount + fee + dust) continue;
2939
+ }
2940
+ // Negative: cost of using input is more than value provided (negative)
2941
+ // By default 'blackjack' mode in coinselect doesn't use that, which means
2942
+ // it will use negative output if sorted by 'smallest'
2943
+ if (skipNegative && value <= 0n) continue;
2944
+ weight = newWeight;
2945
+ if (estimate.hasWitnesses) hasWitnesses = true;
2946
+ num++;
2947
+ inputsAmount += amount;
2948
+ res.push(idx);
2949
+ // inputsAmount is enough to cover cost of tx
2950
+ if (!all && targetAmount + fee < inputsAmount)
2951
+ return { indices: res, fee, weight: totalWeight, total: inputsAmount };
2952
+ }
2953
+ if (all) {
2954
+ const newWeight = weight + 4 * CompactSizeLen.encode(num).length;
2955
+ return { indices: res, fee, weight: newWeight, total: inputsAmount };
2956
+ }
2957
+ return undefined;
2958
+ }
2959
+
2960
+ // Works like coinselect default method
2961
+ default() {
2962
+ const { biggest } = this;
2963
+ const exact = this.accumulate(biggest, true, false);
2964
+ if (exact) return exact;
2965
+ return this.accumulate(biggest);
2966
+ }
2967
+
2968
+ private select(strategy: SelectionStrategy) {
2969
+ if (strategy === 'all') {
2970
+ return this.accumulate(
2971
+ this.inputs.map((_, j) => j),
2972
+ false,
2973
+ true,
2974
+ true
2975
+ );
2976
+ }
2977
+ if (strategy === 'default') return this.default();
2978
+ const data: Record<SortStrategy, () => number[]> = {
2979
+ Oldest: () => this.oldest,
2980
+ Newest: () => this.newest,
2981
+ Smallest: () => this.smallest,
2982
+ Biggest: () => this.biggest,
2983
+ };
2984
+ if (strategy.startsWith('exact')) {
2985
+ const [exactData, left] = strategy.slice(5).split('/') as [SortStrategy, SelectionStrategy];
2986
+ if (!data[exactData]) throw new Error(`Estimator.select: wrong strategy=${strategy}`);
2987
+ strategy = left;
2988
+ const exact = this.accumulate(data[exactData](), true, true);
2989
+ if (exact) return exact;
2990
+ }
2991
+ if (strategy.startsWith('accum')) {
2992
+ const accumData = strategy.slice(5) as SortStrategy;
2993
+ if (!data[accumData]) throw new Error(`Estimator.select: wrong strategy=${strategy}`);
2994
+ return this.accumulate(data[accumData]());
2995
+ }
2996
+ throw new Error(`Estimator.select: wrong strategy=${strategy}`);
2997
+ }
2998
+
2999
+ result(strategy: SelectionStrategy) {
3000
+ const s = this.select(strategy);
3001
+ if (!s) return;
3002
+ const { indices, weight, total } = s;
3003
+ let needChange = this.opts.alwaysChange;
3004
+ const changeWeight = this.opts.alwaysChange
3005
+ ? weight
3006
+ : weight + (this.changeWeight - this.baseWeight);
3007
+
3008
+ const changeFee = this.getSatoshi(changeWeight);
3009
+ let fee = s.fee;
3010
+ const change = total - this.amount - changeFee;
3011
+ if (change > this.dust) needChange = true;
3012
+ let inputs = indices;
3013
+ let outputs = Array.from(this.outputs);
3014
+ if (needChange) {
3015
+ fee = changeFee;
3016
+ // this shouldn't happen!
3017
+ if (change < 0n) throw new Error(`Estimator.result: negative change=${change}`);
3018
+ outputs.push({ address: this.opts.changeAddress, amount: change });
3019
+ }
3020
+ if (this.opts.bip69) {
3021
+ inputs = this.sortIndices(inputs);
3022
+ outputs = this.sortOutputs(outputs).map((i) => outputs[i]);
3023
+ }
3024
+ const res = {
3025
+ inputs: inputs.map((i) => this.inputs[i]),
3026
+ outputs,
3027
+ fee,
3028
+ weight: this.opts.alwaysChange ? s.weight : changeWeight,
3029
+ change: !!needChange,
3030
+ };
3031
+ let tx;
3032
+ if (this.opts.createTx) {
3033
+ const { inputs, outputs } = res;
3034
+ tx = new Transaction(this.opts);
3035
+ for (const i of inputs) tx.addInput(i);
3036
+ for (const o of outputs)
3037
+ tx.addOutput({ ...o, script: getScript(o, this.opts, this.opts.network) });
3038
+ }
3039
+ return { ...res, tx };
3040
+ }
3041
+ }
3042
+
3043
+ export function selectUTXO(
3044
+ inputs: TransactionInputUpdate[],
3045
+ outputs: Output[],
3046
+ strategy: SelectionStrategy,
3047
+ opts: EstimatorOpts
3048
+ ) {
3049
+ // Defaults: do we want bip69 by default?
3050
+ const _opts = { createTx: true, bip69: true, ...opts };
3051
+ const est = new _Estimator(inputs, outputs, _opts);
3052
+ return est.result(strategy);
3053
+ }