UNPKG

@lifi/composer-sdk

Version:

Public Composer SDK for building and submitting flows

257 lines (201 loc) 12 kB
# @lifi/composer-sdk TypeScript SDK for building and submitting LI.FI Compose flows. ## Install `@lifi/compose-spec` is a peer dependency and must be installed alongside the SDK at the same version (they are versioned in lockstep). ```bash npm install @lifi/composer-sdk @lifi/compose-spec ``` ## Quick start Swap WETH to USDC, then zap the USDC into an Aave lending position — all in a single transaction. ```ts import { createComposeSdk, resources, guards, materialisers, } from '@lifi/composer-sdk'; const WETH = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'; const USDC = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'; const A_ETH_USDC = '0x98C23E9d8f34FEFb1B7BD6a91B7FF122F4e16F5c'; // Aave aEthUSDC const OWNER = '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'; // Create the SDK pointed at the Compose API. const sdk = createComposeSdk({ baseUrl: 'https://composer.li.quest', apiKey: process.env.LIFI_API_KEY, // optional }); // Build a two-step flow on Ethereum mainnet. const builder = sdk.flow(1, { name: 'swap-and-zap-weth-to-aave', inputs: { amountIn: resources.erc20(WETH, 1), }, }); // Step 1: Swap WETH → USDC via LI.FI. const swapOutputs = builder.lifi.swap('swap', { bind: { amountIn: builder.inputs.amountIn }, config: { resourceOut: resources.erc20(USDC, 1), slippage: 0.03, }, }); // Step 2: Zap the swapped USDC into Aave. // The swap's amountOut handle threads directly into the zap's amountIn. builder.lifi.zap('zap', { bind: { amountIn: swapOutputs.amountOut }, config: { resourceOut: resources.erc20(A_ETH_USDC, 1), }, guards: [guards.slippage({ port: 'amountOut', bps: 100 })], }); const flow = builder.build(); // Compile the flow into transaction calldata. const request = sdk.request(flow, { signer: OWNER, inputs: { amountIn: materialisers.directDeposit({ amount: '1000000000000000000', }), }, sweepTo: builder.context.sender, // Opt into partial results. Without this, the default 'strict' policy // throws a ComposeError (HTTP 422) when simulation detects a revert, // and the `partial` branch below is never reached. simulationPolicy: 'allow-revert', }); const result = await sdk.client.compile(request); if (result.status === 'success') { // Full success — transactionRequest includes gasLimit. console.log(result.transactionRequest); } else { // result.status === 'partial' — simulation reverted. // Transaction is still available but without gasLimit. console.log(result.simulationRevert); } ``` ## Core concepts **Flows and operations** — A flow is a sequence of on-chain operations. You declare inputs, chain operations together, and the backend compiles everything into a single transaction. Operations are namespaced (e.g., `builder.lifi.swap`, `builder.core.split`). **Resources**`resources.erc20(address, chainId)` and `resources.native(chainId)` describe the tokens flowing through your operations. They carry chain and address metadata used for routing and validation. **Handles** — Operations produce typed output handles (e.g. `OutputHandle<'resource'>`, `OutputHandle<'uint256'>`) that you bind to downstream inputs. The type system enforces compatibility at compile time — a resource handle can flow into a `uint256` slot (since resources are amounts), but an `address` handle cannot. **Runtime inputs (materialisers)** — Materialisers resolve input values at execution time rather than at build time. `directDeposit` is exact by default when you provide an amount; pass `allowNonExact: true` to permit capped ERC-20 deposits or deposit-all behavior. `balanceOf` reads the wallet's current balance; `call` measures a balance delta after an arbitrary contract call. **Preconditions** — Expected on-chain state at execution time: `erc20Balance` and `nativeBalance` assert wallet holdings, `erc20Allowance` asserts token approvals. **Guards** — Protect against slippage and other runtime conditions. Applied per-operation via the `guards` field. ## API surface **SDK factory** - `createComposeSdk({ baseUrl, fetch?, apiKey? })` — creates the SDK instance **Flow building** - `sdk.flow(chainId, options)` — creates a `FlowBuilder` - `builder.<namespace>.<operation>(id, { bind, config })` — adds an operation, returns typed `OutputHandle<T>` per port - `builder.untypedOp(id, op, args)` — escape hatch for operations not in the manifest (returns `void`; use `raw.ref<T>()` to reference its outputs) - `builder.build()` — produces a `Flow` document - `sdk.request(flow, { signer, inputs, preconditions, sweepTo, ... })` — builds a compile request **HTTP client** - `sdk.client.compile(request)` — sends the flow to the backend and returns a `ComposeCompileResult` (discriminated union: `status: 'success'` or `status: 'partial'`) - `sdk.client.getManifest()` — fetches the operation manifest - `sdk.client.getZapPacks(options?)` — fetches the available routing edges grouped by protocol, returning `ZapPackOverview[]`. The catalog is dynamic (reflects the backend's current routing snapshot) and is not cached by the SDK. Filter to specific protocols via `GetZapPacksOptions`; each entry's edges are typed as `ZapPackEdge`. **Helpers** - `resources.erc20(address, chainId)` / `resources.native(chainId)` — resource constructors - `guards.*` — guard factories (e.g., slippage) - `materialisers.*` — materialiser factories (directDeposit, balanceOf, call) - `preconditions.*` — precondition factories (erc20Balance, nativeBalance, erc20Allowance) - `raw.ref<T>(path)` — create a typed `$ref` pointer for use in bind slots (escape hatch for `untypedOp` outputs) - `raw.guard(kind, config?)` / `raw.materialiser(kind, config?)` — low-level factories for guards and materialisers ## Simulation policy and partial results By default, the Compose backend simulates the compiled transaction and returns an error (HTTP 422) if simulation detects a revert. You can opt into receiving a **partial result** instead by passing `simulationPolicy: 'allow-revert'`: ```ts const result = await builder.compile({ signer: OWNER, inputs: { amountIn: materialisers.balanceOf({ owner: OWNER }) }, simulationPolicy: 'allow-revert', }); if (result.status === 'success') { // Simulation succeeded. transactionRequest includes gasLimit. const tx = result.transactionRequest; console.log(tx.gasLimit); // string } else { // result.status === 'partial' // Simulation reverted, but a transaction is still available (without gasLimit). console.log(result.error.kind); // 'simulation_revert' console.log(result.error.message); // human-readable revert description // Revert diagnostics const revert = result.simulationRevert; console.log(revert.code); // e.g. 3 console.log(revert.rawErrorBytes); // raw ABI-encoded error // Decoded error candidates (when available) if (revert.decodeResult?.errorCandidates) { for (const c of revert.decodeResult.errorCandidates) { console.log(c.decodedErrorSignature, c.decodedParams); } } // The transactionRequest is still usable — the caller must estimate gas themselves. const tx = result.transactionRequest; console.log(tx.to, tx.data, tx.value); } ``` The `simulationPolicy` field accepts two values: - `'strict'` (default) — revert causes a thrown `ComposeError` with code `VALIDATION_ERROR` - `'allow-revert'` — revert returns a partial result with `status: 'partial'` 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: ```ts const result = await builder.compile({ signer: OWNER, inputs: { amountIn: materialisers.balanceOf({ owner: OWNER }) }, checkOnChainAllowances: true, }); ``` ## Error handling All SDK errors are thrown as `ComposeError` with a `code` property: ```ts import { isComposeError } from '@lifi/composer-sdk'; try { const result = await sdk.client.compile(request); } catch (err) { if (isComposeError(err)) { console.error(err.code, err.message); // Codes: VALIDATION_ERROR, SERVER_ERROR, RATE_LIMITED, NETWORK_ERROR, ... } } ``` ## Examples The `src/examples/` directory contains complete working examples: - **lifiSwap** — Single token swap (WETH to USDC) - **lifiZap** — Swap into a DeFi position - **swapAndZap** — Multi-step: swap then deposit - **splitAndZap** — Split a resource and zap each portion into a different vault - **splitWithArithmetic** — Split then verify with add/subtract/assertEqual assertions - **dustSweep** — Split and partially use tokens, sweep leftover dust back to sender - **depositFromProxy** — Read tokens already on the proxy via `balanceOf`, with a precondition guard - **approveAndDeposit** — Approve a vault, deposit, and graduate shares via `asResource` - **consolidateToUsdc** — Consolidate multiple tokens into USDC - **consolidateToEth** — Consolidate multiple tokens into ETH - **swapToRecipient** — Swap and send to a different address - **swapWithBalanceCheck** — Swap with balance precondition - **swapWithOutputValidation** — Swap with computed slippage bounds using bpsDown/bpsUp/assertInRange - **rawCallWithArithmetic** — Query a contract with pre-encoded calldata, then scale with multiply/divide - **readContractState** — Compare peek (compile-time), staticCall (execution-time), and balanceOf (resource) - **swapWithAllowRevert** — Swap with `simulationPolicy: 'allow-revert'` and handle the `ComposeCompileResult` discriminated union - **swapWithFee** — Swap while collecting an integrator fee via `integratorFeeBps` (requires an integration-scoped `apiKey`) - **transferTokens** — Transfer ERC-20 tokens from the proxy to an arbitrary recipient - **callContract** — Call an arbitrary contract (ERC-4626 redeem; reward claim) without a dedicated typed op - **aaveRepay** — Repay an Aave v3 variable-rate debt, sweeping the unspent residual back to the sender - **aaveRepayWithATokens** — Repay Aave v3 debt by burning aToken collateral already held by the proxy - **aaveClaimRewards** — Claim accrued Aave rewards and forward the claimed amount to a recipient - **aaveSetEMode** — Switch the proxy's Aave v3 eMode category - **untypedOpWithTypedRef** — Insert an untyped operation node via `untypedOp`, then bridge its output into typed operations using `raw.ref<T>()` ## Staging channel Some operations exist on the Compose backend but are deliberately held back from the default SDK — for example an op whose required contract changes are not yet live on production. These **staged** operations are published on a separate npm dist-tag, `staging`: ```bash npm install @lifi/composer-sdk@staging @lifi/compose-spec@staging ``` The `staging` build includes the not-yet-public operations in its typed surface (e.g. `lifi.flashloanRepay`), so you can author flows against them with full type-checking. The default install (`@lifi/composer-sdk`, the `latest` dist-tag) never exposes them. 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: ```ts const sdk = createComposeSdk({ baseUrl: '<staging-backend-url>', // a backend that has the staged ops enabled }); ``` Calling a staged operation against a backend that does not have it enabled fails at runtime as a service-level error; the typed surface being present does not guarantee backend availability. The channel and the backend URL are independent: - 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`. - 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. ## License Apache-2.0