export type DriftTick = { ts: string; market_index: number; symbol: string; oracle_price: number; mark_price?: number | null; oracle_slot?: number | null; source?: string; }; type GraphQLError = { message: string }; function getApiUrl(): string | undefined { const v = (import.meta as any).env?.VITE_API_URL; if (v) return String(v); // Default to same-origin API proxy at /api (Vite dev server proxies /api -> trade-api). return '/api'; } function getHasuraUrl(): string { return (import.meta as any).env?.VITE_HASURA_URL || '/graphql'; } function getAuthToken(): string | undefined { const v = (import.meta as any).env?.VITE_HASURA_AUTH_TOKEN; return v ? String(v) : undefined; } function getAdminSecret(): string | undefined { const v = (import.meta as any).env?.VITE_HASURA_ADMIN_SECRET; return v ? String(v) : undefined; } export async function hasuraRequest(query: string, variables: Record): Promise { const headers: Record = { 'content-type': 'application/json', }; const token = getAuthToken(); if (token) { headers.authorization = `Bearer ${token}`; } else { const secret = getAdminSecret(); if (secret) headers['x-hasura-admin-secret'] = secret; } const res = await fetch(getHasuraUrl(), { method: 'POST', headers, body: JSON.stringify({ query, variables }), }); const text = await res.text(); if (!res.ok) throw new Error(`Hasura HTTP ${res.status}: ${text}`); const json = JSON.parse(text) as { data?: T; errors?: GraphQLError[] }; if (json.errors?.length) throw new Error(json.errors.map((e) => e.message).join(' | ')); if (!json.data) throw new Error('Hasura: empty response'); return json.data; } export async function fetchLatestTicks(symbol: string, limit: number, source?: string): Promise { const apiUrl = getApiUrl(); if (apiUrl) { const u = new URL(apiUrl, window.location.origin); u.pathname = u.pathname && u.pathname !== '/' ? u.pathname.replace(/\/$/, '') + '/v1/ticks' : '/v1/ticks'; u.searchParams.set('symbol', symbol); u.searchParams.set('limit', String(limit)); if (source && source.trim()) u.searchParams.set('source', source.trim()); const res = await fetch(u.toString()); const text = await res.text(); if (!res.ok) throw new Error(`API HTTP ${res.status}: ${text}`); const json = JSON.parse(text) as { ok?: boolean; ticks?: any[]; error?: string }; if (!json.ok) throw new Error(json.error || 'API: error'); return (json.ticks || []).map((t: any) => ({ ts: String(t.ts), market_index: Number(t.market_index), symbol: String(t.symbol), oracle_price: Number(t.oracle_price), mark_price: t.mark_price == null ? null : Number(t.mark_price), oracle_slot: t.oracle_slot == null ? null : Number(t.oracle_slot), source: t.source == null ? undefined : String(t.source), })); } const where: any = { symbol: { _eq: symbol } }; if (source && source.trim()) where.source = { _eq: source.trim() }; const query = ` query LatestTicks($where: drift_ticks_bool_exp!, $limit: Int!) { drift_ticks(where: $where, order_by: {ts: desc}, limit: $limit) { ts market_index symbol oracle_price mark_price oracle_slot source } } `; const data = await hasuraRequest<{ drift_ticks: Array & { oracle_price: any; mark_price?: any }>; }>(query, { where, limit }); return data.drift_ticks .map((t) => ({ ...t, oracle_price: Number(t.oracle_price), mark_price: t.mark_price == null ? null : Number(t.mark_price), })) .reverse(); }