@sovryn-zero/lib-base
Version:
Sovryn Zero SDK shared interfaces
457 lines (364 loc) • 12.1 kB
text/typescript
import assert from "assert";
import { BigNumber } from "@ethersproject/bignumber";
const getDigits = (numDigits: number) => TEN.pow(numDigits);
const MAX_UINT_256 = "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff";
const PRECISION = 18;
const ONE = BigNumber.from(1);
const TEN = BigNumber.from(10);
const DIGITS = getDigits(PRECISION);
const stringRepresentationFormat = /^[0-9]*(\.[0-9]*)?(e[-+]?[0-9]+)?$/;
const trailingZeros = /0*$/;
const magnitudes = ["", "K", "M", "B", "T"];
const roundedMul = (x: BigNumber, y: BigNumber) => x.mul(y).add(Decimal.HALF.hex).div(DIGITS);
/**
* Types that can be converted into a Decimal.
*
* @public
*/
export type Decimalish = Decimal | number | string;
/**
* Fixed-point decimal bignumber with 18 digits of precision.
*
* @remarks
* Used by Zero libraries to precisely represent native currency (e.g. Ether), ZUSD and ZERO
* amounts, as well as derived metrics like collateral ratios.
*
* @public
*/
export class Decimal {
static readonly INFINITY = Decimal.fromBigNumberString(MAX_UINT_256);
static readonly ZERO = Decimal.from(0);
static readonly HALF = Decimal.from(0.5);
static readonly ONE = Decimal.from(1);
private readonly _bigNumber: BigNumber;
/** @internal */
get hex(): string {
return this._bigNumber.toHexString();
}
/** @internal */
get bigNumber(): string {
return this._bigNumber.toString();
}
private constructor(bigNumber: BigNumber) {
if (bigNumber.isNegative()) {
throw new Error("negatives not supported by Decimal");
}
this._bigNumber = bigNumber;
}
static fromBigNumberString(bigNumberString: string): Decimal {
return new Decimal(BigNumber.from(bigNumberString));
}
private static _fromString(representation: string): Decimal {
if (!representation || !representation.match(stringRepresentationFormat)) {
throw new Error(`bad decimal format: "${representation}"`);
}
if (representation.includes("e")) {
// eslint-disable-next-line prefer-const
let [coefficient, exponent] = representation.split("e");
if (exponent.startsWith("-")) {
return new Decimal(
Decimal._fromString(coefficient)._bigNumber.div(
TEN.pow(BigNumber.from(exponent.substr(1)))
)
);
}
if (exponent.startsWith("+")) {
exponent = exponent.substr(1);
}
return new Decimal(
Decimal._fromString(coefficient)._bigNumber.mul(TEN.pow(BigNumber.from(exponent)))
);
}
if (!representation.includes(".")) {
return new Decimal(BigNumber.from(representation).mul(DIGITS));
}
// eslint-disable-next-line prefer-const
let [characteristic, mantissa] = representation.split(".");
if (mantissa.length < PRECISION) {
mantissa += "0".repeat(PRECISION - mantissa.length);
} else {
mantissa = mantissa.substr(0, PRECISION);
}
return new Decimal(
BigNumber.from(characteristic || 0)
.mul(DIGITS)
.add(mantissa)
);
}
static from(decimalish: Decimalish): Decimal {
switch (typeof decimalish) {
case "object":
if (decimalish instanceof Decimal) {
return decimalish;
} else {
throw new Error("invalid Decimalish value");
}
case "string":
return Decimal._fromString(decimalish);
case "number":
return Decimal._fromString(decimalish.toString());
default:
throw new Error("invalid Decimalish value");
}
}
private _toStringWithAutomaticPrecision() {
const characteristic = this._bigNumber.div(DIGITS);
const mantissa = this._bigNumber.mod(DIGITS);
if (mantissa.isZero()) {
return characteristic.toString();
} else {
const paddedMantissa = mantissa.toString().padStart(PRECISION, "0");
const trimmedMantissa = paddedMantissa.replace(trailingZeros, "");
return characteristic.toString() + "." + trimmedMantissa;
}
}
private _roundUp(precision: number) {
const halfDigit = getDigits(PRECISION - 1 - precision).mul(5);
return this._bigNumber.add(halfDigit);
}
private _toStringWithPrecision(precision: number) {
if (precision < 0) {
throw new Error("precision must not be negative");
}
const value = precision < PRECISION ? this._roundUp(precision) : this._bigNumber;
const characteristic = value.div(DIGITS);
const mantissa = value.mod(DIGITS);
if (precision === 0) {
return characteristic.toString();
} else {
const paddedMantissa = mantissa.toString().padStart(PRECISION, "0");
const trimmedMantissa = paddedMantissa.substr(0, precision);
return characteristic.toString() + "." + trimmedMantissa;
}
}
toString(precision?: number): string {
if (this.infinite) {
return "∞";
} else if (precision !== undefined) {
return this._toStringWithPrecision(precision);
} else {
return this._toStringWithAutomaticPrecision();
}
}
prettify(precision = 2): string {
const [characteristic, mantissa] = parseFloat(this.toString(precision)).toString().split(".");
const prettyCharacteristic = characteristic.replace(/(\d)(?=(\d{3})+(?!\d))/g, "$1,");
return mantissa !== undefined ? prettyCharacteristic + "." + mantissa : prettyCharacteristic;
}
shorten(): string {
const characteristicLength = this.toString(0).length;
const magnitude = Math.min(Math.floor((characteristicLength - 1) / 3), magnitudes.length - 1);
const precision = Math.max(3 * (magnitude + 1) - characteristicLength, 0);
const normalized = this.div(new Decimal(getDigits(PRECISION + 3 * magnitude)));
return normalized.prettify(precision) + magnitudes[magnitude];
}
add(addend: Decimalish): Decimal {
return new Decimal(this._bigNumber.add(Decimal.from(addend)._bigNumber));
}
sub(subtrahend: Decimalish): Decimal {
return new Decimal(this._bigNumber.sub(Decimal.from(subtrahend)._bigNumber));
}
mul(multiplier: Decimalish): Decimal {
return new Decimal(this._bigNumber.mul(Decimal.from(multiplier)._bigNumber).div(DIGITS));
}
div(divider: Decimalish): Decimal {
divider = Decimal.from(divider);
if (divider.isZero) {
return Decimal.INFINITY;
}
return new Decimal(this._bigNumber.mul(DIGITS).div(divider._bigNumber));
}
/** @internal */
_divCeil(divider: Decimalish): Decimal {
divider = Decimal.from(divider);
if (divider.isZero) {
return Decimal.INFINITY;
}
return new Decimal(
this._bigNumber.mul(DIGITS).add(divider._bigNumber.sub(ONE)).div(divider._bigNumber)
);
}
mulDiv(multiplier: Decimalish, divider: Decimalish): Decimal {
multiplier = Decimal.from(multiplier);
divider = Decimal.from(divider);
if (divider.isZero) {
return Decimal.INFINITY;
}
return new Decimal(this._bigNumber.mul(multiplier._bigNumber).div(divider._bigNumber));
}
pow(exponent: number): Decimal {
assert(Number.isInteger(exponent));
assert(0 <= exponent && exponent <= 0xffffffff); // Ensure we're safe to use bitwise ops
if (exponent === 0) {
return Decimal.ONE;
}
if (exponent === 1) {
return this;
}
let x = this._bigNumber;
let y = DIGITS;
for (; exponent > 1; exponent >>>= 1) {
if (exponent & 1) {
y = roundedMul(x, y);
}
x = roundedMul(x, x);
}
return new Decimal(roundedMul(x, y));
}
get isZero(): boolean {
return this._bigNumber.isZero();
}
get zero(): this | undefined {
if (this.isZero) {
return this;
}
}
get nonZero(): this | undefined {
if (!this.isZero) {
return this;
}
}
get infinite(): this | undefined {
if (this.eq(Decimal.INFINITY)) {
return this;
}
}
get finite(): this | undefined {
if (!this.eq(Decimal.INFINITY)) {
return this;
}
}
/** @internal */
get absoluteValue(): this {
return this;
}
lt(that: Decimalish): boolean {
return this._bigNumber.lt(Decimal.from(that)._bigNumber);
}
eq(that: Decimalish): boolean {
return this._bigNumber.eq(Decimal.from(that)._bigNumber);
}
gt(that: Decimalish): boolean {
return this._bigNumber.gt(Decimal.from(that)._bigNumber);
}
gte(that: Decimalish): boolean {
return this._bigNumber.gte(Decimal.from(that)._bigNumber);
}
lte(that: Decimalish): boolean {
return this._bigNumber.lte(Decimal.from(that)._bigNumber);
}
static min(a: Decimalish, b: Decimalish): Decimal {
a = Decimal.from(a);
b = Decimal.from(b);
return a.lt(b) ? a : b;
}
static max(a: Decimalish, b: Decimalish): Decimal {
a = Decimal.from(a);
b = Decimal.from(b);
return a.gt(b) ? a : b;
}
}
type DifferenceRepresentation = { sign: "" | "+" | "-"; absoluteValue: Decimal };
/** @alpha */
export class Difference {
private _number?: DifferenceRepresentation;
private constructor(number?: DifferenceRepresentation) {
this._number = number;
}
static between(d1: Decimalish | undefined, d2: Decimalish | undefined): Difference {
if (d1 === undefined || d2 === undefined) {
return new Difference(undefined);
}
d1 = Decimal.from(d1);
d2 = Decimal.from(d2);
if (d1.infinite && d2.infinite) {
return new Difference(undefined);
} else if (d1.infinite) {
return new Difference({ sign: "+", absoluteValue: d1 });
} else if (d2.infinite) {
return new Difference({ sign: "-", absoluteValue: d2 });
} else if (d1.gt(d2)) {
return new Difference({ sign: "+", absoluteValue: Decimal.from(d1).sub(d2) });
} else if (d2.gt(d1)) {
return new Difference({ sign: "-", absoluteValue: Decimal.from(d2).sub(d1) });
} else {
return new Difference({ sign: "", absoluteValue: Decimal.ZERO });
}
}
toString(precision?: number): string {
if (!this._number) {
return "N/A";
}
return this._number.sign + this._number.absoluteValue.toString(precision);
}
prettify(precision?: number): string {
if (!this._number) {
return this.toString();
}
return this._number.sign + this._number.absoluteValue.prettify(precision);
}
mul(multiplier: Decimalish): Difference {
return new Difference(
this._number && {
sign: this._number.sign,
absoluteValue: this._number.absoluteValue.mul(multiplier)
}
);
}
get nonZero(): this | undefined {
return this._number?.absoluteValue.nonZero && this;
}
get positive(): this | undefined {
return this._number?.sign === "+" ? this : undefined;
}
get negative(): this | undefined {
return this._number?.sign === "-" ? this : undefined;
}
get absoluteValue(): Decimal | undefined {
return this._number?.absoluteValue;
}
get infinite(): this | undefined {
return this._number?.absoluteValue.infinite && this;
}
get finite(): this | undefined {
return this._number?.absoluteValue.finite && this;
}
}
/** @alpha */
export class Percent<
T extends {
infinite?: T | undefined;
absoluteValue?: A | undefined;
mul?(hundred: 100): T;
toString(precision?: number): string;
},
A extends {
gte(n: string): boolean;
}
> {
private _percent: T;
public constructor(ratio: T) {
this._percent = ratio.infinite || (ratio.mul && ratio.mul(100)) || ratio;
}
nonZeroish(precision: number): this | undefined {
const zeroish = `0.${"0".repeat(precision)}5`;
if (this._percent.absoluteValue?.gte(zeroish)) {
return this;
}
}
toString(precision: number): string {
return (
this._percent.toString(precision) +
(this._percent.absoluteValue && !this._percent.infinite ? "%" : "")
);
}
prettify(): string {
if (this._percent.absoluteValue?.gte("1000")) {
return this.toString(0);
} else if (this._percent.absoluteValue?.gte("10")) {
return this.toString(1);
} else {
return this.toString(2);
}
}
}