116 lines
3.7 KiB
TypeScript
116 lines
3.7 KiB
TypeScript
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<T>(query: string, variables: Record<string, unknown>): Promise<T> {
|
|
const headers: Record<string, string> = {
|
|
'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<DriftTick[]> {
|
|
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<Omit<DriftTick, 'oracle_price' | 'mark_price'> & { 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();
|
|
}
|