@ox-fun/drift-sdk
Version:
SDK for Drift Protocol
661 lines (527 loc) • 17 kB
text/typescript
import { BN } from '@coral-xyz/anchor';
import { assert } from '../assert/assert';
import { ZERO } from './../constants/numericConstants';
export class BigNum {
val: BN;
precision: BN;
static delim = '.';
static spacer = ',';
public static setLocale(locale: string): void {
BigNum.delim = (1.1).toLocaleString(locale).slice(1, 2) || '.';
BigNum.spacer = (1000).toLocaleString(locale).slice(1, 2) || ',';
}
constructor(
val: BN | number | string,
precisionVal: BN | number | string = new BN(0)
) {
this.val = new BN(val);
this.precision = new BN(precisionVal);
}
private bigNumFromParam(bn: BigNum | BN) {
return BN.isBN(bn) ? BigNum.from(bn) : bn;
}
public add(bn: BigNum): BigNum {
assert(bn.precision.eq(this.precision), 'Adding unequal precisions');
return BigNum.from(this.val.add(bn.val), this.precision);
}
public sub(bn: BigNum): BigNum {
assert(bn.precision.eq(this.precision), 'Subtracting unequal precisions');
return BigNum.from(this.val.sub(bn.val), this.precision);
}
public mul(bn: BigNum | BN): BigNum {
const mulVal = this.bigNumFromParam(bn);
return BigNum.from(
this.val.mul(mulVal.val),
this.precision.add(mulVal.precision)
);
}
/**
* Multiplies by another big number then scales the result down by the big number's precision so that we're in the same precision space
* @param bn
* @returns
*/
public scalarMul(bn: BigNum | BN): BigNum {
if (BN.isBN(bn)) return BigNum.from(this.val.mul(bn), this.precision);
return BigNum.from(
this.val.mul(bn.val),
this.precision.add(bn.precision)
).shift(bn.precision.neg());
}
public div(bn: BigNum | BN): BigNum {
if (BN.isBN(bn)) return BigNum.from(this.val.div(bn), this.precision);
return BigNum.from(this.val.div(bn.val), this.precision.sub(bn.precision));
}
/**
* Shift precision up or down
* @param exponent
* @param skipAdjustingPrecision
* @returns
*/
public shift(exponent: BN | number, skipAdjustingPrecision = false): BigNum {
const shiftVal = typeof exponent === 'number' ? new BN(exponent) : exponent;
return BigNum.from(
shiftVal.isNeg()
? this.val.div(new BN(10).pow(shiftVal))
: this.val.mul(new BN(10).pow(shiftVal)),
skipAdjustingPrecision ? this.precision : this.precision.add(shiftVal)
);
}
/**
* Shift to a target precision
* @param targetPrecision
* @returns
*/
public shiftTo(targetPrecision: BN): BigNum {
return this.shift(targetPrecision.sub(this.precision));
}
/**
* Scale the number by a fraction
* @param numerator
* @param denominator
* @returns
*/
public scale(numerator: BN | number, denominator: BN | number): BigNum {
return this.mul(BigNum.from(new BN(numerator))).div(new BN(denominator));
}
public toPercentage(denominator: BigNum, precision: number): string {
return this.shift(precision)
.shift(2, true)
.div(denominator)
.toPrecision(precision);
}
public gt(bn: BigNum | BN, ignorePrecision?: boolean): boolean {
const comparisonVal = this.bigNumFromParam(bn);
if (!ignorePrecision && !comparisonVal.eq(ZERO)) {
assert(
comparisonVal.precision.eq(this.precision),
'Trying to compare numbers with different precision. Yo can opt to ignore precision using the ignorePrecision parameter'
);
}
return this.val.gt(comparisonVal.val);
}
public lt(bn: BigNum | BN, ignorePrecision?: boolean): boolean {
const comparisonVal = this.bigNumFromParam(bn);
if (!ignorePrecision && !comparisonVal.val.eq(ZERO)) {
assert(
comparisonVal.precision.eq(this.precision),
'Trying to compare numbers with different precision. Yo can opt to ignore precision using the ignorePrecision parameter'
);
}
return this.val.lt(comparisonVal.val);
}
public gte(bn: BigNum | BN, ignorePrecision?: boolean): boolean {
const comparisonVal = this.bigNumFromParam(bn);
if (!ignorePrecision && !comparisonVal.val.eq(ZERO)) {
assert(
comparisonVal.precision.eq(this.precision),
'Trying to compare numbers with different precision. Yo can opt to ignore precision using the ignorePrecision parameter'
);
}
return this.val.gte(comparisonVal.val);
}
public lte(bn: BigNum | BN, ignorePrecision?: boolean): boolean {
const comparisonVal = this.bigNumFromParam(bn);
if (!ignorePrecision && !comparisonVal.val.eq(ZERO)) {
assert(
comparisonVal.precision.eq(this.precision),
'Trying to compare numbers with different precision. Yo can opt to ignore precision using the ignorePrecision parameter'
);
}
return this.val.lte(comparisonVal.val);
}
public eq(bn: BigNum | BN, ignorePrecision?: boolean): boolean {
const comparisonVal = this.bigNumFromParam(bn);
if (!ignorePrecision && !comparisonVal.val.eq(ZERO)) {
assert(
comparisonVal.precision.eq(this.precision),
'Trying to compare numbers with different precision. Yo can opt to ignore precision using the ignorePrecision parameter'
);
}
return this.val.eq(comparisonVal.val);
}
public eqZero() {
return this.val.eq(ZERO);
}
public gtZero() {
return this.val.gt(ZERO);
}
public ltZero() {
return this.val.lt(ZERO);
}
public gteZero() {
return this.val.gte(ZERO);
}
public lteZero() {
return this.val.lte(ZERO);
}
public abs(): BigNum {
return new BigNum(this.val.abs(), this.precision);
}
public neg(): BigNum {
return new BigNum(this.val.neg(), this.precision);
}
public toString = (base?: number | 'hex', length?: number): string =>
this.val.toString(base, length);
/**
* Pretty print the underlying value in human-readable form. Depends on precision being correct for the output string to be correct
* @returns
*/
public print(): string {
assert(
this.precision.gte(ZERO),
'Tried to print a BN with precision lower than zero'
);
const isNeg = this.isNeg();
const plainString = this.abs().toString();
const precisionNum = this.precision.toNumber();
// make a string with at least the precisionNum number of zeroes
let printString = [
...Array(this.precision.toNumber()).fill(0),
...plainString.split(''),
].join('');
// inject decimal
printString =
printString.substring(0, printString.length - precisionNum) +
BigNum.delim +
printString.substring(printString.length - precisionNum);
// remove leading zeroes
printString = printString.replace(/^0+/, '');
// add zero if leading delim
if (printString[0] === BigNum.delim) printString = `0${printString}`;
// Add minus if negative
if (isNeg) printString = `-${printString}`;
// remove trailing delim
if (printString[printString.length - 1] === BigNum.delim)
printString = printString.slice(0, printString.length - 1);
return printString;
}
public prettyPrint(
useTradePrecision?: boolean,
precisionOverride?: number
): string {
const [leftSide, rightSide] = this.printShort(
useTradePrecision,
precisionOverride
).split(BigNum.delim);
let formattedLeftSide = leftSide;
const isNeg = formattedLeftSide.includes('-');
if (isNeg) {
formattedLeftSide = formattedLeftSide.replace('-', '');
}
let index = formattedLeftSide.length - 3;
while (index >= 1) {
const formattedLeftSideArray = formattedLeftSide.split('');
formattedLeftSideArray.splice(index, 0, BigNum.spacer);
formattedLeftSide = formattedLeftSideArray.join('');
index -= 3;
}
return `${isNeg ? '-' : ''}${formattedLeftSide}${
rightSide ? `${BigNum.delim}${rightSide}` : ''
}`;
}
/**
* Print and remove unnecessary trailing zeroes
* @returns
*/
public printShort(
useTradePrecision?: boolean,
precisionOverride?: number
): string {
const printVal = precisionOverride
? this.toPrecision(precisionOverride)
: useTradePrecision
? this.toTradePrecision()
: this.print();
if (!printVal.includes(BigNum.delim)) return printVal;
return printVal.replace(/0+$/g, '').replace(/\.$/, '').replace(/,$/, '');
}
public debug() {
console.log(
`${this.toString()} | ${this.print()} | ${this.precision.toString()}`
);
}
/**
* Pretty print with the specified number of decimal places
* @param fixedPrecision
* @returns
*/
public toFixed(fixedPrecision: number, rounded = false): string {
if (rounded) {
return this.toRounded(fixedPrecision).toFixed(fixedPrecision);
}
const printString = this.print();
const [leftSide, rightSide] = printString.split(BigNum.delim);
const filledRightSide = [
...(rightSide ?? '').slice(0, fixedPrecision),
...Array(fixedPrecision).fill('0'),
]
.slice(0, fixedPrecision)
.join('');
return `${leftSide}${BigNum.delim}${filledRightSide}`;
}
private getZeroes(count: number) {
return new Array(Math.max(count, 0)).fill('0').join('');
}
public toRounded(roundingPrecision: number) {
const printString = this.toString();
let shouldRoundUp = false;
const roundingDigitChar = printString[roundingPrecision];
if (roundingDigitChar) {
const roundingDigitVal = Number(roundingDigitChar);
if (roundingDigitVal >= 5) shouldRoundUp = true;
}
if (shouldRoundUp) {
const valueWithRoundedPrecisionAdded = this.add(
BigNum.from(
new BN(10).pow(new BN(printString.length - roundingPrecision)),
this.precision
)
);
const roundedUpPrintString =
valueWithRoundedPrecisionAdded.toString().slice(0, roundingPrecision) +
this.getZeroes(printString.length - roundingPrecision);
return BigNum.from(roundedUpPrintString, this.precision);
} else {
const roundedDownPrintString =
printString.slice(0, roundingPrecision) +
this.getZeroes(printString.length - roundingPrecision);
return BigNum.from(roundedDownPrintString, this.precision);
}
}
/**
* Pretty print to the specified number of significant figures
* @param fixedPrecision
* @returns
*/
public toPrecision(
fixedPrecision: number,
trailingZeroes = false,
rounded = false
): string {
if (rounded) {
return this.toRounded(fixedPrecision).toPrecision(
fixedPrecision,
trailingZeroes
);
}
const isNeg = this.isNeg();
const printString = this.abs().print();
const thisString = this.abs().toString();
let precisionPrintString = printString.slice(0, fixedPrecision + 1);
if (
!printString.includes(BigNum.delim) &&
thisString.length < fixedPrecision
) {
const precisionMismatch = fixedPrecision - thisString.length;
return BigNum.from(
(isNeg ? '-' : '') + thisString + this.getZeroes(precisionMismatch),
precisionMismatch
).toPrecision(fixedPrecision, trailingZeroes);
}
if (
!precisionPrintString.includes(BigNum.delim) ||
precisionPrintString[precisionPrintString.length - 1] === BigNum.delim
) {
precisionPrintString = printString.slice(0, fixedPrecision);
}
const pointsOfPrecision = precisionPrintString.replace(
BigNum.delim,
''
).length;
if (pointsOfPrecision < fixedPrecision) {
precisionPrintString = [
...precisionPrintString.split(''),
...Array(fixedPrecision - pointsOfPrecision).fill('0'),
].join('');
}
if (!precisionPrintString.includes(BigNum.delim)) {
const delimFullStringLocation = printString.indexOf(BigNum.delim);
let skipExponent = false;
if (delimFullStringLocation === -1) {
// no decimal, not missing any precision
skipExponent = true;
}
if (
precisionPrintString[precisionPrintString.length - 1] === BigNum.delim
) {
// decimal is at end of string, not missing any precision, do nothing
skipExponent = true;
}
if (printString.indexOf(BigNum.delim) === fixedPrecision) {
// decimal is at end of string, not missing any precision, do nothing
skipExponent = true;
}
if (!skipExponent) {
const exponent = delimFullStringLocation - fixedPrecision;
if (trailingZeroes) {
precisionPrintString = `${precisionPrintString}${Array(exponent)
.fill('0')
.join('')}`;
} else {
precisionPrintString = `${precisionPrintString}e${exponent}`;
}
}
}
return `${isNeg ? '-' : ''}${precisionPrintString}`;
}
public toTradePrecision(rounded = false): string {
return this.toPrecision(6, true, rounded);
}
/**
* Print dollar formatted value. Defaults to fixed decimals two unless a given precision is given.
* @param useTradePrecision
* @param precisionOverride
* @returns
*/
public toNotional(
useTradePrecision?: boolean,
precisionOverride?: number
): string {
const prefix = `${this.lt(BigNum.zero()) ? `-` : ``}$`;
const usingCustomPrecision =
true && (useTradePrecision || precisionOverride);
let val = usingCustomPrecision
? this.prettyPrint(useTradePrecision, precisionOverride)
: BigNum.fromPrint(this.toFixed(2), new BN(2)).prettyPrint();
// Append trailing zeroes out to 2 decimal places if not using custom precision
if (!usingCustomPrecision) {
const [_, rightSide] = val.split(BigNum.delim);
const trailingLength = rightSide?.length ?? 0;
if (trailingLength === 0) {
val = `${val}${BigNum.delim}00`;
} else if (trailingLength === 1) {
val = `${val}0`;
}
}
return `${prefix}${val.replace('-', '')}`;
}
public toMillified(
precision = 3,
rounded = false,
type: 'financial' | 'scientific' = 'financial'
): string {
if (rounded) {
return this.toRounded(precision).toMillified(precision);
}
const isNeg = this.isNeg();
const stringVal = this.abs().print();
const [leftSide] = stringVal.split(BigNum.delim);
if (!leftSide) {
return this.shift(new BN(precision)).toPrecision(precision, true);
}
if (leftSide.length <= precision) {
return this.toPrecision(precision);
}
if (leftSide.length <= 3) {
return this.shift(new BN(precision)).toPrecision(precision, true);
}
const unitTicks =
type === 'financial'
? ['', 'K', 'M', 'B', 'T', 'Q']
: ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'];
// TODO -- handle nubers which are larger than the max unit tick
const unitNumber = Math.floor((leftSide.length - 1) / 3);
const unit = unitTicks[unitNumber];
let leadDigits = leftSide.slice(0, precision);
if (leadDigits.length < precision) {
leadDigits = [
...leadDigits.split(''),
...Array(precision - leadDigits.length).fill('0'),
].join('');
}
const decimalLocation = leftSide.length - 3 * unitNumber;
let leadString = '';
if (decimalLocation >= precision) {
leadString = `${leadDigits}`;
} else {
leadString = `${leadDigits.slice(0, decimalLocation)}${
BigNum.delim
}${leadDigits.slice(decimalLocation)}`;
}
return `${isNeg ? '-' : ''}${leadString}${unit}`;
}
public toJSON() {
return {
val: this.val.toString(),
precision: this.precision.toString(),
};
}
public isNeg() {
return this.lt(ZERO, true);
}
public isPos() {
return !this.isNeg();
}
/**
* Get the numerical value of the BigNum. This can break if the BigNum is too large.
* @returns
*/
public toNum() {
let printedValue = this.print();
// Must convert any non-US delimiters and spacers to US format before using parseFloat
if (BigNum.delim !== '.' || BigNum.spacer !== ',') {
printedValue = printedValue
.split('')
.map((char) => {
if (char === BigNum.delim) return '.';
if (char === BigNum.spacer) return ',';
return char;
})
.join('');
}
return parseFloat(printedValue);
}
static fromJSON(json: { val: string; precision: string }) {
return BigNum.from(new BN(json.val), new BN(json.precision));
}
/**
* Create a BigNum instance
* @param val
* @param precision
* @returns
*/
static from(
val: BN | number | string = ZERO,
precision?: BN | number | string
): BigNum {
assert(
new BN(precision).lt(new BN(100)),
'Tried to create a bignum with precision higher than 10^100'
);
return new BigNum(val, precision);
}
/**
* Create a BigNum instance from a printed BigNum
* @param val
* @param precisionOverride
* @returns
*/
static fromPrint(val: string, precisionShift?: BN): BigNum {
// Handle empty number edge cases
if (!val) return BigNum.from(ZERO, precisionShift);
if (!val.replace(BigNum.delim, '')) {
return BigNum.from(ZERO, precisionShift);
}
if (val.includes('e'))
val = (+val).toFixed(precisionShift?.toNumber() ?? 9); // prevent small numbers e.g. 3.1e-8, use assume max precision 9 as default
const sides = val.split(BigNum.delim);
const rightSide = sides[1];
const leftSide = sides[0].replace(/\s/g, '');
const bnInput = `${leftSide ?? ''}${rightSide ?? ''}`;
const rawBn = new BN(bnInput);
const rightSideLength = rightSide?.length ?? 0;
const totalShift = precisionShift
? precisionShift.sub(new BN(rightSideLength))
: ZERO;
return BigNum.from(rawBn, precisionShift).shift(totalShift, true);
}
static max(a: BigNum, b: BigNum): BigNum {
return a.gt(b) ? a : b;
}
static min(a: BigNum, b: BigNum): BigNum {
return a.lt(b) ? a : b;
}
static zero(precision?: BN | number): BigNum {
return BigNum.from(0, precision);
}
}