@lifi/compose-spec
Version:
Public wire-format types and schemas for Compose flows
336 lines (297 loc) • 10.7 kB
text/typescript
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>;