@lifi/compose-spec
Version:
Public wire-format types and schemas for Compose flows
196 lines (180 loc) • 5.99 kB
text/typescript
import type {
HandlePort,
Port,
ResourceOutputPort,
ResourcePort,
} from './flow.js';
import type { HandleInput, Ref, ResourceInput, SolType } from './flowSchema.js';
import type { Availability } from './resource.js';
import { erc20Resource, nativeResource } from './resource.js';
import type { HandleUnits } from './zodSchemas.js';
export interface OpHandleOptions<Opt extends boolean = boolean> {
readonly expose?: boolean;
readonly units?: HandleUnits;
readonly optional?: Opt;
}
export const inputRef = (port: string): Ref => ({ $ref: `input.${port}` });
export const outputRef = (node: string, port: string): Ref => ({
$ref: `${node}.${port}`,
});
/** Returns `{ [key]: value }` when `value` is not nullish, else `{}`. */
const optionalProp = <K extends string, V>(
key: K,
value: V | undefined | null,
): Partial<Record<K, V>> =>
value !== undefined && value !== null
? ({ [key]: value } as Partial<Record<K, V>>)
: {};
export const linear = <N extends string, T extends SolType>(
name: N,
type: T,
): Port & { readonly name: N; readonly type: T; readonly mode: 'linear' } => ({
name,
type,
mode: 'linear',
availability: 'now',
});
export const copy = <N extends string, T extends SolType>(
name: N,
type: T,
): Port & { readonly name: N; readonly type: T; readonly mode: 'copy' } => ({
name,
type,
mode: 'copy',
availability: 'now',
});
export const handle = <N extends string, T extends SolType>(
name: N,
type: T,
): HandleInput & { readonly name: N; readonly type: T } => ({ name, type });
export const native = <N extends string>(
name: N,
chainId: number,
): ResourceInput & { readonly name: N } => ({
name,
resource: nativeResource(chainId),
});
export const erc20 = <N extends string>(
name: N,
token: string,
chainId: number,
): ResourceInput & { readonly name: N } => ({
name,
resource: erc20Resource(token, chainId),
});
// `NoInfer` on the return type is load-bearing: these ports land in tuples
// contextually typed by `ResourcePort`/`HandlePort`, whose `optional?: boolean`
// would otherwise win the inference race and widen `Opt` to `boolean`,
// collapsing the optional-aware binding types downstream.
export const resource = <N extends string, Opt extends boolean = false>(
name: N,
accepts: 'erc20' | 'native' | 'any',
options?: { readonly optional?: Opt },
): ResourcePort & { readonly name: N; readonly optional: NoInfer<Opt> } =>
({
kind: 'resource',
name,
accepts,
mode: 'linear',
...optionalProp('optional', options?.optional),
}) as ResourcePort & { readonly name: N; readonly optional: Opt };
export const readResource = <N extends string>(
name: N,
accepts: 'erc20' | 'native' | 'any',
): ResourcePort & { readonly name: N; readonly mode: 'copy' } => ({
kind: 'resource',
name,
accepts,
mode: 'copy',
});
export const opHandle = <
N extends string,
T extends SolType,
Opt extends boolean = false,
>(
name: N,
type: T,
options?: OpHandleOptions<Opt>,
): HandlePort & {
readonly name: N;
readonly type: T;
readonly optional: NoInfer<Opt>;
} =>
({
kind: 'handle',
name,
type,
mode: 'copy',
...optionalProp('expose', options?.expose),
...optionalProp('units', options?.units),
...optionalProp('optional', options?.optional),
}) as HandlePort & {
readonly name: N;
readonly type: T;
readonly optional: Opt;
};
// Options accepted by `resourceOutput`: the bare `Availability` shorthand or
// the full record form.
type ResourceOutputOptions =
| Availability
| {
readonly availability?: Availability;
readonly providesMinimum?: boolean;
readonly omitIfZero?: boolean;
readonly deliveryAddressInput?: string;
};
// Resolve the *type-level* availability of a resource output from the options
// the caller passed. Preserving the availability literal is load-bearing:
// `defineOp` extracts the `'future'` port names from this literal, so widening
// it here would erase the future/now distinction the lowering contract needs.
//
// The runtime `availability` property may be ABSENT — the `'now'` cases omit it
// (see `optionalProp` below) because a missing availability already means
// `'now'` everywhere it is read. The type nonetheless reports `'now'`: a
// documented type-level fiction that treats the missing property as its
// effective value rather than surfacing `undefined`.
//
// A widened `Availability` value (neither literal) stays widened. It is not the
// `'future'` literal, so downstream treats it as non-future — a handle is
// required — which is the conservative choice (supplying a handle is always
// allowed).
type OutputAvailability<O> = [O] extends ['future']
? 'future'
: [O] extends ['now']
? 'now'
: [O] extends [string]
? Availability
: O extends { readonly availability: infer A }
? [A] extends ['future']
? 'future'
: [A] extends ['now']
? 'now'
: Availability
: 'now';
export const resourceOutput = <
N extends string,
const O extends ResourceOutputOptions = 'now',
>(
name: N,
options?: O,
): ResourceOutputPort & {
readonly name: N;
readonly availability: OutputAvailability<O>;
} => {
// Widen away the generic before narrowing: `typeof` cannot narrow a value
// of generic type `O`, so the union members would keep their string arms.
const raw: ResourceOutputOptions | undefined = options;
const opts = typeof raw === 'string' ? { availability: raw } : raw;
return {
kind: 'resource_output',
name,
mode: 'linear',
...optionalProp('availability', opts?.availability),
...optionalProp('providesMinimum', opts?.providesMinimum),
...optionalProp('omitIfZero', opts?.omitIfZero),
...optionalProp('deliveryAddressInput', opts?.deliveryAddressInput),
} as ResourceOutputPort & {
readonly name: N;
readonly availability: OutputAvailability<O>;
};
};