Repository Public
1// Routes of GMX v2 trading (v8) — all under /api/v1/derivatives/gmx. Authentication and CSRF are the global hook of server.ts.2// Nothing here holds a key or judges a passkey: assertions go to the isolated signer untouched. Shapes: docs/v4/API-LIVE-NOTES.md.3import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';4import { z } from 'zod';5import { jurisdictionAck } from '@spw/derivatives/live';6import { UserError } from '../config.ts';7import { venueFailure } from './data.ts';8import { GmxApproveAuthSchema, GmxAssertionSchema, GmxAuthBodySchema, GmxCloseBodySchema, GmxOrderBodySchema, type GmxService } from './gmx-exec.ts';9import { gmx } from './gmx-live.ts';1011const P = '/api/v1/derivatives/gmx';12const IDEMPOTENCY = /^[A-Za-z0-9._:-]{8,100}$/;1314export function gmxRoutes(app: FastifyInstance, given?: GmxService): void {15 const svc = (): GmxService => given ?? gmx();16 const op = (req: FastifyRequest): string => z.uuid().parse((req.params as { opId: string }).opId);17 const locale = (req: FastifyRequest): 'fr' | 'en' => ((req.query as { locale?: unknown } | undefined)?.locale === 'fr' ? 'fr' : 'en');18 const idem = (req: FastifyRequest): string => { const k = req.headers['idempotency-key']; if (typeof k !== 'string' || !IDEMPOTENCY.test(k)) throw new UserError('idempotency', 'an Idempotency-Key header (8 to 100 characters: letters, digits, . _ : -) is required', 400); return k; };19 /** a chain or a venue that cannot be read is a 503, a request that is refused a 4xx — never a 500 */20 const run = <T>(fn: (s: GmxService, req: FastifyRequest, reply: FastifyReply) => Promise<T>) => async (req: FastifyRequest, reply: FastifyReply): Promise<T> => { try { return await fn(svc(), req, reply); } catch (e) { throw venueFailure(e); } };2122 // the account is the wallet's own address: settings, pins, balances, authorization, positions and pending orders read on-chain, the engine's operations23 app.get(`${P}/status`, run(async (s, req) => s.status(locale(req))));24 app.get(`${P}/jurisdiction`, run(async () => ({ venue: 'gmx' as const, fr: jurisdictionAck('gmx', 'fr'), en: jurisdictionAck('gmx', 'en') })));25 // enabling: prepare (venue policy + acknowledgement text + WebAuthn request) → approve (assertion); disabling: revoke (no passkey)26 app.post(`${P}/authorization/prepare`, run(async (s, req) => s.prepareAuthorization(GmxAuthBodySchema.parse(req.body))));27 app.post(`${P}/authorization/approve`, run(async (s, req) => s.approveAuthorization(GmxApproveAuthSchema.parse(req.body), locale(req))));28 app.post(`${P}/authorization/revoke`, run(async (s, req) => s.revoke(locale(req))));29 // orders: preview (nothing stored) → prepare (stored proposal, WebAuthn request over the intent hash) → approve (assertion)30 app.post(`${P}/orders/preview`, run(async (s, req) => s.preview(GmxOrderBodySchema.parse(req.body))));31 app.post(`${P}/orders`, run(async (s, req) => s.prepareOrder(GmxOrderBodySchema.parse(req.body), idem(req))));32 app.post(`${P}/positions/close`, run(async (s, req) => s.prepareClose(GmxCloseBodySchema.parse(req.body), idem(req))));33 app.post(`${P}/orders/:opId/cancel`, run(async (s, req) => s.prepareCancel(op(req))));34 app.post(`${P}/operations/:opId/approve`, run(async (s, req) => s.approve(op(req), GmxAssertionSchema.parse(req.body).assertion)));35 app.post(`${P}/operations/:opId/withdraw`, run(async (s, req) => s.withdraw(op(req))));36 app.get(`${P}/operations/:opId`, run(async (s, req) => s.operation(op(req))));37}38