@lifi/composer-sdk 0.4.1 → 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 (92) hide show
  1. package/README.md +6 -3
  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 +3 -0
  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 +2 -0
  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 +70 -36
  25. package/dist/client.cjs.map +1 -1
  26. package/dist/client.d.cts +33 -4
  27. package/dist/client.d.ts +33 -4
  28. package/dist/client.js +70 -36
  29. package/dist/client.js.map +1 -1
  30. package/dist/discovery.cjs.map +1 -1
  31. package/dist/discovery.d.cts +18 -2
  32. package/dist/discovery.d.ts +18 -2
  33. package/dist/generated/config.generated.cjs.map +1 -1
  34. package/dist/generated/config.generated.d.cts +21 -1
  35. package/dist/generated/config.generated.d.ts +21 -1
  36. package/dist/generated/operations.generated.cjs +6 -0
  37. package/dist/generated/operations.generated.cjs.map +1 -1
  38. package/dist/generated/operations.generated.d.cts +18 -2
  39. package/dist/generated/operations.generated.d.ts +18 -2
  40. package/dist/generated/operations.generated.js +6 -0
  41. package/dist/generated/operations.generated.js.map +1 -1
  42. package/dist/index.cjs.map +1 -1
  43. package/dist/index.d.cts +6 -6
  44. package/dist/index.d.ts +6 -6
  45. package/dist/index.js.map +1 -1
  46. package/dist/{raw-IxCF_bSS.d.ts → raw-BvRghN3r.d.ts} +3 -2
  47. package/dist/{raw-Dj_2rBSQ.d.cts → raw-s1rer8X3.d.cts} +3 -2
  48. package/dist/raw.cjs +2 -0
  49. package/dist/raw.cjs.map +1 -1
  50. package/dist/raw.d.cts +1 -1
  51. package/dist/raw.d.ts +1 -1
  52. package/dist/raw.js +2 -1
  53. package/dist/raw.js.map +1 -1
  54. package/dist/run/inputs.cjs +14 -15
  55. package/dist/run/inputs.cjs.map +1 -1
  56. package/dist/run/inputs.d.cts +8 -0
  57. package/dist/run/inputs.d.ts +8 -0
  58. package/dist/run/inputs.js +14 -15
  59. package/dist/run/inputs.js.map +1 -1
  60. package/dist/run/simulate.cjs +5 -0
  61. package/dist/run/simulate.cjs.map +1 -1
  62. package/dist/run/simulate.d.cts +5 -2
  63. package/dist/run/simulate.d.ts +5 -2
  64. package/dist/run/simulate.js +5 -0
  65. package/dist/run/simulate.js.map +1 -1
  66. package/dist/sdk.cjs +2 -0
  67. package/dist/sdk.cjs.map +1 -1
  68. package/dist/sdk.d.cts +28 -1
  69. package/dist/sdk.d.ts +28 -1
  70. package/dist/sdk.js +2 -0
  71. package/dist/sdk.js.map +1 -1
  72. package/dist/types.cjs.map +1 -1
  73. package/dist/types.d.cts +18 -2
  74. package/dist/types.d.ts +18 -2
  75. package/package.json +3 -10
  76. package/src/authoring/FlowBuilderCore.ts +13 -15
  77. package/src/authoring/handles.ts +41 -5
  78. package/src/authoring/raw.ts +36 -2
  79. package/src/authoring/signatureArgs.ts +17 -13
  80. package/src/client.ts +133 -39
  81. package/src/discovery.ts +18 -1
  82. package/src/examples/flySwap.ts +167 -0
  83. package/src/examples/run.ts +29 -4
  84. package/src/examples/supportedChains.ts +122 -0
  85. package/src/generated/config.generated.ts +24 -0
  86. package/src/generated/operations.generated.ts +26 -0
  87. package/src/index.ts +10 -1
  88. package/src/raw.ts +1 -1
  89. package/src/run/inputs.ts +30 -11
  90. package/src/run/simulate.ts +14 -2
  91. package/src/sdk.ts +30 -0
  92. package/src/types.ts +20 -1
@@ -1,9 +1,9 @@
1
1
  import type {
2
2
  AppliedGuard,
3
+ BindValue,
3
4
  Call,
4
5
  Flow,
5
6
  FlowInput,
6
- Ref,
7
7
  Resource,
8
8
  SolType,
9
9
  } from '@lifi/compose-spec';
@@ -13,6 +13,7 @@ import type {
13
13
  InputSchema,
14
14
  OutputKind,
15
15
  TypedGuard,
16
+ TypedLiteral,
16
17
  TypedRef,
17
18
  } from '../types.js';
18
19
 
@@ -21,7 +22,7 @@ import type {
21
22
  OutputHandle,
22
23
  ResourceInputHandle,
23
24
  } from './handles.js';
24
- import { handleToRef } from './handles.js';
25
+ import { toBindValue } from './handles.js';
25
26
  import { ref } from './raw.js';
26
27
 
27
28
  /**
@@ -37,17 +38,14 @@ export interface CallArgs {
37
38
  }
38
39
 
39
40
  /**
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.
41
+ * Any value that can be bound to an operation parameter: an input handle, an
42
+ * output handle from a previous operation, a typed `$ref` pointer, or a typed
43
+ * literal.
42
44
  *
43
- * Use `raw.ref<T>()` to create a typed ref from a raw path string.
45
+ * Use `raw.ref<T>()` to create a typed ref from a raw path string, and
46
+ * `raw.literal<T>()` to bake in a constant.
44
47
  */
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
- };
48
+ export type AnyBindable = InputHandle | OutputHandle | TypedRef | TypedLiteral;
51
49
 
52
50
  /**
53
51
  * Provides references to runtime context values available inside a flow.
@@ -127,7 +125,7 @@ export interface FlowBuilderCore<T extends InputSchema = InputSchema> {
127
125
  id: string,
128
126
  op: string,
129
127
  args: {
130
- readonly bind: Record<string, Ref>;
128
+ readonly bind: Record<string, BindValue>;
131
129
  readonly config: Record<string, unknown>;
132
130
  readonly guards?: readonly AppliedGuard[];
133
131
  },
@@ -196,9 +194,9 @@ export const createFlowBuilderCore = <T extends InputSchema>(
196
194
  if (nodeIds.has(nodeId)) throw new Error(`Duplicate node id: "${nodeId}"`);
197
195
  nodeIds.add(nodeId);
198
196
 
199
- const bind: Record<string, Ref> = {};
197
+ const bind: Record<string, BindValue> = {};
200
198
  for (const [key, val] of Object.entries(args.bind)) {
201
- bind[key] = toRef(val);
199
+ bind[key] = toBindValue(val);
202
200
  }
203
201
 
204
202
  const node: Call = {
@@ -230,7 +228,7 @@ export const createFlowBuilderCore = <T extends InputSchema>(
230
228
  nodeId: string,
231
229
  op: string,
232
230
  args: {
233
- readonly bind: Record<string, Ref>;
231
+ readonly bind: Record<string, BindValue>;
234
232
  readonly config: Record<string, unknown>;
235
233
  readonly guards?: readonly AppliedGuard[];
236
234
  },
@@ -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.
@@ -39,6 +39,40 @@ export const ref = <T extends OutputKind>(path: string): TypedRef<T> => ({
39
39
  $ref: path,
40
40
  });
41
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 });
75
+
42
76
  /**
43
77
  * Creates a raw guard object for use in the `guards` array of an operation call.
44
78
  *
@@ -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
@@ -9,7 +9,11 @@ import type {
9
9
  SimulateResult,
10
10
  } from '@lifi/compose-spec';
11
11
 
12
- import type { GetZapPacksOptions, ZapPackOverview } from './discovery.js';
12
+ import type {
13
+ ChainSummary,
14
+ GetZapPacksOptions,
15
+ ZapPackOverview,
16
+ } from './discovery.js';
13
17
  import { ComposeError, errorFromHttpResponse } from './errors.js';
14
18
  import {
15
19
  parseCompilePartialEnvelope,
@@ -55,6 +59,28 @@ export interface ComposeClient {
55
59
  * @throws {@link ComposeError} on network, validation, or server errors.
56
60
  */
57
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[]>;
58
84
  /**
59
85
  * Submits a compile request and returns the result.
60
86
  *
@@ -75,9 +101,9 @@ export interface ComposeClient {
75
101
  * `POST /compose/route`.
76
102
  *
77
103
  * A convenience path that skips flow authoring: the server builds a one-node
78
- * `lifi.zap` flow with a `directDeposit` input from the `fromToken` /
79
- * `toToken` pair and runs it through the same pipeline as {@link compile}.
80
- * The response is therefore the same discriminated result — HTTP 206 under
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
81
107
  * `simulationPolicy: 'allow-revert'` yields `status: 'partial'` rather than
82
108
  * throwing, exactly as it does for {@link compile}.
83
109
  *
@@ -88,6 +114,13 @@ export interface ComposeClient {
88
114
  * author richer flows later, so the single-step limit is the current state of
89
115
  * the server, not the shape of this contract.
90
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
+ *
91
124
  * Use {@link compile} for what the server does not author for you: several
92
125
  * chained operations, splits, explicit preconditions, or per-op guards.
93
126
  *
@@ -149,6 +182,67 @@ const bigintReplacer = (_key: string, value: unknown): unknown =>
149
182
  const isNonNullObject = (v: unknown): v is Record<string, unknown> =>
150
183
  typeof v === 'object' && v !== null;
151
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
+
152
246
  const parseBody = async <T>(res: Response, url: string): Promise<T> => {
153
247
  const body = await res.json().catch((_) => null);
154
248
  if (!isNonNullObject(body) || !('data' in body)) {
@@ -164,6 +258,7 @@ const parseCompileSuccessBody = async (
164
258
  url: string,
165
259
  ): Promise<ComposeCompileResult> => {
166
260
  const data = await parseBody<ComposeCompileSuccessData>(res, url);
261
+ decodeSimulatedAmounts(data, url);
167
262
  return { ...data, status: 'success' as const };
168
263
  };
169
264
 
@@ -184,6 +279,7 @@ const parsePartialBody = async (
184
279
  // shape as a hand-authored type, so we narrow it here rather than re-declaring
185
280
  // that type as a schema. `error` is fully validated — no cast needed.
186
281
  const data = envelope.data as unknown as ComposeCompilePartialData;
282
+ decodeSimulatedAmounts(data, url);
187
283
  return { ...data, status: 'partial' as const, error: envelope.error };
188
284
  };
189
285
 
@@ -240,6 +336,29 @@ const postCompile = async (
240
336
  return await parseCompileSuccessBody(res, url);
241
337
  };
242
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
+ };
361
+
243
362
  /**
244
363
  * Creates a low-level Compose API client.
245
364
  *
@@ -272,24 +391,11 @@ export const createComposeClient = (
272
391
  };
273
392
  const transport: ComposeTransport = { fetchFn, baseHeaders };
274
393
 
275
- const getManifest = async (): Promise<ComposeManifest> => {
276
- const url = `${base}/compose/manifest`;
277
- let res: Response;
278
- try {
279
- res = await fetchFn(url, {
280
- method: 'GET',
281
- headers: { ...baseHeaders },
282
- });
283
- } catch (err) {
284
- const message = err instanceof Error ? err.message : String(err);
285
- throw new ComposeError('NETWORK_ERROR', message, { cause: err });
286
- }
287
- if (!res.ok) {
288
- const body = await res.text();
289
- throw errorFromHttpResponse(res.status, body, url);
290
- }
291
- return await parseBody<ComposeManifest>(res, url);
292
- };
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`);
293
399
 
294
400
  const compile = async (
295
401
  request: ComposeCompileRequest,
@@ -312,22 +418,10 @@ export const createComposeClient = (
312
418
  params.set('protocols', list);
313
419
  }
314
420
  const qs = params.toString();
315
- const url = `${base}/compose/zap-packs${qs ? `?${qs}` : ''}`;
316
- let res: Response;
317
- try {
318
- res = await fetchFn(url, {
319
- method: 'GET',
320
- headers: { ...baseHeaders },
321
- });
322
- } catch (err) {
323
- const message = err instanceof Error ? err.message : String(err);
324
- throw new ComposeError('NETWORK_ERROR', message, { cause: err });
325
- }
326
- if (!res.ok) {
327
- const body = await res.text();
328
- throw errorFromHttpResponse(res.status, body, url);
329
- }
330
- return await parseBody<readonly ZapPackOverview[]>(res, url);
421
+ return getJson<readonly ZapPackOverview[]>(
422
+ transport,
423
+ `${base}/compose/zap-packs${qs ? `?${qs}` : ''}`,
424
+ );
331
425
  };
332
426
 
333
427
  const simulate = async (
@@ -358,5 +452,5 @@ export const createComposeClient = (
358
452
  throw errorFromHttpResponse(res.status, body, url);
359
453
  };
360
454
 
361
- return { getManifest, compile, route, getZapPacks, simulate };
455
+ return { getManifest, getChains, compile, route, getZapPacks, simulate };
362
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.