UNPKG

@node-dlc/core

Version:
422 lines 22.1 kB
"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