src/numbers/x18.test.ts
v0.2.0 · 5.4 KB
import { describe, expect, it } from 'vitest';
import {
absX18,
decimalToX18,
notionalX18,
numberToX18,
parseUint,
parseX18,
stepDecimals,
toWire,
X18,
x18ToBigInt,
x18ToDecimalString,
x18ToNumber,
} from './x18.js';
describe('x18ToBigInt', () => {
it('parses integer strings, including negatives and surrounding whitespace', () => {
expect(x18ToBigInt('1000000000000000000')).toBe(X18);
expect(x18ToBigInt(' -50000000000000 ')).toBe(-50000000000000n);
expect(x18ToBigInt('0')).toBe(0n);
});
it('accepts safe-integer numbers and bigints, rejects everything else', () => {
expect(x18ToBigInt(12, 'x')).toBe(12n);
expect(x18ToBigInt(7n)).toBe(7n);
expect(() => x18ToBigInt(1e20, 'min_size')).toThrow(/min_size/);
expect(() => x18ToBigInt('1e20', 'min_size')).toThrow(/min_size/);
expect(() => x18ToBigInt('1.5')).toThrow(TypeError);
expect(() => x18ToBigInt('')).toThrow(TypeError);
expect(() => x18ToBigInt(null)).toThrow(TypeError);
expect(() => x18ToBigInt(undefined)).toThrow(TypeError);
expect(() => x18ToBigInt('0x10')).toThrow(TypeError);
});
});
describe('parseUint', () => {
it('reads nonces above 2^53 losslessly from strings', () => {
const nonce = '1234567890123456789'; // synthetic 19-digit value
expect(parseUint(nonce)).toBe(1234567890123456789n);
expect(parseUint(nonce) > BigInt(Number.MAX_SAFE_INTEGER)).toBe(true);
});
it('rejects negatives, decimals and non-numeric input', () => {
expect(() => parseUint('-1')).toThrow(TypeError);
expect(() => parseUint('1.0')).toThrow(TypeError);
expect(() => parseUint(-1)).toThrow(TypeError);
expect(() => parseUint(-1n)).toThrow(TypeError);
expect(() => parseUint({})).toThrow(TypeError);
expect(parseUint(5)).toBe(5n);
});
});
describe('x18ToDecimalString / x18ToNumber', () => {
it('matches the knowledge-base identities', () => {
expect(x18ToDecimalString(-50000000000000n)).toBe('-0.00005');
expect(x18ToNumber(50000000000000n)).toBe(0.00005);
expect(x18ToDecimalString(66119n * X18)).toBe('66119');
expect(x18ToDecimalString(0n)).toBe('0');
expect(x18ToDecimalString(100000000000000n)).toBe('0.0001');
expect(x18ToDecimalString(1n)).toBe('0.000000000000000001');
});
it('decodes live symbols values without float loss', () => {
// min_size = $100 on every market; Number('1e20') would print 100000000000000000000.
expect(x18ToNumber(x18ToBigInt('100000000000000000000'))).toBe(100);
expect(parseX18('980000000000000000')).toBe(0.98);
expect(parseX18('350000000000000')).toBe(0.00035);
});
it('refuses values that do not fit a double', () => {
expect(() => x18ToNumber(10n ** 330n)).toThrow(RangeError);
});
});
describe('decimalToX18', () => {
it('converts plain decimals exactly', () => {
expect(decimalToX18('0.00005')).toBe(50000000000000n);
expect(decimalToX18('66119')).toBe(66119n * X18);
expect(decimalToX18('-0.5')).toBe(-(X18 / 2n));
expect(decimalToX18(' 1.000000000000000001 ')).toBe(X18 + 1n);
expect(decimalToX18('5')).toBe(5n * X18);
});
it('rejects exponent notation, more than 18 decimals and junk', () => {
expect(() => decimalToX18('1e-7')).toThrow(TypeError);
expect(() => decimalToX18('1.0000000000000000001')).toThrow(TypeError);
expect(() => decimalToX18('.5')).toThrow(TypeError);
expect(() => decimalToX18('abc')).toThrow(TypeError);
expect(() => decimalToX18(1 as unknown as string)).toThrow(TypeError);
});
});
describe('numberToX18', () => {
it('uses the shortest round-trip representation, not the binary expansion', () => {
expect(numberToX18(0.1)).toBe(100000000000000000n);
expect(numberToX18(66119.7)).toBe(decimalToX18('66119.7'));
expect(numberToX18(-0.00005)).toBe(-50000000000000n);
expect(numberToX18(0)).toBe(0n);
});
it('expands exponent forms that String(n) produces', () => {
expect(numberToX18(1e-7)).toBe(100000000000n);
expect(numberToX18(1.5e21)).toBe(1500000000000000000000n * X18);
expect(numberToX18(-2.5e-8)).toBe(-25000000000n);
});
it('rejects non-finite numbers and too many decimals', () => {
expect(() => numberToX18(Number.NaN)).toThrow(TypeError);
expect(() => numberToX18(Number.POSITIVE_INFINITY)).toThrow(TypeError);
expect(() => numberToX18(1e-19)).toThrow(TypeError);
});
});
describe('stepDecimals', () => {
it('counts decimal places of ticks and lots', () => {
expect(stepDecimals(X18)).toBe(0);
expect(stepDecimals(50000000000000n)).toBe(5); // 0.00005
expect(stepDecimals(5n * X18)).toBe(0); // 5
expect(stepDecimals(100000000000000000n)).toBe(1); // 0.1
expect(stepDecimals(10000000000000n)).toBe(5); // 0.00001
expect(stepDecimals(1n)).toBe(18);
});
it('rejects non-positive steps', () => {
expect(() => stepDecimals(0n)).toThrow(RangeError);
expect(() => stepDecimals(-1n)).toThrow(RangeError);
});
});
describe('notionalX18 / absX18 / toWire', () => {
it('computes |price * size| in quote units', () => {
const px = 66120n * X18;
const sz = -decimalToX18('0.0005');
expect(notionalX18(px, sz)).toBe(decimalToX18('33.06'));
expect(absX18(-3n)).toBe(3n);
expect(absX18(3n)).toBe(3n);
});
it('formats bigints as decimal strings for the JSON body', () => {
expect(toWire((1n << 64n) - 1n)).toBe('18446744073709551615');
expect(toWire(-500000000000000n)).toBe('-500000000000000');
});
});