Skip to content
markpaper

scripts/smoke.ts

v0.2.1 · 5.6 KB

Download file
// Read-only live smoke test against PUBLIC data of the robinhoodchain instance. No account, no keys,
// no signer, no orders. Run: npm run smoke  (or LIGHTER_SMOKE_INSTANCE=mainnet npm run smoke)
//
// Exercises the package entry the way a user would: REST client, orderBooks + orderBookDetails
// decoding, USDG duplicates, quantization on a real grid, both minimums, the meta cache.

import {
  baseMarkets,
  createMarketCache,
  createRestClient,
  formatSize,
  isOnPriceGrid,
  type LighterMarket,
  marketByBaseSymbol,
  maxLeverageFromImf,
  meetsMinimums,
  meetsMinimumsExact,
  orphanUsdgDuplicates,
  planOrderSize,
  quantizePrice,
  quantizeSize,
  toWire,
} from '../src/index.js';

let failures = 0;
function check(label: string, ok: boolean, detail = ''): void {
  if (!ok) failures++;
  console.log(`${ok ? 'ok  ' : 'FAIL'} ${label}${detail ? ` - ${detail}` : ''}`);
}

async function main(): Promise<void> {
  const instance = process.env.LIGHTER_SMOKE_INSTANCE ?? 'robinhoodchain';
  const client = createRestClient({ baseUrl: instance, timeoutMs: 15_000 });
  console.log(`instance: ${instance} -> ${client.baseUrl}`);

  // --- orderBooks: the flat listing --------------------------------------------------------------
  const rows = await client.orderBooks();
  const dupRows = rows.filter((r) => r.isUsdgDuplicate).length;
  check('orderBooks listed', rows.length > 0, `rows=${rows.length} usdgDuplicates=${dupRows}`);

  // --- orderBookDetails: the meta ------------------------------------------------------------------
  const markets = await client.orderBookDetails();
  const base = baseMarkets(markets.values());
  const active = base.filter((m) => m.status === 'active');
  check(
    'orderBookDetails decoded',
    markets.size > 0,
    `perps=${markets.size} base=${base.length} active=${active.length}`,
  );
  check('no orphan USDG duplicates', orphanUsdgDuplicates(markets.values()).length === 0);
  check(
    'orderBooks and orderBookDetails agree on symbols',
    rows.every((r) => markets.has(r.symbol) || r.marketType !== 'perp'),
  );

  const market: LighterMarket | undefined = marketByBaseSymbol(markets.values(), 'ETH') ?? active[0];
  if (!market) {
    check('a market to exercise', false, 'no active base perp');
    return;
  }
  console.log(
    `market ${market.symbol}: market_id=${market.marketId} sizeDecimals=${market.sizeDecimals} priceDecimals=${market.priceDecimals} ` +
      `min_base=${market.minBaseAmountRaw} min_quote=${market.minQuoteUsdRaw} mark=${market.markPriceRaw} imf=${market.minInitialMarginFraction} maxLev=${market.maxLeverage}`,
  );
  check(`${market.symbol} has a mark price`, market.markPrice > 0);
  check(
    `${market.symbol} cap = floor(10000 / imf)`,
    market.maxLeverage === maxLeverageFromImf(market.minInitialMarginFraction),
  );

  // --- quantization on the real grid -----------------------------------------------------------------
  const pxStr = quantizePrice(market.markPrice * 0.97, market.priceDecimals, 'floor');
  check(
    `${market.symbol} price on grid`,
    isOnPriceGrid(pxStr, market.priceDecimals),
    `mark=${market.markPriceRaw} px=${pxStr}`,
  );
  const wantUsd = Math.max(12, market.minQuoteUsd * 1.2);
  const size = quantizeSize(wantUsd / Number(pxStr), market.sizeDecimals, 'ceil');
  check(
    `${market.symbol} size on lot`,
    formatSize(size, market.sizeDecimals) === size.toFixed(market.sizeDecimals),
    `sz=${size}`,
  );
  const wire = toWire(size, pxStr, market.sizeDecimals);
  check(
    `${market.symbol} wire integers`,
    Number.isSafeInteger(wire.base_amount) && Number.isSafeInteger(wire.price),
    JSON.stringify(wire),
  );

  // --- both minimums, float and exact --------------------------------------------------------------
  const floatCheck = meetsMinimums({
    notional: size * Number(pxStr),
    baseAmount: size,
    minBaseAmount: market.minBaseAmount,
    minQuoteUsd: market.minQuoteUsd,
  });
  const exactCheck = meetsMinimumsExact({
    baseAmount: wire.base_amount,
    price: wire.price,
    sizeDecimals: market.sizeDecimals,
    priceDecimals: market.priceDecimals,
    minBaseAmount: market.minBaseAmountRaw || undefined,
    minQuoteUsd: market.minQuoteUsdRaw || undefined,
  });
  check(
    `${market.symbol} $${wantUsd.toFixed(2)} passes min_quote`,
    !floatCheck.failing.includes('min_quote_amount'),
    `failing=${floatCheck.failing.join(',') || 'none'}`,
  );
  check(
    `${market.symbol} float and exact minimum checks agree`,
    floatCheck.ok === exactCheck.ok,
    `float=${floatCheck.ok} exact=${exactCheck.ok}`,
  );
  const plan = planOrderSize({
    intent: 'fullClose',
    size: market.minBaseAmount > 0 ? market.minBaseAmount / 2 : size / 2,
    px: Number(pxStr),
    sizeDecimals: market.sizeDecimals,
    minBaseAmount: market.minBaseAmount,
    minQuoteUsd: market.minQuoteUsd,
  });
  check(
    `${market.symbol} full close plan above both minimums`,
    plan.action === 'place' && plan.notional >= market.minQuoteUsd,
    JSON.stringify(plan),
  );

  // --- meta cache over the same transport --------------------------------------------------------
  const cache = createMarketCache({ fetchDetails: () => client.orderBookDetailsRaw(), ttlMs: 60_000 });
  const first = await cache.get();
  const second = await cache.get();
  check('meta cache serves one fetch twice', first === second && first.size === markets.size, `markets=${first.size}`);
}

main()
  .catch((err) => {
    failures++;
    console.error('FAIL smoke crashed:', err);
  })
  .finally(() => {
    console.log(failures === 0 ? 'SMOKE PASSED' : `SMOKE FAILED (${failures})`);
    process.exit(failures === 0 ? 0 : 1);
  });
All files