import {z} from 'zod' export type TimeseriesMetric = 'volume.perps' | 'volume.swaps' | 'volume.total' | 'fees.trading' | 'fees.swap' | 'fees.total' | 'revenue.protocol' | 'revenue.lp' | 'trades.perps' | 'trades.swaps' | 'users.active'; export type TimeseriesGroupBy = 'network' | 'version' | 'none'; export type Network = 'arbitrum' | 'avalanche' | 'megaeth' | 'solana'; export type Version = 'v1' | 'v2'; export type GetApiStatsTimeseriesArgs = {metric: TimeseriesMetric, groupBy?: TimeseriesGroupBy | undefined, networks?: Array | undefined, versions?: Array | undefined, from?: number | undefined, to?: number | undefined}; export type GetApiStatsTimeseriesOptions = {baseUrl?: string | undefined, fetch?: typeof fetch | undefined}; export const timeseriesPointResponse = z.object({provisional: z.boolean(), value: z.union([z.string(), z.number()]).nullable(), day: z.number()}); export const timeseriesGroupResponse = z.object({points: z.array(timeseriesPointResponse), key: z.string()}); export const timeseriesGroupBy = z.enum(["network", "version", "none"]); export const timeseriesMetric = z.enum(["volume.perps", "volume.swaps", "volume.total", "fees.trading", "fees.swap", "fees.total", "revenue.protocol", "revenue.lp", "trades.perps", "trades.swaps", "users.active"]); 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 timeseriesResponse = z.object({groups: z.array(timeseriesGroupResponse), groupBy: timeseriesGroupBy, interval: z.literal("1d"), metric: timeseriesMetric, 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/timeseries */ export const getApiStatsTimeseries = async (args: GetApiStatsTimeseriesArgs, options?: GetApiStatsTimeseriesOptions) => { const {baseUrl = "https://arbitrum.gmxapi.io/v1", fetch: fetchFn = fetch} = options ?? {} const query = new URLSearchParams() const headers = new Headers() headers.set('accept', "application/json") if (args.metric != null) query.append("metric", String(args.metric)) if (args.groupBy != null) query.append("groupBy", String(args.groupBy)) for (const item of args.networks ?? []) query.append("networks", String(item)) for (const item of args.versions ?? []) query.append("versions", String(item)) if (args.from != null) query.append("from", String(args.from)) if (args.to != null) query.append("to", String(args.to)) const url = `${baseUrl.replace(/\/+$/, '')}/stats/timeseries${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(timeseriesResponse, await res.json()) };