All files / lib/dlc CoinSelect.ts

100% Statements 46/46
100% Branches 6/6
100% Functions 9/9
100% Lines 38/38

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 1181x   1x                   1x           1x 207x     1x 95x       1x 76x     1x         35x 35x 35x   35x         104x 35x   35x                                           1x         9x 38x     9x 9x   9x     9x 36x 36x 36x 36x     36x 6x 2x       4x     30x 30x   30x     30x   5x     2x          
import { FundingInput } from '@node-dlc/messaging';
 
import { DualFundingTxFinalizer } from './TxFinalizer';
 
export interface UTXO {
  txid: string;
  vout: number;
  value: number;
  address: string;
  derivationPath?: string;
}
 
const TX_INPUT_SIZE = {
  LEGACY: 148,
  P2SH: 92,
  BECH32: 69,
};
 
const inputBytes = () => {
  return BigInt(TX_INPUT_SIZE.BECH32);
};
 
export const dustThreshold = (feeRate: bigint): bigint => {
  return BigInt(inputBytes()) * feeRate;
};
 
// order by descending value, minus the inputs approximate fee
const utxoScore = (x: UTXO, feeRate: bigint): bigint => {
  return BigInt(x.value) - feeRate * inputBytes();
};
 
export const dualFees = (
  feeRate: bigint,
  numInputs: number,
  numContracts: number,
): bigint => {
  const input = new FundingInput();
  input.maxWitnessLen = 108;
  input.redeemScript = Buffer.from('', 'hex');
 
  const fakeSPK = Buffer.from(
    '0014663117d27e78eb432505180654e603acb30e8a4a',
    'hex',
  );
 
  const offerInputs = Array.from({ length: numInputs }, () => input);
  const acceptInputs = Array.from({ length: 1 }, () => input);
 
  return new DualFundingTxFinalizer(
    offerInputs,
    fakeSPK,
    fakeSPK,
    acceptInputs,
    fakeSPK,
    fakeSPK,
    feeRate,
    numContracts,
  ).offerFees;
};
 
/**
 * Selects UTXOs for dual funding
 * @param utxos - UTXOs to select from
 * @param collaterals - Collaterals to fund (just one for non-batch tx)
 * @param feeRate - Fee rate in satoshis per byte
 * @returns Inputs and fee
 * @description
 * Add inputs until we reach or surpass the target value (or deplete)
 * Worst-case: O(n)
 */
export const dualFundingCoinSelect = (
  utxos: UTXO[],
  collaterals: bigint[], // in satoshis
  feeRate: bigint,
): { inputs: UTXO[]; fee: bigint } => {
  utxos = [...utxos].sort((a, b) =>
    Number(utxoScore(b, feeRate) - utxoScore(a, feeRate)),
  );
 
  let inAccum = 0;
  const inputs: UTXO[] = [];
  const outAccum =
    collaterals.reduce((acc, val) => acc + val, BigInt(0)) +
    dustThreshold(feeRate); // sum of collaterals
 
  for (let i = 0; i < utxos.length; ++i) {
    const utxo = utxos[i];
    const utxoBytes = inputBytes();
    const utxoFee = feeRate * utxoBytes;
    const utxoValue = utxo.value;
 
    // skip detrimental input
    if (utxoFee > utxo.value) {
      if (i === utxos.length - 1)
        return {
          fee: dualFees(feeRate, 1, collaterals.length),
          inputs: [],
        };
      continue;
    }
 
    inAccum += utxoValue;
    inputs.push(utxo);
 
    const fee = dualFees(feeRate, inputs.length, collaterals.length);
 
    // go again?
    if (inAccum < outAccum + fee) continue;
 
    return { inputs, fee };
  }
 
  return {
    fee: dualFees(feeRate, 1, collaterals.length),
    inputs: [],
  };
};