import {z} from 'zod' export type Network = 'arbitrum' | 'avalanche' | 'megaeth' | 'solana'; export type Version = 'v1' | 'v2'; export type GetApiStatsSummaryArgs = {networks?: Array | undefined, versions?: Array | undefined}; export type GetApiStatsSummaryOptions = {baseUrl?: string | undefined, fetch?: typeof fetch | undefined}; export const tradesResponse = z.object({total: z.number().nullable(), swaps: z.number().nullable(), perps: z.number().nullable()}); export const usersResponse = z.object({distinctness: z.literal("source"), all: z.number().nullable(), lps: z.number().nullable(), traders: z.number().nullable()}); export const usd = z.string(); export const tvlResponse = z.object({valuedAt: z.number().nullable(), poolsValuedAt: z.number().nullable(), total: usd, store: usd.nullable(), glv: usd, pools: usd}); export const buybackToken = z.enum(["GMX", "GT"]); export const revenueResponse = z.object({treasury: usd.nullable(), protocol: usd, buybackToken: buybackToken.nullable(), buyback: usd.nullable(), lp: usd}); export const feesResponse = z.object({total: usd, swap: usd.nullable(), trading: usd.nullable()}); export const openInterestResponse = z.object({total: usd, short: usd, long: usd}); export const volumeResponse = z.object({lpFlows: usd.nullable(), total: usd, swaps: usd.nullable(), perps: usd.nullable()}); export const metricSetResponse = z.object({trades: tradesResponse, users: usersResponse, tvl: tvlResponse.nullable(), revenue: revenueResponse.nullable(), fees24h: feesResponse.nullable(), fees: feesResponse, openInterest: openInterestResponse.nullable(), volume24h: volumeResponse.nullable(), volume: volumeResponse}); export const partialRecordVersionMetricSetResponse = z.object({v1: metricSetResponse.optional(), v2: metricSetResponse.optional()}); export const partialRecordNetworkMetricSetResponse = z.object({arbitrum: metricSetResponse.optional(), avalanche: metricSetResponse.optional(), megaeth: metricSetResponse.optional(), solana: metricSetResponse.optional()}); export const sourceHealth = z.enum(["fresh", "stale", "missing"]); export const metricGroup = z.enum(["volume", "volume24h", "openInterest", "fees", "fees24h", "revenue", "tvl", "users", "trades"]); export const version = z.enum(["v1", "v2"]); export const network = z.enum(["arbitrum", "avalanche", "megaeth", "solana"]); export const sourceStatusResponse = z.object({lastError: z.string().nullable(), lagSeconds: z.number().nullable(), health: sourceHealth, fetchedAt: z.number().nullable(), asOf: z.number().nullable(), metrics: z.array(metricGroup), version: version, network: network, id: z.string()}); export const completeness = z.enum(["complete", "partial"]); export const summaryMetaResponse = z.object({schemaVersion: z.string(), sources: z.array(sourceStatusResponse), completeness: completeness, asOf: z.number()}); export const summaryResponse = z.object({byVersion: partialRecordVersionMetricSetResponse, byNetwork: partialRecordNetworkMetricSetResponse, totals: metricSetResponse, meta: summaryMetaResponse}); export const validateResponse = async ( schema: { readonly '~standard': { readonly validate: (value: unknown) => ({ readonly value: Output; readonly issues?: undefined } | { readonly issues: ReadonlyArray<{ readonly message: string; readonly path?: ReadonlyArray | undefined }> }) | Promise<{ readonly value: Output; readonly issues?: undefined } | { readonly issues: ReadonlyArray<{ readonly message: string; readonly path?: ReadonlyArray | undefined }> }> } }, value: unknown ): Promise => { const result = await schema['~standard'].validate(value) if (result.issues !== undefined) { const detail = result.issues .map(({ message, path }) => path?.length ? `${path.map((segment) => String(typeof segment === 'object' ? segment.key : segment)).join('.')}: ${message}` : message ) .join('; ') throw new Error(`Response does not match the schema: ${detail}`, { cause: { issues: result.issues } }) } return result.value }; /** * GET /stats/summary */ export const getApiStatsSummary = async (args: GetApiStatsSummaryArgs, options?: GetApiStatsSummaryOptions) => { const {baseUrl = "https://arbitrum.gmxapi.io/v1", fetch: fetchFn = fetch} = options ?? {} const query = new URLSearchParams() const headers = new Headers() headers.set('accept', "application/json") for (const item of args.networks ?? []) query.append("networks", String(item)) for (const item of args.versions ?? []) query.append("versions", String(item)) const url = `${baseUrl.replace(/\/+$/, '')}/stats/summary${query.size > 0 ? `?${query}` : ''}` const res = await fetchFn(url, {method: "GET", headers}) if (!res.ok) throw new Error(`Request failed with status ${res.status}`, {cause: {status: res.status, body: await res.text()}}) return validateResponse(summaryResponse, await res.json()) };