@lifi/composer-sdk 0.6.1 → 0.6.3
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 +213 -189
- package/dist/client.cjs +1 -1
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +7 -7
- package/dist/client.d.ts +7 -7
- package/dist/client.js +1 -1
- package/dist/client.js.map +1 -1
- package/dist/errors.cjs +20 -1
- package/dist/errors.cjs.map +1 -1
- package/dist/errors.d.cts +18 -1
- package/dist/errors.d.ts +18 -1
- package/dist/errors.js +20 -1
- package/dist/errors.js.map +1 -1
- package/dist/index.cjs +3 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/reachability.cjs +123 -0
- package/dist/reachability.cjs.map +1 -0
- package/dist/reachability.d.cts +71 -0
- package/dist/reachability.d.ts +71 -0
- package/dist/reachability.js +99 -0
- package/dist/reachability.js.map +1 -0
- package/dist/responseSchemas.cjs +5 -1
- package/dist/responseSchemas.cjs.map +1 -1
- package/dist/responseSchemas.d.cts +4 -0
- package/dist/responseSchemas.d.ts +4 -0
- package/dist/responseSchemas.js +5 -1
- package/dist/responseSchemas.js.map +1 -1
- package/dist/sdk.cjs.map +1 -1
- package/dist/sdk.d.cts +5 -6
- package/dist/sdk.d.ts +5 -6
- package/dist/sdk.js.map +1 -1
- package/package.json +3 -2
- package/src/client.ts +7 -7
- package/src/errors.ts +22 -1
- package/src/index.ts +7 -0
- package/src/reachability.ts +203 -0
- package/src/responseSchemas.ts +11 -1
- package/src/sdk.ts +5 -6
package/README.md
CHANGED
|
@@ -1,34 +1,22 @@
|
|
|
1
1
|
# @lifi/composer-sdk
|
|
2
2
|
|
|
3
|
-
TypeScript SDK for building
|
|
3
|
+
TypeScript SDK for building LI.FI Compose flows and compiling them into submit-ready transactions.
|
|
4
|
+
|
|
5
|
+
Full documentation: [docs.li.fi/composer](https://docs.li.fi/composer/overview). SDK API reference: [SDK API](https://docs.li.fi/composer/composer-api/reference/sdk-api.md).
|
|
4
6
|
|
|
5
7
|
## Install
|
|
6
8
|
|
|
7
|
-
`@lifi/compose-spec` is a peer dependency
|
|
9
|
+
`@lifi/compose-spec` is a peer dependency. Install it at exactly the same version as the SDK; the two are versioned in lockstep. Node.js 24 or later is required.
|
|
8
10
|
|
|
9
11
|
```bash
|
|
10
12
|
npm install @lifi/composer-sdk @lifi/compose-spec
|
|
11
13
|
```
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
**Breaking:** `apiKey` is now required on `createComposeSdk()`.
|
|
16
|
-
|
|
17
|
-
The Compose API has always rejected unauthenticated requests, but `apiKey` was typed as optional, so omitting it compiled and only failed on the first request. It is now a required `string`, and a missing or blank value throws a `ComposeError` (`VALIDATION_ERROR`) at construction time — including for plain-JavaScript callers that bypass the type check.
|
|
18
|
-
|
|
19
|
-
```diff
|
|
20
|
-
-const sdk = createComposeSdk({ baseUrl: 'https://composer.li.quest' });
|
|
21
|
-
+const sdk = createComposeSdk({
|
|
22
|
-
+ baseUrl: 'https://composer.li.quest',
|
|
23
|
-
+ apiKey: process.env.LIFI_API_KEY!,
|
|
24
|
-
+});
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
Create a key at [portal.li.fi](https://portal.li.fi).
|
|
15
|
+
`apiKey` is required (create one at [portal.li.fi](https://portal.li.fi)). A missing or blank key throws a `ComposeError` with code `VALIDATION_ERROR` when you call `createComposeSdk()`, including from plain JavaScript that skips the type check. It has been required since 0.3.0.
|
|
28
16
|
|
|
29
17
|
## Quick start
|
|
30
18
|
|
|
31
|
-
Swap WETH to USDC, then zap the USDC into an Aave lending position
|
|
19
|
+
Swap WETH to USDC, then zap the USDC into an Aave lending position, in one transaction.
|
|
32
20
|
|
|
33
21
|
```ts
|
|
34
22
|
import {
|
|
@@ -43,13 +31,12 @@ const USDC = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48';
|
|
|
43
31
|
const A_ETH_USDC = '0x98C23E9d8f34FEFb1B7BD6a91B7FF122F4e16F5c'; // Aave aEthUSDC
|
|
44
32
|
const OWNER = '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045';
|
|
45
33
|
|
|
46
|
-
// Create the SDK pointed at the Compose API.
|
|
47
34
|
const sdk = createComposeSdk({
|
|
48
35
|
baseUrl: 'https://composer.li.quest',
|
|
49
|
-
apiKey: process.env.LIFI_API_KEY!,
|
|
36
|
+
apiKey: process.env.LIFI_API_KEY!,
|
|
50
37
|
});
|
|
51
38
|
|
|
52
|
-
//
|
|
39
|
+
// A flow on Ethereum mainnet with one token input.
|
|
53
40
|
const builder = sdk.flow(1, {
|
|
54
41
|
name: 'swap-and-zap-weth-to-aave',
|
|
55
42
|
inputs: {
|
|
@@ -57,7 +44,7 @@ const builder = sdk.flow(1, {
|
|
|
57
44
|
},
|
|
58
45
|
});
|
|
59
46
|
|
|
60
|
-
// Step 1:
|
|
47
|
+
// Step 1: swap WETH to USDC via LI.FI.
|
|
61
48
|
const swapOutputs = builder.lifi.swap('swap', {
|
|
62
49
|
bind: { amountIn: builder.inputs.amountIn },
|
|
63
50
|
config: {
|
|
@@ -66,8 +53,8 @@ const swapOutputs = builder.lifi.swap('swap', {
|
|
|
66
53
|
},
|
|
67
54
|
});
|
|
68
55
|
|
|
69
|
-
// Step 2:
|
|
70
|
-
//
|
|
56
|
+
// Step 2: zap the swapped USDC into Aave. The swap's amountOut handle
|
|
57
|
+
// feeds the zap's amountIn directly.
|
|
71
58
|
builder.lifi.zap('zap', {
|
|
72
59
|
bind: { amountIn: swapOutputs.amountOut },
|
|
73
60
|
config: {
|
|
@@ -78,7 +65,6 @@ builder.lifi.zap('zap', {
|
|
|
78
65
|
|
|
79
66
|
const flow = builder.build();
|
|
80
67
|
|
|
81
|
-
// Compile the flow into transaction calldata.
|
|
82
68
|
const request = sdk.request(flow, {
|
|
83
69
|
signer: OWNER,
|
|
84
70
|
inputs: {
|
|
@@ -87,87 +73,108 @@ const request = sdk.request(flow, {
|
|
|
87
73
|
}),
|
|
88
74
|
},
|
|
89
75
|
sweepTo: builder.context.sender,
|
|
90
|
-
// Opt into partial results.
|
|
91
|
-
// throws a ComposeError
|
|
92
|
-
// and the `partial` branch below is never reached.
|
|
76
|
+
// Opt into partial results. Under the default 'strict' policy a simulated
|
|
77
|
+
// revert throws a ComposeError instead, and the `partial` branch is unreachable.
|
|
93
78
|
simulationPolicy: 'allow-revert',
|
|
94
79
|
});
|
|
95
80
|
|
|
96
81
|
const result = await sdk.client.compile(request);
|
|
97
82
|
|
|
98
83
|
if (result.status === 'success') {
|
|
99
|
-
//
|
|
84
|
+
// transactionRequest includes gasLimit.
|
|
100
85
|
console.log(result.transactionRequest);
|
|
101
86
|
|
|
102
|
-
//
|
|
103
|
-
// terminal
|
|
104
|
-
// delivered to; the swap's output is `role: 'consumed'` by the zap.
|
|
87
|
+
// One entry per output port. Nothing downstream spends the zap's output, so
|
|
88
|
+
// it is terminal and carries its owner. The swap's output is `consumed` by the zap.
|
|
105
89
|
const position = result.outputs['zap.amountOut'];
|
|
106
90
|
if (position?.role === 'terminal') {
|
|
107
91
|
console.log(position.owner, position.amount.estimate?.value);
|
|
108
92
|
}
|
|
109
93
|
} else {
|
|
110
|
-
//
|
|
111
|
-
//
|
|
94
|
+
// status === 'partial': simulation reverted. The transaction is still
|
|
95
|
+
// returned, without gasLimit.
|
|
112
96
|
console.log(result.simulationRevert);
|
|
113
97
|
}
|
|
114
98
|
```
|
|
115
99
|
|
|
100
|
+
`builder.compile(run)` is shorthand for `sdk.client.compile(sdk.request(builder.build(), run))`.
|
|
101
|
+
|
|
116
102
|
## Core concepts
|
|
117
103
|
|
|
118
|
-
**Flows and operations
|
|
104
|
+
**Flows and operations**: a flow is an ordered list of on-chain operations. You declare inputs, chain operations, and the backend compiles everything into one transaction. Operations are namespaced, e.g. `builder.lifi.swap` and `builder.core.split`. Each call takes a node id that must be unique in the flow.
|
|
119
105
|
|
|
120
|
-
**Resources
|
|
106
|
+
**Resources**: `resources.erc20(address, chainId)` and `resources.native(chainId)` describe the tokens flowing through the flow. A resource input must be on the flow's chain.
|
|
121
107
|
|
|
122
|
-
**Handles
|
|
108
|
+
**Handles**: operations return typed output handles, e.g. `OutputHandle<'resource'>` or `OutputHandle<'uint256'>`, which you bind to downstream inputs. TypeScript enforces the port types. A resource handle fits a `uint256` slot (resources are amounts), but an `address` handle does not.
|
|
123
109
|
|
|
124
|
-
**Runtime inputs (materialisers)
|
|
110
|
+
**Runtime inputs (materialisers)**: a materialiser tells the backend how to fund a flow input when the transaction executes.
|
|
125
111
|
|
|
126
|
-
|
|
112
|
+
- `directDeposit({ amount })` pulls exactly `amount`.
|
|
113
|
+
- `directDeposit({ amount, allowNonExact: true })` permits a capped ERC-20 deposit of up to `amount`; `directDeposit({ allowNonExact: true })` deposits with no on-chain cap (ERC-20 only).
|
|
114
|
+
- `balanceOf({ owner })` reads a current balance.
|
|
115
|
+
- `call({ target, calldata })` measures the balance delta after an arbitrary contract call.
|
|
127
116
|
|
|
128
|
-
**
|
|
117
|
+
**Preconditions**: on-chain state the backend assumes when it simulates, e.g. a balance the signer will hold at execution time. The factories are `erc20Balance`, `nativeBalance`, and `erc20Allowance`.
|
|
129
118
|
|
|
130
|
-
**
|
|
119
|
+
**Guards**: runtime checks on an operation's output, attached through its `guards` field. `guards.slippage({ port, bps })` floors an output at `bps` below its simulated amount. The `port` argument is typed against the ports a guard can protect. For example, `lifi.swap`'s `amountOut` already carries the aggregator's own floor, so it rejects a slippage guard at compile time.
|
|
131
120
|
|
|
132
|
-
|
|
121
|
+
**Outputs**: a compile result carries `outputs`, keyed `'<nodeId>.<portName>'`, with one entry per resource output. An entry is either:
|
|
122
|
+
|
|
123
|
+
- `role: 'terminal'`: delivered to `owner`. `delivery.when` says whether it lands with this transaction (`'now'`) or later (`'future'`).
|
|
124
|
+
- `role: 'consumed'`: spent by the node named in `consumedBy`.
|
|
125
|
+
|
|
126
|
+
Narrow on `role` before reading those fields. `amount.estimate` and `amount.minimum` are both optional. An absent minimum means no floor is enforced; there is no `0` default. `terminalOutputs(result.outputs)` keeps only the terminal entries.
|
|
133
127
|
|
|
134
|
-
|
|
128
|
+
## API surface
|
|
135
129
|
|
|
136
|
-
|
|
130
|
+
**SDK**
|
|
137
131
|
|
|
138
|
-
|
|
132
|
+
- `createComposeSdk({ baseUrl, apiKey, fetch? })`: `baseUrl` must be an HTTP(S) URL. A trailing slash is stripped.
|
|
133
|
+
- `sdk.flow(chainId, { inputs, name? })`: returns a `FlowBuilder`. `name` defaults to a random UUID.
|
|
134
|
+
- `builder.<namespace>.<operation>(nodeId, { bind, config, guards? })`: adds a node and returns one typed handle per output port.
|
|
135
|
+
- `builder.untypedOp(nodeId, op, { bind, config, guards? })`: escape hatch for ops without a typed method. Returns `void`; reference its outputs with `raw.ref<T>(path)`.
|
|
136
|
+
- `builder.build()`: returns the `Flow` document.
|
|
137
|
+
- `builder.compile(run)`: builds, then compiles.
|
|
138
|
+
- `sdk.request(flow, run)`: builds a `ComposeCompileRequest` from a flow and a `ComposeRunInput`.
|
|
139
|
+
- `sdk.route(request)`: compiles a token pair without authoring a flow. See [Routing a token pair](#routing-a-token-pair).
|
|
140
|
+
- `sdk.simulate(request)`: simulates a raw transaction. See [Simulating a raw transaction](#simulating-a-raw-transaction).
|
|
141
|
+
- `sdk.getSupportedChains()`: same as `sdk.client.getChains()`.
|
|
139
142
|
|
|
140
|
-
|
|
141
|
-
- `builder.<namespace>.<operation>(id, { bind, config })` — adds an operation, returns typed `OutputHandle<T>` per port
|
|
142
|
-
- `builder.untypedOp(id, op, args)` — escape hatch for operations not in the manifest (returns `void`; use `raw.ref<T>()` to reference its outputs)
|
|
143
|
-
- `builder.build()` — produces a `Flow` document
|
|
144
|
-
- `sdk.request(flow, { signer, inputs, preconditions, sweepTo, ... })` — builds a compile request
|
|
143
|
+
**`sdk.client` (HTTP)**
|
|
145
144
|
|
|
146
|
-
|
|
145
|
+
- `compile(request)`: `POST /compose`. Returns a `ComposeCompileResult` with `status: 'success'` or `status: 'partial'`.
|
|
146
|
+
- `route(request)`: `POST /compose/route`. Returns the same union.
|
|
147
|
+
- `getManifest()`: `GET /compose/manifest`, the live operation registry.
|
|
148
|
+
- `getChains()`: `GET /chains`. Returns `readonly ChainSummary[]` (`{ chainId, name }`), ascending by `chainId`.
|
|
149
|
+
- A chain is listed once the VM contract is confirmed deployed on it. `compile`, `route`, and `simulate` validate against the same condition, so they reject an unlisted chain. A listed chain can still have no routing edge for a given pair.
|
|
150
|
+
- `name` is a display label that can change, so key on `chainId`. A chain with no configured label reports `Chain <id>`.
|
|
151
|
+
- `getZapPacks(options?)`: `GET /compose/zap-packs`. Returns the routing edges grouped by protocol as `ZapPackOverview[]`. Filter with `{ protocols: 'aave' }` or an array. See [Discovering routable pairs](#discovering-routable-pairs).
|
|
152
|
+
- `simulate(request)`: `POST /simulate`.
|
|
147
153
|
|
|
148
|
-
|
|
149
|
-
- `sdk.client.getManifest()` — fetches the operation manifest
|
|
150
|
-
- `sdk.client.getChains()` / `sdk.getSupportedChains()` — fetches the chains this deployment supports as `readonly ChainSummary[]` (`{ chainId, name }`), ascending by `chainId`. A chain is listed once the VM contract is confirmed deployed on it, which is the same condition `compile`, `route` and `simulate` validate against — so a chain missing from this list is rejected by those methods. The converse does not hold: a listed chain can still have no routing edge for a given pair, reported by `route` as `no_route_error`. `name` is a display label maintained by the deployment, so render it but key on `chainId`; a chain with no configured label reports `Chain <id>`. The list tracks the backend's chain registry, which refreshes periodically, and is not cached by the SDK.
|
|
151
|
-
- `sdk.client.getZapPacks(options?)` — fetches the available routing edges grouped by protocol, returning `ZapPackOverview[]`. The catalog is dynamic and is not cached by the SDK. Filter through `GetZapPacksOptions`. An edge with `recipient: 'required'` requires the zap's `recipient` binding, delivers the output externally, and produces an output that cannot be chained or swept. An edge with `availability: 'future'` produces an output that settles only after the initiating transaction; `route` compiles it for you, and on the authoring path it needs `lifi.zapAsync` with a bound `recipient`. Delivery timing governs custody only — a future output still reports an `amount.estimate` (`basis: 'simulated'`, an in-transaction estimate; the delivered amount arrives after settlement), with an `amount.minimum` present only when a minimum source applied. The catalog publishes no settlement duration: a deferred output's ETA rides on the compile result as each future terminal output's `delivery.etaSeconds` in `outputs` (expected seconds until the proceeds land, present only on a `delivery.when: 'future'` output and only when the protocol publishes an estimate; absence means unknown, not zero).
|
|
152
|
-
- `sdk.client.simulate(request)` / `sdk.simulate(request)` — simulates a raw, pre-encoded transaction and returns a `SimulateResult` (discriminated union: `status: 'ok' | 'revert' | 'error'`). See [Simulating a raw transaction](#simulating-a-raw-transaction).
|
|
153
|
-
- `sdk.client.route(request)` / `sdk.route(request)` — compiles a single from/to token pair on one chain via `POST /compose/route`, returning the same `ComposeCompileResult` as `compile()`. See [Routing a token pair](#routing-a-token-pair).
|
|
154
|
+
The SDK caches none of these responses.
|
|
154
155
|
|
|
155
156
|
**Helpers**
|
|
156
157
|
|
|
157
|
-
- `resources.erc20(address, chainId)` / `resources.native(chainId)
|
|
158
|
-
- `guards
|
|
159
|
-
- `materialisers
|
|
160
|
-
- `preconditions
|
|
161
|
-
- `raw.ref<T>(path)`
|
|
162
|
-
- `
|
|
163
|
-
- `
|
|
164
|
-
- `
|
|
158
|
+
- `resources.erc20(address, chainId)` / `resources.native(chainId)`.
|
|
159
|
+
- `guards.slippage`, `guards.coreNumericInvariant`.
|
|
160
|
+
- `materialisers.directDeposit`, `materialisers.balanceOf`, `materialisers.call`.
|
|
161
|
+
- `preconditions.erc20Balance`, `preconditions.nativeBalance`, `preconditions.erc20Allowance`.
|
|
162
|
+
- `raw.ref<T>(path)`, `raw.literal(kind, value)`, `raw.guard(kind, config?)`, `raw.materialiser(kind, config?)`: low-level constructors for cases the typed helpers do not cover.
|
|
163
|
+
- `routeAmount.exact(amount)` / `routeAmount.all(simAmount)`: amount constructors for `route`.
|
|
164
|
+
- `buildReachability(packs, { maxHops?, allowAsyncRoutes? })`: the pairs `route` can connect (pure, no I/O).
|
|
165
|
+
- `buildSimulateRequest(...)`: turns a compile result into a `SimulateRequest` (pure, no I/O).
|
|
166
|
+
- `terminalOutputs(outputs)`.
|
|
167
|
+
- `isComposeError`, `isComposePreparationError`, `isComposeSdkOutdatedError`, `isComposeServerOutdatedError`: type guards. `ComposeError` is exported as a type only, so narrow with these rather than `instanceof`.
|
|
165
168
|
|
|
166
169
|
## Routing a token pair
|
|
167
170
|
|
|
168
|
-
|
|
171
|
+
`sdk.route(...)` is the shortcut for when the backend can choose the operations. You pass a from/to token pair on one chain, an amount, and a signer. It returns the same `ComposeCompileResult` that `compile()` does.
|
|
172
|
+
|
|
173
|
+
The backend plans the shortest chain of routing-catalog edges for the pair: entering or exiting a protocol position, a wrap or unwrap, a mint or burn. The route can be up to the server's hop ceiling (2 by default). The chain may open with one `lifi.swap` when the input has to be sold first; the swap counts as a hop, so at the default ceiling it precedes at most one catalog edge. That swap only targets a hub token or the underlying of the next deposit, so `route` is not a general swap aggregator.
|
|
169
174
|
|
|
170
|
-
|
|
175
|
+
- Node ids are `hop0`, `hop1`, … and the last node is always `zap`, so the delivered output is `result.outputs['zap.amountOut']`.
|
|
176
|
+
- Every intermediate step settles inside the proxy in the same transaction. Only the last step may pay out to a recipient, or land after settlement (only with `allowAsyncRoutes`).
|
|
177
|
+
- A route that opens with a swap requires `sweepTo`, because the swap can leave unspent input in the proxy. Omitting it fails with `validation_error`.
|
|
171
178
|
|
|
172
179
|
```ts
|
|
173
180
|
import { createComposeSdk, routeAmount } from '@lifi/composer-sdk';
|
|
@@ -180,73 +187,103 @@ const sdk = createComposeSdk({
|
|
|
180
187
|
const result = await sdk.route({
|
|
181
188
|
chainId: 1,
|
|
182
189
|
fromToken: '0x0000000000000000000000000000000000000000', // native ETH
|
|
183
|
-
toToken: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', // WETH
|
|
190
|
+
toToken: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', // WETH, a `wrap` edge
|
|
184
191
|
amount: routeAmount.exact(100_000_000_000_000_000n), // 0.1 ETH
|
|
185
192
|
signer: '0xYourAddress...',
|
|
186
193
|
slippageBps: 100,
|
|
187
194
|
});
|
|
188
195
|
|
|
189
196
|
if (result.status === 'success') {
|
|
190
|
-
console.log(result.transactionRequest); // to, data, value, gasLimit
|
|
197
|
+
console.log(result.transactionRequest); // to, chainId, data, value, gasLimit
|
|
191
198
|
}
|
|
192
199
|
```
|
|
193
200
|
|
|
194
201
|
### Discovering routable pairs
|
|
195
202
|
|
|
196
|
-
The catalog
|
|
203
|
+
The catalog changes as protocols, chains, and token blacklists move, so discover pairs instead of hardcoding them. `getZapPacks` lists single edges; `route` chains them. `buildReachability` computes the chained closure locally, using the planner's rule:
|
|
197
204
|
|
|
198
205
|
```ts
|
|
199
|
-
|
|
206
|
+
import { buildReachability } from '@lifi/composer-sdk';
|
|
200
207
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
208
|
+
const packs = await sdk.client.getZapPacks();
|
|
209
|
+
const reach = buildReachability(packs);
|
|
210
|
+
|
|
211
|
+
reach.canRoute(aUsdc, yvUsdc); // aUSDC -> USDC -> yvUSDC: true
|
|
212
|
+
for (const pair of reach.routableFrom(aUsdc)) {
|
|
213
|
+
console.log(pair.in.address, '->', pair.out.address, `(${pair.hops} hops)`);
|
|
205
214
|
}
|
|
206
215
|
```
|
|
207
216
|
|
|
208
|
-
|
|
217
|
+
`pair.in` and `pair.out` are the `fromToken` and `toToken` of a `route` call on `pair.in.chainId`. The rule the helper applies, and the server enforces:
|
|
209
218
|
|
|
210
|
-
-
|
|
211
|
-
-
|
|
219
|
+
- Every edge except the last must leave its output in the proxy for the next edge to consume. An edge with `recipient: 'required'` or `availability: 'future'` can only be the last hop.
|
|
220
|
+
- The last edge must be synchronous, unless you pass `allowAsyncRoutes: true` to both `buildReachability` and `route`.
|
|
221
|
+
- A route never visits the same token twice and never exceeds `maxHops`.
|
|
212
222
|
|
|
213
|
-
|
|
223
|
+
The helper can overstate or miss what `route` accepts in a few ways:
|
|
224
|
+
|
|
225
|
+
- `maxHops` defaults to 2, the deployment default. The server reports its effective value in `no_route_error` `details.maxHops`; pass that value if it differs.
|
|
226
|
+
- The helper cannot check `inAmountMin` / `inAmountMax` on an intermediate hop without an amount. A reported pair is routable structurally, just as a listed edge is.
|
|
227
|
+
- The server drops blacklisted tokens as intermediates, so a reported pair can still return `no_route_error`.
|
|
228
|
+
- Routes that open with a `lifi.swap` depend on what the aggregator can sell, so the helper does not report them.
|
|
214
229
|
|
|
215
|
-
|
|
230
|
+
Two edge fields change how you call `route`:
|
|
216
231
|
|
|
217
|
-
- **`
|
|
218
|
-
- **`
|
|
232
|
+
- **`recipient: 'required'`**: the protocol mints to a named receiver. Set `sweepTo` when the output should not land with the signer.
|
|
233
|
+
- **`availability: 'future'`**: the output only exists once the transaction settles, and `route` uses such an edge only with `allowAsyncRoutes: true`. Once opted in:
|
|
234
|
+
- `sweepTo` names the future output's owner.
|
|
235
|
+
- No slippage floor applies.
|
|
236
|
+
- You must track settlement.
|
|
237
|
+
- A synchronous route still wins whenever one exists, so read each terminal output's `delivery.when`.
|
|
219
238
|
|
|
220
|
-
|
|
239
|
+
A future output still reports `amount.estimate` (`basis: 'simulated'`, the in-transaction estimate). The catalog carries no settlement time. The compile result carries it as `delivery.etaSeconds` on a future terminal output; that field is absent when the protocol publishes no estimate, and absence means unknown, not zero.
|
|
240
|
+
|
|
241
|
+
### Amounts
|
|
242
|
+
|
|
243
|
+
- **`routeAmount.exact(amount)`**: spend exactly `amount` of `fromToken`, in its smallest unit.
|
|
244
|
+
- **`routeAmount.all(simAmount)`**: spend the signer's whole `fromToken` balance, read on-chain at execution time.
|
|
245
|
+
- The quote, price impact, and returned approvals are computed against `simAmount`. The transaction moves the real balance, so pass a realistic estimate.
|
|
246
|
+
- Not supported for a native `fromToken`: a gas coin arrives via `msg.value`, so there is no balance to sweep.
|
|
247
|
+
|
|
248
|
+
Both accept a `bigint` or a decimal string.
|
|
221
249
|
|
|
222
250
|
### Options
|
|
223
251
|
|
|
224
|
-
|
|
252
|
+
The SDK sends only the fields you set. The server applies the defaults.
|
|
225
253
|
|
|
226
|
-
- **`slippageBps
|
|
227
|
-
- **`maxPriceImpactBps
|
|
228
|
-
- **`integratorFeeBps
|
|
229
|
-
- **`referrer
|
|
230
|
-
- **`sweepTo
|
|
231
|
-
- **`checkOnChainAllowances
|
|
232
|
-
- **`simulationPolicy
|
|
233
|
-
- **`simulateUserProgram
|
|
234
|
-
- **`allowAsyncRoutes
|
|
254
|
+
- **`slippageBps`**: default `100` (1%). An end-to-end tolerance on the route's final output, not a per-hop one.
|
|
255
|
+
- **`maxPriceImpactBps`**: reject the route when its priced impact exceeds this. Default `500`; `0` disables the check.
|
|
256
|
+
- **`integratorFeeBps`**: your fee, taken from the input amount. Range 0 to 9000, default `0`. A non-zero value requires an integration-scoped `apiKey`.
|
|
257
|
+
- **`referrer`**: opaque tag forwarded to the underlying quote provider.
|
|
258
|
+
- **`sweepTo`**: owner of everything the route leaves behind. An address or `{ $ref: 'context.sender' }`; `context.executionAddress` is rejected. Defaults to the signer, except that a route with a swap requires it.
|
|
259
|
+
- **`checkOnChainAllowances`**: omit approvals that current allowances already cover. Default `false`.
|
|
260
|
+
- **`simulationPolicy`**: `'strict'` (default) or `'allow-revert'`. See [Simulation policy and partial results](#simulation-policy-and-partial-results).
|
|
261
|
+
- **`simulateUserProgram`**: also simulate the final user-facing transaction after the structured simulation succeeds. Default `false`.
|
|
262
|
+
- **`allowAsyncRoutes`**: accept a route whose output arrives after the transaction settles. Default `false`, which guarantees every terminal output is `delivery.when: 'now'`.
|
|
235
263
|
|
|
236
264
|
### When to author a flow instead
|
|
237
265
|
|
|
238
|
-
|
|
266
|
+
Use `sdk.flow(...)` for anything `route` cannot express:
|
|
267
|
+
|
|
268
|
+
- a chain you choose yourself, or one longer than the hop ceiling
|
|
269
|
+
- one input split across several destinations
|
|
270
|
+
- guards on intermediate outputs
|
|
271
|
+
- explicit preconditions or arithmetic
|
|
272
|
+
- funding from proxy-held balances
|
|
273
|
+
- combining an asynchronous position with other steps
|
|
274
|
+
|
|
275
|
+
For example, swapping WETH to USDC and depositing the USDC into Aave is a route when the planner can connect it, and a flow when you want to control the swap or add steps.
|
|
239
276
|
|
|
240
277
|
### Errors
|
|
241
278
|
|
|
242
|
-
`route` maps failures
|
|
279
|
+
`route` maps failures like `compile` does (see [Error handling](#error-handling)). Two outcomes are specific to it:
|
|
243
280
|
|
|
244
|
-
- **`NOT_FOUND`** with `kind: 'no_route_error'
|
|
245
|
-
- **`VALIDATION_ERROR
|
|
281
|
+
- **`NOT_FOUND`** with `kind: 'no_route_error'`: the planner cannot connect the pair within `details.maxHops` edges. `details.searchTruncated` reports whether the search hit its work bound.
|
|
282
|
+
- **`VALIDATION_ERROR`**: the pair connects but the request is rejected. Examples: price impact above `maxPriceImpactBps`, or a simulated revert under `'strict'`.
|
|
246
283
|
|
|
247
284
|
## Simulation policy and partial results
|
|
248
285
|
|
|
249
|
-
|
|
286
|
+
The backend simulates every compiled transaction. Under the default `simulationPolicy: 'strict'`, a revert throws a `ComposeError` with code `VALIDATION_ERROR` (HTTP 422). With `'allow-revert'` you get a **partial result** (HTTP 206) instead:
|
|
250
287
|
|
|
251
288
|
```ts
|
|
252
289
|
const result = await builder.compile({
|
|
@@ -256,57 +293,35 @@ const result = await builder.compile({
|
|
|
256
293
|
});
|
|
257
294
|
|
|
258
295
|
if (result.status === 'success') {
|
|
259
|
-
|
|
260
|
-
const tx = result.transactionRequest;
|
|
261
|
-
console.log(tx.gasLimit); // string
|
|
296
|
+
console.log(result.transactionRequest?.gasLimit); // string
|
|
262
297
|
} else {
|
|
263
|
-
//
|
|
264
|
-
//
|
|
298
|
+
// status === 'partial': the transaction is returned without gasLimit;
|
|
299
|
+
// estimate gas yourself.
|
|
265
300
|
console.log(result.error.kind); // 'simulation_revert'
|
|
266
|
-
console.log(result.error.message);
|
|
301
|
+
console.log(result.error.message);
|
|
267
302
|
|
|
268
|
-
// Revert diagnostics
|
|
269
303
|
const revert = result.simulationRevert;
|
|
270
|
-
console.log(revert.code);
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
// Decoded error candidates (when available)
|
|
274
|
-
if (revert.decodeResult?.errorCandidates) {
|
|
275
|
-
for (const c of revert.decodeResult.errorCandidates) {
|
|
276
|
-
console.log(c.decodedErrorSignature, c.decodedParams);
|
|
277
|
-
}
|
|
304
|
+
console.log(revert.code, revert.rawErrorBytes);
|
|
305
|
+
for (const c of revert.decodeResult?.errorCandidates ?? []) {
|
|
306
|
+
console.log(c.decodedErrorSignature, c.decodedParams);
|
|
278
307
|
}
|
|
279
308
|
|
|
280
|
-
// The transactionRequest is still usable — the caller must estimate gas themselves.
|
|
281
309
|
const tx = result.transactionRequest;
|
|
282
310
|
console.log(tx.to, tx.data, tx.value);
|
|
283
311
|
}
|
|
284
312
|
```
|
|
285
313
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
- `'strict'` (default) — revert causes a thrown `ComposeError` with code `VALIDATION_ERROR`
|
|
289
|
-
- `'allow-revert'` — revert returns a partial result with `status: 'partial'`
|
|
290
|
-
|
|
291
|
-
You can also pass `checkOnChainAllowances: true` to have the server filter the returned `approvals` array against current on-chain allowances, omitting approvals that are already sufficient:
|
|
292
|
-
|
|
293
|
-
```ts
|
|
294
|
-
const result = await builder.compile({
|
|
295
|
-
signer: OWNER,
|
|
296
|
-
inputs: { amountIn: materialisers.balanceOf({ owner: OWNER }) },
|
|
297
|
-
checkOnChainAllowances: true,
|
|
298
|
-
});
|
|
299
|
-
```
|
|
314
|
+
Pass `checkOnChainAllowances: true` to have the server drop entries from `approvals` that current on-chain allowances already cover.
|
|
300
315
|
|
|
301
316
|
## Simulating a raw transaction
|
|
302
317
|
|
|
303
|
-
`sdk.client.simulate(...)` (and
|
|
318
|
+
`sdk.client.simulate(...)` (and `sdk.simulate(...)`) answers a question compile does not: if I send this exact transaction, how do specific balances change, and how much gas does it burn? It takes a pre-encoded transaction (`to`, hex `data`, optional `value`). It funds the sender as `requirements` describe, runs the transaction in one `eth_call`, and reports the watched balances before and after, their signed deltas, and the inner-call gas.
|
|
304
319
|
|
|
305
320
|
```ts
|
|
306
321
|
import { createComposeSdk } from '@lifi/composer-sdk';
|
|
307
322
|
|
|
308
323
|
const sdk = createComposeSdk({
|
|
309
|
-
baseUrl: 'https://li.quest',
|
|
324
|
+
baseUrl: 'https://composer.li.quest',
|
|
310
325
|
apiKey: process.env.LIFI_API_KEY!,
|
|
311
326
|
});
|
|
312
327
|
|
|
@@ -315,13 +330,13 @@ const result = await sdk.client.simulate({
|
|
|
315
330
|
from: '0x1111111111111111111111111111111111111111',
|
|
316
331
|
to: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC
|
|
317
332
|
data: '0xa9059cbb...', // pre-encoded transfer calldata
|
|
318
|
-
value: 0n,
|
|
333
|
+
value: 0n,
|
|
319
334
|
requirements: [
|
|
320
335
|
{
|
|
321
336
|
type: 'Erc20Balance',
|
|
322
337
|
wallet: '0x1111111111111111111111111111111111111111',
|
|
323
338
|
token: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
|
|
324
|
-
balance: 1_000_000n,
|
|
339
|
+
balance: 1_000_000n,
|
|
325
340
|
},
|
|
326
341
|
],
|
|
327
342
|
trackedBalances: [
|
|
@@ -338,25 +353,27 @@ const result = await sdk.client.simulate({
|
|
|
338
353
|
|
|
339
354
|
switch (result.status) {
|
|
340
355
|
case 'ok':
|
|
341
|
-
// Successful simulation.
|
|
342
356
|
console.log(result.gasUsed, result.deltas);
|
|
343
357
|
break;
|
|
344
358
|
case 'revert':
|
|
345
|
-
// The simulation ran
|
|
359
|
+
// The simulation ran and the transaction reverted. Not an error.
|
|
346
360
|
console.log(result.revertReason, result.decodeResult);
|
|
347
361
|
break;
|
|
348
362
|
case 'error':
|
|
349
|
-
//
|
|
363
|
+
// Well-formed request, but the simulation could not be set up or run.
|
|
350
364
|
console.log(result.message);
|
|
351
365
|
break;
|
|
352
366
|
}
|
|
353
367
|
```
|
|
354
368
|
|
|
355
|
-
`requirements`
|
|
369
|
+
- `requirements` entries are `Erc20Balance`, `NativeBalance`, or `Erc20Allowance`. Use the zero address as a `trackedBalances` token to watch the native balance.
|
|
370
|
+
- Amount fields accept `bigint` or decimal strings.
|
|
371
|
+
- `SIMULATE_MAX_TRACKED_BALANCES` and `SIMULATE_MAX_REQUIREMENTS` (both `40`) are exported.
|
|
372
|
+
- A `revert` is returned, not thrown. Only transport failures and HTTP 400/401/403/404/429/5xx throw `ComposeError`.
|
|
373
|
+
- `gasUsed` is inner-call execution gas only; it excludes the 21000 base cost and calldata gas.
|
|
374
|
+
- EOA-only behaviour, such as `msg.sender == tx.origin` checks, is not simulated faithfully, because the call runs through injected VM bytecode rather than a real EOA.
|
|
356
375
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
To simulate a transaction you just compiled, `buildSimulateRequest(...)` assembles the request without re-typing the transaction fields. `chainId` and `signer` are required (a compile result carries neither), and `trackedBalances`/`requirements` cannot be inferred from a flow:
|
|
376
|
+
To simulate a transaction you just compiled, `buildSimulateRequest(...)` fills in the transaction fields from the compile result. A compile result carries neither `chainId` nor `signer`, so pass both. Nothing in a flow says which balances to watch, so pass `trackedBalances` and any `requirements` too. It throws when the result has no single `transactionRequest` (a flow funded by an external action); submit that result's `actions` instead.
|
|
360
377
|
|
|
361
378
|
```ts
|
|
362
379
|
import { buildSimulateRequest } from '@lifi/composer-sdk';
|
|
@@ -376,13 +393,11 @@ const req = buildSimulateRequest({
|
|
|
376
393
|
const sim = await sdk.client.simulate(req);
|
|
377
394
|
```
|
|
378
395
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
Two runnable examples cover both paths: [`src/examples/simulateRawTransaction.ts`](src/examples/simulateRawTransaction.ts) builds a raw `SimulateRequest` directly, and [`src/examples/simulateCompiledSwap.ts`](src/examples/simulateCompiledSwap.ts) shows the end-to-end compile → `buildSimulateRequest` → `simulate` arc.
|
|
396
|
+
Runnable versions: [`src/examples/simulateRawTransaction.ts`](src/examples/simulateRawTransaction.ts) and [`src/examples/simulateCompiledSwap.ts`](src/examples/simulateCompiledSwap.ts).
|
|
382
397
|
|
|
383
398
|
## Error handling
|
|
384
399
|
|
|
385
|
-
|
|
400
|
+
HTTP and transport failures are thrown as a `ComposeError`. Builder mistakes (a duplicate node id, a missing output port) throw a plain `Error` at the call site. Narrow with `isComposeError`:
|
|
386
401
|
|
|
387
402
|
```ts
|
|
388
403
|
import { isComposeError } from '@lifi/composer-sdk';
|
|
@@ -391,13 +406,26 @@ try {
|
|
|
391
406
|
const result = await sdk.client.compile(request);
|
|
392
407
|
} catch (err) {
|
|
393
408
|
if (isComposeError(err)) {
|
|
394
|
-
console.error(err.code, err.message);
|
|
395
|
-
//
|
|
409
|
+
console.error(err.code, err.status, err.kind, err.message);
|
|
410
|
+
console.error(err.path, err.port, err.fix); // when the server names a location or a fix
|
|
396
411
|
}
|
|
397
412
|
}
|
|
398
413
|
```
|
|
399
414
|
|
|
400
|
-
|
|
415
|
+
| `code` | HTTP status | Typical cause |
|
|
416
|
+
| ------------------ | ----------- | --------------------------------------------------------------- |
|
|
417
|
+
| `VALIDATION_ERROR` | 400, 422 | Malformed or rejected flow or request, simulated revert, guard |
|
|
418
|
+
| `UNAUTHENTICATED` | 401 | Missing or invalid API key |
|
|
419
|
+
| `FORBIDDEN` | 403 | The server refuses the request |
|
|
420
|
+
| `NOT_FOUND` | 404 | `no_route_error` |
|
|
421
|
+
| `RATE_LIMITED` | 429 | Too many requests |
|
|
422
|
+
| `SERVER_ERROR` | 5xx | Backend failure or a dependency temporarily unavailable |
|
|
423
|
+
| `NETWORK_ERROR` | none | The request never got a response |
|
|
424
|
+
| `UNKNOWN_ERROR` | other | Unexpected status |
|
|
425
|
+
|
|
426
|
+
`kind` is the backend's finer-grained classification, e.g. `simulation_revert`, `guard_error`, or `price_impact_exceeded`. When `kind` is `simulation_revert`, `err.details` has the same shape as `result.simulationRevert`.
|
|
427
|
+
|
|
428
|
+
A `preparation_error` (HTTP 422) means some prepared ops (ops that fetch external quotes at compile time, such as `lifi.swap`) failed while others succeeded, e.g. a mixed basket with one unroutable leg. Narrow with `isComposePreparationError`, drop the failed node ids from the flow, and resubmit:
|
|
401
429
|
|
|
402
430
|
```ts
|
|
403
431
|
import { isComposePreparationError } from '@lifi/composer-sdk';
|
|
@@ -409,72 +437,68 @@ try {
|
|
|
409
437
|
for (const op of err.failedOps) {
|
|
410
438
|
console.error(op.callId, op.op, op.kind, op.message);
|
|
411
439
|
}
|
|
412
|
-
//
|
|
413
|
-
//
|
|
440
|
+
// Only the prepared ops that succeeded, not the rest of the flow. To retry,
|
|
441
|
+
// remove the failedOps callIds from the original flow.
|
|
414
442
|
console.error('prepared ok:', err.succeededOps);
|
|
415
443
|
}
|
|
416
444
|
}
|
|
417
445
|
```
|
|
418
446
|
|
|
419
|
-
|
|
447
|
+
**Version mismatch.** `compile` and `route` fail with `kind: 'sdk_outdated'` when this SDK is older than the oldest version the backend serves. They fail with `kind: 'server_outdated'` when the SDK is newer than the backend in any version component, which happens between an npm release and the backend rollout. Both come back as `VALIDATION_ERROR`. `isComposeSdkOutdatedError(err)` exposes `err.sdkOutdated.minimumSdkVersion`; `isComposeServerOutdatedError(err)` exposes `err.serverOutdated.serverVersion`. On `server_outdated`, pin both packages to the server's version or wait for the rollout.
|
|
420
448
|
|
|
421
449
|
## Examples
|
|
422
450
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
- **lifiSwap
|
|
426
|
-
- **lifiZap
|
|
427
|
-
- **lifiZapAsync
|
|
428
|
-
- **swapAndZap
|
|
429
|
-
- **splitAndZap
|
|
430
|
-
- **splitWithArithmetic
|
|
431
|
-
- **dustSweep
|
|
432
|
-
- **depositFromProxy
|
|
433
|
-
- **approveAndDeposit
|
|
434
|
-
- **consolidateToUsdc**
|
|
435
|
-
- **
|
|
436
|
-
- **
|
|
437
|
-
- **
|
|
438
|
-
- **
|
|
439
|
-
- **rawCallWithArithmetic
|
|
440
|
-
- **readContractState
|
|
441
|
-
- **
|
|
442
|
-
- **
|
|
443
|
-
- **
|
|
444
|
-
- **
|
|
445
|
-
- **
|
|
446
|
-
- **
|
|
447
|
-
- **
|
|
448
|
-
- **
|
|
449
|
-
- **
|
|
450
|
-
- **
|
|
451
|
-
- **
|
|
451
|
+
Each file in `src/examples/` is a complete flow:
|
|
452
|
+
|
|
453
|
+
- **lifiSwap**: single token swap (WETH to USDC)
|
|
454
|
+
- **lifiZap**: swap into a DeFi position
|
|
455
|
+
- **lifiZapAsync**: zap into a position whose output settles after the initiating transaction
|
|
456
|
+
- **swapAndZap**: swap, then deposit
|
|
457
|
+
- **splitAndZap**: split a resource and zap each part into a different vault
|
|
458
|
+
- **splitWithArithmetic**: split, then check with add/subtract/assertEqual
|
|
459
|
+
- **dustSweep**: split, use part of the tokens, sweep the rest back to the sender
|
|
460
|
+
- **depositFromProxy**: spend tokens already on the proxy via `balanceOf`, with a precondition
|
|
461
|
+
- **approveAndDeposit**: approve a vault, deposit, and turn the shares into a resource via `asResource`
|
|
462
|
+
- **consolidateToUsdc** / **consolidateToEth**: consolidate several tokens into one
|
|
463
|
+
- **swapToRecipient**: swap and send to another address
|
|
464
|
+
- **swapWithBalanceCheck**: swap with a balance precondition
|
|
465
|
+
- **swapWithOutputValidation**: swap with computed bounds via bpsDown/bpsUp/assertInRange
|
|
466
|
+
- **invariantChecks**: swap, then assert the output with `invariant.gte` and an allowance with `invariant.allowanceAtLeast`
|
|
467
|
+
- **rawCallWithArithmetic**: query a contract with pre-encoded calldata, then scale the result
|
|
468
|
+
- **readContractState**: compare `peek` (compile time), `staticCall` (execution time), and `balanceOf` (resource)
|
|
469
|
+
- **emitCustomEvent**: emit a custom event with `core.emitEvent2`; `bytes32` ports accept any static 32-byte handle
|
|
470
|
+
- **swapWithAllowRevert**: handle both branches of `ComposeCompileResult` under `'allow-revert'`
|
|
471
|
+
- **swapWithFee**: collect an integrator fee via `integratorFeeBps` (requires an integration-scoped `apiKey`)
|
|
472
|
+
- **transferTokens**: transfer ERC-20 tokens from the proxy to any recipient
|
|
473
|
+
- **callContract**: call an arbitrary contract (ERC-4626 redeem, reward claim) without a typed op
|
|
474
|
+
- **flySwap**: on Base, exit a Pendle PT via `lifi.zap`, then swap all proceeds to WETH with `fly.swap`
|
|
475
|
+
- **aaveRepay**: repay Aave v3 variable debt and sweep the unspent residual to the sender
|
|
476
|
+
- **aaveRepayWithATokens**: repay Aave v3 debt by burning aTokens held by the proxy
|
|
477
|
+
- **aaveClaimRewards**: claim Aave rewards and forward them to a recipient
|
|
478
|
+
- **aaveSetEMode**: switch the proxy's Aave v3 eMode category
|
|
479
|
+
- **untypedOpWithTypedRef**: add a node with `untypedOp`, then use its output in typed ops via `raw.ref<T>()`
|
|
480
|
+
- **routeTokenPair**: `sdk.route(...)` with both the `exact` and `all` amount variants
|
|
481
|
+
- **supportedChains**: cache `sdk.getSupportedChains()`, then fail an unsupported chain locally with a message naming the alternatives
|
|
482
|
+
- **simulateRawTransaction** / **simulateCompiledSwap**: raw and compiled simulation
|
|
452
483
|
|
|
453
484
|
## Staging channel
|
|
454
485
|
|
|
455
|
-
Some operations
|
|
486
|
+
Some operations run on the backend but are held back from the default SDK, for example because their contract changes are not live in production yet. These **staged** operations are published on the `staging` npm dist-tag:
|
|
456
487
|
|
|
457
488
|
```bash
|
|
458
489
|
npm install @lifi/composer-sdk@staging @lifi/compose-spec@staging
|
|
459
490
|
```
|
|
460
491
|
|
|
461
|
-
The `staging` build
|
|
462
|
-
|
|
463
|
-
A staged operation only runs if the backend you point at actually has it enabled. The SDK has no default backend — you supply one per instance:
|
|
492
|
+
The `staging` build types the staged operations (e.g. `lifi.flashloanRepay`). The default install never exposes them. A staged operation only runs on a backend that has it enabled, so point `baseUrl` at one:
|
|
464
493
|
|
|
465
494
|
```ts
|
|
466
495
|
const sdk = createComposeSdk({
|
|
467
|
-
baseUrl: '<staging-backend-url>',
|
|
496
|
+
baseUrl: '<staging-backend-url>',
|
|
468
497
|
apiKey: process.env.LIFI_API_KEY!,
|
|
469
498
|
});
|
|
470
499
|
```
|
|
471
500
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
The channel and the backend URL are independent:
|
|
475
|
-
|
|
476
|
-
- The `staging` **channel** is durable — it is a permanent property of `main`, and which operations it carries rotates over time as ops graduate to `latest`.
|
|
477
|
-
- The `baseUrl` is **per-environment and disposable** — it is a runtime argument, not a build-time property, so the same staging build can target whichever backend currently has the ops enabled.
|
|
501
|
+
Against a backend without the operation, the call fails at runtime; having the types does not mean the backend supports the operation. The channel is permanent, and its contents rotate as ops graduate to `latest`. The backend URL is a runtime argument, so one staging build can target whichever backend has the ops enabled.
|
|
478
502
|
|
|
479
503
|
## License
|
|
480
504
|
|