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