@node-dlc/core
Version:
422 lines • 22.1 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildRoundingIntervalsFromIntervals = exports.buildCustomStrategyOrderOffer = exports.buildLinearOrderOffer = exports.buildLongPutOrderOffer = exports.buildLongCallOrderOffer = exports.buildShortPutOrderOffer = exports.buildCoveredCallOrderOffer = exports.buildOptionOrderOffer = exports.buildOrderOffer = exports.getDigitDecompositionEventDescriptor = exports.computeRoundingModulus = exports.roundDownToNearestMultiplier = exports.roundUpToNearestMultiplier = exports.roundToNearestMultiplier = exports.UNIT_MULTIPLIER = void 0;
const bitcoin_1 = require("@node-dlc/bitcoin");
const messaging_1 = require("@node-dlc/messaging");
const bitcoin_networks_1 = require("bitcoin-networks");
const decimal_js_1 = __importDefault(require("decimal.js"));
const CoinSelect_1 = require("../CoinSelect");
const TxFinalizer_1 = require("../TxFinalizer");
const CoveredCall_1 = require("./CoveredCall");
const LinearPayout_1 = require("./LinearPayout");
const LongCall_1 = require("./LongCall");
const LongPut_1 = require("./LongPut");
const ShortPut_1 = require("./ShortPut");
exports.UNIT_MULTIPLIER = {
bits: BigInt(1e2),
sats: BigInt(1),
};
/**
* Round a number to the nearest multiple of a given multiplier.
*
* @param num - The number to be rounded.
* @param multiplier - The multiplier to round to.
* @returns The number rounded to the nearest multiple of the multiplier.
*
* @example
* ```typescript
* // Example: rounding to nearest 100
* const number = BigInt(354);
* const multiplier = BigInt(100);
* const roundedNumber = roundToNearestMultiplier(number, multiplier);
* console.log(roundedNumber); // Output: 300
* ```
*/
const roundToNearestMultiplier = (num, multiplier) => (num / multiplier) * multiplier;
exports.roundToNearestMultiplier = roundToNearestMultiplier;
/**
* Round a number up to the nearest multiple of a given multiplier.
*
* @param num - The number to be rounded.
* @param multiplier - The multiplier to round to.
* @returns The number rounded up to the nearest multiple of the multiplier.
*
* @example
* ```typescript
* // Example: rounding up to nearest 100
* const number = BigInt(354);
* const multiplier = BigInt(100);
* const roundedNumber = roundToNearestMultiplier(number, multiplier);
* console.log(roundedNumber); // Output: 400
* ```
*/
const roundUpToNearestMultiplier = (num, multiplier) => ((num + multiplier - BigInt(1)) / multiplier) * multiplier;
exports.roundUpToNearestMultiplier = roundUpToNearestMultiplier;
const roundDownToNearestMultiplier = (num, multiplier) => num - (num % multiplier);
exports.roundDownToNearestMultiplier = roundDownToNearestMultiplier;
/**
* Compute rounding intervals for a linear or hyperbola payout curve
*
* @param {number | bigint | Value} rounding rounding interval in sats
* @param {number | bigint | Value} contractSize contract size in sats
* @returns rounding mod for contract size
*/
const computeRoundingModulus = (rounding, contractSize) => {
const roundingInSats = rounding instanceof bitcoin_1.Value
? rounding.sats
: typeof rounding === 'number'
? BigInt(Math.round(rounding * 1e8)) // Convert bitcoin amount to satoshis
: rounding;
const contractSizeInSats = contractSize instanceof bitcoin_1.Value
? contractSize.sats
: typeof contractSize === 'number'
? BigInt(Math.round(contractSize * 1e8)) // Convert bitcoin amount to satoshis
: contractSize;
return (roundingInSats * contractSizeInSats) / BigInt(1e8);
};
exports.computeRoundingModulus = computeRoundingModulus;
/**
* Get digit decomposition event descriptor from oracle announcement
*
* @param {OracleAnnouncement} announcement oracle announcement
* @returns {DigitDecompositionEventDescriptorV0} event descriptor
*/
const getDigitDecompositionEventDescriptor = (announcement) => {
if (announcement.oracleEvent.eventDescriptor.type !==
messaging_1.MessageType.DigitDecompositionEventDescriptorV0)
throw Error('Only DigitDecompositionEventDescriptorV0 currently supported');
const eventDescriptor = announcement.oracleEvent
.eventDescriptor;
return eventDescriptor;
};
exports.getDigitDecompositionEventDescriptor = getDigitDecompositionEventDescriptor;
/**
* Build an orderoffer for ContractDescriptorV1
*
* @param {OracleAnnouncement} announcement oracle announcement
* @param {bigint} totalCollateral total collateral in satoshis
* @param {bigint} offerCollateral offer collateral in satoshis
* @param {PayoutFunction} payoutFunction
* @param {RoundingIntervals} roundingIntervals
* @param {bigint} feePerByte sats/vbyte
* @param {NetworkName} network
* @returns {OrderOffer} Returns order offer
*/
const buildOrderOffer = (announcement, totalCollateral, offerCollateral, payoutFunction, roundingIntervals, feePerByte, network) => {
const eventDescriptor = (0, exports.getDigitDecompositionEventDescriptor)(announcement);
const contractDescriptor = new messaging_1.NumericalDescriptor();
contractDescriptor.numDigits = eventDescriptor.nbDigits;
contractDescriptor.payoutFunction = payoutFunction;
contractDescriptor.roundingIntervals = roundingIntervals;
const oracleInfo = new messaging_1.SingleOracleInfo();
oracleInfo.announcement = announcement;
const contractInfo = new messaging_1.SingleContractInfo();
contractInfo.totalCollateral = totalCollateral;
contractInfo.contractDescriptor = contractDescriptor;
contractInfo.oracleInfo = oracleInfo;
const orderOffer = new messaging_1.OrderOffer();
// Set required fields for OrderOffer
orderOffer.contractFlags = Buffer.from('00', 'hex'); // Default contract flags
// Generate a random 32-byte temporary contract ID
orderOffer.temporaryContractId = Buffer.from(Array.from({ length: 32 }, () => Math.floor(Math.random() * 256)));
orderOffer.chainHash = (0, bitcoin_networks_1.chainHashFromNetwork)(bitcoin_networks_1.BitcoinNetworks[network]);
orderOffer.contractInfo = contractInfo;
orderOffer.offerCollateral = offerCollateral; // Use correct property name
orderOffer.feeRatePerVb = feePerByte;
orderOffer.cetLocktime = Math.floor(new Date().getTime() / 1000); // set to current time
orderOffer.refundLocktime =
announcement.oracleEvent.eventMaturityEpoch + 2419200; // 4 weeks after maturity
return orderOffer;
};
exports.buildOrderOffer = buildOrderOffer;
/**
* Builds an order offer for a covered call or short put
*
* @param {OracleAnnouncement} announcement oracle announcement
* @param {number} contractSize contract size in satoshis
* @param {number} strikePrice strike price of contract
* @param {number} premium premium of contract in satoshis
* @param {number | bigint} feePerByte sats/vbyte
* @param {number} rounding rounding interval
* @param {string} network bitcoin network type
* @param {string} type call or put
* @param {number} _totalCollateral total collateral in satoshis (applicable only for short put)
* @returns {OrderOffer} Returns order offer
*/
const buildOptionOrderOffer = (announcement, contractSize, strikePrice, premium, feePerByte, rounding, network, type, direction, _totalCollateral) => {
const eventDescriptor = (0, exports.getDigitDecompositionEventDescriptor)(announcement);
let totalCollateral;
let payoutFunctionInfo;
const roundingIntervals = new messaging_1.RoundingIntervals();
const roundingMod = (0, exports.computeRoundingModulus)(rounding, contractSize);
if (direction === 'short') {
if (type === 'call') {
payoutFunctionInfo = CoveredCall_1.CoveredCall.buildPayoutFunction(BigInt(strikePrice), contractSize.sats, eventDescriptor.base, eventDescriptor.nbDigits);
totalCollateral = payoutFunctionInfo.totalCollateral;
roundingIntervals.intervals = [
{
beginInterval: BigInt(0),
roundingMod: BigInt(1),
},
{
beginInterval: BigInt(strikePrice),
roundingMod,
},
];
}
else {
payoutFunctionInfo = ShortPut_1.ShortPut.buildPayoutFunction(BigInt(strikePrice), contractSize.sats, _totalCollateral.sats, eventDescriptor.base, eventDescriptor.nbDigits);
totalCollateral = _totalCollateral.sats;
roundingIntervals.intervals = [
{
beginInterval: BigInt(0),
roundingMod,
},
{
beginInterval: BigInt(strikePrice),
roundingMod: BigInt(1),
},
];
}
}
else {
totalCollateral = _totalCollateral.sats;
if (type === 'call') {
payoutFunctionInfo = LongCall_1.LongCall.buildPayoutFunction(BigInt(strikePrice), contractSize.sats, totalCollateral, eventDescriptor.base, eventDescriptor.nbDigits);
roundingIntervals.intervals = [
{
beginInterval: BigInt(0),
roundingMod: BigInt(1),
},
{
beginInterval: BigInt(strikePrice),
roundingMod,
},
];
}
else {
payoutFunctionInfo = LongPut_1.LongPut.buildPayoutFunction(BigInt(strikePrice), contractSize.sats, totalCollateral, eventDescriptor.base, eventDescriptor.nbDigits);
roundingIntervals.intervals = [
{
beginInterval: BigInt(0),
roundingMod,
},
{
beginInterval: BigInt(strikePrice),
roundingMod: BigInt(1),
},
];
}
}
const payoutFunction = payoutFunctionInfo.payoutFunction;
const offerCollateral = direction === 'short' ? totalCollateral - premium.sats : premium.sats;
return (0, exports.buildOrderOffer)(announcement, totalCollateral, offerCollateral, payoutFunction, roundingIntervals, BigInt(feePerByte), network);
};
exports.buildOptionOrderOffer = buildOptionOrderOffer;
/**
* Builds an order offer for a covered call
*
* @param {OracleAnnouncement} announcement oracle announcement
* @param {number} contractSize contract size in satoshis
* @param {number} strikePrice strike price of contract
* @param {number} premium premium of contract in satoshis
* @param {number} feePerByte sats/vbyte
* @param {number} rounding rounding interval
* @param {string} network bitcoin network type
* @returns {OrderOffer} Returns order offer
*/
const buildCoveredCallOrderOffer = (announcement, contractSize, strikePrice, premium, feePerByte, rounding, network) => {
return (0, exports.buildOptionOrderOffer)(announcement, contractSize, strikePrice, premium, feePerByte, rounding, network, 'call', 'short');
};
exports.buildCoveredCallOrderOffer = buildCoveredCallOrderOffer;
/**
* Builds an order offer for a short put
*
* @param {OracleAnnouncement} announcement oracle announcement
* @param {number} contractSize contract size in satoshis
* @param {number} strikePrice strike price of contract
* @param {number} totalCollateral total collateral in satoshis
* @param {number} premium premium of contract in satoshis
* @param {number} feePerByte sats/vbyte
* @param {number} rounding rounding interval
* @param {string} network bitcoin network type
* @returns {OrderOffer} Returns order offer
*/
const buildShortPutOrderOffer = (announcement, contractSize, strikePrice, totalCollateral, premium, feePerByte, rounding, network) => {
return (0, exports.buildOptionOrderOffer)(announcement, contractSize, strikePrice, premium, feePerByte, rounding, network, 'put', 'short', totalCollateral);
};
exports.buildShortPutOrderOffer = buildShortPutOrderOffer;
/**
* Builds an order offer for a long call
*
* @param {OracleAnnouncement} announcement oracle announcement
* @param {number} contractSize contract size in satoshis
* @param {number} strikePrice strike price of contract
* @param {number} maxGain maximum amount that can be gained (totalCollateral)
* @param {number} premium premium of contract in satoshis
* @param {number} feePerByte sats/vbyte
* @param {number} rounding rounding interval
* @param {string} network bitcoin network type
* @returns {OrderOffer} Returns order offer
*/
const buildLongCallOrderOffer = (announcement, contractSize, strikePrice, maxGain, premium, feePerByte, rounding, network) => {
return (0, exports.buildOptionOrderOffer)(announcement, contractSize, strikePrice, premium, feePerByte, rounding, network, 'call', 'long', maxGain);
};
exports.buildLongCallOrderOffer = buildLongCallOrderOffer;
/**
* Builds an order offer for a long put
*
* @param {OracleAnnouncement} announcement oracle announcement
* @param {number} contractSize contract size in satoshis
* @param {number} strikePrice strike price of contract
* @param {number} maxGain maximum amount that can be gained (totalCollateral)
* @param {number} premium premium of contract in satoshis
* @param {number} feePerByte sats/vbyte
* @param {number} rounding rounding interval
* @param {string} network bitcoin network type
* @returns {OrderOffer} Returns order offer
*/
const buildLongPutOrderOffer = (announcement, contractSize, strikePrice, maxGain, premium, feePerByte, rounding, network) => {
return (0, exports.buildOptionOrderOffer)(announcement, contractSize, strikePrice, premium, feePerByte, rounding, network, 'put', 'long', maxGain);
};
exports.buildLongPutOrderOffer = buildLongPutOrderOffer;
/**
* Builds an order offer for a linear curve
*
* @param {OracleAnnouncement} announcement oracle announcement
* @param {Value} offerCollateral offer collateral amount
* @param {Value} minPayout minimum payout
* @param {Value} maxPayout maximum payout (also total collateral)
* @param {bigint} startOutcome oracle outcome (in bits or sats)
* @param {bigint} endOutcome oracle outcome (in bits or sats)
* @param {bigint} feePerByte sats/vbyte
* @param {Value} rounding rounding mod for RoundingInterval
* @param {BitcoinNetwork} network bitcoin, bitcoin_testnet or bitcoin_regtest
* @param {DlcParty} [shiftForFees] shift for offerer, acceptor or neither (who should pay fees)
* @param {Value} [fees] fees to shift
* @returns {OrderOffer}
*/
const buildLinearOrderOffer = (announcement, offerCollateral, minPayout, maxPayout, startOutcome, endOutcome, feePerByte, roundingIntervals, network, shiftForFees = 'neither', fees = bitcoin_1.Value.fromSats(0)) => {
if (maxPayout.lt(minPayout))
throw Error('maxPayout must be greater than minPayout');
if (endOutcome < startOutcome)
throw Error('endOutcome must be greater than startOutcome');
const eventDescriptor = (0, exports.getDigitDecompositionEventDescriptor)(announcement);
const totalCollateral = maxPayout.sats;
const { payoutFunction } = LinearPayout_1.LinearPayout.buildPayoutFunction(minPayout.sats, maxPayout.sats, startOutcome, endOutcome, eventDescriptor.base, eventDescriptor.nbDigits);
const orderOffer = (0, exports.buildOrderOffer)(announcement, totalCollateral, offerCollateral.sats, payoutFunction, roundingIntervals, feePerByte, network.name);
const positionInfo = new messaging_1.OrderPositionInfo();
positionInfo.shiftForFees = shiftForFees;
positionInfo.fees = shiftForFees === 'neither' ? BigInt(0) : fees.sats;
orderOffer.positionInfo = positionInfo;
return orderOffer;
};
exports.buildLinearOrderOffer = buildLinearOrderOffer;
/**
* Builds a custom strategy order offer
*
* Calculates offer fees
* calculates the minimum max gain
* maxLoss and maxGain are normalized to 1 BTC contracts
*
* shiftForFees 'offeror' means 'offeror' pays network fees
* shiftForFees 'acceptor' means 'acceptor' pays network fees
*
* numContracts refers to the number of DLCs in the funding transaction
* if it's not a batch dlc funding transaction, then this is not relevant
*
* @param {OracleAnnouncement} announcement oracle announcement
* @param {Value} contractSize contract size
* @param {Value} normalizedMaxLoss maximum amount that can be lost based on 1 BTC contract
* @param {Value} normalizedMaxGain maximum amount that can be gained based on 1 BTC contract
* @param {bigint} feePerByte sats/vbyte
* @param {Value} roundingIntervals rounding mod for RoundingInterval
* @param {BitcoinNetwork} network bitcoin, bitcoin_testnet or bitcoin_regtest
* @param {DlcParty} [shiftForFees] shift for offerer, acceptor or neither
* @param {Value} [fees_] fees to shift
* @param {Value} [collateral] collateral to use
* @param {number} [numOfferInputs] number of inputs to use
* @param {number} [numContracts] number of DLCs in the funding transaction
*
* @returns {OrderOffer}
*/
const buildCustomStrategyOrderOffer = (announcement, contractSize, normalizedMaxLoss, normalizedMaxGain, feePerByte, roundingIntervals, network, shiftForFees = 'neither', fees_ = bitcoin_1.Value.fromSats(0), // NOTE: fees should be divided before doing batch transaction
collateral = bitcoin_1.Value.fromSats(0), numOfferInputs = 1, numContracts = 1, skipValidation = false) => {
if (contractSize.eq(bitcoin_1.Value.zero())) {
throw Error('contractSize must be greater than 0');
}
if (collateral.eq(bitcoin_1.Value.zero())) {
collateral = contractSize.clone();
}
const eventDescriptor = (0, exports.getDigitDecompositionEventDescriptor)(announcement);
const unit = eventDescriptor.unit;
const fees = bitcoin_1.Value.fromSats(Number(new decimal_js_1.default(Number(fees_.sats)).dividedBy(numContracts).toFixed(0)));
const finalizer = (0, TxFinalizer_1.getFinalizerByCount)(feePerByte, numOfferInputs, 1, numContracts);
const offerFees = bitcoin_1.Value.fromSats(finalizer.offerFees);
// Use offerFees + dustThreshold for min max gain, to ensure in the case of
// 0 PnL the acceptor payout is not dust
const minMaxGainForContractSize_ = offerFees.addn(bitcoin_1.Value.fromSats((0, CoinSelect_1.dustThreshold)(BigInt(feePerByte))));
const minMaxGainForContractSize = bitcoin_1.Value.fromSats((0, exports.roundUpToNearestMultiplier)(minMaxGainForContractSize_.sats, BigInt(exports.UNIT_MULTIPLIER[unit.toLowerCase()])));
const maxLossForContractSize = bitcoin_1.Value.fromSats((0, exports.roundUpToNearestMultiplier)((normalizedMaxLoss.sats * contractSize.sats) / BigInt(1e8), BigInt(exports.UNIT_MULTIPLIER[unit.toLowerCase()])));
const tempMaxGainForContractSize = bitcoin_1.Value.fromSats((0, exports.roundUpToNearestMultiplier)((normalizedMaxGain.sats * contractSize.sats) / BigInt(1e8), BigInt(exports.UNIT_MULTIPLIER[unit.toLowerCase()])));
const maxGainForContractSize = bitcoin_1.Value.zero();
if (tempMaxGainForContractSize.lt(minMaxGainForContractSize)) {
maxGainForContractSize.add(minMaxGainForContractSize);
}
else {
maxGainForContractSize.add(tempMaxGainForContractSize);
}
const maxGainAdjusted = bitcoin_1.Value.fromSats((0, exports.roundToNearestMultiplier)((maxGainForContractSize.sats * BigInt(1e8)) / contractSize.sats, BigInt(exports.UNIT_MULTIPLIER[unit.toLowerCase()])));
const feesAdjusted = bitcoin_1.Value.fromSats((0, exports.roundToNearestMultiplier)((fees.sats * BigInt(1e8)) / contractSize.sats, BigInt(exports.UNIT_MULTIPLIER[unit.toLowerCase()])));
const minPayout = collateral.clone();
if (minPayout.lt(maxLossForContractSize.addn(maxGainForContractSize))) {
throw new Error('Subtraction would result in a negative value for minPayout.');
}
minPayout.sub(maxLossForContractSize);
minPayout.sub(maxGainForContractSize);
const maxPayout = collateral.clone();
const startOutcomeValue = bitcoin_1.Value.fromBitcoin(1);
startOutcomeValue.sub(normalizedMaxLoss);
const endOutcomeValue = bitcoin_1.Value.fromBitcoin(1);
endOutcomeValue.add(maxGainAdjusted);
if (shiftForFees === 'offeror') {
startOutcomeValue.add(feesAdjusted);
endOutcomeValue.add(feesAdjusted);
}
else if (shiftForFees === 'acceptor') {
startOutcomeValue.sub(feesAdjusted);
endOutcomeValue.sub(feesAdjusted);
}
const startOutcome = startOutcomeValue.sats / BigInt(exports.UNIT_MULTIPLIER[unit.toLowerCase()]);
const endOutcome = endOutcomeValue.sats / BigInt(exports.UNIT_MULTIPLIER[unit.toLowerCase()]);
const offerCollateral = collateral.clone();
if (offerCollateral.lt(maxGainForContractSize)) {
throw new Error('Subtraction would result in a negative value for offerCollateral.');
}
offerCollateral.sub(maxGainForContractSize);
const orderOffer = (0, exports.buildLinearOrderOffer)(announcement, offerCollateral, minPayout, maxPayout, startOutcome, endOutcome, feePerByte, roundingIntervals, network, shiftForFees, fees);
orderOffer.positionInfo.contractSize =
contractSize.sats;
orderOffer.positionInfo.instrumentName = orderOffer.contractInfo
.oracleInfo.announcement.oracleEvent.eventId;
if (!skipValidation)
orderOffer.validate();
return orderOffer;
};
exports.buildCustomStrategyOrderOffer = buildCustomStrategyOrderOffer;
const buildRoundingIntervalsFromIntervals = (contractSize, intervals) => {
const roundingIntervals = new messaging_1.RoundingIntervals();
roundingIntervals.intervals = intervals.map((interval) => {
const roundingMod = (0, exports.computeRoundingModulus)(interval.rounding, contractSize);
return {
beginInterval: interval.beginInterval,
roundingMod,
};
});
return roundingIntervals;
};
exports.buildRoundingIntervalsFromIntervals = buildRoundingIntervalsFromIntervals;
//# sourceMappingURL=Builder.js.map