@lifi/compose-spec
Version:
Public wire-format types and schemas for Compose flows
140 lines (123 loc) • 4.16 kB
text/typescript
import { Schema } from 'effect';
import { normalizeResource } from './chainCapabilities.js';
export const SolTypeSchema = Schema.Literal(
'uint8',
'uint16',
'uint32',
'uint64',
'uint128',
'uint256',
'int128',
'int256',
'address',
'bool',
'bytes',
'bytes4',
'bytes32',
'string',
);
// A positive integer chain id. Single source of truth on the Effect side
// (`continuation.ts` imports it; `zodSchemas.ts` mirrors it as `chainIdZod`).
export const ChainIdSchema = Schema.Number.pipe(
Schema.int(),
Schema.positive(),
);
const RawResourceSchema = Schema.Union(
Schema.Struct({ kind: Schema.Literal('native'), chainId: ChainIdSchema }),
Schema.Struct({
kind: Schema.Literal('erc20'),
token: Schema.String,
chainId: ChainIdSchema,
}),
);
/**
* THE chain-capabilities chokepoint.
*
* `ResourceSchema` is a decode transform: it parses the plain `native | erc20`
* wire union (`RawResourceSchema`) and then runs `normalizeResource`, so on
* `as-erc20` chains (e.g. Tempo) a `{ kind: "native", chainId }` resource is
* rewritten to its ERC-20 form (PathUSD) before any application code sees it.
* Because every Resource-bearing schema embeds this one — `FlowSchema` for flow
* inputs, op `configSchema`s for op configs — making the schema itself
* chain-aware covers every parsed entry point in one place.
*
* The `encode` direction is the identity: the ERC-20 form is itself valid wire
* format, and the rewrite is deliberately not reversible (we never want to
* re-introduce a `native` resource when serialising). `JSONSchema.make` derives
* the published wire format from the encoded (`from`) side, so the manifest
* keeps advertising the plain union with both arms.
*/
export const ResourceSchema = Schema.transform(
RawResourceSchema,
Schema.typeSchema(RawResourceSchema),
{
strict: true,
decode: (raw) => normalizeResource(raw),
encode: (resource) => resource,
},
);
export const ResourceInputSchema = Schema.Struct({
name: Schema.String,
resource: ResourceSchema,
});
export const HandleInputSchema = Schema.Struct({
name: Schema.String,
type: SolTypeSchema,
});
export const FlowInputSchema = Schema.Union(
ResourceInputSchema,
HandleInputSchema,
);
export const RefSchema = Schema.Struct({
$ref: Schema.String.pipe(
Schema.filter((s) => s.indexOf('.') > 0, {
message: () =>
'$ref must be a dotpath like "input.name" or "nodeId.port"',
}),
),
});
export const LiteralBindingSchema = Schema.Struct({
kind: SolTypeSchema,
value: Schema.String,
});
export const BindValueSchema = Schema.Union(RefSchema, LiteralBindingSchema);
export const AppliedGuardSchema = Schema.Struct(
{ kind: Schema.String },
{ key: Schema.String, value: Schema.Unknown },
);
export const CallSchema = Schema.Struct({
id: Schema.String,
op: Schema.String,
bind: Schema.optionalWith(
Schema.Record({ key: Schema.String, value: BindValueSchema }),
{ default: () => ({}) },
),
config: Schema.optionalWith(
Schema.Record({ key: Schema.String, value: Schema.Unknown }),
{ default: () => ({}) },
),
guards: Schema.optional(Schema.Array(AppliedGuardSchema)),
});
export const ContinuationSchema = Schema.Struct({
awaits: Schema.String,
flowId: Schema.String,
});
export const FlowSchema = Schema.Struct({
version: Schema.Literal(1),
id: Schema.String,
chainId: ChainIdSchema,
inputs: Schema.Array(FlowInputSchema),
nodes: Schema.Array(CallSchema),
continuations: Schema.optional(Schema.Array(ContinuationSchema)),
});
export type SolType = typeof SolTypeSchema.Type;
export type ResourceInput = typeof ResourceInputSchema.Type;
export type HandleInput = typeof HandleInputSchema.Type;
export type FlowInput = typeof FlowInputSchema.Type;
export type Ref = typeof RefSchema.Type;
export type LiteralBinding = typeof LiteralBindingSchema.Type;
export type BindValue = typeof BindValueSchema.Type;
export type AppliedGuard = typeof AppliedGuardSchema.Type;
export type Call = typeof CallSchema.Type;
export type Continuation = typeof ContinuationSchema.Type;
export type Flow = typeof FlowSchema.Type;