@lifi/composer-sdk
v0.0.1-alpha
Published
Public Composer SDK for building and submitting flows
Readme
@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).
npm install @lifi/composer-sdk @lifi/compose-specQuick start
Swap WETH to USDC, then zap the USDC into an Aave lending position — all in a single transaction.
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://compose.li.fi',
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,
});
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 aFlowBuilderbuilder.<namespace>.<operation>(id, { bind, config })— adds an operation, returns typedOutputHandle<T>per portbuilder.untypedOp(id, op, args)— escape hatch for operations not in the manifest (returnsvoid; useraw.ref<T>()to reference its outputs)builder.build()— produces aFlowdocumentsdk.request(flow, { signer, inputs, preconditions, sweepTo, ... })— builds a compile request
HTTP client
sdk.client.compile(request)— sends the flow to the backend and returns aComposeCompileResult(discriminated union:status: 'success'orstatus: 'partial')sdk.client.getManifest()— fetches the operation manifest
Helpers
resources.erc20(address, chainId)/resources.native(chainId)— resource constructorsguards.*— guard factories (e.g., slippage)materialisers.*— materialiser factories (directDeposit, balanceOf, call)preconditions.*— precondition factories (erc20Balance, nativeBalance, erc20Allowance)raw.ref<T>(path)— create a typed$refpointer for use in bind slots (escape hatch foruntypedOpoutputs)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':
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 thrownComposeErrorwith codeVALIDATION_ERROR'allow-revert'— revert returns a partial result withstatus: '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:
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:
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 theComposeCompileResultdiscriminated union - untypedOpWithTypedRef — Insert an untyped operation node via
untypedOp, then bridge its output into typed operations usingraw.ref<T>()
License
Apache-2.0
