Repository Public
1// zod schemas of the Hyperliquid answers that the adapter reads. Loose objects: unknown fields are tolerated, a missing or2// mistyped field is `bad_answer`. Every numeric string is checked and canonicalised as a decimal string — never parsed to a number.3// Shapes: https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint (+ /perpetuals) and4// .../websocket/subscriptions, confirmed against live answers (fixtures in test/fixtures/hyperliquid/).5import { z } from 'zod';6import { D } from '../../decimal.ts';78/** a venue decimal ("73699.0", "-63.0", "0.0000117479") → canonical DecStr ("73699", "-63", "0.0000117479") */9export const hlDec = z.string().regex(/^-?(0|[1-9]\d*)(\.\d+)?$/).max(60).transform((s) => D.str(D.from(s)));10const ms = z.number().int().nonnegative();11const coin = z.string().min(1).max(40);12export const hlAddressSchema = z.string().regex(/^0x[0-9a-fA-F]{40}$/).transform((s) => s.toLowerCase());13const cloid = z.string().regex(/^0x[0-9a-fA-F]{32}$/).transform((s) => s.toLowerCase());14const hash = z.string().regex(/^0x[0-9a-fA-F]{64}$/);15const side = z.enum(['B', 'A']); // B = bid = buy, A = ask = sell (notation page)1617// --- meta / metaAndAssetCtxs -------------------------------------------------------------------------------------------------18export const hlUniverseEntry = z.looseObject({19 name: coin, szDecimals: z.number().int().min(0).max(6), maxLeverage: z.number().int().min(1).max(1000),20 onlyIsolated: z.boolean().optional(), isDelisted: z.boolean().optional(), marginMode: z.string().max(40).optional(),21});22export const hlMeta = z.looseObject({ universe: z.array(hlUniverseEntry).min(1).max(5000), collateralToken: z.number().int().nonnegative().optional() });23export type HlMeta = z.infer<typeof hlMeta>;2425export const hlAssetCtx = z.looseObject({26 funding: hlDec, openInterest: hlDec, prevDayPx: hlDec, dayNtlVlm: hlDec, premium: hlDec.nullable(), oraclePx: hlDec, markPx: hlDec,27 midPx: hlDec.nullable(), dayBaseVlm: hlDec.optional(),28});29export type HlAssetCtx = z.infer<typeof hlAssetCtx>;30export const hlMetaAndAssetCtxs = z.tuple([hlMeta, z.array(hlAssetCtx)]);3132// --- l2Book / recentTrades / candleSnapshot / fundingHistory -------------------------------------------------------------------33const level = z.looseObject({ px: hlDec, sz: hlDec, n: z.number().int().nonnegative() });34export const hlL2Book = z.looseObject({ coin, time: ms, levels: z.tuple([z.array(level).max(500), z.array(level).max(500)]) });35export const hlTrade = z.looseObject({ coin, side, px: hlDec, sz: hlDec, time: ms, hash, tid: z.number().int().nonnegative() });36export const hlTrades = z.array(hlTrade).max(5000);37export const hlCandle = z.looseObject({ t: ms, T: ms, s: coin, i: z.string(), o: hlDec, c: hlDec, h: hlDec, l: hlDec, v: hlDec, n: z.number().int().nonnegative() });38export const hlCandles = z.array(hlCandle).max(5000);39export const hlFundingHistory = z.array(z.looseObject({ coin, fundingRate: hlDec, premium: hlDec.nullable(), time: ms })).max(5000);4041// --- clearinghouseState --------------------------------------------------------------------------------------------------------42const marginSummary = z.looseObject({ accountValue: hlDec, totalNtlPos: hlDec, totalRawUsd: hlDec, totalMarginUsed: hlDec });43export const hlPosition = z.looseObject({44 coin, szi: hlDec, entryPx: hlDec.nullable(), positionValue: hlDec, unrealizedPnl: hlDec, liquidationPx: hlDec.nullable(), marginUsed: hlDec,45 leverage: z.looseObject({ type: z.enum(['cross', 'isolated']), value: z.number().int().min(1).max(1000) }),46 cumFunding: z.looseObject({ allTime: hlDec, sinceOpen: hlDec, sinceChange: hlDec }),47});48export const hlClearinghouseState = z.looseObject({49 marginSummary, crossMarginSummary: marginSummary, crossMaintenanceMarginUsed: hlDec, withdrawable: hlDec,50 assetPositions: z.array(z.looseObject({ type: z.string(), position: hlPosition })).max(5000), time: ms,51});5253// --- orders ------------------------------------------------------------------------------------------------------------------54export const hlFrontendOrder = z.looseObject({55 coin, side, limitPx: hlDec, sz: hlDec, oid: z.number().int().nonnegative(), timestamp: ms, origSz: hlDec,56 isTrigger: z.boolean(), triggerPx: hlDec, triggerCondition: z.string().optional(), reduceOnly: z.boolean(),57 orderType: z.string().max(40), tif: z.string().max(40).nullable(), cloid: cloid.nullable().optional(), isPositionTpsl: z.boolean().optional(),58});59export type HlFrontendOrder = z.infer<typeof hlFrontendOrder>;60export const hlFrontendOpenOrders = z.array(hlFrontendOrder).max(10_000);61export const hlHistoricalOrders = z.array(z.looseObject({ order: hlFrontendOrder, status: z.string().max(60), statusTimestamp: ms })).max(10_000);62/** orderUpdates (websocket): the basic order has no trigger / tif information */63export const hlWsOrder = z.looseObject({64 order: z.looseObject({ coin, side, limitPx: hlDec, sz: hlDec, oid: z.number().int().nonnegative(), timestamp: ms, origSz: hlDec, cloid: cloid.nullable().optional() }),65 status: z.string().max(60), statusTimestamp: ms,66});67export const hlWsOrders = z.array(hlWsOrder).max(10_000);6869// --- fills / funding ---------------------------------------------------------------------------------------------------------70export const hlFill = z.looseObject({71 coin, px: hlDec, sz: hlDec, side, time: ms, closedPnl: hlDec, hash, oid: z.number().int().nonnegative(), fee: hlDec,72 tid: z.number().int().nonnegative(), feeToken: z.string().max(40), cloid: cloid.nullable().optional(),73 liquidation: z.looseObject({}).nullable().optional(), dir: z.string().optional(), crossed: z.boolean().optional(),74});75export type HlFill = z.infer<typeof hlFill>;76export const hlFills = z.array(hlFill).max(10_000);77export const hlFundingDelta = z.looseObject({ type: z.literal('funding'), coin, usdc: hlDec, szi: hlDec, fundingRate: hlDec });78export const hlUserFunding = z.array(z.looseObject({ time: ms, hash, delta: hlFundingDelta })).max(10_000);7980// --- exchange answers --------------------------------------------------------------------------------------------------------81const orderStatus = z.union([82 z.looseObject({ resting: z.looseObject({ oid: z.number().int().nonnegative(), cloid: cloid.optional() }) }),83 z.looseObject({ filled: z.looseObject({ oid: z.number().int().nonnegative(), totalSz: hlDec, avgPx: hlDec, cloid: cloid.optional() }) }),84 z.looseObject({ error: z.string() }),85 z.string(), // "success" (cancel), "waitingForFill", "waitingForTrigger" (seen in the SDK for trigger / tpsl orders)86]);87export const hlExchangeAnswer = z.union([88 z.looseObject({ status: z.literal('err'), response: z.string() }),89 z.looseObject({90 status: z.literal('ok'),91 response: z.looseObject({ type: z.string(), data: z.looseObject({ statuses: z.array(orderStatus).max(1000) }).optional() }),92 }),93]);94export type HlExchangeAnswer = z.infer<typeof hlExchangeAnswer>;9596// --- websocket ---------------------------------------------------------------------------------------------------------------97export const hlWsEnvelope = z.looseObject({ channel: z.string().min(1).max(60), data: z.unknown().optional() });98export const hlWsActiveAssetCtx = z.looseObject({ coin, ctx: hlAssetCtx });99export const hlWsAllMids = z.looseObject({ mids: z.record(z.string(), hlDec) });100export const hlWsUserFills = z.looseObject({ isSnapshot: z.boolean().optional(), user: hlAddressSchema, fills: hlFills });101export const hlWsUserEvent = z.looseObject({102 fills: hlFills.optional(),103 funding: z.looseObject({ time: ms, coin, usdc: hlDec, szi: hlDec, fundingRate: hlDec }).optional(),104 liquidation: z.looseObject({}).optional(),105 nonUserCancel: z.array(z.looseObject({ coin, oid: z.number().int().nonnegative() })).optional(),106});107