import { useEffect, useMemo, useState } from 'react'; import { subscribeGraphqlWs } from '../../lib/graphqlWs'; export type OrderbookRow = { price: number; sizeBase: number; sizeUsd: number; totalBase: number; totalUsd: number; }; export type DlobL2 = { marketName: string; bids: OrderbookRow[]; asks: OrderbookRow[]; bestBid: number | null; bestAsk: number | null; mid: number | null; updatedAt: string | null; }; function envNumber(name: string): number | undefined { const raw = (import.meta as any).env?.[name]; if (raw == null) return undefined; const n = Number(raw); return Number.isFinite(n) ? n : undefined; } function toNum(v: unknown): number | null { if (v == null) return null; if (typeof v === 'number') return Number.isFinite(v) ? v : null; if (typeof v === 'string') { const s = v.trim(); if (!s) return null; const n = Number(s); return Number.isFinite(n) ? n : null; } return null; } function normalizeJson(v: unknown): unknown { if (typeof v !== 'string') return v; const s = v.trim(); if (!s) return null; try { return JSON.parse(s); } catch { return v; } } function parseLevels(raw: unknown, pricePrecision: number, basePrecision: number): Array<{ price: number; size: number }> { const v = normalizeJson(raw); if (!Array.isArray(v)) return []; const out: Array<{ price: number; size: number }> = []; for (const item of v) { const priceInt = toNum((item as any)?.price); const sizeInt = toNum((item as any)?.size); if (priceInt == null || sizeInt == null) continue; const price = priceInt / pricePrecision; const size = sizeInt / basePrecision; if (!Number.isFinite(price) || !Number.isFinite(size)) continue; out.push({ price, size }); } return out; } function withTotals(levels: Array<{ price: number; sizeBase: number }>): OrderbookRow[] { let totalBase = 0; let totalUsd = 0; return levels.map((l) => { const sizeUsd = l.sizeBase * l.price; totalBase += l.sizeBase; totalUsd += sizeUsd; return { price: l.price, sizeBase: l.sizeBase, sizeUsd, totalBase, totalUsd, }; }); } type HasuraDlobL2Row = { market_name: string; bids?: unknown; asks?: unknown; updated_at?: string | null; }; type SubscriptionData = { dlob_l2_latest: HasuraDlobL2Row[]; }; export function useDlobL2( marketName: string, opts?: { levels?: number } ): { l2: DlobL2 | null; connected: boolean; error: string | null } { const [l2, setL2] = useState(null); const [connected, setConnected] = useState(false); const [error, setError] = useState(null); const normalizedMarket = useMemo(() => (marketName || '').trim(), [marketName]); const levels = useMemo(() => Math.max(1, opts?.levels ?? 14), [opts?.levels]); const pricePrecision = useMemo(() => envNumber('VITE_DLOB_PRICE_PRECISION') ?? 1_000_000, []); const basePrecision = useMemo(() => envNumber('VITE_DLOB_BASE_PRECISION') ?? 1_000_000_000, []); useEffect(() => { if (!normalizedMarket) { setL2(null); setError(null); setConnected(false); return; } setError(null); const query = ` subscription DlobL2($market: String!) { dlob_l2_latest(where: {market_name: {_eq: $market}}, limit: 1) { market_name bids asks updated_at } } `; const sub = subscribeGraphqlWs({ query, variables: { market: normalizedMarket }, onStatus: ({ connected }) => setConnected(connected), onError: (e) => setError(e), onData: (data) => { const row = data?.dlob_l2_latest?.[0]; if (!row?.market_name) return; const bidsSorted = parseLevels(row.bids, pricePrecision, basePrecision) .slice() .sort((a, b) => b.price - a.price) .slice(0, levels) .map((l) => ({ price: l.price, sizeBase: l.size })); const asksSorted = parseLevels(row.asks, pricePrecision, basePrecision) .slice() .sort((a, b) => a.price - b.price) .slice(0, levels) .map((l) => ({ price: l.price, sizeBase: l.size })); // We compute totals from best -> worse. // For UI we display asks with best ask closest to mid (at the bottom), so we reverse. const bids = withTotals(bidsSorted); const asks = withTotals(asksSorted).slice().reverse(); const bestBid = bidsSorted.length ? bidsSorted[0].price : null; const bestAsk = asksSorted.length ? asksSorted[0].price : null; const mid = bestBid != null && bestAsk != null ? (bestBid + bestAsk) / 2 : null; setL2({ marketName: row.market_name, bids, asks, bestBid, bestAsk, mid, updatedAt: row.updated_at ?? null, }); }, }); return () => sub.unsubscribe(); }, [normalizedMarket, levels, pricePrecision, basePrecision]); return { l2, connected, error }; }