npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@lifi/composer-sdk

v0.2.0

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-spec

Quick 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://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).

Resourcesresources.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.
  • 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.

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
  • buildSimulateRequest({ result, chainId, signer, trackedBalances, requirements?, block?, value? }) — assembles a SimulateRequest from a compile result (pure, no I/O)

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 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:

const result = await builder.compile({
  signer: OWNER,
  inputs: { amountIn: materialisers.balanceOf({ owner: OWNER }) },
  checkOnChainAllowances: true,
});

Simulating a raw transaction

sdk.client.simulate(...) (and the sdk.simulate(...) pass-through) answer a question the compile pipeline does not: if I send this exact transaction, how do specific token balances change and how much gas does it burn? It takes a raw, pre-encoded transaction (a to, hex data, optional native value), funds a sender, runs it in one eth_call, and reports the watched balances before/after, their signed deltas, and the inner-call gas.

import { createComposeSdk } from '@lifi/composer-sdk';

const sdk = createComposeSdk({ baseUrl: 'https://li.quest' });

const result = await sdk.client.simulate({
  chainId: 1,
  from: '0x1111111111111111111111111111111111111111',
  to: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC
  data: '0xa9059cbb...', // pre-encoded transfer calldata
  value: 0n, // bigint accepted; serialised to "0"
  requirements: [
    {
      type: 'Erc20Balance',
      wallet: '0x1111111111111111111111111111111111111111',
      token: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
      balance: 1_000_000n, // bigint accepted
    },
  ],
  trackedBalances: [
    {
      token: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
      owner: '0x1111111111111111111111111111111111111111',
    },
    {
      token: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
      owner: '0x2222222222222222222222222222222222222222',
    },
  ],
});

switch (result.status) {
  case 'ok':
    // Successful simulation.
    console.log(result.gasUsed, result.deltas);
    break;
  case 'revert':
    // The simulation ran but the transaction reverted on-chain — NOT an error.
    console.log(result.revertReason, result.decodeResult);
    break;
  case 'error':
    // The request was well-formed but the simulation could not be set up/run.
    console.log(result.message);
    break;
}

requirements is the funding-instruction union (Erc20Balance, NativeBalance, Erc20Allowance); use the zero address as a trackedBalances token to watch native balance. Amount fields accept bigint (serialised to decimal strings) or strings. The caps SIMULATE_MAX_TRACKED_BALANCES and SIMULATE_MAX_REQUIREMENTS (both 40) are exported for reference.

Unlike compile, a revert is returned (not thrown): a revert is a successful simulation whose execution reverted. Only transport failures and HTTP 400/401/403/404/429/5xx throw ComposeError.

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:

import { buildSimulateRequest } from '@lifi/composer-sdk';

const compiled = await builder.compile({ inputs: { ... }, signer: '0x1111...' });
const req = buildSimulateRequest({
  result: compiled,
  chainId: 1,
  signer: '0x1111111111111111111111111111111111111111',
  trackedBalances: [
    {
      token: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
      owner: '0x1111111111111111111111111111111111111111',
    },
  ],
});
const sim = await sdk.client.simulate(req);

Two caveats from the endpoint: gasUsed is the inner-call execution gas only (it excludes the 21000 base tx cost and calldata gas), and EOA-only behaviour (e.g. msg.sender == tx.origin checks) is not faithfully simulated because the call runs through injected VM bytecode rather than a real EOA. See docs/references/simulate-endpoint.md for the full contract.

Two runnable examples cover both paths: src/examples/simulateRawTransaction.ts builds a raw SimulateRequest directly, and src/examples/simulateCompiledSwap.ts shows the end-to-end compile → buildSimulateRequestsimulate arc.

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 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:

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:

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