Repository Public
1// Hyperliquid read side against real answers recorded from the public info endpoint of mainnet and testnet2// (fixtures/hyperliquid/record.mts, 2026-11-10/11). No network.3import { describe, expect, it } from 'vitest';4import { D, VenueError, AccountStateSchema, BookSchema, CandleSchema, FillSchema, FundingPaymentSchema, InstrumentSchema, OrderSchema, PositionSchema, PublicTradeSchema, TickerSchema } from '../../src/index.ts';5import { HlWeightBudget, createHyperliquidAdapter, hlExchangeWeight, hlInfoExtraWeight, hlInfoWeight, hlOrderStatus } from '../../src/venues/hyperliquid/index.ts';6import { fx, replay, type Route } from './helpers.ts';78const NETWORKS = ['mainnet', 'testnet'] as const;9const routesOf = (network: 'mainnet' | 'testnet', extra: Record<string, Route> = {}): Record<string, Route> => {10 const f = (n: string) => { const x = fx(`${network}-${n}`); return { status: x.status, body: x.body }; };11 return {12 meta: f('meta'), metaAndAssetCtxs: f('metaAndAssetCtxs'), l2Book: f('l2Book-BTC'), recentTrades: f('recentTrades-BTC'), candleSnapshot: f('candleSnapshot-BTC-1h'),13 fundingHistory: f('fundingHistory-BTC'), clearinghouseState: f('clearinghouseState'), frontendOpenOrders: f('frontendOpenOrders'), historicalOrders: f('historicalOrders'),14 userFills: f('userFills'), userFillsByTime: f('userFillsByTime'), userFunding: f('userFunding'), ...extra,15 };16};17const make = (network: 'mainnet' | 'testnet', extra: Record<string, Route> = {}, now = Date.parse(fx(`${network}-meta`).recordedAt)) => {18 const r = replay(routesOf(network, extra));19 let t = now;20 const adapter = createHyperliquidAdapter({ network, fetch: r.fetchImpl, now: () => t });21 return { adapter, calls: r.calls, advance: (ms: number) => { t += ms; } };22};23const canon = (s: string) => D.str(D.from(s));24const code = async (p: Promise<unknown>): Promise<string> => { try { await p; return 'no error'; } catch (e) { return e instanceof VenueError ? e.code : `not a VenueError: ${e}`; } };2526describe.each(NETWORKS)('hyperliquid read side — %s fixtures', (network) => {27 const account = (fx(`${network}-clearinghouseState`).request as { user: string }).user;2829 it('instruments: lot from szDecimals, finest tick, leverage, isolated-only and delisted flags', async () => {30 const { adapter, calls } = make(network);31 const list = await adapter.instruments(), rawU = fx(`${network}-meta`).body.universe as any[];32 expect(calls[0]!.url).toBe(`https://api.hyperliquid${network === 'testnet' ? '-testnet' : ''}.xyz/info`);33 expect(calls[0]!.body).toEqual({ type: 'meta' });34 expect(list.length).toBe(rawU.length);35 for (const i of list) InstrumentSchema.parse(i);36 const btc = list.find((i) => i.symbol === 'BTC')!, rawBtc = rawU.find((u) => u.name === 'BTC');37 expect(rawBtc.szDecimals).toBe(5);38 expect(btc).toMatchObject({ id: 'hyperliquid:BTC', venue: 'hyperliquid', base: 'BTC', quote: 'USD', kind: 'perpetual', collateral: 'USDC', lotSize: '0.00001', tickSize: '0.1', minNotional: '10', minOrderSize: null,39 maxLeverage: String(rawBtc.maxLeverage), marginModes: ['cross', 'isolated'], fundingIntervalSec: 3600, status: 'active' });40 for (const u of rawU) {41 const i = list.find((x) => x.symbol === u.name)!;42 expect(i.status).toBe(u.isDelisted ? 'halted' : 'active');43 expect(i.marginModes).toEqual(u.onlyIsolated || u.marginMode === 'strictIsolated' || u.marginMode === 'noCross' ? ['isolated'] : ['cross', 'isolated']);44 expect(D.str(D.mul(D.from(i.lotSize), D.from(`1${'0'.repeat(u.szDecimals)}`)))).toBe('1');45 expect(D.str(D.mul(D.from(i.tickSize), D.from(`1${'0'.repeat(6 - u.szDecimals)}`)))).toBe('1');46 }47 expect(list.some((i) => i.status === 'halted')).toBe(true);48 expect(list.some((i) => i.marginModes.length === 1)).toBe(true);49 });5051 it('tickers: mark, oracle as index, mid, hourly funding, open interest, 24 h notional volume and change', async () => {52 const { adapter } = make(network);53 const t = await adapter.tickers(), [meta, ctxs] = fx(`${network}-metaAndAssetCtxs`).body as [any, any[]];54 expect(t.length).toBe(meta.universe.length);55 for (const x of t) TickerSchema.parse(x);56 const i = meta.universe.findIndex((u: any) => u.name === 'BTC'), btc = t[i]!, c = ctxs[i];57 expect(btc).toMatchObject({ instrument: 'hyperliquid:BTC', mark: canon(c.markPx), index: canon(c.oraclePx), mid: canon(c.midPx), funding: canon(c.funding), openInterest: canon(c.openInterest), volume24h: canon(c.dayNtlVlm) });58 // change = (mark − prevDayPx) / prevDayPx rounded to 6 decimals: |change × prev − (mark − prev)| ≤ prev × 0.000000559 const mark = D.from(canon(c.markPx)), prev = D.from(canon(c.prevDayPx));60 const residual = D.abs(D.sub(D.mul(D.from(btc.change24h!), prev), D.sub(mark, prev)));61 expect(D.cmp(residual, D.mul(prev, D.from('0.0000005')))).toBeLessThanOrEqual(0);62 expect(D.from(btc.change24h!).s).toBeLessThanOrEqual(6);63 // an asset without a book has no mid and no premium: null, not zero64 const dead = ctxs.findIndex((x: any) => x.midPx === null);65 if (dead >= 0) expect(t[dead]!.mid).toBeNull();66 expect(typeof JSON.stringify(t)).toBe('string');67 for (const x of t) for (const k of ['mark', 'index', 'mid', 'funding', 'openInterest', 'volume24h', 'change24h'] as const) expect(x[k] === null || typeof x[k] === 'string').toBe(true);68 });6970 it('book: bids descending, asks ascending, depth cut, venue timestamp', async () => {71 const { adapter, calls } = make(network);72 const b = await adapter.book('BTC'), rawB = fx(`${network}-l2Book-BTC`).body;73 BookSchema.parse(b);74 expect(calls.at(-1)!.body).toEqual({ type: 'l2Book', coin: 'BTC' });75 expect(b.ts).toBe(rawB.time);76 expect(b.bids.length).toBe(rawB.levels[0].length);77 expect(b.bids[0]).toEqual([canon(rawB.levels[0][0].px), canon(rawB.levels[0][0].sz)]);78 expect(b.asks[0]).toEqual([canon(rawB.levels[1][0].px), canon(rawB.levels[1][0].sz)]);79 expect(D.cmp(D.from(b.bids[0]![0]), D.from(b.asks[0]![0]))).toBe(-1);80 const top = await adapter.book('BTC', 3);81 expect([top.bids.length, top.asks.length]).toEqual([3, 3]);82 });8384 it('trades: side of the aggressor, unique id from (time, tid), newest first', async () => {85 const { adapter } = make(network);86 const t = await adapter.trades('BTC'), rawT = fx(`${network}-recentTrades-BTC`).body as any[];87 expect(t.length).toBe(rawT.length);88 for (const x of t) PublicTradeSchema.parse(x);89 const r0 = rawT[0], mine = t.find((x) => x.id === `${r0.time}-${r0.tid}`)!;90 expect(mine).toMatchObject({ instrument: 'hyperliquid:BTC', price: canon(r0.px), size: canon(r0.sz), side: r0.side === 'B' ? 'buy' : 'sell', ts: r0.time });91 for (let i = 1; i < t.length; i++) expect(t[i - 1]!.ts).toBeGreaterThanOrEqual(t[i]!.ts);92 expect((await adapter.trades('BTC', 2)).length).toBe(2);93 });9495 it('candles: request shape, ascending, limit', async () => {96 const { adapter, calls } = make(network);97 const end = Date.parse(fx(`${network}-candleSnapshot-BTC-1h`).recordedAt);98 const c = await adapter.candles('BTC', '1h', { end, limit: 10 }), rawC = fx(`${network}-candleSnapshot-BTC-1h`).body as any[];99 expect(calls.at(-1)!.body).toEqual({ type: 'candleSnapshot', req: { coin: 'BTC', interval: '1h', startTime: end - 10 * 3_600_000, endTime: end } });100 expect(c.length).toBe(10);101 for (const x of c) CandleSchema.parse(x);102 const last = rawC.at(-1);103 expect(c.at(-1)).toEqual({ ts: last.t, open: canon(last.o), high: canon(last.h), low: canon(last.l), close: canon(last.c), volume: canon(last.v) });104 for (let i = 1; i < c.length; i++) expect(c[i]!.ts - c[i - 1]!.ts).toBe(3_600_000);105 expect(await code(adapter.candles('BTC', '2h' as never, {}))).toBe('unsupported');106 expect(await code(adapter.candles('BTC', '1h', { limit: 5001 }))).toBe('unsupported');107 });108109 it('funding history: hourly rate and premium', async () => {110 const { adapter, calls } = make(network);111 const f = await adapter.fundingHistory('BTC', { start: 1 }), rawF = fx(`${network}-fundingHistory-BTC`).body as any[];112 expect(calls.at(-1)!.body).toEqual({ type: 'fundingHistory', coin: 'BTC', startTime: 1 });113 expect(f.length).toBe(rawF.length);114 expect(f[0]).toEqual({ ts: rawF[0].time, rate: canon(rawF[0].fundingRate), premium: canon(rawF[0].premium) });115 await adapter.fundingHistory('BTC', { start: 1, end: 2 });116 expect(calls.at(-1)!.body).toEqual({ type: 'fundingHistory', coin: 'BTC', startTime: 1, endTime: 2 });117 });118119 it('account state: account value, margin used, withdrawable, cross maintenance margin', async () => {120 const { adapter, calls } = make(network);121 const a = await adapter.account(account.toUpperCase().replace('0X', '0x')), s = fx(`${network}-clearinghouseState`).body;122 AccountStateSchema.parse(a);123 expect(calls.at(-1)!.body).toEqual({ type: 'clearinghouseState', user: account.toLowerCase() });124 expect(a).toMatchObject({ venue: 'hyperliquid', account: account.toLowerCase(), equity: canon(s.marginSummary.accountValue), marginUsed: canon(s.marginSummary.totalMarginUsed),125 withdrawable: canon(s.withdrawable), maintenanceMargin: canon(s.crossMaintenanceMarginUsed), ts: s.time, source: 'venue' });126 const upnl = (s.assetPositions as any[]).reduce((n, p) => D.add(n, D.from(canon(p.position.unrealizedPnl))), D.from('0'));127 expect(a.collateral).toBe(D.str(D.sub(D.from(a.equity), upnl)));128 expect(D.str(D.add(D.from(a.freeCollateral), D.from(a.marginUsed)))).toBe(a.equity);129 });130131 it('positions: side, size, entry, venue mark, liquidation price, leverage, margin mode, PnL, funding', async () => {132 const { adapter } = make(network);133 const p = await adapter.positions(account), s = fx(`${network}-clearinghouseState`).body, [meta, ctxs] = fx(`${network}-metaAndAssetCtxs`).body as [any, any[]];134 expect(p.length).toBe(s.assetPositions.length);135 expect(p.length).toBeGreaterThan(0);136 for (const x of p) PositionSchema.parse(x);137 for (const { position: r } of s.assetPositions as any[]) {138 const x = p.find((y) => y.instrument === `hyperliquid:${r.coin}`)!, szi = D.from(canon(r.szi));139 expect(x).toMatchObject({ side: szi.m > 0n ? 'long' : 'short', size: D.str(D.abs(szi)), entryPrice: canon(r.entryPx), liquidationPrice: r.liquidationPx === null ? null : canon(r.liquidationPx),140 leverage: String(r.leverage.value), marginMode: r.leverage.type, margin: canon(r.marginUsed), unrealizedPnl: canon(r.unrealizedPnl), realizedPnl: null, fundingPaid: canon(r.cumFunding.sinceOpen), source: 'venue' });141 expect(x.markPrice).toBe(canon(ctxs[meta.universe.findIndex((u: any) => u.name === r.coin)].markPx));142 }143 });144145 it('an account without positions: empty list, no second request', async () => {146 const empty = fx(`${network}-clearinghouseState-empty`);147 const { adapter, calls } = make(network, { clearinghouseState: { body: empty.body } });148 expect(await adapter.positions(account)).toEqual([]);149 expect(calls.map((c) => c.body.type)).toEqual(['clearinghouseState']);150 const a = await adapter.account(account);151 expect(a.marginUsed).toBe('0');152 expect(a.equity).toBe(a.collateral);153 });154155 it('open orders: trigger orders, reduce-only, time in force, cloid', async () => {156 const { adapter, calls } = make(network);157 const o = await adapter.openOrders(account), rawO = fx(`${network}-frontendOpenOrders`).body as any[];158 expect(calls.at(-1)!.body).toEqual({ type: 'frontendOpenOrders', user: account.toLowerCase() });159 expect(o.length).toBe(rawO.length);160 expect(o.length).toBeGreaterThan(0);161 for (const x of o) OrderSchema.parse(x);162 const types: Record<string, string> = { Limit: 'limit', 'Stop Market': 'stop_market', 'Stop Limit': 'stop_limit', 'Take Profit Market': 'take_profit_market', 'Take Profit Limit': 'take_profit_limit', Market: 'market' };163 for (const r of rawO) {164 const x = o.find((y) => y.venueOrderId === String(r.oid))!;165 expect(x).toMatchObject({ instrument: `hyperliquid:${r.coin}`, side: r.side === 'B' ? 'buy' : 'sell', type: types[r.orderType], size: canon(r.origSz), price: canon(r.limitPx), reduceOnly: r.reduceOnly,166 triggerPrice: r.isTrigger ? canon(r.triggerPx) : null, tif: r.tif === null ? 'gtc' : r.tif.toLowerCase(), clientId: r.cloid ?? `oid:${r.oid}`, createdAt: r.timestamp, rejectReason: null });167 expect(x.filled).toBe(D.str(D.sub(D.from(canon(r.origSz)), D.from(canon(r.sz)))));168 expect(x.status).toBe(x.filled === '0' ? 'open' : 'partially_filled');169 }170 expect(o.some((x) => x.triggerPrice !== null && x.reduceOnly)).toBe(true);171 });172173 it('order history: one entry per order with its latest status', async () => {174 const { adapter } = make(network);175 const h = await adapter.orderHistory(account), rawH = fx(`${network}-historicalOrders`).body as any[];176 for (const x of h) OrderSchema.parse(x);177 expect(new Set(h.map((x) => x.venueOrderId)).size).toBe(h.length);178 expect(h.length).toBe(new Set(rawH.map((x) => x.order.oid)).size);179 const statuses = new Set(h.map((x) => x.status));180 expect(statuses.has('filled') || statuses.has('cancelled')).toBe(true);181 const cancelledByVenue = rawH.find((x) => /Canceled$/.test(x.status));182 if (cancelledByVenue) expect(h.find((x) => x.venueOrderId === String(cancelledByVenue.order.oid))).toMatchObject({ status: 'cancelled', rejectReason: cancelledByVenue.status });183 const filled = rawH.find((x) => x.status === 'filled');184 if (filled) expect(h.find((x) => x.venueOrderId === String(filled.order.oid))).toMatchObject({ status: 'filled', filled: canon(filled.order.origSz), size: canon(filled.order.origSz) });185 for (let i = 1; i < h.length; i++) expect(h[i - 1]!.updatedAt).toBeGreaterThanOrEqual(h[i]!.updatedAt);186 expect((await adapter.orderHistory(account, { limit: 3 })).length).toBe(3);187 });188189 it('fills: userFills, and userFillsByTime when a start is given', async () => {190 const { adapter, calls } = make(network);191 const f = await adapter.fills(account), rawF = fx(`${network}-userFills`).body as any[];192 expect(calls.at(-1)!.body).toEqual({ type: 'userFills', user: account.toLowerCase() });193 expect(f.length).toBe(rawF.length);194 for (const x of f) FillSchema.parse(x);195 expect(new Set(f.map((x) => x.id)).size).toBe(f.length);196 const r = rawF[0], x = f.find((y) => y.id === `${r.hash}-${r.tid}`)!;197 expect(x).toMatchObject({ instrument: `hyperliquid:${r.coin}`, orderId: String(r.oid), clientId: r.cloid ?? null, side: r.side === 'B' ? 'buy' : 'sell', price: canon(r.px), size: canon(r.sz), fee: canon(r.fee),198 feeAsset: r.feeToken, closedPnl: canon(r.closedPnl), ts: r.time, liquidation: false });199 const g = await adapter.fills(account, { start: 123, limit: 5 });200 expect(calls.at(-1)!.body).toEqual({ type: 'userFillsByTime', user: account.toLowerCase(), startTime: 123 });201 expect(g.length).toBe(5);202 });203204 it('a liquidation fill is flagged', async () => {205 const body = structuredClone(fx(`${network}-userFills`).body);206 body[0].liquidation = { liquidatedUser: account, markPx: '1.0', method: 'market' };207 const { adapter } = make(network, { userFills: { body } });208 const f = await adapter.fills(account);209 expect(f.filter((x) => x.liquidation).length).toBe(1);210 });211212 it('funding payments: signed amount, rate, signed position', async () => {213 const { adapter, calls, advance } = make(network);214 const f = await adapter.funding(account, { start: 5 }), rawF = fx(`${network}-userFunding`).body as any[];215 expect(calls.at(-1)!.body).toEqual({ type: 'userFunding', user: account.toLowerCase(), startTime: 5 });216 expect(f.length).toBe(rawF.length);217 for (const x of f) FundingPaymentSchema.parse(x);218 const r = rawF[0];219 expect(f.find((x) => x.ts === r.time && x.instrument === `hyperliquid:${r.delta.coin}`)).toEqual({ instrument: `hyperliquid:${r.delta.coin}`, amount: canon(r.delta.usdc), rate: canon(r.delta.fundingRate), size: canon(r.delta.szi), ts: r.time });220 advance(0);221 await adapter.funding(account);222 expect(calls.at(-1)!.body.startTime).toBe(Date.parse(fx(`${network}-meta`).recordedAt) - 7 * 86_400_000);223 });224225 it('recorded error answers: unknown coin is HTTP 500 "null", bad type and bad user are HTTP 422', async () => {226 const unknown = fx(`${network}-error-unknown-coin`), badType = fx(`${network}-error-bad-type`), badUser = fx(`${network}-error-bad-user`);227 expect([unknown.status, unknown.body]).toEqual([500, null]);228 expect([badType.status, badUser.status]).toEqual([422, 422]);229 expect(badType.body).toBe('Failed to deserialize the JSON body into the target type');230 // the adapter never sends an unknown coin or a malformed address: both are refused locally231 const { adapter, calls } = make(network);232 expect(await code(adapter.book('NOT-A-COIN'))).toBe('unsupported');233 expect(calls.map((c) => c.body.type)).toEqual(['meta']);234 expect(await code(adapter.account('not-an-address'))).toBe('unsupported');235 expect(calls.length).toBe(1);236 // and if the venue answers that way anyway237 const a = make(network, { l2Book: { status: unknown.status, body: 'null' } });238 expect(await code(a.adapter.book('BTC'))).toBe('unavailable');239 const b = make(network, { clearinghouseState: { status: 422, body: badUser.body } });240 expect(await code(b.adapter.account(account))).toBe('bad_answer');241 });242});243244describe('hyperliquid read side — malformed, delayed and rate-limited answers', () => {245 const account = (fx('mainnet-clearinghouseState').request as { user: string }).user;246 const patched = (name: string, patch: (b: any) => void) => { const b = structuredClone(fx(`mainnet-${name}`).body); patch(b); return { body: b }; };247248 it.each([249 ['meta without szDecimals', 'instruments', { meta: patched('meta', (b) => { delete b.universe[0].szDecimals; }) }],250 ['meta with a float leverage', 'instruments', { meta: patched('meta', (b) => { b.universe[0].maxLeverage = 2.5; }) }],251 ['a price sent as a JSON number', 'book', { l2Book: patched('l2Book-BTC', (b) => { b.levels[0][0].px = 73699.5; }) }],252 ['a price in exponent notation', 'book', { l2Book: patched('l2Book-BTC', (b) => { b.levels[0][0].px = '7.3e4'; }) }],253 ['a crossed book', 'book', { l2Book: patched('l2Book-BTC', (b) => { b.levels[0][0].px = '999999999'; }) }],254 ['bids out of order', 'book', { l2Book: patched('l2Book-BTC', (b) => { b.levels[0].reverse(); }) }],255 ['a book for another coin', 'book', { l2Book: patched('l2Book-BTC', (b) => { b.coin = 'ETH'; }) }],256 ['a book without time', 'book', { l2Book: patched('l2Book-BTC', (b) => { delete b.time; }) }],257 ['contexts shorter than the universe', 'tickers', { metaAndAssetCtxs: patched('metaAndAssetCtxs', (b) => { b[1].pop(); }) }],258 ['a context without mark price', 'tickers', { metaAndAssetCtxs: patched('metaAndAssetCtxs', (b) => { delete b[1][0].markPx; }) }],259 ['a trade with an unknown side', 'trades', { recentTrades: patched('recentTrades-BTC', (b) => { b[0].side = 'X'; }) }],260 ['a candle with low above high', 'candles', { candleSnapshot: patched('candleSnapshot-BTC-1h', (b) => { b[0].l = '999999999'; }) }],261 ['an account state without withdrawable', 'account', { clearinghouseState: patched('clearinghouseState', (b) => { delete b.withdrawable; }) }],262 ['a position with a non-numeric size', 'positions', { clearinghouseState: patched('clearinghouseState', (b) => { b.assetPositions[0].position.szi = 'NaN'; }) }],263 ['an order with an unknown status', 'orderHistory', { historicalOrders: patched('historicalOrders', (b) => { b[0].status = 'somethingNew'; }) }],264 ['a fill without a trade id', 'fills', { userFills: patched('userFills', (b) => { delete b[0].tid; }) }],265 ['a funding payment without rate', 'funding', { userFunding: patched('userFunding', (b) => { delete b[0].delta.fundingRate; }) }],266 ['an HTML page', 'instruments', { meta: { body: '<html>maintenance</html>' } }],267 ['an empty body', 'instruments', { meta: { body: '' } }],268 ['null', 'tickers', { metaAndAssetCtxs: { body: 'null' } }],269 ] as [string, string, Record<string, Route>][])('%s → bad_answer', async (_n, method, extra) => {270 const { adapter } = make('mainnet', extra);271 const call: Record<string, () => Promise<unknown>> = {272 instruments: () => adapter.instruments(), tickers: () => adapter.tickers(), book: () => adapter.book('BTC'), trades: () => adapter.trades('BTC'), candles: () => adapter.candles('BTC', '1h', {}),273 account: () => adapter.account(account), positions: () => adapter.positions(account), orderHistory: () => adapter.orderHistory(account), fills: () => adapter.fills(account), funding: () => adapter.funding(account),274 };275 expect(await code(call[method]!())).toBe('bad_answer');276 });277278 it('unknown fields are tolerated', async () => {279 const { adapter } = make('mainnet', { l2Book: patched('l2Book-BTC', (b) => { b.somethingNew = { a: 1 }; b.levels[0][0].extra = true; }) });280 expect((await adapter.book('BTC')).bids.length).toBeGreaterThan(0);281 });282283 it('an order type that cannot be represented is not hidden: unsupported', async () => {284 const { adapter } = make('mainnet', { frontendOpenOrders: patched('frontendOpenOrders', (b) => { b[0].orderType = 'Trailing Stop'; }) });285 expect(await code(adapter.openOrders(account))).toBe('unsupported');286 });287288 it('a delayed answer is cut at the timeout: unavailable', async () => {289 const fetchImpl = ((_u: string, init: RequestInit) => new Promise((_res, rej) => { init.signal!.addEventListener('abort', () => rej(init.signal!.reason)); })) as unknown as typeof fetch;290 const adapter = createHyperliquidAdapter({ network: 'mainnet', fetch: fetchImpl, timeoutMs: 30 });291 const t0 = Date.now();292 try { await adapter.instruments(); expect.unreachable(); }293 catch (e) { expect(e).toBeInstanceOf(VenueError); expect((e as VenueError).code).toBe('unavailable'); expect(((e as VenueError).detail as Record<string, any>).timeout).toBe(true); }294 expect(Date.now() - t0).toBeLessThan(2000);295 });296297 it('a network failure and an HTTP 502 are unavailable', async () => {298 const down = createHyperliquidAdapter({ network: 'mainnet', fetch: (async () => { throw new TypeError('fetch failed'); }) as unknown as typeof fetch });299 expect(await code(down.instruments())).toBe('unavailable');300 expect(await code(make('mainnet', { meta: { status: 502, body: 'Bad Gateway' } }).adapter.instruments())).toBe('unavailable');301 });302303 it('HTTP 429 is rate_limited and closes the local budget for a minute', async () => {304 const { adapter, advance } = make('mainnet', { meta: { status: 429, body: '' } });305 try { await adapter.instruments(); expect.unreachable(); }306 catch (e) { expect((e as VenueError).code).toBe('rate_limited'); expect(((e as VenueError).detail as Record<string, any>).retryAfterMs).toBe(60_000); }307 expect(adapter.http.budget.used()).toBe(1200);308 try { await adapter.book('BTC'); expect.unreachable(); }309 catch (e) { expect((e as VenueError).code).toBe('rate_limited'); expect(((e as VenueError).detail as Record<string, any>).local).toBe(true); }310 advance(60_001);311 expect(adapter.http.budget.used()).toBe(0);312 });313314 it('documented weights: 2 / 20 / 60, per-item surcharges, exchange batches', () => {315 for (const t of ['l2Book', 'allMids', 'clearinghouseState', 'orderStatus', 'spotClearinghouseState', 'exchangeStatus']) expect(hlInfoWeight(t)).toBe(2);316 for (const t of ['meta', 'metaAndAssetCtxs', 'recentTrades', 'candleSnapshot', 'frontendOpenOrders', 'historicalOrders', 'userFills', 'userFunding', 'fundingHistory']) expect(hlInfoWeight(t)).toBe(20);317 expect(hlInfoWeight('userRole')).toBe(60);318 expect(hlInfoExtraWeight('userFills', 2000)).toBe(100);319 expect(hlInfoExtraWeight('recentTrades', 19)).toBe(0);320 expect(hlInfoExtraWeight('candleSnapshot', 5000)).toBe(83);321 expect(hlInfoExtraWeight('meta', 5000)).toBe(0);322 expect([hlExchangeWeight(1), hlExchangeWeight(39), hlExchangeWeight(40), hlExchangeWeight(79), hlExchangeWeight(80)]).toEqual([1, 1, 2, 2, 3]);323 });324325 it('the local budget refuses the request that would pass 1200 per minute, before sending it', async () => {326 const { adapter, calls, advance } = make('mainnet');327 for (let i = 0; i < 60; i++) await adapter.instruments(); // 60 × 20 = 1200328 expect(calls.length).toBe(60);329 try { await adapter.instruments(); expect.unreachable(); }330 catch (e) { expect((e as VenueError).code).toBe('rate_limited'); expect(((e as VenueError).detail as Record<string, any>).retryAfterMs).toBeGreaterThan(0); }331 expect(calls.length).toBe(60);332 advance(60_001);333 await adapter.instruments();334 expect(calls.length).toBe(61);335 });336337 it('per-item surcharge is charged after the answer', async () => {338 const { adapter } = make('mainnet');339 await adapter.fills(account); // 20 + floor(40 / 20)340 expect(adapter.http.budget.used()).toBe(22);341 const b = new HlWeightBudget(100, () => 0);342 b.take(90, 'x');343 expect(b.retryAfterMs(10)).toBe(0);344 expect(b.retryAfterMs(11)).toBe(60_000);345 });346347 it('order status strings of the documentation', () => {348 expect(hlOrderStatus('open').status).toBe('open');349 expect(hlOrderStatus('filled').status).toBe('filled');350 expect(hlOrderStatus('canceled').status).toBe('cancelled');351 expect(hlOrderStatus('triggered').status).toBe('triggered');352 expect(hlOrderStatus('rejected')).toEqual({ status: 'rejected', rejectReason: 'rejected' });353 for (const s of ['marginCanceled', 'vaultWithdrawalCanceled', 'openInterestCapCanceled', 'selfTradeCanceled', 'reduceOnlyCanceled', 'siblingFilledCanceled', 'delistedCanceled', 'liquidatedCanceled'])354 expect(hlOrderStatus(s)).toEqual({ status: 'cancelled', rejectReason: s });355 expect(hlOrderStatus('tickRejected')).toEqual({ status: 'rejected', rejectReason: 'tickRejected' });356 });357});358