// Offline mock of the Hyperliquid public endpoints (leaderboard, info API on two hosts).
// fail: set of 'leaderboard' | 'meta' | 'state' (all accounts) | 'state:half' | 'state:some' (30%) | 'fills' | 'primary' (api host down) | 'all'
const A = (n) => `0x${String(n).padStart(40, '0')}`;
const W = (mp, ap, mv = 1e6) => [['day', { pnl: '1', roi: '0', vlm: '1' }], ['week', { pnl: String(mp / 4), roi: '0.01', vlm: '1' }], ['month', { pnl: String(mp), roi: '0.2', vlm: String(mv) }], ['allTime', { pnl: String(ap), roi: '0.5', vlm: '1' }]];
// 10 eligible accounts (A1..A10, month PnL descending by index) + 4 that fail the rules
export const LEADERBOARD = { leaderboardRows: [
...Array.from({ length: 10 }, (_, i) => ({ ethAddress: A(i + 1), accountValue: String(1_000_000 + i), windowPerformances: W(1_000_000 - i * 10_000, 5_000_000), prize: 0, displayName: i === 0 ? 'top dog' : null })),
{ ethAddress: A(90), accountValue: '50000', windowPerformances: W(900_000, 1_000_000), prize: 0, displayName: null }, // too small
{ ethAddress: A(91), accountValue: '5000000', windowPerformances: W(-5, 1_000_000), prize: 0, displayName: null }, // month loss
{ ethAddress: A(92), accountValue: '5000000', windowPerformances: W(2_000_000, -1), prize: 0, displayName: null }, // all-time loss
{ ethAddress: A(93), accountValue: '1000000', windowPerformances: W(3_000_000, 9_000_000, 5e9), prize: 0, displayName: null }, // MM-like volume
...Array.from({ length: 100 }, (_, i) => ({ ethAddress: A(1000 + i), accountValue: '10', windowPerformances: W(1, 1), prize: 0, displayName: null })), // filler (rows >= 100)
] };
const META = [{ universe: [{ name: 'BTC' }, { name: 'ETH' }, { name: 'SOL' }, { name: 'kPEPE' }, { name: 'OLD', isDelisted: true }] }, [{ markPx: '80000' }, { markPx: '3000' }, { markPx: '150' }, { markPx: '0.01' }, { markPx: '1' }]];
const pos = (coin, szi, px, lev, liq, upnl = 0) => ({ type: 'oneWay', position: { coin, szi: String(szi), entryPx: String(px), positionValue: String(Math.abs(szi) * (coin === 'BTC' ? 80000 : coin === 'ETH' ? 3000 : 150)), leverage: { type: 'cross', value: lev }, liquidationPx: liq === null ? null : String(liq), unrealizedPnl: String(upnl) } });
// BTC: A1..A4 short (A1 huge), A5 long -> strong_short; ETH: A2,A3,A6 long, A7 short -> lean/strong long; SOL: only A8 long (insufficient)
const POS = {
[A(1)]: [pos('BTC', -100, 79000, 5, 95000, -100000)],
[A(2)]: [pos('BTC', -1, 82000, 3, 120000, 2000), pos('ETH', 50, 2900, 4, 2300, 5000)],
[A(3)]: [pos('BTC', -2, 81000, 10, 87000), pos('ETH', 40, 2950, 5, 2500)],
[A(4)]: [pos('BTC', -1, 80500, 2, null)],
[A(5)]: [pos('BTC', 1, 78000, 20, 78500)],
[A(6)]: [pos('ETH', 30, 3100, 3, 1500)],
[A(7)]: [pos('ETH', -10, 3050, 2, 4500)],
[A(8)]: [pos('SOL', 100, 140, 2, 80)],
};
export const state = (a) => ({ marginSummary: { accountValue: '1000000', totalNtlPos: '500000', totalMarginUsed: '100000' }, assetPositions: POS[a] || [], time: 1 });
const FILLS = [{ coin: 'BTC', px: '80000', sz: '1', closedPnl: '0', fee: '20' }, { coin: 'BTC', px: '81000', sz: '1', closedPnl: '1000', fee: '20' }, { coin: 'ETH', px: '3000', sz: '10', closedPnl: '-200', fee: '6' }];
export function chunked(text, size = 97) {
const enc = new TextEncoder().encode(text); let i = 0;
return new Response(new ReadableStream({ pull(c) { if (i >= enc.length) return c.close(); c.enqueue(enc.slice(i, i + size)); i += size; } }), { headers: { 'content-type': 'application/json' } });
}
export function mockHL({ fail = new Set(), calls = [] } = {}) {
let stateN = 0;
const fetchImpl = async (url, init = {}) => {
const u = new URL(String(url)); calls.push(u.href);
if (fail.has('all')) return new Response('down', { status: 503 });
if (u.host === 'stats-data.hyperliquid.xyz') {
if (fail.has('leaderboard')) return new Response('busy', { status: 503 });
if ((init.method || 'GET') === 'HEAD') return new Response(null, { status: 200 });
return chunked(JSON.stringify(LEADERBOARD, null, 2));
}
if (u.host === 'api.hyperliquid.xyz' || u.host === 'api-ui.hyperliquid.xyz') {
if (fail.has('primary') && u.host === 'api.hyperliquid.xyz') return new Response('busy', { status: 429 });
const b = JSON.parse(init.body || '{}');
if (b.type === 'metaAndAssetCtxs') return fail.has('meta') ? new Response('x', { status: 500 }) : Response.json(META);
if (b.type === 'meta') return Response.json(META[0]);
if (b.type === 'clearinghouseState') {
if (fail.has('state')) return new Response('x', { status: 500 });
if (u.host === 'api.hyperliquid.xyz') stateN++;
const idx = Number(BigInt(b.user)); // A(n) -> n
if (fail.has('state:half') && idx % 2 === 0) return new Response('x', { status: 500 });
if (fail.has('state:some') && idx % 3 === 0) return new Response('x', { status: 500 });
return Response.json(state(b.user));
}
if (b.type === 'userFillsByTime') return fail.has('fills') ? new Response('x', { status: 500 }) : Response.json(FILLS);
}
throw new TypeError(`no network in tests: ${u.href}`);
};
return { calls, fetchImpl };
}
export const mockVenues = mockHL; // name used by the shared publicbase / cdp tests
export { A };
agentgate-source: code mirror on Cloudflare Pages. All repos