UNPKG

@node-dlc/core

Version:
218 lines (184 loc) 6.35 kB
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) => { if (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); };