@lifi/composer-sdk 0.4.0 → 0.5.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 (106) hide show
  1. package/README.md +98 -5
  2. package/dist/authoring/FlowBuilderCore.cjs +1 -5
  3. package/dist/authoring/FlowBuilderCore.cjs.map +1 -1
  4. package/dist/authoring/FlowBuilderCore.d.cts +9 -7
  5. package/dist/authoring/FlowBuilderCore.d.ts +9 -7
  6. package/dist/authoring/FlowBuilderCore.js +2 -6
  7. package/dist/authoring/FlowBuilderCore.js.map +1 -1
  8. package/dist/authoring/handles.cjs +5 -2
  9. package/dist/authoring/handles.cjs.map +1 -1
  10. package/dist/authoring/handles.d.cts +25 -6
  11. package/dist/authoring/handles.d.ts +25 -6
  12. package/dist/authoring/handles.js +3 -1
  13. package/dist/authoring/handles.js.map +1 -1
  14. package/dist/authoring/raw.cjs +6 -1
  15. package/dist/authoring/raw.cjs.map +1 -1
  16. package/dist/authoring/raw.d.cts +33 -3
  17. package/dist/authoring/raw.d.ts +33 -3
  18. package/dist/authoring/raw.js +5 -1
  19. package/dist/authoring/raw.js.map +1 -1
  20. package/dist/authoring/signatureArgs.cjs.map +1 -1
  21. package/dist/authoring/signatureArgs.d.cts +16 -12
  22. package/dist/authoring/signatureArgs.d.ts +16 -12
  23. package/dist/authoring/signatureArgs.js.map +1 -1
  24. package/dist/client.cjs +94 -58
  25. package/dist/client.cjs.map +1 -1
  26. package/dist/client.d.cts +61 -2
  27. package/dist/client.d.ts +61 -2
  28. package/dist/client.js +94 -58
  29. package/dist/client.js.map +1 -1
  30. package/dist/discovery.cjs.map +1 -1
  31. package/dist/discovery.d.cts +31 -2
  32. package/dist/discovery.d.ts +31 -2
  33. package/dist/generated/config.generated.cjs.map +1 -1
  34. package/dist/generated/config.generated.d.cts +35 -3
  35. package/dist/generated/config.generated.d.ts +35 -3
  36. package/dist/generated/operations.generated.cjs +9 -0
  37. package/dist/generated/operations.generated.cjs.map +1 -1
  38. package/dist/generated/operations.generated.d.cts +33 -2
  39. package/dist/generated/operations.generated.d.ts +33 -2
  40. package/dist/generated/operations.generated.js +9 -0
  41. package/dist/generated/operations.generated.js.map +1 -1
  42. package/dist/index.cjs +5 -1
  43. package/dist/index.cjs.map +1 -1
  44. package/dist/index.d.cts +26 -7
  45. package/dist/index.d.ts +26 -7
  46. package/dist/index.js +4 -1
  47. package/dist/index.js.map +1 -1
  48. package/dist/{raw-IxCF_bSS.d.ts → raw-BvRghN3r.d.ts} +3 -2
  49. package/dist/{raw-Dj_2rBSQ.d.cts → raw-s1rer8X3.d.cts} +3 -2
  50. package/dist/raw.cjs +2 -0
  51. package/dist/raw.cjs.map +1 -1
  52. package/dist/raw.d.cts +1 -1
  53. package/dist/raw.d.ts +1 -1
  54. package/dist/raw.js +2 -1
  55. package/dist/raw.js.map +1 -1
  56. package/dist/routeAmount-DdbJYJyM.d.cts +55 -0
  57. package/dist/routeAmount-DdbJYJyM.d.ts +55 -0
  58. package/dist/routeAmount.cjs +38 -0
  59. package/dist/routeAmount.cjs.map +1 -0
  60. package/dist/routeAmount.d.cts +2 -0
  61. package/dist/routeAmount.d.ts +2 -0
  62. package/dist/routeAmount.js +13 -0
  63. package/dist/routeAmount.js.map +1 -0
  64. package/dist/run/inputs.cjs +14 -15
  65. package/dist/run/inputs.cjs.map +1 -1
  66. package/dist/run/inputs.d.cts +8 -0
  67. package/dist/run/inputs.d.ts +8 -0
  68. package/dist/run/inputs.js +14 -15
  69. package/dist/run/inputs.js.map +1 -1
  70. package/dist/run/simulate.cjs +5 -0
  71. package/dist/run/simulate.cjs.map +1 -1
  72. package/dist/run/simulate.d.cts +5 -2
  73. package/dist/run/simulate.d.ts +5 -2
  74. package/dist/run/simulate.js +5 -0
  75. package/dist/run/simulate.js.map +1 -1
  76. package/dist/sdk.cjs +3 -0
  77. package/dist/sdk.cjs.map +1 -1
  78. package/dist/sdk.d.cts +42 -2
  79. package/dist/sdk.d.ts +42 -2
  80. package/dist/sdk.js +3 -0
  81. package/dist/sdk.js.map +1 -1
  82. package/dist/types.cjs.map +1 -1
  83. package/dist/types.d.cts +18 -2
  84. package/dist/types.d.ts +18 -2
  85. package/package.json +6 -13
  86. package/src/authoring/FlowBuilderCore.ts +13 -15
  87. package/src/authoring/handles.ts +41 -5
  88. package/src/authoring/raw.ts +39 -4
  89. package/src/authoring/signatureArgs.ts +17 -13
  90. package/src/client.ts +204 -58
  91. package/src/discovery.ts +31 -1
  92. package/src/examples/flySwap.ts +167 -0
  93. package/src/examples/lifiZap.ts +4 -0
  94. package/src/examples/lifiZapAsync.ts +88 -0
  95. package/src/examples/routeTokenPair.ts +58 -0
  96. package/src/examples/run.ts +56 -6
  97. package/src/examples/supportedChains.ts +122 -0
  98. package/src/generated/config.generated.ts +45 -3
  99. package/src/generated/operations.generated.ts +56 -8
  100. package/src/index.ts +33 -1
  101. package/src/raw.ts +1 -1
  102. package/src/routeAmount.ts +61 -0
  103. package/src/run/inputs.ts +30 -11
  104. package/src/run/simulate.ts +14 -2
  105. package/src/sdk.ts +47 -0
  106. package/src/types.ts +20 -1
@@ -1,6 +1,11 @@
1
- import type { Resource, StaticSolType } from '@lifi/compose-spec';
1
+ import type {
2
+ BindValue,
3
+ Resource,
4
+ SolType,
5
+ StaticSolType,
6
+ } from '@lifi/compose-spec';
2
7
 
3
- import type { OutputKind, TypedRef } from '../types.js';
8
+ import type { OutputKind, TypedLiteral, TypedRef } from '../types.js';
4
9
 
5
10
  /**
6
11
  * A handle referencing a flow input declared in the flow's input schema.
@@ -75,16 +80,47 @@ type SlotAssignable<T extends OutputKind> = T extends 'bytes32'
75
80
  : T;
76
81
 
77
82
  /**
78
- * The set of values accepted by a typed bind slot.
83
+ * The values accepted by a typed slot that must resolve to a `$ref`:
79
84
  * - `OutputHandle<T>` / `InputHandle<T>` (same type)
80
85
  * - For `'uint256'` slots, also `'resource'`-tagged handles
81
86
  * - For `'bytes32'` slots, any static-32-byte handle (and `'resource'`)
82
87
  * - `TypedRef<T>` (typed context refs, subject to `SlotAssignable`)
83
88
  *
84
- * Plain `{ $ref: '...' }` objects are **not** accepted — use `raw.ref<T>()`
89
+ * Plain `{ $ref: '...' }` objects are **not** accepted - use `raw.ref<T>()`
85
90
  * to create a typed ref when you need to reference a raw path.
86
91
  */
87
- export type Bindable<T extends OutputKind> =
92
+ export type RefBindable<T extends OutputKind> =
88
93
  | OutputHandle<SlotAssignable<T>>
89
94
  | InputHandle<SlotAssignable<T>>
90
95
  | TypedRef<SlotAssignable<T>>;
96
+
97
+ /**
98
+ * Any ref-resolving bindable, with no per-slot type constraint.
99
+ */
100
+ export type AnyRefBindable = InputHandle | OutputHandle | TypedRef;
101
+
102
+ /**
103
+ * The set of values accepted by a typed operation bind slot: everything
104
+ * {@link RefBindable} accepts, plus a {@link TypedLiteral} constant baked
105
+ * into the flow document. A `'resource'` slot rejects a literal: a token
106
+ * resource is not a scalar constant.
107
+ *
108
+ * Use `raw.literal<T>()` to create one.
109
+ */
110
+ export type Bindable<T extends OutputKind> =
111
+ RefBindable<T> | LiteralBindable<SlotAssignable<T>>;
112
+
113
+ // Distributes over the slot's assignable kinds and keeps the scalar ones: a
114
+ // `'resource'` slot has no scalar arm, so its literal arm collapses to
115
+ // `never` and a literal is rejected outright.
116
+ type LiteralBindable<K extends OutputKind> = K extends SolType
117
+ ? TypedLiteral<K>
118
+ : never;
119
+
120
+ /**
121
+ * Converts a bindable to the wire `bind` value it denotes: handles become
122
+ * `$ref` pointers, refs and literals pass through as-is.
123
+ */
124
+ export const toBindValue = (
125
+ bindable: InputHandle | OutputHandle | TypedRef | TypedLiteral,
126
+ ): BindValue => ('_tag' in bindable ? handleToRef(bindable) : bindable);
@@ -1,6 +1,6 @@
1
- import type { AppliedGuard } from '@lifi/compose-spec';
1
+ import type { AppliedGuard, SolType } from '@lifi/compose-spec';
2
2
 
3
- import type { OutputKind, TypedRef } from '../types.js';
3
+ import type { OutputKind, TypedLiteral, TypedRef } from '../types.js';
4
4
 
5
5
  /**
6
6
  * Creates a typed `$ref` pointer for use in bind slots.
@@ -35,8 +35,43 @@ import type { OutputKind, TypedRef } from '../types.js';
35
35
  * });
36
36
  * ```
37
37
  */
38
- export const ref = <T extends OutputKind>(path: string): TypedRef<T> =>
39
- ({ $ref: path }) as TypedRef<T>;
38
+ export const ref = <T extends OutputKind>(path: string): TypedRef<T> => ({
39
+ $ref: path,
40
+ });
41
+
42
+ /**
43
+ * Creates a typed literal for use in bind slots: a constant baked into the
44
+ * flow document rather than supplied at run time.
45
+ *
46
+ * Prefer a declared flow input and `builder.inputs.<name>` — a value the
47
+ * caller supplies per run stays out of the document. Use a literal when the
48
+ * flow has no run of its own to take the value from: the `continuationFlow`
49
+ * of a `continuation.settle` node is stored by the settlement backend and
50
+ * re-run later against the deferred proceeds, so its delivery address has to
51
+ * be part of the committed document.
52
+ *
53
+ * @typeParam T - The Solidity type of the literal (e.g. `'address'`).
54
+ * @param kind - The literal's Solidity type.
55
+ * @param value - The literal value, as the wire format carries it: a string.
56
+ * @returns A {@link TypedLiteral} accepted by `Bindable<T>` slots matching `T`.
57
+ *
58
+ * @example
59
+ * ```ts
60
+ * import { raw } from '@lifi/composer-sdk';
61
+ *
62
+ * builder.core.transfer('deliver', {
63
+ * bind: {
64
+ * amount: zap.amountOut,
65
+ * recipient: raw.literal('address', '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'),
66
+ * },
67
+ * config: {},
68
+ * });
69
+ * ```
70
+ */
71
+ export const literal = <T extends SolType>(
72
+ kind: T,
73
+ value: string,
74
+ ): TypedLiteral<T> => ({ kind, value });
40
75
 
41
76
  /**
42
77
  * Creates a raw guard object for use in the `guards` array of an operation call.
@@ -3,32 +3,36 @@ import { type ParseAbiItem, parseAbiItem } from 'abitype';
3
3
 
4
4
  import type { AbiTypeToOutputKind, TypedGuard } from '../types.js';
5
5
 
6
- import type { AnyBindable, CallArgs } from './FlowBuilderCore.js';
7
- import type { Bindable, OutputHandle } from './handles.js';
6
+ import type { CallArgs } from './FlowBuilderCore.js';
7
+ import type { AnyRefBindable, OutputHandle, RefBindable } from './handles.js';
8
8
  import { handleToRef } from './handles.js';
9
9
 
10
10
  /**
11
11
  * Type-level extraction of named bind keys from a function signature string.
12
12
  * When `TSig` is a string literal matching a Solidity function signature,
13
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
14
+ * `RefBindable<kind>` where the kind is derived from each parameter's Solidity
15
15
  * type via `AbiTypeToOutputKind`. For recognised `SolType` parameters
16
16
  * (e.g. `uint256`, `address`, `uint128`) the bind slot is precisely typed;
17
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.
18
+ * Falls back to `Record<string, AnyRefBindable>` for non-literal strings.
19
+ *
20
+ * These slots are lowered into `config.args`, which is positional calldata
21
+ * rather than a node `bind` record, so a literal is not accepted here: only
22
+ * values that resolve to a `$ref`.
19
23
  */
20
24
  export type SignatureBind<TSig extends string> = string extends TSig
21
- ? Record<string, AnyBindable>
25
+ ? Record<string, AnyRefBindable>
22
26
  : ParseAbiItem<TSig> extends {
23
27
  type: 'function';
24
28
  inputs: infer I extends readonly { name: string; type: string }[];
25
29
  }
26
30
  ? {
27
- readonly [P in I[number] as P['name']]: Bindable<
31
+ readonly [P in I[number] as P['name']]: RefBindable<
28
32
  AbiTypeToOutputKind<P['type']>
29
33
  >;
30
34
  }
31
- : Record<string, AnyBindable>;
35
+ : Record<string, AnyRefBindable>;
32
36
 
33
37
  // ---------------------------------------------------------------------------
34
38
  // Return-type inference from function signatures
@@ -126,18 +130,18 @@ export const parseFunctionParams = (functionSignature: string): string[] => {
126
130
  };
127
131
 
128
132
  /**
129
- * Converts any `AnyBindable` to a `Ref` (`{ $ref: string }`).
133
+ * Converts a ref-resolving bindable to a `Ref` (`{ $ref: string }`).
130
134
  * Handles carry a `_tag` property and are converted via `handleToRef`;
131
135
  * `Ref` objects pass through as-is.
132
136
  */
133
- export const toBindRef = (bindable: AnyBindable): Ref => {
137
+ export const toBindRef = (bindable: AnyRefBindable): Ref => {
134
138
  if ('_tag' in bindable) return handleToRef(bindable);
135
139
  return bindable;
136
140
  };
137
141
 
138
142
  export interface BuildCallWireFormatInput {
139
- readonly resource?: AnyBindable;
140
- readonly bind: Record<string, AnyBindable>;
143
+ readonly resource?: AnyRefBindable;
144
+ readonly bind: Record<string, AnyRefBindable>;
141
145
  readonly config: Record<string, unknown>;
142
146
  readonly guards?: readonly TypedGuard[];
143
147
  }
@@ -155,7 +159,7 @@ export interface CallWireResult {
155
159
  */
156
160
  const resolvePositionalArgs = (
157
161
  label: string,
158
- bind: Record<string, AnyBindable>,
162
+ bind: Record<string, AnyRefBindable>,
159
163
  config: Record<string, unknown>,
160
164
  ): Ref[] => {
161
165
  const functionSignature = config.functionSignature;
@@ -222,7 +226,7 @@ export const buildCallWireFormat = (
222
226
  };
223
227
 
224
228
  export interface BuildStaticCallWireFormatInput {
225
- readonly bind: Record<string, AnyBindable>;
229
+ readonly bind: Record<string, AnyRefBindable>;
226
230
  readonly config: Record<string, unknown>;
227
231
  readonly guards?: readonly TypedGuard[];
228
232
  }
package/src/client.ts CHANGED
@@ -4,11 +4,16 @@ import type {
4
4
  ComposeCompileResult,
5
5
  ComposeCompileSuccessData,
6
6
  ComposeManifest,
7
+ ComposeRouteRequest,
7
8
  SimulateRequest,
8
9
  SimulateResult,
9
10
  } from '@lifi/compose-spec';
10
11
 
11
- import type { GetZapPacksOptions, ZapPackOverview } from './discovery.js';
12
+ import type {
13
+ ChainSummary,
14
+ GetZapPacksOptions,
15
+ ZapPackOverview,
16
+ } from './discovery.js';
12
17
  import { ComposeError, errorFromHttpResponse } from './errors.js';
13
18
  import {
14
19
  parseCompilePartialEnvelope,
@@ -54,6 +59,28 @@ export interface ComposeClient {
54
59
  * @throws {@link ComposeError} on network, validation, or server errors.
55
60
  */
56
61
  readonly getManifest: () => Promise<ComposeManifest>;
62
+ /**
63
+ * Fetches the chains the Compose API supports, ascending by `chainId`.
64
+ *
65
+ * A chain is listed once the server confirms the VM contract is deployed on
66
+ * it — the same condition {@link compile}, {@link route} and
67
+ * {@link simulate} validate against. A chain absent from this list is
68
+ * rejected outright by those endpoints.
69
+ *
70
+ * The converse does not hold: a listed chain can still have no routing edge
71
+ * for a given token pair, which {@link route} reports as `no_route_error`.
72
+ * Use {@link getZapPacks} to enumerate the pairs that do route.
73
+ *
74
+ * Each entry carries a display `name` alongside its `chainId`. The name is a
75
+ * deployment-maintained label — render it, but key on `chainId`.
76
+ *
77
+ * The list tracks the server's chain registry, which refreshes periodically.
78
+ * Results are not cached by the SDK; callers should cache as appropriate.
79
+ *
80
+ * @returns The supported chains, ascending by `chainId`.
81
+ * @throws {@link ComposeError} on network or server errors.
82
+ */
83
+ readonly getChains: () => Promise<readonly ChainSummary[]>;
57
84
  /**
58
85
  * Submits a compile request and returns the result.
59
86
  *
@@ -69,6 +96,45 @@ export interface ComposeClient {
69
96
  readonly compile: (
70
97
  request: ComposeCompileRequest,
71
98
  ) => Promise<ComposeCompileResult>;
99
+ /**
100
+ * Compiles a token pair into a submit-ready transaction via
101
+ * `POST /compose/route`.
102
+ *
103
+ * A convenience path that skips flow authoring: the server builds a one-node
104
+ * zap flow with a `directDeposit` input from the `fromToken` / `toToken`
105
+ * pair and runs it through the same pipeline as {@link compile}. The
106
+ * response is therefore the same discriminated result — HTTP 206 under
107
+ * `simulationPolicy: 'allow-revert'` yields `status: 'partial'` rather than
108
+ * throwing, exactly as it does for {@link compile}.
109
+ *
110
+ * As of today that flow is a single zap step: one edge of the backend's
111
+ * routing catalog (a position entry or exit, a wrap or unwrap, a mint or
112
+ * burn). Enumerate the pairs it covers with {@link getZapPacks}; a pair with
113
+ * no edge is rejected with `kind: 'no_route_error'`. The endpoint is meant to
114
+ * author richer flows later, so the single-step limit is the current state of
115
+ * the server, not the shape of this contract.
116
+ *
117
+ * Only synchronous edges are considered unless the request sets
118
+ * `allowAsyncRoutes: true`, so by default every produced resource comes back
119
+ * `availability: 'now'` and a pair reachable only after settlement is a
120
+ * `no_route_error`. Opting in adds the `lifi.zapAsync` fallback, which still
121
+ * loses to a synchronous edge when the pair has one; read
122
+ * `producedResources[*].availability` to see which representation you got.
123
+ *
124
+ * Use {@link compile} for what the server does not author for you: several
125
+ * chained operations, splits, explicit preconditions, or per-op guards.
126
+ *
127
+ * `bigint` amounts in `amount` are serialised to decimal strings
128
+ * automatically.
129
+ *
130
+ * @param request - The from/to token pair, amount, signer, and route options.
131
+ * @returns A discriminated result: `status: 'success'` or `status: 'partial'`.
132
+ * @throws {@link ComposeError} on network, validation, or server errors —
133
+ * including `NOT_FOUND` (HTTP 404) when no catalog edge covers the pair.
134
+ */
135
+ readonly route: (
136
+ request: ComposeRouteRequest,
137
+ ) => Promise<ComposeCompileResult>;
72
138
  /**
73
139
  * Fetches the available routing edges grouped by protocol.
74
140
  *
@@ -116,6 +182,67 @@ const bigintReplacer = (_key: string, value: unknown): unknown =>
116
182
  const isNonNullObject = (v: unknown): v is Record<string, unknown> =>
117
183
  typeof v === 'object' && v !== null;
118
184
 
185
+ // Non-negative decimal integer string, as the server sends token amounts.
186
+ const INTEGER_STRING = /^\d+$/;
187
+
188
+ const decodeAmount = (
189
+ value: unknown,
190
+ key: string,
191
+ field: 'amountOut' | 'amountOutMin',
192
+ url: string,
193
+ ): bigint => {
194
+ if (typeof value !== 'string' || !INTEGER_STRING.test(value)) {
195
+ throw new ComposeError(
196
+ 'UNKNOWN_ERROR',
197
+ `Malformed compose response: producedResources.${key}.simulated.${field} is ${
198
+ value === undefined ? 'absent' : 'not an integer string'
199
+ }`,
200
+ { url },
201
+ );
202
+ }
203
+ return BigInt(value);
204
+ };
205
+
206
+ // `simulated.amountOut` / `.amountOutMin` are declared `bigint` on
207
+ // `SimulatedAmounts` (the domain truth: `compose-core` produces real bigints)
208
+ // but arrive as decimal strings on the wire (the backend stringifies them).
209
+ // Decode at this seam so a consumer reading the declared type gets the value
210
+ // the type promises. A resource without `simulated` is left untouched; a
211
+ // `simulated` claiming a missing or non-numeric amount is a contract violation
212
+ // and surfaces as a `ComposeError` rather than `NaN` / `BigInt(undefined)`.
213
+ const decodeSimulatedAmounts = (data: unknown, url: string): void => {
214
+ if (!isNonNullObject(data)) return;
215
+ const resources = data.producedResources;
216
+ if (!isNonNullObject(resources)) return;
217
+ for (const key of Object.keys(resources)) {
218
+ const resource = resources[key];
219
+ if (!isNonNullObject(resource)) continue;
220
+ const simulated = resource.simulated;
221
+ if (simulated === undefined) continue;
222
+ if (!isNonNullObject(simulated)) {
223
+ throw new ComposeError(
224
+ 'UNKNOWN_ERROR',
225
+ `Malformed compose response: producedResources.${key}.simulated is not an object`,
226
+ { url },
227
+ );
228
+ }
229
+ simulated.amountOut = decodeAmount(
230
+ simulated.amountOut,
231
+ key,
232
+ 'amountOut',
233
+ url,
234
+ );
235
+ if (simulated.amountOutMin !== undefined) {
236
+ simulated.amountOutMin = decodeAmount(
237
+ simulated.amountOutMin,
238
+ key,
239
+ 'amountOutMin',
240
+ url,
241
+ );
242
+ }
243
+ }
244
+ };
245
+
119
246
  const parseBody = async <T>(res: Response, url: string): Promise<T> => {
120
247
  const body = await res.json().catch((_) => null);
121
248
  if (!isNonNullObject(body) || !('data' in body)) {
@@ -131,6 +258,7 @@ const parseCompileSuccessBody = async (
131
258
  url: string,
132
259
  ): Promise<ComposeCompileResult> => {
133
260
  const data = await parseBody<ComposeCompileSuccessData>(res, url);
261
+ decodeSimulatedAmounts(data, url);
134
262
  return { ...data, status: 'success' as const };
135
263
  };
136
264
 
@@ -151,6 +279,7 @@ const parsePartialBody = async (
151
279
  // shape as a hand-authored type, so we narrow it here rather than re-declaring
152
280
  // that type as a schema. `error` is fully validated — no cast needed.
153
281
  const data = envelope.data as unknown as ComposeCompilePartialData;
282
+ decodeSimulatedAmounts(data, url);
154
283
  return { ...data, status: 'partial' as const, error: envelope.error };
155
284
  };
156
285
 
@@ -173,6 +302,62 @@ const parseSimulateBody = async (
173
302
  }
174
303
  return result;
175
304
  };
305
+ // `POST /compose` and `POST /compose/route` differ only in path and request
306
+ // shape: both return the enveloped success data on 200 and the partial
307
+ // envelope on 206, so they share one transport.
308
+ type ComposeTransport = {
309
+ readonly fetchFn: typeof fetch;
310
+ readonly baseHeaders: Record<string, string>;
311
+ };
312
+
313
+ const postCompile = async (
314
+ { fetchFn, baseHeaders }: ComposeTransport,
315
+ url: string,
316
+ request: ComposeCompileRequest | ComposeRouteRequest,
317
+ ): Promise<ComposeCompileResult> => {
318
+ let res: Response;
319
+ try {
320
+ res = await fetchFn(url, {
321
+ method: 'POST',
322
+ headers: { ...baseHeaders, 'Content-Type': 'application/json' },
323
+ body: JSON.stringify(request, bigintReplacer),
324
+ });
325
+ } catch (err) {
326
+ const message = err instanceof Error ? err.message : String(err);
327
+ throw new ComposeError('NETWORK_ERROR', message, { cause: err });
328
+ }
329
+ if (res.status === 206) {
330
+ return await parsePartialBody(res, url);
331
+ }
332
+ if (!res.ok) {
333
+ const body = await res.text();
334
+ throw errorFromHttpResponse(res.status, body, url);
335
+ }
336
+ return await parseCompileSuccessBody(res, url);
337
+ };
338
+
339
+ // Every enveloped GET on this API behaves identically: no body, no query
340
+ // semantics of its own, `{ data }` on success, an error envelope otherwise.
341
+ const getJson = async <T>(
342
+ { fetchFn, baseHeaders }: ComposeTransport,
343
+ url: string,
344
+ ): Promise<T> => {
345
+ let res: Response;
346
+ try {
347
+ res = await fetchFn(url, {
348
+ method: 'GET',
349
+ headers: { ...baseHeaders },
350
+ });
351
+ } catch (err) {
352
+ const message = err instanceof Error ? err.message : String(err);
353
+ throw new ComposeError('NETWORK_ERROR', message, { cause: err });
354
+ }
355
+ if (!res.ok) {
356
+ const body = await res.text();
357
+ throw errorFromHttpResponse(res.status, body, url);
358
+ }
359
+ return await parseBody<T>(res, url);
360
+ };
176
361
 
177
362
  /**
178
363
  * Creates a low-level Compose API client.
@@ -204,50 +389,23 @@ export const createComposeClient = (
204
389
  'x-lifi-composer-sdk': SDK_VERSION,
205
390
  'x-lifi-api-key': trimmedApiKey,
206
391
  };
392
+ const transport: ComposeTransport = { fetchFn, baseHeaders };
207
393
 
208
- const getManifest = async (): Promise<ComposeManifest> => {
209
- const url = `${base}/compose/manifest`;
210
- let res: Response;
211
- try {
212
- res = await fetchFn(url, {
213
- method: 'GET',
214
- headers: { ...baseHeaders },
215
- });
216
- } catch (err) {
217
- const message = err instanceof Error ? err.message : String(err);
218
- throw new ComposeError('NETWORK_ERROR', message, { cause: err });
219
- }
220
- if (!res.ok) {
221
- const body = await res.text();
222
- throw errorFromHttpResponse(res.status, body, url);
223
- }
224
- return await parseBody<ComposeManifest>(res, url);
225
- };
394
+ const getManifest = async (): Promise<ComposeManifest> =>
395
+ getJson<ComposeManifest>(transport, `${base}/compose/manifest`);
396
+
397
+ const getChains = async (): Promise<readonly ChainSummary[]> =>
398
+ getJson<readonly ChainSummary[]>(transport, `${base}/chains`);
226
399
 
227
400
  const compile = async (
228
401
  request: ComposeCompileRequest,
229
- ): Promise<ComposeCompileResult> => {
230
- const url = `${base}/compose`;
231
- let res: Response;
232
- try {
233
- res = await fetchFn(url, {
234
- method: 'POST',
235
- headers: { ...baseHeaders, 'Content-Type': 'application/json' },
236
- body: JSON.stringify(request, bigintReplacer),
237
- });
238
- } catch (err) {
239
- const message = err instanceof Error ? err.message : String(err);
240
- throw new ComposeError('NETWORK_ERROR', message, { cause: err });
241
- }
242
- if (res.status === 206) {
243
- return await parsePartialBody(res, url);
244
- }
245
- if (!res.ok) {
246
- const body = await res.text();
247
- throw errorFromHttpResponse(res.status, body, url);
248
- }
249
- return await parseCompileSuccessBody(res, url);
250
- };
402
+ ): Promise<ComposeCompileResult> =>
403
+ postCompile(transport, `${base}/compose`, request);
404
+
405
+ const route = async (
406
+ request: ComposeRouteRequest,
407
+ ): Promise<ComposeCompileResult> =>
408
+ postCompile(transport, `${base}/compose/route`, request);
251
409
 
252
410
  const getZapPacks = async (
253
411
  options?: GetZapPacksOptions,
@@ -260,22 +418,10 @@ export const createComposeClient = (
260
418
  params.set('protocols', list);
261
419
  }
262
420
  const qs = params.toString();
263
- const url = `${base}/compose/zap-packs${qs ? `?${qs}` : ''}`;
264
- let res: Response;
265
- try {
266
- res = await fetchFn(url, {
267
- method: 'GET',
268
- headers: { ...baseHeaders },
269
- });
270
- } catch (err) {
271
- const message = err instanceof Error ? err.message : String(err);
272
- throw new ComposeError('NETWORK_ERROR', message, { cause: err });
273
- }
274
- if (!res.ok) {
275
- const body = await res.text();
276
- throw errorFromHttpResponse(res.status, body, url);
277
- }
278
- return await parseBody<readonly ZapPackOverview[]>(res, url);
421
+ return getJson<readonly ZapPackOverview[]>(
422
+ transport,
423
+ `${base}/compose/zap-packs${qs ? `?${qs}` : ''}`,
424
+ );
279
425
  };
280
426
 
281
427
  const simulate = async (
@@ -306,5 +452,5 @@ export const createComposeClient = (
306
452
  throw errorFromHttpResponse(res.status, body, url);
307
453
  };
308
454
 
309
- return { getManifest, compile, getZapPacks, simulate };
455
+ return { getManifest, getChains, compile, route, getZapPacks, simulate };
310
456
  };
package/src/discovery.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Types for the Compose discovery API (`GET /compose/zap-packs`).
2
+ * Types for the Compose discovery API (`GET /compose/zap-packs`, `GET /chains`).
3
3
  *
4
4
  * These are SDK-facing presentation types — not part of the core compile
5
5
  * contract in compose-spec. The backend response is the source of truth;
@@ -11,6 +11,23 @@
11
11
  * `"enter-position"`, `"exit-position"`, `"wrap"`, `"unwrap"`, `"mint"`, `"burn"`.
12
12
  */
13
13
 
14
+ /**
15
+ * A chain the Compose deployment supports.
16
+ */
17
+ export interface ChainSummary {
18
+ /** EVM chain ID. The stable identifier — key on this. */
19
+ readonly chainId: number;
20
+ /**
21
+ * Human-readable label, suitable for display in a chain selector.
22
+ *
23
+ * Maintained by the deployment rather than derived from canonical chain
24
+ * branding, so it can differ from a chain's marketed name and can change
25
+ * without notice. Never treat it as an identifier, and never match on it.
26
+ * A chain the deployment has no configured label for reports `Chain <id>`.
27
+ */
28
+ readonly name: string;
29
+ }
30
+
14
31
  /**
15
32
  * A single routing edge representing a supported token-to-token path
16
33
  * through a DeFi protocol.
@@ -26,6 +43,19 @@ export interface ZapPackEdge {
26
43
  readonly inAmountMin?: string;
27
44
  /** Maximum input amount accepted (stringified integer), if constrained. */
28
45
  readonly inAmountMax?: string;
46
+ /**
47
+ * Recipient binding obligation. `"required"` means the zap must bind its
48
+ * `recipient` input. The output is externally owned, terminal, and unswept.
49
+ * The loose string type preserves forward compatibility with new values.
50
+ */
51
+ readonly recipient?: string;
52
+ /**
53
+ * Output availability. `"future"` means the output becomes available only
54
+ * after the initiating transaction settles; route via `lifi.zapAsync` with
55
+ * a bound recipient. Absent means the output is available synchronously.
56
+ * The loose string type preserves forward compatibility with new values.
57
+ */
58
+ readonly availability?: string;
29
59
  }
30
60
 
31
61
  /**