@lifi/composer-sdk
Version:
Public Composer SDK for building and submitting flows
128 lines (113 loc) • 4.76 kB
text/typescript
import type {
BindValue,
Ref,
Resource,
SolType,
StaticSolType,
} from '@lifi/compose-spec';
import type { OutputKind, TypedLiteral, TypedRef } from '../types.js';
/**
* A handle referencing a flow input declared in the flow's input schema.
* The phantom `__outputKind` field carries the type parameter through
* TypeScript's structural type system without affecting runtime shape.
*/
export interface InputHandle<T extends OutputKind = OutputKind> {
readonly _tag: 'input';
readonly inputName: string;
readonly __outputKind?: T;
}
/**
* An input handle for a resource (token) input, carrying the resource
* declaration alongside the reference.
*/
export interface ResourceInputHandle extends InputHandle<'resource'> {
readonly resource: Resource;
}
/**
* A handle referencing an output port of a previously declared operation node.
* The phantom `__outputKind` field carries the type parameter through
* TypeScript's structural type system without affecting runtime shape.
*/
export interface OutputHandle<T extends OutputKind = OutputKind> {
readonly _tag: 'output';
readonly nodeId: string;
readonly portName: string;
readonly __outputKind?: T;
}
/** Discriminated union of all handle types that can be bound to operation arguments. */
export type AnyHandle = InputHandle | OutputHandle;
/** Type guard that narrows an {@link AnyHandle} to an {@link InputHandle}. */
export const isInputHandle = (h: AnyHandle): h is InputHandle =>
h._tag === 'input';
/** Type guard that narrows an {@link AnyHandle} to an {@link OutputHandle}. */
export const isOutputHandle = (h: AnyHandle): h is OutputHandle =>
h._tag === 'output';
/**
* Converts a handle to a JSON `$ref` pointer used in the flow document.
* Input handles become `"input.<name>"`; output handles become `"<nodeId>.<port>"`.
*/
export const handleToRef = (h: AnyHandle): { $ref: string } => {
if (h._tag === 'input') {
if (!h.inputName) throw new Error('InputHandle has empty inputName');
return { $ref: `input.${h.inputName}` };
}
if (!h.nodeId) throw new Error('OutputHandle has empty nodeId');
if (!h.portName) throw new Error('OutputHandle has empty portName');
return { $ref: `${h.nodeId}.${h.portName}` };
};
/**
* For a bind slot expecting type T, widen the set of accepted handle types:
* - `'bytes32'` is the raw 32-byte word type: accept any static-32-byte handle
* (`uint256`/`uintN`/`address`/`bool`/`bytes32`) plus `'resource'` (a uint256
* amount). At the IR1 boundary every value is a type-erased word, so any
* static-32-byte handle is a sound bind for a `bytes32` payload port.
* - `'uint256'` keeps its existing widening to also accept `'resource'`-tagged
* handles (resources are uint256 amounts).
* - every other type keeps exact-SolType matching.
*/
type SlotAssignable<T extends OutputKind> = T extends 'bytes32'
? StaticSolType | 'resource'
: T extends 'uint256'
? T | 'resource'
: T;
/**
* The values accepted by a typed slot that must resolve to a `$ref`:
* - `OutputHandle<T>` / `InputHandle<T>` (same type)
* - For `'uint256'` slots, also `'resource'`-tagged handles
* - For `'bytes32'` slots, any static-32-byte handle (and `'resource'`)
* - `TypedRef<T>` (typed context refs, subject to `SlotAssignable`)
*
* Plain `{ $ref: '...' }` objects are **not** accepted - use `raw.ref<T>()`
* to create a typed ref when you need to reference a raw path.
*/
export type RefBindable<T extends OutputKind> =
| OutputHandle<SlotAssignable<T>>
| InputHandle<SlotAssignable<T>>
| TypedRef<SlotAssignable<T>>;
/**
* Any ref-resolving bindable, with no per-slot type constraint.
*/
export type AnyRefBindable = InputHandle | OutputHandle | TypedRef;
/**
* The set of values accepted by a typed operation bind slot: everything
* {@link RefBindable} accepts, plus a {@link TypedLiteral} constant baked
* into the flow document. A `'resource'` slot rejects a literal: a token
* resource is not a scalar constant.
*
* Use `raw.literal<T>()` to create one.
*/
export type Bindable<T extends OutputKind> =
RefBindable<T> | LiteralBindable<SlotAssignable<T>>;
// Distributes over the slot's assignable kinds and keeps the scalar ones: a
// `'resource'` slot has no scalar arm, so its literal arm collapses to
// `never` and a literal is rejected outright.
type LiteralBindable<K extends OutputKind> = K extends SolType
? TypedLiteral<K>
: never;
/**
* Converts a bindable to the wire `bind` value it denotes: handles become
* `$ref` pointers, refs and literals pass through as-is.
*/
export const toBindValue = (
bindable: InputHandle | OutputHandle | TypedRef | TypedLiteral | Ref,
): BindValue => ('_tag' in bindable ? handleToRef(bindable) : bindable);