UNPKG

@node-dlc/core

Version:
229 lines (198 loc) 6.84 kB
import { LockTime, OutPoint, Script, Tx, TxBuilder, Value, } from '@node-dlc/bitcoin'; import { DlcAcceptWithoutSigs, DlcOfferV0, FundingInputV0, MessageType, } from '@node-dlc/messaging'; import Decimal from 'decimal.js'; import { DualFundingTxFinalizer } from './TxFinalizer'; export class DlcTxBuilder { constructor( readonly dlcOffer: DlcOfferV0, readonly dlcAccept: DlcAcceptWithoutSigs, ) {} public buildFundingTransaction(): Tx { const txBuilder = new BatchDlcTxBuilder([this.dlcOffer], [this.dlcAccept]); return txBuilder.buildFundingTransaction(); } } export class BatchDlcTxBuilder { constructor( readonly dlcOffers: DlcOfferV0[], readonly dlcAccepts: DlcAcceptWithoutSigs[], ) {} public buildFundingTransaction(): Tx { const tx = new TxBuilder(); tx.version = 2; tx.locktime = LockTime.zero(); if (this.dlcOffers.length !== this.dlcAccepts.length) throw Error('DlcOffers and DlcAccepts must be the same length'); if (this.dlcOffers.length === 0) throw Error('DlcOffers must not be empty'); if (this.dlcAccepts.length === 0) throw Error('DlcAccepts must not be empty'); // Ensure all DLC offers and accepts have the same funding inputs this.ensureSameFundingInputs(); const multisigScripts: Script[] = []; for (let i = 0; i < this.dlcOffers.length; i++) { const offer = this.dlcOffers[i]; const accept = this.dlcAccepts[i]; multisigScripts.push( Buffer.compare(offer.fundingPubKey, accept.fundingPubKey) === -1 ? Script.p2msLock(2, offer.fundingPubKey, accept.fundingPubKey) : Script.p2msLock(2, accept.fundingPubKey, offer.fundingPubKey), ); } const witScripts = multisigScripts.map((multisigScript) => Script.p2wshLock(multisigScript), ); const finalizer = new DualFundingTxFinalizer( this.dlcOffers[0].fundingInputs, this.dlcOffers[0].payoutSPK, this.dlcOffers[0].changeSPK, this.dlcAccepts[0].fundingInputs, this.dlcAccepts[0].payoutSPK, this.dlcAccepts[0].changeSPK, this.dlcOffers[0].feeRatePerVb, this.dlcOffers.length, ); this.dlcOffers[0].fundingInputs.forEach((input) => { if (input.type !== MessageType.FundingInputV0) throw Error('FundingInput must be V0'); }); const offerFundingInputs: FundingInputV0[] = this.dlcOffers[0].fundingInputs.map( (input) => input as FundingInputV0, ); const offerTotalFunding = offerFundingInputs.reduce((total, input) => { return total + input.prevTx.outputs[input.prevTxVout].value.sats; }, BigInt(0)); const acceptTotalFunding = this.dlcAccepts[0].fundingInputs.reduce( (total, input) => { return total + input.prevTx.outputs[input.prevTxVout].value.sats; }, BigInt(0), ); const fundingInputs: FundingInputV0[] = [ ...offerFundingInputs, ...this.dlcAccepts[0].fundingInputs, ]; fundingInputs.sort( (a, b) => Number(a.inputSerialId) - Number(b.inputSerialId), ); fundingInputs.forEach((input) => { tx.addInput( OutPoint.fromString( `${input.prevTx.txId.toString()}:${input.prevTxVout}`, ), ); }); const offerInput = this.dlcOffers.reduce( (total, offer) => total + offer.offerCollateralSatoshis, BigInt(0), ); const acceptInput = this.dlcAccepts.reduce( (total, accept) => total + accept.acceptCollateralSatoshis, BigInt(0), ); const totalInputs = this.dlcOffers.map((offer, i) => { const offerInput = offer.offerCollateralSatoshis; const acceptInput = this.dlcAccepts[i].acceptCollateralSatoshis; return offerInput + acceptInput; }); const fundingValues = totalInputs.map((totalInput) => { const offerFutureFeePerOffer = new Decimal( finalizer.offerFutureFee.toString(), ) .div(this.dlcOffers.length) .ceil() .toNumber(); const acceptFutureFeePerAccept = new Decimal( finalizer.acceptFutureFee.toString(), ) .div(this.dlcAccepts.length) .ceil() .toNumber(); return ( totalInput + Value.fromSats(offerFutureFeePerOffer).sats + Value.fromSats(acceptFutureFeePerAccept).sats ); }); const offerChangeValue = offerTotalFunding - offerInput - finalizer.offerFees; const acceptChangeValue = acceptTotalFunding - acceptInput - finalizer.acceptFees; const outputs: Output[] = []; witScripts.forEach((witScript, i) => { outputs.push({ value: Value.fromSats(Number(fundingValues[i])), script: witScript, serialId: this.dlcOffers[i].fundOutputSerialId, }); }); outputs.push({ value: Value.fromSats(Number(offerChangeValue)), script: Script.p2wpkhLock(this.dlcOffers[0].changeSPK.slice(2)), serialId: this.dlcOffers[0].changeSerialId, }); outputs.push({ value: Value.fromSats(Number(acceptChangeValue)), script: Script.p2wpkhLock(this.dlcAccepts[0].changeSPK.slice(2)), serialId: this.dlcAccepts[0].changeSerialId, }); outputs.sort((a, b) => Number(a.serialId) - Number(b.serialId)); outputs.forEach((output) => { tx.addOutput(output.value, output.script); }); return tx.toTx(); } private ensureSameFundingInputs(): void { // Check for offers const referenceOfferInputs = this.dlcOffers[0].fundingInputs.map((input) => input.serialize().toString('hex'), ); for (let i = 1; i < this.dlcOffers.length; i++) { const currentInputs = this.dlcOffers[i].fundingInputs.map((input) => input.serialize().toString('hex'), ); if (!this.arraysEqual(referenceOfferInputs, currentInputs)) { throw new Error( `Funding inputs for offer ${i} do not match the first offer's funding inputs.`, ); } } // Check for accepts const referenceAcceptInputs = this.dlcAccepts[0].fundingInputs.map( (input) => input.serialize().toString('hex'), ); for (let i = 1; i < this.dlcAccepts.length; i++) { const currentInputs = this.dlcAccepts[i].fundingInputs.map((input) => input.serialize().toString('hex'), ); if (!this.arraysEqual(referenceAcceptInputs, currentInputs)) { throw new Error( `Funding inputs for accept ${i} do not match the first accept's funding inputs.`, ); } } } private arraysEqual(arr1: string[], arr2: string[]): boolean { if (arr1.length !== arr2.length) return false; for (let i = 0; i < arr1.length; i++) { if (arr1[i] !== arr2[i]) return false; } return true; } } interface Output { value: Value; script: Script; serialId: bigint; }