@node-dlc/core
Version:
143 lines • 7.33 kB
JavaScript
;
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