UNPKG

@node-dlc/core

Version:
267 lines (231 loc) 8.46 kB
import { MessageType, PayoutCurvePieceType, PayoutFunction, PolynomialPayoutCurvePiece, RoundingIntervals, } from '@node-dlc/messaging'; import BigNumber from 'bignumber.js'; import { fromPrecision, getPrecision } from '../utils/Precision'; import { CETPayout, mergePayouts, splitIntoRanges } from './CETCalculator'; interface DlcPoint { outcome: BigNumber; payout: BigNumber; } export class PolynomialPayoutCurve { private readonly points: DlcPoint[]; private readonly slope: BigNumber; private readonly left: DlcPoint; private readonly right: DlcPoint; constructor(points: DlcPoint[]) { if (points.length !== 2) throw new Error('Must have two points'); this.points = points; this.left = points[0]; this.right = points[points.length - 1]; // m = (y2 - y1) / (x2 - x1) this.slope = this.right.payout .minus(this.left.payout) .dividedBy(this.right.outcome.minus(this.left.outcome)); } /** * Get the payout for a given outcome * @param outcome The outcome to get the payout for * @returns The payout for the outcome */ getPayout(outcome: bigint): BigNumber { const { left, slope } = this; const x = new BigNumber(Number(outcome)); // y = mx + b const payout = slope.times(x.minus(left.outcome)).plus(left.payout); return payout; } /** * Get the outcome for a given payout * @param payout The payout to get the outcome for * @returns The outcome for the payout */ getOutcomeForPayout(payout: BigNumber): bigint { const { left, right, slope } = this; const y = new BigNumber(Number(payout)); // Handle constant payout curve (slope = 0) if (slope.isZero()) { // For constant curves, if the payout matches, return any outcome in the range // If it doesn't match, this is an invalid request if (y.eq(left.payout)) { return BigInt(left.outcome.toString()); } else { // For constant curves, if payout doesn't match, return left outcome // This prevents Infinity errors in calculations return BigInt(left.outcome.toString()); } } // Find the x value for the given y // slope = (y2 - y1) / (x2 - x1) // x1 = (y2 - y1) / slope + x2 const outcome = y .minus(left.payout) .dividedBy(slope) .plus(left.outcome) .integerValue(); // Clamp the outcome to the valid range const clampedOutcome = BigNumber.max( left.outcome, BigNumber.min(outcome, right.outcome), ); return BigInt(clampedOutcome.toString()); } /** * Serializes PolynomialPayoutCurve to a PolynomialPayoutCurvePiece (for transport) * @returns A PolynomialPayoutCurvePiece */ toPayoutCurvePiece(): PolynomialPayoutCurvePiece { const { points } = this; const piece = new PolynomialPayoutCurvePiece(); piece.points = points.map((point) => { const eventOutcome = BigInt(point.outcome.toString()); const outcomePayout = BigInt(point.payout.toString()); const extraPrecision = getPrecision(point.payout); return { eventOutcome, outcomePayout, extraPrecision }; }); return piece; } /** * Determine if the payout curve is equal to another * @param curve A PolynomialPayoutCurve * @returns True if the curves are the same */ equals(curve: PolynomialPayoutCurve): boolean { return this.points.every((point, i) => { const otherPoint = curve.points[i]; return ( point.outcome.eq(otherPoint.outcome) && point.payout.eq(otherPoint.payout) ); }); } /** * Creates a PolynomialPayoutCurve from a PolynomialPayoutCurvePiece * @param piece * @returns A PolynomialPayoutCurve */ static fromPayoutCurvePiece( piece: PolynomialPayoutCurvePiece, ): PolynomialPayoutCurve { const points = piece.points.map((point) => { const outcome = new BigNumber(point.eventOutcome.toString()); const payout = new BigNumber(point.outcomePayout.toString()).plus( fromPrecision(point.extraPrecision), ); return { outcome, payout }; }); return new PolynomialPayoutCurve(points); } /** * Computes all CETs for a given payout curve * @param payoutFunction The payout function * @param totalCollateral The total collateral * @param roundingIntervals The rounding intervals * @returns A list of CETs */ static computePayouts( payoutFunction: PayoutFunction, totalCollateral: bigint, roundingIntervals: RoundingIntervals, ): CETPayout[] { if (payoutFunction.payoutFunctionPieces.length < 1) throw new Error('Must have at least one piece'); payoutFunction.payoutFunctionPieces.forEach((piece) => { if ( piece.payoutCurvePiece.payoutCurvePieceType !== PayoutCurvePieceType.Polynomial && piece.payoutCurvePiece.type !== MessageType.PolynomialPayoutCurvePiece ) throw new Error('Payout curve piece must be a polynomial'); }); const CETS: CETPayout[] = []; // 1. Add the first piece to the list const { payoutCurvePiece } = payoutFunction.payoutFunctionPieces[0]; const curve = this.fromPayoutCurvePiece( payoutCurvePiece as PolynomialPayoutCurvePiece, ); // For the first piece, start from 0 and go to the first endpoint const firstPiece = payoutFunction.payoutFunctionPieces[0]; // Calculate the start payout by evaluating the curve at outcome 0 const startPayout = curve.getPayout(BigInt(0)); const startPayoutBigInt = BigInt(startPayout.integerValue().toString()); // Only add ranges if there's actually a range to cover if (firstPiece.endPoint.eventOutcome > 0) { CETS.push( ...splitIntoRanges( BigInt(0), // Start from 0 firstPiece.endPoint.eventOutcome, startPayoutBigInt, // Start payout calculated from curve firstPiece.endPoint.outcomePayout, // End payout from endpoint totalCollateral, curve, roundingIntervals.intervals, ), ); } // 2. If there are subsequent pieces, add them to the list for (let i = 1; i < payoutFunction.payoutFunctionPieces.length; i++) { const { payoutCurvePiece } = payoutFunction.payoutFunctionPieces[i]; const curve = this.fromPayoutCurvePiece( payoutCurvePiece as PolynomialPayoutCurvePiece, ); CETS.push( ...splitIntoRanges( payoutFunction.payoutFunctionPieces[i - 1].endPoint.eventOutcome, payoutFunction.payoutFunctionPieces[i].endPoint.eventOutcome, payoutFunction.payoutFunctionPieces[i - 1].endPoint.outcomePayout, payoutFunction.payoutFunctionPieces[i].endPoint.outcomePayout, totalCollateral, curve, roundingIntervals.intervals, ), ); } // 3. Handle the final range from the last piece to lastEndpoint if it exists if ( payoutFunction.lastEndpoint && payoutFunction.payoutFunctionPieces.length > 0 ) { const lastPieceIndex = payoutFunction.payoutFunctionPieces.length - 1; const lastPiece = payoutFunction.payoutFunctionPieces[lastPieceIndex]; // Check if there's a range to cover if ( payoutFunction.lastEndpoint.eventOutcome > lastPiece.endPoint.eventOutcome ) { // For the final range, we'll assume a constant payout from the last piece endpoint to lastEndpoint // This is a common pattern for DLC payout functions const finalCurve = new PolynomialPayoutCurve([ { outcome: new BigNumber(lastPiece.endPoint.eventOutcome.toString()), payout: new BigNumber(lastPiece.endPoint.outcomePayout.toString()), }, { outcome: new BigNumber( payoutFunction.lastEndpoint.eventOutcome.toString(), ), payout: new BigNumber( payoutFunction.lastEndpoint.outcomePayout.toString(), ), }, ]); CETS.push( ...splitIntoRanges( lastPiece.endPoint.eventOutcome, payoutFunction.lastEndpoint.eventOutcome, lastPiece.endPoint.outcomePayout, payoutFunction.lastEndpoint.outcomePayout, totalCollateral, finalCurve, roundingIntervals.intervals, ), ); } } return mergePayouts(CETS); } }