scripts/smoke.ts
v0.2.1 · 5.6 KB
// 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);
});