Skip to content
markpaper

src/core.test.ts

v0.1.0 · 16.8 KB

Download file
import { randomBytes } from 'node:crypto';
import { describe, expect, it } from 'vitest';
import * as kit from './index.js';
import {
  delegate,
  exchangeKeys,
  globalBytes,
  headerBytes,
  market,
  memoryPolicy,
  owner,
  pda,
  restPolicy,
  state,
  token,
  view,
} from './testing/fixtures.js';

describe('public API', () => {
  it('exposes each namespace without silent collisions', () => {
    for (const n of [
      'account',
      'address',
      'binding',
      'ids',
      'margin',
      'markets',
      'numbers',
      'onchain',
      'ops',
      'orders',
      'pending',
      'rest',
      'signer',
    ] as const) {
      for (const [k, v] of Object.entries(kit[n])) expect((kit as Record<string, unknown>)[k]).toBe(v);
    }
  });
  it('keeps protocol limits visible', () => {
    expect(kit.MAX_TX_BYTES).toBe(1232);
    expect(kit.MAX_CANCEL_IDS).toBe(30);
    expect(kit.MAX_LIMIT_ORDERS_PER_SIDE).toBe(64);
  });
});
describe('numbers and addresses', () => {
  it.each(['0', '42', (1n << 63n).toString(), kit.U64_MAX.toString()])('parses exact u64 %s', (s) =>
    expect(kit.parseU64(s)).toBe(BigInt(s)),
  );
  it.each([1, 1n, '-1', '01', '1.1', (1n << 64n).toString(), null])('rejects noncanonical u64', (s) =>
    expect(kit.parseU64(s)).toBeNull(),
  );
  it('parses signed bounds and rejects overflowing signed strings', () => {
    expect(kit.parseI64(kit.I64_MIN.toString())).toBe(kit.I64_MIN);
    expect(kit.parseI64(kit.I64_MAX.toString())).toBe(kit.I64_MAX);
    expect(kit.parseI64((kit.I64_MAX + 1n).toString())).toBeNull();
    expect(kit.parseI64('-0')).toBeNull();
    expect(kit.parseI64(5)).toBeNull();
  });
  it('round-trips random byte arrays including leading zeroes', () => {
    for (let i = 0; i < 100; i++) {
      const bytes = randomBytes((i % 65) + 1);
      bytes[0] = 0;
      expect(kit.base58Decode(kit.base58Encode(bytes))).toEqual(new Uint8Array(bytes));
    }
  });
  it('validates canonical keys and signatures by byte length', () => {
    expect(kit.isBase58Pubkey(owner)).toBe(true);
    expect(kit.isBase58Pubkey('1' + owner)).toBe(false);
    expect(kit.isBase58Pubkey(kit.base58Encode(randomBytes(31)))).toBe(false);
    expect(kit.isBase58Pubkey(kit.base58Encode(randomBytes(33)))).toBe(false);
    expect(kit.base58Decode('0')).toBeNull();
    expect(kit.isSignatureString(kit.base58Encode(randomBytes(64)))).toBe(true);
  });
  it('quantizes ticks and lots without floating truncation', () => {
    const q = kit.createQuant({ tickSize: 100n, baseLotsDecimals: 4 });
    expect(q.tickUsd).toBe(1);
    expect(q.pxToTicks(801.5, 'nearest')).toBe(802n);
    expect(q.pxToTicks(801.5, 'floor')).toBe(801n);
    expect(q.pxToTicks(801.5, 'ceil')).toBe(802n);
    expect(q.lotsToStr(-5n)).toBe('-0.0005');
    expect(q.sizeToLots(1, 'floor')).toBe(10000n);
    expect(q.pxToStr(NaN)).toBe('0');
  });
  it('applies epsilon to calculated sizes and supports negative lot decimals', () => {
    const q = kit.createQuant({ tickSize: 10n, baseLotsDecimals: 2 });
    expect(q.sizeToLots(0.29, 'floor')).toBe(29n);
    expect(q.sizeToLots(0.30000000000000004, 'ceil')).toBe(30n);
    expect(q.sizeToLots(0.29999999999999993, 'floor')).toBe(30n);
    const bulk = kit.createQuant({ tickSize: 10n, baseLotsDecimals: -2 });
    expect(bulk.lot).toBe(100);
    expect(bulk.ticksToStr(1n)).toBe('0.0000001');
    expect(bulk.lotsToStr(2n)).toBe('200');
  });
  it('rejects invalid quantizer parameters', () => {
    expect(() => kit.createQuant({ tickSize: 0n, baseLotsDecimals: 2 })).toThrow();
    expect(() => kit.createQuant({ tickSize: 1n, baseLotsDecimals: 19 })).toThrow();
  });
  it('preserves compound order identity above double precision', () => {
    const seq = ((1n << 63n) + 7n).toString();
    expect(kit.isValidOrderId({ symbol: 'BTC', priceTicks: '800', seq })).toBe(true);
    expect(kit.isBidSeq(seq)).toBe(true);
    expect(kit.orderKey('BTC', '800', seq)).not.toBe(kit.orderKey('ETH', '800', seq));
    expect(kit.isValidOrderId({ symbol: 'BTC', priceTicks: '0', seq })).toBe(false);
  });
});
describe('markets', () => {
  it('accepts both payload forms and negative lot decimals', () => {
    expect(kit.decodeMarkets([market()]).size).toBe(1);
    const m = { ...market('SYNTH', 2), baseLotsDecimals: -2 };
    expect(kit.decodeMarkets({ markets: [m] }).get('SYNTH')?.baseLotsDecimals).toBe(-2);
  });
  it.each([
    { tickSize: 0 },
    { baseLotsDecimals: 99 },
    { isolatedOnly: undefined },
    { leverageTiers: [] },
    { assetId: -1 },
    { marketStatus: '' },
  ])('fails the whole invalid payload', (patch) =>
    expect(() => kit.decodeMarkets([{ ...market(), ...patch }])).toThrow(),
  );
  it('rejects duplicate symbols and ids', () => {
    expect(() => kit.decodeMarkets([market(), market()])).toThrow();
    expect(() => kit.decodeMarkets([market(), market('ETH', 1)])).toThrow();
  });
  it('distinguishes paused from delisted and retains isolated restrictions', () => {
    const m = kit.decodeMarkets([market()]).get('BTC')!;
    expect(kit.tradability({ ...m, status: 'paused' })).toBe('unknown');
    expect(kit.tradability({ ...m, status: 'closed' })).toBe('not_tradable');
    expect(kit.tradability({ ...m, status: 'postOnly' })).toBe('tradable');
    expect(kit.decodeMarkets([{ ...market(), isolatedOnly: true }]).get('BTC')?.isolatedOnly).toBe(true);
  });
  it('uses only fresh, unexpired after-hours bands', () => {
    const m = kit
      .decodeMarkets([
        {
          ...market(),
          commodityMetadata: {
            isAfterHours: true,
            executionPriceBand: { min: { ui: '10' }, max: { ui: '20' } },
            lastIndexExpiryTimestamp: 2000000000,
          },
          metadata: { calendar: { nextMarketTransitionUtc: '2030-01-01T00:00:00Z' } },
        },
      ])
      .get('BTC')!;
    expect(kit.afterHoursBand(m, 1000, true)).toEqual({ min: 10, max: 20 }); // value takes precedence over ui in protocol TokenAmount
    expect(kit.afterHoursBand(m, 1000, false)).toBeNull();
    expect(kit.afterHoursBand({ ...m, afterHours: false }, 1000, true)).toBeNull();
    expect(kit.afterHoursBand({ ...m, nextTransitionAt: 10 }, 10, true)).toBeNull();
  });
  it('keeps broken optional bands separate from core metadata', () => {
    expect(
      kit
        .decodeMarkets([
          {
            ...market(),
            commodityMetadata: { isAfterHours: true, executionPriceBand: { min: 'broken', max: 'broken' } },
          },
        ])
        .get('BTC')?.executionBand,
    ).toBeNull();
  });
  it('freezes an update changing a market in use and retries on caller policy', async () => {
    let now = 0;
    let raw = market();
    const c = kit.createMarketCache({
      fetchMarkets: async () => [raw],
      ttlMs: 10,
      freshForMs: 20,
      failedBackoffMs: 5,
      now: () => now,
    });
    c.noteInterest('BTC');
    await c.get();
    now = 15;
    raw = { ...raw, tickSize: 200 };
    expect((await c.get()).get('BTC')?.tickSize).toBe(100n);
    expect(c.fresh()).toBe(true);
    now = 25;
    expect(c.fresh()).toBe(false);
  });
  it('validates exchange keys and refuses TOFU reticks', () => {
    const raw = market();
    const ex = kit.decodeExchangeView({ keys: exchangeKeys, markets: [raw] });
    expect(ex.skipped).toEqual([]);
    const pins = new Map();
    expect(kit.pinMarkets(pins, ex.markets)).toEqual([]);
    expect(kit.pinMarkets(pins, new Map([['BTC', { ...ex.markets.get('BTC')!, tickSize: '200' }]]))).toHaveLength(1);
  });
});
describe('account reads', () => {
  it('decodes lots exactly and respects address case', () => {
    expect(kit.decodeTraderState(state(), { authority: owner, traderPdaIndex: 0 }).positions[0]?.lots).toBe(3n);
    expect(() => kit.decodeTraderState(state(), { authority: delegate, traderPdaIndex: 0 })).toThrow();
  });
  it('accepts omitted empty arrays while refusing malformed arrays', () => {
    const s = state('0');
    delete (s.snapshot.subaccounts[0] as { positions?: unknown }).positions;
    expect(kit.decodeTraderState(s, { authority: owner, traderPdaIndex: 0 }).positions).toEqual([]);
    const bad = state();
    (bad.snapshot.subaccounts[0] as { positions: unknown }).positions = {};
    expect(() => kit.decodeTraderState(bad, { authority: owner, traderPdaIndex: 0 })).toThrow();
  });
  it('rejects numeric lots and missing cross subaccount', () => {
    const s = state();
    (s.snapshot.subaccounts[0]!.positions[0] as unknown as { basePositionLots: unknown }).basePositionLots = 3;
    expect(() => kit.decodeTraderState(s, { authority: owner, traderPdaIndex: 0 })).toThrow();
    const z = state();
    z.snapshot.subaccounts[0]!.subaccountIndex = 1;
    expect(() => kit.decodeTraderState(z, { authority: owner, traderPdaIndex: 0 })).toThrow();
  });
  it('rejects closed snapshot rows and side/sequence disagreement', () => {
    for (const patch of [
      { change: 'closed' },
      { orderSequenceNumber: '8' },
      { reduceOnly: 'true' },
      { orderSequenceNumber: 8 },
    ]) {
      const s = state();
      (s.snapshot.subaccounts[0] as { orders: unknown }).orders = [
        {
          symbol: 'BTC',
          orders: [
            {
              side: 'bid',
              orderSequenceNumber: ((1n << 63n) + 8n).toString(),
              priceTicks: '900',
              sizeRemainingLots: '3',
              reduceOnly: false,
              ...patch,
            },
          ],
        },
      ];
      expect(() => kit.decodeTraderState(s, { authority: owner, traderPdaIndex: 0 })).toThrow();
    }
  });
  it('does not accept numeric TokenAmount ui', () => {
    const v = view();
    (v.portfolioValue as { ui: unknown }).ui = 72;
    expect(() => kit.decodeTraderView(v, { authority: owner, traderPda: pda })).toThrow();
    expect(() => kit.decodeTraderView(view(), { authority: owner, traderPda: delegate })).toThrow();
  });
  it('rejects nonfinite required UI amounts and ignores nonfinite optional margin', () => {
    const v = view();
    v.portfolioValue.ui = '9'.repeat(400);
    expect(() => kit.decodeTraderView(v, { authority: owner, traderPda: pda })).toThrow();
    const optional = view();
    optional.effectiveCollateral.ui = '9'.repeat(400);
    expect(kit.decodeTraderView(optional, { authority: owner, traderPda: pda }).collateralUsd).toBeNull();
    const s = state();
    s.snapshot.subaccounts[0]!.positions[0]!.entryPriceUsd = '9'.repeat(400);
    expect(() => kit.decodeTraderState(s, { authority: owner, traderPdaIndex: 0 })).toThrow();
  });
  it('rejects duplicate position rows rather than summing or overwriting', () => {
    const s = state();
    s.snapshot.subaccounts[0]!.positions.push({ ...s.snapshot.subaccounts[0]!.positions[0]! });
    expect(() => kit.decodeTraderState(s, { authority: owner, traderPdaIndex: 0 })).toThrow();
    const v = view();
    v.positions.push({ ...v.positions[0]! });
    expect(() => kit.decodeTraderView(v, { authority: owner, traderPda: pda })).toThrow();
  });
  it('detects a flat snapshot contradicting the intervening equity view', () => {
    const flat = kit.decodeTraderState(state('0'), { authority: owner, traderPdaIndex: 0 });
    const v = kit.decodeTraderView(view(), { authority: owner, traderPda: pda });
    expect(kit.viewContradictsState(flat, flat, v)).not.toBeNull();
    expect(kit.viewContradictsState(flat, flat, { ...v, slot: 101 })).toBeNull();
  });
  it('stable reads fence state, view and state without hidden margin coupling', async () => {
    const memory = kit.createWriteMemory(memoryPolicy);
    const r = await kit.readStableAccount({
      authority: owner,
      traderPda: pda,
      memory,
      catchUpMs: 0,
      catchUpGapMs: 1,
      rest: { traderState: async () => state(), traderView: async () => view() },
    });
    expect(r.trusted).toBe(true);
    expect(r.equityAvailable).toBe(true);
  });
  it('retains independently failed equity as explicit missing information', async () => {
    const r = await kit.readStableAccount({
      authority: owner,
      traderPda: pda,
      memory: kit.createWriteMemory(memoryPolicy),
      catchUpMs: 0,
      catchUpGapMs: 1,
      rest: {
        traderState: async () => state(),
        traderView: async () => {
          throw new Error('read failed');
        },
      },
    });
    expect(r.trusted).toBe(true);
    expect(r.equityAvailable).toBe(false);
    expect(r.viewError).toBe('read failed');
  });
  it('converts exact order ids and position sizes without numeric surrogates', () => {
    const s = kit.decodeTraderState(state(), { authority: owner, traderPdaIndex: 0 });
    expect(kit.positionsToSizes(s, kit.decodeMarkets([market()]))[0]?.sizeString).toBe('0.0003');
    const seq = ((1n << 63n) + 5n).toString();
    const r = kit.ordersToRows(
      [{ symbol: 'BTC', side: 'bid', priceTicks: '900', seq, lots: 3n, reduceOnly: false }],
      kit.decodeMarkets([market()]),
    );
    expect(r[0]?.id.seq).toBe(seq);
  });
});
describe('on-chain and binding checks', () => {
  it('checks program ownership, discriminator and header length', () => {
    expect(kit.parseTraderHeader(headerBytes(), kit.PROGRAM_ADDRESS).ok).toBe(true);
    expect(kit.parseTraderHeader(headerBytes(), owner).ok).toBe(false);
    expect(kit.parseTraderHeader(new Uint8Array(12), kit.PROGRAM_ADDRESS).ok).toBe(false);
    const bad = headerBytes();
    bad[0] = bad[0]! ^ 1;
    expect(kit.parseTraderHeader(bad, kit.PROGRAM_ADDRESS).ok).toBe(false);
  });
  it('cold and hot flags do not imply delegation loss', () => {
    for (const flags of [62, 63]) {
      const parsed = kit.parseTraderHeader(headerBytes(flags), kit.PROGRAM_ADDRESS);
      if (!parsed.ok) throw new Error(parsed.why);
      expect(kit.headerProblems(parsed.header, { authority: owner, delegate, traderPda: pda })).toEqual([]);
      expect(kit.traderRestrictions(kit.describeCapabilities(flags))).toEqual([]);
    }
  });
  it('frozen state preserves a valid binding and reports restrictions', () => {
    const parsed = kit.parseTraderHeader(headerBytes(4), kit.PROGRAM_ADDRESS);
    if (!parsed.ok) throw new Error(parsed.why);
    expect(kit.headerDelegationProblems(parsed.header, { authority: owner, delegate, traderPda: pda })).toEqual([]);
    expect(kit.isFrozen(4)).toBe(true);
    expect(kit.traderRestrictions(kit.describeCapabilities(4))).toContain('Account frozen');
  });
  it('decodes global config and matches REST keys', () => {
    const gc = kit.parseGlobalConfig(globalBytes(), kit.PROGRAM_ADDRESS);
    expect(gc.ok).toBe(true);
    expect(kit.checkViewKeysAgainstChain(exchangeKeys, gc)).toEqual([]);
    expect(kit.keyRiskOf(kit.describePreferences(2), gc).positionAuthoritySwap).toBe('closed_by_trader');
    expect(kit.keyRiskOf(kit.describePreferences(0), gc).positionAuthoritySwap).toBe('open');
  });
  it('unknown observations preserve a proof and initial unknown blocks writes', () => {
    const g = kit.createBindingGate();
    expect(g.writeBlock()).not.toBeNull();
    g.note('ok');
    g.note('unknown');
    expect(g.writeBlock()).toBeNull();
    g.note('lost', ['Delegate revoked']);
    g.note('unknown');
    expect(g.writeBlock()).toContain('Delegate revoked');
    g.note('ok');
    expect(g.writeBlock()).toBeNull();
  });
  it('restart hold permits cancels and full exits and releases on zero counts', () => {
    const g = kit.createStartupHold();
    g.note(1, 0);
    expect(g.allows('place')).toBe(false);
    expect(g.allows('cancel')).toBe(true);
    expect(g.allows('fullClose')).toBe(true);
    g.note(0, 0);
    expect(g.placementsHeld()).toBe(false);
  });
  it('reports venue margin rejection and explicit free margin', () => {
    expect(kit.isMarginReject('InsufficientFunds')).toBe(true);
    expect(kit.freeMarginUsd({ collateralUsd: 70, initialMarginUsd: 4 })).toBe(66);
    expect(kit.freeMarginUsd({ collateralUsd: null, initialMarginUsd: 4 })).toBeNull();
  });
});
describe('read transport', () => {
  it('never retries other 4xx', async () => {
    let n = 0;
    const c = kit.createRestClient({
      ...restPolicy,
      fetch: async () => {
        n++;
        return new Response('{}', { status: 403 });
      },
    });
    await expect(c.markets()).rejects.toMatchObject({ status: 403 });
    expect(n).toBe(1);
  });
  it('honors shared Retry-After and retries a read', async () => {
    let n = 0;
    let now = 0;
    const c = kit.createRestClient({
      ...restPolicy,
      now: () => now,
      sleep: async (ms) => {
        now += ms;
      },
      fetch: async () =>
        ++n === 1 ? new Response('{}', { status: 429, headers: { 'retry-after': '2' } }) : new Response('[]'),
    });
    expect(await c.markets()).toEqual([]);
    expect(now).toBe(2000);
  });
  it('uses a mark fallback only after an invalid orderbook mid', async () => {
    const c = kit.createRestClient({
      ...restPolicy,
      fetch: async (u) =>
        String(u).includes('orderbook')
          ? new Response('{"mid":null}')
          : new Response('{"markets":[{"symbol":"BTC","mark_price":"900"}]}'),
    });
    expect(await c.mid('BTC')).toBe(900);
  });
});
All files