@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.
- package/README.md +98 -5
- package/dist/authoring/FlowBuilderCore.cjs +1 -5
- package/dist/authoring/FlowBuilderCore.cjs.map +1 -1
- package/dist/authoring/FlowBuilderCore.d.cts +9 -7
- package/dist/authoring/FlowBuilderCore.d.ts +9 -7
- package/dist/authoring/FlowBuilderCore.js +2 -6
- package/dist/authoring/FlowBuilderCore.js.map +1 -1
- package/dist/authoring/handles.cjs +5 -2
- package/dist/authoring/handles.cjs.map +1 -1
- package/dist/authoring/handles.d.cts +25 -6
- package/dist/authoring/handles.d.ts +25 -6
- package/dist/authoring/handles.js +3 -1
- package/dist/authoring/handles.js.map +1 -1
- package/dist/authoring/raw.cjs +6 -1
- package/dist/authoring/raw.cjs.map +1 -1
- package/dist/authoring/raw.d.cts +33 -3
- package/dist/authoring/raw.d.ts +33 -3
- package/dist/authoring/raw.js +5 -1
- package/dist/authoring/raw.js.map +1 -1
- package/dist/authoring/signatureArgs.cjs.map +1 -1
- package/dist/authoring/signatureArgs.d.cts +16 -12
- package/dist/authoring/signatureArgs.d.ts +16 -12
- package/dist/authoring/signatureArgs.js.map +1 -1
- package/dist/client.cjs +94 -58
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +61 -2
- package/dist/client.d.ts +61 -2
- package/dist/client.js +94 -58
- package/dist/client.js.map +1 -1
- package/dist/discovery.cjs.map +1 -1
- package/dist/discovery.d.cts +31 -2
- package/dist/discovery.d.ts +31 -2
- package/dist/generated/config.generated.cjs.map +1 -1
- package/dist/generated/config.generated.d.cts +35 -3
- package/dist/generated/config.generated.d.ts +35 -3
- package/dist/generated/operations.generated.cjs +9 -0
- package/dist/generated/operations.generated.cjs.map +1 -1
- package/dist/generated/operations.generated.d.cts +33 -2
- package/dist/generated/operations.generated.d.ts +33 -2
- package/dist/generated/operations.generated.js +9 -0
- package/dist/generated/operations.generated.js.map +1 -1
- package/dist/index.cjs +5 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +26 -7
- package/dist/index.d.ts +26 -7
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/{raw-IxCF_bSS.d.ts → raw-BvRghN3r.d.ts} +3 -2
- package/dist/{raw-Dj_2rBSQ.d.cts → raw-s1rer8X3.d.cts} +3 -2
- package/dist/raw.cjs +2 -0
- package/dist/raw.cjs.map +1 -1
- package/dist/raw.d.cts +1 -1
- package/dist/raw.d.ts +1 -1
- package/dist/raw.js +2 -1
- package/dist/raw.js.map +1 -1
- package/dist/routeAmount-DdbJYJyM.d.cts +55 -0
- package/dist/routeAmount-DdbJYJyM.d.ts +55 -0
- package/dist/routeAmount.cjs +38 -0
- package/dist/routeAmount.cjs.map +1 -0
- package/dist/routeAmount.d.cts +2 -0
- package/dist/routeAmount.d.ts +2 -0
- package/dist/routeAmount.js +13 -0
- package/dist/routeAmount.js.map +1 -0
- package/dist/run/inputs.cjs +14 -15
- package/dist/run/inputs.cjs.map +1 -1
- package/dist/run/inputs.d.cts +8 -0
- package/dist/run/inputs.d.ts +8 -0
- package/dist/run/inputs.js +14 -15
- package/dist/run/inputs.js.map +1 -1
- package/dist/run/simulate.cjs +5 -0
- package/dist/run/simulate.cjs.map +1 -1
- package/dist/run/simulate.d.cts +5 -2
- package/dist/run/simulate.d.ts +5 -2
- package/dist/run/simulate.js +5 -0
- package/dist/run/simulate.js.map +1 -1
- package/dist/sdk.cjs +3 -0
- package/dist/sdk.cjs.map +1 -1
- package/dist/sdk.d.cts +42 -2
- package/dist/sdk.d.ts +42 -2
- package/dist/sdk.js +3 -0
- package/dist/sdk.js.map +1 -1
- package/dist/types.cjs.map +1 -1
- package/dist/types.d.cts +18 -2
- package/dist/types.d.ts +18 -2
- package/package.json +6 -13
- package/src/authoring/FlowBuilderCore.ts +13 -15
- package/src/authoring/handles.ts +41 -5
- package/src/authoring/raw.ts +39 -4
- package/src/authoring/signatureArgs.ts +17 -13
- package/src/client.ts +204 -58
- package/src/discovery.ts +31 -1
- package/src/examples/flySwap.ts +167 -0
- package/src/examples/lifiZap.ts +4 -0
- package/src/examples/lifiZapAsync.ts +88 -0
- package/src/examples/routeTokenPair.ts +58 -0
- package/src/examples/run.ts +56 -6
- package/src/examples/supportedChains.ts +122 -0
- package/src/generated/config.generated.ts +45 -3
- package/src/generated/operations.generated.ts +56 -8
- package/src/index.ts +33 -1
- package/src/raw.ts +1 -1
- package/src/routeAmount.ts +61 -0
- package/src/run/inputs.ts +30 -11
- package/src/run/simulate.ts +14 -2
- package/src/sdk.ts +47 -0
- 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
|
+
};
|
package/src/examples/lifiZap.ts
CHANGED
|
@@ -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
|
+
});
|
package/src/examples/run.ts
CHANGED
|
@@ -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 {
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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;
|