Skip to content
markpaper

src/numbers/x18.test.ts

v0.2.0 · 5.4 KB

Download file
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');
  });
});
All files