@node-dlc/core
Version:
451 lines (385 loc) • 12.5 kB
text/typescript
import BigNumber from 'bignumber.js';
import { BigIntMath, toBigInt } from '../utils/BigIntUtils';
import { HyperbolaPayoutCurve } from './HyperbolaPayoutCurve';
import { PolynomialPayoutCurve } from './PolynomialPayoutCurve';
export function zipWithIndex<T>(arr: T[]): [T, number][] {
return arr.map((a, i) => [a, i]);
}
export function dropUntil<T>(data: T[], check: (_: T) => boolean): T[] {
for (let i = 0; i < data.length; i++) {
if (check(data[i])) {
return data.slice(i);
}
}
return [];
}
export function decompose(
num: bigint,
base: number,
numDigits: number,
): number[] {
let currentNumber = num;
const digits = [];
while (numDigits > 0) {
digits.push(Number(currentNumber % BigInt(base)));
currentNumber = currentNumber / BigInt(base);
numDigits--;
}
return digits.reverse();
}
export function separatePrefix(
start: bigint,
end: bigint,
base: number,
numDigits: number,
): {
prefixDigits: number[];
startDigits: number[];
endDigits: number[];
} {
const startDigits = decompose(start, base, numDigits);
const endDigits = decompose(end, base, numDigits);
const prefixDigits: number[] = [];
for (let i = 0; i < startDigits.length; i++) {
if (startDigits[i] === endDigits[i]) {
prefixDigits.push(startDigits[i]);
} else break;
}
return {
prefixDigits,
startDigits: startDigits.splice(prefixDigits.length),
endDigits: endDigits.splice(prefixDigits.length),
};
}
export function frontGroupings(digits: number[], base: number): number[][] {
const digitsReversed = Array.from(digits).reverse();
const nonZeroDigits = dropUntil(
zipWithIndex(digitsReversed),
(a) => a[0] !== 0,
);
if (nonZeroDigits.length === 0) {
return [[0]];
}
const fromFront = nonZeroDigits
.filter((_, i) => i !== nonZeroDigits.length - 1)
.flatMap(([d, i]) => {
const fixedDigits = Array.from(digits);
fixedDigits.length = fixedDigits.length - (i + 1);
const result: number[][] = [];
for (let n = d + 1; n < base; n++) {
result.push([...Array.from(fixedDigits), n]);
}
return result;
});
return [nonZeroDigits.map((a) => a[0]).reverse(), ...fromFront];
}
export function backGroupings(digits: number[], base: number): number[][] {
const digitsReversed = Array.from(digits).reverse();
const nonMaxDigits = dropUntil(
zipWithIndex(digitsReversed),
(a) => a[0] !== base - 1,
);
if (nonMaxDigits.length === 0) {
return [[base - 1]];
}
const fromBack = nonMaxDigits
.filter((_, i) => i !== nonMaxDigits.length - 1)
.flatMap(([d, i]) => {
const fixedDigits = Array.from(digits);
fixedDigits.length = fixedDigits.length - (i + 1);
const result: number[][] = [];
for (let n = d - 1; n >= 0; n--) {
result.push([...Array.from(fixedDigits), n]);
}
return result;
});
return [...fromBack.reverse(), nonMaxDigits.map((a) => a[0]).reverse()];
}
export function middleGroupings(
firstDigitStart: number,
firstDigitEnd: number,
): number[][] {
const result = [];
while (++firstDigitStart < firstDigitEnd) {
result.push([firstDigitStart]);
}
return result;
}
export function groupByIgnoringDigits(
start: bigint,
end: bigint,
base: number,
numDigits: number,
): number[][] {
const { prefixDigits, startDigits, endDigits } = separatePrefix(
start,
end,
base,
numDigits,
);
if (
start === end ||
(startDigits.every((n) => n === 0) &&
endDigits.every((n) => n === base - 1) &&
prefixDigits.length !== 0)
) {
return [prefixDigits];
} else if (prefixDigits.length === numDigits - 1) {
const result = [];
for (
let i = startDigits[startDigits.length - 1];
i <= endDigits[endDigits.length - 1];
i++
) {
result.push([...prefixDigits, i]);
}
return result;
} else {
const front = frontGroupings(startDigits, base);
const middle = middleGroupings(startDigits[0], endDigits[0]);
const back = backGroupings(endDigits, base);
const groupings = [...front, ...middle, ...back];
return groupings.map((g) => [...prefixDigits, ...g]);
}
}
export interface RoundingInterval {
beginInterval: bigint;
roundingMod: bigint;
}
export type CETPayout = {
indexFrom: bigint;
indexTo: bigint;
payout: bigint;
};
/**
* Performs optimized evaluation and rounding for strictly monotonic hyperbolas on intervals (from, to)
* e.g. hyperbolas with the form b = c = 0
*
* The next start of a payout range is determined by finding the outcome at the next mid-rounding payout.
* Uses an inverse function of the hyperbola to find the outcome.
*
* Optimizes rounding from O(to - from) to O(totalCollateral / rounding)
*
*
* Evaluates and rounds a payout_function equivalent to:
*
* payout_function_v0
* num_pieces: 1
* endpoint_0: from
* endpoint_payout_0: fromPayout
* extra_precision_0: 0
* payout_curve_piece: HyperbolaPayoutCurve
* endpoint_1: to
* endpoint_payout_1: toPayout
*/
export function splitIntoRanges(
from: bigint,
to: bigint,
fromPayout: bigint, // endpoint_payout
toPayout: bigint, // endpoint_payout
totalCollateral: bigint,
curve: HyperbolaPayoutCurve | PolynomialPayoutCurve,
roundingIntervals: RoundingInterval[],
): CETPayout[] {
if (to - from <= 0) {
throw new Error('`to` must be strictly greater than `from`');
}
const reversedIntervals = [...roundingIntervals].reverse();
const getRoundingForOutcome = (outcome: bigint): [bigint, number] => {
const roundingIndex = reversedIntervals.findIndex(
(interval) => interval.beginInterval <= outcome,
);
return [
roundingIndex !== -1
? reversedIntervals[roundingIndex].roundingMod
: BigInt(1),
roundingIndex,
];
};
const clamp = (val: bigint) =>
BigIntMath.clamp(BigInt(0), val, totalCollateral);
const totalCollateralBN = new BigNumber(totalCollateral.toString());
const clampBN = (val: BigNumber) =>
BigNumber.max(0, BigNumber.min(val, totalCollateralBN));
const result: CETPayout[] = [];
// outcome = endpoint_0
result.push({
payout: fromPayout,
indexFrom: from,
indexTo: from,
});
// In the case of a constant payout curve (fromPayout === toPayout), we can skip the range evaluation
if (fromPayout === toPayout) {
result.push({
payout: toPayout,
indexFrom: from,
indexTo: to,
});
// outcome = endpoint_1
result.push({
payout: toPayout,
indexFrom: to,
indexTo: to,
});
// merge neighbouring ranges with same payout
return mergePayouts(result);
}
let currentOutcome = from + BigInt(1);
// iterate over entire range of outcomes from [from, to]
while (currentOutcome < to) {
const [rounding, roundingIndex] = getRoundingForOutcome(currentOutcome);
// either the next rounding interval, or the end of the range
const nextFirstRoundingOutcome =
reversedIntervals[roundingIndex - 1]?.beginInterval || to;
// temporary variable to hold the current payout
let currentPayout = new BigNumber(
roundPayout(
clampBN(curve.getPayout(currentOutcome)),
rounding,
).toString(),
);
let currentMidRoundedOutcome = currentOutcome;
const isAscending = curve
.getPayout(nextFirstRoundingOutcome)
.gt(currentPayout);
// Add loop counter to prevent infinite loops
let loopCounter = 0;
const maxIterations = Number(to - from) + 1000; // Allow some extra iterations
while (currentMidRoundedOutcome < nextFirstRoundingOutcome) {
// Prevent infinite loops
if (++loopCounter > maxIterations) {
// Breaking out of potential infinite loop - add remaining range and exit
result.push({
payout: clamp(toBigInt(currentPayout)),
indexFrom: currentMidRoundedOutcome,
indexTo: to - BigInt(1),
});
currentOutcome = to;
break;
}
const nextRoundedPayout = currentPayout
.integerValue()
.plus(isAscending ? Number(rounding) : -Number(rounding));
const nextRoundedPayoutBigInt = toBigInt(nextRoundedPayout);
const nextMidRoundedPayout = currentPayout.plus(
isAscending ? Number(rounding) / 2 : -Number(rounding) / 2,
);
let nextMidRoundedOutcome = curve.getOutcomeForPayout(
nextMidRoundedPayout,
);
// Handle invalid outcomes from getOutcomeForPayout
if (nextMidRoundedOutcome < 0) {
// If getOutcomeForPayout returns invalid value, advance manually
nextMidRoundedOutcome = currentMidRoundedOutcome + BigInt(1);
}
if (
(!isAscending &&
nextMidRoundedOutcome >= 0 &&
curve.getPayout(nextMidRoundedOutcome).lt(nextMidRoundedPayout)) ||
(isAscending &&
nextMidRoundedOutcome >= 0 &&
curve.getPayout(nextMidRoundedOutcome).gte(nextMidRoundedPayout))
) {
nextMidRoundedOutcome = nextMidRoundedOutcome - BigInt(1);
// Ensure we don't go negative
if (nextMidRoundedOutcome < 0) {
nextMidRoundedOutcome = currentMidRoundedOutcome;
}
}
const nextOutcome = curve.getOutcomeForPayout(nextRoundedPayout);
if (nextMidRoundedOutcome >= nextFirstRoundingOutcome) {
result.push({
payout: clamp(toBigInt(currentPayout)),
indexFrom: currentMidRoundedOutcome,
indexTo: nextFirstRoundingOutcome - BigInt(1),
});
currentOutcome = nextFirstRoundingOutcome;
break;
}
if (
nextRoundedPayoutBigInt > totalCollateral ||
nextRoundedPayoutBigInt < 0 ||
nextOutcome >= to ||
nextOutcome < 0 // undefined on curve
) {
if (nextMidRoundedOutcome < from || nextMidRoundedOutcome > to) {
result.push({
payout: clamp(toBigInt(currentPayout)),
indexFrom: currentMidRoundedOutcome,
indexTo: to,
});
} else {
result.push(
{
payout: clamp(toBigInt(currentPayout)),
indexFrom: currentMidRoundedOutcome,
indexTo: nextMidRoundedOutcome,
},
{
payout: clamp(nextRoundedPayoutBigInt),
indexFrom: nextMidRoundedOutcome + BigInt(1),
indexTo: to - BigInt(1),
},
);
}
currentOutcome = to;
break;
}
result.push({
payout: clamp(toBigInt(currentPayout)),
indexFrom: currentMidRoundedOutcome,
indexTo: nextMidRoundedOutcome,
});
// Handle case where nextOutcome is invalid
if (nextOutcome < 0) {
// Advance manually if getOutcomeForPayout returns invalid value
currentOutcome = currentMidRoundedOutcome + BigInt(1);
} else {
currentOutcome = nextOutcome + BigInt(1);
}
currentPayout = nextRoundedPayout;
// Additional safety check: ensure we're making progress
const previousMidRoundedOutcome = currentMidRoundedOutcome;
currentMidRoundedOutcome = nextMidRoundedOutcome + BigInt(1);
if (currentMidRoundedOutcome <= previousMidRoundedOutcome) {
// No progress detected in splitIntoRanges loop, breaking
currentOutcome = nextFirstRoundingOutcome;
break;
}
}
}
// outcome = endpoint_1
result.push({
payout: toPayout,
indexFrom: to,
indexTo: to,
});
// merge neighbouring ranges with same payout
return mergePayouts(result);
}
export const mergePayouts = (payouts: CETPayout[]): CETPayout[] => {
return payouts.reduce((acc: CETPayout[], range) => {
const prev = acc[acc.length - 1];
if (prev) {
if (
(prev.indexTo === range.indexFrom ||
prev.indexTo + BigInt(1) === range.indexFrom) &&
prev.payout === range.payout
) {
prev.indexTo = range.indexTo;
return acc;
}
}
return [...acc, range];
}, []);
};
export function roundPayout(payout: BigNumber, rounding: bigint): bigint {
const roundingBN = new BigNumber(rounding.toString());
const mod = payout.gte(0)
? payout.mod(roundingBN)
: payout.mod(roundingBN).plus(roundingBN);
const roundedPayout = mod.gte(roundingBN.dividedBy(2))
? payout.plus(roundingBN).minus(mod)
: payout.minus(mod);
return toBigInt(roundedPayout);
}