Repository Public
1// Records real answers of the PUBLIC, READ-ONLY Hyperliquid endpoints (POST /info and websocket subscriptions) as fixtures.2// Nothing is signed, nothing is sent to /exchange. Account data is public on-chain state of an address picked from recent trades.3// Run from the repository root: node packages/derivatives/test/fixtures/hyperliquid/record.mts4import { writeFileSync } from 'node:fs';56const API = { mainnet: 'https://api.hyperliquid.xyz', testnet: 'https://api.hyperliquid-testnet.xyz' } as const;7const WS = { mainnet: 'wss://api.hyperliquid.xyz/ws', testnet: 'wss://api.hyperliquid-testnet.xyz/ws' } as const;8type Net = keyof typeof API;9const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));10const out = (name: string, v: unknown) => { writeFileSync(new URL(`./${name}.json`, import.meta.url), `${JSON.stringify(v, null, 1)}\n`); console.log(` ${name}.json`); };1112async function info(network: Net, request: Record<string, unknown>): Promise<{ status: number; body: any }> {13 const r = await fetch(`${API[network]}/info`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(request) });14 const text = await r.text();15 let body: unknown;16 try { body = JSON.parse(text); } catch { body = text; }17 await sleep(250);18 return { status: r.status, body };19}20async function record(network: Net, name: string, request: Record<string, unknown>, trim?: (body: any) => unknown) {21 const { status, body } = await info(network, request);22 out(`${network}-${name}`, { recordedAt: new Date().toISOString(), network, request, status, body: trim && status === 200 ? trim(body) : body });23 return body;24}2526/** an address with open orders (trigger orders if possible), positions and fills — public state, found through recent trades */27async function pickAccount(network: Net): Promise<string> {28 const users = new Set<string>();29 for (const coin of ['BTC', 'ETH', 'SOL']) for (const t of (await info(network, { type: 'recentTrades', coin })).body) for (const u of t.users) users.add(u);30 let best: { user: string; score: number } | null = null;31 for (const user of [...users].slice(0, 30)) {32 const orders = (await info(network, { type: 'frontendOpenOrders', user })).body, state = (await info(network, { type: 'clearinghouseState', user })).body;33 if (!Array.isArray(orders) || !state?.assetPositions) continue;34 const score = (orders.some((o: any) => o.isTrigger) ? 8 : 0) + (orders.some((o: any) => o.cloid) ? 2 : 0) + (orders.some((o: any) => !o.isTrigger) ? 3 : 0)35 + (state.assetPositions.length ? 3 : 0) + (state.assetPositions.some((p: any) => p.position.leverage.type === 'isolated') ? 2 : 0) + (state.assetPositions.some((p: any) => p.position.leverage.type === 'cross') ? 2 : 0)36 + (orders.length > 0 && orders.length <= 40 ? 2 : 0);37 if (!best || score > best.score) best = { user, score };38 if (score >= 20) break;39 }40 if (!best) throw new Error('no account found');41 return best.user;42}4344async function recordWs(network: Net, user: string) {45 const messages: unknown[] = [], perChannel = new Map<string, number>();46 const ws = new WebSocket(WS[network]);47 await new Promise<void>((resolve, reject) => { ws.onopen = () => resolve(); ws.onerror = () => reject(new Error('websocket error')); });48 ws.onmessage = (ev) => {49 const m = JSON.parse(String(ev.data)), n = perChannel.get(m.channel) ?? 0;50 perChannel.set(m.channel, n + 1);51 if (n >= (m.channel === 'subscriptionResponse' ? 20 : 3)) return;52 if (m.channel === 'trades' && Array.isArray(m.data)) m.data = m.data.slice(0, 12);53 if (m.channel === 'userFills' && m.data?.fills) m.data.fills = m.data.fills.slice(0, 12);54 if (m.channel === 'allMids' && m.data?.mids) m.data.mids = Object.fromEntries(Object.entries(m.data.mids).filter(([k]) => !/^[#@+]/.test(k)).slice(0, 40));55 if (m.channel === 'orderUpdates' && Array.isArray(m.data)) m.data = m.data.slice(0, 12);56 messages.push(m);57 };58 const subs = [{ type: 'l2Book', coin: 'BTC' }, { type: 'trades', coin: 'BTC' }, { type: 'activeAssetCtx', coin: 'BTC' }, { type: 'allMids' },59 { type: 'userFills', user }, { type: 'orderUpdates', user }, { type: 'userEvents', user }];60 for (const subscription of subs) ws.send(JSON.stringify({ method: 'subscribe', subscription }));61 ws.send(JSON.stringify({ method: 'ping' }));62 // answered by an `error` channel message. NOT sent here: subscribing to an unknown coin, which makes the server drop the63 // connection without a message (close 1006, observed 2026-11-10) — the adapter checks symbols before subscribing.64 ws.send('not json');65 ws.send(JSON.stringify({ method: 'subscribe', subscription: { type: 'l2Book', coin: 'BTC' } }));66 await sleep(9000);67 ws.close();68 out(`${network}-ws`, { recordedAt: new Date().toISOString(), network, subscriptions: subs, counts: Object.fromEntries(perChannel), messages });69}7071for (const network of ['mainnet', 'testnet'] as Net[]) {72 console.log(network);73 const now = Date.now();74 await record(network, 'meta', { type: 'meta' }, (b) => ({ ...b, marginTables: b.marginTables?.slice(0, 2) }));75 await record(network, 'metaAndAssetCtxs', { type: 'metaAndAssetCtxs' }, (b) => [{ ...b[0], marginTables: b[0].marginTables?.slice(0, 2) }, b[1]]);76 await record(network, 'l2Book-BTC', { type: 'l2Book', coin: 'BTC' });77 await record(network, 'recentTrades-BTC', { type: 'recentTrades', coin: 'BTC' });78 await record(network, 'candleSnapshot-BTC-1h', { type: 'candleSnapshot', req: { coin: 'BTC', interval: '1h', startTime: now - 24 * 3_600_000, endTime: now } });79 await record(network, 'fundingHistory-BTC', { type: 'fundingHistory', coin: 'BTC', startTime: now - 12 * 3_600_000 });80 await record(network, 'error-unknown-coin', { type: 'l2Book', coin: 'NOT-A-COIN' });81 await record(network, 'error-bad-type', { type: 'notAnInfoType' });82 const user = await pickAccount(network);83 console.log(` account ${user}`);84 await record(network, 'clearinghouseState', { type: 'clearinghouseState', user });85 await record(network, 'frontendOpenOrders', { type: 'frontendOpenOrders', user }, (b) => b.slice(0, 40));86 await record(network, 'historicalOrders', { type: 'historicalOrders', user }, (b) => b.slice(0, 80));87 await record(network, 'userFills', { type: 'userFills', user }, (b) => b.slice(0, 40));88 await record(network, 'userFillsByTime', { type: 'userFillsByTime', user, startTime: now - 24 * 3_600_000 }, (b) => b.slice(0, 40));89 await record(network, 'userFunding', { type: 'userFunding', user, startTime: now - 24 * 3_600_000 }, (b) => b.slice(0, 40));90 await record(network, 'clearinghouseState-empty', { type: 'clearinghouseState', user: '0x00000000000000000000000000000000000000ff' });91 await record(network, 'error-bad-user', { type: 'clearinghouseState', user: 'not-an-address' });92 await recordWs(network, user);93}94