@lifi/composer-sdk 0.0.1-alpha

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 (160) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +220 -0
  3. package/dist/authoring/FlowBuilderCore.cjs +115 -0
  4. package/dist/authoring/FlowBuilderCore.cjs.map +1 -0
  5. package/dist/authoring/FlowBuilderCore.d.cts +114 -0
  6. package/dist/authoring/FlowBuilderCore.d.ts +114 -0
  7. package/dist/authoring/FlowBuilderCore.js +91 -0
  8. package/dist/authoring/FlowBuilderCore.js.map +1 -0
  9. package/dist/authoring/handles.cjs +43 -0
  10. package/dist/authoring/handles.cjs.map +1 -0
  11. package/dist/authoring/handles.d.cts +61 -0
  12. package/dist/authoring/handles.d.ts +61 -0
  13. package/dist/authoring/handles.js +17 -0
  14. package/dist/authoring/handles.js.map +1 -0
  15. package/dist/authoring/raw.cjs +41 -0
  16. package/dist/authoring/raw.cjs.map +1 -0
  17. package/dist/authoring/raw.d.cts +47 -0
  18. package/dist/authoring/raw.d.ts +47 -0
  19. package/dist/authoring/raw.js +16 -0
  20. package/dist/authoring/raw.js.map +1 -0
  21. package/dist/authoring/signatureArgs.cjs +114 -0
  22. package/dist/authoring/signatureArgs.cjs.map +1 -0
  23. package/dist/authoring/signatureArgs.d.cts +146 -0
  24. package/dist/authoring/signatureArgs.d.ts +146 -0
  25. package/dist/authoring/signatureArgs.js +87 -0
  26. package/dist/authoring/signatureArgs.js.map +1 -0
  27. package/dist/client.cjs +140 -0
  28. package/dist/client.cjs.map +1 -0
  29. package/dist/client.d.cts +67 -0
  30. package/dist/client.d.ts +67 -0
  31. package/dist/client.js +116 -0
  32. package/dist/client.js.map +1 -0
  33. package/dist/discovery.cjs +17 -0
  34. package/dist/discovery.cjs.map +1 -0
  35. package/dist/discovery.d.cts +56 -0
  36. package/dist/discovery.d.ts +56 -0
  37. package/dist/discovery.js +1 -0
  38. package/dist/discovery.js.map +1 -0
  39. package/dist/errors.cjs +91 -0
  40. package/dist/errors.cjs.map +1 -0
  41. package/dist/errors.d.cts +77 -0
  42. package/dist/errors.d.ts +77 -0
  43. package/dist/errors.js +65 -0
  44. package/dist/errors.js.map +1 -0
  45. package/dist/generated/config.generated.cjs +17 -0
  46. package/dist/generated/config.generated.cjs.map +1 -0
  47. package/dist/generated/config.generated.d.cts +144 -0
  48. package/dist/generated/config.generated.d.ts +144 -0
  49. package/dist/generated/config.generated.js +1 -0
  50. package/dist/generated/config.generated.js.map +1 -0
  51. package/dist/generated/guards.generated.cjs +32 -0
  52. package/dist/generated/guards.generated.cjs.map +1 -0
  53. package/dist/generated/guards.generated.d.cts +4 -0
  54. package/dist/generated/guards.generated.d.ts +4 -0
  55. package/dist/generated/guards.generated.js +8 -0
  56. package/dist/generated/guards.generated.js.map +1 -0
  57. package/dist/generated/materialisers.generated.cjs +44 -0
  58. package/dist/generated/materialisers.generated.cjs.map +1 -0
  59. package/dist/generated/materialisers.generated.d.cts +4 -0
  60. package/dist/generated/materialisers.generated.d.ts +4 -0
  61. package/dist/generated/materialisers.generated.js +18 -0
  62. package/dist/generated/materialisers.generated.js.map +1 -0
  63. package/dist/generated/operations.generated.cjs +110 -0
  64. package/dist/generated/operations.generated.cjs.map +1 -0
  65. package/dist/generated/operations.generated.d.cts +337 -0
  66. package/dist/generated/operations.generated.d.ts +337 -0
  67. package/dist/generated/operations.generated.js +89 -0
  68. package/dist/generated/operations.generated.js.map +1 -0
  69. package/dist/generated/preconditions.generated.cjs +44 -0
  70. package/dist/generated/preconditions.generated.cjs.map +1 -0
  71. package/dist/generated/preconditions.generated.d.cts +4 -0
  72. package/dist/generated/preconditions.generated.d.ts +4 -0
  73. package/dist/generated/preconditions.generated.js +18 -0
  74. package/dist/generated/preconditions.generated.js.map +1 -0
  75. package/dist/guards.generated-DKKDKREA.d.ts +14 -0
  76. package/dist/guards.generated-hrQ1KPNv.d.cts +14 -0
  77. package/dist/index.cjs +76 -0
  78. package/dist/index.cjs.map +1 -0
  79. package/dist/index.d.cts +99 -0
  80. package/dist/index.d.ts +99 -0
  81. package/dist/index.js +30 -0
  82. package/dist/index.js.map +1 -0
  83. package/dist/materialisers.generated-BsA1uDW8.d.ts +24 -0
  84. package/dist/materialisers.generated-nMxTAaVi.d.cts +24 -0
  85. package/dist/preconditions.generated-DblS13Wq.d.cts +24 -0
  86. package/dist/preconditions.generated-U7YpVh30.d.ts +24 -0
  87. package/dist/raw-Dj_2rBSQ.d.cts +19 -0
  88. package/dist/raw-IxCF_bSS.d.ts +19 -0
  89. package/dist/raw.cjs +34 -0
  90. package/dist/raw.cjs.map +1 -0
  91. package/dist/raw.d.cts +4 -0
  92. package/dist/raw.d.ts +4 -0
  93. package/dist/raw.js +8 -0
  94. package/dist/raw.js.map +1 -0
  95. package/dist/resources-BKlFxjo8.d.cts +56 -0
  96. package/dist/resources-CYis1_DS.d.ts +56 -0
  97. package/dist/resources.cjs +39 -0
  98. package/dist/resources.cjs.map +1 -0
  99. package/dist/resources.d.cts +3 -0
  100. package/dist/resources.d.ts +3 -0
  101. package/dist/resources.js +14 -0
  102. package/dist/resources.js.map +1 -0
  103. package/dist/run/inputs.cjs +51 -0
  104. package/dist/run/inputs.cjs.map +1 -0
  105. package/dist/run/inputs.d.cts +91 -0
  106. package/dist/run/inputs.d.ts +91 -0
  107. package/dist/run/inputs.js +26 -0
  108. package/dist/run/inputs.js.map +1 -0
  109. package/dist/sdk.cjs +60 -0
  110. package/dist/sdk.cjs.map +1 -0
  111. package/dist/sdk.d.cts +112 -0
  112. package/dist/sdk.d.ts +112 -0
  113. package/dist/sdk.js +40 -0
  114. package/dist/sdk.js.map +1 -0
  115. package/dist/types.cjs +17 -0
  116. package/dist/types.cjs.map +1 -0
  117. package/dist/types.d.cts +74 -0
  118. package/dist/types.d.ts +74 -0
  119. package/dist/types.js +1 -0
  120. package/dist/types.js.map +1 -0
  121. package/package.json +81 -0
  122. package/src/authoring/FlowBuilderCore.ts +270 -0
  123. package/src/authoring/handles.ts +81 -0
  124. package/src/authoring/raw.ts +60 -0
  125. package/src/authoring/signatureArgs.ts +249 -0
  126. package/src/client.ts +230 -0
  127. package/src/discovery.ts +51 -0
  128. package/src/errors.ts +150 -0
  129. package/src/examples/approveAndDeposit.ts +84 -0
  130. package/src/examples/callContract.ts +199 -0
  131. package/src/examples/config.ts +9 -0
  132. package/src/examples/consolidateToEth.ts +94 -0
  133. package/src/examples/consolidateToUsdc.ts +95 -0
  134. package/src/examples/depositFromProxy.ts +87 -0
  135. package/src/examples/dustSweep.ts +66 -0
  136. package/src/examples/lifiSwap.ts +56 -0
  137. package/src/examples/lifiZap.ts +60 -0
  138. package/src/examples/rawCallWithArithmetic.ts +78 -0
  139. package/src/examples/readContractState.ts +77 -0
  140. package/src/examples/run.ts +168 -0
  141. package/src/examples/splitAndZap.ts +73 -0
  142. package/src/examples/splitWithArithmetic.ts +93 -0
  143. package/src/examples/swapAndZap.ts +72 -0
  144. package/src/examples/swapToRecipient.ts +109 -0
  145. package/src/examples/swapWithAllowRevert.ts +119 -0
  146. package/src/examples/swapWithBalanceCheck.ts +73 -0
  147. package/src/examples/swapWithOutputValidation.ts +125 -0
  148. package/src/examples/transferTokens.ts +123 -0
  149. package/src/examples/untypedOpWithTypedRef.ts +77 -0
  150. package/src/generated/config.generated.ts +174 -0
  151. package/src/generated/guards.generated.ts +14 -0
  152. package/src/generated/materialisers.generated.ts +44 -0
  153. package/src/generated/operations.generated.ts +568 -0
  154. package/src/generated/preconditions.generated.ts +43 -0
  155. package/src/index.ts +149 -0
  156. package/src/raw.ts +10 -0
  157. package/src/resources.ts +61 -0
  158. package/src/run/inputs.ts +117 -0
  159. package/src/sdk.ts +173 -0
  160. package/src/types.ts +101 -0
package/src/client.ts ADDED
@@ -0,0 +1,230 @@
1
+ import type {
2
+ ComposeCompilePartialData,
3
+ ComposeCompileRequest,
4
+ ComposeCompileResult,
5
+ ComposeCompileSuccessData,
6
+ ComposeManifest,
7
+ } from '@lifi/compose-spec';
8
+
9
+ import type { GetZapPacksOptions, ZapPackOverview } from './discovery.js';
10
+ import { ComposeError, errorFromHttpResponse } from './errors.js';
11
+
12
+ // __SDK_VERSION__ is a compile-time constant injected by tsup (via `define` in tsup.config.ts)
13
+ // and by vitest (via `define` in vitest.config.ts). Both read the version from package.json
14
+ // at build/test time and replace this identifier with the literal string value.
15
+ // It is sent as the `x-lifi-composer-sdk` request header so the server can identify the caller.
16
+ // Falls back to 'dev' when running via tsx without tsup substitution (e.g. the example harness).
17
+ declare const __SDK_VERSION__: string;
18
+
19
+ const SDK_VERSION: string =
20
+ typeof __SDK_VERSION__ !== 'undefined' ? __SDK_VERSION__ : 'dev';
21
+
22
+ /**
23
+ * Configuration for creating a low-level Compose API client.
24
+ */
25
+ export interface ComposeClientOptions {
26
+ /** Base URL of the Compose API. */
27
+ readonly baseUrl: string;
28
+ /** Optional custom `fetch` implementation. Defaults to `globalThis.fetch`. */
29
+ readonly fetch?: typeof globalThis.fetch;
30
+ /** Optional LI.FI API key. When set, sent as the `x-lifi-api-key` header on every request. */
31
+ readonly apiKey?: string;
32
+ }
33
+
34
+ /**
35
+ * Low-level HTTP client for the Compose API.
36
+ *
37
+ * Handles request serialization, SDK version headers, and error mapping.
38
+ * Prefer using {@link ComposeSdk} for the full builder experience. Use this
39
+ * directly when you need to decouple request building from submission — e.g.
40
+ * build via `sdk.request()` then submit via `client.compile()` with custom
41
+ * retry logic or request inspection.
42
+ */
43
+ export interface ComposeClient {
44
+ /**
45
+ * Fetches the server's operation manifest describing all supported operations,
46
+ * guards, materialisers, and preconditions.
47
+ * @returns The manifest document.
48
+ * @throws {@link ComposeError} on network, validation, or server errors.
49
+ */
50
+ readonly getManifest: () => Promise<ComposeManifest>;
51
+ /**
52
+ * Submits a compile request and returns the result.
53
+ *
54
+ * When the caller passes `simulationPolicy: 'allow-revert'` and the transaction
55
+ * reverts in simulation, the server responds with HTTP 206 and the SDK returns a
56
+ * partial result (`status: 'partial'`) instead of throwing. The partial result
57
+ * includes the transaction (without `gasLimit`) and revert diagnostics.
58
+ *
59
+ * @param request - The full compile request including flow and run inputs.
60
+ * @returns A discriminated result: `status: 'success'` or `status: 'partial'`.
61
+ * @throws {@link ComposeError} on network, validation, or server errors.
62
+ */
63
+ readonly compile: (
64
+ request: ComposeCompileRequest,
65
+ ) => Promise<ComposeCompileResult>;
66
+ /**
67
+ * Fetches the available routing edges grouped by protocol.
68
+ *
69
+ * The edge catalog is dynamic — it reflects the current state of the
70
+ * backend's routing snapshot (protocols, chains, token blacklists).
71
+ * Results are not cached by the SDK; callers should cache as appropriate.
72
+ *
73
+ * @param options - Optional filter to restrict results to specific protocols.
74
+ * @returns An array of {@link ZapPackOverview} objects, one per protocol.
75
+ * @throws {@link ComposeError} on network or server errors (503 when the
76
+ * routing catalog is not yet initialized).
77
+ */
78
+ readonly getZapPacks: (
79
+ options?: GetZapPacksOptions,
80
+ ) => Promise<readonly ZapPackOverview[]>;
81
+ }
82
+
83
+ const bigintReplacer = (_key: string, value: unknown): unknown =>
84
+ typeof value === 'bigint' ? value.toString() : value;
85
+
86
+ const isNonNullObject = (v: unknown): v is Record<string, unknown> =>
87
+ typeof v === 'object' && v !== null;
88
+
89
+ const parseBody = async <T>(res: Response, url: string): Promise<T> => {
90
+ const body = await res.json().catch((_) => null);
91
+ if (!isNonNullObject(body) || !('data' in body)) {
92
+ throw new ComposeError('UNKNOWN_ERROR', 'Unexpected response format', {
93
+ url,
94
+ });
95
+ }
96
+ return body.data as T;
97
+ };
98
+
99
+ const parseCompileSuccessBody = async (
100
+ res: Response,
101
+ url: string,
102
+ ): Promise<ComposeCompileResult> => {
103
+ const data = await parseBody<ComposeCompileSuccessData>(res, url);
104
+ return { ...data, status: 'success' as const };
105
+ };
106
+
107
+ const parsePartialBody = async (
108
+ res: Response,
109
+ url: string,
110
+ ): Promise<ComposeCompileResult> => {
111
+ const body = await res.json().catch((_) => null);
112
+ if (
113
+ !isNonNullObject(body) ||
114
+ !('data' in body) ||
115
+ !isNonNullObject(body.data) ||
116
+ !('error' in body) ||
117
+ !isNonNullObject(body.error)
118
+ ) {
119
+ throw new ComposeError(
120
+ 'UNKNOWN_ERROR',
121
+ 'Unexpected partial response format',
122
+ { url },
123
+ );
124
+ }
125
+ const data = body.data as unknown as ComposeCompilePartialData;
126
+ const error = body.error as unknown as { kind: string; message: string };
127
+ return { ...data, status: 'partial' as const, error };
128
+ };
129
+
130
+ /**
131
+ * Creates a low-level Compose API client.
132
+ *
133
+ * @param options - Client configuration including the API base URL.
134
+ * @returns A {@link ComposeClient} instance.
135
+ */
136
+ export const createComposeClient = (
137
+ options: ComposeClientOptions,
138
+ ): ComposeClient => {
139
+ if (!options.baseUrl || !/^https?:\/\//i.test(options.baseUrl)) {
140
+ throw new ComposeError(
141
+ 'VALIDATION_ERROR',
142
+ `Invalid baseUrl: expected an HTTP(S) URL, got "${options.baseUrl}"`,
143
+ );
144
+ }
145
+ const fetchFn = options.fetch ?? globalThis.fetch;
146
+ const base = options.baseUrl.replace(/\/$/, '');
147
+
148
+ const trimmedApiKey = options.apiKey?.trim() || undefined;
149
+
150
+ const baseHeaders: Record<string, string> = {
151
+ Accept: 'application/json',
152
+ 'x-lifi-composer-sdk': SDK_VERSION,
153
+ ...(trimmedApiKey ? { 'x-lifi-api-key': trimmedApiKey } : {}),
154
+ };
155
+
156
+ const getManifest = async (): Promise<ComposeManifest> => {
157
+ const url = `${base}/compose/manifest`;
158
+ let res: Response;
159
+ try {
160
+ res = await fetchFn(url, {
161
+ method: 'GET',
162
+ headers: { ...baseHeaders },
163
+ });
164
+ } catch (err) {
165
+ const message = err instanceof Error ? err.message : String(err);
166
+ throw new ComposeError('NETWORK_ERROR', message, { cause: err });
167
+ }
168
+ if (!res.ok) {
169
+ const body = await res.text();
170
+ throw errorFromHttpResponse(res.status, body, url);
171
+ }
172
+ return await parseBody<ComposeManifest>(res, url);
173
+ };
174
+
175
+ const compile = async (
176
+ request: ComposeCompileRequest,
177
+ ): Promise<ComposeCompileResult> => {
178
+ const url = `${base}/compose`;
179
+ let res: Response;
180
+ try {
181
+ res = await fetchFn(url, {
182
+ method: 'POST',
183
+ headers: { ...baseHeaders, 'Content-Type': 'application/json' },
184
+ body: JSON.stringify(request, bigintReplacer),
185
+ });
186
+ } catch (err) {
187
+ const message = err instanceof Error ? err.message : String(err);
188
+ throw new ComposeError('NETWORK_ERROR', message, { cause: err });
189
+ }
190
+ if (res.status === 206) {
191
+ return await parsePartialBody(res, url);
192
+ }
193
+ if (!res.ok) {
194
+ const body = await res.text();
195
+ throw errorFromHttpResponse(res.status, body, url);
196
+ }
197
+ return await parseCompileSuccessBody(res, url);
198
+ };
199
+
200
+ const getZapPacks = async (
201
+ options?: GetZapPacksOptions,
202
+ ): Promise<readonly ZapPackOverview[]> => {
203
+ const params = new URLSearchParams();
204
+ if (options?.protocols != null) {
205
+ // Backend expects a single comma-separated value, not repeated keys.
206
+ const raw = options.protocols;
207
+ const list = typeof raw === 'string' ? raw : raw.join(',');
208
+ params.set('protocols', list);
209
+ }
210
+ const qs = params.toString();
211
+ const url = `${base}/compose/zap-packs${qs ? `?${qs}` : ''}`;
212
+ let res: Response;
213
+ try {
214
+ res = await fetchFn(url, {
215
+ method: 'GET',
216
+ headers: { ...baseHeaders },
217
+ });
218
+ } catch (err) {
219
+ const message = err instanceof Error ? err.message : String(err);
220
+ throw new ComposeError('NETWORK_ERROR', message, { cause: err });
221
+ }
222
+ if (!res.ok) {
223
+ const body = await res.text();
224
+ throw errorFromHttpResponse(res.status, body, url);
225
+ }
226
+ return await parseBody<readonly ZapPackOverview[]>(res, url);
227
+ };
228
+
229
+ return { getManifest, compile, getZapPacks };
230
+ };
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Types for the Compose discovery API (`GET /compose/zap-packs`).
3
+ *
4
+ * These are SDK-facing presentation types — not part of the core compile
5
+ * contract in compose-spec. The backend response is the source of truth;
6
+ * the SDK provides typed access.
7
+ *
8
+ * The `type` field on edges is a plain `string` rather than a strict union
9
+ * so the SDK remains forward-compatible when the backend introduces new
10
+ * edge types. Known values at time of writing:
11
+ * `"enter-position"`, `"exit-position"`, `"wrap"`, `"unwrap"`, `"mint"`, `"burn"`.
12
+ */
13
+
14
+ /**
15
+ * A single routing edge representing a supported token-to-token path
16
+ * through a DeFi protocol.
17
+ */
18
+ export interface ZapPackEdge {
19
+ /** Edge type (e.g. `"enter-position"`, `"exit-position"`, `"wrap"`). */
20
+ readonly type: string;
21
+ /** Input token. */
22
+ readonly in: { readonly address: string; readonly chainId: number };
23
+ /** Output token. */
24
+ readonly out: { readonly address: string; readonly chainId: number };
25
+ /** Minimum input amount accepted (stringified integer), if constrained. */
26
+ readonly inAmountMin?: string;
27
+ /** Maximum input amount accepted (stringified integer), if constrained. */
28
+ readonly inAmountMax?: string;
29
+ }
30
+
31
+ /**
32
+ * All routing edges for a single protocol.
33
+ */
34
+ export interface ZapPackOverview {
35
+ /** Protocol identifier (e.g. `"aave"`, `"morpho"`). */
36
+ readonly protocol: string;
37
+ /** Available routing edges for this protocol. */
38
+ readonly edges: readonly ZapPackEdge[];
39
+ }
40
+
41
+ /**
42
+ * Options for {@link ComposeClient.getZapPacks}.
43
+ */
44
+ export interface GetZapPacksOptions {
45
+ /**
46
+ * Filter to specific protocols. Pass a single string or an array.
47
+ * Only edges belonging to the listed protocols are returned.
48
+ * Omit to fetch all available protocols.
49
+ */
50
+ readonly protocols?: string | readonly string[];
51
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,150 @@
1
+ import type { ComposeErrorKind, SimulationRevert } from '@lifi/compose-spec';
2
+
3
+ /**
4
+ * Machine-readable error codes returned by the SDK.
5
+ *
6
+ * - `NETWORK_ERROR` — The HTTP request failed (DNS, timeout, connection refused).
7
+ * - `VALIDATION_ERROR` — The server rejected the request (HTTP 400/422).
8
+ * - `UNAUTHENTICATED` — The request lacks valid authentication credentials (HTTP 401).
9
+ * - `FORBIDDEN` — The server understood the request but refuses to authorise it (HTTP 403).
10
+ * - `SERVER_ERROR` — The server returned a 5xx status.
11
+ * - `RATE_LIMITED` — The server returned HTTP 429.
12
+ * - `NOT_FOUND` — The requested resource does not exist (HTTP 404).
13
+ * - `UNKNOWN_ERROR` — An unexpected error that doesn't fit other categories.
14
+ */
15
+ export type ComposeErrorCode =
16
+ | 'NETWORK_ERROR'
17
+ | 'VALIDATION_ERROR'
18
+ | 'UNAUTHENTICATED'
19
+ | 'FORBIDDEN'
20
+ | 'SERVER_ERROR'
21
+ | 'RATE_LIMITED'
22
+ | 'NOT_FOUND'
23
+ | 'UNKNOWN_ERROR';
24
+
25
+ /**
26
+ * Error class for all failures originating from the Compose SDK or API.
27
+ *
28
+ * Includes structured metadata beyond the error message to support
29
+ * programmatic error handling.
30
+ *
31
+ * @example
32
+ * ```ts
33
+ * try {
34
+ * await builder.compile(run);
35
+ * } catch (e) {
36
+ * if (isComposeError(e) && e.code === 'VALIDATION_ERROR') {
37
+ * console.error('Invalid request:', e.message, e.path);
38
+ * }
39
+ * }
40
+ * ```
41
+ */
42
+ export class ComposeError extends Error {
43
+ override readonly name = 'ComposeError';
44
+ /** Machine-readable error category. */
45
+ readonly code: ComposeErrorCode;
46
+ /** HTTP status code, when the error originated from an HTTP response. */
47
+ readonly status?: number;
48
+ /** The request URL that produced the error. */
49
+ readonly url?: string;
50
+ /** Server-provided error kind for finer-grained classification. */
51
+ readonly kind?: ComposeErrorKind;
52
+ /** JSON-pointer path to the field that caused a validation error. */
53
+ readonly path?: string;
54
+ /**
55
+ * Simulation revert diagnostics attached to `simulation_revert` errors.
56
+ * Contains the raw error bytes and decoded error candidates when the
57
+ * backend can parse the revert reason.
58
+ */
59
+ readonly details?: SimulationRevert;
60
+
61
+ constructor(
62
+ code: ComposeErrorCode,
63
+ message: string,
64
+ options?: {
65
+ status?: number;
66
+ url?: string;
67
+ cause?: unknown;
68
+ kind?: ComposeErrorKind;
69
+ path?: string;
70
+ details?: SimulationRevert;
71
+ },
72
+ ) {
73
+ super(message, { cause: options?.cause });
74
+ this.code = code;
75
+ this.status = options?.status;
76
+ this.url = options?.url;
77
+ this.kind = options?.kind;
78
+ this.path = options?.path;
79
+ this.details = options?.details;
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Type guard that narrows an unknown error to {@link ComposeError}.
85
+ * @param e - The value to check.
86
+ * @returns `true` if `e` is an instance of `ComposeError`.
87
+ */
88
+ export const isComposeError = (e: unknown): e is ComposeError =>
89
+ e instanceof ComposeError ||
90
+ (e instanceof Error && e.name === 'ComposeError' && 'code' in e);
91
+
92
+ const STATUS_TO_CODE: ReadonlyMap<number, ComposeErrorCode> = new Map<
93
+ number,
94
+ ComposeErrorCode
95
+ >([
96
+ [400, 'VALIDATION_ERROR'],
97
+ [401, 'UNAUTHENTICATED'],
98
+ [403, 'FORBIDDEN'],
99
+ [404, 'NOT_FOUND'],
100
+ [422, 'VALIDATION_ERROR'],
101
+ [429, 'RATE_LIMITED'],
102
+ ]);
103
+
104
+ interface ServerErrorBody {
105
+ readonly error?: {
106
+ readonly kind?: string;
107
+ readonly message?: string;
108
+ readonly path?: string;
109
+ readonly details?: SimulationRevert;
110
+ };
111
+ }
112
+
113
+ const tryParseErrorBody = (body: string): ServerErrorBody | null => {
114
+ try {
115
+ return JSON.parse(body) as ServerErrorBody;
116
+ } catch {
117
+ return null;
118
+ }
119
+ };
120
+
121
+ /**
122
+ * Constructs a {@link ComposeError} from an HTTP error response, extracting
123
+ * structured error details from the response body when available.
124
+ *
125
+ * @param status - The HTTP status code.
126
+ * @param body - The raw response body text.
127
+ * @param url - The request URL that produced the error.
128
+ * @returns A `ComposeError` with the appropriate error code and metadata.
129
+ */
130
+ export const errorFromHttpResponse = (
131
+ status: number,
132
+ body: string,
133
+ url: string,
134
+ ): ComposeError => {
135
+ const parsed = tryParseErrorBody(body);
136
+ const serverError = parsed?.error;
137
+
138
+ return new ComposeError(
139
+ STATUS_TO_CODE.get(status) ??
140
+ (status >= 500 ? 'SERVER_ERROR' : 'UNKNOWN_ERROR'),
141
+ (serverError?.message ?? body) || `HTTP ${status}`,
142
+ {
143
+ status,
144
+ url,
145
+ kind: serverError?.kind as ComposeErrorKind | undefined,
146
+ path: serverError?.path,
147
+ details: serverError?.details,
148
+ },
149
+ );
150
+ };
@@ -0,0 +1,84 @@
1
+ import type { ComposeCompileRequest, Flow } from '@lifi/compose-spec';
2
+
3
+ import { createComposeSdk, materialisers, resources } from '../index.js';
4
+ import type { Address } from '../types.js';
5
+
6
+ import { BASE_URL } from './config.js';
7
+
8
+ const USDC = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48';
9
+ // Example ERC-4626 vault address (Steakhouse USDC on Ethereum mainnet)
10
+ const VAULT = '0xBEEF01735c132Ada46AA9aA4c54623cAA92A64CB';
11
+
12
+ export interface ApproveAndDepositInput {
13
+ readonly owner: Address;
14
+ readonly amount: `${bigint}`;
15
+ }
16
+
17
+ /**
18
+ * Approve a vault to spend USDC, deposit into it, and track the resulting
19
+ * shares as a resource.
20
+ *
21
+ * Demonstrates:
22
+ * - core.approve to grant ERC20 allowance to the vault
23
+ * - core.call to invoke the vault's deposit function
24
+ * - core.asResource to graduate the raw return value to a tracked resource
25
+ */
26
+ export const buildApproveAndDeposit = ({
27
+ owner,
28
+ amount,
29
+ }: ApproveAndDepositInput): {
30
+ flow: Flow;
31
+ request: ComposeCompileRequest;
32
+ } => {
33
+ const sdk = createComposeSdk({ baseUrl: BASE_URL });
34
+
35
+ const builder = sdk.flow(1, {
36
+ name: 'approve-and-deposit',
37
+ inputs: {
38
+ amountIn: resources.erc20(USDC, 1),
39
+ },
40
+ });
41
+
42
+ // Approve the vault to spend our USDC.
43
+ builder.core.approve('approve-vault', {
44
+ bind: { amount: builder.inputs.amountIn },
45
+ config: { spender: VAULT },
46
+ });
47
+
48
+ // Call vault.deposit(uint256 assets, address receiver) returns (uint256).
49
+ // The input resource (USDC) is consumed by the call.
50
+ // The `returns (uint256)` clause gives typed access to `.result` (shares minted).
51
+ const depositResult = builder.core.call('deposit', {
52
+ resource: builder.inputs.amountIn,
53
+ bind: {
54
+ assets: builder.inputs.amountIn,
55
+ receiver: builder.context.executionAddress,
56
+ },
57
+ config: {
58
+ target: VAULT,
59
+ functionSignature:
60
+ 'function deposit(uint256 assets, address receiver) returns (uint256)',
61
+ },
62
+ });
63
+
64
+ // Graduate the raw shares handle to a tracked resource so downstream
65
+ // nodes can use it with slippage tracking and linearity checks.
66
+ builder.core.asResource('shares', {
67
+ bind: { handle: depositResult.result },
68
+ config: {
69
+ resource: resources.erc20(VAULT, 1),
70
+ },
71
+ });
72
+
73
+ const flow = builder.build();
74
+
75
+ const request = sdk.request(flow, {
76
+ signer: owner,
77
+ inputs: {
78
+ amountIn: materialisers.directDeposit({ amount }),
79
+ },
80
+ sweepTo: builder.context.sender,
81
+ });
82
+
83
+ return { flow, request };
84
+ };