Skip to content
markpaper

src/account/edge.test.ts

v0.3.0 · 10.4 KB

Download file
// Edge cases added in review: float artifacts, empty/zero answers, HIP-3 and spot corner cases.
import { describe, expect, it } from 'vitest';
import type { InfoCallOptions, InfoRequest, InfoRequester } from '../transport/types.js';
import { DEFAULT_STABLES, computeEquity } from './equity.js';
import { parseUserFees } from './fees.js';
import { parseDexState, parseSpotStables } from './parse.js';
import { listZeroSizePositions, normalizePositions } from './positions.js';
import { createLastGoodCache, isEquityImplausible, ledgerExplainsDrop, median } from './reliability.js';
import { resolveTradingAccount } from './roles.js';
import { fetchAccountSnapshot, fetchPerpDexNames } from './snapshot.js';
import type { AccountSnapshot } from './types.js';

const addr = (n: number) => `0x${n.toString(16).padStart(40, '0')}`;
const USER = addr(1);

function pos(coin: string, szi: string, extra: Record<string, unknown> = {}) {
  return {
    type: 'oneWay',
    position: {
      coin,
      szi,
      leverage: { type: 'cross', value: 5 },
      entryPx: '100.0',
      positionValue: '100.0',
      unrealizedPnl: '0.0',
      returnOnEquity: '0.0',
      liquidationPx: null,
      marginUsed: '20.0',
      ...extra,
    },
  };
}

function ch(av: string, used = '0.0', positions: unknown[] = [], ntl = '0.0') {
  return {
    marginSummary: { accountValue: av, totalNtlPos: ntl, totalRawUsd: '0.0', totalMarginUsed: used },
    crossMarginSummary: { accountValue: av, totalNtlPos: ntl, totalRawUsd: '0.0', totalMarginUsed: used },
    crossMaintenanceMarginUsed: '0.0',
    withdrawable: av,
    assetPositions: positions,
    time: 1,
  };
}

/** Plain (non-async) requester stub: the handler decides whether to return a promise or throw. */
function makeInfo(handler: (body: InfoRequest, opts?: InfoCallOptions) => unknown): { info: InfoRequester; calls: InfoRequest[] } {
  const calls: InfoRequest[] = [];
  const info = ((body: InfoRequest, opts?: InfoCallOptions) => {
    calls.push(body);
    return handler(body, opts);
  }) as InfoRequester;
  return { info, calls };
}

const snap = (perp: Array<[string, unknown]>, spot: unknown): AccountSnapshot => ({
  user: USER as `0x${string}`,
  perp: perp.map(([dex, state]) => ({ dex, state })),
  spot: { ok: true, state: spot },
  fetchedAt: 0,
});

describe('zero-size position rows are "no data", not a close', () => {
  it('parseDexState lists them in zeroSizeCoins', () => {
    const d = parseDexState(ch('100', '20', [pos('A', '0.0'), pos('B', '1', { positionValue: '0.0' }), pos('C', '-2')]), '');
    expect(d.positions.map((p) => p.coin)).toEqual(['C']);
    expect(d.zeroSizeCoins).toEqual(['A', 'B']);
  });

  it('listZeroSizePositions keeps dex and prefixed HIP-3 coin; normalizePositions still excludes them', () => {
    const s = { perp: [{ dex: '', state: ch('10') }, { dex: 'xyz', state: ch('10', '0.0', [pos('xyz:NVDA', '0.0')]) }] };
    expect(listZeroSizePositions(s)).toEqual([{ dex: 'xyz', coin: 'xyz:NVDA' }]);
    expect(normalizePositions(s)).toEqual([]);
  });

  it('computeEquity reports them per dex', () => {
    const e = computeEquity(snap([['', ch('10', '0.0', [pos('ETH', '0')])]], { balances: [] }));
    expect(e.perDex[0]?.zeroSizeCoins).toEqual(['ETH']);
    expect(e.perDex[0]?.openPositions).toBe(0);
  });
});

describe('perpDexs parsing is fail-closed', () => {
  it('a live-shaped answer (null only at index 0) maps to names in order', async () => {
    const { info } = makeInfo(async () => [null, { name: 'xyz', fullName: 'x' }, { name: 'flx', fullName: 'f' }]);
    expect(await fetchPerpDexNames(info)).toEqual(['', 'xyz', 'flx']);
  });

  it('a malformed non-null entry throws instead of silently dropping a dex', async () => {
    const { info } = makeInfo(async () => [null, { name: 'xyz' }, { fullName: 'no name' }]);
    await expect(fetchPerpDexNames(info)).rejects.toMatchObject({ code: 'INVALID_RESPONSE' });
    const { info: info2 } = makeInfo(async () => [null, 'xyz']);
    await expect(fetchPerpDexNames(info2)).rejects.toMatchObject({ code: 'INVALID_RESPONSE' });
  });

  it('an empty array still yields the main dex', async () => {
    const { info } = makeInfo(async () => []);
    expect(await fetchPerpDexNames(info)).toEqual(['']);
  });
});

describe('fetchAccountSnapshot with a plain-function requester', () => {
  it('a synchronous throw of the spot call does not reject the snapshot', async () => {
    const { info } = makeInfo((body) => {
      if (body.type === 'spotClearinghouseState') throw new Error('sync boom');
      return Promise.resolve(ch('1'));
    });
    const s = await fetchAccountSnapshot(info, USER, { dexes: [''] });
    expect(s.spot).toEqual({ ok: false, error: expect.stringContaining('sync boom') });
  });

  it('a null spot answer is degraded, not zero', async () => {
    const { info } = makeInfo((body) => Promise.resolve(body.type === 'spotClearinghouseState' ? null : ch('1')));
    const s = await fetchAccountSnapshot(info, USER, { dexes: [''] });
    expect(s.spot.ok).toBe(false);
    const e = computeEquity(s);
    expect(e.total).toBeNull();
    expect(e.perpEquity).toBe(1);
  });

  it('a null perp answer rejects the snapshot as DEGRADED_PERP', async () => {
    const { info } = makeInfo((body) => Promise.resolve(body.type === 'clearinghouseState' ? null : { balances: [] }));
    await expect(fetchAccountSnapshot(info, USER, { dexes: [''] })).rejects.toMatchObject({ code: 'DEGRADED_PERP', dex: '' });
  });
});

describe('exact money arithmetic on real-looking strings', () => {
  it('free stables do not suffer float artifacts (0.3 - 0.1 = 0.2 exactly)', () => {
    const e = computeEquity(snap([['', ch('0.1')], ['xyz', ch('0.2')]], { balances: [{ coin: 'USDC', total: '0.3', hold: '0.1' }] }));
    expect(e.exact.spotFreeStables).toBe('0.2');
    expect(e.exact.total).toBe('0.5');
    expect(e.total).toBe(0.5);
  });

  it('reserve above total by 1e-8 is rejected on a regular account (exact compare, no epsilon)', () => {
    const s = parseSpotStables({ balances: [{ coin: 'USDC', total: '1000.0', hold: '1000.00000001' }] }, DEFAULT_STABLES);
    expect(s.ok).toBe(false);
    const eq = parseSpotStables({ balances: [{ coin: 'USDC', total: '1000.00000000', hold: '1000.0' }] }, DEFAULT_STABLES);
    expect(eq.ok && eq.free.units).toBe(0n);
  });

  it('a HIP-3 dex with accountValue 0 and no positions is a valid zero, not degraded', () => {
    const e = computeEquity(snap([['', ch('500')], ['flx', ch('0.0')]], { balances: [] }));
    expect(e.perpAccountValue).toEqual({ '': 500, flx: 0 });
    expect(e.total).toBe(500);
  });

  it('non-strict numbers in perp answers are degraded (exponent, whitespace, plus sign)', () => {
    for (const bad of ['1e3', ' 100', '+5', '1,000', '.5', '']) {
      expect(() => parseDexState(ch(bad), '')).toThrow(expect.objectContaining({ code: 'DEGRADED_PERP' }));
    }
  });

  it('negative zero strings are zero', () => {
    const d = parseDexState(ch('-0.0', '0.0', [pos('A', '-0.0')]), '');
    expect(d.positions).toEqual([]);
    expect(d.zeroSizeCoins).toEqual(['A']);
  });

  it('userFees with discounted rates converts to bps exactly', () => {
    const f = parseUserFees({ userAddRate: '0.000144', userCrossRate: '0.000432', activeReferralDiscount: '0.04' });
    expect(f.exact.makerBps).toBe('1.44');
    expect(f.exact.takerBps).toBe('4.32');
    expect(f.takerBps).toBe(4.32);
  });
});

describe('isEquityImplausible boundaries are not flipped by float artifacts', () => {
  it('exactly +10% exposure still counts as "exposure held"', () => {
    // 0.33 - 0.3 = 0.030000000000000027 > 0.1 * 0.3 in plain floats
    expect(isEquityImplausible({ accountValue: 100, totalNtlPos: 0.33 }, { accountValue: 1000, totalNtlPos: 0.3 })).toBe(true);
  });

  it('exactly x0.75 equity is not flagged, just below is', () => {
    expect(isEquityImplausible({ accountValue: 0.3, totalNtlPos: 1 }, { accountValue: 0.4, totalNtlPos: 1 })).toBe(false);
    expect(isEquityImplausible({ accountValue: 0.29, totalNtlPos: 1 }, { accountValue: 0.4, totalNtlPos: 1 })).toBe(true);
  });

  it('exactly x1.33 equity is not flagged, just above is', () => {
    expect(isEquityImplausible({ accountValue: 133, totalNtlPos: 1 }, { accountValue: 100, totalNtlPos: 1 })).toBe(false);
    expect(isEquityImplausible({ accountValue: 133.01, totalNtlPos: 1 }, { accountValue: 100, totalNtlPos: 1 })).toBe(true);
  });

  it('non-positive previous exposure disables the detector (first tick semantics)', () => {
    expect(isEquityImplausible({ accountValue: 1, totalNtlPos: -5 }, { accountValue: 100, totalNtlPos: -5 })).toBe(false);
  });
});

describe('median and ledger edge cases', () => {
  it('median rejects non-finite values instead of returning an arbitrary element', () => {
    expect(() => median([1, Number.NaN, 3])).toThrow(RangeError);
    expect(median([5])).toBe(5);
  });

  it('ledger amounts may arrive as numbers; blank strings are ignored', () => {
    expect(ledgerExplainsDrop([{ time: 10, delta: { usdc: -60 } }], 0, 100)).toBe(true);
    expect(ledgerExplainsDrop([{ time: 10, delta: { usdc: '' } }], 0, 100)).toBe(false);
  });

  it('exactly 50% coverage explains the drop; an entry at exactly sinceMs counts', () => {
    expect(ledgerExplainsDrop([{ time: 5, delta: { amount: '50' } }], 5, 100)).toBe(true);
    expect(ledgerExplainsDrop([{ time: 4, delta: { amount: '50' } }], 5, 100)).toBe(false);
  });
});

describe('last-good cache TTL boundary', () => {
  it('stale value is served at exactly TTL and dropped after', () => {
    let t = 0;
    const c = createLastGoodCache({ ttlMs: 1000, now: () => t });
    c.update({ trusted: true, value: 100 });
    t = 1000;
    expect(c.update({ trusted: false, value: null })).toEqual({ ok: true, value: 100, fresh: false, ageMs: 1000 });
    t = 1001;
    expect(c.update({ trusted: false, value: null }).ok).toBe(false);
  });

  it('an untrusted positive value never writes the cache', () => {
    const c = createLastGoodCache({ now: () => 0 });
    expect(c.update({ trusted: false, value: 500 }).ok).toBe(false);
    expect(c.peek()).toBeNull();
  });
});

describe('resolveTradingAccount abort', () => {
  it('a caller abort is rethrown even with a configured master', async () => {
    const controller = new AbortController();
    controller.abort();
    const { info } = makeInfo(() => Promise.reject(new Error('aborted')));
    await expect(resolveTradingAccount(info, addr(5), { configuredMaster: addr(1), signal: controller.signal })).rejects.toThrow(
      'aborted',
    );
  });
});
All files