All files / lib/dlc TxFinalizer.ts

63.53% Statements 54/85
30% Branches 3/10
40.63% Functions 13/32
63.29% Lines 50/79

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 2111x 1x   1x 1x   1x   129x 129x 129x 129x 129x 129x 129x 129x                 129x 262x     129x 262x     129x 129x         129x     129x 262x     129x 129x 129x 129x 129x   129x       127x                 2x                 127x 127x                       2x 2x                       1x                                                                                                                                   1x           94x 94x 94x   94x         94x   94x   94x                       1x           94x 94x 94x   162x 112x 94x    
import { FundingInput, FundingInputV0, 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 {
    _inputs.forEach((input) => {
      Iif (input.type !== MessageType.FundingInputV0)
        throw Error('FundingInput must be V0');
    });
    const inputs: FundingInputV0[] = _inputs.map(
      (input) => input as FundingInputV0,
    );
    // 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.FundingInputV0)
        throw Error('FundingInput must be V0');
    });
    const inputs: FundingInputV0[] = _inputs.map(
      (input) => input as FundingInputV0,
    );
    // 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?: FundingInputV0[],
  acceptInputs?: FundingInputV0[],
  numContracts?: number,
): DualFundingTxFinalizer => {
  const input = new FundingInputV0();
  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 FundingInputV0();
  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);
};