UNPKG

@node-dlc/core

Version:
143 lines 7.33 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.BatchDlcTxBuilder = exports.DlcTxBuilder = void 0; const bitcoin_1 = require("@node-dlc/bitcoin"); const messaging_1 = require("@node-dlc/messaging"); const decimal_js_1 = __importDefault(require("decimal.js")); const TxFinalizer_1 = require("./TxFinalizer"); class DlcTxBuilder { constructor(dlcOffer, dlcAccept) { this.dlcOffer = dlcOffer; this.dlcAccept = dlcAccept; } buildFundingTransaction() { const txBuilder = new BatchDlcTxBuilder([this.dlcOffer], [this.dlcAccept]); return txBuilder.buildFundingTransaction(); } } exports.DlcTxBuilder = DlcTxBuilder; class BatchDlcTxBuilder { constructor(dlcOffers, dlcAccepts) { this.dlcOffers = dlcOffers; this.dlcAccepts = dlcAccepts; } buildFundingTransaction() { const tx = new bitcoin_1.TxBuilder(); tx.version = 2; tx.locktime = bitcoin_1.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 = []; 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 ? bitcoin_1.Script.p2msLock(2, offer.fundingPubKey, accept.fundingPubKey) : bitcoin_1.Script.p2msLock(2, accept.fundingPubKey, offer.fundingPubKey)); } const witScripts = multisigScripts.map((multisigScript) => bitcoin_1.Script.p2wshLock(multisigScript)); const finalizer = new TxFinalizer_1.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 !== messaging_1.MessageType.FundingInputV0) throw Error('FundingInput must be V0'); }); const offerFundingInputs = this.dlcOffers[0].fundingInputs.map((input) => input); 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 = [ ...offerFundingInputs, ...this.dlcAccepts[0].fundingInputs, ]; fundingInputs.sort((a, b) => Number(a.inputSerialId) - Number(b.inputSerialId)); fundingInputs.forEach((input) => { tx.addInput(bitcoin_1.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_js_1.default(finalizer.offerFutureFee.toString()) .div(this.dlcOffers.length) .ceil() .toNumber(); const acceptFutureFeePerAccept = new decimal_js_1.default(finalizer.acceptFutureFee.toString()) .div(this.dlcAccepts.length) .ceil() .toNumber(); return (totalInput + bitcoin_1.Value.fromSats(offerFutureFeePerOffer).sats + bitcoin_1.Value.fromSats(acceptFutureFeePerAccept).sats); }); const offerChangeValue = offerTotalFunding - offerInput - finalizer.offerFees; const acceptChangeValue = acceptTotalFunding - acceptInput - finalizer.acceptFees; const outputs = []; witScripts.forEach((witScript, i) => { outputs.push({ value: bitcoin_1.Value.fromSats(Number(fundingValues[i])), script: witScript, serialId: this.dlcOffers[i].fundOutputSerialId, }); }); outputs.push({ value: bitcoin_1.Value.fromSats(Number(offerChangeValue)), script: bitcoin_1.Script.p2wpkhLock(this.dlcOffers[0].changeSPK.slice(2)), serialId: this.dlcOffers[0].changeSerialId, }); outputs.push({ value: bitcoin_1.Value.fromSats(Number(acceptChangeValue)), script: bitcoin_1.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(); } ensureSameFundingInputs() { // 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.`); } } } arraysEqual(arr1, arr2) { if (arr1.length !== arr2.length) return false; for (let i = 0; i < arr1.length; i++) { if (arr1[i] !== arr2[i]) return false; } return true; } } exports.BatchDlcTxBuilder = BatchDlcTxBuilder; //# sourceMappingURL=TxBuilder.js.map