@lifi/composer-sdk 0.6.2 → 0.7.0

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 (38) hide show
  1. package/README.md +202 -189
  2. package/dist/authoring/FlowBuilderCore.cjs +16 -3
  3. package/dist/authoring/FlowBuilderCore.cjs.map +1 -1
  4. package/dist/authoring/FlowBuilderCore.d.cts +12 -4
  5. package/dist/authoring/FlowBuilderCore.d.ts +12 -4
  6. package/dist/authoring/FlowBuilderCore.js +19 -3
  7. package/dist/authoring/FlowBuilderCore.js.map +1 -1
  8. package/dist/authoring/handles.cjs.map +1 -1
  9. package/dist/authoring/handles.d.cts +35 -1
  10. package/dist/authoring/handles.d.ts +35 -1
  11. package/dist/authoring/handles.js.map +1 -1
  12. package/dist/client.cjs +1 -1
  13. package/dist/client.js +1 -1
  14. package/dist/generated/config.generated.cjs.map +1 -1
  15. package/dist/generated/config.generated.d.cts +500 -6
  16. package/dist/generated/config.generated.d.ts +500 -6
  17. package/dist/generated/operations.generated.cjs +7 -0
  18. package/dist/generated/operations.generated.cjs.map +1 -1
  19. package/dist/generated/operations.generated.d.cts +20 -5
  20. package/dist/generated/operations.generated.d.ts +20 -5
  21. package/dist/generated/operations.generated.js +7 -0
  22. package/dist/generated/operations.generated.js.map +1 -1
  23. package/dist/index.cjs.map +1 -1
  24. package/dist/index.d.cts +3 -3
  25. package/dist/index.d.ts +3 -3
  26. package/dist/index.js.map +1 -1
  27. package/dist/sdk.cjs.map +1 -1
  28. package/dist/sdk.d.cts +5 -6
  29. package/dist/sdk.d.ts +5 -6
  30. package/dist/sdk.js.map +1 -1
  31. package/package.json +4 -3
  32. package/src/authoring/FlowBuilderCore.ts +55 -18
  33. package/src/authoring/handles.ts +67 -0
  34. package/src/examples/splitAndZap.ts +9 -8
  35. package/src/generated/config.generated.ts +516 -5
  36. package/src/generated/operations.generated.ts +52 -3
  37. package/src/index.ts +5 -1
  38. package/src/sdk.ts +5 -6
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lifi/composer-sdk",
3
- "version": "0.6.2",
3
+ "version": "0.7.0",
4
4
  "description": "Public Composer SDK for building and submitting flows",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -47,16 +47,17 @@
47
47
  "example:staged": "tsx src/examples/run.ts --staged"
48
48
  },
49
49
  "peerDependencies": {
50
- "@lifi/compose-spec": "0.6.2"
50
+ "@lifi/compose-spec": "0.7.0"
51
51
  },
52
52
  "dependencies": {
53
53
  "abitype": "^1.2.3",
54
54
  "effect": "^3.21.0",
55
- "zod": "^4.1.11"
55
+ "zod": "^4.6.5"
56
56
  },
57
57
  "devDependencies": {
58
58
  "@eslint/compat": "^1.4.0",
59
59
  "@lifi/compose-spec": "workspace:^",
60
+ "@lifi/vitest-config": "workspace:^",
60
61
  "@types/node": "^25.6.0",
61
62
  "@typescript-eslint/eslint-plugin": "^8.68.0",
62
63
  "@typescript-eslint/parser": "^8.68.0",
@@ -1,12 +1,14 @@
1
- import type {
2
- AppliedGuard,
3
- BindValue,
4
- Call,
5
- Flow,
6
- FlowInput,
7
- Ref,
8
- Resource,
9
- SolType,
1
+ import {
2
+ type AppliedGuard,
3
+ type BindValue,
4
+ type Call,
5
+ familySize,
6
+ type Flow,
7
+ type FlowInput,
8
+ instancePortName,
9
+ type Ref,
10
+ type Resource,
11
+ type SolType,
10
12
  } from '@lifi/compose-spec';
11
13
 
12
14
  import type {
@@ -146,16 +148,34 @@ export interface FlowBuilderCore<T extends InputSchema = InputSchema> {
146
148
  /** Maps output port names to their output kind for compile-time and runtime validation. */
147
149
  export type OutputSpec = Record<string, OutputKind>;
148
150
 
151
+ /**
152
+ * Maps each output family's port name to the config key that sizes it: an
153
+ * array (one instance per element) or a non-negative integer count.
154
+ * A family port is also listed in the {@link OutputSpec}, as `'resource'`.
155
+ */
156
+ export type FamilySpec = Readonly<Record<string, string>>;
157
+
158
+ /** The handles `call` returns for an {@link OutputSpec} and its families. */
159
+ export type OutputHandles<O extends OutputSpec, F extends FamilySpec> = {
160
+ readonly [K in keyof O & string]: K extends keyof F
161
+ ? readonly OutputHandle<'resource'>[]
162
+ : OutputHandle<O[K]>;
163
+ };
164
+
149
165
  /** Internal interface exposed to bindGeneratedOps — not part of the public API. */
150
166
  export interface FlowBuilderInternal<
151
167
  T extends InputSchema = InputSchema,
152
168
  > extends FlowBuilderCore<T> {
153
- readonly call: <O extends OutputSpec>(
169
+ readonly call: <
170
+ O extends OutputSpec,
171
+ F extends FamilySpec = Record<never, string>,
172
+ >(
154
173
  nodeId: string,
155
174
  op: string,
156
175
  args: CallArgs,
157
176
  outputs: O,
158
- ) => { readonly [K in keyof O & string]: OutputHandle<O[K]> };
177
+ families?: F,
178
+ ) => OutputHandles<O, F>;
159
179
  }
160
180
 
161
181
  const isResource = (decl: InputDecl): decl is Resource =>
@@ -194,12 +214,16 @@ export const createFlowBuilderCore = <T extends InputSchema>(
194
214
  }
195
215
  }
196
216
 
197
- const call = <O extends OutputSpec>(
217
+ const call = <
218
+ O extends OutputSpec,
219
+ F extends FamilySpec = Record<never, string>,
220
+ >(
198
221
  nodeId: string,
199
222
  op: string,
200
223
  args: CallArgs,
201
224
  outputs: O,
202
- ): { readonly [K in keyof O & string]: OutputHandle<O[K]> } => {
225
+ families?: F,
226
+ ): OutputHandles<O, F> => {
203
227
  if (nodeIds.has(nodeId)) throw new Error(`Duplicate node id: "${nodeId}"`);
204
228
  nodeIds.add(nodeId);
205
229
 
@@ -208,18 +232,26 @@ export const createFlowBuilderCore = <T extends InputSchema>(
208
232
  bind[key] = toBindValue(val);
209
233
  }
210
234
 
235
+ const config = (args.config ?? {}) as Record<string, unknown>;
211
236
  const node: Call = {
212
237
  id: nodeId,
213
238
  op,
214
239
  bind,
215
- config: (args.config ?? {}) as Record<string, unknown>,
240
+ config,
216
241
  ...(args.guards && args.guards.length > 0 && { guards: args.guards }),
217
242
  };
218
243
  nodes.push(node);
219
244
 
220
- type Result = { readonly [K in keyof O & string]: OutputHandle<O[K]> };
221
- return new Proxy({} as Result, {
222
- get: (_target, prop): OutputHandle | undefined => {
245
+ const outputHandle = (portName: string): OutputHandle => ({
246
+ _tag: 'output',
247
+ nodeId,
248
+ portName,
249
+ });
250
+ return new Proxy({} as OutputHandles<O, F>, {
251
+ get: (
252
+ _target,
253
+ prop,
254
+ ): OutputHandle | readonly OutputHandle[] | undefined => {
223
255
  if (typeof prop !== 'string' || prop === 'then') return undefined;
224
256
  if (!(prop in outputs)) {
225
257
  throw new Error(
@@ -228,7 +260,12 @@ export const createFlowBuilderCore = <T extends InputSchema>(
228
260
  ).join(', ')}`,
229
261
  );
230
262
  }
231
- return { _tag: 'output', nodeId, portName: prop };
263
+ const countFrom = families?.[prop];
264
+ if (countFrom === undefined) return outputHandle(prop);
265
+ const size = familySize({ name: prop, countFrom }, config);
266
+ return Array.from({ length: size }, (_, index) =>
267
+ outputHandle(instancePortName(prop, index)),
268
+ );
232
269
  },
233
270
  });
234
271
  };
@@ -39,6 +39,73 @@ export interface OutputHandle<T extends OutputKind = OutputKind> {
39
39
  readonly __outputKind?: T;
40
40
  }
41
41
 
42
+ /**
43
+ * What an output family's sizing parameter accepts, given the config field the
44
+ * family counts from: an array of that field's elements, or a number.
45
+ */
46
+ export type FamilySource<Field> = Field extends readonly (infer E)[]
47
+ ? readonly E[]
48
+ : number;
49
+
50
+ /** Whether a number literal is a valid family count: a non-negative integer. */
51
+ type IsCountLiteral<N extends number> = `${N}` extends
52
+ `-${string}` | `${string}.${string}` | `${string}e${string}`
53
+ ? false
54
+ : true;
55
+
56
+ /**
57
+ * `N` resource handles as a tuple. A non-literal `number`, or a literal that is
58
+ * not a valid count, gives an open array; {@link FamilyCount} rejects the
59
+ * invalid literal at the call site.
60
+ */
61
+ type HandleTuple<
62
+ N extends number,
63
+ Acc extends readonly OutputHandle<'resource'>[] = readonly [],
64
+ > = number extends N
65
+ ? readonly OutputHandle<'resource'>[]
66
+ : IsCountLiteral<N> extends false
67
+ ? readonly OutputHandle<'resource'>[]
68
+ : Acc['length'] extends N
69
+ ? Acc
70
+ : HandleTuple<N, readonly [...Acc, OutputHandle<'resource'>]>;
71
+
72
+ /**
73
+ * The handles of an output family: one resource {@link OutputHandle} per
74
+ * element of the config array the family counts from, or as many as the config
75
+ * count. A literal array or count yields a tuple of that length, so indexing
76
+ * past the last instance is a type error.
77
+ */
78
+ export type OutputFamilyHandles<TCount extends readonly unknown[] | number> =
79
+ TCount extends readonly unknown[]
80
+ ? { readonly [I in keyof TCount]: OutputHandle<'resource'> }
81
+ : TCount extends number
82
+ ? HandleTuple<TCount>
83
+ : never;
84
+
85
+ /**
86
+ * Holds the config value an output family counts from to the op's own type. A
87
+ * runtime-sized array or count passes as is. A literal tuple must also satisfy
88
+ * `Shape`, so a tuple shorter than the op's `minItems` fails at the call site
89
+ * instead of at the server's bind step. A literal count must be a non-negative
90
+ * integer.
91
+ */
92
+ export type FamilyCount<
93
+ TCount extends readonly unknown[] | number,
94
+ Shape,
95
+ > = TCount extends readonly unknown[]
96
+ ? number extends TCount['length']
97
+ ? unknown
98
+ : TCount extends Shape
99
+ ? unknown
100
+ : { readonly 'output family config array has too few entries': never }
101
+ : TCount extends number
102
+ ? IsCountLiteral<TCount> extends true
103
+ ? unknown
104
+ : {
105
+ readonly 'output family count must be a non-negative integer': never;
106
+ }
107
+ : unknown;
108
+
42
109
  /** Discriminated union of all handle types that can be bound to operation arguments. */
43
110
  export type AnyHandle = InputHandle | OutputHandle;
44
111
 
@@ -18,8 +18,8 @@ const STEAKHOUSE_USDC = '0xBEEF01735c132Ada46AA9aA4c54623cAA92A64CB';
18
18
  * Split USDC 60/40 and zap each portion into a different vault.
19
19
  *
20
20
  * Demonstrates:
21
- * - core.split to divide a resource by basis-point ratio
22
- * - Threading split outputs into two independent lifi.zap nodes
21
+ * - core.partition to divide a resource into one part per basis-point share
22
+ * - Destructuring the typed `parts` tuple into two independent lifi.zap nodes
23
23
  * - Slippage guards on both zap outputs
24
24
  */
25
25
  export const buildSplitAndZapExample = (): {
@@ -35,15 +35,16 @@ export const buildSplitAndZapExample = (): {
35
35
  },
36
36
  });
37
37
 
38
- // Split USDC 60/40 — 6000 bps to `a`, remainder to `b`.
39
- const { a, b } = builder.core.split('split', {
38
+ // Partition USDC 60/40 — one part per `bps` entry. Every part but the last
39
+ // is rounded down; the last takes the remainder.
40
+ const [toAave, toMorpho] = builder.core.partition('split', {
40
41
  bind: { source: builder.inputs.amountIn },
41
- config: { bps: 6000 },
42
- });
42
+ config: { bps: [6000, 4000] },
43
+ }).parts;
43
44
 
44
45
  // Zap 60% into Aave v3 aEthUSDC.
45
46
  builder.lifi.zap('zap-aave', {
46
- bind: { amountIn: a },
47
+ bind: { amountIn: toAave },
47
48
  config: {
48
49
  resourceOut: resources.erc20(A_ETH_USDC, 1),
49
50
  },
@@ -52,7 +53,7 @@ export const buildSplitAndZapExample = (): {
52
53
 
53
54
  // Zap 40% into Steakhouse USDC (Morpho).
54
55
  builder.lifi.zap('zap-morpho', {
55
- bind: { amountIn: b },
56
+ bind: { amountIn: toMorpho },
56
57
  config: {
57
58
  resourceOut: resources.erc20(STEAKHOUSE_USDC, 1),
58
59
  },