@lifi/composer-sdk
Version:
Public Composer SDK for building and submitting flows
144 lines (128 loc) • 5.14 kB
text/typescript
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;
};