@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
@@ -0,0 +1,167 @@
1
+ import type { ComposeCompileRequest, Flow } from '@lifi/compose-spec';
2
+
3
+ import { createComposeSdk, materialisers, resources } from '../index.js';
4
+
5
+ import { API_KEY, BASE_URL, OWNER } from './config.js';
6
+
7
+ // Base mainnet. Pendle PT-USDC, its underlying USDC, and WETH.
8
+ const BASE = 8453;
9
+ const PT_USDC = '0x0d18b7bab00988a442e31065e76286844809dc9a';
10
+ const USDC = '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913';
11
+ const WETH = '0x4200000000000000000000000000000000000006';
12
+
13
+ /**
14
+ * Exit a Pendle PT position and swap **all** of the proceeds to WETH on Base.
15
+ *
16
+ * ## The problem this solves
17
+ *
18
+ * Every other swap op bakes a fixed input amount into the router calldata at
19
+ * quote time. That is fine when you know the amount up front, and useless when
20
+ * you do not: a PT exit routes through an on-chain AMM quote, so it can deliver
21
+ * a little more or a little less than the estimate. With `lifi.swap` you must
22
+ * name an amount, and whatever the exit over-delivers is stranded in the proxy
23
+ * (or, if it under-delivers, the flow reverts).
24
+ *
25
+ * `fly.swap` is the only swap that can sit downstream of a producer whose output
26
+ * is not known until execution. Fly's `DexAggregator` supports a variable-input
27
+ * mode — the `amountIn` word is excluded from the signed quote, so leaving it
28
+ * unset makes the contract resolve the amount on-chain: for an erc20 input,
29
+ * `min(our balance, the allowance we granted)`; for a native input, whatever
30
+ * `msg.value` the op forwards. The op therefore swaps *all* of whatever
31
+ * actually arrives. Either leg may be the chain's gas coin.
32
+ *
33
+ * ## The two numbers, which are independent
34
+ *
35
+ * This is the thing to get right, because they sound alike and are not:
36
+ *
37
+ * - **`inputVarianceBps`** describes the **upstream**: how far its real output
38
+ * may sit either side of the estimate, symmetrically. It is a quantity, not a
39
+ * price.
40
+ * - **`maxSlippageBps`** is ordinary **price** protection: expected rate versus
41
+ * achieved rate, sized on the pair, and completely unaffected by how much
42
+ * input turns up. It is enforced on-chain as a *rate* against the input
43
+ * actually consumed, so the percentage protection is identical at any size.
44
+ *
45
+ * Neither is derived from the other and neither constrains the other. A
46
+ * `maxSlippageBps` below `inputVarianceBps` is perfectly legitimate.
47
+ *
48
+ * ## Choosing `inputVarianceBps` (default 50 = 0.5%)
49
+ *
50
+ * The compile-time input amount is **simulated** at the current block, not
51
+ * guessed — the compiler runs the upstream and reads what it produced. So this
52
+ * field covers only simulate-to-execute drift: block delay, yield accrued in
53
+ * that window, and third parties moving the upstream's rate in between. Tens of
54
+ * basis points, not percent.
55
+ *
56
+ * Widening it is safe but not free. The bottom of the band is what the compose
57
+ * response publishes as `amountOutMin`, so a wider band advertises a weaker
58
+ * minimum to whoever consumes your quote.
59
+ *
60
+ * ## Ignore the 50% slippage in the Fly calldata
61
+ *
62
+ * If you inspect the compiled transaction you will see a 50% slippage figure
63
+ * where you configured 1%. That is deliberate and it is not your setting. All
64
+ * Fly's own `slippage` parameter does is price its `amountOutMin`, which is an
65
+ * absolute amount fixed at quote time and therefore cannot scale down when a
66
+ * legitimately smaller input arrives. The op sends a high value so that floor
67
+ * can never be the binding constraint, and enforces its own rate-based bound
68
+ * instead — which is strictly tighter for every in-band delivery. The number
69
+ * that protects you is `maxSlippageBps`.
70
+ *
71
+ * ## What happens outside the band
72
+ *
73
+ * With `E` the simulated exit output, `v = inputVarianceBps`:
74
+ *
75
+ * E = 1000, v = 200 (2%), p = 100 (1%) -> band [980, 1020]
76
+ *
77
+ * delivered 1020 -> consumes 1020, nothing left over (band top)
78
+ * delivered 1000 -> consumes 1000, nothing left over
79
+ * delivered 980 -> consumes 980, nothing left over (band bottom)
80
+ * delivered 950 -> reverts on the op's own floor invariant
81
+ * delivered 1200 -> consumes 1020; the 180 excess is swept back to you
82
+ *
83
+ * Both out-of-band outcomes are safe. The upward cap exists because Fly
84
+ * confiscates output above the quoted expectation rather than rejecting it, so
85
+ * the op never approves more than it quoted for. Note the excess only comes back
86
+ * if the run supplies `sweepTo` — without it the residue sits in the proxy until
87
+ * a later flow or an explicit `POST /compose/sweep` collects it.
88
+ *
89
+ * ## Two limitations worth knowing before you author
90
+ *
91
+ * 1. **`fly.swap` cannot sit downstream of another *prepared* op.** `lifi.zap`
92
+ * (used here) and other plain ops are fine. `lifi.swap` and `paraswap.buy`
93
+ * are prepared, and the compiler rejects those chains with a message about
94
+ * splitting the flow across a continuation.
95
+ * 2. **The upstream must deliver to the proxy.** `lifi.zap` does so by
96
+ * construction. An upstream that exposes its own delivery address — such as
97
+ * `morphoBlue.withdraw`'s `receiver` — must have it bound to
98
+ * `context.executionAddress`, or the amount it reports will diverge from what
99
+ * the proxy actually holds and the op's custody assertion reverts.
100
+ *
101
+ * Demonstrates:
102
+ * - A variable-input swap consuming an upstream's entire, runtime-determined output
103
+ * - `lifi.zap` as the nondeterministic producer (a PT exit through an AMM quote)
104
+ * - `sweepTo`, which is what returns any out-of-band residue to the sender
105
+ *
106
+ * `fly.swap` is always registered on the backend; without `FLY_API_KEY` it uses
107
+ * Fly's public host.
108
+ *
109
+ * See `docs/references/fly-api.md` for the router behaviour all of this rests on.
110
+ */
111
+ export const buildFlySwapExample = (): {
112
+ flow: Flow;
113
+ request: ComposeCompileRequest;
114
+ } => {
115
+ const sdk = createComposeSdk({ baseUrl: BASE_URL, apiKey: API_KEY });
116
+
117
+ const builder = sdk.flow(BASE, {
118
+ name: 'pt-usdc-exit-swap-all-to-weth',
119
+ inputs: {
120
+ amountIn: resources.erc20(PT_USDC, BASE),
121
+ },
122
+ });
123
+
124
+ // Exit the PT position to its underlying USDC. `lifi.zap` declares no
125
+ // output-amount estimator, so the compiler cannot resolve the next node's
126
+ // input statically and instead simulates this node to read what it really
127
+ // produced. That simulated value is what `fly.swap` quotes against, and it is
128
+ // why a 50 bps band is enough.
129
+ const exitPt = builder.lifi.zap('exitPt', {
130
+ bind: { amountIn: builder.inputs.amountIn },
131
+ config: {
132
+ resourceOut: resources.erc20(USDC, BASE),
133
+ },
134
+ });
135
+
136
+ // Swap the entire exit output to WETH.
137
+ //
138
+ // No slippage guard here, and none is accepted: `amountOut` declares
139
+ // `providesMinimum`, so the op owns its own bound. Attaching one is a
140
+ // `guard_error`. The published `amountOutMin` is the op's, derived from the
141
+ // bottom of the input band.
142
+ builder.fly.swap('swapAllToWeth', {
143
+ bind: { amountIn: exitPt.amountOut },
144
+ config: {
145
+ resourceOut: resources.erc20(WETH, BASE),
146
+ // The exit is a same-block simulation, so only drift needs covering.
147
+ inputVarianceBps: 50,
148
+ // Price protection on the USDC/WETH pair, independent of the above.
149
+ maxSlippageBps: 100,
150
+ },
151
+ });
152
+
153
+ const flow = builder.build();
154
+
155
+ const request = sdk.request(flow, {
156
+ signer: OWNER,
157
+ inputs: {
158
+ // 1 PT-USDC (6 decimals).
159
+ amountIn: materialisers.directDeposit({ amount: '1000000' }),
160
+ },
161
+ // Load-bearing, not boilerplate: this is what returns the unconsumed input
162
+ // to the sender if the exit over-delivers past the top of the band.
163
+ sweepTo: builder.context.sender,
164
+ });
165
+
166
+ return { flow, request };
167
+ };
@@ -21,6 +21,7 @@
21
21
  * split-arithmetic — split USDC 70/30 with arithmetic assertions
22
22
  * zap — zap USDC into Aave lending position
23
23
  * zap-async — zap USDC into an async Aave position (future output)
24
+ * fly-swap — exit a Pendle PT position and swap ALL proceeds to WETH
24
25
  * swap-zap — swap WETH to USDC then zap into Aave
25
26
  * swap-check — swap WETH to USDC with balance check
26
27
  * swap-recipient — swap WETH + DAI to USDC and send to recipient
@@ -35,13 +36,15 @@
35
36
  * untyped-ref — mix untypedOp with typed handles via raw.ref
36
37
  * route-exact — wrap 0.1 native ETH into WETH via /compose/route
37
38
  * route-all — unwrap the whole WETH balance to ETH via /compose/route
39
+ * supported-chains — list the chains this deployment supports, and guard on them
38
40
  */
39
41
  import type {
40
42
  ComposeCompileRequest,
41
43
  ComposeRouteRequest,
42
44
  } from '@lifi/compose-spec';
43
45
 
44
- import { createComposeSdk } from '../sdk.js';
46
+ import { bigintReplacer } from '../client.js';
47
+ import { type ComposeSdk, createComposeSdk } from '../sdk.js';
45
48
 
46
49
  import { buildAaveRepay } from './aaveRepay.js';
47
50
  import { buildAaveRepayWithATokens } from './aaveRepayWithATokens.js';
@@ -53,6 +56,7 @@ import { buildConsolidateToEth } from './consolidateToEth.js';
53
56
  import { buildConsolidateStablesToUsdc } from './consolidateToUsdc.js';
54
57
  import { buildDepositFromProxy } from './depositFromProxy.js';
55
58
  import { buildDustSweepExample } from './dustSweep.js';
59
+ import { buildFlySwapExample } from './flySwap.js';
56
60
  import { buildLifiSwapExample } from './lifiSwap.js';
57
61
  import { buildLifiZapExample } from './lifiZap.js';
58
62
  import { buildLifiZapAsyncExample } from './lifiZapAsync.js';
@@ -64,6 +68,7 @@ import {
64
68
  } from './routeTokenPair.js';
65
69
  import { buildSplitAndZapExample } from './splitAndZap.js';
66
70
  import { buildSplitWithArithmetic } from './splitWithArithmetic.js';
71
+ import { runSupportedChainsExample } from './supportedChains.js';
67
72
  import { buildSwapAndZapExample } from './swapAndZap.js';
68
73
  import { buildSwapToRecipient } from './swapToRecipient.js';
69
74
  import { buildSwapWithAllowRevertExample } from './swapWithAllowRevert.js';
@@ -72,6 +77,9 @@ import { buildSwapWithOutputValidation } from './swapWithOutputValidation.js';
72
77
  import { buildPartialTransfer, buildTransferTokens } from './transferTokens.js';
73
78
  import { buildUntypedOpWithTypedRef } from './untypedOpWithTypedRef.js';
74
79
 
80
+ const stringifyJson = (value: unknown) =>
81
+ JSON.stringify(value, bigintReplacer, 2);
82
+
75
83
  const EXAMPLES: Record<string, () => ComposeCompileRequest> = {
76
84
  'aave-repay': () =>
77
85
  buildAaveRepay({
@@ -119,6 +127,7 @@ const EXAMPLES: Record<string, () => ComposeCompileRequest> = {
119
127
  zap: () => buildLifiZapExample().request,
120
128
  'zap-async': () =>
121
129
  buildLifiZapAsyncExample({ owner: OWNER, recipient: RECIPIENT }).request,
130
+ 'fly-swap': () => buildFlySwapExample().request,
122
131
  'swap-zap': () => buildSwapAndZapExample().request,
123
132
  'swap-check': () =>
124
133
  buildSwapWithBalanceCheck({ owner: OWNER, amount: '1000000000000000000' })
@@ -166,6 +175,12 @@ const ROUTE_EXAMPLES: Record<string, () => ComposeRouteRequest> = {
166
175
  'route-all': buildRouteAllExample,
167
176
  };
168
177
 
178
+ // Query examples read from the API instead of building a request to compile,
179
+ // so they take the sdk and print their own output.
180
+ const QUERY_EXAMPLES: Record<string, (sdk: ComposeSdk) => Promise<void>> = {
181
+ 'supported-chains': runSupportedChainsExample,
182
+ };
183
+
169
184
  const run = async () => {
170
185
  const args = process.argv.slice(2);
171
186
  const useStaged = args.includes('--staged');
@@ -186,11 +201,16 @@ const run = async () => {
186
201
  examples = { ...EXAMPLES, ...stagedModule.STAGED_EXAMPLES };
187
202
  }
188
203
 
204
+ const queryExample = name ? QUERY_EXAMPLES[name] : undefined;
189
205
  const routeBuilder = name ? ROUTE_EXAMPLES[name] : undefined;
190
206
  const builder = name ? examples[name] : undefined;
191
207
 
192
- if (!builder && !routeBuilder) {
193
- const valid = [...Object.keys(examples), ...Object.keys(ROUTE_EXAMPLES)]
208
+ if (!builder && !routeBuilder && !queryExample) {
209
+ const valid = [
210
+ ...Object.keys(examples),
211
+ ...Object.keys(ROUTE_EXAMPLES),
212
+ ...Object.keys(QUERY_EXAMPLES),
213
+ ]
194
214
  .sort()
195
215
  .join(', ');
196
216
  console.error(
@@ -204,18 +224,20 @@ const run = async () => {
204
224
  }
205
225
  console.log(`Running example: ${name}`);
206
226
 
207
- const request = routeBuilder ? routeBuilder() : builder!();
208
227
  const sdk = createComposeSdk({ baseUrl: BASE_URL, apiKey: API_KEY });
209
228
 
229
+ // Query examples do their own I/O and printing - there is no request to
230
+ // show and nothing to compile.
231
+ if (queryExample) {
232
+ console.log(`\n--- Querying ${BASE_URL} ---`);
233
+ await queryExample(sdk);
234
+ return;
235
+ }
236
+
237
+ const request = routeBuilder ? routeBuilder() : builder!();
238
+
210
239
  console.log('--- Request ---');
211
- console.log(
212
- JSON.stringify(
213
- request,
214
- (_key, value: unknown) =>
215
- typeof value === 'bigint' ? value.toString() : value,
216
- 2,
217
- ),
218
- );
240
+ console.log(stringifyJson(request));
219
241
 
220
242
  console.log(`\n--- Compiling against ${BASE_URL} ---`);
221
243
  const result = routeBuilder
@@ -223,7 +245,7 @@ const run = async () => {
223
245
  : await sdk.client.compile(request as ComposeCompileRequest);
224
246
 
225
247
  console.log('\n--- Result ---');
226
- console.log(JSON.stringify(result, null, 2));
248
+ console.log(stringifyJson(result));
227
249
  };
228
250
 
229
251
  run().catch((err: unknown) => {
@@ -0,0 +1,122 @@
1
+ import type { ChainSummary } from '../discovery.js';
2
+ import type { ComposeSdk } from '../sdk.js';
3
+
4
+ /**
5
+ * The slice of {@link ComposeSdk} these helpers need. Depending on the one
6
+ * capability rather than the whole SDK keeps them testable with a two-line
7
+ * fake, and documents that nothing here touches the compile path.
8
+ */
9
+ export type SupportedChainsSource = Pick<ComposeSdk, 'getSupportedChains'>;
10
+
11
+ /**
12
+ * A memoised view of the supported-chain list.
13
+ */
14
+ export interface SupportedChainsCache {
15
+ /** Supported chains, ascending by `chainId`. Fetched once, then served from memory. */
16
+ readonly get: () => Promise<readonly ChainSummary[]>;
17
+ /**
18
+ * Resolves when `chainId` is supported, rejects with an actionable message
19
+ * naming the alternatives when it is not.
20
+ */
21
+ readonly assertSupported: (chainId: number) => Promise<void>;
22
+ /**
23
+ * Display label for `chainId`, or `undefined` when the deployment does not
24
+ * support it. Served from the same cached fetch as {@link get}.
25
+ */
26
+ readonly nameOf: (chainId: number) => Promise<string | undefined>;
27
+ /** Drops the memoised list so the next {@link get} refetches. */
28
+ readonly refresh: () => void;
29
+ }
30
+
31
+ /**
32
+ * Wraps `sdk.getSupportedChains()` in a cache so a long-lived process pays for
33
+ * one round trip instead of one per check.
34
+ *
35
+ * The SDK deliberately does not cache the list — the backend's chain registry
36
+ * refreshes periodically, so the right lifetime is the caller's to choose.
37
+ * Chains are added rarely, so caching for the life of a request handler (or
38
+ * refreshing on a timer via {@link SupportedChainsCache.refresh}) is usually
39
+ * right.
40
+ *
41
+ * Demonstrates:
42
+ * - `sdk.getSupportedChains()`, the convenience accessor over `GET /chains`
43
+ * - Preflighting a chain so an unsupported one fails locally with a message
44
+ * naming the alternatives, instead of costing a server round trip
45
+ * - Turning the per-chain `name` into display labels without a second lookup
46
+ * or a hardcoded chain-name table
47
+ * - Memoising the in-flight promise, not just the settled value, so concurrent
48
+ * first callers share one request
49
+ */
50
+ export const createSupportedChainsCache = (
51
+ sdk: SupportedChainsSource,
52
+ ): SupportedChainsCache => {
53
+ let inFlight: Promise<readonly ChainSummary[]> | undefined;
54
+
55
+ const get = (): Promise<readonly ChainSummary[]> => {
56
+ // A rejected fetch must not be memoised, or one transient network blip
57
+ // would poison every later lookup.
58
+ inFlight ??= sdk.getSupportedChains().catch((err: unknown) => {
59
+ inFlight = undefined;
60
+ throw err;
61
+ });
62
+ return inFlight;
63
+ };
64
+
65
+ const find = async (chainId: number): Promise<ChainSummary | undefined> =>
66
+ (await get()).find((chain) => chain.chainId === chainId);
67
+
68
+ return {
69
+ get,
70
+ assertSupported: async (chainId: number): Promise<void> => {
71
+ if (await find(chainId)) return;
72
+ const chains = await get();
73
+ throw new Error(
74
+ `Chain ${chainId} is not supported by this Compose deployment. ` +
75
+ `Supported chains: ${chains
76
+ .map((chain) => `${chain.name} (${chain.chainId})`)
77
+ .join(', ')}`,
78
+ );
79
+ },
80
+ nameOf: async (chainId: number): Promise<string | undefined> =>
81
+ (await find(chainId))?.name,
82
+ refresh: (): void => {
83
+ inFlight = undefined;
84
+ },
85
+ };
86
+ };
87
+
88
+ /** Ethereum mainnet — supported by every deployment. */
89
+ const MAINNET = 1;
90
+ /** Not an EVM chain ID any deployment serves; stands in for a bad input. */
91
+ const UNSUPPORTED = 999_999;
92
+
93
+ /**
94
+ * Harness entry point: prints the live chain list with names, then shows the
95
+ * guard accepting a supported chain and rejecting an unsupported one.
96
+ */
97
+ export const runSupportedChainsExample = async (
98
+ sdk: SupportedChainsSource,
99
+ ): Promise<void> => {
100
+ const chains = createSupportedChainsCache(sdk);
101
+
102
+ const supported = await chains.get();
103
+ console.log(`Supported chains (${supported.length}):`);
104
+ for (const { chainId, name } of supported) {
105
+ console.log(` ${chainId}\t${name}`);
106
+ }
107
+
108
+ await chains.assertSupported(MAINNET);
109
+ // Served from cache: no second round trip.
110
+ console.log(
111
+ `\n${await chains.nameOf(MAINNET)} (${MAINNET}) is supported — safe to build a request.`,
112
+ );
113
+
114
+ await chains
115
+ .assertSupported(UNSUPPORTED)
116
+ .then(() => {
117
+ throw new Error(`expected chain ${UNSUPPORTED} to be rejected`);
118
+ })
119
+ .catch((err: unknown) => {
120
+ console.log(`\nGuard rejected as expected: ${(err as Error).message}`);
121
+ });
122
+ };
@@ -731,6 +731,30 @@ export interface CorePeekConfig {
731
731
  args?: string[];
732
732
  }
733
733
 
734
+ /**
735
+ * a number between 0 and 3318
736
+ */
737
+ export type Between03318 = number;
738
+ /**
739
+ * a number between 0 and 5000
740
+ */
741
+ export type Between05000 = number;
742
+
743
+ export interface FlySwapConfig {
744
+ resourceOut:
745
+ | {
746
+ kind: 'native';
747
+ chainId: Positive;
748
+ }
749
+ | {
750
+ kind: 'erc20';
751
+ token: Address;
752
+ chainId: Positive;
753
+ };
754
+ inputVarianceBps?: Between03318;
755
+ maxSlippageBps?: Between05000;
756
+ }
757
+
734
758
  export interface SlippageGuardConfig {
735
759
  port: string;
736
760
  bps: Between010000;
@@ -35,6 +35,7 @@ import type {
35
35
  CoreSplitConfig,
36
36
  CoreStaticCallConfig,
37
37
  CoreTransferConfig,
38
+ FlySwapConfig,
38
39
  InvariantEqConfig,
39
40
  InvariantGtConfig,
40
41
  InvariantGteConfig,
@@ -550,6 +551,15 @@ export interface CorePeekArgs {
550
551
  readonly guards?: readonly TypedGuard<'result'>[];
551
552
  }
552
553
 
554
+ /** Swap via Fly's DexAggregator, sized at runtime: the router calldata leaves `amountIn` unset, so an upstream op's output can feed the swap without being known at plan time. Either leg may be the chain-native coin. */
555
+ export interface FlySwapArgs {
556
+ readonly bind: {
557
+ readonly amountIn: Bindable<'resource'>;
558
+ };
559
+ readonly config: FlySwapConfig;
560
+ readonly guards?: readonly TypedGuard<'unspentIn'>[];
561
+ }
562
+
553
563
  export interface CoreSplitOutputs {
554
564
  readonly a: OutputHandle<'resource'>;
555
565
  readonly b: OutputHandle<'resource'>;
@@ -651,6 +661,11 @@ export interface CorePeekOutputs {
651
661
  readonly result: OutputHandle<'uint256'>;
652
662
  }
653
663
 
664
+ export interface FlySwapOutputs {
665
+ readonly amountOut: OutputHandle<'resource'>;
666
+ readonly unspentIn: OutputHandle<'resource'>;
667
+ }
668
+
654
669
  export type GeneratedOps = {
655
670
  readonly core: {
656
671
  /** Split a resource into two outputs by basis-point ratio — original id: "core.split" */
@@ -858,6 +873,10 @@ export type GeneratedOps = {
858
873
  /** Exact-output (BUY) swap via the Paraswap/Velora Market API: produce exactly `exactAmountOut` of the destination token, pulling only the source needed and exposing the genuinely-unspent source as `unspentIn`. — original id: "paraswap.buy" */
859
874
  readonly buy: (nodeId: string, args: ParaswapBuyArgs) => ParaswapBuyOutputs;
860
875
  };
876
+ readonly fly: {
877
+ /** Swap via Fly's DexAggregator, sized at runtime: the router calldata leaves `amountIn` unset, so an upstream op's output can feed the swap without being known at plan time. Either leg may be the chain-native coin. — original id: "fly.swap" */
878
+ readonly swap: (nodeId: string, args: FlySwapArgs) => FlySwapOutputs;
879
+ };
861
880
  };
862
881
 
863
882
  export const bindGeneratedOps = (
@@ -1100,4 +1119,11 @@ export const bindGeneratedOps = (
1100
1119
  unspentIn: 'resource',
1101
1120
  } as const),
1102
1121
  },
1122
+ fly: {
1123
+ swap: (nodeId: string, args: FlySwapArgs) =>
1124
+ builder.call(nodeId, 'fly.swap', args, {
1125
+ amountOut: 'resource',
1126
+ unspentIn: 'resource',
1127
+ } as const),
1128
+ },
1103
1129
  });
package/src/index.ts CHANGED
@@ -19,9 +19,11 @@ export type {
19
19
  OutputKind,
20
20
  TypedGuard,
21
21
  TypedRef,
22
+ TypedLiteral,
22
23
  } from './types.js';
23
24
 
24
25
  export type {
26
+ ChainSummary,
25
27
  GetZapPacksOptions,
26
28
  ZapPackEdge,
27
29
  ZapPackOverview,
@@ -29,6 +31,8 @@ export type {
29
31
  export type {
30
32
  ComposeError,
31
33
  ComposeErrorCode,
34
+ ComposeSdkOutdated,
35
+ ComposeServerOutdated,
32
36
  ComposePreparationOps,
33
37
  } from './errors.js';
34
38
  export type {
@@ -36,10 +40,13 @@ export type {
36
40
  ApprovalEntry,
37
41
  Call,
38
42
  ComposeCompilePartialData,
43
+ ComposeAction,
44
+ ComposeFill,
45
+ ComposeFee,
46
+ ComposeUserProgramSimulation,
39
47
  ComposeCompileRequest,
40
48
  ComposeCompileResult,
41
49
  ComposeCompileSuccessData,
42
- ComposeContinuation,
43
50
  ComposeErrorKind,
44
51
  ComposeRouteRequest,
45
52
  ComposeTransactionRequest,
@@ -51,11 +58,13 @@ export type {
51
58
  Precondition,
52
59
  PreconditionConfigOf,
53
60
  PriceImpact,
61
+ ProducedResource,
54
62
  Ref,
55
63
  Resource,
56
64
  RouteAmount,
57
65
  RouteAmountAll,
58
66
  RouteAmountExact,
67
+ SimulatedAmounts,
59
68
  SimulationPolicy,
60
69
  SimulationRevert,
61
70
  SolType,
@@ -83,10 +92,17 @@ export {
83
92
  SIMULATE_MAX_TRACKED_BALANCES,
84
93
  SIMULATE_MAX_REQUIREMENTS,
85
94
  } from '@lifi/compose-spec';
86
- export { isComposeError, isComposePreparationError } from './errors.js';
95
+ export {
96
+ isComposeError,
97
+ isComposePreparationError,
98
+ isComposeSdkOutdatedError,
99
+ isComposeServerOutdatedError,
100
+ } from './errors.js';
87
101
 
88
102
  export type {
89
103
  Bindable,
104
+ RefBindable,
105
+ AnyRefBindable,
90
106
  InputHandle,
91
107
  ResourceInputHandle,
92
108
  OutputHandle,
package/src/raw.ts CHANGED
@@ -6,5 +6,5 @@
6
6
  * For most use cases, prefer the generated operation methods on {@link FlowBuilder}
7
7
  * (e.g. `builder.lifi.swap(...)`) and the typed guard/materialiser helpers.
8
8
  */
9
- export { rawGuard as guard, ref } from './authoring/raw.js';
9
+ export { rawGuard as guard, literal, ref } from './authoring/raw.js';
10
10
  export { materialiser } from './run/inputs.js';
@@ -131,6 +131,9 @@ export interface ServerErrorBody {
131
131
  readonly kind?: string;
132
132
  readonly message?: string;
133
133
  readonly path?: string;
134
+ readonly sdkVersion?: string;
135
+ readonly serverVersion?: string;
136
+ readonly minimumSdkVersion?: string;
134
137
  readonly details?: SimulationRevert;
135
138
  readonly failedOps?: readonly ServerFailedPreparedOp[];
136
139
  readonly succeededOps?: readonly string[];
@@ -158,6 +161,9 @@ const serverErrorBodySchema = z.object({
158
161
  kind: z.string().optional(),
159
162
  message: z.string().optional(),
160
163
  path: z.string().optional(),
164
+ sdkVersion: z.string().optional(),
165
+ serverVersion: z.string().optional(),
166
+ minimumSdkVersion: z.string().optional(),
161
167
  details: z.custom<SimulationRevert>().optional(),
162
168
  failedOps: z.array(failedPreparedOpSchema).readonly().optional(),
163
169
  succeededOps: z.array(z.string()).readonly().optional(),
package/src/run/inputs.ts CHANGED
@@ -54,6 +54,13 @@ export interface ComposeRunInput<T extends InputSchema = InputSchema> {
54
54
  * result with the transaction and revert diagnostics.
55
55
  */
56
56
  readonly simulationPolicy?: SimulationPolicy;
57
+ /**
58
+ * When `true`, the server also simulates the final user-facing transaction
59
+ * after the structured simulation succeeds; reverts honour
60
+ * `simulationPolicy`. On success the result is echoed back on the response as
61
+ * `userProgramSimulation`. Defaults to `false`.
62
+ */
63
+ readonly simulateUserProgram?: boolean;
57
64
  /**
58
65
  * When `true`, the server checks on-chain ERC-20 allowances and omits
59
66
  * approvals that are already sufficient. Defaults to `false`.
@@ -89,6 +96,21 @@ export const materialiser = (
89
96
  ...config,
90
97
  });
91
98
 
99
+ /**
100
+ * Drops the keys whose value is `undefined`, so optional fields are omitted
101
+ * from the wire payload rather than serialized as `null`.
102
+ *
103
+ * A local copy of `pickDefined` from `@lifi/utils`: this package is published
104
+ * to npm and `@lifi/utils` is a private workspace package, so it cannot be a
105
+ * runtime dependency here.
106
+ */
107
+ const pickDefined = <S extends Record<string, unknown>>(
108
+ obj: S,
109
+ ): Partial<{ [K in keyof S]: Exclude<S[K], undefined> }> =>
110
+ Object.fromEntries(
111
+ Object.entries(obj).filter(([, value]) => value !== undefined),
112
+ ) as Partial<{ [K in keyof S]: Exclude<S[K], undefined> }>;
113
+
92
114
  /**
93
115
  * Assembles a {@link ComposeRun} object from individual parameters.
94
116
  * Used internally by the SDK; prefer {@link ComposeRunInput} for external use.
@@ -105,24 +127,21 @@ export const buildRun = (run: {
105
127
  maxPriceImpactBps?: number;
106
128
  sweepTo?: SweepTo;
107
129
  simulationPolicy?: SimulationPolicy;
130
+ simulateUserProgram?: boolean;
108
131
  checkOnChainAllowances?: boolean;
109
132
  }): ComposeRun => ({
110
133
  inputs: run.inputs,
111
- ...(run.preconditions?.length && { preconditions: run.preconditions }),
112
134
  signer: run.signer,
113
- ...(run.assumptions && { assumptions: run.assumptions }),
114
- ...(run.referrer !== undefined && { referrer: run.referrer }),
115
- ...(run.integratorFeeBps !== undefined && {
135
+ ...pickDefined({
136
+ // An empty precondition list is the same as none, so drop it too.
137
+ preconditions: run.preconditions?.length ? run.preconditions : undefined,
138
+ assumptions: run.assumptions,
139
+ referrer: run.referrer,
116
140
  integratorFeeBps: run.integratorFeeBps,
117
- }),
118
- ...(run.maxPriceImpactBps !== undefined && {
119
141
  maxPriceImpactBps: run.maxPriceImpactBps,
120
- }),
121
- ...(run.sweepTo !== undefined && { sweepTo: run.sweepTo }),
122
- ...(run.simulationPolicy !== undefined && {
142
+ sweepTo: run.sweepTo,
123
143
  simulationPolicy: run.simulationPolicy,
124
- }),
125
- ...(run.checkOnChainAllowances !== undefined && {
144
+ simulateUserProgram: run.simulateUserProgram,
126
145
  checkOnChainAllowances: run.checkOnChainAllowances,
127
146
  }),
128
147
  });