@lifi/composer-sdk 0.4.1 → 0.5.1

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 (108) 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 +112 -55
  25. package/dist/client.cjs.map +1 -1
  26. package/dist/client.d.cts +42 -5
  27. package/dist/client.d.ts +42 -5
  28. package/dist/client.js +116 -55
  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/errors.cjs +42 -3
  34. package/dist/errors.cjs.map +1 -1
  35. package/dist/errors.d.cts +67 -4
  36. package/dist/errors.d.ts +67 -4
  37. package/dist/errors.js +29 -2
  38. package/dist/errors.js.map +1 -1
  39. package/dist/generated/config.generated.cjs.map +1 -1
  40. package/dist/generated/config.generated.d.cts +21 -1
  41. package/dist/generated/config.generated.d.ts +21 -1
  42. package/dist/generated/operations.generated.cjs +6 -0
  43. package/dist/generated/operations.generated.cjs.map +1 -1
  44. package/dist/generated/operations.generated.d.cts +18 -2
  45. package/dist/generated/operations.generated.d.ts +18 -2
  46. package/dist/generated/operations.generated.js +6 -0
  47. package/dist/generated/operations.generated.js.map +1 -1
  48. package/dist/index.cjs +4 -0
  49. package/dist/index.cjs.map +1 -1
  50. package/dist/index.d.cts +7 -7
  51. package/dist/index.d.ts +7 -7
  52. package/dist/index.js +8 -1
  53. package/dist/index.js.map +1 -1
  54. package/dist/{raw-IxCF_bSS.d.ts → raw-BvRghN3r.d.ts} +3 -2
  55. package/dist/{raw-Dj_2rBSQ.d.cts → raw-s1rer8X3.d.cts} +3 -2
  56. package/dist/raw.cjs +2 -0
  57. package/dist/raw.cjs.map +1 -1
  58. package/dist/raw.d.cts +1 -1
  59. package/dist/raw.d.ts +1 -1
  60. package/dist/raw.js +2 -1
  61. package/dist/raw.js.map +1 -1
  62. package/dist/responseSchemas.cjs +3 -0
  63. package/dist/responseSchemas.cjs.map +1 -1
  64. package/dist/responseSchemas.d.cts +3 -0
  65. package/dist/responseSchemas.d.ts +3 -0
  66. package/dist/responseSchemas.js +3 -0
  67. package/dist/responseSchemas.js.map +1 -1
  68. package/dist/run/inputs.cjs +14 -15
  69. package/dist/run/inputs.cjs.map +1 -1
  70. package/dist/run/inputs.d.cts +8 -0
  71. package/dist/run/inputs.d.ts +8 -0
  72. package/dist/run/inputs.js +14 -15
  73. package/dist/run/inputs.js.map +1 -1
  74. package/dist/run/simulate.cjs +5 -0
  75. package/dist/run/simulate.cjs.map +1 -1
  76. package/dist/run/simulate.d.cts +5 -2
  77. package/dist/run/simulate.d.ts +5 -2
  78. package/dist/run/simulate.js +5 -0
  79. package/dist/run/simulate.js.map +1 -1
  80. package/dist/sdk.cjs +2 -0
  81. package/dist/sdk.cjs.map +1 -1
  82. package/dist/sdk.d.cts +28 -1
  83. package/dist/sdk.d.ts +28 -1
  84. package/dist/sdk.js +2 -0
  85. package/dist/sdk.js.map +1 -1
  86. package/dist/types.cjs.map +1 -1
  87. package/dist/types.d.cts +18 -2
  88. package/dist/types.d.ts +18 -2
  89. package/package.json +3 -10
  90. package/src/authoring/FlowBuilderCore.ts +13 -15
  91. package/src/authoring/handles.ts +41 -5
  92. package/src/authoring/raw.ts +36 -2
  93. package/src/authoring/signatureArgs.ts +17 -13
  94. package/src/client.ts +222 -62
  95. package/src/discovery.ts +18 -1
  96. package/src/errors.ts +116 -3
  97. package/src/examples/flySwap.ts +167 -0
  98. package/src/examples/run.ts +35 -13
  99. package/src/examples/supportedChains.ts +122 -0
  100. package/src/generated/config.generated.ts +24 -0
  101. package/src/generated/operations.generated.ts +26 -0
  102. package/src/index.ts +18 -2
  103. package/src/raw.ts +1 -1
  104. package/src/responseSchemas.ts +6 -0
  105. package/src/run/inputs.ts +30 -11
  106. package/src/run/simulate.ts +14 -2
  107. package/src/sdk.ts +30 -0
  108. package/src/types.ts +20 -1
package/src/client.ts CHANGED
@@ -8,8 +8,18 @@ import type {
8
8
  SimulateRequest,
9
9
  SimulateResult,
10
10
  } from '@lifi/compose-spec';
11
+ import {
12
+ checkCompatibility,
13
+ COMPOSE_VERSION_HEADER,
14
+ COMPOSER_SDK_VERSION_HEADER,
15
+ parseVersion,
16
+ } from '@lifi/compose-spec';
11
17
 
12
- import type { GetZapPacksOptions, ZapPackOverview } from './discovery.js';
18
+ import type {
19
+ ChainSummary,
20
+ GetZapPacksOptions,
21
+ ZapPackOverview,
22
+ } from './discovery.js';
13
23
  import { ComposeError, errorFromHttpResponse } from './errors.js';
14
24
  import {
15
25
  parseCompilePartialEnvelope,
@@ -19,12 +29,14 @@ import {
19
29
  // __SDK_VERSION__ is a compile-time constant injected by tsup (via `define` in tsup.config.ts)
20
30
  // and by vitest (via `define` in vitest.config.ts). Both read the version from package.json
21
31
  // at build/test time and replace this identifier with the literal string value.
22
- // It is sent as the `x-lifi-composer-sdk` request header so the server can identify the caller.
32
+ // It is sent as the `COMPOSER_SDK_VERSION_HEADER` request header so the server can identify the caller.
23
33
  // Falls back to 'dev' when running via tsx without tsup substitution (e.g. the example harness).
24
34
  declare const __SDK_VERSION__: string;
25
35
 
26
36
  const SDK_VERSION: string =
27
37
  typeof __SDK_VERSION__ !== 'undefined' ? __SDK_VERSION__ : 'dev';
38
+ // `undefined` for the `dev` sentinel, which disables the response version check.
39
+ const SDK_VERSION_TRIPLE = parseVersion(SDK_VERSION);
28
40
 
29
41
  /**
30
42
  * Configuration for creating a low-level Compose API client.
@@ -55,6 +67,28 @@ export interface ComposeClient {
55
67
  * @throws {@link ComposeError} on network, validation, or server errors.
56
68
  */
57
69
  readonly getManifest: () => Promise<ComposeManifest>;
70
+ /**
71
+ * Fetches the chains the Compose API supports, ascending by `chainId`.
72
+ *
73
+ * A chain is listed once the server confirms the VM contract is deployed on
74
+ * it — the same condition {@link compile}, {@link route} and
75
+ * {@link simulate} validate against. A chain absent from this list is
76
+ * rejected outright by those endpoints.
77
+ *
78
+ * The converse does not hold: a listed chain can still have no routing edge
79
+ * for a given token pair, which {@link route} reports as `no_route_error`.
80
+ * Use {@link getZapPacks} to enumerate the pairs that do route.
81
+ *
82
+ * Each entry carries a display `name` alongside its `chainId`. The name is a
83
+ * deployment-maintained label — render it, but key on `chainId`.
84
+ *
85
+ * The list tracks the server's chain registry, which refreshes periodically.
86
+ * Results are not cached by the SDK; callers should cache as appropriate.
87
+ *
88
+ * @returns The supported chains, ascending by `chainId`.
89
+ * @throws {@link ComposeError} on network or server errors.
90
+ */
91
+ readonly getChains: () => Promise<readonly ChainSummary[]>;
58
92
  /**
59
93
  * Submits a compile request and returns the result.
60
94
  *
@@ -75,9 +109,9 @@ export interface ComposeClient {
75
109
  * `POST /compose/route`.
76
110
  *
77
111
  * 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
112
+ * zap flow with a `directDeposit` input from the `fromToken` / `toToken`
113
+ * pair and runs it through the same pipeline as {@link compile}. The
114
+ * response is therefore the same discriminated result — HTTP 206 under
81
115
  * `simulationPolicy: 'allow-revert'` yields `status: 'partial'` rather than
82
116
  * throwing, exactly as it does for {@link compile}.
83
117
  *
@@ -88,6 +122,13 @@ export interface ComposeClient {
88
122
  * author richer flows later, so the single-step limit is the current state of
89
123
  * the server, not the shape of this contract.
90
124
  *
125
+ * Only synchronous edges are considered unless the request sets
126
+ * `allowAsyncRoutes: true`, so by default every produced resource comes back
127
+ * `availability: 'now'` and a pair reachable only after settlement is a
128
+ * `no_route_error`. Opting in adds the `lifi.zapAsync` fallback, which still
129
+ * loses to a synchronous edge when the pair has one; read
130
+ * `producedResources[*].availability` to see which representation you got.
131
+ *
91
132
  * Use {@link compile} for what the server does not author for you: several
92
133
  * chained operations, splits, explicit preconditions, or per-op guards.
93
134
  *
@@ -143,12 +184,80 @@ export interface ComposeClient {
143
184
  readonly simulate: (request: SimulateRequest) => Promise<SimulateResult>;
144
185
  }
145
186
 
146
- const bigintReplacer = (_key: string, value: unknown): unknown =>
187
+ /**
188
+ * `JSON.stringify` replacer that renders `bigint` amounts as decimal strings.
189
+ *
190
+ * The compose wire carries token amounts as decimal strings, and `bigint` has
191
+ * no JSON representation, so every payload this client sends goes through it.
192
+ * Exported so the example harness prints a request exactly as it is sent.
193
+ */
194
+ export const bigintReplacer = (_key: string, value: unknown): unknown =>
147
195
  typeof value === 'bigint' ? value.toString() : value;
148
196
 
149
197
  const isNonNullObject = (v: unknown): v is Record<string, unknown> =>
150
198
  typeof v === 'object' && v !== null;
151
199
 
200
+ // Non-negative decimal integer string, as the server sends token amounts.
201
+ const INTEGER_STRING = /^\d+$/;
202
+
203
+ const decodeAmount = (
204
+ value: unknown,
205
+ key: string,
206
+ field: 'amountOut' | 'amountOutMin',
207
+ url: string,
208
+ ): bigint => {
209
+ if (typeof value !== 'string' || !INTEGER_STRING.test(value)) {
210
+ throw new ComposeError(
211
+ 'UNKNOWN_ERROR',
212
+ `Malformed compose response: producedResources.${key}.simulated.${field} is ${
213
+ value === undefined ? 'absent' : 'not an integer string'
214
+ }`,
215
+ { url },
216
+ );
217
+ }
218
+ return BigInt(value);
219
+ };
220
+
221
+ // `simulated.amountOut` / `.amountOutMin` are declared `bigint` on
222
+ // `SimulatedAmounts` (the domain truth: `compose-core` produces real bigints)
223
+ // but arrive as decimal strings on the wire (the backend stringifies them).
224
+ // Decode at this seam so a consumer reading the declared type gets the value
225
+ // the type promises. A resource without `simulated` is left untouched; a
226
+ // `simulated` claiming a missing or non-numeric amount is a contract violation
227
+ // and surfaces as a `ComposeError` rather than `NaN` / `BigInt(undefined)`.
228
+ const decodeSimulatedAmounts = (data: unknown, url: string): void => {
229
+ if (!isNonNullObject(data)) return;
230
+ const resources = data.producedResources;
231
+ if (!isNonNullObject(resources)) return;
232
+ for (const key of Object.keys(resources)) {
233
+ const resource = resources[key];
234
+ if (!isNonNullObject(resource)) continue;
235
+ const simulated = resource.simulated;
236
+ if (simulated === undefined) continue;
237
+ if (!isNonNullObject(simulated)) {
238
+ throw new ComposeError(
239
+ 'UNKNOWN_ERROR',
240
+ `Malformed compose response: producedResources.${key}.simulated is not an object`,
241
+ { url },
242
+ );
243
+ }
244
+ simulated.amountOut = decodeAmount(
245
+ simulated.amountOut,
246
+ key,
247
+ 'amountOut',
248
+ url,
249
+ );
250
+ if (simulated.amountOutMin !== undefined) {
251
+ simulated.amountOutMin = decodeAmount(
252
+ simulated.amountOutMin,
253
+ key,
254
+ 'amountOutMin',
255
+ url,
256
+ );
257
+ }
258
+ }
259
+ };
260
+
152
261
  const parseBody = async <T>(res: Response, url: string): Promise<T> => {
153
262
  const body = await res.json().catch((_) => null);
154
263
  if (!isNonNullObject(body) || !('data' in body)) {
@@ -164,6 +273,7 @@ const parseCompileSuccessBody = async (
164
273
  url: string,
165
274
  ): Promise<ComposeCompileResult> => {
166
275
  const data = await parseBody<ComposeCompileSuccessData>(res, url);
276
+ decodeSimulatedAmounts(data, url);
167
277
  return { ...data, status: 'success' as const };
168
278
  };
169
279
 
@@ -184,6 +294,7 @@ const parsePartialBody = async (
184
294
  // shape as a hand-authored type, so we narrow it here rather than re-declaring
185
295
  // that type as a schema. `error` is fully validated — no cast needed.
186
296
  const data = envelope.data as unknown as ComposeCompilePartialData;
297
+ decodeSimulatedAmounts(data, url);
187
298
  return { ...data, status: 'partial' as const, error: envelope.error };
188
299
  };
189
300
 
@@ -214,22 +325,92 @@ type ComposeTransport = {
214
325
  readonly baseHeaders: Record<string, string>;
215
326
  };
216
327
 
217
- const postCompile = async (
328
+ // A patch bump is additive and a minor bump is breaking, so a 200 from a
329
+ // backend behind this SDK cannot be trusted: the newer flow this SDK authored
330
+ // may have compiled under the server's older semantics without raising an
331
+ // error. This covers the deploy window where the SDK is published to npm ahead
332
+ // of the backend rollout. Only the compile-family POSTs are checked; discovery
333
+ // GETs stay reachable from a mismatched SDK, as on the server. An absent header
334
+ // (a pre-gate backend, a header-stripping proxy) is accepted, mirroring the
335
+ // server's pass-through of a compile that carries no version header.
336
+ const assertServerVersion = (res: Response, url: string): void => {
337
+ const serverVersion = res.headers.get(COMPOSE_VERSION_HEADER);
338
+ if (serverVersion === null || SDK_VERSION_TRIPLE === undefined) return;
339
+ const serverTriple = parseVersion(serverVersion);
340
+ if (serverTriple === undefined) return;
341
+
342
+ const compatibility = checkCompatibility(SDK_VERSION_TRIPLE, serverTriple);
343
+ if (compatibility === 'compatible') return;
344
+
345
+ const versions = { sdkVersion: SDK_VERSION, serverVersion };
346
+ if (compatibility === 'sdk_outdated') {
347
+ throw new ComposeError(
348
+ 'VALIDATION_ERROR',
349
+ `composer-sdk ${SDK_VERSION} is older than the compose contract served at ${url} (${serverVersion}); upgrade @lifi/composer-sdk and @lifi/compose-spec`,
350
+ {
351
+ status: res.status,
352
+ url,
353
+ kind: 'sdk_outdated',
354
+ sdkOutdated: {
355
+ ...versions,
356
+ minimumSdkVersion: `${serverTriple.major}.${serverTriple.minor}.0`,
357
+ },
358
+ },
359
+ );
360
+ }
361
+ throw new ComposeError(
362
+ 'VALIDATION_ERROR',
363
+ `composer-sdk ${SDK_VERSION} targets a newer compose contract than the server at ${url} serves (${serverVersion}); pin @lifi/composer-sdk and @lifi/compose-spec to ${serverVersion} or wait for the server rollout`,
364
+ {
365
+ status: res.status,
366
+ url,
367
+ kind: 'server_outdated',
368
+ serverOutdated: versions,
369
+ },
370
+ );
371
+ };
372
+
373
+ type SendInit =
374
+ | { readonly method: 'GET' }
375
+ | { readonly method: 'POST'; readonly body: unknown };
376
+
377
+ // GETs on this API carry no body; POSTs carry a JSON one with `bigint` amounts
378
+ // rendered as decimal strings.
379
+ const requestInit = (
380
+ baseHeaders: Record<string, string>,
381
+ init: SendInit,
382
+ ): RequestInit =>
383
+ init.method === 'GET'
384
+ ? { method: 'GET', headers: { ...baseHeaders } }
385
+ : {
386
+ method: 'POST',
387
+ headers: { ...baseHeaders, 'Content-Type': 'application/json' },
388
+ body: JSON.stringify(init.body, bigintReplacer),
389
+ };
390
+
391
+ // One fetch path for every route: maps transport failures to `NETWORK_ERROR`.
392
+ // The response version check is not here — only the compile-family POSTs gate
393
+ // on it (see `postCompile`); discovery GETs and `/simulate` are ungated.
394
+ const send = async (
218
395
  { fetchFn, baseHeaders }: ComposeTransport,
219
396
  url: string,
220
- request: ComposeCompileRequest | ComposeRouteRequest,
221
- ): Promise<ComposeCompileResult> => {
222
- let res: Response;
397
+ init: SendInit,
398
+ ): Promise<Response> => {
223
399
  try {
224
- res = await fetchFn(url, {
225
- method: 'POST',
226
- headers: { ...baseHeaders, 'Content-Type': 'application/json' },
227
- body: JSON.stringify(request, bigintReplacer),
228
- });
400
+ return await fetchFn(url, requestInit(baseHeaders, init));
229
401
  } catch (err) {
230
402
  const message = err instanceof Error ? err.message : String(err);
231
403
  throw new ComposeError('NETWORK_ERROR', message, { cause: err });
232
404
  }
405
+ };
406
+
407
+ const postCompile = async (
408
+ transport: ComposeTransport,
409
+ url: string,
410
+ request: ComposeCompileRequest | ComposeRouteRequest,
411
+ ): Promise<ComposeCompileResult> => {
412
+ const res = await send(transport, url, { method: 'POST', body: request });
413
+ assertServerVersion(res, url);
233
414
  if (res.status === 206) {
234
415
  return await parsePartialBody(res, url);
235
416
  }
@@ -240,6 +421,20 @@ const postCompile = async (
240
421
  return await parseCompileSuccessBody(res, url);
241
422
  };
242
423
 
424
+ // Every enveloped GET on this API behaves identically: no body, no query
425
+ // semantics of its own, `{ data }` on success, an error envelope otherwise.
426
+ const getJson = async <T>(
427
+ transport: ComposeTransport,
428
+ url: string,
429
+ ): Promise<T> => {
430
+ const res = await send(transport, url, { method: 'GET' });
431
+ if (!res.ok) {
432
+ const body = await res.text();
433
+ throw errorFromHttpResponse(res.status, body, url);
434
+ }
435
+ return await parseBody<T>(res, url);
436
+ };
437
+
243
438
  /**
244
439
  * Creates a low-level Compose API client.
245
440
  *
@@ -267,29 +462,16 @@ export const createComposeClient = (
267
462
 
268
463
  const baseHeaders: Record<string, string> = {
269
464
  Accept: 'application/json',
270
- 'x-lifi-composer-sdk': SDK_VERSION,
465
+ [COMPOSER_SDK_VERSION_HEADER]: SDK_VERSION,
271
466
  'x-lifi-api-key': trimmedApiKey,
272
467
  };
273
468
  const transport: ComposeTransport = { fetchFn, baseHeaders };
274
469
 
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
- };
470
+ const getManifest = async (): Promise<ComposeManifest> =>
471
+ getJson<ComposeManifest>(transport, `${base}/compose/manifest`);
472
+
473
+ const getChains = async (): Promise<readonly ChainSummary[]> =>
474
+ getJson<readonly ChainSummary[]>(transport, `${base}/chains`);
293
475
 
294
476
  const compile = async (
295
477
  request: ComposeCompileRequest,
@@ -312,39 +494,17 @@ export const createComposeClient = (
312
494
  params.set('protocols', list);
313
495
  }
314
496
  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);
497
+ return getJson<readonly ZapPackOverview[]>(
498
+ transport,
499
+ `${base}/compose/zap-packs${qs ? `?${qs}` : ''}`,
500
+ );
331
501
  };
332
502
 
333
503
  const simulate = async (
334
504
  request: SimulateRequest,
335
505
  ): Promise<SimulateResult> => {
336
506
  const url = `${base}/simulate`;
337
- let res: Response;
338
- try {
339
- res = await fetchFn(url, {
340
- method: 'POST',
341
- headers: { ...baseHeaders, 'Content-Type': 'application/json' },
342
- body: JSON.stringify(request, bigintReplacer),
343
- });
344
- } catch (err) {
345
- const message = err instanceof Error ? err.message : String(err);
346
- throw new ComposeError('NETWORK_ERROR', message, { cause: err });
347
- }
507
+ const res = await send(transport, url, { method: 'POST', body: request });
348
508
  // Only 200 (carries `ok`/`revert`) and 422 (carries the `error` member of
349
509
  // the union) have a discriminated body. 422 is deliberately intercepted
350
510
  // here (not thrown as VALIDATION_ERROR) so callers get one exhaustive
@@ -358,5 +518,5 @@ export const createComposeClient = (
358
518
  throw errorFromHttpResponse(res.status, body, url);
359
519
  };
360
520
 
361
- return { getManifest, compile, route, getZapPacks, simulate };
521
+ return { getManifest, getChains, compile, route, getZapPacks, simulate };
362
522
  };
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.
package/src/errors.ts CHANGED
@@ -1,9 +1,11 @@
1
1
  import type {
2
2
  ComposeErrorKind,
3
+ ComposeRouteErrorKind,
3
4
  FailedPreparedOp,
4
5
  GenericComposeErrorKind,
5
6
  SimulationRevert,
6
7
  } from '@lifi/compose-spec';
8
+ import z from 'zod';
7
9
 
8
10
  import {
9
11
  parseServerErrorBody,
@@ -32,6 +34,24 @@ export type ComposeErrorCode =
32
34
  | 'NOT_FOUND'
33
35
  | 'UNKNOWN_ERROR';
34
36
 
37
+ /** Version details returned when the backend rejects an outdated SDK. */
38
+ export interface ComposeSdkOutdated {
39
+ readonly sdkVersion: string;
40
+ readonly serverVersion: string;
41
+ readonly minimumSdkVersion: string;
42
+ }
43
+
44
+ /**
45
+ * Version details attached when the SDK is ahead of the backend it reached.
46
+ * Raised by the backend's version gate, or by the SDK itself when the
47
+ * `x-lifi-composer-version` response header names an older contract than the
48
+ * one this SDK was built against.
49
+ */
50
+ export interface ComposeServerOutdated {
51
+ readonly sdkVersion: string;
52
+ readonly serverVersion: string;
53
+ }
54
+
35
55
  /**
36
56
  * Error class for all failures originating from the Compose SDK or API.
37
57
  *
@@ -58,7 +78,7 @@ export class ComposeError extends Error {
58
78
  /** The request URL that produced the error. */
59
79
  readonly url?: string;
60
80
  /** Server-provided error kind for finer-grained classification. */
61
- readonly kind?: ComposeErrorKind;
81
+ readonly kind?: ComposeErrorKind | ComposeRouteErrorKind;
62
82
  /** JSON-pointer path to the field that caused a validation error. */
63
83
  readonly path?: string;
64
84
  /**
@@ -78,6 +98,10 @@ export class ComposeError extends Error {
78
98
  * `preparation_error` errors alongside {@link ComposeError.failedOps}.
79
99
  */
80
100
  readonly succeededOps?: readonly string[];
101
+ /** Version details attached only to `sdk_outdated` errors. */
102
+ readonly sdkOutdated?: ComposeSdkOutdated;
103
+ /** Version details attached only to `server_outdated` errors. */
104
+ readonly serverOutdated?: ComposeServerOutdated;
81
105
 
82
106
  constructor(
83
107
  code: ComposeErrorCode,
@@ -86,11 +110,13 @@ export class ComposeError extends Error {
86
110
  status?: number;
87
111
  url?: string;
88
112
  cause?: unknown;
89
- kind?: ComposeErrorKind;
113
+ kind?: ComposeErrorKind | ComposeRouteErrorKind;
90
114
  path?: string;
91
115
  details?: SimulationRevert;
92
116
  failedOps?: readonly FailedPreparedOp[];
93
117
  succeededOps?: readonly string[];
118
+ sdkOutdated?: ComposeSdkOutdated;
119
+ serverOutdated?: ComposeServerOutdated;
94
120
  },
95
121
  ) {
96
122
  super(message, { cause: options?.cause });
@@ -102,6 +128,8 @@ export class ComposeError extends Error {
102
128
  this.details = options?.details;
103
129
  this.failedOps = options?.failedOps;
104
130
  this.succeededOps = options?.succeededOps;
131
+ this.sdkOutdated = options?.sdkOutdated;
132
+ this.serverOutdated = options?.serverOutdated;
105
133
  }
106
134
  }
107
135
 
@@ -153,6 +181,69 @@ export const isComposePreparationError = (
153
181
  Array.isArray(e.failedOps) &&
154
182
  Array.isArray(e.succeededOps);
155
183
 
184
+ // `isComposeError` accepts a structurally-similar error by name, so an error
185
+ // raised by a *second* copy of this package in the dependency tree reaches the
186
+ // guard below without its `sdkOutdated` field being type-guaranteed. Parse it.
187
+ const sdkOutdatedSchema = z.object({
188
+ sdkVersion: z.string(),
189
+ serverVersion: z.string(),
190
+ minimumSdkVersion: z.string(),
191
+ });
192
+
193
+ /**
194
+ * Narrows an outdated-SDK rejection to its required version details.
195
+ *
196
+ * @example
197
+ * ```ts
198
+ * try {
199
+ * await builder.compile(run);
200
+ * } catch (e) {
201
+ * if (isComposeSdkOutdatedError(e)) {
202
+ * console.error(`Upgrade to at least ${e.sdkOutdated.minimumSdkVersion}`);
203
+ * return;
204
+ * }
205
+ * throw e;
206
+ * }
207
+ * ```
208
+ */
209
+ export const isComposeSdkOutdatedError = (
210
+ e: unknown,
211
+ ): e is ComposeError & { readonly sdkOutdated: ComposeSdkOutdated } =>
212
+ isComposeError(e) &&
213
+ e.kind === 'sdk_outdated' &&
214
+ sdkOutdatedSchema.safeParse(e.sdkOutdated).success;
215
+
216
+ const serverOutdatedSchema = z.object({
217
+ sdkVersion: z.string(),
218
+ serverVersion: z.string(),
219
+ });
220
+
221
+ /**
222
+ * Narrows a server-behind-SDK rejection to its required version details.
223
+ *
224
+ * This is the deploy-window error: the SDK on npm is ahead of the backend that
225
+ * answered. Pin the SDK to the server's `major.minor`, or wait for the rollout.
226
+ *
227
+ * @example
228
+ * ```ts
229
+ * try {
230
+ * await builder.compile(run);
231
+ * } catch (e) {
232
+ * if (isComposeServerOutdatedError(e)) {
233
+ * console.error(`Server serves ${e.serverOutdated.serverVersion}`);
234
+ * return;
235
+ * }
236
+ * throw e;
237
+ * }
238
+ * ```
239
+ */
240
+ export const isComposeServerOutdatedError = (
241
+ e: unknown,
242
+ ): e is ComposeError & { readonly serverOutdated: ComposeServerOutdated } =>
243
+ isComposeError(e) &&
244
+ e.kind === 'server_outdated' &&
245
+ serverOutdatedSchema.safeParse(e.serverOutdated).success;
246
+
156
247
  const STATUS_TO_CODE: ReadonlyMap<number, ComposeErrorCode> = new Map<
157
248
  number,
158
249
  ComposeErrorCode
@@ -175,6 +266,24 @@ const tryParseErrorBody = (body: string): ServerErrorBody | null => {
175
266
  return parseServerErrorBody(json);
176
267
  };
177
268
 
269
+ /**
270
+ * Extracts the version triple an `sdk_outdated` or `server_outdated` envelope
271
+ * carries. The slots reuse the same schemas the type guards narrow with. Zod
272
+ * strips keys the schema does not declare, so the parsed value is exactly the
273
+ * triple; a partial payload fails the parse and yields `undefined` rather than
274
+ * a half-populated object.
275
+ */
276
+ const readVersionSlots = (serverError: ServerErrorBody['error']) => ({
277
+ sdkOutdated:
278
+ serverError?.kind === 'sdk_outdated'
279
+ ? sdkOutdatedSchema.safeParse(serverError).data
280
+ : undefined,
281
+ serverOutdated:
282
+ serverError?.kind === 'server_outdated'
283
+ ? serverOutdatedSchema.safeParse(serverError).data
284
+ : undefined,
285
+ });
286
+
178
287
  /**
179
288
  * Constructs a {@link ComposeError} from an HTTP error response, extracting
180
289
  * structured error details from the response body when available.
@@ -191,6 +300,7 @@ export const errorFromHttpResponse = (
191
300
  ): ComposeError => {
192
301
  const parsed = tryParseErrorBody(body);
193
302
  const serverError = parsed?.error;
303
+ const { sdkOutdated, serverOutdated } = readVersionSlots(serverError);
194
304
 
195
305
  return new ComposeError(
196
306
  STATUS_TO_CODE.get(status) ??
@@ -199,7 +309,8 @@ export const errorFromHttpResponse = (
199
309
  {
200
310
  status,
201
311
  url,
202
- kind: serverError?.kind as ComposeErrorKind | undefined,
312
+ kind: serverError?.kind as
313
+ ComposeErrorKind | ComposeRouteErrorKind | undefined,
203
314
  path: serverError?.path,
204
315
  details: serverError?.details,
205
316
  // The wire schema types `kind` as a bare string; the server only ever
@@ -209,6 +320,8 @@ export const errorFromHttpResponse = (
209
320
  kind: op.kind as GenericComposeErrorKind,
210
321
  })),
211
322
  succeededOps: serverError?.succeededOps,
323
+ sdkOutdated,
324
+ serverOutdated,
212
325
  },
213
326
  );
214
327
  };