Skip to content
markpaper

scripts/smoke.ts

v0.2.0 · 5 KB

Download file
// Read-only live smoke test against PUBLIC mainnet data. No keys, no addresses of real accounts, no orders.
// Run: npm run smoke
//
// Exercises the package the way a user would: gateway client + throttle, network preflight (`contracts`),
// `symbols` decoding and the cache, quantization on real ticks/lots, `market_price`, and a
// `subaccount_info` read of the zero-address subaccount (a public, empty account).

import {
  CHAIN_ID,
  createGatewayClient,
  createQuantizer,
  createSymbolsCache,
  createWeightThrottle,
  marketMode,
  meetsMinSize,
  parseSubaccountInfo,
  parseX18,
  subaccountBytes32,
  verifyNetwork,
  x18ToDecimalString,
  ZERO_ADDRESS,
} from '../src/index.js';

let failures = 0;
function requiredPositiveEnv(name: string): number {
  const value = Number(process.env[name]);
  if (!Number.isFinite(value) || value <= 0) throw new Error(`${name} must be a positive number`);
  return value;
}

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 throttle = createWeightThrottle({
    queriesPerMinute: requiredPositiveEnv('NADO_QUERIES_PER_MINUTE'),
    queryBurst: requiredPositiveEnv('NADO_QUERY_BURST'),
    executesPerMinute: requiredPositiveEnv('NADO_EXECUTES_PER_MINUTE'),
    executeBurst: requiredPositiveEnv('NADO_EXECUTE_BURST'),
    maxConcurrent: requiredPositiveEnv('NADO_MAX_CONCURRENT'),
  });
  const client = createGatewayClient({ network: 'mainnet', throttle, timeoutMs: 15_000 });
  check('gateway url', client.url === 'https://gateway.prod.nado.xyz/v1', client.url);

  // --- Network preflight ------------------------------------------------------------------------
  const contracts = await verifyNetwork(client.query, CHAIN_ID.mainnet, { url: client.url });
  check(
    'contracts chain_id = 57073',
    contracts.chainId === CHAIN_ID.mainnet,
    `endpoint=${contracts.endpointAddr.slice(0, 6)}...`,
  );

  // --- Symbols ----------------------------------------------------------------------------------
  const cache = createSymbolsCache({ load: () => client.query({ type: 'symbols', product_type: 'perp' }) });
  const universe = await cache.get();
  check(
    'symbols decoded',
    universe.byCoin.size >= 50,
    `perps=${universe.byCoin.size} spotSkipped=${universe.skippedSpot.length}`,
  );
  check('symbols cache fresh', cache.isFresh());
  const btc = universe.byCoin.get('BTC');
  check(
    'BTC-PERP present',
    btc !== undefined,
    btc
      ? `id=${btc.productId} tick=${btc.tick} lot=${btc.lot} min=$${btc.minNotionalUsd} status=${btc.tradingStatus}`
      : 'missing',
  );
  const statuses = new Map<string, number>();
  for (const p of universe.byCoin.values()) statuses.set(p.tradingStatus, (statuses.get(p.tradingStatus) ?? 0) + 1);
  console.log(`     trading_status: ${[...statuses].map(([s, n]) => `${s}=${n}`).join(' ')}`);
  const unverified = [...universe.byCoin.values()]
    .filter((p) => !marketMode(p.tradingStatus).verified)
    .map((p) => `${p.coin}:${p.tradingStatus}`);
  check('all trading_status values known', unverified.length === 0, unverified.slice(0, 5).join(','));
  const nonCrypto = ['AAPL', 'NVDA', 'SPY', 'XAUT'].filter((c) => universe.byCoin.has(c));
  console.log(`     non-crypto perps seen: ${nonCrypto.join(',') || '(none of the sample)'}`);

  // --- Quantization on the real grid ----------------------------------------------------------
  if (btc) {
    const mp = await client.query<{ bid_x18: string; ask_x18: string }>({
      type: 'market_price',
      product_id: btc.productId,
    });
    const bid = parseX18(mp.bid_x18, 'bid');
    const ask = parseX18(mp.ask_x18, 'ask');
    check('BTC market_price sane', bid > 0 && ask >= bid, `bid=${bid} ask=${ask}`);
    const q = createQuantizer(btc);
    const px = q.priceToX18(bid * 0.97, 'floor');
    const sz = q.sizeToX18(120 / (bid * 0.97), 'ceil');
    check('price on tick', q.onGrid({ priceX18: px }), x18ToDecimalString(px));
    check('size on lot', q.onGrid({ sizeX18: sz }), x18ToDecimalString(sz));
    check(
      'notional >= min_size',
      meetsMinSize(px, sz, btc.minSizeX18),
      `${x18ToDecimalString(px)} x ${x18ToDecimalString(sz)}`,
    );
  }

  // --- Subaccount read (public zero address) --------------------------------------------------
  const zero = subaccountBytes32(ZERO_ADDRESS, 'default');
  const info = parseSubaccountInfo(await client.query({ type: 'subaccount_info', subaccount: zero }));
  check(
    'subaccount_info decodes',
    typeof info.exists === 'boolean',
    `exists=${info.exists} value=${info.accountValue}`,
  );

  console.log(`     throttle: ${JSON.stringify(throttle.stats())}`);
  console.log(
    `     counts: ${throttle
      .drainCounts()
      .map((c) => `${c.label}=${c.n}`)
      .join(' ')}`,
  );
}

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