@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.
- package/README.md +6 -3
- package/dist/authoring/FlowBuilderCore.cjs +1 -5
- package/dist/authoring/FlowBuilderCore.cjs.map +1 -1
- package/dist/authoring/FlowBuilderCore.d.cts +9 -7
- package/dist/authoring/FlowBuilderCore.d.ts +9 -7
- package/dist/authoring/FlowBuilderCore.js +2 -6
- package/dist/authoring/FlowBuilderCore.js.map +1 -1
- package/dist/authoring/handles.cjs +5 -2
- package/dist/authoring/handles.cjs.map +1 -1
- package/dist/authoring/handles.d.cts +25 -6
- package/dist/authoring/handles.d.ts +25 -6
- package/dist/authoring/handles.js +3 -1
- package/dist/authoring/handles.js.map +1 -1
- package/dist/authoring/raw.cjs +3 -0
- package/dist/authoring/raw.cjs.map +1 -1
- package/dist/authoring/raw.d.cts +33 -3
- package/dist/authoring/raw.d.ts +33 -3
- package/dist/authoring/raw.js +2 -0
- package/dist/authoring/raw.js.map +1 -1
- package/dist/authoring/signatureArgs.cjs.map +1 -1
- package/dist/authoring/signatureArgs.d.cts +16 -12
- package/dist/authoring/signatureArgs.d.ts +16 -12
- package/dist/authoring/signatureArgs.js.map +1 -1
- package/dist/client.cjs +112 -55
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +42 -5
- package/dist/client.d.ts +42 -5
- package/dist/client.js +116 -55
- package/dist/client.js.map +1 -1
- package/dist/discovery.cjs.map +1 -1
- package/dist/discovery.d.cts +18 -2
- package/dist/discovery.d.ts +18 -2
- package/dist/errors.cjs +42 -3
- package/dist/errors.cjs.map +1 -1
- package/dist/errors.d.cts +67 -4
- package/dist/errors.d.ts +67 -4
- package/dist/errors.js +29 -2
- package/dist/errors.js.map +1 -1
- package/dist/generated/config.generated.cjs.map +1 -1
- package/dist/generated/config.generated.d.cts +21 -1
- package/dist/generated/config.generated.d.ts +21 -1
- package/dist/generated/operations.generated.cjs +6 -0
- package/dist/generated/operations.generated.cjs.map +1 -1
- package/dist/generated/operations.generated.d.cts +18 -2
- package/dist/generated/operations.generated.d.ts +18 -2
- package/dist/generated/operations.generated.js +6 -0
- package/dist/generated/operations.generated.js.map +1 -1
- package/dist/index.cjs +4 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -7
- package/dist/index.d.ts +7 -7
- package/dist/index.js +8 -1
- package/dist/index.js.map +1 -1
- package/dist/{raw-IxCF_bSS.d.ts → raw-BvRghN3r.d.ts} +3 -2
- package/dist/{raw-Dj_2rBSQ.d.cts → raw-s1rer8X3.d.cts} +3 -2
- package/dist/raw.cjs +2 -0
- package/dist/raw.cjs.map +1 -1
- package/dist/raw.d.cts +1 -1
- package/dist/raw.d.ts +1 -1
- package/dist/raw.js +2 -1
- package/dist/raw.js.map +1 -1
- package/dist/responseSchemas.cjs +3 -0
- package/dist/responseSchemas.cjs.map +1 -1
- package/dist/responseSchemas.d.cts +3 -0
- package/dist/responseSchemas.d.ts +3 -0
- package/dist/responseSchemas.js +3 -0
- package/dist/responseSchemas.js.map +1 -1
- package/dist/run/inputs.cjs +14 -15
- package/dist/run/inputs.cjs.map +1 -1
- package/dist/run/inputs.d.cts +8 -0
- package/dist/run/inputs.d.ts +8 -0
- package/dist/run/inputs.js +14 -15
- package/dist/run/inputs.js.map +1 -1
- package/dist/run/simulate.cjs +5 -0
- package/dist/run/simulate.cjs.map +1 -1
- package/dist/run/simulate.d.cts +5 -2
- package/dist/run/simulate.d.ts +5 -2
- package/dist/run/simulate.js +5 -0
- package/dist/run/simulate.js.map +1 -1
- package/dist/sdk.cjs +2 -0
- package/dist/sdk.cjs.map +1 -1
- package/dist/sdk.d.cts +28 -1
- package/dist/sdk.d.ts +28 -1
- package/dist/sdk.js +2 -0
- package/dist/sdk.js.map +1 -1
- package/dist/types.cjs.map +1 -1
- package/dist/types.d.cts +18 -2
- package/dist/types.d.ts +18 -2
- package/package.json +3 -10
- package/src/authoring/FlowBuilderCore.ts +13 -15
- package/src/authoring/handles.ts +41 -5
- package/src/authoring/raw.ts +36 -2
- package/src/authoring/signatureArgs.ts +17 -13
- package/src/client.ts +222 -62
- package/src/discovery.ts +18 -1
- package/src/errors.ts +116 -3
- package/src/examples/flySwap.ts +167 -0
- package/src/examples/run.ts +35 -13
- package/src/examples/supportedChains.ts +122 -0
- package/src/generated/config.generated.ts +24 -0
- package/src/generated/operations.generated.ts +26 -0
- package/src/index.ts +18 -2
- package/src/raw.ts +1 -1
- package/src/responseSchemas.ts +6 -0
- package/src/run/inputs.ts +30 -11
- package/src/run/simulate.ts +14 -2
- package/src/sdk.ts +30 -0
- 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 {
|
|
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 `
|
|
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
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
221
|
-
): Promise<
|
|
222
|
-
let res: Response;
|
|
397
|
+
init: SendInit,
|
|
398
|
+
): Promise<Response> => {
|
|
223
399
|
try {
|
|
224
|
-
|
|
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
|
-
|
|
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
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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
|
-
|
|
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
|
|
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
|
};
|