> ## Documentation Index
> Fetch the complete documentation index at: https://docs.li.fi/llms.txt
> Use this file to discover all available pages before exploring further.

# Build a Flow

> 端到端演练如何编写一个 Composer Flow：setup、inputs、ops、materialisers、guards、preconditions 和提交。

本指南逐步讲解使用 `@lifi/composer-sdk` 构建一个 Flow 的每一步：为生产环境配置 SDK、声明 inputs、链接 ops、供应运行时值、附加安全不变量，以及提交。首次集成时请从头读到尾；之后可作为参考回到特定小节。

关于 5 分钟的首个 flow 演示，参见 [Quickstart](/composer/composer-api/quickstart)。关于 SDK 类型面，参见 [SDK API reference](/composer/composer-api/reference/sdk-api)。

## 生产环境设置

### 配置 SDK

```ts theme={"system"}
import { createComposeSdk } from '@lifi/composer-sdk';

const sdk = createComposeSdk({
  baseUrl: process.env.COMPOSE_BASE_URL ?? 'https://composer.li.quest',
  apiKey: process.env.LIFI_API_KEY,
});

if (!process.env.LIFI_API_KEY && process.env.NODE_ENV === 'production') {
  throw new Error('LIFI_API_KEY is required');
}
```

`ComposeSdkOptions`：

| 选项        | 类型                         | 说明                                                                                       |
| --------- | -------------------------- | ---------------------------------------------------------------------------------------- |
| `baseUrl` | `string`                   | Compose API 的 base URL。SDK 默认为 `https://composer.li.quest`。                              |
| `apiKey`  | `string`                   | 技术预览期间必需。作为 `x-lifi-api-key` header 随每个请求发送。在 [portal.li.fi](https://portal.li.fi) 创建一个。 |
| `fetch`   | `typeof globalThis.fetch?` | 可选。注入一个 polyfill、日志包装器，或重试/退避中间件。                                                        |

### 接入一个 signer

`@lifi/composer-sdk` 本身**不**签名或广播交易。它返回一个 `transactionRequest` 对象（`{ to, data, value, gasLimit? }`），你把它交给你的钱包库。

```ts theme={"system"}
import { createWalletClient, custom } from 'viem';
import { mainnet } from 'viem/chains';

const wallet = createWalletClient({
  chain: mainnet,
  transport: custom(window.ethereum),
});

const [signer] = await wallet.requestAddresses();

const result = await builder.compile({
  signer,
  inputs: { /* … */ },
});

const hash = await wallet.sendTransaction({
  account: signer,
  to: result.transactionRequest.to as `0x${string}`,
  data: result.transactionRequest.data as `0x${string}`,
  value: BigInt(result.transactionRequest.value ?? '0'),
});
```

`run` 中的 `signer` 字段对编译器而言纯粹是提示性的；它据此派生用户的执行 proxy 地址并获取授权。SDK 不会验证该地址是否控制某个私钥。

### 自定义 fetch（可选）

```ts theme={"system"}
const sdk = createComposeSdk({
  baseUrl: 'https://composer.li.quest',
  fetch: async (url, init) => {
    const start = Date.now();
    const res   = await globalThis.fetch(url, init);
    console.log(`[compose] ${init?.method ?? 'GET'} ${url} → ${res.status} in ${Date.now() - start}ms`);
    return res;
  },
});
```

常见用途：指标与追踪、指数退避重试、请求签名、`AbortSignal` 接线。

## 声明 inputs

Inputs 是一个具名记录。每一项要么是一个**资源声明**（token），要么是一个**标量类型名**。

```ts theme={"system"}
import { createComposeSdk, resources } from '@lifi/composer-sdk';

const WETH = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2';

const builder = sdk.flow(1, {
  name: 'my-flow',
  inputs: {
    amountIn: resources.erc20(WETH, 1),
  },
});
```

Builder 以 `builder.inputs.<name>` 的形式暴露类型化 handles：

* `builder.inputs.amountIn` 是一个携带 WETH 资源的 `ResourceInputHandle`。

Inputs 也可以是标量 Solidity 类型（例如 `'address'`、`'uint256'`），每一个都以 `InputHandle<T>` 的形式呈现。仅当之后某个 op 或 `sweepTo` 实际消费某个标量输入时才声明它 —— 例如一个你转发给 `sweepTo` 的 `recipient: 'address'`（参见 [`swapToRecipient.ts`](https://github.com/lifinance/composer-sdk-examples/blob/main/examples/swapToRecipient.ts)）。

`options.name` 是可选的；当省略时，SDK 会为该 Flow 的 `id` 字段生成一个 UUID。资源语义（线性消费、`asResource` 晋升、port 行为）在 [Resource Model](/composer/composer-api/concepts/resources-and-ports) 中介绍。

## 在你的 flow 中链接 ops

SDK 为 Composer API 支持的每个 op 提供一个类型化方法：

* `builder.lifi.swap(id, { bind, config, guards? })`
* `builder.lifi.zap(id, { bind, config, guards? })`
* `builder.core.call(id, { bind, config, ... })`
* `builder.core.asResource(id, ...)`、`builder.core.balanceOf(id, ...)`，以及算术方法 `builder.core.add(id, ...)`、`subtract`、`multiply`、`divideDown`、`divideUp`、`bpsDown`、`bpsUp` —— 每个算术 op 一个方法。

每个方法返回一个以输出 port 名称为键的类型化输出 handles 映射。把一个 call 的输出 handle 直接绑定进另一个 call 的 `bind` 映射：

```ts theme={"system"}
import { resources } from '@lifi/composer-sdk';

const USDC = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48';
const A_ETH_USDC = '0x98C23E9d8f34FEFb1B7BD6a91B7FF122F4e16F5c';

const swap = builder.lifi.swap('swap', {
  bind: { amountIn: builder.inputs.amountIn },
  config: {
    resourceOut: resources.erc20(USDC, 1),
    slippage: 0.03,
  },
});

builder.lifi.zap('zap', {
  bind: { amountIn: swap.amountOut },          // ← thread the swap output straight in
  config: { resourceOut: resources.erc20(A_ETH_USDC, 1) },
});
```

每个方法都是类型化的：把一个 `'resource'` handle 传入一个非资源槽位是编译期错误。SDK 最终序列化为的 dotpath 字符串（例如 `swap.amountOut`）在 [References](/composer/composer-api/concepts/ref-grammar) 中介绍。

### 运行时上下文

编译器在执行时填入的运行时值通过 `builder.context` 访问：

* `builder.context.sender` 是对签名者地址（`address`）的一个类型化 ref。
* `builder.context.executionAddress` 是对预测的执行 proxy 地址（`address`）的一个类型化 ref。

它们最常用于希望以签名者作为收款方的 `bind` 槽位，或作为 `sweepTo` 目标。

### `untypedOp` 应急出口

`builder.untypedOp` 是针对类型化 SDK 面尚未暴露的能力的应急出口 —— 一个生成的方法尚未涵盖的新 op，或一个实验性的 config 字段。它让你直接编写该节点，而不必等待某个 SDK 发布：

```ts theme={"system"}
builder.untypedOp('custom', 'experimental.op', {
  bind: { x: { $ref: 'input.amountIn' } },
  config: { customField: 42 },
});
```

`untypedOp` 返回 `void`，并在其 `bind` 映射中接受原始的 `Ref` 值 —— 没有 handle 转换，没有类型检查。要把一个 `untypedOp` 节点的输出馈送到一个类型化的下游 call 中，用 `raw.ref<T>(path)` 包裹该路径；你提供幻影类型（phantom type），SDK 会在任何 `Bindable<T>` 槽位中接受它（没有运行时校验）：

```ts theme={"system"}
import { raw } from '@lifi/composer-sdk';

builder.core.asResource('wrapped', {
  bind: { handle: raw.ref<'uint256'>('custom.result') },
  config: { resource: resources.erc20(USDC, 1) },
});
```

只要类型化 builder 方法存在，就优先使用它们；仅当它们不存在时才使用 `untypedOp`。

## 接入运行时值

一个 flow 声明每个输入期望*哪种类型*的值。具体的值在编译时通过 `run.inputs` 供应，即当你调用 `builder.compile(run)` 或 `sdk.request(flow, run)` 时。

### 三种输入形态

对于 flow 声明的每个输入，`run.inputs[name]` 必须是以下三种形态之一：

* **`bigint`。** 一个字面数值。用于声明为 `uint256` 的标量输入，以及数量你已预先知晓的资源输入。在线格式上序列化为一个 `0x` 填充的 32 字节十六进制字符串。
* **`string`。** 一个预编码的值。对于标量输入，该字符串必须已符合所声明的类型（例如，`address` 输入用 `0x…` 地址，`uint256` 用十进制或十六进制整数）。
* **`MaterialiserInput`。** 一个描述符 `{ kind: "<materialiser>", …config }`，告诉服务*如何*计算该值（见下文）。

```ts theme={"system"}
const result = await builder.compile({
  signer: '0xYourSignerAddress',
  inputs: {
    minOut: 2_400_000_000n,                                  // literal bigint (uint256 scalar input)
    amountIn: materialisers.directDeposit({                  // materialiser descriptor
      amount: '1000000000000000000',
    }),
  },
  sweepTo: builder.context.sender,
});
```

flow 声明的每个输入都**必须**出现在 `run.inputs` 中。缺失或未知的键会以 `validation_error` 失败。

### materialiser 是什么

**materialiser** 是一个确定性纯函数 `(flow, context) → binding`，它在编译时解析一个 flow 输入。它告诉服务如何在程序开头产生 VM 所消费的 handle：例如，"通过 `transferFrom` 恰好存入 1 WETH"（`directDeposit`），或"在执行时读取签名者的链上 USDC 余额"（`balanceOf`）。

为什么不总是用字面值？

* **该值直到执行时才知道。** `balanceOf` 在交易时读取签名者的余额，这让你能编写像"把我现在持有的任意数量的 USDC 都 zap 掉"这样的 flow。
* **该值需要特定的 VM 指令。** `directDeposit` 不只是推入一个数字；它还烘焙进了把代币拉入 proxy 的 `transferFrom` 语义，以及（对于精确存入而言）一个校验预期数量已到账的相等性检查。

内置的 materialisers：

| Kind            | 接受         | 作用                                                                                                                                                                            |
| --------------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `directDeposit` | `resource` | 存入 VM。原生币通过 `msg.value`，ERC-20 通过 `transferFrom`。`{ amount }` 表示精确；`{ allowNonExact: true }` 表示存入授权所允许的任意数量。                                                                  |
| `balanceOf`     | `resource` | 在执行时对该资源代币读取 `balanceOf(owner)`（对原生币读取原生 `balance`），并将其用作输入数量。                                                                                                                |
| `call`          | `resource` | 执行一个任意合约调用，然后测量执行 proxy 对该资源的余额变化量，并把该变化量用作输入数量。                                                                                                                              |
| `flashloan`     | `resource` | 从一个 flashloan 提供方（`aave-v3`、`erc3156`、`balancer-v2` 或 `morpho-blue`）借入该数量；proxy 在回调时填充它。尚未公开，请联系我们以获得早期访问。详情参见 [`flashloan`](/composer/composer-api/materialisers/flashloan)。 |

关于实时列表和逐 kind 的 config schema，参见 [materialisers catalog](/composer/composer-api/materialisers)。

### 运行时数量的假设

当你使用一个运行时 materialiser（`balanceOf`、`call`，或带 `allowNonExact: true` 的 `directDeposit`）时，编译器在编译时并不知道具体的数量。大多数情况下这没问题；VM 会在执行时读取该数量。但当编译器需要该数量来**规划路由**时（例如，为一次 `lifi.swap` 挑选一个聚合器报价），它会退回到 `run.assumptions[name]`，即一个逐输入的 `bigint` 提示。

```ts theme={"system"}
await builder.compile({
  signer: '0xYourSignerAddress',
  inputs: {
    amountIn: materialisers.balanceOf({ owner: '0xYourSignerAddress' }),
  },
  assumptions: {
    amountIn: 1_000_000_000_000_000_000n,
  },
  sweepTo: builder.context.sender,
});
```

如果某个路由规划依赖一个运行时数量而没有提供假设，编译可能会以 `preparation_error`（HTTP 422）失败。每当你预期运行时数量会与默认值有实质性差异时，都提供一个假设。

## 添加安全性：preconditions 和 guards

一个 flow 的安全故事有两半。**Preconditions（前置条件）** 描述在交易运行*之前*必须为真的状态，作为覆盖馈送给模拟器。**Guards** 是 VM 在交易*期间*强制执行的不变量，被烘焙进 calldata。

### Preconditions：执行前的状态

一个 precondition 是关于链上状态的一个声明式断言。注册了三种类型：

| 类型               | 断言                                                  | Config                                 |
| ---------------- | --------------------------------------------------- | -------------------------------------- |
| `Erc20Balance`   | 某个钱包持有至少给定的 ERC-20 余额                               | `{ wallet, token, balance }`           |
| `NativeBalance`  | 某个钱包持有至少给定的原生币余额                                    | `{ wallet, balance }`                  |
| `Erc20Allowance` | 某个 `owner` 已授予某个 `spender` 至少 `allowance` 的 `token` | `{ owner, spender, token, allowance }` |

SDK 暴露类型化工厂：

```ts theme={"system"}
import { preconditions } from '@lifi/composer-sdk';

preconditions.erc20Balance({
  wallet: '0xYourSignerAddress',
  token: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
  balance: '1000000',
});
preconditions.nativeBalance({ wallet: '0x…', balance: '1000000000000000000' });
preconditions.erc20Allowance({
  owner: '0x…',
  spender: '0x…',
  token: '0x…',
  allowance: '1000000000000000000',
});
```

Preconditions 通过两条路径到达模拟器，并在编译时被拼接起来：

* **显式。** 你在 `run.preconditions` 中传入的任何内容。
* **由 materialiser 产生。** 例如，对一个 ERC-20 资源使用 `directDeposit({ amount })` 会自动发出一个 `Erc20Balance` precondition 和一个 `Erc20Allowance` precondition。

你很少需要显式地重复 materialiser 产生的 preconditions。当 flow 的期望尚未被其 inputs 捕获时才添加显式 precondition，例如当使用 `balanceOf` 来获取一个本应已在 proxy 上的数量时。

在编译时，preconditions 作为**状态覆盖要求（state override requirements）**发送给模拟器。模拟器会打补丁修改存储槽，使钱包拥有指定的余额 / 授权，然后针对该合成状态运行模拟。模拟**不会**仅因为实际链上状态不匹配某个 precondition 而失败；其全部要点就是让模拟器*仿佛*该 precondition 成立那样继续。调用方负责在提交交易之前等待真实状态赶上。

### Guards：执行期间的不变量

Guards 作为 op 调用上的 `guards` 选项传入：

```ts theme={"system"}
import { guards } from '@lifi/composer-sdk';

builder.lifi.zap('zap', {
  bind: { amountIn: builder.inputs.amountIn },
  config: { resourceOut: resources.erc20(A_ETH_USDC, 1) },
  guards: [
    // 100 bps (1%) slippage floor on the zap's amountOut port.
    guards.slippage({ port: 'amountOut', bps: 100 }),
  ],
});
```

Builder 在编译期拒绝与 call 的 port 不兼容的 guards。关于 guards 在底层做什么，参见 [Guards](/composer/composer-api/concepts/simulation-and-guards)；关于每一个已注册的 guard，参见 [guards catalog](/composer/composer-api/guards)。

你可以对同一个 op 附加多个 guard；它们在模拟期间独立运行。当两个 guard 为同一个 port 产生一个最小值时，较大的值胜出，并被报告在 `producedResources[*].simulated.amountOutMin` 上。

不要施加会重复某个 op 内部行为的 guards。每当某个 op 接受 `slippage` config 字段时，就在 op 本身上配置 `slippage`，而不是通过 guard。`lifi.swap` 是典型例子：传入 `slippage: 0.03` 会将 3% 的下限烘焙进聚合器报价，并且 `amountOut` port 携带 `providesMinimum: true`，因此一个冗余的滑点 guard 会在编译期被拒绝。

### 何时用哪个

|           | Preconditions           | Guards                    |
| --------- | ----------------------- | ------------------------- |
| **何时**    | 交易运行之前（用于模拟的状态覆盖）       | 交易期间（链上断言）                |
| **在哪里强制** | 调用方的责任（等待真实状态匹配）        | 编译进交易本身                   |
| **形态**    | 纯数据：`{ type, …config }` | 附加在 op 上、带被观测 port 的不变量   |
| **产生**    | 显式或由 materialiser 产生    | 通过 `CallArgs.guards` 附加   |
| **失败**    | 调用方可见的预检不匹配             | 模拟期的 `guard_error`；执行期的回滚 |

用 **preconditions** 来描述在交易运行*之前*必须为真的东西。用 **guards** 来强制在交易*期间*必须为真的东西。

## 提交 flow

```ts theme={"system"}
const result = await builder.compile({
  signer,
  inputs: {
    amountIn: materialisers.directDeposit({ amount: '1000000000000000000' }),
  },
  sweepTo: builder.context.sender,
});
```

`builder.compile(run)` 是标准的一步式路径：它调用 `builder.build()`，通过 `sdk.request` 构造一个 `ComposeCompileRequest`，并 POST 到 `/compose`。关于更底层的模式（排队、服务端 proxy、签名请求检查），直接调用 `builder.build()` 和 `sdk.request(flow, run)`。参见 [SDK API reference](/composer/composer-api/reference/sdk-api)。

在 SDK 级别**不存在** `sdk.compile()`。Compile 位于 builder 上。

### run 输入

每次提交都传入一个 `ComposeRunInput`：逐输入的值、签名者地址、可选的 preconditions、针对终端资源的 `sweepTo` 目标，以及策略字段（`simulationPolicy`、`checkOnChainAllowances`、`maxPriceImpactBps`）。关于完整的类型签名，参见 [SDK API reference](/composer/composer-api/reference/sdk-api)。

`sweepTo` 是一个兜底项，它把**每一份**残余的 proxy 余额移动到目标 —— 但某些终端资源无法被移动（例如支撑一笔未平仓债务的 aToken）。关于何时改为发出显式转账，参见 [Terminal Resources](/composer/composer-api/concepts/sweeping-and-amounts#non-transferable-terminal-resources)。

### Approvals

`ComposeCompileResult.approvals` 列出在 compose 交易可以执行之前，签名者必须授予的任何 ERC-20 授权。签名者必须为每个输入代币向执行 proxy 授权。先提交这些授权，再提交 compose 交易。

在 `run` 中传入 `checkOnChainAllowances: true`，让服务器检查当前链上授权并省略那些已经充足的授权。

## 你可能会看到的错误

编译期错误：

| Kind                                                      | 原因                                                  |
| --------------------------------------------------------- | --------------------------------------------------- |
| `Missing input "<name>"`（`validation_error`）              | 一个声明的 flow 输入在 `run.inputs` 中没有对应项。                 |
| `Unknown input "<name>"`（`validation_error`）              | `run.inputs` 有一个 flow 未声明的键。检查是否有拼写错误。              |
| `Unknown materialiser "<kind>"`                           | 描述符的 `kind` 未在 manifest 中注册。                        |
| `Materialiser "<kind>" accepts resource inputs only`      | 该 materialiser 是为另一种输入类型定型的。                        |
| `linearity_error`                                         | 两个 op 都将同一个资源 handle 绑定为消费型输入。                      |
| `Guard targets port "<p>" which provides its own minimum` | 对一个其 op 已声明 `providesMinimum` 的 port 附加了最小输出 guard。 |
| `Guard targets port "<p>" which does not exist`           | Port 名称拼写错误或 op 用错了。                                |

模拟期和运行时错误：

| Kind                          | 原因                              |
| ----------------------------- | ------------------------------- |
| `preparation_error`（HTTP 422） | 需要一个运行时数量却没有提供 `assumptions` 值。 |
| `no_route_error`（HTTP 404）    | 路由层无法为某条 `lifi.zap` 边找到路径。      |
| `guard_error`（HTTP 422）       | 某个 guard 断言会在执行时失败。放宽容差或调查该报价。  |
| `simulation_revert`（HTTP 422） | 被模拟的交易在链上回滚。检查回滚详情。             |

在 `simulationPolicy: "allow-revert"` 下，一个 `simulation_revert` 会以 HTTP 206 返回编译后的 calldata *以及*回滚诊断信息，而不是彻底失败。当你想向用户呈现回滚原因时使用它。

关于完整的错误目录，参见 [Errors](/composer/composer-api/reference/error-codes)。

## 另请参阅

* [Quickstart](/composer/composer-api/quickstart) —— 五分钟的首个 flow 演练。
* [Recipes](/composer/composer-api/recipes) —— 三个你可以复制的典型 flow。
* [SDK API reference](/composer/composer-api/reference/sdk-api) —— 详尽的 API 面。
* [Concepts](/composer/composer-api/concepts/flow-anatomy) —— Flow 形态、refs、resources、执行模型、guards。
* [Errors](/composer/composer-api/reference/error-codes) —— 完整的 `ComposeError` 目录。
