All files / lib/dlc TxFinalizer.ts

67.05% Statements 59/88
41.67% Branches 5/12
50% Functions 16/32
67.07% Lines 55/82

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 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 2181x 1x   1x 1x   1x   149x 149x 149x 149x 149x 149x 149x 149x                   194x 8x     186x 328x         186x 328x     186x 186x         186x     186x 328x     186x 186x 186x 186x 186x   186x       170x                 24x                 139x 139x       21x       10x       13x 13x       11x               1x                                                                                                                                   1x           94x 94x 94x   94x         94x   94x   94x                       1x           94x 94x 94x   162x 112x 94x    
import { FundingInput, MessageType } from '@node-dlc/messaging';
import { Decimal } from 'decimal.js';
 
const BATCH_FUND_TX_BASE_WEIGHT = 42;
const FUNDING_OUTPUT_SIZE = 43;
 
export class DualFundingTxFinalizer {
  constructor(
    readonly offerInputs: FundingInput[],
    readonly offerPayoutSPK: Buffer,
    readonly offerChangeSPK: Buffer,
    readonly acceptInputs: FundingInput[],
    readonly acceptPayoutSPK: Buffer,
    readonly acceptChangeSPK: Buffer,
    readonly feeRate: bigint,
    readonly numContracts = 1,
  ) {}
 
  private computeFees(
    _inputs: FundingInput[],
    payoutSPK: Buffer,
    changeSPK: Buffer,
    numContracts: number,
  ): IFees {
    // If no inputs, return zero fees (matches C++ layer behavior for single-funded DLCs)
    if (_inputs.length === 0) {
      return { futureFee: BigInt(0), fundingFee: BigInt(0) };
    }
 
    _inputs.forEach((input) => {
      Iif (input.type !== MessageType.FundingInput) {
        console.error('input', input);
        throw new Error('Input is not a funding input');
      }
    });
    const inputs: FundingInput[] = _inputs.map(
      (input) => input as FundingInput,
    );
    // https://github.com/discreetlogcontracts/dlcspecs/blob/8ee4bbe816c9881c832b1ce320b9f14c72e3506f/Transactions.md#expected-weight-of-the-contract-execution-or-refund-transaction
    const futureFeeWeight = 249 + 4 * payoutSPK.length;
    const futureFeeVBytes = new Decimal(futureFeeWeight)
      .times(numContracts)
      .div(4)
      .ceil()
      .toNumber();
    const futureFee = this.feeRate * BigInt(futureFeeVBytes);
 
    // https://github.com/discreetlogcontracts/dlcspecs/blob/8ee4bbe816c9881c832b1ce320b9f14c72e3506f/Transactions.md#expected-weight-of-the-funding-transaction
    const inputWeight = inputs.reduce((total, input) => {
      return total + 164 + input.maxWitnessLen + input.scriptSigLength();
    }, 0);
    const contractWeight =
      (BATCH_FUND_TX_BASE_WEIGHT + FUNDING_OUTPUT_SIZE * numContracts * 4) / 2;
    const outputWeight = 36 + 4 * changeSPK.length + contractWeight;
    const weight = outputWeight + inputWeight;
    const vbytes = new Decimal(weight).div(4).ceil().toNumber();
    const fundingFee = this.feeRate * BigInt(vbytes);
 
    return { futureFee, fundingFee };
  }
 
  private getOfferFees(): IFees {
    return this.computeFees(
      this.offerInputs,
      this.offerPayoutSPK,
      this.offerChangeSPK,
      this.numContracts,
    );
  }
 
  private getAcceptFees(): IFees {
    return this.computeFees(
      this.acceptInputs,
      this.acceptPayoutSPK,
      this.acceptChangeSPK,
      this.numContracts,
    );
  }
 
  public get offerFees(): bigint {
    const { futureFee, fundingFee } = this.getOfferFees();
    return futureFee + fundingFee;
  }
 
  public get offerFutureFee(): bigint {
    return this.getOfferFees().futureFee;
  }
 
  public get offerFundingFee(): bigint {
    return this.getOfferFees().fundingFee;
  }
 
  public get acceptFees(): bigint {
    const { futureFee, fundingFee } = this.getAcceptFees();
    return futureFee + fundingFee;
  }
 
  public get acceptFutureFee(): bigint {
    return this.getAcceptFees().futureFee;
  }
 
  public get acceptFundingFee(): bigint {
    return this.getAcceptFees().fundingFee;
  }
}
 
export class DualClosingTxFinalizer {
  constructor(
    readonly initiatorInputs: FundingInput[],
    readonly offerPayoutSPK: Buffer,
    readonly acceptPayoutSPK: Buffer,
    readonly feeRate: bigint,
  ) {}
 
  private computeFees(payoutSPK: Buffer, _inputs: FundingInput[] = []): bigint {
    _inputs.forEach((input) => {
      if (input.type !== MessageType.FundingInput)
        throw new Error('Input is not a funding input');
    });
    const inputs: FundingInput[] = _inputs.map(
      (input) => input as FundingInput,
    );
    // https://gist.github.com/matthewjablack/08c36baa513af9377508111405b22e03
    const inputWeight = inputs.reduce((total, input) => {
      return total + 164 + input.maxWitnessLen + input.scriptSigLength();
    }, 0);
    const outputWeight = 36 + 4 * payoutSPK.length;
    const weight = 213 + outputWeight + inputWeight;
    const vbytes = new Decimal(weight).div(4).ceil().toNumber();
    const fee = this.feeRate * BigInt(vbytes);
 
    return fee;
  }
 
  private getOfferInitiatorFees(): bigint {
    return this.computeFees(this.offerPayoutSPK, this.initiatorInputs);
  }
 
  private getOfferReciprocatorFees(): bigint {
    return this.computeFees(this.offerPayoutSPK);
  }
 
  private getAcceptInitiatorFees(): bigint {
    return this.computeFees(this.acceptPayoutSPK, this.initiatorInputs);
  }
 
  private getAcceptReciprocatorFees(): bigint {
    return this.computeFees(this.acceptPayoutSPK);
  }
 
  public get offerInitiatorFees(): bigint {
    return this.getOfferInitiatorFees();
  }
 
  public get offerReciprocatorFees(): bigint {
    return this.getOfferReciprocatorFees();
  }
 
  public get acceptInitiatorFees(): bigint {
    return this.getAcceptInitiatorFees();
  }
 
  public get acceptReciprocatorFees(): bigint {
    return this.getAcceptReciprocatorFees();
  }
}
 
interface IFees {
  futureFee: bigint;
  fundingFee: bigint;
}
 
export const getFinalizer = (
  feeRate: bigint,
  offerInputs?: FundingInput[],
  acceptInputs?: FundingInput[],
  numContracts?: number,
): DualFundingTxFinalizer => {
  const input = new FundingInput();
  input.maxWitnessLen = 108;
  input.redeemScript = Buffer.from('', 'hex');
 
  const fakeSPK = Buffer.from(
    '0014663117d27e78eb432505180654e603acb30e8a4a',
    'hex',
  );
 
  offerInputs = offerInputs || Array.from({ length: 1 }, () => input);
 
  acceptInputs = acceptInputs || Array.from({ length: 1 }, () => input);
 
  return new DualFundingTxFinalizer(
    offerInputs,
    fakeSPK,
    fakeSPK,
    acceptInputs,
    fakeSPK,
    fakeSPK,
    feeRate,
    numContracts,
  );
};
 
export const getFinalizerByCount = (
  feeRate: bigint,
  numOfferInputs: number,
  numAcceptInputs: number,
  numContracts: number,
): DualFundingTxFinalizer => {
  const input = new FundingInput();
  input.maxWitnessLen = 108;
  input.redeemScript = Buffer.from('', 'hex');
 
  const offerInputs = Array.from({ length: numOfferInputs }, () => input);
  const acceptInputs = Array.from({ length: numAcceptInputs }, () => input);
  return getFinalizer(feeRate, offerInputs, acceptInputs, numContracts);
};