Skip to content
markpaper

src/account/orders.test.ts

v0.2.0 · 7.3 KB

Download file
import { describe, expect, it } from 'vitest';
import { decimalToX18, X18 } from '../numbers/x18.js';
import { buildNonce } from '../signing/nonce.js';
import type { Hex } from '../signing/types.js';
import type { QueryRequest, QueryRequester } from '../transport/types.js';
import { NOW_MS, ordersData, rawOrder, UNIVERSE } from './fixtures.js';
import {
  createSweepClock,
  DEFAULT_FULL_SWEEP_MS,
  flattenOrdersData,
  OrdersParseError,
  parseOpenOrders,
  readOpenOrders,
} from './orders.js';

const SENDER = `0x${'0'.repeat(64)}` as Hex;

describe('flattenOrdersData', () => {
  it('flattens product_orders and fails closed on other shapes', () => {
    expect(flattenOrdersData({ product_orders: [{ orders: [1, 2] }, { orders: [] }, { orders: [3] }] })).toEqual([
      1, 2, 3,
    ]);
    expect(() => flattenOrdersData({})).toThrow(/product_orders/);
    expect(() => flattenOrdersData({ product_orders: [{}] })).toThrow(/malformed/);
    expect(() => flattenOrdersData(null)).toThrow(OrdersParseError);
  });
});

describe('parseOpenOrders', () => {
  it('decodes the live shape: side from the sign, size = |unfilled|, tag and flag bit from the nonce', () => {
    const [o] = parseOpenOrders([rawOrder()], UNIVERSE.byProductId);
    expect(o).toMatchObject({
      coin: 'BTC',
      productId: 2,
      side: 'sell',
      price: 67000,
      priceX18: 67000n * X18,
      unfilled: 0.25,
      unfilledX18: decimalToX18('0.25'),
      amountX18: -decimalToX18('0.25'),
      orderType: 'default',
      reduceOnly: false,
      expiration: (1n << 64n) - 1n,
      placedAt: NOW_MS / 1000,
      ours: true,
      reduceIntent: false,
      digest: `0x${'ab'.repeat(32)}`,
    });
    expect(o?.nonce).toBe(buildNonce({ recvTimeMs: NOW_MS + 60_000, random: 3 }));
  });

  it('recognises our flag bit and foreign orders by tag', () => {
    const tp = rawOrder({
      nonce: buildNonce({ recvTimeMs: NOW_MS, tag: 0x321, reduceIntent: true, random: 1 }).toString(),
      digest: `0x${'cd'.repeat(32)}`,
    });
    const foreign = rawOrder({ nonce: (BigInt(NOW_MS) << 20n).toString(), digest: `0x${'ef'.repeat(32)}` });
    const [a, b] = parseOpenOrders([tp, foreign], UNIVERSE.byProductId, { tag: 0x321 });
    expect(a).toMatchObject({ ours: true, reduceIntent: true });
    expect(b).toMatchObject({ ours: false, reduceIntent: null });
  });

  it('skips fully filled remnants and tolerates a missing appendix / placed_at', () => {
    const filled = rawOrder({ unfilled_amount: '0', digest: `0x${'11'.repeat(32)}` });
    const buy = rawOrder({
      amount: decimalToX18('1').toString(),
      unfilled_amount: decimalToX18('0.4').toString(),
      appendix: undefined,
      placed_at: undefined,
      digest: `0x${'22'.repeat(32)}`,
    });
    const out = parseOpenOrders([filled, buy], UNIVERSE.byProductId);
    expect(out).toHaveLength(1);
    expect(out[0]).toMatchObject({ side: 'buy', unfilled: 0.4, orderType: null, appendix: null, placedAt: null });
  });

  it('fails closed on unknown products, malformed numbers, bad digests and duplicates', () => {
    expect(() => parseOpenOrders([rawOrder({ product_id: 999 })], UNIVERSE.byProductId)).toThrow(/unknown product/);
    expect(() => parseOpenOrders([rawOrder({ product_id: '2' })], UNIVERSE.byProductId)).toThrow(/product_id/);
    // A nonce that arrived as a JSON number (~1.87e18 > 2^53) is already corrupted: refuse it.
    const bigNonce = buildNonce({ recvTimeMs: NOW_MS + 60_000, tag: 0x5a5, random: 7 });
    expect(() => parseOpenOrders([rawOrder({ nonce: Number(bigNonce) })], UNIVERSE.byProductId)).toThrow(/nonce/);
    expect(() => parseOpenOrders([rawOrder({ nonce: '1.5' })], UNIVERSE.byProductId)).toThrow(/nonce/);
    expect(() => parseOpenOrders([rawOrder({ price_x18: '0' })], UNIVERSE.byProductId)).toThrow(/non-positive price/);
    expect(() => parseOpenOrders([rawOrder({ digest: '0x12' })], UNIVERSE.byProductId)).toThrow(/digest/);
    expect(() => parseOpenOrders([rawOrder(), rawOrder()], UNIVERSE.byProductId)).toThrow(/duplicate digest/);
    expect(() => parseOpenOrders([null], UNIVERSE.byProductId)).toThrow(/malformed/);
  });
});

function stubQuery(handler: (body: QueryRequest) => unknown): { query: QueryRequester; bodies: QueryRequest[] } {
  const bodies: QueryRequest[] = [];
  const query: QueryRequester = async <T>(body: QueryRequest) => {
    bodies.push(body);
    return handler(body) as T;
  };
  return { query, bodies };
}

describe('readOpenOrders', () => {
  it('reads by chunks, splits ours / foreign and reports completeness', async () => {
    const universe = UNIVERSE.byProductId; // ids 2, 8, 188
    const { query, bodies } = stubQuery((body) => {
      const ids = body.product_ids as number[];
      return ordersData(
        ids.includes(2)
          ? [rawOrder(), rawOrder({ nonce: (BigInt(NOW_MS) << 20n).toString(), digest: `0x${'ef'.repeat(32)}` })]
          : [],
      );
    });
    const partial = await readOpenOrders({
      query,
      sender: SENDER,
      byProductId: universe,
      productIds: [2, 8],
      chunkSize: 1,
    });
    expect(bodies.map((b) => b.product_ids)).toEqual([[2], [8]]);
    expect(bodies[0]).toMatchObject({ type: 'orders', sender: SENDER });
    expect(partial.orders).toHaveLength(2);
    expect(partial.ours).toHaveLength(1);
    expect(partial.foreign).toHaveLength(1);
    expect(partial.ordersComplete).toBe(false);
    expect(partial.productIds).toEqual([2, 8]);

    bodies.length = 0;
    const full = await readOpenOrders({ query, sender: SENDER, byProductId: universe, productIds: 'all', chunkSize: 40 });
    expect(bodies).toHaveLength(1);
    expect(bodies[0]?.product_ids).toEqual([2, 8, 188]);
    expect(full.ordersComplete).toBe(true);

    const covering = await readOpenOrders({
      query,
      sender: SENDER,
      byProductId: universe,
      productIds: [188, 2, 8, 2],
      chunkSize: 40,
    });
    expect(covering.ordersComplete).toBe(true);
    expect(covering.productIds).toEqual([188, 2, 8]);
  });

  it('a failed chunk throws instead of reporting fewer orders; unknown ids are refused', async () => {
    const { query } = stubQuery((body) => {
      if ((body.product_ids as number[]).includes(8)) throw new Error('503');
      return ordersData([rawOrder()]);
    });
    await expect(
      readOpenOrders({ query, sender: SENDER, byProductId: UNIVERSE.byProductId, productIds: [2, 8], chunkSize: 1 }),
    ).rejects.toThrow('503');
    await expect(
      readOpenOrders({ query, sender: SENDER, byProductId: UNIVERSE.byProductId, productIds: [999], chunkSize: 40 }),
    ).rejects.toThrow(/not in the universe/);
  });
});

describe('createSweepClock', () => {
  it('is due at first, then again after the interval, per key', () => {
    let t = 0;
    const clock = createSweepClock({ now: () => t });
    expect(DEFAULT_FULL_SWEEP_MS).toBe(300_000);
    expect(clock.due('a')).toBe(true);
    clock.mark('a');
    expect(clock.due('a')).toBe(false);
    expect(clock.due('b')).toBe(true); // another subaccount keeps its own window
    t = DEFAULT_FULL_SWEEP_MS - 1;
    expect(clock.due('a')).toBe(false);
    t = DEFAULT_FULL_SWEEP_MS;
    expect(clock.due('a')).toBe(true);
    clock.mark('a');
    clock.reset('a');
    expect(clock.due('a')).toBe(true);
    clock.mark('b');
    clock.reset();
    expect(clock.due('b')).toBe(true);
    expect(() => createSweepClock({ intervalMs: 0 })).toThrow(RangeError);
  });
});
All files