@lifi/composer-sdk 0.0.1-alpha

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +220 -0
  3. package/dist/authoring/FlowBuilderCore.cjs +115 -0
  4. package/dist/authoring/FlowBuilderCore.cjs.map +1 -0
  5. package/dist/authoring/FlowBuilderCore.d.cts +114 -0
  6. package/dist/authoring/FlowBuilderCore.d.ts +114 -0
  7. package/dist/authoring/FlowBuilderCore.js +91 -0
  8. package/dist/authoring/FlowBuilderCore.js.map +1 -0
  9. package/dist/authoring/handles.cjs +43 -0
  10. package/dist/authoring/handles.cjs.map +1 -0
  11. package/dist/authoring/handles.d.cts +61 -0
  12. package/dist/authoring/handles.d.ts +61 -0
  13. package/dist/authoring/handles.js +17 -0
  14. package/dist/authoring/handles.js.map +1 -0
  15. package/dist/authoring/raw.cjs +41 -0
  16. package/dist/authoring/raw.cjs.map +1 -0
  17. package/dist/authoring/raw.d.cts +47 -0
  18. package/dist/authoring/raw.d.ts +47 -0
  19. package/dist/authoring/raw.js +16 -0
  20. package/dist/authoring/raw.js.map +1 -0
  21. package/dist/authoring/signatureArgs.cjs +114 -0
  22. package/dist/authoring/signatureArgs.cjs.map +1 -0
  23. package/dist/authoring/signatureArgs.d.cts +146 -0
  24. package/dist/authoring/signatureArgs.d.ts +146 -0
  25. package/dist/authoring/signatureArgs.js +87 -0
  26. package/dist/authoring/signatureArgs.js.map +1 -0
  27. package/dist/client.cjs +140 -0
  28. package/dist/client.cjs.map +1 -0
  29. package/dist/client.d.cts +67 -0
  30. package/dist/client.d.ts +67 -0
  31. package/dist/client.js +116 -0
  32. package/dist/client.js.map +1 -0
  33. package/dist/discovery.cjs +17 -0
  34. package/dist/discovery.cjs.map +1 -0
  35. package/dist/discovery.d.cts +56 -0
  36. package/dist/discovery.d.ts +56 -0
  37. package/dist/discovery.js +1 -0
  38. package/dist/discovery.js.map +1 -0
  39. package/dist/errors.cjs +91 -0
  40. package/dist/errors.cjs.map +1 -0
  41. package/dist/errors.d.cts +77 -0
  42. package/dist/errors.d.ts +77 -0
  43. package/dist/errors.js +65 -0
  44. package/dist/errors.js.map +1 -0
  45. package/dist/generated/config.generated.cjs +17 -0
  46. package/dist/generated/config.generated.cjs.map +1 -0
  47. package/dist/generated/config.generated.d.cts +144 -0
  48. package/dist/generated/config.generated.d.ts +144 -0
  49. package/dist/generated/config.generated.js +1 -0
  50. package/dist/generated/config.generated.js.map +1 -0
  51. package/dist/generated/guards.generated.cjs +32 -0
  52. package/dist/generated/guards.generated.cjs.map +1 -0
  53. package/dist/generated/guards.generated.d.cts +4 -0
  54. package/dist/generated/guards.generated.d.ts +4 -0
  55. package/dist/generated/guards.generated.js +8 -0
  56. package/dist/generated/guards.generated.js.map +1 -0
  57. package/dist/generated/materialisers.generated.cjs +44 -0
  58. package/dist/generated/materialisers.generated.cjs.map +1 -0
  59. package/dist/generated/materialisers.generated.d.cts +4 -0
  60. package/dist/generated/materialisers.generated.d.ts +4 -0
  61. package/dist/generated/materialisers.generated.js +18 -0
  62. package/dist/generated/materialisers.generated.js.map +1 -0
  63. package/dist/generated/operations.generated.cjs +110 -0
  64. package/dist/generated/operations.generated.cjs.map +1 -0
  65. package/dist/generated/operations.generated.d.cts +337 -0
  66. package/dist/generated/operations.generated.d.ts +337 -0
  67. package/dist/generated/operations.generated.js +89 -0
  68. package/dist/generated/operations.generated.js.map +1 -0
  69. package/dist/generated/preconditions.generated.cjs +44 -0
  70. package/dist/generated/preconditions.generated.cjs.map +1 -0
  71. package/dist/generated/preconditions.generated.d.cts +4 -0
  72. package/dist/generated/preconditions.generated.d.ts +4 -0
  73. package/dist/generated/preconditions.generated.js +18 -0
  74. package/dist/generated/preconditions.generated.js.map +1 -0
  75. package/dist/guards.generated-DKKDKREA.d.ts +14 -0
  76. package/dist/guards.generated-hrQ1KPNv.d.cts +14 -0
  77. package/dist/index.cjs +76 -0
  78. package/dist/index.cjs.map +1 -0
  79. package/dist/index.d.cts +99 -0
  80. package/dist/index.d.ts +99 -0
  81. package/dist/index.js +30 -0
  82. package/dist/index.js.map +1 -0
  83. package/dist/materialisers.generated-BsA1uDW8.d.ts +24 -0
  84. package/dist/materialisers.generated-nMxTAaVi.d.cts +24 -0
  85. package/dist/preconditions.generated-DblS13Wq.d.cts +24 -0
  86. package/dist/preconditions.generated-U7YpVh30.d.ts +24 -0
  87. package/dist/raw-Dj_2rBSQ.d.cts +19 -0
  88. package/dist/raw-IxCF_bSS.d.ts +19 -0
  89. package/dist/raw.cjs +34 -0
  90. package/dist/raw.cjs.map +1 -0
  91. package/dist/raw.d.cts +4 -0
  92. package/dist/raw.d.ts +4 -0
  93. package/dist/raw.js +8 -0
  94. package/dist/raw.js.map +1 -0
  95. package/dist/resources-BKlFxjo8.d.cts +56 -0
  96. package/dist/resources-CYis1_DS.d.ts +56 -0
  97. package/dist/resources.cjs +39 -0
  98. package/dist/resources.cjs.map +1 -0
  99. package/dist/resources.d.cts +3 -0
  100. package/dist/resources.d.ts +3 -0
  101. package/dist/resources.js +14 -0
  102. package/dist/resources.js.map +1 -0
  103. package/dist/run/inputs.cjs +51 -0
  104. package/dist/run/inputs.cjs.map +1 -0
  105. package/dist/run/inputs.d.cts +91 -0
  106. package/dist/run/inputs.d.ts +91 -0
  107. package/dist/run/inputs.js +26 -0
  108. package/dist/run/inputs.js.map +1 -0
  109. package/dist/sdk.cjs +60 -0
  110. package/dist/sdk.cjs.map +1 -0
  111. package/dist/sdk.d.cts +112 -0
  112. package/dist/sdk.d.ts +112 -0
  113. package/dist/sdk.js +40 -0
  114. package/dist/sdk.js.map +1 -0
  115. package/dist/types.cjs +17 -0
  116. package/dist/types.cjs.map +1 -0
  117. package/dist/types.d.cts +74 -0
  118. package/dist/types.d.ts +74 -0
  119. package/dist/types.js +1 -0
  120. package/dist/types.js.map +1 -0
  121. package/package.json +81 -0
  122. package/src/authoring/FlowBuilderCore.ts +270 -0
  123. package/src/authoring/handles.ts +81 -0
  124. package/src/authoring/raw.ts +60 -0
  125. package/src/authoring/signatureArgs.ts +249 -0
  126. package/src/client.ts +230 -0
  127. package/src/discovery.ts +51 -0
  128. package/src/errors.ts +150 -0
  129. package/src/examples/approveAndDeposit.ts +84 -0
  130. package/src/examples/callContract.ts +199 -0
  131. package/src/examples/config.ts +9 -0
  132. package/src/examples/consolidateToEth.ts +94 -0
  133. package/src/examples/consolidateToUsdc.ts +95 -0
  134. package/src/examples/depositFromProxy.ts +87 -0
  135. package/src/examples/dustSweep.ts +66 -0
  136. package/src/examples/lifiSwap.ts +56 -0
  137. package/src/examples/lifiZap.ts +60 -0
  138. package/src/examples/rawCallWithArithmetic.ts +78 -0
  139. package/src/examples/readContractState.ts +77 -0
  140. package/src/examples/run.ts +168 -0
  141. package/src/examples/splitAndZap.ts +73 -0
  142. package/src/examples/splitWithArithmetic.ts +93 -0
  143. package/src/examples/swapAndZap.ts +72 -0
  144. package/src/examples/swapToRecipient.ts +109 -0
  145. package/src/examples/swapWithAllowRevert.ts +119 -0
  146. package/src/examples/swapWithBalanceCheck.ts +73 -0
  147. package/src/examples/swapWithOutputValidation.ts +125 -0
  148. package/src/examples/transferTokens.ts +123 -0
  149. package/src/examples/untypedOpWithTypedRef.ts +77 -0
  150. package/src/generated/config.generated.ts +174 -0
  151. package/src/generated/guards.generated.ts +14 -0
  152. package/src/generated/materialisers.generated.ts +44 -0
  153. package/src/generated/operations.generated.ts +568 -0
  154. package/src/generated/preconditions.generated.ts +43 -0
  155. package/src/index.ts +149 -0
  156. package/src/raw.ts +10 -0
  157. package/src/resources.ts +61 -0
  158. package/src/run/inputs.ts +117 -0
  159. package/src/sdk.ts +173 -0
  160. package/src/types.ts +101 -0
@@ -0,0 +1,270 @@
1
+ import type {
2
+ AppliedGuard,
3
+ Call,
4
+ Flow,
5
+ FlowInput,
6
+ Ref,
7
+ Resource,
8
+ SolType,
9
+ } from '@lifi/compose-spec';
10
+
11
+ import type {
12
+ InputDecl,
13
+ InputSchema,
14
+ OutputKind,
15
+ TypedGuard,
16
+ TypedRef,
17
+ } from '../types.js';
18
+
19
+ import type {
20
+ InputHandle,
21
+ OutputHandle,
22
+ ResourceInputHandle,
23
+ } from './handles.js';
24
+ import { handleToRef } from './handles.js';
25
+ import { ref } from './raw.js';
26
+
27
+ /**
28
+ * Arguments for calling an operation node in a flow.
29
+ */
30
+ export interface CallArgs {
31
+ /** Maps parameter names to input/output handles or raw `$ref` pointers. */
32
+ readonly bind: Record<string, AnyBindable>;
33
+ /** Optional operation-specific configuration. */
34
+ readonly config?: object;
35
+ /** Optional guards (e.g. slippage checks) applied to this operation's outputs. */
36
+ readonly guards?: readonly TypedGuard[];
37
+ }
38
+
39
+ /**
40
+ * Any value that can be bound to an operation parameter: an input handle,
41
+ * an output handle from a previous operation, or a typed `$ref` pointer.
42
+ *
43
+ * Use `raw.ref<T>()` to create a typed ref from a raw path string.
44
+ */
45
+ export type AnyBindable = InputHandle | OutputHandle | TypedRef;
46
+
47
+ const toRef = (bindable: AnyBindable): Ref => {
48
+ if ('_tag' in bindable) return handleToRef(bindable);
49
+ return bindable;
50
+ };
51
+
52
+ /**
53
+ * Provides references to runtime context values available inside a flow.
54
+ * Access via `builder.context`.
55
+ */
56
+ export interface ContextAccessor {
57
+ /** A `$ref` pointing to the transaction sender (signer) address. */
58
+ readonly sender: TypedRef<'address'>;
59
+ /** A `$ref` pointing to the on-chain execution (proxy) address. */
60
+ readonly executionAddress: TypedRef<'address'>;
61
+ }
62
+
63
+ type InputHandleOf<D extends InputDecl> = D extends Resource
64
+ ? ResourceInputHandle
65
+ : D extends SolType
66
+ ? InputHandle<D>
67
+ : InputHandle;
68
+
69
+ /**
70
+ * A mapped type that converts an input schema into typed handles.
71
+ * Resource inputs produce {@link ResourceInputHandle}; scalar inputs produce {@link InputHandle}.
72
+ *
73
+ * @typeParam T - The flow's input schema.
74
+ */
75
+ export type InputHandles<T extends InputSchema> = {
76
+ readonly [K in keyof T]: InputHandleOf<T[K]>;
77
+ };
78
+
79
+ /**
80
+ * Options for creating a new flow builder.
81
+ *
82
+ * @typeParam T - The flow's input schema.
83
+ */
84
+ export interface FlowOptions<T extends InputSchema> {
85
+ /** Optional human-readable name for the flow. Defaults to a random UUID. */
86
+ readonly name?: string;
87
+ /** Input declarations mapping names to resource or scalar types. */
88
+ readonly inputs: T;
89
+ }
90
+
91
+ /**
92
+ * A Flow document branded with its input schema for type-safe request building.
93
+ *
94
+ * The `__inputs` field is a phantom type — it exists only at compile time for
95
+ * TypeScript inference and is never present at runtime.
96
+ */
97
+ export type TypedFlow<T extends InputSchema = InputSchema> = Flow & {
98
+ readonly __inputs?: T;
99
+ };
100
+
101
+ /**
102
+ * Core flow builder interface providing low-level access to flow construction.
103
+ *
104
+ * For most use cases, prefer the {@link FlowBuilder} type returned by `sdk.flow()`
105
+ * which adds generated operation methods and a `compile` method.
106
+ *
107
+ * @typeParam T - The flow's input schema.
108
+ */
109
+ export interface FlowBuilderCore<T extends InputSchema = InputSchema> {
110
+ /** Runtime context references (sender address, execution address). */
111
+ readonly context: ContextAccessor;
112
+ /** Typed handles for each declared flow input, used to bind inputs to operations. */
113
+ readonly inputs: InputHandles<T>;
114
+ /**
115
+ * Appends an untyped operation node to the flow. This is an escape hatch
116
+ * for operations not yet covered by the generated methods.
117
+ *
118
+ * Use `raw.ref<T>()` to reference this node's outputs in subsequent typed
119
+ * operations.
120
+ *
121
+ * @param id - Unique node identifier within the flow.
122
+ * @param op - The operation name (e.g. `"custom.vaultQuery"`).
123
+ * @param args - Bind map, config, and optional guards for the node.
124
+ * @throws Error if a node with the same `id` already exists in the flow.
125
+ */
126
+ readonly untypedOp: (
127
+ id: string,
128
+ op: string,
129
+ args: {
130
+ readonly bind: Record<string, Ref>;
131
+ readonly config: Record<string, unknown>;
132
+ readonly guards?: readonly AppliedGuard[];
133
+ },
134
+ ) => void;
135
+ /** Serialises the builder state into a {@link TypedFlow} document. */
136
+ readonly build: () => TypedFlow<T>;
137
+ }
138
+
139
+ /** Maps output port names to their output kind for compile-time and runtime validation. */
140
+ export type OutputSpec = Record<string, OutputKind>;
141
+
142
+ /** Internal interface exposed to bindGeneratedOps — not part of the public API. */
143
+ export interface FlowBuilderInternal<
144
+ T extends InputSchema = InputSchema,
145
+ > extends FlowBuilderCore<T> {
146
+ readonly call: <O extends OutputSpec>(
147
+ nodeId: string,
148
+ op: string,
149
+ args: CallArgs,
150
+ outputs: O,
151
+ ) => { readonly [K in keyof O & string]: OutputHandle<O[K]> };
152
+ }
153
+
154
+ const isResource = (decl: InputDecl): decl is Resource =>
155
+ typeof decl === 'object' && decl !== null && 'kind' in decl;
156
+
157
+ /**
158
+ * Creates a new flow builder targeting the given chain.
159
+ *
160
+ * @param chainId - The EVM chain ID (e.g. `1` for Ethereum mainnet).
161
+ * @param options - Flow configuration including input declarations.
162
+ * @returns A {@link FlowBuilderInternal} instance.
163
+ * @internal
164
+ */
165
+ export const createFlowBuilderCore = <T extends InputSchema>(
166
+ chainId: number,
167
+ options: FlowOptions<T>,
168
+ ): FlowBuilderInternal<T> => {
169
+ const id = options.name ?? crypto.randomUUID();
170
+ const flowInputs: FlowInput[] = [];
171
+ const nodes: Call[] = [];
172
+ const nodeIds = new Set<string>();
173
+
174
+ const inputHandles = {} as Record<string, InputHandle | ResourceInputHandle>;
175
+ for (const [name, decl] of Object.entries(options.inputs)) {
176
+ if (isResource(decl)) {
177
+ if (decl.chainId !== chainId) {
178
+ throw new Error(
179
+ `Input "${name}" has chainId ${decl.chainId} but flow targets chain ${chainId}`,
180
+ );
181
+ }
182
+ flowInputs.push({ name, resource: decl });
183
+ inputHandles[name] = { _tag: 'input', inputName: name, resource: decl };
184
+ } else {
185
+ flowInputs.push({ name, type: decl });
186
+ inputHandles[name] = { _tag: 'input', inputName: name };
187
+ }
188
+ }
189
+
190
+ const call = <O extends OutputSpec>(
191
+ nodeId: string,
192
+ op: string,
193
+ args: CallArgs,
194
+ outputs: O,
195
+ ): { readonly [K in keyof O & string]: OutputHandle<O[K]> } => {
196
+ if (nodeIds.has(nodeId)) throw new Error(`Duplicate node id: "${nodeId}"`);
197
+ nodeIds.add(nodeId);
198
+
199
+ const bind: Record<string, Ref> = {};
200
+ for (const [key, val] of Object.entries(args.bind)) {
201
+ bind[key] = toRef(val);
202
+ }
203
+
204
+ const node: Call = {
205
+ id: nodeId,
206
+ op,
207
+ bind,
208
+ config: (args.config ?? {}) as Record<string, unknown>,
209
+ ...(args.guards && args.guards.length > 0 && { guards: args.guards }),
210
+ };
211
+ nodes.push(node);
212
+
213
+ type Result = { readonly [K in keyof O & string]: OutputHandle<O[K]> };
214
+ return new Proxy({} as Result, {
215
+ get: (_target, prop): OutputHandle | undefined => {
216
+ if (typeof prop !== 'string' || prop === 'then') return undefined;
217
+ if (!(prop in outputs)) {
218
+ throw new Error(
219
+ `Op "${op}" has no output port "${prop}". Valid ports: ${Object.keys(
220
+ outputs,
221
+ ).join(', ')}`,
222
+ );
223
+ }
224
+ return { _tag: 'output', nodeId, portName: prop };
225
+ },
226
+ });
227
+ };
228
+
229
+ const untypedOp = (
230
+ nodeId: string,
231
+ op: string,
232
+ args: {
233
+ readonly bind: Record<string, Ref>;
234
+ readonly config: Record<string, unknown>;
235
+ readonly guards?: readonly AppliedGuard[];
236
+ },
237
+ ): void => {
238
+ if (nodeIds.has(nodeId)) throw new Error(`Duplicate node id: "${nodeId}"`);
239
+ nodeIds.add(nodeId);
240
+ const node: Call = {
241
+ id: nodeId,
242
+ op,
243
+ bind: args.bind,
244
+ config: args.config,
245
+ ...(args.guards && args.guards.length > 0 && { guards: args.guards }),
246
+ };
247
+ nodes.push(node);
248
+ };
249
+
250
+ const build = (): TypedFlow<T> => ({
251
+ version: 1,
252
+ id,
253
+ chainId,
254
+ inputs: [...flowInputs],
255
+ nodes: [...nodes],
256
+ });
257
+
258
+ const context: ContextAccessor = {
259
+ sender: ref<'address'>('context.sender'),
260
+ executionAddress: ref<'address'>('context.executionAddress'),
261
+ };
262
+
263
+ return {
264
+ context,
265
+ inputs: inputHandles as InputHandles<T>,
266
+ call,
267
+ untypedOp,
268
+ build,
269
+ };
270
+ };
@@ -0,0 +1,81 @@
1
+ import type { Resource } from '@lifi/compose-spec';
2
+
3
+ import type { OutputKind, TypedRef } from '../types.js';
4
+
5
+ /**
6
+ * A handle referencing a flow input declared in the flow's input schema.
7
+ * The phantom `__outputKind` field carries the type parameter through
8
+ * TypeScript's structural type system without affecting runtime shape.
9
+ */
10
+ export interface InputHandle<T extends OutputKind = OutputKind> {
11
+ readonly _tag: 'input';
12
+ readonly inputName: string;
13
+ readonly __outputKind?: T;
14
+ }
15
+
16
+ /**
17
+ * An input handle for a resource (token) input, carrying the resource
18
+ * declaration alongside the reference.
19
+ */
20
+ export interface ResourceInputHandle extends InputHandle<'resource'> {
21
+ readonly resource: Resource;
22
+ }
23
+
24
+ /**
25
+ * A handle referencing an output port of a previously declared operation node.
26
+ * The phantom `__outputKind` field carries the type parameter through
27
+ * TypeScript's structural type system without affecting runtime shape.
28
+ */
29
+ export interface OutputHandle<T extends OutputKind = OutputKind> {
30
+ readonly _tag: 'output';
31
+ readonly nodeId: string;
32
+ readonly portName: string;
33
+ readonly __outputKind?: T;
34
+ }
35
+
36
+ /** Discriminated union of all handle types that can be bound to operation arguments. */
37
+ export type AnyHandle = InputHandle | OutputHandle;
38
+
39
+ /** Type guard that narrows an {@link AnyHandle} to an {@link InputHandle}. */
40
+ export const isInputHandle = (h: AnyHandle): h is InputHandle =>
41
+ h._tag === 'input';
42
+
43
+ /** Type guard that narrows an {@link AnyHandle} to an {@link OutputHandle}. */
44
+ export const isOutputHandle = (h: AnyHandle): h is OutputHandle =>
45
+ h._tag === 'output';
46
+
47
+ /**
48
+ * Converts a handle to a JSON `$ref` pointer used in the flow document.
49
+ * Input handles become `"input.<name>"`; output handles become `"<nodeId>.<port>"`.
50
+ */
51
+ export const handleToRef = (h: AnyHandle): { $ref: string } => {
52
+ if (h._tag === 'input') {
53
+ if (!h.inputName) throw new Error('InputHandle has empty inputName');
54
+ return { $ref: `input.${h.inputName}` };
55
+ }
56
+ if (!h.nodeId) throw new Error('OutputHandle has empty nodeId');
57
+ if (!h.portName) throw new Error('OutputHandle has empty portName');
58
+ return { $ref: `${h.nodeId}.${h.portName}` };
59
+ };
60
+
61
+ /**
62
+ * For a bind slot expecting type T, also accept `'resource'`-tagged
63
+ * handles when T is `'uint256'` (resources are uint256 amounts).
64
+ */
65
+ type ResourceAssignable<T extends OutputKind> = T extends 'uint256'
66
+ ? T | 'resource'
67
+ : T;
68
+
69
+ /**
70
+ * The set of values accepted by a typed bind slot.
71
+ * - `OutputHandle<T>` / `InputHandle<T>` (same type)
72
+ * - For `'uint256'` slots, also `'resource'`-tagged handles
73
+ * - `TypedRef<T>` (typed context refs, subject to `ResourceAssignable`)
74
+ *
75
+ * Plain `{ $ref: '...' }` objects are **not** accepted — use `raw.ref<T>()`
76
+ * to create a typed ref when you need to reference a raw path.
77
+ */
78
+ export type Bindable<T extends OutputKind> =
79
+ | OutputHandle<ResourceAssignable<T>>
80
+ | InputHandle<ResourceAssignable<T>>
81
+ | TypedRef<ResourceAssignable<T>>;
@@ -0,0 +1,60 @@
1
+ import type { AppliedGuard } from '@lifi/compose-spec';
2
+
3
+ import type { OutputKind, TypedRef } from '../types.js';
4
+
5
+ /**
6
+ * Creates a typed `$ref` pointer for use in bind slots.
7
+ *
8
+ * This is an **escape hatch** for referencing values that the typed builder
9
+ * API doesn't cover — for example, outputs of an `untypedOp` node or custom
10
+ * context paths. The caller is responsible for choosing the correct type
11
+ * parameter; no runtime validation occurs.
12
+ *
13
+ * Prefer typed handles (`OutputHandle`, `InputHandle`) and `builder.context`
14
+ * whenever possible. Use `ref` only when you need to construct a `$ref` path
15
+ * manually and want it accepted by a `Bindable<T>` slot.
16
+ *
17
+ * @typeParam T - The output kind this ref represents (e.g. `'address'`, `'uint256'`, `'resource'`).
18
+ * @param path - The `$ref` path (e.g. `"context.sender"`, `"myNode.result"`).
19
+ * @returns A {@link TypedRef} accepted by `Bindable<T>` slots matching `T`.
20
+ *
21
+ * @example
22
+ * ```ts
23
+ * import { raw } from '@lifi/composer-sdk';
24
+ *
25
+ * // Reference an untypedOp output in a typed operation:
26
+ * builder.untypedOp('custom', 'some.op', {
27
+ * bind: { x: { $ref: 'input.token' } },
28
+ * config: {},
29
+ * });
30
+ * builder.core.add('sum', {
31
+ * bind: {
32
+ * a: raw.ref<'uint256'>('custom.result'),
33
+ * b: someTypedHandle,
34
+ * },
35
+ * });
36
+ * ```
37
+ */
38
+ export const ref = <T extends OutputKind>(path: string): TypedRef<T> =>
39
+ ({ $ref: path }) as TypedRef<T>;
40
+
41
+ /**
42
+ * Creates a raw guard object for use in the `guards` array of an operation call.
43
+ *
44
+ * @param kind - The guard type (e.g. `"slippage"`).
45
+ * @param config - Guard-specific configuration (e.g. `{ toleranceBps: 300 }`).
46
+ * @returns An {@link AppliedGuard} object.
47
+ */
48
+ export const rawGuard = (
49
+ kind: string,
50
+ config: Record<string, unknown> = {},
51
+ ): AppliedGuard => {
52
+ if ('kind' in config) {
53
+ throw new Error(
54
+ `rawGuard: 'kind' must not appear in config (got "${String(
55
+ config.kind,
56
+ )}"). Pass it as the first argument instead.`,
57
+ );
58
+ }
59
+ return { ...config, kind };
60
+ };
@@ -0,0 +1,249 @@
1
+ import type { Ref } from '@lifi/compose-spec';
2
+ import { type ParseAbiItem, parseAbiItem } from 'abitype';
3
+
4
+ import type { AbiTypeToOutputKind, TypedGuard } from '../types.js';
5
+
6
+ import type { AnyBindable, CallArgs } from './FlowBuilderCore.js';
7
+ import type { Bindable, OutputHandle } from './handles.js';
8
+ import { handleToRef } from './handles.js';
9
+
10
+ /**
11
+ * Type-level extraction of named bind keys from a function signature string.
12
+ * When `TSig` is a string literal matching a Solidity function signature,
13
+ * resolves to a record whose keys are the parameter names and values are
14
+ * `Bindable<kind>` where the kind is derived from each parameter's Solidity
15
+ * type via `AbiTypeToOutputKind`. For recognised `SolType` parameters
16
+ * (e.g. `uint256`, `address`, `uint128`) the bind slot is precisely typed;
17
+ * for other Solidity types (e.g. tuples) the slot accepts any scalar handle.
18
+ * Falls back to `Record<string, AnyBindable>` for non-literal strings.
19
+ */
20
+ export type SignatureBind<TSig extends string> = string extends TSig
21
+ ? Record<string, AnyBindable>
22
+ : ParseAbiItem<TSig> extends {
23
+ type: 'function';
24
+ inputs: infer I extends readonly { name: string; type: string }[];
25
+ }
26
+ ? {
27
+ readonly [P in I[number] as P['name']]: Bindable<
28
+ AbiTypeToOutputKind<P['type']>
29
+ >;
30
+ }
31
+ : Record<string, AnyBindable>;
32
+
33
+ // ---------------------------------------------------------------------------
34
+ // Return-type inference from function signatures
35
+ // ---------------------------------------------------------------------------
36
+
37
+ /**
38
+ * Describes the output type(s) of a parsed function signature as a
39
+ * human-readable string for inclusion in type-level error messages.
40
+ */
41
+ type DescribeOutputs<O extends readonly { type: string }[]> =
42
+ O extends readonly [{ type: infer T extends string }] ? T : 'multiple values';
43
+
44
+ /**
45
+ * A string-literal type that surfaces a clear error when the user provides a
46
+ * function signature whose return type is not supported by core.call.
47
+ * The message is visible in IDE tooltips and compiler diagnostics.
48
+ */
49
+ type UnsupportedReturnTypeError<TLabel extends string, TSig extends string> =
50
+ ParseAbiItem<TSig> extends {
51
+ type: 'function';
52
+ outputs: infer O extends readonly { type: string }[];
53
+ }
54
+ ? `[TypeError] ${TLabel}: return type '${DescribeOutputs<O>}' is not supported — only 'uint256' or void signatures are allowed`
55
+ : `[TypeError] ${TLabel}: unable to parse return type from signature`;
56
+
57
+ /**
58
+ * Conditional output type for `core.call` based on the function signature's
59
+ * return type:
60
+ *
61
+ * - `returns (uint256)` → `{ result: OutputHandle }`
62
+ * - No returns clause → `{ result: undefined }`
63
+ * - Anything else → `{ result: "[TypeError] ..." }` (compile-time error)
64
+ *
65
+ * When `TSig` is a non-literal `string` (e.g. a variable, not a string
66
+ * constant), the type falls back to `{ result: OutputHandle | undefined }`
67
+ * so that both void and uint256 call sites remain assignable.
68
+ */
69
+ export type CoreCallResult<TSig extends string> = string extends TSig
70
+ ? { readonly result: OutputHandle<'uint256'> | undefined }
71
+ : ParseAbiItem<TSig> extends { type: 'function'; outputs: readonly [] }
72
+ ? { readonly result: undefined }
73
+ : ParseAbiItem<TSig> extends {
74
+ type: 'function';
75
+ outputs: readonly [{ type: 'uint256' }];
76
+ }
77
+ ? { readonly result: OutputHandle<'uint256'> }
78
+ : { readonly result: UnsupportedReturnTypeError<'core.call', TSig> };
79
+
80
+ /**
81
+ * Conditional output type for `core.staticCall`. staticCall always reads a
82
+ * value, so void signatures are also rejected:
83
+ *
84
+ * - `returns (uint256)` → `{ result: OutputHandle }`
85
+ * - No returns / other → `{ result: "[TypeError] ..." }` (compile-time error)
86
+ */
87
+ export type StaticCallResult<TSig extends string> = string extends TSig
88
+ ? { readonly result: OutputHandle<'uint256'> }
89
+ : ParseAbiItem<TSig> extends {
90
+ type: 'function';
91
+ outputs: readonly [{ type: 'uint256' }];
92
+ }
93
+ ? { readonly result: OutputHandle<'uint256'> }
94
+ : ParseAbiItem<TSig> extends { type: 'function'; outputs: readonly [] }
95
+ ? {
96
+ readonly result: `[TypeError] core.staticCall: function has no return value — staticCall requires a 'returns (uint256)' signature`;
97
+ }
98
+ : {
99
+ readonly result: UnsupportedReturnTypeError<'core.staticCall', TSig>;
100
+ };
101
+
102
+ /**
103
+ * Parses a Solidity function signature and returns parameter names in order.
104
+ * Delegates to abitype's runtime `parseAbiItem` — the same parser that powers
105
+ * the compile-time `ParseAbiItem<TSig>` type — so runtime and type-level
106
+ * parsing can never diverge.
107
+ *
108
+ * @throws if the signature is not a valid function signature or a parameter is unnamed
109
+ */
110
+ export const parseFunctionParams = (functionSignature: string): string[] => {
111
+ const parsed = parseAbiItem(functionSignature);
112
+ if (parsed.type !== 'function') {
113
+ throw new Error(
114
+ `core.call: expected a function signature, got "${parsed.type}": "${functionSignature}"`,
115
+ );
116
+ }
117
+
118
+ return parsed.inputs.map((input, index) => {
119
+ if (!input.name) {
120
+ throw new Error(
121
+ `core.call: parameter at index ${index} in signature has no name. All parameters must be named for named binds to work.`,
122
+ );
123
+ }
124
+ return input.name;
125
+ });
126
+ };
127
+
128
+ /**
129
+ * Converts any `AnyBindable` to a `Ref` (`{ $ref: string }`).
130
+ * Handles carry a `_tag` property and are converted via `handleToRef`;
131
+ * `Ref` objects pass through as-is.
132
+ */
133
+ export const toBindRef = (bindable: AnyBindable): Ref => {
134
+ if ('_tag' in bindable) return handleToRef(bindable);
135
+ return bindable;
136
+ };
137
+
138
+ export interface BuildCallWireFormatInput {
139
+ readonly resource?: AnyBindable;
140
+ readonly bind: Record<string, AnyBindable>;
141
+ readonly config: Record<string, unknown>;
142
+ readonly guards?: readonly TypedGuard[];
143
+ }
144
+
145
+ export interface CallWireResult {
146
+ readonly op: 'core.call' | 'core.invoke';
147
+ readonly bind: Record<string, Ref>;
148
+ readonly config: Record<string, unknown>;
149
+ readonly guards?: readonly TypedGuard[];
150
+ }
151
+
152
+ /**
153
+ * Parses a function signature from `config`, validates bind keys against it,
154
+ * and converts named binds to positional `$ref` args.
155
+ */
156
+ const resolvePositionalArgs = (
157
+ label: string,
158
+ bind: Record<string, AnyBindable>,
159
+ config: Record<string, unknown>,
160
+ ): Ref[] => {
161
+ const functionSignature = config.functionSignature;
162
+ if (typeof functionSignature !== 'string') {
163
+ throw new Error(
164
+ `${label}: config.functionSignature is required and must be a string`,
165
+ );
166
+ }
167
+
168
+ const paramNames = parseFunctionParams(functionSignature);
169
+ const paramSet = new Set(paramNames);
170
+ const bindKeys = Object.keys(bind);
171
+ const bindSet = new Set(bindKeys);
172
+
173
+ const missing = paramNames.filter((n) => !bindSet.has(n));
174
+ const extra = bindKeys.filter((k) => !paramSet.has(k));
175
+
176
+ if (missing.length > 0 || extra.length > 0) {
177
+ throw new Error(
178
+ `${label}: bind keys [${bindKeys.join(
179
+ ', ',
180
+ )}] do not match signature parameters [${paramNames.join(
181
+ ', ',
182
+ )}]. Missing: ${missing.join(', ') || '(none)'}. Extra: ${
183
+ extra.join(', ') || '(none)'
184
+ }.`,
185
+ );
186
+ }
187
+
188
+ return paramNames.map((name) => {
189
+ const bindable = bind[name];
190
+ if (bindable == null) {
191
+ throw new Error(
192
+ `${label}: bind value for parameter "${name}" is undefined`,
193
+ );
194
+ }
195
+ return toBindRef(bindable);
196
+ });
197
+ };
198
+
199
+ /**
200
+ * Transforms the user-facing `resource` + named `bind` API into the wire
201
+ * format that the backend expects:
202
+ * - With resource: `core.call` with `bind: { input: <resourceRef> }`
203
+ * - Without resource: `core.invoke` with `bind: {}`
204
+ * - `config: { ...userConfig, args: [positional refs...] }`
205
+ */
206
+ export const buildCallWireFormat = (
207
+ args: BuildCallWireFormatInput,
208
+ ): CallWireResult => {
209
+ const positionalArgs = resolvePositionalArgs(
210
+ 'core.call',
211
+ args.bind,
212
+ args.config,
213
+ );
214
+ const hasResource = args.resource != null;
215
+
216
+ return {
217
+ op: hasResource ? 'core.call' : 'core.invoke',
218
+ bind: hasResource ? { input: toBindRef(args.resource) } : {},
219
+ config: { ...args.config, args: positionalArgs },
220
+ ...(args.guards && args.guards.length > 0 && { guards: args.guards }),
221
+ };
222
+ };
223
+
224
+ export interface BuildStaticCallWireFormatInput {
225
+ readonly bind: Record<string, AnyBindable>;
226
+ readonly config: Record<string, unknown>;
227
+ readonly guards?: readonly TypedGuard[];
228
+ }
229
+
230
+ /**
231
+ * Transforms the user-facing named `bind` API into the wire format that the
232
+ * backend expects for `core.staticCall`. Same as `buildCallWireFormat` but
233
+ * with no `resource` field — staticCall has no resource inputs.
234
+ */
235
+ export const buildStaticCallWireFormat = (
236
+ args: BuildStaticCallWireFormatInput,
237
+ ): CallArgs => {
238
+ const positionalArgs = resolvePositionalArgs(
239
+ 'core.staticCall',
240
+ args.bind,
241
+ args.config,
242
+ );
243
+
244
+ return {
245
+ bind: {},
246
+ config: { ...args.config, args: positionalArgs },
247
+ ...(args.guards && args.guards.length > 0 && { guards: args.guards }),
248
+ };
249
+ };