@scure/btc-signer 1.1.1 → 1.2.1

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
@@ -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
 
@@ -714,12 +714,26 @@ const PSBTInputCoder = P.validate(PSBTKeyMap(PSBTInput), (i) => {
714
714
  }
715
715
  }
716
716
  // Validate txid for nonWitnessUtxo is correct
717
- if (i.nonWitnessUtxo && i.index && i.txid) {
717
+ if (i.nonWitnessUtxo && i.index !== undefined && i.txid) {
718
718
  const outputs = i.nonWitnessUtxo.outputs;
719
719
  if (outputs.length - 1 < i.index) throw new Error('nonWitnessUtxo: incorect output index');
720
- const tx = Transaction.fromRaw(RawTx.encode(i.nonWitnessUtxo));
720
+ // At this point, we are using previous tx output to create new input.
721
+ // Script safety checks are unnecessary:
722
+ // - User has no control over previous tx. If somebody send money in same tx
723
+ // as unspendable output, we still want user able to spend money
724
+ // - We still want some checks to notify user about possible errors early
725
+ // in case user wants to use wrong input by mistake
726
+ // - Worst case: tx will be rejected by nodes. Still better than disallowing user
727
+ // to spend real input, no matter how broken it looks
728
+ const tx = Transaction.fromRaw(RawTx.encode(i.nonWitnessUtxo), {
729
+ allowUnknownOutputs: true,
730
+ disableScriptCheck: true,
731
+ allowUnknownInputs: true,
732
+ });
721
733
  const txid = hex.encode(i.txid);
722
- if (tx.id !== txid) throw new Error(`nonWitnessUtxo: wrong txid, exp=${txid} got=${tx.id}`);
734
+ // PSBTv2 vectors have non-final tx in inputs
735
+ if (tx.isFinal && tx.id !== txid)
736
+ throw new Error(`nonWitnessUtxo: wrong txid, exp=${txid} got=${tx.id}`);
723
737
  }
724
738
  return i;
725
739
  });
@@ -1705,6 +1719,109 @@ function validateOpts(opts: TxOpts) {
1705
1719
  return Object.freeze(_opts);
1706
1720
  }
1707
1721
 
1722
+ // Normalizes input
1723
+ function getPrevOut(input: TransactionInput): P.UnwrapCoder<typeof RawOutput> {
1724
+ if (input.nonWitnessUtxo) {
1725
+ if (input.index === undefined) throw new Error('Unknown input index');
1726
+ return input.nonWitnessUtxo.outputs[input.index];
1727
+ } else if (input.witnessUtxo) return input.witnessUtxo;
1728
+ else throw new Error('Cannot find previous output info');
1729
+ }
1730
+
1731
+ function normalizeInput(
1732
+ i: TransactionInputUpdate,
1733
+ cur?: TransactionInput,
1734
+ allowedFields?: (keyof TransactionInput)[],
1735
+ disableScriptCheck = false
1736
+ ): TransactionInput {
1737
+ let { nonWitnessUtxo, txid } = i;
1738
+ // String support for common fields. We usually prefer Uint8Array to avoid errors
1739
+ // like hex looking string accidentally passed, however, in case of nonWitnessUtxo
1740
+ // it is better to expect string, since constructing this complex object will be
1741
+ // difficult for user
1742
+ if (typeof nonWitnessUtxo === 'string') nonWitnessUtxo = hex.decode(nonWitnessUtxo);
1743
+ if (isBytes(nonWitnessUtxo)) nonWitnessUtxo = RawTx.decode(nonWitnessUtxo);
1744
+ if (!('nonWitnessUtxo' in i) && nonWitnessUtxo === undefined)
1745
+ nonWitnessUtxo = cur?.nonWitnessUtxo;
1746
+ if (typeof txid === 'string') txid = hex.decode(txid);
1747
+ // TODO: if we have nonWitnessUtxo, we can extract txId from here
1748
+ if (txid === undefined) txid = cur?.txid;
1749
+ let res: PSBTKeyMapKeys<typeof PSBTInput> = { ...cur, ...i, nonWitnessUtxo, txid };
1750
+ if (!('nonWitnessUtxo' in i) && res.nonWitnessUtxo === undefined) delete res.nonWitnessUtxo;
1751
+ if (res.sequence === undefined) res.sequence = DEFAULT_SEQUENCE;
1752
+ if (res.tapMerkleRoot === null) delete res.tapMerkleRoot;
1753
+ res = mergeKeyMap(PSBTInput, res, cur, allowedFields);
1754
+ PSBTInputCoder.encode(res); // Validates that everything is correct at this point
1755
+
1756
+ let prevOut;
1757
+ if (res.nonWitnessUtxo && res.index !== undefined)
1758
+ prevOut = res.nonWitnessUtxo.outputs[res.index];
1759
+ else if (res.witnessUtxo) prevOut = res.witnessUtxo;
1760
+ if (prevOut && !disableScriptCheck)
1761
+ checkScript(prevOut && prevOut.script, res.redeemScript, res.witnessScript);
1762
+ return res;
1763
+ }
1764
+
1765
+ function getInputType(input: TransactionInput, allowLegacyWitnessUtxo = false) {
1766
+ let txType = 'legacy';
1767
+ let defaultSighash = SignatureHash.ALL;
1768
+ const prevOut = getPrevOut(input);
1769
+ const first = OutScript.decode(prevOut.script);
1770
+ let type = first.type;
1771
+ let cur = first;
1772
+ const stack = [first];
1773
+ if (first.type === 'tr') {
1774
+ defaultSighash = SignatureHash.DEFAULT;
1775
+ return {
1776
+ txType: 'taproot',
1777
+ type: 'tr',
1778
+ last: first,
1779
+ lastScript: prevOut.script,
1780
+ defaultSighash,
1781
+ sighash: input.sighashType || defaultSighash,
1782
+ };
1783
+ } else {
1784
+ if (first.type === 'wpkh' || first.type === 'wsh') txType = 'segwit';
1785
+ if (first.type === 'sh') {
1786
+ if (!input.redeemScript) throw new Error('inputType: sh without redeemScript');
1787
+ let child = OutScript.decode(input.redeemScript);
1788
+ if (child.type === 'wpkh' || child.type === 'wsh') txType = 'segwit';
1789
+ stack.push(child);
1790
+ cur = child;
1791
+ type += `-${child.type}`;
1792
+ }
1793
+ // wsh can be inside sh
1794
+ if (cur.type === 'wsh') {
1795
+ if (!input.witnessScript) throw new Error('inputType: wsh without witnessScript');
1796
+ let child = OutScript.decode(input.witnessScript);
1797
+ if (child.type === 'wsh') txType = 'segwit';
1798
+ stack.push(child);
1799
+ cur = child;
1800
+ type += `-${child.type}`;
1801
+ }
1802
+ const last = stack[stack.length - 1];
1803
+ if (last.type === 'sh' || last.type === 'wsh')
1804
+ throw new Error('inputType: sh/wsh cannot be terminal type');
1805
+ const lastScript = OutScript.encode(last);
1806
+ const res = {
1807
+ type,
1808
+ txType,
1809
+ last,
1810
+ lastScript,
1811
+ defaultSighash,
1812
+ sighash: input.sighashType || defaultSighash,
1813
+ };
1814
+ if (txType === 'legacy' && !allowLegacyWitnessUtxo && !input.nonWitnessUtxo) {
1815
+ throw new Error(
1816
+ `Transaction/sign: legacy input without nonWitnessUtxo, can result in attack that forces paying higher fees. Pass allowLegacyWitnessUtxo=true, if you sure`
1817
+ );
1818
+ }
1819
+ return res;
1820
+ }
1821
+ }
1822
+
1823
+ const toVsize = (weight: number) => Math.ceil(weight / 4);
1824
+
1708
1825
  export class Transaction {
1709
1826
  private global: PSBTKeyMapKeys<typeof PSBTGlobal> = {};
1710
1827
  private inputs: TransactionInput[] = []; // use getInput()
@@ -1845,7 +1962,7 @@ export class Transaction {
1845
1962
  // We will lose some vectors -> smaller test coverage of preimages (very important!)
1846
1963
  private inputSighash(idx: number) {
1847
1964
  this.checkInputIdx(idx);
1848
- const sighash = this.inputType(this.inputs[idx]).sighash;
1965
+ const sighash = getInputType(this.inputs[idx], this.opts.allowLegacyWitnessUtxo).sighash;
1849
1966
  // ALL or DEFAULT -- everything signed
1850
1967
  // NONE -- all inputs + no outputs
1851
1968
  // SINGLE -- all inputs + output with same index
@@ -1898,23 +2015,23 @@ export class Transaction {
1898
2015
  // https://en.bitcoin.it/wiki/Weight_units
1899
2016
  get weight(): number {
1900
2017
  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
2018
  let out = 32;
2019
+ // Outputs
1903
2020
  const outputs = this.outputs.map(outputBeforeSign);
2021
+ out += 4 * CompactSizeLen.encode(this.outputs.length).length;
2022
+ for (const o of outputs) out += 32 + 4 * VarBytes.encode(o.script).length;
2023
+ // Inputs
1904
2024
  if (this.hasWitnesses) out += 2;
1905
2025
  out += 4 * CompactSizeLen.encode(this.inputs.length).length;
1906
- out += 4 * CompactSizeLen.encode(this.outputs.length).length;
1907
- for (const i of this.inputs)
2026
+ for (const i of this.inputs) {
1908
2027
  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;
2028
+ if (this.hasWitnesses && i.finalScriptWitness)
2029
+ out += RawWitness.encode(i.finalScriptWitness).length;
1913
2030
  }
1914
2031
  return out;
1915
2032
  }
1916
2033
  get vsize(): number {
1917
- return Math.ceil(this.weight / 4);
2034
+ return toVsize(this.weight);
1918
2035
  }
1919
2036
  toBytes(withScriptSig = false, withWitness = false) {
1920
2037
  return RawTx.encode({
@@ -1957,40 +2074,10 @@ export class Transaction {
1957
2074
  return this.inputs.length;
1958
2075
  }
1959
2076
  // 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' in i) && nonWitnessUtxo === undefined)
1971
- nonWitnessUtxo = cur?.nonWitnessUtxo;
1972
- if (typeof txid === 'string') txid = hex.decode(txid);
1973
- if (txid === undefined) txid = cur?.txid;
1974
- let res: PSBTKeyMapKeys<typeof PSBTInput> = { ...cur, ...i, nonWitnessUtxo, txid };
1975
- if (!('nonWitnessUtxo' in i) && res.nonWitnessUtxo === undefined) delete res.nonWitnessUtxo;
1976
- if (res.sequence === undefined) res.sequence = DEFAULT_SEQUENCE;
1977
- if (res.tapMerkleRoot === null) delete res.tapMerkleRoot;
1978
- res = mergeKeyMap(PSBTInput, res, cur, allowedFields);
1979
- PSBTInputCoder.encode(res); // Validates that everything is correct at this point
1980
-
1981
- let prevOut;
1982
- if (res.nonWitnessUtxo && res.index !== undefined)
1983
- prevOut = res.nonWitnessUtxo.outputs[res.index];
1984
- else if (res.witnessUtxo) prevOut = res.witnessUtxo;
1985
- if (prevOut && !this.opts.disableScriptCheck)
1986
- checkScript(prevOut && prevOut.script, res.redeemScript, res.witnessScript);
1987
-
1988
- return res;
1989
- }
1990
2077
  addInput(input: TransactionInputUpdate, _ignoreSignStatus = false): number {
1991
2078
  if (!_ignoreSignStatus && !this.signStatus().addInput)
1992
2079
  throw new Error('Tx has signed inputs, cannot add new one');
1993
- this.inputs.push(this.normalizeInput(input));
2080
+ this.inputs.push(normalizeInput(input, undefined, undefined, this.opts.disableScriptCheck));
1994
2081
  return this.inputs.length - 1;
1995
2082
  }
1996
2083
  updateInput(idx: number, input: TransactionInputUpdate, _ignoreSignStatus = false) {
@@ -2000,7 +2087,12 @@ export class Transaction {
2000
2087
  const status = this.signStatus();
2001
2088
  if (!status.addInput || status.inputs.includes(idx)) allowedFields = PSBTInputUnsignedKeys;
2002
2089
  }
2003
- this.inputs[idx] = this.normalizeInput(input, this.inputs[idx], allowedFields);
2090
+ this.inputs[idx] = normalizeInput(
2091
+ input,
2092
+ this.inputs[idx],
2093
+ allowedFields,
2094
+ this.opts.disableScriptCheck
2095
+ );
2004
2096
  }
2005
2097
  // Output stuff
2006
2098
  private checkOutputIdx(idx: number) {
@@ -2034,7 +2126,7 @@ export class Transaction {
2034
2126
  OutScript.decode(res.script).type === 'unknown'
2035
2127
  ) {
2036
2128
  throw new Error(
2037
- 'Transaction/output: unknown output script type, there is a chance that input is unspendable. Pass allowUnknownScript=true, if you sure'
2129
+ 'Transaction/output: unknown output script type, there is a chance that input is unspendable. Pass allowUnknownOutputs=true, if you sure'
2038
2130
  );
2039
2131
  }
2040
2132
  if (!this.opts.disableScriptCheck) checkScript(res.script, res.redeemScript, res.witnessScript);
@@ -2062,7 +2154,7 @@ export class Transaction {
2062
2154
  get fee(): bigint {
2063
2155
  let res = 0n;
2064
2156
  for (const i of this.inputs) {
2065
- const prevOut = this.prevOut(i);
2157
+ const prevOut = getPrevOut(i);
2066
2158
  if (!prevOut) throw new Error('Empty input amount');
2067
2159
  res += prevOut.amount;
2068
2160
  }
@@ -2110,7 +2202,7 @@ export class Transaction {
2110
2202
  });
2111
2203
  return sha256x2(tmpTx, P.I32LE.encode(hashType));
2112
2204
  }
2113
- private preimageWitnessV0(idx: number, prevOutScript: Bytes, hashType: number, amount: bigint) {
2205
+ preimageWitnessV0(idx: number, prevOutScript: Bytes, hashType: number, amount: bigint) {
2114
2206
  const { isAny, isNone, isSingle } = unpackSighash(hashType);
2115
2207
  let inputHash = EMPTY32;
2116
2208
  let sequenceHash = EMPTY32;
@@ -2139,7 +2231,7 @@ export class Transaction {
2139
2231
  P.U32LE.encode(hashType)
2140
2232
  );
2141
2233
  }
2142
- private preimageWitnessV1(
2234
+ preimageWitnessV1(
2143
2235
  idx: number,
2144
2236
  prevOutScript: Bytes[],
2145
2237
  hashType: number,
@@ -2194,78 +2286,11 @@ export class Transaction {
2194
2286
  out.push(tapLeafHash(leafScript, leafVer), P.U8.encode(0), P.I32LE.encode(codeSeparator));
2195
2287
  return schnorr.utils.taggedHash('TapSighash', ...out);
2196
2288
  }
2197
- // Utils for sign/finalize
2198
- // Used pretty often, should be fast
2199
- private prevOut(input: TransactionInput): P.UnwrapCoder<typeof RawOutput> {
2200
- if (input.nonWitnessUtxo) {
2201
- if (input.index === undefined) throw new Error('Unknown input index');
2202
- return input.nonWitnessUtxo.outputs[input.index];
2203
- } else if (input.witnessUtxo) return input.witnessUtxo;
2204
- else throw new Error('Cannot find previous output info');
2205
- }
2206
- private inputType(input: TransactionInput) {
2207
- let txType = 'legacy';
2208
- let defaultSighash = SignatureHash.ALL;
2209
- const prevOut = this.prevOut(input);
2210
- const first = OutScript.decode(prevOut.script);
2211
- let type = first.type;
2212
- let cur = first;
2213
- const stack = [first];
2214
- if (first.type === 'tr') {
2215
- defaultSighash = SignatureHash.DEFAULT;
2216
- return {
2217
- txType: 'taproot',
2218
- type: 'tr',
2219
- last: first,
2220
- lastScript: prevOut.script,
2221
- defaultSighash,
2222
- sighash: input.sighashType || defaultSighash,
2223
- };
2224
- } else {
2225
- if (first.type === 'wpkh' || first.type === 'wsh') txType = 'segwit';
2226
- if (first.type === 'sh') {
2227
- if (!input.redeemScript) throw new Error('inputType: sh without redeemScript');
2228
- let child = OutScript.decode(input.redeemScript);
2229
- if (child.type === 'wpkh' || child.type === 'wsh') txType = 'segwit';
2230
- stack.push(child);
2231
- cur = child;
2232
- type += `-${child.type}`;
2233
- }
2234
- // wsh can be inside sh
2235
- if (cur.type === 'wsh') {
2236
- if (!input.witnessScript) throw new Error('inputType: wsh without witnessScript');
2237
- let child = OutScript.decode(input.witnessScript);
2238
- if (child.type === 'wsh') txType = 'segwit';
2239
- stack.push(child);
2240
- cur = child;
2241
- type += `-${child.type}`;
2242
- }
2243
- const last = stack[stack.length - 1];
2244
- if (last.type === 'sh' || last.type === 'wsh')
2245
- throw new Error('inputType: sh/wsh cannot be terminal type');
2246
- const lastScript = OutScript.encode(last);
2247
- const res = {
2248
- type,
2249
- txType,
2250
- last,
2251
- lastScript,
2252
- defaultSighash,
2253
- sighash: input.sighashType || defaultSighash,
2254
- };
2255
- if (txType === 'legacy' && !this.opts.allowLegacyWitnessUtxo && !input.nonWitnessUtxo) {
2256
- throw new Error(
2257
- `Transaction/sign: legacy input without nonWitnessUtxo, can result in attack that forces paying higher fees. Pass allowLegacyWitnessUtxo=true, if you sure`
2258
- );
2259
- }
2260
- return res;
2261
- }
2262
- }
2263
-
2264
2289
  // Signer can be privateKey OR instance of bip32 HD stuff
2265
2290
  signIdx(privateKey: Signer, idx: number, allowedSighash?: SigHash[], _auxRand?: Bytes): boolean {
2266
2291
  this.checkInputIdx(idx);
2267
2292
  const input = this.inputs[idx];
2268
- const inputType = this.inputType(input);
2293
+ const inputType = getInputType(input, this.opts.allowLegacyWitnessUtxo);
2269
2294
  // Handle BIP32 HDKey
2270
2295
  if (!isBytes(privateKey)) {
2271
2296
  if (!input.bip32Derivation || !input.bip32Derivation.length)
@@ -2308,10 +2333,10 @@ export class Transaction {
2308
2333
 
2309
2334
  // Actual signing
2310
2335
  // Taproot
2311
- const prevOut = this.prevOut(input);
2336
+ const prevOut = getPrevOut(input);
2312
2337
  if (inputType.txType === 'taproot') {
2313
2338
  if (input.tapBip32Derivation) throw new Error('tapBip32Derivation unsupported');
2314
- const prevOuts = this.inputs.map(this.prevOut);
2339
+ const prevOuts = this.inputs.map(getPrevOut);
2315
2340
  const prevOutScript = prevOuts.map((i) => i.script);
2316
2341
  const amount = prevOuts.map((i) => i.amount);
2317
2342
  let signed = false;
@@ -2428,7 +2453,7 @@ export class Transaction {
2428
2453
  this.checkInputIdx(idx);
2429
2454
  if (this.fee < 0n) throw new Error('Outputs spends more than inputs amount');
2430
2455
  const input = this.inputs[idx];
2431
- const inputType = this.inputType(input);
2456
+ const inputType = getInputType(input, this.opts.allowLegacyWitnessUtxo);
2432
2457
  // Taproot finalize
2433
2458
  if (inputType.txType === 'taproot') {
2434
2459
  if (input.tapKeySig) input.finalScriptWitness = [input.tapKeySig];
@@ -2640,3 +2665,403 @@ export function PSBTCombine(psbts: Bytes[]): Bytes {
2640
2665
  for (let i = 1; i < psbts.length; i++) tx.combine(Transaction.fromPSBT(psbts[i]));
2641
2666
  return tx.toPSBT();
2642
2667
  }
2668
+
2669
+ // UTXO Select
2670
+ type Output = { address: string; amount: bigint } | { script: Uint8Array; amount: bigint };
2671
+
2672
+ function estimateInput(
2673
+ inputType: ReturnType<typeof getInputType>,
2674
+ input: TransactionInput,
2675
+ opts: TxOpts
2676
+ ) {
2677
+ let script: Bytes = P.EMPTY,
2678
+ witness: Bytes[] | undefined;
2679
+
2680
+ // schnorr sig is always 64 bytes. except for cases when sighash is not default!
2681
+ if (inputType.txType === 'taproot') {
2682
+ const SCHNORR_SIG_SIZE = inputType.sighash !== SignatureHash.DEFAULT ? 65 : 64;
2683
+ if (input.tapInternalKey && !P.equalBytes(input.tapInternalKey, TAPROOT_UNSPENDABLE_KEY)) {
2684
+ witness = [new Uint8Array(SCHNORR_SIG_SIZE)];
2685
+ } else if (input.tapLeafScript) {
2686
+ // If user want to select specific leaf (which can signed, it is possible to remove all other leafs manually);
2687
+ // Sort leafs by control block length.
2688
+ const leafs = input.tapLeafScript.sort(
2689
+ (a, b) => TaprootControlBlock.encode(a[0]).length - TaprootControlBlock.encode(b[0]).length
2690
+ );
2691
+ for (const [cb, _script] of leafs) {
2692
+ // Last byte is version
2693
+ const script = _script.slice(0, -1);
2694
+ const outScript = OutScript.decode(script);
2695
+ let signatures: Bytes[] = [];
2696
+ if (outScript.type === 'tr_ms') {
2697
+ const m = outScript.m;
2698
+ for (let i = 0; i < m; i++) signatures.push(new Uint8Array(SCHNORR_SIG_SIZE));
2699
+ const n = outScript.pubkeys.length - m;
2700
+ for (let i = 0; i < n; i++) signatures.push(P.EMPTY);
2701
+ } else if (outScript.type === 'tr_ns') {
2702
+ for (const _pub of outScript.pubkeys) signatures.push(new Uint8Array(SCHNORR_SIG_SIZE));
2703
+ } else throw new Error('Finalize: Unknown tapLeafScript');
2704
+ // Witness is stack, so last element will be used first
2705
+ witness = signatures.reverse().concat([script, TaprootControlBlock.encode(cb)]);
2706
+ break;
2707
+ }
2708
+ } else throw new Error('estimateInput/taproot: unknown input');
2709
+ } else {
2710
+ // It is possible to grind signatures until it has minimal size (but changing fee value +N satoshi),
2711
+ // which will make estimations exact. But will be very hard for multi sig (need to make sure all signatures has small size).
2712
+ const SIG_SIZE = 72; // Maximum size of signatures
2713
+ const PUB_KEY_SIZE = 33;
2714
+ let inputScript = P.EMPTY;
2715
+ let inputWitness: Uint8Array[] = [];
2716
+ if (inputType.last.type === 'ms') {
2717
+ const m = inputType.last.m;
2718
+ const sig: (number | Uint8Array)[] = [0];
2719
+ for (let i = 0; i < m; i++) sig.push(new Uint8Array(SIG_SIZE));
2720
+ inputScript = Script.encode(sig);
2721
+ } else if (inputType.last.type === 'pk') {
2722
+ // 71 sig + 1 sighash
2723
+ inputScript = Script.encode([new Uint8Array(SIG_SIZE)]);
2724
+ } else if (inputType.last.type === 'pkh') {
2725
+ inputScript = Script.encode([new Uint8Array(SIG_SIZE), new Uint8Array(PUB_KEY_SIZE)]);
2726
+ } else if (inputType.last.type === 'wpkh') {
2727
+ inputScript = P.EMPTY;
2728
+ inputWitness = [new Uint8Array(SIG_SIZE), new Uint8Array(PUB_KEY_SIZE)];
2729
+ } else if (inputType.last.type === 'unknown' && !opts.allowUnknownInputs)
2730
+ throw new Error('Unknown inputs not allowed');
2731
+ if (inputType.type.includes('wsh-')) {
2732
+ // P2WSH
2733
+ if (inputScript.length && inputType.lastScript.length) {
2734
+ inputWitness = Script.decode(inputScript).map((i) => {
2735
+ if (i === 0) return P.EMPTY;
2736
+ if (isBytes(i)) return i;
2737
+ throw new Error(`Wrong witness op=${i}`);
2738
+ });
2739
+ }
2740
+ inputWitness = inputWitness.concat(inputType.lastScript);
2741
+ }
2742
+ if (inputType.txType === 'segwit') witness = inputWitness;
2743
+ if (inputType.type.startsWith('sh-wsh-')) {
2744
+ script = Script.encode([Script.encode([0, new Uint8Array(sha256.outputLen)])]);
2745
+ } else if (inputType.type.startsWith('sh-')) {
2746
+ script = Script.encode([...Script.decode(inputScript), inputType.lastScript]);
2747
+ } else if (inputType.type.startsWith('wsh-')) {
2748
+ } else if (inputType.txType !== 'segwit') script = inputScript;
2749
+ }
2750
+ let weight = 160 + 4 * VarBytes.encode(script).length;
2751
+ let hasWitnesses = false;
2752
+ if (witness) {
2753
+ weight += RawWitness.encode(witness).length;
2754
+ hasWitnesses = true;
2755
+ }
2756
+ return { weight, hasWitnesses };
2757
+ }
2758
+
2759
+ // Exported for tests, internal method
2760
+ export const _cmpBig = (a: bigint, b: bigint) => {
2761
+ const n = a - b;
2762
+ if (n < 0n) return -1;
2763
+ else if (n > 0n) return 1;
2764
+ return 0;
2765
+ };
2766
+
2767
+ export type EstimatorOpts = TxOpts & {
2768
+ // NOTE: fees less than 1 satoshi per vbyte is not supported. Please create issue if you have valid use case for that.
2769
+ feePerByte: bigint; // satoshi per vbyte
2770
+ changeAddress: string; // address where change will be sent
2771
+ // Optional
2772
+ alwaysChange?: boolean; // always create change, even if less than dust threshold
2773
+ bip69?: boolean; // https://github.com/bitcoin/bips/blob/master/bip-0069.mediawiki
2774
+ network?: typeof NETWORK;
2775
+ dust?: number; // how much vbytes considered dust?
2776
+ createTx?: boolean; // Create tx inside selection
2777
+ };
2778
+
2779
+ function getScript(o: Output, opts: TxOpts = {}, network = NETWORK) {
2780
+ let script;
2781
+ if ('script' in o && o.script instanceof Uint8Array) {
2782
+ script = o.script;
2783
+ }
2784
+ if ('address' in o) {
2785
+ if (typeof o.address !== 'string')
2786
+ throw new Error(`Estimator: wrong output address=${o.address}`);
2787
+ script = OutScript.encode(Address(network).decode(o.address));
2788
+ }
2789
+ if (!script) throw new Error('Estimator: wrong output script');
2790
+ if (typeof o.amount !== 'bigint') throw new Error(`Estimator: wrong output amount=${o.amount}`);
2791
+ if (script && !opts.allowUnknownOutputs && OutScript.decode(script).type === 'unknown') {
2792
+ throw new Error(
2793
+ 'Estimator: unknown output script type, there is a chance that input is unspendable. Pass allowUnknownOutputs=true, if you sure'
2794
+ );
2795
+ }
2796
+ if (!opts.disableScriptCheck) checkScript(script);
2797
+ return script;
2798
+ }
2799
+
2800
+ // exact is meaningless without additional accum (will often fail if not possible to find right utxo)
2801
+ // -> we support only exact+accum or accum
2802
+ type SortStrategy = 'Newest' | 'Oldest' | 'Smallest' | 'Biggest';
2803
+ type ExactStrategy = `exact${SortStrategy}`;
2804
+ type AccumStrategy = `accum${SortStrategy}`;
2805
+
2806
+ export type SelectionStrategy =
2807
+ | 'all'
2808
+ | 'default'
2809
+ | AccumStrategy
2810
+ | `${ExactStrategy}/${AccumStrategy}`;
2811
+
2812
+ // class, because we need to re-use normalized inputs, instead of parsing each time
2813
+ // internal stuff, exported for tests only
2814
+ export class _Estimator {
2815
+ private baseWeight: number;
2816
+ private changeWeight: number;
2817
+ private amount: bigint;
2818
+ private normalizedInputs: {
2819
+ inputType: ReturnType<typeof getInputType>;
2820
+ normalized: ReturnType<typeof normalizeInput>;
2821
+ amount: bigint;
2822
+ value: bigint;
2823
+ estimate: { weight: number; hasWitnesses: boolean };
2824
+ }[];
2825
+ // https://github.com/bitcoin/bitcoin/blob/f90603ac6d24f5263649675d51233f1fce8b2ecd/src/policy/policy.cpp#L44
2826
+ // 32 + 4 + 1 + 107 + 4
2827
+ // Dust used in accumExact + change address algo
2828
+ // - change address: can be smaller for segwit
2829
+ // - accumExact: ???
2830
+ private dust = 148n; // compat with coinselect
2831
+
2832
+ constructor(
2833
+ private inputs: TransactionInputUpdate[],
2834
+ private outputs: Output[],
2835
+ private opts: EstimatorOpts
2836
+ ) {
2837
+ if (typeof opts.feePerByte !== 'bigint')
2838
+ throw new Error(`Estimator: wrong feePerByte=${opts.feePerByte}`);
2839
+ if (opts.dust) {
2840
+ if (typeof opts.dust !== 'bigint') throw new Error(`Estimator: wrong dust=${opts.dust}`);
2841
+ this.dust = opts.dust;
2842
+ }
2843
+ const network = opts.network || NETWORK;
2844
+ let amount = 0n;
2845
+ // Base weight: tx with outputs, no inputs
2846
+ let baseWeight = 32;
2847
+ for (const o of outputs) {
2848
+ const script = getScript(o, opts, opts.network);
2849
+ baseWeight += 32 + 4 * VarBytes.encode(script).length;
2850
+ amount += o.amount;
2851
+ }
2852
+ if (typeof opts.changeAddress !== 'string')
2853
+ throw new Error(`Estimator: wrong change address=${opts.changeAddress}`);
2854
+ let changeWeight =
2855
+ baseWeight +
2856
+ 32 +
2857
+ 4 * VarBytes.encode(OutScript.encode(Address(network).decode(opts.changeAddress))).length;
2858
+ baseWeight += 4 * CompactSizeLen.encode(outputs.length).length;
2859
+ // If there a lot of outputs change can change fee
2860
+ changeWeight += 4 * CompactSizeLen.encode(outputs.length + 1).length;
2861
+ this.baseWeight = baseWeight;
2862
+ this.changeWeight = changeWeight;
2863
+ this.amount = amount;
2864
+ this.normalizedInputs = this.inputs.map((i) => {
2865
+ const normalized = normalizeInput(i, undefined, undefined, opts.disableScriptCheck);
2866
+ inputBeforeSign(normalized); // check fields
2867
+ const inputType = getInputType(normalized, opts.allowLegacyWitnessUtxo);
2868
+ const prev = getPrevOut(normalized);
2869
+ const estimate = estimateInput(inputType, normalized, this.opts);
2870
+ const value = prev.amount - opts.feePerByte * BigInt(toVsize(estimate.weight)); // value = amount-fee
2871
+ return { inputType, normalized, amount: prev.amount, value, estimate };
2872
+ });
2873
+ }
2874
+ private checkInputIdx(idx: number) {
2875
+ if (!Number.isSafeInteger(idx) || 0 > idx || idx >= this.inputs.length)
2876
+ throw new Error(`Wrong input index=${idx}`);
2877
+ return idx;
2878
+ }
2879
+ private sortIndices(indices: number[]) {
2880
+ return indices.slice().sort((a, b) => {
2881
+ const ai = this.normalizedInputs[this.checkInputIdx(a)];
2882
+ const bi = this.normalizedInputs[this.checkInputIdx(b)];
2883
+ const out = _cmpBytes(ai.normalized.txid!, bi.normalized.txid!);
2884
+ if (out !== 0) return out;
2885
+ return ai.normalized.index! - bi.normalized.index!;
2886
+ });
2887
+ }
2888
+ private sortOutputs(outputs: Output[]) {
2889
+ const scripts = outputs.map((o) => getScript(o, this.opts, this.opts.network));
2890
+ const indices = outputs.map((_, j) => j);
2891
+ return indices.sort((a, b) => {
2892
+ const aa = outputs[a].amount;
2893
+ const ba = outputs[b].amount;
2894
+ const out = _cmpBig(aa, ba);
2895
+ if (out !== 0) return out;
2896
+ return _cmpBytes(scripts[a], scripts[b]);
2897
+ });
2898
+ }
2899
+ private getSatoshi(weigth: number) {
2900
+ return this.opts.feePerByte * BigInt(toVsize(weigth));
2901
+ }
2902
+
2903
+ // Sort by value instead of amount
2904
+ get biggest() {
2905
+ return this.inputs
2906
+ .map((_i, j) => j)
2907
+ .sort((a, b) => _cmpBig(this.normalizedInputs[b].value, this.normalizedInputs[a].value));
2908
+ }
2909
+ get smallest() {
2910
+ return this.biggest.reverse();
2911
+ }
2912
+ // These assume that UTXO array has historical order.
2913
+ // Otherwise, we have no way to know which tx is oldest
2914
+ // Explorers usually give UTXO in this order.
2915
+ get oldest() {
2916
+ return this.inputs.map((_i, j) => j);
2917
+ }
2918
+ get newest() {
2919
+ return this.oldest.reverse();
2920
+ }
2921
+ // exact - like blackjack from coinselect.
2922
+ // exact(biggest) will select one big utxo which is closer to targetValue+dust, if possible.
2923
+ // If not, it will accumulate largest utxo until value is close to targetValue+dust.
2924
+ accumulate(indices: number[], exact = false, skipNegative = true, all = false) {
2925
+ const { feePerByte } = this.opts;
2926
+ // TODO: how to handle change addresses?
2927
+ // - cost of input
2928
+ // - cost of change output (if input requires change)
2929
+ // - cost of output spending
2930
+ // Dust threshold should be significantly bigger, no point in
2931
+ // creating an output, which cannot be spent.
2932
+ // coinselect doesn't consider cost of output address for dust.
2933
+ // Changing that can actually reduce privacy
2934
+ let weight = this.opts.alwaysChange ? this.changeWeight : this.baseWeight;
2935
+ let hasWitnesses = false;
2936
+ let num = 0;
2937
+ let inputsAmount = 0n;
2938
+ const targetAmount = this.amount;
2939
+ const res = [];
2940
+ let fee;
2941
+ for (const idx of indices) {
2942
+ this.checkInputIdx(idx);
2943
+ const { estimate, amount, value } = this.normalizedInputs[idx];
2944
+ let newWeight = weight + estimate.weight;
2945
+ if (!hasWitnesses && estimate.hasWitnesses) newWeight += 2; // enable witness if needed
2946
+ const totalWeight = newWeight + 4 * CompactSizeLen.encode(num).length; // number of outputs can change weight
2947
+ fee = this.getSatoshi(totalWeight);
2948
+ // Best case scenario exact(biggest) -> we find biggest output, less than target+threshold
2949
+ if (exact) {
2950
+ const dust = this.dust * feePerByte;
2951
+ // skip if added value is bigger than dust
2952
+ if (amount + inputsAmount > targetAmount + fee + dust) continue;
2953
+ }
2954
+ // Negative: cost of using input is more than value provided (negative)
2955
+ // By default 'blackjack' mode in coinselect doesn't use that, which means
2956
+ // it will use negative output if sorted by 'smallest'
2957
+ if (skipNegative && value <= 0n) continue;
2958
+ weight = newWeight;
2959
+ if (estimate.hasWitnesses) hasWitnesses = true;
2960
+ num++;
2961
+ inputsAmount += amount;
2962
+ res.push(idx);
2963
+ // inputsAmount is enough to cover cost of tx
2964
+ if (!all && targetAmount + fee < inputsAmount)
2965
+ return { indices: res, fee, weight: totalWeight, total: inputsAmount };
2966
+ }
2967
+ if (all) {
2968
+ const newWeight = weight + 4 * CompactSizeLen.encode(num).length;
2969
+ return { indices: res, fee, weight: newWeight, total: inputsAmount };
2970
+ }
2971
+ return undefined;
2972
+ }
2973
+
2974
+ // Works like coinselect default method
2975
+ default() {
2976
+ const { biggest } = this;
2977
+ const exact = this.accumulate(biggest, true, false);
2978
+ if (exact) return exact;
2979
+ return this.accumulate(biggest);
2980
+ }
2981
+
2982
+ private select(strategy: SelectionStrategy) {
2983
+ if (strategy === 'all') {
2984
+ return this.accumulate(
2985
+ this.inputs.map((_, j) => j),
2986
+ false,
2987
+ true,
2988
+ true
2989
+ );
2990
+ }
2991
+ if (strategy === 'default') return this.default();
2992
+ const data: Record<SortStrategy, () => number[]> = {
2993
+ Oldest: () => this.oldest,
2994
+ Newest: () => this.newest,
2995
+ Smallest: () => this.smallest,
2996
+ Biggest: () => this.biggest,
2997
+ };
2998
+ if (strategy.startsWith('exact')) {
2999
+ const [exactData, left] = strategy.slice(5).split('/') as [SortStrategy, SelectionStrategy];
3000
+ if (!data[exactData]) throw new Error(`Estimator.select: wrong strategy=${strategy}`);
3001
+ strategy = left;
3002
+ const exact = this.accumulate(data[exactData](), true, true);
3003
+ if (exact) return exact;
3004
+ }
3005
+ if (strategy.startsWith('accum')) {
3006
+ const accumData = strategy.slice(5) as SortStrategy;
3007
+ if (!data[accumData]) throw new Error(`Estimator.select: wrong strategy=${strategy}`);
3008
+ return this.accumulate(data[accumData]());
3009
+ }
3010
+ throw new Error(`Estimator.select: wrong strategy=${strategy}`);
3011
+ }
3012
+
3013
+ result(strategy: SelectionStrategy) {
3014
+ const s = this.select(strategy);
3015
+ if (!s) return;
3016
+ const { indices, weight, total } = s;
3017
+ let needChange = this.opts.alwaysChange;
3018
+ const changeWeight = this.opts.alwaysChange
3019
+ ? weight
3020
+ : weight + (this.changeWeight - this.baseWeight);
3021
+
3022
+ const changeFee = this.getSatoshi(changeWeight);
3023
+ let fee = s.fee;
3024
+ const change = total - this.amount - changeFee;
3025
+ if (change > this.dust) needChange = true;
3026
+ let inputs = indices;
3027
+ let outputs = Array.from(this.outputs);
3028
+ if (needChange) {
3029
+ fee = changeFee;
3030
+ // this shouldn't happen!
3031
+ if (change < 0n) throw new Error(`Estimator.result: negative change=${change}`);
3032
+ outputs.push({ address: this.opts.changeAddress, amount: change });
3033
+ }
3034
+ if (this.opts.bip69) {
3035
+ inputs = this.sortIndices(inputs);
3036
+ outputs = this.sortOutputs(outputs).map((i) => outputs[i]);
3037
+ }
3038
+ const res = {
3039
+ inputs: inputs.map((i) => this.inputs[i]),
3040
+ outputs,
3041
+ fee,
3042
+ weight: this.opts.alwaysChange ? s.weight : changeWeight,
3043
+ change: !!needChange,
3044
+ };
3045
+ let tx;
3046
+ if (this.opts.createTx) {
3047
+ const { inputs, outputs } = res;
3048
+ tx = new Transaction(this.opts);
3049
+ for (const i of inputs) tx.addInput(i);
3050
+ for (const o of outputs)
3051
+ tx.addOutput({ ...o, script: getScript(o, this.opts, this.opts.network) });
3052
+ }
3053
+ return { ...res, tx };
3054
+ }
3055
+ }
3056
+
3057
+ export function selectUTXO(
3058
+ inputs: TransactionInputUpdate[],
3059
+ outputs: Output[],
3060
+ strategy: SelectionStrategy,
3061
+ opts: EstimatorOpts
3062
+ ) {
3063
+ // Defaults: do we want bip69 by default?
3064
+ const _opts = { createTx: true, bip69: true, ...opts };
3065
+ const est = new _Estimator(inputs, outputs, _opts);
3066
+ return est.result(strategy);
3067
+ }