@lifi/composer-sdk 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/README.md +98 -5
  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 +6 -1
  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 +5 -1
  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 +94 -58
  25. package/dist/client.cjs.map +1 -1
  26. package/dist/client.d.cts +61 -2
  27. package/dist/client.d.ts +61 -2
  28. package/dist/client.js +94 -58
  29. package/dist/client.js.map +1 -1
  30. package/dist/discovery.cjs.map +1 -1
  31. package/dist/discovery.d.cts +31 -2
  32. package/dist/discovery.d.ts +31 -2
  33. package/dist/generated/config.generated.cjs.map +1 -1
  34. package/dist/generated/config.generated.d.cts +35 -3
  35. package/dist/generated/config.generated.d.ts +35 -3
  36. package/dist/generated/operations.generated.cjs +9 -0
  37. package/dist/generated/operations.generated.cjs.map +1 -1
  38. package/dist/generated/operations.generated.d.cts +33 -2
  39. package/dist/generated/operations.generated.d.ts +33 -2
  40. package/dist/generated/operations.generated.js +9 -0
  41. package/dist/generated/operations.generated.js.map +1 -1
  42. package/dist/index.cjs +5 -1
  43. package/dist/index.cjs.map +1 -1
  44. package/dist/index.d.cts +26 -7
  45. package/dist/index.d.ts +26 -7
  46. package/dist/index.js +4 -1
  47. package/dist/index.js.map +1 -1
  48. package/dist/{raw-IxCF_bSS.d.ts → raw-BvRghN3r.d.ts} +3 -2
  49. package/dist/{raw-Dj_2rBSQ.d.cts → raw-s1rer8X3.d.cts} +3 -2
  50. package/dist/raw.cjs +2 -0
  51. package/dist/raw.cjs.map +1 -1
  52. package/dist/raw.d.cts +1 -1
  53. package/dist/raw.d.ts +1 -1
  54. package/dist/raw.js +2 -1
  55. package/dist/raw.js.map +1 -1
  56. package/dist/routeAmount-DdbJYJyM.d.cts +55 -0
  57. package/dist/routeAmount-DdbJYJyM.d.ts +55 -0
  58. package/dist/routeAmount.cjs +38 -0
  59. package/dist/routeAmount.cjs.map +1 -0
  60. package/dist/routeAmount.d.cts +2 -0
  61. package/dist/routeAmount.d.ts +2 -0
  62. package/dist/routeAmount.js +13 -0
  63. package/dist/routeAmount.js.map +1 -0
  64. package/dist/run/inputs.cjs +14 -15
  65. package/dist/run/inputs.cjs.map +1 -1
  66. package/dist/run/inputs.d.cts +8 -0
  67. package/dist/run/inputs.d.ts +8 -0
  68. package/dist/run/inputs.js +14 -15
  69. package/dist/run/inputs.js.map +1 -1
  70. package/dist/run/simulate.cjs +5 -0
  71. package/dist/run/simulate.cjs.map +1 -1
  72. package/dist/run/simulate.d.cts +5 -2
  73. package/dist/run/simulate.d.ts +5 -2
  74. package/dist/run/simulate.js +5 -0
  75. package/dist/run/simulate.js.map +1 -1
  76. package/dist/sdk.cjs +3 -0
  77. package/dist/sdk.cjs.map +1 -1
  78. package/dist/sdk.d.cts +42 -2
  79. package/dist/sdk.d.ts +42 -2
  80. package/dist/sdk.js +3 -0
  81. package/dist/sdk.js.map +1 -1
  82. package/dist/types.cjs.map +1 -1
  83. package/dist/types.d.cts +18 -2
  84. package/dist/types.d.ts +18 -2
  85. package/package.json +6 -13
  86. package/src/authoring/FlowBuilderCore.ts +13 -15
  87. package/src/authoring/handles.ts +41 -5
  88. package/src/authoring/raw.ts +39 -4
  89. package/src/authoring/signatureArgs.ts +17 -13
  90. package/src/client.ts +204 -58
  91. package/src/discovery.ts +31 -1
  92. package/src/examples/flySwap.ts +167 -0
  93. package/src/examples/lifiZap.ts +4 -0
  94. package/src/examples/lifiZapAsync.ts +88 -0
  95. package/src/examples/routeTokenPair.ts +58 -0
  96. package/src/examples/run.ts +56 -6
  97. package/src/examples/supportedChains.ts +122 -0
  98. package/src/generated/config.generated.ts +45 -3
  99. package/src/generated/operations.generated.ts +56 -8
  100. package/src/index.ts +33 -1
  101. package/src/raw.ts +1 -1
  102. package/src/routeAmount.ts +61 -0
  103. package/src/run/inputs.ts +30 -11
  104. package/src/run/simulate.ts +14 -2
  105. package/src/sdk.ts +47 -0
  106. 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
+ };
@@ -53,6 +53,10 @@ export const buildLifiZapExample = (): {
53
53
  inputs: {
54
54
  amountIn: materialisers.directDeposit({ amount: '1000000000' }),
55
55
  },
56
+ // This zap's edge keeps `amountOut` inside the execution VM, so sweeping
57
+ // leftovers back to the sender is safe. Omit `sweepTo` for an edge that
58
+ // delivers externally (`recipient: 'required'` in discovery): the output
59
+ // never enters the VM, so a sweep would find nothing to move.
56
60
  sweepTo: builder.context.sender,
57
61
  });
58
62
 
@@ -0,0 +1,88 @@
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 { API_KEY, BASE_URL } from './config.js';
7
+
8
+ const USDC = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48';
9
+ // Aave aEthUSDC receipt token on Ethereum mainnet
10
+ const A_ETH_USDC = '0x98C23E9d8f34FEFb1B7BD6a91B7FF122F4e16F5c';
11
+
12
+ export interface LifiZapAsyncInput {
13
+ readonly owner: Address;
14
+ readonly recipient: Address;
15
+ }
16
+
17
+ /**
18
+ * Zap USDC into a DeFi position whose output settles asynchronously via LI.FI.
19
+ *
20
+ * Demonstrates:
21
+ * - `lifi.zapAsync`, the future-availability sibling of `lifi.zap`
22
+ * - Binding the required `recipient` input (the async op has no optional
23
+ * recipient — the decl is non-optional, so `bind.recipient` is mandatory)
24
+ * - Omitting `sweepTo`: `availability: 'future'` governs custody, so the
25
+ * output is delivered externally and never enters the execution VM — a
26
+ * sweep would find nothing to move (custody, not amount reporting)
27
+ * - Amount reporting is unaffected by availability: when the async lowering
28
+ * binds a terminal handle the output still reports `simulated.amountOut`,
29
+ * the protocol's in-transaction expected value (an estimate; the delivered
30
+ * amount arrives after settlement). `simulated.amountOutMin` appears only
31
+ * when a minimum source exists — for a future output that means a floor the
32
+ * edge lowering itself reported, never a user guard
33
+ * - No output guards: the reported value is the initiating-transaction
34
+ * estimate, not the asynchronously delivered amount a guard could enforce,
35
+ * so `amountOut` is excluded from the generated `TypedGuard` union
36
+ * (`guards` typechecks as `TypedGuard<never>[]`) and the compiler rejects
37
+ * any guard on it
38
+ */
39
+ export const buildLifiZapAsyncExample = ({
40
+ owner,
41
+ recipient,
42
+ }: LifiZapAsyncInput): {
43
+ flow: Flow;
44
+ request: ComposeCompileRequest;
45
+ } => {
46
+ const sdk = createComposeSdk({ baseUrl: BASE_URL, apiKey: API_KEY });
47
+
48
+ // Declare the flow with a single USDC input and the recipient address input.
49
+ const builder = sdk.flow(1, {
50
+ name: 'zap-async-usdc-to-aave',
51
+ inputs: {
52
+ amountIn: resources.erc20(USDC, 1),
53
+ recipient: 'address',
54
+ },
55
+ });
56
+
57
+ // Zap USDC into an async Aave position via LI.FI.
58
+ // The future output is delivered to `recipient` once the initiating
59
+ // transaction settles; the recipient binding is required by the op.
60
+ builder.lifi.zapAsync('zap', {
61
+ bind: {
62
+ amountIn: builder.inputs.amountIn,
63
+ recipient: builder.inputs.recipient,
64
+ },
65
+ config: {
66
+ resourceOut: resources.erc20(A_ETH_USDC, 1),
67
+ },
68
+ });
69
+
70
+ const flow = builder.build();
71
+
72
+ // Build the compile request.
73
+ // directDeposit transfers a fixed amount of USDC into the VM; the recipient
74
+ // handle passes the delivery address as a plain string.
75
+ // No `sweepTo`: the async output is delivered externally to the recipient,
76
+ // never landing in the VM for a sweep to reach.
77
+ // NOTE: no production edge sets `availability: 'future'` yet, so this request
78
+ // resolves to a typed no_route today; it documents the intended shape.
79
+ const request = sdk.request(flow, {
80
+ signer: owner,
81
+ inputs: {
82
+ amountIn: materialisers.directDeposit({ amount: '1000000000' }),
83
+ recipient,
84
+ },
85
+ });
86
+
87
+ return { flow, request };
88
+ };
@@ -0,0 +1,58 @@
1
+ import type { ComposeRouteRequest } from '@lifi/compose-spec';
2
+
3
+ import { routeAmount } from '../index.js';
4
+
5
+ import { OWNER } from './config.js';
6
+
7
+ const NATIVE_ETH = '0x0000000000000000000000000000000000000000';
8
+ const WETH = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2';
9
+
10
+ /**
11
+ * Wrap 0.1 native ETH into WETH on Ethereum mainnet through
12
+ * `POST /compose/route`.
13
+ *
14
+ * Demonstrates:
15
+ * - The convenience path: a from/to token pair instead of an authored flow.
16
+ * The pair resolves to the catalog's `wrap` edge, and the server authors the
17
+ * zap step for it
18
+ * - `routeAmount.exact` for a fixed input amount (`bigint`, serialised on send)
19
+ * - The native sentinel address as `fromToken`
20
+ *
21
+ * Pure: returns a plain {@link ComposeRouteRequest}, performs no network I/O.
22
+ * Pass it to `sdk.route(...)` / `sdk.client.route(...)` to compile it.
23
+ */
24
+ export const buildRouteExactExample = (): ComposeRouteRequest => ({
25
+ chainId: 1,
26
+ fromToken: NATIVE_ETH,
27
+ toToken: WETH,
28
+ amount: routeAmount.exact(100_000_000_000_000_000n), // 0.1 ETH
29
+ signer: OWNER,
30
+ slippageBps: 100,
31
+ });
32
+
33
+ /**
34
+ * Unwrap the signer's entire WETH balance back into native ETH on Ethereum
35
+ * mainnet — the catalog's `unwrap` edge.
36
+ *
37
+ * Demonstrates:
38
+ * - `routeAmount.all` for a balance resolved on-chain at execution time, where
39
+ * the passed value is only the quoting stand-in
40
+ * - `sweepTo` naming the address that receives the output
41
+ * - `checkOnChainAllowances` so already-sufficient approvals are omitted from
42
+ * the response
43
+ *
44
+ * Pure: returns a plain {@link ComposeRouteRequest}, performs no network I/O.
45
+ */
46
+ export const buildRouteAllExample = (): ComposeRouteRequest => ({
47
+ chainId: 1,
48
+ fromToken: WETH,
49
+ toToken: NATIVE_ETH,
50
+ // Estimate of the balance on hand — the quote and the derived preconditions
51
+ // are computed against this, but the amount actually moved is the real
52
+ // balance at execution time.
53
+ amount: routeAmount.all(10n ** 18n), // ~1 WETH
54
+ signer: OWNER,
55
+ sweepTo: OWNER,
56
+ slippageBps: 50,
57
+ checkOnChainAllowances: true,
58
+ });
@@ -20,6 +20,8 @@
20
20
  * split-zap — split USDC 60/40 and zap into Aave + Morpho vaults
21
21
  * split-arithmetic — split USDC 70/30 with arithmetic assertions
22
22
  * zap — zap USDC into Aave lending position
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
23
25
  * swap-zap — swap WETH to USDC then zap into Aave
24
26
  * swap-check — swap WETH to USDC with balance check
25
27
  * swap-recipient — swap WETH + DAI to USDC and send to recipient
@@ -32,10 +34,16 @@
32
34
  * transfer — transfer full token balance to a recipient
33
35
  * partial-transfer — transfer a specific amount, keeping the remainder
34
36
  * untyped-ref — mix untypedOp with typed handles via raw.ref
37
+ * route-exact — wrap 0.1 native ETH into WETH via /compose/route
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
35
40
  */
36
- import type { ComposeCompileRequest } from '@lifi/compose-spec';
41
+ import type {
42
+ ComposeCompileRequest,
43
+ ComposeRouteRequest,
44
+ } from '@lifi/compose-spec';
37
45
 
38
- import { createComposeSdk } from '../sdk.js';
46
+ import { type ComposeSdk, createComposeSdk } from '../sdk.js';
39
47
 
40
48
  import { buildAaveRepay } from './aaveRepay.js';
41
49
  import { buildAaveRepayWithATokens } from './aaveRepayWithATokens.js';
@@ -47,12 +55,19 @@ import { buildConsolidateToEth } from './consolidateToEth.js';
47
55
  import { buildConsolidateStablesToUsdc } from './consolidateToUsdc.js';
48
56
  import { buildDepositFromProxy } from './depositFromProxy.js';
49
57
  import { buildDustSweepExample } from './dustSweep.js';
58
+ import { buildFlySwapExample } from './flySwap.js';
50
59
  import { buildLifiSwapExample } from './lifiSwap.js';
51
60
  import { buildLifiZapExample } from './lifiZap.js';
61
+ import { buildLifiZapAsyncExample } from './lifiZapAsync.js';
52
62
  import { buildRawCallWithArithmetic } from './rawCallWithArithmetic.js';
53
63
  import { buildReadContractState } from './readContractState.js';
64
+ import {
65
+ buildRouteAllExample,
66
+ buildRouteExactExample,
67
+ } from './routeTokenPair.js';
54
68
  import { buildSplitAndZapExample } from './splitAndZap.js';
55
69
  import { buildSplitWithArithmetic } from './splitWithArithmetic.js';
70
+ import { runSupportedChainsExample } from './supportedChains.js';
56
71
  import { buildSwapAndZapExample } from './swapAndZap.js';
57
72
  import { buildSwapToRecipient } from './swapToRecipient.js';
58
73
  import { buildSwapWithAllowRevertExample } from './swapWithAllowRevert.js';
@@ -106,6 +121,9 @@ const EXAMPLES: Record<string, () => ComposeCompileRequest> = {
106
121
  'split-arithmetic': () =>
107
122
  buildSplitWithArithmetic({ owner: OWNER, amount: '1000000000' }).request,
108
123
  zap: () => buildLifiZapExample().request,
124
+ 'zap-async': () =>
125
+ buildLifiZapAsyncExample({ owner: OWNER, recipient: RECIPIENT }).request,
126
+ 'fly-swap': () => buildFlySwapExample().request,
109
127
  'swap-zap': () => buildSwapAndZapExample().request,
110
128
  'swap-check': () =>
111
129
  buildSwapWithBalanceCheck({ owner: OWNER, amount: '1000000000000000000' })
@@ -146,6 +164,19 @@ const EXAMPLES: Record<string, () => ComposeCompileRequest> = {
146
164
  'untyped-ref': () => buildUntypedOpWithTypedRef({ owner: OWNER }).request,
147
165
  };
148
166
 
167
+ // `/compose/route` examples build a `ComposeRouteRequest` and go to
168
+ // `sdk.route`, not `sdk.client.compile`, so they live in their own registry.
169
+ const ROUTE_EXAMPLES: Record<string, () => ComposeRouteRequest> = {
170
+ 'route-exact': buildRouteExactExample,
171
+ 'route-all': buildRouteAllExample,
172
+ };
173
+
174
+ // Query examples read from the API instead of building a request to compile,
175
+ // so they take the sdk and print their own output.
176
+ const QUERY_EXAMPLES: Record<string, (sdk: ComposeSdk) => Promise<void>> = {
177
+ 'supported-chains': runSupportedChainsExample,
178
+ };
179
+
149
180
  const run = async () => {
150
181
  const args = process.argv.slice(2);
151
182
  const useStaged = args.includes('--staged');
@@ -166,10 +197,18 @@ const run = async () => {
166
197
  examples = { ...EXAMPLES, ...stagedModule.STAGED_EXAMPLES };
167
198
  }
168
199
 
200
+ const queryExample = name ? QUERY_EXAMPLES[name] : undefined;
201
+ const routeBuilder = name ? ROUTE_EXAMPLES[name] : undefined;
169
202
  const builder = name ? examples[name] : undefined;
170
203
 
171
- if (!builder) {
172
- const valid = Object.keys(examples).join(', ');
204
+ if (!builder && !routeBuilder && !queryExample) {
205
+ const valid = [
206
+ ...Object.keys(examples),
207
+ ...Object.keys(ROUTE_EXAMPLES),
208
+ ...Object.keys(QUERY_EXAMPLES),
209
+ ]
210
+ .sort()
211
+ .join(', ');
173
212
  console.error(
174
213
  `Error: ${
175
214
  name ? `unknown example "${name}"` : 'no example specified'
@@ -181,9 +220,18 @@ const run = async () => {
181
220
  }
182
221
  console.log(`Running example: ${name}`);
183
222
 
184
- const request = builder();
185
223
  const sdk = createComposeSdk({ baseUrl: BASE_URL, apiKey: API_KEY });
186
224
 
225
+ // Query examples do their own I/O and printing - there is no request to
226
+ // show and nothing to compile.
227
+ if (queryExample) {
228
+ console.log(`\n--- Querying ${BASE_URL} ---`);
229
+ await queryExample(sdk);
230
+ return;
231
+ }
232
+
233
+ const request = routeBuilder ? routeBuilder() : builder!();
234
+
187
235
  console.log('--- Request ---');
188
236
  console.log(
189
237
  JSON.stringify(
@@ -195,7 +243,9 @@ const run = async () => {
195
243
  );
196
244
 
197
245
  console.log(`\n--- Compiling against ${BASE_URL} ---`);
198
- const result = await sdk.client.compile(request);
246
+ const result = routeBuilder
247
+ ? await sdk.route(request as ComposeRouteRequest)
248
+ : await sdk.client.compile(request as ComposeCompileRequest);
199
249
 
200
250
  console.log('\n--- Result ---');
201
251
  console.log(JSON.stringify(result, null, 2));
@@ -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
+ };
@@ -203,9 +203,14 @@ export interface CoreCallConfig {
203
203
  )[]
204
204
  )[]
205
205
  )[];
206
- approvals?: {
207
- spender: Address;
208
- }[];
206
+ approvals?: (
207
+ | {
208
+ spender: Address;
209
+ }
210
+ | {
211
+ from: 'sender';
212
+ }
213
+ )[];
209
214
  }
210
215
 
211
216
  export interface CoreStaticCallConfig {
@@ -684,6 +689,19 @@ export interface LifiZapConfig {
684
689
  };
685
690
  }
686
691
 
692
+ export interface LifiZapAsyncConfig {
693
+ resourceOut:
694
+ | {
695
+ kind: 'native';
696
+ chainId: Positive;
697
+ }
698
+ | {
699
+ kind: 'erc20';
700
+ token: Address;
701
+ chainId: Positive;
702
+ };
703
+ }
704
+
687
705
  export interface ParaswapBuyConfig {
688
706
  resourceOut:
689
707
  | {
@@ -713,6 +731,30 @@ export interface CorePeekConfig {
713
731
  args?: string[];
714
732
  }
715
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
+
716
758
  export interface SlippageGuardConfig {
717
759
  port: string;
718
760
  bps: Between010000;