UNPKG

@lifi/compose-spec

Version:

Public wire-format types and schemas for Compose flows

336 lines (297 loc) 10.7 kB
import z from 'zod'; import { BYTES32_LOWER_HEX } from './hexPatterns.js'; // A positive integer chain id. Zod mirror of the Effect `ChainIdSchema` in // `flowSchema.ts`, single-sourced so the wire schemas stay consistent. export const chainIdZod = z.number().int().positive(); const SolTypeZod = z.enum([ 'uint8', 'uint16', 'uint32', 'uint64', 'uint128', 'uint256', 'int128', 'int256', 'address', 'bool', 'bytes', 'bytes4', 'bytes32', 'string', ]); export const ResourcePortZod = z.object({ kind: z.literal('resource'), name: z.string(), accepts: z.enum(['erc20', 'native', 'any']), mode: z.enum(['linear', 'copy']), optional: z.boolean().optional(), }); export const HANDLE_UNITS = [ 'raw', 'wad', 'ray', 'bps', 'token-decimals', ] as const; // NOTE: the uint members below must stay in sync with the `UintN` union in // `ts/packages/lifi_ir1_ts/src/types/ValueHandle.ts`. When adding a new // uintN width, update both locations. export const STATIC_SOL_TYPES = [ 'uint256', 'uint128', 'uint64', 'uint32', 'uint16', 'uint8', 'address', 'bool', 'bytes32', ] as const; export const HandleUnitsZod = z.enum(HANDLE_UNITS); export const StaticSolTypeZod = z.enum(STATIC_SOL_TYPES); const STATIC_SOL_TYPE_SET: ReadonlySet<string> = new Set(STATIC_SOL_TYPES); export const isStaticSolType = (value: string): value is StaticSolType => STATIC_SOL_TYPE_SET.has(value); export const HandlePortZod = z.object({ kind: z.literal('handle'), name: z.string(), type: SolTypeZod, mode: z.enum(['linear', 'copy']), expose: z.boolean().optional(), units: HandleUnitsZod.optional(), optional: z.boolean().optional(), }); export const InputPortZod = z.discriminatedUnion('kind', [ ResourcePortZod, HandlePortZod, ]); export const ResourceOutputPortZod = z.object({ kind: z.literal('resource_output'), name: z.string(), mode: z.enum(['linear', 'copy']), availability: z.enum(['now', 'future']).optional(), providesMinimum: z.boolean().optional(), omitIfZero: z.boolean().optional(), deliveryAddressInput: z.string().optional(), }); export const OutputPortZod = z.discriminatedUnion('kind', [ ResourceOutputPortZod, HandlePortZod, ]); export const ManifestOperationZod = z.object({ id: z.string(), description: z.string().optional(), inputs: z.array(InputPortZod), outputs: z.array(OutputPortZod), configSchema: z.unknown().optional(), }); const PortKindEnum = z.enum(['resource', 'resource_output', 'handle']); const SelectorMatchZod = z.object({ kind: z.union([PortKindEnum, z.array(PortKindEnum)]), mode: z.enum(['linear', 'copy']).optional(), type: z.enum(['erc20', 'native', 'any']).optional(), }); const ConfigSelectionZod = z.object({ kind: z.literal('config'), configKey: z.string(), cardinality: z.enum(['one', 'many']), }); const AllMatchingSelectionZod = z.object({ kind: z.literal('all_matching'), }); const SelectorSelectionZod = z.discriminatedUnion('kind', [ ConfigSelectionZod, AllMatchingSelectionZod, ]); export const GuardSelectorZod = z.object({ binding: z.string(), source: z.enum(['inputs', 'outputs']), match: SelectorMatchZod, selection: SelectorSelectionZod, }); export const GuardCompatibilityZod = z.object({ selectors: z.array(GuardSelectorZod), }); export const ManifestGuardZod = z.object({ kind: z.string(), description: z.string().optional(), configSchema: z.unknown().optional(), compatibility: GuardCompatibilityZod.optional(), }); export const ManifestMaterialiserZod = z.object({ kind: z.string(), description: z.string().optional(), accepts: z.enum(['resource', 'handle', 'any']), configSchema: z.unknown().optional(), }); export const ManifestPreconditionZod = z.object({ type: z.string(), description: z.string().optional(), configSchema: z.unknown().optional(), }); export const ComposeManifestZod = z.object({ manifestVersion: z.number(), manifestHash: z.string(), flowSchema: z.object({}).passthrough(), operations: z.array(ManifestOperationZod), guards: z.array(ManifestGuardZod), materialisers: z.array(ManifestMaterialiserZod), preconditions: z.array(ManifestPreconditionZod).optional(), }); // HTTP-layer mirror of `Phase1ArtifactSchema` (the Effect schema in // `continuation.ts`). The Effect schema validates the Flow; this Zod schema // validates the `/compose` response. `transaction` carries an optional // `transactionRequest` only if a venue later needs it; for v1 it is just // `{ kind: "transaction" }` and the client funds via the existing // response-level `transactionRequest`. export const Phase1ArtifactZod = z.discriminatedUnion('kind', [ z.object({ kind: z.literal('transaction'), transactionRequest: z .object({ to: z.string(), data: z.string(), value: z.string(), }) .optional(), }), z.object({ kind: z.literal('signables'), typedData: z.array(z.unknown()), }), z.object({ kind: z.literal('none'), }), ]); // HTTP-layer mirror of `CheckSchema` (the Effect schema in `continuation.ts`). // A readiness predicate the client can poll and that is also embedded into // `nextFlow` as an on-chain assertion. Amounts are decimal strings on the wire. // Address/bytes fields require a `0x` prefix (lenient, mirroring `CheckSchema`). const hexStringZod = z.string().startsWith('0x'); // Mirror of `AssertionOwnerSchema` (continuation.ts): a concrete hex address or // one of the two symbolic owners resolved at settlement. Applies ONLY to the // derivation-only delta members — the wire `Check` members keep hex-only // owners (see the note in continuation.ts). const assertionOwnerZod = z.union([ hexStringZod, z.enum(['escrow', 'delivery']), ]); export const CheckZod = z.discriminatedUnion('kind', [ z.object({ kind: z.literal('erc20BalanceGte'), chainId: chainIdZod, token: hexStringZod, owner: hexStringZod, minAmount: z.string(), }), z.object({ kind: z.literal('nativeBalanceGte'), chainId: chainIdZod, owner: hexStringZod, minAmount: z.string(), }), z.object({ kind: z.literal('call'), chainId: chainIdZod, to: hexStringZod, calldata: hexStringZod, comparator: z.enum(['gte', 'eq', 'lte']), value: z.string(), }), ]); // HTTP-layer mirror of `AssertionSpecSchema` (continuation.ts): the three // `Check` members plus the two derivation-only members. Reuses `CheckZod`'s // options so the shared members cannot drift from `Check`. export const AssertionSpecZod = z.discriminatedUnion('kind', [ ...CheckZod.options, z.object({ kind: z.literal('erc20BalanceDeltaGte'), chainId: z.number().int().positive(), token: hexStringZod, owner: assertionOwnerZod, minDelta: z.string(), }), z.object({ kind: z.literal('nativeBalanceDeltaGte'), chainId: z.number().int().positive(), owner: assertionOwnerZod, minDelta: z.string(), }), ]); export type AssertionSpecWire = z.infer<typeof AssertionSpecZod>; // HTTP-layer mirror of the GD-3 descriptor union `AssertedPredicateSchema` // (descriptor.ts, seam C4). It is the disclosure-path widening of // `AssertionSpecZod`: the two balance-`Gte` members accept a symbolic owner // (`assertionOwnerZod`) as well as hex, because the derivation resolves symbolic // owners to validator-injected slots at settlement. The wire `Check` members // stay hex-only (`CheckZod`); this widening is confined to the descriptor. The // `call` member and the two derivation-only delta members are reused verbatim by // selecting them from `AssertionSpecZod.options` by their `kind` literal, so // they cannot drift from the source union. const assertionSpecOptionByKind = (kind: string) => { const option = AssertionSpecZod.options.find( (o) => o.shape.kind.value === kind, ); if (option === undefined) { throw new Error(`AssertionSpecZod has no member with kind "${kind}"`); } return option; }; export const AssertedPredicateZod = z.discriminatedUnion('kind', [ z.object({ kind: z.literal('erc20BalanceGte'), chainId: z.number().int().positive(), token: hexStringZod, owner: assertionOwnerZod, minAmount: z.string(), }), z.object({ kind: z.literal('nativeBalanceGte'), chainId: z.number().int().positive(), owner: assertionOwnerZod, minAmount: z.string(), }), assertionSpecOptionByKind('call'), assertionSpecOptionByKind('erc20BalanceDeltaGte'), assertionSpecOptionByKind('nativeBalanceDeltaGte'), ]); export const VerificationDescriptorZod = z.object({ validationProgramHash: z.string().regex(BYTES32_LOWER_HEX), // The C5 echo's second hash (GD-8): the params-vector content hash // (`paramsHash`, C1). Required and jointly necessary with the value-blind // `validationProgramHash`. paramsHash: z.string().regex(BYTES32_LOWER_HEX), assertedPredicate: z.array(AssertedPredicateZod), }); export type AssertedPredicateWire = z.infer<typeof AssertedPredicateZod>; export type VerificationDescriptorWire = z.infer< typeof VerificationDescriptorZod >; // HTTP-layer mirror of `ContinuationPlanSchema`. `nextFlow` is typed // `z.unknown()` rather than mirroring the full Flow schema in Zod: the client // treats it as an opaque, re-submittable document and POSTs it back to // `/compose`, where the Effect `FlowSchema` validates it. The internal // `ContinuationPlan` type keeps `nextFlow: Flow` precisely typed. export const ContinuationPlanZod = z.object({ chainId: chainIdZod, nextFlow: z.unknown(), check: CheckZod, validity: z.object({ expiresAtMs: z.number() }), correlationId: z.string().optional(), }); export type ResourcePort = z.infer<typeof ResourcePortZod>; export type HandlePort = z.infer<typeof HandlePortZod>; export type HandleUnits = z.infer<typeof HandleUnitsZod>; export type StaticSolType = z.infer<typeof StaticSolTypeZod>; export type OpInputPort = z.infer<typeof InputPortZod>; export type ResourceOutputPort = z.infer<typeof ResourceOutputPortZod>; export type OpOutputPort = z.infer<typeof OutputPortZod>; export type ManifestOperation = z.infer<typeof ManifestOperationZod>; export type GuardSelector = z.infer<typeof GuardSelectorZod>; export type GuardCompatibility = z.infer<typeof GuardCompatibilityZod>; export type ManifestGuard = z.infer<typeof ManifestGuardZod>; export type ManifestMaterialiser = z.infer<typeof ManifestMaterialiserZod>; export type ManifestPrecondition = z.infer<typeof ManifestPreconditionZod>; export type ComposeManifest = z.infer<typeof ComposeManifestZod>; export type Phase1ArtifactWire = z.infer<typeof Phase1ArtifactZod>; export type ContinuationPlanWire = z.infer<typeof ContinuationPlanZod>;