UNPKG

@lifi/compose-spec

Version:

Public wire-format types and schemas for Compose flows

196 lines (180 loc) 5.99 kB
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>; }; };