Skip to main content
本指南逐步讲解使用 @lifi/composer-sdk 构建一个 Flow 的每一步:为生产环境配置 SDK、声明 inputs、链接 ops、供应运行时值、附加安全不变量,以及提交。首次集成时请从头读到尾;之后可作为参考回到特定小节。 关于 5 分钟的首个 flow 演示,参见 Quickstart。关于 SDK 类型面,参见 SDK API reference

生产环境设置

配置 SDK

ComposeSdkOptions

接入一个 signer

@lifi/composer-sdk 本身签名或广播交易。它返回一个 transactionRequest 对象({ to, data, value, gasLimit? }),你把它交给你的钱包库。
run 中的 signer 字段对编译器而言纯粹是提示性的;它据此派生用户的执行 proxy 地址并获取授权。SDK 不会验证该地址是否控制某个私钥。

自定义 fetch(可选)

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

声明 inputs

Inputs 是一个具名记录。每一项要么是一个资源声明(token),要么是一个标量类型名
Builder 以 builder.inputs.<name> 的形式暴露类型化 handles:
  • builder.inputs.amountIn 是一个携带 WETH 资源的 ResourceInputHandle
Inputs 也可以是标量 Solidity 类型(例如 'address''uint256'),每一个都以 InputHandle<T> 的形式呈现。仅当之后某个 op 或 sweepTo 实际消费某个标量输入时才声明它 —— 例如一个你转发给 sweepTorecipient: 'address'(参见 swapToRecipient.ts)。 options.name 是可选的;当省略时,SDK 会为该 Flow 的 id 字段生成一个 UUID。资源语义(线性消费、asResource 晋升、port 行为)在 Resource Model 中介绍。

在你的 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, ...)subtractmultiplydivideDowndivideUpbpsDownbpsUp —— 每个算术 op 一个方法。
每个方法返回一个以输出 port 名称为键的类型化输出 handles 映射。把一个 call 的输出 handle 直接绑定进另一个 call 的 bind 映射:
每个方法都是类型化的:把一个 'resource' handle 传入一个非资源槽位是编译期错误。SDK 最终序列化为的 dotpath 字符串(例如 swap.amountOut)在 References 中介绍。

运行时上下文

编译器在执行时填入的运行时值通过 builder.context 访问:
  • builder.context.sender 是对签名者地址(address)的一个类型化 ref。
  • builder.context.executionAddress 是对预测的执行 proxy 地址(address)的一个类型化 ref。
它们最常用于希望以签名者作为收款方的 bind 槽位,或作为 sweepTo 目标。

untypedOp 应急出口

builder.untypedOp 是针对类型化 SDK 面尚未暴露的能力的应急出口 —— 一个生成的方法尚未涵盖的新 op,或一个实验性的 config 字段。它让你直接编写该节点,而不必等待某个 SDK 发布:
untypedOp 返回 void,并在其 bind 映射中接受原始的 Ref 值 —— 没有 handle 转换,没有类型检查。要把一个 untypedOp 节点的输出馈送到一个类型化的下游 call 中,用 raw.ref<T>(path) 包裹该路径;你提供幻影类型(phantom type),SDK 会在任何 Bindable<T> 槽位中接受它(没有运行时校验):
只要类型化 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 },告诉服务如何计算该值(见下文)。
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 的 config schema,参见 materialisers catalog

运行时数量的假设

当你使用一个运行时 materialiser(balanceOfcall,或带 allowNonExact: truedirectDeposit)时,编译器在编译时并不知道具体的数量。大多数情况下这没问题;VM 会在执行时读取该数量。但当编译器需要该数量来规划路由时(例如,为一次 lifi.swap 挑选一个聚合器报价),它会退回到 run.assumptions[name],即一个逐输入的 bigint 提示。
如果某个路由规划依赖一个运行时数量而没有提供假设,编译可能会以 preparation_error(HTTP 422)失败。每当你预期运行时数量会与默认值有实质性差异时,都提供一个假设。

添加安全性:preconditions 和 guards

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

Preconditions:执行前的状态

一个 precondition 是关于链上状态的一个声明式断言。注册了三种类型: SDK 暴露类型化工厂:
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 选项传入:
Builder 在编译期拒绝与 call 的 port 不兼容的 guards。关于 guards 在底层做什么,参见 Guards;关于每一个已注册的 guard,参见 guards catalog 你可以对同一个 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 来强制在交易期间必须为真的东西。

提交 flow

builder.compile(run) 是标准的一步式路径:它调用 builder.build(),通过 sdk.request 构造一个 ComposeCompileRequest,并 POST 到 /compose。关于更底层的模式(排队、服务端 proxy、签名请求检查),直接调用 builder.build()sdk.request(flow, run)。参见 SDK API reference 在 SDK 级别不存在 sdk.compile()。Compile 位于 builder 上。

run 输入

每次提交都传入一个 ComposeRunInput:逐输入的值、签名者地址、可选的 preconditions、针对终端资源的 sweepTo 目标,以及策略字段(simulationPolicycheckOnChainAllowancesmaxPriceImpactBps)。关于完整的类型签名,参见 SDK API reference sweepTo 是一个兜底项,它把每一份残余的 proxy 余额移动到目标 —— 但某些终端资源无法被移动(例如支撑一笔未平仓债务的 aToken)。关于何时改为发出显式转账,参见 Terminal Resources

Approvals

ComposeCompileResult.approvals 列出在 compose 交易可以执行之前,签名者必须授予的任何 ERC-20 授权。签名者必须为每个输入代币向执行 proxy 授权。先提交这些授权,再提交 compose 交易。 run 中传入 checkOnChainAllowances: true,让服务器检查当前链上授权并省略那些已经充足的授权。

你可能会看到的错误

编译期错误: 模拟期和运行时错误: simulationPolicy: "allow-revert" 下,一个 simulation_revert 会以 HTTP 206 返回编译后的 calldata 以及回滚诊断信息,而不是彻底失败。当你想向用户呈现回滚原因时使用它。 关于完整的错误目录,参见 Errors

另请参阅

  • Quickstart —— 五分钟的首个 flow 演练。
  • Recipes —— 三个你可以复制的典型 flow。
  • SDK API reference —— 详尽的 API 面。
  • Concepts —— Flow 形态、refs、resources、执行模型、guards。
  • Errors —— 完整的 ComposeError 目录。