UNPKG

@lifi/composer-sdk

Version:

Public Composer SDK for building and submitting flows

144 lines (128 loc) 5.14 kB
import type { SimulateResult, SimulationRevert } from '@lifi/compose-spec'; import z from 'zod'; /** * Zod-backed parsers for Compose API response bodies. * * Server responses are untrusted input crossing into the SDK, so the shapes the * client reads back are validated here rather than asserted with `as` casts. The * schemas are module-private (the package builds with `--isolatedDeclarations`, * which forbids exporting un-annotated complex values); the public surface is * the typed `parse*` helpers, whose explicit return types double as the contract. * * The simulate union is the one response the SDK fully owns and parses itself * (it is un-enveloped, unlike `/compose`), so it gets a complete schema kept in * lockstep with the canonical `SimulateResult` in `@lifi/compose-spec` by the * compile-time assertion below — the same anchoring `@lifi/api-schemas` uses for * its server-side simulate schema, so the two cannot silently drift. */ const balanceEntrySchema = z.object({ token: z.string(), owner: z.string(), amount: z.string(), }); const simulateOkSchema = z.object({ status: z.literal('ok'), block: z.number(), timestamp: z.number(), balancesBefore: z.array(balanceEntrySchema), balancesAfter: z.array(balanceEntrySchema), deltas: z.array(balanceEntrySchema), gasUsed: z.string(), }); const simulateRevertDecodeResultSchema = z.object({ errorCandidates: z .array( z.object({ decodedErrorSignature: z.string(), decodedParams: z.array(z.string()), }), ) .optional(), error: z.string().optional(), }); const simulateRevertSchema = z.object({ status: z.literal('revert'), block: z.number(), timestamp: z.number(), revertReason: z.string().optional(), code: z.number().optional(), rawErrorBytes: z.string().optional(), decodeResult: simulateRevertDecodeResultSchema.optional(), }); const simulateSetupErrorSchema = z.object({ status: z.literal('error'), message: z.string(), }); const simulateResultSchema = z.discriminatedUnion('status', [ simulateOkSchema, simulateRevertSchema, simulateSetupErrorSchema, ]); // The type the schema actually parses out. Kept private (referencing it from an // exported declaration would break `--isolatedDeclarations`) and pinned to the // canonical `SimulateResult` inside `parseSimulateResult` below. type SimulateResultWire = z.infer<typeof simulateResultSchema>; /** * Parses a `POST /simulate` body into a {@link SimulateResult}. Returns `null` * when the body does not match the `ok`/`revert`/`error` union, letting the * caller raise a transport-level error. */ export const parseSimulateResult = (body: unknown): SimulateResult | null => { const parsed = simulateResultSchema.safeParse(body); if (!parsed.success) return null; // Pin the schema to the canonical contract in both directions, so it cannot // silently drift (mirroring the conformance assertion `@lifi/api-schemas` runs // on its server-side simulate schema). Assigning the inferred wire value to a // `SimulateResult` checks wire→spec; `satisfies SimulateResultWire` checks // spec→wire. A missing or mistyped field on either side fails to compile. const result: SimulateResult = parsed.data; return result satisfies SimulateResultWire; }; /** * The validated members of an HTTP 206 partial compile envelope. `data` is * verified to be an object but kept untyped — compose-spec owns its full shape * (`ComposeCompilePartialData`) as a hand-authored type, so the caller narrows * it. `error` is fully validated. */ export interface CompilePartialEnvelope { readonly data: Record<string, unknown>; readonly error: { readonly kind: string; readonly message: string }; } const compilePartialEnvelopeSchema = z.object({ data: z.record(z.string(), z.unknown()), error: z.object({ kind: z.string(), message: z.string() }), }); /** Parses an HTTP 206 partial compile envelope; `null` when it does not match. */ export const parseCompilePartialEnvelope = ( body: unknown, ): CompilePartialEnvelope | null => { const parsed = compilePartialEnvelopeSchema.safeParse(body); return parsed.success ? parsed.data : null; }; /** Error envelope returned on non-2xx responses. */ export interface ServerErrorBody { readonly error?: { readonly kind?: string; readonly message?: string; readonly path?: string; readonly details?: SimulationRevert; }; } // `details` is passed through (`z.custom`) rather than parsed: the SDK does not // own the `SimulationRevert` schema and forwards the field verbatim onto // `ComposeError`. const serverErrorBodySchema = z.object({ error: z .object({ kind: z.string().optional(), message: z.string().optional(), path: z.string().optional(), details: z.custom<SimulationRevert>().optional(), }) .optional(), }); /** Validates an already-parsed error envelope; `null` when it does not match. */ export const parseServerErrorBody = (json: unknown): ServerErrorBody | null => { const parsed = serverErrorBodySchema.safeParse(json); return parsed.success ? parsed.data : null; };