@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.
@@ -779,13 +779,26 @@ const PSBTInputCoder = P.validate(PSBTKeyMap(PSBTInput), (i) => {
779
779
  }
780
780
  }
781
781
  // Validate txid for nonWitnessUtxo is correct
782
- if (i.nonWitnessUtxo && i.index && i.txid) {
782
+ if (i.nonWitnessUtxo && i.index !== undefined && i.txid) {
783
783
  const outputs = i.nonWitnessUtxo.outputs;
784
784
  if (outputs.length - 1 < i.index)
785
785
  throw new Error('nonWitnessUtxo: incorect output index');
786
- const tx = Transaction.fromRaw(RawTx.encode(i.nonWitnessUtxo));
786
+ // At this point, we are using previous tx output to create new input.
787
+ // Script safety checks are unnecessary:
788
+ // - User has no control over previous tx. If somebody send money in same tx
789
+ // as unspendable output, we still want user able to spend money
790
+ // - We still want some checks to notify user about possible errors early
791
+ // in case user wants to use wrong input by mistake
792
+ // - Worst case: tx will be rejected by nodes. Still better than disallowing user
793
+ // to spend real input, no matter how broken it looks
794
+ const tx = Transaction.fromRaw(RawTx.encode(i.nonWitnessUtxo), {
795
+ allowUnknownOutputs: true,
796
+ disableScriptCheck: true,
797
+ allowUnknownInputs: true,
798
+ });
787
799
  const txid = hex.encode(i.txid);
788
- if (tx.id !== txid)
800
+ // PSBTv2 vectors have non-final tx in inputs
801
+ if (tx.isFinal && tx.id !== txid)
789
802
  throw new Error(`nonWitnessUtxo: wrong txid, exp=${txid} got=${tx.id}`);
790
803
  }
791
804
  return i;
@@ -1662,6 +1675,115 @@ function validateOpts(opts) {
1662
1675
  }
1663
1676
  return Object.freeze(_opts);
1664
1677
  }
1678
+ // Normalizes input
1679
+ function getPrevOut(input) {
1680
+ if (input.nonWitnessUtxo) {
1681
+ if (input.index === undefined)
1682
+ throw new Error('Unknown input index');
1683
+ return input.nonWitnessUtxo.outputs[input.index];
1684
+ }
1685
+ else if (input.witnessUtxo)
1686
+ return input.witnessUtxo;
1687
+ else
1688
+ throw new Error('Cannot find previous output info');
1689
+ }
1690
+ function normalizeInput(i, cur, allowedFields, disableScriptCheck = false) {
1691
+ let { nonWitnessUtxo, txid } = i;
1692
+ // String support for common fields. We usually prefer Uint8Array to avoid errors
1693
+ // like hex looking string accidentally passed, however, in case of nonWitnessUtxo
1694
+ // it is better to expect string, since constructing this complex object will be
1695
+ // difficult for user
1696
+ if (typeof nonWitnessUtxo === 'string')
1697
+ nonWitnessUtxo = hex.decode(nonWitnessUtxo);
1698
+ if (isBytes(nonWitnessUtxo))
1699
+ nonWitnessUtxo = RawTx.decode(nonWitnessUtxo);
1700
+ if (!('nonWitnessUtxo' in i) && nonWitnessUtxo === undefined)
1701
+ nonWitnessUtxo = cur?.nonWitnessUtxo;
1702
+ if (typeof txid === 'string')
1703
+ txid = hex.decode(txid);
1704
+ // TODO: if we have nonWitnessUtxo, we can extract txId from here
1705
+ if (txid === undefined)
1706
+ txid = cur?.txid;
1707
+ let res = { ...cur, ...i, nonWitnessUtxo, txid };
1708
+ if (!('nonWitnessUtxo' in i) && res.nonWitnessUtxo === undefined)
1709
+ delete res.nonWitnessUtxo;
1710
+ if (res.sequence === undefined)
1711
+ res.sequence = DEFAULT_SEQUENCE;
1712
+ if (res.tapMerkleRoot === null)
1713
+ delete res.tapMerkleRoot;
1714
+ res = mergeKeyMap(PSBTInput, res, cur, allowedFields);
1715
+ PSBTInputCoder.encode(res); // Validates that everything is correct at this point
1716
+ let prevOut;
1717
+ if (res.nonWitnessUtxo && res.index !== undefined)
1718
+ prevOut = res.nonWitnessUtxo.outputs[res.index];
1719
+ else if (res.witnessUtxo)
1720
+ prevOut = res.witnessUtxo;
1721
+ if (prevOut && !disableScriptCheck)
1722
+ checkScript(prevOut && prevOut.script, res.redeemScript, res.witnessScript);
1723
+ return res;
1724
+ }
1725
+ function getInputType(input, allowLegacyWitnessUtxo = false) {
1726
+ let txType = 'legacy';
1727
+ let defaultSighash = SignatureHash.ALL;
1728
+ const prevOut = getPrevOut(input);
1729
+ const first = OutScript.decode(prevOut.script);
1730
+ let type = first.type;
1731
+ let cur = first;
1732
+ const stack = [first];
1733
+ if (first.type === 'tr') {
1734
+ defaultSighash = SignatureHash.DEFAULT;
1735
+ return {
1736
+ txType: 'taproot',
1737
+ type: 'tr',
1738
+ last: first,
1739
+ lastScript: prevOut.script,
1740
+ defaultSighash,
1741
+ sighash: input.sighashType || defaultSighash,
1742
+ };
1743
+ }
1744
+ else {
1745
+ if (first.type === 'wpkh' || first.type === 'wsh')
1746
+ txType = 'segwit';
1747
+ if (first.type === 'sh') {
1748
+ if (!input.redeemScript)
1749
+ throw new Error('inputType: sh without redeemScript');
1750
+ let child = OutScript.decode(input.redeemScript);
1751
+ if (child.type === 'wpkh' || child.type === 'wsh')
1752
+ txType = 'segwit';
1753
+ stack.push(child);
1754
+ cur = child;
1755
+ type += `-${child.type}`;
1756
+ }
1757
+ // wsh can be inside sh
1758
+ if (cur.type === 'wsh') {
1759
+ if (!input.witnessScript)
1760
+ throw new Error('inputType: wsh without witnessScript');
1761
+ let child = OutScript.decode(input.witnessScript);
1762
+ if (child.type === 'wsh')
1763
+ txType = 'segwit';
1764
+ stack.push(child);
1765
+ cur = child;
1766
+ type += `-${child.type}`;
1767
+ }
1768
+ const last = stack[stack.length - 1];
1769
+ if (last.type === 'sh' || last.type === 'wsh')
1770
+ throw new Error('inputType: sh/wsh cannot be terminal type');
1771
+ const lastScript = OutScript.encode(last);
1772
+ const res = {
1773
+ type,
1774
+ txType,
1775
+ last,
1776
+ lastScript,
1777
+ defaultSighash,
1778
+ sighash: input.sighashType || defaultSighash,
1779
+ };
1780
+ if (txType === 'legacy' && !allowLegacyWitnessUtxo && !input.nonWitnessUtxo) {
1781
+ throw new Error(`Transaction/sign: legacy input without nonWitnessUtxo, can result in attack that forces paying higher fees. Pass allowLegacyWitnessUtxo=true, if you sure`);
1782
+ }
1783
+ return res;
1784
+ }
1785
+ }
1786
+ const toVsize = (weight) => Math.ceil(weight / 4);
1665
1787
  export class Transaction {
1666
1788
  constructor(opts = {}) {
1667
1789
  this.global = {};
@@ -1816,7 +1938,7 @@ export class Transaction {
1816
1938
  // We will lose some vectors -> smaller test coverage of preimages (very important!)
1817
1939
  inputSighash(idx) {
1818
1940
  this.checkInputIdx(idx);
1819
- const sighash = this.inputType(this.inputs[idx]).sighash;
1941
+ const sighash = getInputType(this.inputs[idx], this.opts.allowLegacyWitnessUtxo).sighash;
1820
1942
  // ALL or DEFAULT -- everything signed
1821
1943
  // NONE -- all inputs + no outputs
1822
1944
  // SINGLE -- all inputs + output with same index
@@ -1875,26 +1997,25 @@ export class Transaction {
1875
1997
  get weight() {
1876
1998
  if (!this.isFinal)
1877
1999
  throw new Error('Transaction is not finalized');
1878
- // TODO: Can we find out how much witnesses/script will be used before signing?
1879
2000
  let out = 32;
2001
+ // Outputs
1880
2002
  const outputs = this.outputs.map(outputBeforeSign);
2003
+ out += 4 * CompactSizeLen.encode(this.outputs.length).length;
2004
+ for (const o of outputs)
2005
+ out += 32 + 4 * VarBytes.encode(o.script).length;
2006
+ // Inputs
1881
2007
  if (this.hasWitnesses)
1882
2008
  out += 2;
1883
2009
  out += 4 * CompactSizeLen.encode(this.inputs.length).length;
1884
- out += 4 * CompactSizeLen.encode(this.outputs.length).length;
1885
- for (const i of this.inputs)
2010
+ for (const i of this.inputs) {
1886
2011
  out += 160 + 4 * VarBytes.encode(i.finalScriptSig || P.EMPTY).length;
1887
- for (const o of outputs)
1888
- out += 32 + 4 * VarBytes.encode(o.script).length;
1889
- if (this.hasWitnesses) {
1890
- for (const i of this.inputs)
1891
- if (i.finalScriptWitness)
1892
- out += RawWitness.encode(i.finalScriptWitness).length;
2012
+ if (this.hasWitnesses && i.finalScriptWitness)
2013
+ out += RawWitness.encode(i.finalScriptWitness).length;
1893
2014
  }
1894
2015
  return out;
1895
2016
  }
1896
2017
  get vsize() {
1897
- return Math.ceil(this.weight / 4);
2018
+ return toVsize(this.weight);
1898
2019
  }
1899
2020
  toBytes(withScriptSig = false, withWitness = false) {
1900
2021
  return RawTx.encode({
@@ -1938,42 +2059,10 @@ export class Transaction {
1938
2059
  return this.inputs.length;
1939
2060
  }
1940
2061
  // Modification
1941
- normalizeInput(i, cur, allowedFields) {
1942
- let { nonWitnessUtxo, txid } = i;
1943
- // String support for common fields. We usually prefer Uint8Array to avoid errors (like hex looking string accidentally passed),
1944
- // however in case of nonWitnessUtxo it is better to expect string, since constructing this complex object will be difficult for user
1945
- if (typeof nonWitnessUtxo === 'string')
1946
- nonWitnessUtxo = hex.decode(nonWitnessUtxo);
1947
- if (isBytes(nonWitnessUtxo))
1948
- nonWitnessUtxo = RawTx.decode(nonWitnessUtxo);
1949
- if (!('nonWitnessUtxo' in i) && nonWitnessUtxo === undefined)
1950
- nonWitnessUtxo = cur?.nonWitnessUtxo;
1951
- if (typeof txid === 'string')
1952
- txid = hex.decode(txid);
1953
- if (txid === undefined)
1954
- txid = cur?.txid;
1955
- let res = { ...cur, ...i, nonWitnessUtxo, txid };
1956
- if (!('nonWitnessUtxo' in i) && res.nonWitnessUtxo === undefined)
1957
- delete res.nonWitnessUtxo;
1958
- if (res.sequence === undefined)
1959
- res.sequence = DEFAULT_SEQUENCE;
1960
- if (res.tapMerkleRoot === null)
1961
- delete res.tapMerkleRoot;
1962
- res = mergeKeyMap(PSBTInput, res, cur, allowedFields);
1963
- PSBTInputCoder.encode(res); // Validates that everything is correct at this point
1964
- let prevOut;
1965
- if (res.nonWitnessUtxo && res.index !== undefined)
1966
- prevOut = res.nonWitnessUtxo.outputs[res.index];
1967
- else if (res.witnessUtxo)
1968
- prevOut = res.witnessUtxo;
1969
- if (prevOut && !this.opts.disableScriptCheck)
1970
- checkScript(prevOut && prevOut.script, res.redeemScript, res.witnessScript);
1971
- return res;
1972
- }
1973
2062
  addInput(input, _ignoreSignStatus = false) {
1974
2063
  if (!_ignoreSignStatus && !this.signStatus().addInput)
1975
2064
  throw new Error('Tx has signed inputs, cannot add new one');
1976
- this.inputs.push(this.normalizeInput(input));
2065
+ this.inputs.push(normalizeInput(input, undefined, undefined, this.opts.disableScriptCheck));
1977
2066
  return this.inputs.length - 1;
1978
2067
  }
1979
2068
  updateInput(idx, input, _ignoreSignStatus = false) {
@@ -1984,7 +2073,7 @@ export class Transaction {
1984
2073
  if (!status.addInput || status.inputs.includes(idx))
1985
2074
  allowedFields = PSBTInputUnsignedKeys;
1986
2075
  }
1987
- this.inputs[idx] = this.normalizeInput(input, this.inputs[idx], allowedFields);
2076
+ this.inputs[idx] = normalizeInput(input, this.inputs[idx], allowedFields, this.opts.disableScriptCheck);
1988
2077
  }
1989
2078
  // Output stuff
1990
2079
  checkOutputIdx(idx) {
@@ -2016,7 +2105,7 @@ export class Transaction {
2016
2105
  if (res.script &&
2017
2106
  !this.opts.allowUnknownOutputs &&
2018
2107
  OutScript.decode(res.script).type === 'unknown') {
2019
- throw new Error('Transaction/output: unknown output script type, there is a chance that input is unspendable. Pass allowUnknownScript=true, if you sure');
2108
+ throw new Error('Transaction/output: unknown output script type, there is a chance that input is unspendable. Pass allowUnknownOutputs=true, if you sure');
2020
2109
  }
2021
2110
  if (!this.opts.disableScriptCheck)
2022
2111
  checkScript(res.script, res.redeemScript, res.witnessScript);
@@ -2045,7 +2134,7 @@ export class Transaction {
2045
2134
  get fee() {
2046
2135
  let res = 0n;
2047
2136
  for (const i of this.inputs) {
2048
- const prevOut = this.prevOut(i);
2137
+ const prevOut = getPrevOut(i);
2049
2138
  if (!prevOut)
2050
2139
  throw new Error('Empty input amount');
2051
2140
  res += prevOut.amount;
@@ -2156,85 +2245,11 @@ export class Transaction {
2156
2245
  out.push(tapLeafHash(leafScript, leafVer), P.U8.encode(0), P.I32LE.encode(codeSeparator));
2157
2246
  return schnorr.utils.taggedHash('TapSighash', ...out);
2158
2247
  }
2159
- // Utils for sign/finalize
2160
- // Used pretty often, should be fast
2161
- prevOut(input) {
2162
- if (input.nonWitnessUtxo) {
2163
- if (input.index === undefined)
2164
- throw new Error('Unknown input index');
2165
- return input.nonWitnessUtxo.outputs[input.index];
2166
- }
2167
- else if (input.witnessUtxo)
2168
- return input.witnessUtxo;
2169
- else
2170
- throw new Error('Cannot find previous output info');
2171
- }
2172
- inputType(input) {
2173
- let txType = 'legacy';
2174
- let defaultSighash = SignatureHash.ALL;
2175
- const prevOut = this.prevOut(input);
2176
- const first = OutScript.decode(prevOut.script);
2177
- let type = first.type;
2178
- let cur = first;
2179
- const stack = [first];
2180
- if (first.type === 'tr') {
2181
- defaultSighash = SignatureHash.DEFAULT;
2182
- return {
2183
- txType: 'taproot',
2184
- type: 'tr',
2185
- last: first,
2186
- lastScript: prevOut.script,
2187
- defaultSighash,
2188
- sighash: input.sighashType || defaultSighash,
2189
- };
2190
- }
2191
- else {
2192
- if (first.type === 'wpkh' || first.type === 'wsh')
2193
- txType = 'segwit';
2194
- if (first.type === 'sh') {
2195
- if (!input.redeemScript)
2196
- throw new Error('inputType: sh without redeemScript');
2197
- let child = OutScript.decode(input.redeemScript);
2198
- if (child.type === 'wpkh' || child.type === 'wsh')
2199
- txType = 'segwit';
2200
- stack.push(child);
2201
- cur = child;
2202
- type += `-${child.type}`;
2203
- }
2204
- // wsh can be inside sh
2205
- if (cur.type === 'wsh') {
2206
- if (!input.witnessScript)
2207
- throw new Error('inputType: wsh without witnessScript');
2208
- let child = OutScript.decode(input.witnessScript);
2209
- if (child.type === 'wsh')
2210
- txType = 'segwit';
2211
- stack.push(child);
2212
- cur = child;
2213
- type += `-${child.type}`;
2214
- }
2215
- const last = stack[stack.length - 1];
2216
- if (last.type === 'sh' || last.type === 'wsh')
2217
- throw new Error('inputType: sh/wsh cannot be terminal type');
2218
- const lastScript = OutScript.encode(last);
2219
- const res = {
2220
- type,
2221
- txType,
2222
- last,
2223
- lastScript,
2224
- defaultSighash,
2225
- sighash: input.sighashType || defaultSighash,
2226
- };
2227
- if (txType === 'legacy' && !this.opts.allowLegacyWitnessUtxo && !input.nonWitnessUtxo) {
2228
- throw new Error(`Transaction/sign: legacy input without nonWitnessUtxo, can result in attack that forces paying higher fees. Pass allowLegacyWitnessUtxo=true, if you sure`);
2229
- }
2230
- return res;
2231
- }
2232
- }
2233
2248
  // Signer can be privateKey OR instance of bip32 HD stuff
2234
2249
  signIdx(privateKey, idx, allowedSighash, _auxRand) {
2235
2250
  this.checkInputIdx(idx);
2236
2251
  const input = this.inputs[idx];
2237
- const inputType = this.inputType(input);
2252
+ const inputType = getInputType(input, this.opts.allowLegacyWitnessUtxo);
2238
2253
  // Handle BIP32 HDKey
2239
2254
  if (!isBytes(privateKey)) {
2240
2255
  if (!input.bip32Derivation || !input.bip32Derivation.length)
@@ -2279,11 +2294,11 @@ export class Transaction {
2279
2294
  }
2280
2295
  // Actual signing
2281
2296
  // Taproot
2282
- const prevOut = this.prevOut(input);
2297
+ const prevOut = getPrevOut(input);
2283
2298
  if (inputType.txType === 'taproot') {
2284
2299
  if (input.tapBip32Derivation)
2285
2300
  throw new Error('tapBip32Derivation unsupported');
2286
- const prevOuts = this.inputs.map(this.prevOut);
2301
+ const prevOuts = this.inputs.map(getPrevOut);
2287
2302
  const prevOutScript = prevOuts.map((i) => i.script);
2288
2303
  const amount = prevOuts.map((i) => i.amount);
2289
2304
  let signed = false;
@@ -2383,7 +2398,7 @@ export class Transaction {
2383
2398
  if (this.fee < 0n)
2384
2399
  throw new Error('Outputs spends more than inputs amount');
2385
2400
  const input = this.inputs[idx];
2386
- const inputType = this.inputType(input);
2401
+ const inputType = getInputType(input, this.opts.allowLegacyWitnessUtxo);
2387
2402
  // Taproot finalize
2388
2403
  if (inputType.txType === 'taproot') {
2389
2404
  if (input.tapKeySig)
@@ -2621,4 +2636,376 @@ export function PSBTCombine(psbts) {
2621
2636
  tx.combine(Transaction.fromPSBT(psbts[i]));
2622
2637
  return tx.toPSBT();
2623
2638
  }
2624
- //# sourceMappingURL=index.js.map
2639
+ function estimateInput(inputType, input, opts) {
2640
+ let script = P.EMPTY, witness;
2641
+ // schnorr sig is always 64 bytes. except for cases when sighash is not default!
2642
+ if (inputType.txType === 'taproot') {
2643
+ const SCHNORR_SIG_SIZE = inputType.sighash !== SignatureHash.DEFAULT ? 65 : 64;
2644
+ if (input.tapInternalKey && !P.equalBytes(input.tapInternalKey, TAPROOT_UNSPENDABLE_KEY)) {
2645
+ witness = [new Uint8Array(SCHNORR_SIG_SIZE)];
2646
+ }
2647
+ else if (input.tapLeafScript) {
2648
+ // If user want to select specific leaf (which can signed, it is possible to remove all other leafs manually);
2649
+ // Sort leafs by control block length.
2650
+ const leafs = input.tapLeafScript.sort((a, b) => TaprootControlBlock.encode(a[0]).length - TaprootControlBlock.encode(b[0]).length);
2651
+ for (const [cb, _script] of leafs) {
2652
+ // Last byte is version
2653
+ const script = _script.slice(0, -1);
2654
+ const outScript = OutScript.decode(script);
2655
+ let signatures = [];
2656
+ if (outScript.type === 'tr_ms') {
2657
+ const m = outScript.m;
2658
+ for (let i = 0; i < m; i++)
2659
+ signatures.push(new Uint8Array(SCHNORR_SIG_SIZE));
2660
+ const n = outScript.pubkeys.length - m;
2661
+ for (let i = 0; i < n; i++)
2662
+ signatures.push(P.EMPTY);
2663
+ }
2664
+ else if (outScript.type === 'tr_ns') {
2665
+ for (const _pub of outScript.pubkeys)
2666
+ signatures.push(new Uint8Array(SCHNORR_SIG_SIZE));
2667
+ }
2668
+ else
2669
+ throw new Error('Finalize: Unknown tapLeafScript');
2670
+ // Witness is stack, so last element will be used first
2671
+ witness = signatures.reverse().concat([script, TaprootControlBlock.encode(cb)]);
2672
+ break;
2673
+ }
2674
+ }
2675
+ else
2676
+ throw new Error('estimateInput/taproot: unknown input');
2677
+ }
2678
+ else {
2679
+ // It is possible to grind signatures until it has minimal size (but changing fee value +N satoshi),
2680
+ // which will make estimations exact. But will be very hard for multi sig (need to make sure all signatures has small size).
2681
+ const SIG_SIZE = 72; // Maximum size of signatures
2682
+ const PUB_KEY_SIZE = 33;
2683
+ let inputScript = P.EMPTY;
2684
+ let inputWitness = [];
2685
+ if (inputType.last.type === 'ms') {
2686
+ const m = inputType.last.m;
2687
+ const sig = [0];
2688
+ for (let i = 0; i < m; i++)
2689
+ sig.push(new Uint8Array(SIG_SIZE));
2690
+ inputScript = Script.encode(sig);
2691
+ }
2692
+ else if (inputType.last.type === 'pk') {
2693
+ // 71 sig + 1 sighash
2694
+ inputScript = Script.encode([new Uint8Array(SIG_SIZE)]);
2695
+ }
2696
+ else if (inputType.last.type === 'pkh') {
2697
+ inputScript = Script.encode([new Uint8Array(SIG_SIZE), new Uint8Array(PUB_KEY_SIZE)]);
2698
+ }
2699
+ else if (inputType.last.type === 'wpkh') {
2700
+ inputScript = P.EMPTY;
2701
+ inputWitness = [new Uint8Array(SIG_SIZE), new Uint8Array(PUB_KEY_SIZE)];
2702
+ }
2703
+ else if (inputType.last.type === 'unknown' && !opts.allowUnknownInputs)
2704
+ throw new Error('Unknown inputs not allowed');
2705
+ if (inputType.type.includes('wsh-')) {
2706
+ // P2WSH
2707
+ if (inputScript.length && inputType.lastScript.length) {
2708
+ inputWitness = Script.decode(inputScript).map((i) => {
2709
+ if (i === 0)
2710
+ return P.EMPTY;
2711
+ if (isBytes(i))
2712
+ return i;
2713
+ throw new Error(`Wrong witness op=${i}`);
2714
+ });
2715
+ }
2716
+ inputWitness = inputWitness.concat(inputType.lastScript);
2717
+ }
2718
+ if (inputType.txType === 'segwit')
2719
+ witness = inputWitness;
2720
+ if (inputType.type.startsWith('sh-wsh-')) {
2721
+ script = Script.encode([Script.encode([0, new Uint8Array(sha256.outputLen)])]);
2722
+ }
2723
+ else if (inputType.type.startsWith('sh-')) {
2724
+ script = Script.encode([...Script.decode(inputScript), inputType.lastScript]);
2725
+ }
2726
+ else if (inputType.type.startsWith('wsh-')) {
2727
+ }
2728
+ else if (inputType.txType !== 'segwit')
2729
+ script = inputScript;
2730
+ }
2731
+ let weight = 160 + 4 * VarBytes.encode(script).length;
2732
+ let hasWitnesses = false;
2733
+ if (witness) {
2734
+ weight += RawWitness.encode(witness).length;
2735
+ hasWitnesses = true;
2736
+ }
2737
+ return { weight, hasWitnesses };
2738
+ }
2739
+ // Exported for tests, internal method
2740
+ export const _cmpBig = (a, b) => {
2741
+ const n = a - b;
2742
+ if (n < 0n)
2743
+ return -1;
2744
+ else if (n > 0n)
2745
+ return 1;
2746
+ return 0;
2747
+ };
2748
+ function getScript(o, opts = {}, network = NETWORK) {
2749
+ let script;
2750
+ if ('script' in o && o.script instanceof Uint8Array) {
2751
+ script = o.script;
2752
+ }
2753
+ if ('address' in o) {
2754
+ if (typeof o.address !== 'string')
2755
+ throw new Error(`Estimator: wrong output address=${o.address}`);
2756
+ script = OutScript.encode(Address(network).decode(o.address));
2757
+ }
2758
+ if (!script)
2759
+ throw new Error('Estimator: wrong output script');
2760
+ if (typeof o.amount !== 'bigint')
2761
+ throw new Error(`Estimator: wrong output amount=${o.amount}`);
2762
+ if (script && !opts.allowUnknownOutputs && OutScript.decode(script).type === 'unknown') {
2763
+ throw new Error('Estimator: unknown output script type, there is a chance that input is unspendable. Pass allowUnknownOutputs=true, if you sure');
2764
+ }
2765
+ if (!opts.disableScriptCheck)
2766
+ checkScript(script);
2767
+ return script;
2768
+ }
2769
+ // class, because we need to re-use normalized inputs, instead of parsing each time
2770
+ // internal stuff, exported for tests only
2771
+ export class _Estimator {
2772
+ constructor(inputs, outputs, opts) {
2773
+ this.inputs = inputs;
2774
+ this.outputs = outputs;
2775
+ this.opts = opts;
2776
+ // https://github.com/bitcoin/bitcoin/blob/f90603ac6d24f5263649675d51233f1fce8b2ecd/src/policy/policy.cpp#L44
2777
+ // 32 + 4 + 1 + 107 + 4
2778
+ // Dust used in accumExact + change address algo
2779
+ // - change address: can be smaller for segwit
2780
+ // - accumExact: ???
2781
+ this.dust = 148n; // compat with coinselect
2782
+ if (typeof opts.feePerByte !== 'bigint')
2783
+ throw new Error(`Estimator: wrong feePerByte=${opts.feePerByte}`);
2784
+ if (opts.dust) {
2785
+ if (typeof opts.dust !== 'bigint')
2786
+ throw new Error(`Estimator: wrong dust=${opts.dust}`);
2787
+ this.dust = opts.dust;
2788
+ }
2789
+ const network = opts.network || NETWORK;
2790
+ let amount = 0n;
2791
+ // Base weight: tx with outputs, no inputs
2792
+ let baseWeight = 32;
2793
+ for (const o of outputs) {
2794
+ const script = getScript(o, opts, opts.network);
2795
+ baseWeight += 32 + 4 * VarBytes.encode(script).length;
2796
+ amount += o.amount;
2797
+ }
2798
+ if (typeof opts.changeAddress !== 'string')
2799
+ throw new Error(`Estimator: wrong change address=${opts.changeAddress}`);
2800
+ let changeWeight = baseWeight +
2801
+ 32 +
2802
+ 4 * VarBytes.encode(OutScript.encode(Address(network).decode(opts.changeAddress))).length;
2803
+ baseWeight += 4 * CompactSizeLen.encode(outputs.length).length;
2804
+ // If there a lot of outputs change can change fee
2805
+ changeWeight += 4 * CompactSizeLen.encode(outputs.length + 1).length;
2806
+ this.baseWeight = baseWeight;
2807
+ this.changeWeight = changeWeight;
2808
+ this.amount = amount;
2809
+ this.normalizedInputs = this.inputs.map((i) => {
2810
+ const normalized = normalizeInput(i, undefined, undefined, opts.disableScriptCheck);
2811
+ inputBeforeSign(normalized); // check fields
2812
+ const inputType = getInputType(normalized, opts.allowLegacyWitnessUtxo);
2813
+ const prev = getPrevOut(normalized);
2814
+ const estimate = estimateInput(inputType, normalized, this.opts);
2815
+ const value = prev.amount - opts.feePerByte * BigInt(toVsize(estimate.weight)); // value = amount-fee
2816
+ return { inputType, normalized, amount: prev.amount, value, estimate };
2817
+ });
2818
+ }
2819
+ checkInputIdx(idx) {
2820
+ if (!Number.isSafeInteger(idx) || 0 > idx || idx >= this.inputs.length)
2821
+ throw new Error(`Wrong input index=${idx}`);
2822
+ return idx;
2823
+ }
2824
+ sortIndices(indices) {
2825
+ return indices.slice().sort((a, b) => {
2826
+ const ai = this.normalizedInputs[this.checkInputIdx(a)];
2827
+ const bi = this.normalizedInputs[this.checkInputIdx(b)];
2828
+ const out = _cmpBytes(ai.normalized.txid, bi.normalized.txid);
2829
+ if (out !== 0)
2830
+ return out;
2831
+ return ai.normalized.index - bi.normalized.index;
2832
+ });
2833
+ }
2834
+ sortOutputs(outputs) {
2835
+ const scripts = outputs.map((o) => getScript(o, this.opts, this.opts.network));
2836
+ const indices = outputs.map((_, j) => j);
2837
+ return indices.sort((a, b) => {
2838
+ const aa = outputs[a].amount;
2839
+ const ba = outputs[b].amount;
2840
+ const out = _cmpBig(aa, ba);
2841
+ if (out !== 0)
2842
+ return out;
2843
+ return _cmpBytes(scripts[a], scripts[b]);
2844
+ });
2845
+ }
2846
+ getSatoshi(weigth) {
2847
+ return this.opts.feePerByte * BigInt(toVsize(weigth));
2848
+ }
2849
+ // Sort by value instead of amount
2850
+ get biggest() {
2851
+ return this.inputs
2852
+ .map((_i, j) => j)
2853
+ .sort((a, b) => _cmpBig(this.normalizedInputs[b].value, this.normalizedInputs[a].value));
2854
+ }
2855
+ get smallest() {
2856
+ return this.biggest.reverse();
2857
+ }
2858
+ // These assume that UTXO array has historical order.
2859
+ // Otherwise, we have no way to know which tx is oldest
2860
+ // Explorers usually give UTXO in this order.
2861
+ get oldest() {
2862
+ return this.inputs.map((_i, j) => j);
2863
+ }
2864
+ get newest() {
2865
+ return this.oldest.reverse();
2866
+ }
2867
+ // exact - like blackjack from coinselect.
2868
+ // exact(biggest) will select one big utxo which is closer to targetValue+dust, if possible.
2869
+ // If not, it will accumulate largest utxo until value is close to targetValue+dust.
2870
+ accumulate(indices, exact = false, skipNegative = true, all = false) {
2871
+ const { feePerByte } = this.opts;
2872
+ // TODO: how to handle change addresses?
2873
+ // - cost of input
2874
+ // - cost of change output (if input requires change)
2875
+ // - cost of output spending
2876
+ // Dust threshold should be significantly bigger, no point in
2877
+ // creating an output, which cannot be spent.
2878
+ // coinselect doesn't consider cost of output address for dust.
2879
+ // Changing that can actually reduce privacy
2880
+ let weight = this.opts.alwaysChange ? this.changeWeight : this.baseWeight;
2881
+ let hasWitnesses = false;
2882
+ let num = 0;
2883
+ let inputsAmount = 0n;
2884
+ const targetAmount = this.amount;
2885
+ const res = [];
2886
+ let fee;
2887
+ for (const idx of indices) {
2888
+ this.checkInputIdx(idx);
2889
+ const { estimate, amount, value } = this.normalizedInputs[idx];
2890
+ let newWeight = weight + estimate.weight;
2891
+ if (!hasWitnesses && estimate.hasWitnesses)
2892
+ newWeight += 2; // enable witness if needed
2893
+ const totalWeight = newWeight + 4 * CompactSizeLen.encode(num).length; // number of outputs can change weight
2894
+ fee = this.getSatoshi(totalWeight);
2895
+ // Best case scenario exact(biggest) -> we find biggest output, less than target+threshold
2896
+ if (exact) {
2897
+ const dust = this.dust * feePerByte;
2898
+ // skip if added value is bigger than dust
2899
+ if (amount + inputsAmount > targetAmount + fee + dust)
2900
+ continue;
2901
+ }
2902
+ // Negative: cost of using input is more than value provided (negative)
2903
+ // By default 'blackjack' mode in coinselect doesn't use that, which means
2904
+ // it will use negative output if sorted by 'smallest'
2905
+ if (skipNegative && value <= 0n)
2906
+ continue;
2907
+ weight = newWeight;
2908
+ if (estimate.hasWitnesses)
2909
+ hasWitnesses = true;
2910
+ num++;
2911
+ inputsAmount += amount;
2912
+ res.push(idx);
2913
+ // inputsAmount is enough to cover cost of tx
2914
+ if (!all && targetAmount + fee < inputsAmount)
2915
+ return { indices: res, fee, weight: totalWeight, total: inputsAmount };
2916
+ }
2917
+ if (all) {
2918
+ const newWeight = weight + 4 * CompactSizeLen.encode(num).length;
2919
+ return { indices: res, fee, weight: newWeight, total: inputsAmount };
2920
+ }
2921
+ return undefined;
2922
+ }
2923
+ // Works like coinselect default method
2924
+ default() {
2925
+ const { biggest } = this;
2926
+ const exact = this.accumulate(biggest, true, false);
2927
+ if (exact)
2928
+ return exact;
2929
+ return this.accumulate(biggest);
2930
+ }
2931
+ select(strategy) {
2932
+ if (strategy === 'all') {
2933
+ return this.accumulate(this.inputs.map((_, j) => j), false, true, true);
2934
+ }
2935
+ if (strategy === 'default')
2936
+ return this.default();
2937
+ const data = {
2938
+ Oldest: () => this.oldest,
2939
+ Newest: () => this.newest,
2940
+ Smallest: () => this.smallest,
2941
+ Biggest: () => this.biggest,
2942
+ };
2943
+ if (strategy.startsWith('exact')) {
2944
+ const [exactData, left] = strategy.slice(5).split('/');
2945
+ if (!data[exactData])
2946
+ throw new Error(`Estimator.select: wrong strategy=${strategy}`);
2947
+ strategy = left;
2948
+ const exact = this.accumulate(data[exactData](), true, true);
2949
+ if (exact)
2950
+ return exact;
2951
+ }
2952
+ if (strategy.startsWith('accum')) {
2953
+ const accumData = strategy.slice(5);
2954
+ if (!data[accumData])
2955
+ throw new Error(`Estimator.select: wrong strategy=${strategy}`);
2956
+ return this.accumulate(data[accumData]());
2957
+ }
2958
+ throw new Error(`Estimator.select: wrong strategy=${strategy}`);
2959
+ }
2960
+ result(strategy) {
2961
+ const s = this.select(strategy);
2962
+ if (!s)
2963
+ return;
2964
+ const { indices, weight, total } = s;
2965
+ let needChange = this.opts.alwaysChange;
2966
+ const changeWeight = this.opts.alwaysChange
2967
+ ? weight
2968
+ : weight + (this.changeWeight - this.baseWeight);
2969
+ const changeFee = this.getSatoshi(changeWeight);
2970
+ let fee = s.fee;
2971
+ const change = total - this.amount - changeFee;
2972
+ if (change > this.dust)
2973
+ needChange = true;
2974
+ let inputs = indices;
2975
+ let outputs = Array.from(this.outputs);
2976
+ if (needChange) {
2977
+ fee = changeFee;
2978
+ // this shouldn't happen!
2979
+ if (change < 0n)
2980
+ throw new Error(`Estimator.result: negative change=${change}`);
2981
+ outputs.push({ address: this.opts.changeAddress, amount: change });
2982
+ }
2983
+ if (this.opts.bip69) {
2984
+ inputs = this.sortIndices(inputs);
2985
+ outputs = this.sortOutputs(outputs).map((i) => outputs[i]);
2986
+ }
2987
+ const res = {
2988
+ inputs: inputs.map((i) => this.inputs[i]),
2989
+ outputs,
2990
+ fee,
2991
+ weight: this.opts.alwaysChange ? s.weight : changeWeight,
2992
+ change: !!needChange,
2993
+ };
2994
+ let tx;
2995
+ if (this.opts.createTx) {
2996
+ const { inputs, outputs } = res;
2997
+ tx = new Transaction(this.opts);
2998
+ for (const i of inputs)
2999
+ tx.addInput(i);
3000
+ for (const o of outputs)
3001
+ tx.addOutput({ ...o, script: getScript(o, this.opts, this.opts.network) });
3002
+ }
3003
+ return { ...res, tx };
3004
+ }
3005
+ }
3006
+ export function selectUTXO(inputs, outputs, strategy, opts) {
3007
+ // Defaults: do we want bip69 by default?
3008
+ const _opts = { createTx: true, bip69: true, ...opts };
3009
+ const est = new _Estimator(inputs, outputs, _opts);
3010
+ return est.result(strategy);
3011
+ }