# @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
