四个已部署的程序
当前的部署将系统拆分为四个程序,每个程序各司其职。devnet 和 mainnet-beta 上使用相同的程序 id。
程序 ID 与派生账户
程序 ID 标识的是可执行代码。大多数指令还需要持有已部署程序状态的单例程序派生账户(PDA)。这些地址是确定性的,在 devnet 和 mainnet-beta 上相同,因为两个集群使用相同的程序 id 和种子;它们存储的配置内容则是各集群独立的。
Anchor 客户端可以从已发布 IDL 中的种子元数据解析出这些 PDA。其他客户端可以在本地用
PublicKey.findProgramAddressSync([Buffer.from(seed)], programId) 派生它们。普通的 RPC 查询无法推断出一个未知程序的 PDA 种子。
Polymer 的 PDA 存储着它所配置的证明器程序 id,目前在两个集群上都是 CdvSq48QUukYuMczgZAVNZrwcHNshBdtqrjW26sQiGPs。请从 OraclePolymer 账户读取该字段,而不要在订单数据中重复它。链映射、订单 context、填充记录、代币账户和认证都依赖于具体的链或订单,会按需派生;它们不是部署地址。订单 context 和填充记录必须在客户端派生——见程序 IDL。
Wormhole 也已部署在主网上,见部署概览。随附的 Wormhole IDL 是源码接口参考,内嵌地址与主网部署地址不同;不要将其内嵌地址当作主网部署地址。
哪个身份对应哪个字段
订单中每一个 32 字节的身份槽位都指向一个特定账户,程序 ID 和状态 PDA 不能互换。每条规则都对应特定的一端:下表中MandateOutput 各行在该输出位于 Solana 上时成立(EVM 输出携带的则是 EVM 身份);input_oracle 一行在订单是在 Solana 上开启时成立。
对于 Solana → Solana 的同链订单,不涉及任何预言机程序,且只有一条规则:全部三个槽位——
input_oracle、output.oracle 和 output.settler——都填输出结算器的状态 PDA(DHShHmVkTwCzUzAQbCu4GDqJmursuDscNR6o4hTBgeRy)。LI.FI 的构建工具和相关工具都按这种方式编码同链订单。这正是先释放输入的结算所要求的身份,因为该路径会从结算器本身重建全部三个槽位。同一笔订单也可以通过标准的 fill → finalise 路径结算。
在标准路径上,填充产生的
LocalAttestation 会把 output.oracle 记为其消费方。在规范编码下,这个消费方是输出结算器而不是托管程序,因此 finalise 会验证该认证,但不会消费它,其租金(约 0.0012 SOL,由填充者支付)也不会被收回。如果将 output.oracle 设为 input_settler_escrow 的程序 ID,finalise 就能消费该认证并退还租金,但该订单将不再适用于先释放输入的结算。LI.FI 使用的是规范编码。订单结构
每一个 Solana 意图都是一个StandardOrder。用户在 Solana 上承诺一个输入,并描述求解器必须交付的输出——可以在 Solana 上,也可以在某条 EVM 链上。
该结构体携带的是一个输出的
Vec,但在 Solana 上开启的订单实际上只能有一个输出:如果预估的 finalise 交易放不下,open 就会拒绝该订单,而两个输出就已经超出上限(见开启)。源自其他链的订单仍然可以向 Solana 发送多个输出。order_id = keccak256(borsh_serialize(StandardOrder))。任何一方都可以重新计算出它,这也正是下文基于 PDA 的记账方式得以成立的原因。完整的类型定义,包括 Borsh 布局,见程序 IDL。
虽然
MandateOutput.amount 和荷兰式拍卖的 slope 是以 32 字节值传输的,但 Solana 在填充时会将它们收窄为 u64。超过 u64::MAX 的值会使该输出无法被填充(AmountTooLarge / ArithmeticOverflow)。输入结算:input_settler_escrow
输入结算器是一个托管。开启订单会将用户的代币移入一个托管账户;它们只会离开这个账户一次——在 finalise 或 finalise_with_prefill 时流向求解器,或在 refund 或 refund_on_non_fill 时流回用户。这四条路径都会关闭订单的 context PDA,因此一旦其中一条成功执行,其他路径就永远不可能再执行。
程序状态
三种账户类型跟踪一笔订单的生命周期:开启
open(order) 会托管输入:它将 order.input.amount 从用户的代币账户转入一个由订单 OrderContext PDA 所拥有的关联代币账户。该指令需要两个签名者——user,必须等于 order.user 并对代币扣款进行授权;以及 sponsor,负责支付订单各账户的租金。赞助者不必是交易的手续费支付方,也可以与用户是同一个密钥。当订单结算时,赞助者会收回托管代币账户和 order_context 的租金;consumed_order 的租金则永远不会退还——该 PDA 是一个永久性的重放防护——因此在为开启操作提供赞助时,请将其计入每笔订单的成本。
在转移资金之前,open 会验证订单:fill_deadline 不能已经过去(可以等于当前时间),expires 必须严格晚于当前时间且晚于 fill_deadline,订单必须至少有一个输出,链 id 和输入铸币也必须与订单一致。它还会预估最终 finalise 交易的大小,如果该预估值超过 1142 字节——即 1232 字节的数据包上限减去为 ComputeBudget 指令预留的 90 字节余量——就会拒绝该订单(FinaliseTxTooLarge):
callback_data 与 context 合计留出 80 字节(赞助者即用户时为 112 字节)。两个输出的预估至少为 1,332 字节,因此源自 Solana 的订单不能有多于一个输出。
一次成功的 open 会发出一个携带 order_id 和完整 StandardOrder 的 OpenEvent——订单服务器和求解器正是通过这个事件发现链上的开启操作的。
租金退还
每一条结算路径——finalise、finalise_with_prefill、refund、refund_on_non_fill——都会关闭托管代币账户和 order_context,并将两笔租金都退给赞助者。如果赞助者账户是可执行账户(一个程序),两笔租金会改为退给结算签名者:两条完结路径中是 solver,两条退款路径中是 signer。如果该账户同样是可执行账户,指令会以 ExecutableRentRefundAccount 失败。
结算
finalise(order, solve_params) 用于向求解器付款。它会验证每一个输出都已被填充,然后将托管的资金转给求解器选定的目的地,并关闭该订单的各个账户。solve_params 为每个输出携带一条 { solver, timestamp } 记录,按输出顺序排列,且输出 0 的求解器——该订单的规范求解器——必须签名。除托管相关账户外,该指令还需要托管程序的 local_consumer PDA 和 intents_protocol 程序,用于消费以托管程序为消费方的本地认证。
remaining_accounts 为每个输出携带两项:先是每个输出一个认证账户,再是每个输出一个租金退还账户,均按输出顺序排列:
- 同链输出对应的认证是一个
LocalAttestation;跨链输出对应的是一个由该订单的input_oracle创建的Attestation。 - 只有当本地认证的消费方是托管程序(
output.oracle=input_settler_escrow程序 ID)时,租金退还槽位才有意义。此时两个账户都必须可写,且退还账户必须是该认证中存储的rent_refund;finalise会消费该认证并退还其租金。 - 对于其他所有认证——远程认证,以及使用规范编码的同链认证——该槽位必须提供但会被忽略。任何已存在的账户都可以,例如 system program,且两项都可以是只读的。
WrongAttestationCount 失败。交易结构和求解器指南见结算。
结算会发出一个 FinalisedEvent。其 settler 字段是该订单的 order_context PDA,而不是托管单例或程序 ID:
transfer_checked 内部指令中读取结算金额。对于带转账手续费的 Token-2022 铸币,目的地收到的是该金额减去手续费。
先释放输入的结算(finalise_with_prefill)
finalise_with_prefill(order_id) 会在交付输出之前先向求解器付款。它支持单输出的 Solana 订单,其紧凑字段可以重建出规范的 StandardOrder。求解器工作流见先释放输入的 Solana 结算。
该指令会扫描 Instructions sysvar,寻找后续恰好一个 fill_samechain 或 native_fill_samechain。该填充必须是顶层指令,由同一个求解器签名,并携带相同的 order_id。
托管程序会根据紧凑负载和已认证的账户重建出该订单。它会验证订单哈希、确切的账户数量(fill_samechain 为 10 个,native_fill_samechain 为 5 个)、账户地址顺序、规范代币账户、fill_record PDA,以及固定的程序 id。任何不匹配都会使整笔交易失败,并回滚托管资金的释放。
这里不需要检查截止时间。fill_deadline 本身就是 order_id 的一部分,而填充操作自身也会拒绝已过期的截止时间,因此只要某次填充得以执行,就说明它是准时的。
两条结算指令都必须是顶层指令(NotTopLevel)。结算器不会检查它们之间的指令,因此这些指令可以自由地进行兑换、铸造或创建账户。但整笔交易仍必须符合 Solana 在大小、计算量和账户锁定方面的限制。
紧凑填充不会创建 LocalAttestation:结算已经在同一笔交易的前面完成,因此没有什么需要再证明的。它们只会创建 FillRecord。
结算发出的是与 finalise 相同的 FinalisedEvent,其 settler 为 order_context PDA。托管程序会转出其当前的全部余额,因此 open 之后发送到托管账户的代币也会一并付出;请从 transfer_checked 内部指令中读取金额。
退款
托管资金总会通过以下两条无需许可的路径之一,返还给order.user:
refund(order)—— 一旦到达order.expires(now >= expires),任何人都可以调用。refund_on_non_fill(order, output_index)—— 一旦某个输出被证明未被填充,任何人都可以调用,而这最早可以发生在fill_deadline刚过之后。证明一个输出未被填充就会为整个订单退款,因为finalise要求所有输出都被证明已填充。它需要托管程序的local_consumerPDA 和intents_protocol程序,以及恰好两个remaining_accounts:[attestation, rent_refund],其消费或忽略规则与finalise相同。关于未填充证明是如何产生的,见未填充认证。
finalise 会拒绝 timestamp > fill_deadline 的填充(FilledTooLate),并且这些结算路径通过 order_context PDA 互斥。
输出结算:output_settler_simple
输出结算器负责在 Solana 上交付输出,并将每一次交付记录在一个 FillRecord 中;普通填充还会创建一个 LocalAttestation。它支持经典 SPL 代币、兼容的 Token-2022 代币,以及原生 SOL。
订单类型
每个输出的context 字段编码了它的订单类型:第一个字节选择类型,其余部分是特定类型的负载,所有整数均为大端序。
其他任何类型字节都会导致
NotImplemented 失败;负载长度错误会导致 InvalidContextDataLength 失败。这些类型背后的定价公式与 EVM 共用——见拍卖。
紧凑的同链填充只能覆盖其中两种类型:空 context 和独占限价。荷兰式拍卖、独占荷兰式拍卖,或者显式写成 0x00 字节的限价订单,都必须通过 fill 或 native_fill 来处理。
填充
四条指令用于交付输出。标准的一对接受完整的MandateOutput;紧凑的一对接受先释放输入的结算所使用的更小负载。
fill(order_id, mandate_output, fill_deadline, filler_data)—— 用于经典 SPL 和兼容的 Token-2022 输出。它会将代币转给接收方,创建FillRecord和LocalAttestation账户,并发出一个OutputFilledEvent。order_id是输入链的标识符;填充时不会对其进行验证。native_fill(...)—— 用于原生 SOL 输出。输出代币必须是零地址[0; 32];转账通过 system program 进行。fill_samechain(args)和native_fill_samechain(args)—— 同样的两种交付方式,但由CompactSamechainOrder而非MandateOutput驱动。两者都必须作为顶层指令调用(NotTopLevel),都会将签名者记录为求解器而不是读取filler_data字段,并且都只创建FillRecord——不创建LocalAttestation。只能在同一笔交易中一个匹配的finalise_with_prefill之后使用它们。
oracle 和 settler,且不携带任何 callback_data。ExclusiveFor 使用它自身携带的 exclusive_for 公钥;只有 ExclusiveForSelf 会展开为签名者自己的公钥。None 会展开为空 context,因此一个 context 显式写成 0x00 字节的订单无法通过这种方式被填充。
filler_data 是为源链领取而记录的 32 字节求解器 id;它既不是从 Solana 的 filler 签名者派生的,也不会与之比对。请选择一个能够在输入链上为完结授权的身份。特别是当输入位于 Solana 上时,它必须是将调用 finalise 的 Solana 签名者的原始公钥字节。
指令账户和失败条件由程序 IDL 以及上述验证规则定义。
output_settler_simple 要求 callback_data 为空。Solana 上不存在填充后的回调执行。填充记录
每一条填充指令都会创建一个FillRecord PDA——防止重复填充的守卫:它的存在会阻止对同一输出的二次填充。各填充指令将该账户命名为 fill_record;attest_not_filled 则把同一个地址命名为 fill_id。它的种子是 [order_id, mandate_output_hash]——与系统中大多数 PDA 不同,它没有字符串前缀。
mandate_output_hash 是该输出紧凑打包编码(而不是其 Borsh 编码)的 keccak-256 值:
chain_id 和 amount 取其 32 字节大端序值。种子中的 order_id 必须逐字节等于源链的订单标识符:对于源自 Solana 的订单,是订单结构中的 Borsh 哈希;否则是源链自己的订单 ID。以其他任何值记录的填充都无法为该订单结算。
一旦严格超过 close_after,任何人都可以调用 close_fill_record(order_id, output_hash) 关闭该记录,并将其租金退还给记录中的填充者;调用者无法改变租金去向。这个时间点的设计保证:只要某个输出仍可能被创建未填充证明,其记录就无法被关闭。关闭时会发出 FillRecordClosedEvent。过早关闭会以 FillRecordNotClosable 失败。
未填充认证的创建
某个输出的fill_deadline 过后,任何人都可以调用 attest_not_filled 来证明该输出从未被填充。它要求该输出的 FillRecord PDA 尚未初始化——只要填充记录存在,该输出就无法被证明为未填充——并会创建一个”未填充”的 LocalAttestation,从而在输入链上解锁提前退款路径(refund_on_non_fill)。
该认证只能在一个 48 小时的窗口内创建:
FillDeadlineNotPassed 失败;超出窗口后调用会以 NotFilledWindowElapsed 失败。这个窗口限制的是证明的创建时间,而不是它的有效期:在窗口内创建的证明,之后仍可用于 refund_on_non_fill。如果没有人及时创建它,该订单在到期后仍可通过 refund 退款。
一次未填充退款无法被撤销:finalise 会拒绝任何时间戳晚于订单已签名 fill_deadline 的填充(FilledTooLate),而退款会关闭该订单。
认证
在 EVM 链上,输出已被填充的证明存放在验证接口背后的合约存储中。Solana 走的是另一条路:证明就是账户。当一次填充被验证时,协议会创建一个地址由该填充内容派生而来的 PDA——因此预期地址上的账户就是证明。对于LocalAttestation,该账户还必须未被标记为已消费(见消费本地认证)。
一个认证账户并不是一个签名。它是一个确定性的 PDA,只有在某次具体的填充——或某个填充的证明——已按协议规则完成验证时才会被创建。
LocalAttestation在某个输出于 Solana 上通过fill或native_fill被填充时,在 Solana 上创建(紧凑填充不会创建它)。它证明了这次填充发生在它所在的那条链上。Attestation在某个预言机验证了一个远程证明或消息之后,在 Solana 上创建。它代表的是发生在另一条链上的一次填充。
intents_protocol 程序中(IDL:intents_protocol.json)。
调用
PublicKey.findProgramAddressSync(seeds, programId) 时,仅传入上表列出的种子。该函数会自动寻找并返回 bump,不要在输入种子列表中额外添加 bump。重建或验证 PDA 时,再单独使用返回的 bump。
被认证的内容
一份认证锁定的是一个描述某个输出发生了什么的负载。存在两种负载类型,通过一个 4 字节的魔数前缀区分:FillDescription表示某个输出已被填充。它的魔数是keccak("OIF.Fill")的前 4 个字节(0xd1252dff),其布局为MAGIC || solver (32B) || order_id (32B) || timestamp (u32 BE) || common_payload。NotFilledDescription表示某个输出在其截止时间之前未被填充。它的魔数是keccak("OIF.NotFilled")的前 4 个字节(0x830c1e1c),其布局为MAGIC || order_id (32B) || fill_deadline (u32 BE) || common_payload。
common_payload 编码的是输出本身:TOKEN (32B) || AMOUNT (32B) || RECIPIENT (32B) || CALLBACK_LENGTH (u16 BE) + CALLBACK_DATA || CONTEXT_LENGTH (u16 BE) + CONTEXT。这种紧凑打包的大端序编码,与指令参数和账户所使用的 Borsh 编码是分开的。
一份填充描述会以两种方式被哈希,二者的区别在你调试一次失败的完结时很重要:
data_hash,被LocalAttestation使用,是不包含时间戳的描述的 keccak 值。时间戳单独存储在账户中并单独验证。payload_hash,被Attestation使用,是完整描述(包含时间戳)的 keccak 值。
fill_deadline。
长度前缀是
u16,因此 callback_data 和 context 各自的上限是 65,535 字节。该编码与 EVM 实现的 MandateOutputEncodingLib 字节兼容,并由跨实现的测试向量锁定。LocalAttestation:Solana 上的填充
当输出结算器完成一次填充时,它会计算出data_hash,并通过 CPI 调用 intents_protocol 来创建 LocalAttestation。该账户的地址由 b"local_attestation"、attestator(对该 CPI 签名的输出结算器 PDA)、consumer、data_hash 和 PDA 派生过程返回的 bump 派生而来。consumer 是一个 32 字节标识符,标识预期在输出链上消费该证明的对象——通常是输出侧的预言机程序。该种子格式由 intents_protocol 中的 validate_local_attestation(...) 在链上强制执行。
时间戳来自填充发生时的 Solana 时钟,并经过带检查的转换变为 u32(时钟值超出范围会以 InvalidTimestamp 失败)。
该账户存储:
消费本地认证
LocalAttestation 对其约定的消费方而言只能使用一次。当消费方以不可逆的方式使用该证明时,它会通过 intents_protocol::consume_local_attestation 消费该认证:该指令会验证 PDA、存储的时间戳和存储的 rent_refund,然后关闭该账户,并将其租金退还给 rent_refund。为消费签名的 authority 必须是消费方本身,或其程序的 ["local_consumer"] PDA。
其他任何情况——包括规范编码下的同链认证,其消费方是输出结算器——都只会被验证而永远不会被消费,其租金会一直锁定。
如果存储的
rent_refund 是一个可执行账户,消费仍会成功:该账户会被保留并标记为 consumed = true,同时发出 LocalAttestationRentDeferredEvent。之后由 reclaim_local_attestation_rent 将其关闭,其目的地不能是可执行账户。一旦记录中的 rent_refund 不再是可执行账户,任何人都可以调用它,把租金退还给该账户。若要把租金发往其他地址,则需要记录中的 rent_refund 以 rent_refund_authority 的身份签名。只有已消费的认证才能被回收(LocalAttestationNotConsumed)。
已消费的认证——无论是已关闭,还是以 consumed = true 保留——都不再是有效证明,因此不能再被导出,也不能再用于结算。
Attestation:从预言机接收到的证明
当某个预言机程序验证了一个远程证明——一个 Wormhole VAA、一个 Polymer 证明——时,它会通过 CPI 调用intents_protocol,在 Solana 上为每个已验证的负载注册记录。该地址由 b"attestation"、attestator(由 order.input_oracle 指定的输入侧预言机)、chain_id.to_le_bytes()(小端序的 u128)、source、application、payload_hash 和 PDA 派生过程返回的 bump 派生而来。由 validate_attestation(...) 在链上强制执行。
source 和 application 这两个种子,将一份证明与其来源以及是谁发出它绑定在一起:
source—— 输出链上预言机或发出方的 32 字节标识符。application—— 输出结算器的 32 字节标识符,通常就是MandateOutput.settler。
chain_id 来自 output.chain_id,source 来自 output.oracle,application 来自 output.settler。
这正是为什么一笔订单的
MandateOutput.oracle 和 MandateOutput.settler 必须是你所使用的预言机系统在输出链上的正确标识符。如果它们填错了,证明就会落在一个完结逻辑永远不会去查看的 PDA 上。端到端的证明流程
这两种账户类型是同一次中继的两半,分别位于不同的链上。下面的步骤 1 和步骤 2 运行在输出链上(当某个输出在 Solana 上被填充时);步骤 3 和步骤 4 运行在输入链上(当一笔 Solana 订单在等待某个远程填充的证明时)。对于一笔跨链订单,Solana 只扮演这两个角色中的一个。一笔同链的 Solana 订单则会完全跳过这个中继:完结会直接验证该次填充的LocalAttestation(步骤 4)。
1
填充创建 LocalAttestation(输出链)
某个求解器通过
output_settler_simple 在 Solana 上填充一个输出。结算器会转移资产,创建一个用于防止重复填充的 FillRecord PDA,并创建一个锁定该次填充内容和时间戳的 LocalAttestation。2
输出侧预言机对其进行验证(输出链)
在输出链上,预言机会检查
LocalAttestation 是否存在于正确的 PDA 上、是否未被消费,以及其存储的时间戳是否与负载相符:解码 FillDescription,重新计算不含时间戳的 data_hash,验证该 PDA。Polymer 的 submit 只做验证;submit_consume 和 Wormhole 的 submit 还会消费该认证。3
输入侧预言机创建 Attestation(输入链)
在输入链上,预言机会验证它自己的证明格式——一个 Wormhole VAA 或一个 Polymer 证明——并为每一个已验证的
payload_hash 创建一个 Attestation。各预言机的具体细节见预言机系统。4
完结时对每个输出核对一个认证(输入链)
当求解器调用
finalise 时,输入结算器会为每个输出验证一个通过 remaining_accounts 传入的认证账户,同时每个输出还需要一个租金退还槽位。同链输出预期是一个时间戳与该输出 SolveParams.timestamp 相符的 LocalAttestation;跨链输出预期是一个位于由 (input_oracle, chain_id, source, application, payload_hash) 派生出的 PDA 上的 Attestation。未填充认证(提前退款)
同样的两种原语也可以携带”某个输出未被填充”的证明,从而在订单到期之前解锁一次退款:- 在输出链上,
output_settler_simple::attest_not_filled会创建一个LocalAttestation,其data_hash是完整NotFilledDescription的 keccak 值,其存储的时间戳是该输出的fill_deadline。 - 预言机中继它的方式与中继一份填充负载完全相同——
NOT_FILLED_MAGIC前缀会在两端选中”未填充”的哈希规则。 - 在输入链上,
refund_on_non_fill会验证由此产生的LocalAttestation(同链)或Attestation(跨链),并在不等待expires的情况下为该托管退款——见退款。 - 未填充的
LocalAttestation只能在fill_deadline之后 48 小时内创建——见未填充认证的创建。
链配置
intents_protocol 还存储着认证系统所依赖的配置:
ChainId—— 一个单例 PDA(种子"chain_id"),保存该部署的规范链 id。set_chain_id(chain_id)只能被调用一次,任何签名者都可以调用——各部署会将其作为最初的第一步执行,在任何其他人抢先之前完成。各结算器程序会在自己的initialize时复制这个值。ChainMapping—— 一个 PDA(种子为["chain_mapping", oracle, protocol_chain_id_le]),将某个消息传递协议自己的链 id 映射到规范链 id。每个协议都有自己的方案:Polymer 对 EVM 链使用 EVM 链 id,但对 Solana 使用自己的协议 id(2);Wormhole 则使用它自己的u16id。每个预言机的所有者会通过该预言机的set_chain_mapping(protocol_chain_id, chain_id)指令创建映射——仅限所有者调用,且每个映射只能设置一次。跨链认证需要这些映射:如果某个证明来自一条没有映射的链,它就无法被注册。
事件
intents_protocol 会为每一次状态变化发出一个 Anchor 事件,索引器可以订阅它们:
LocalAttestationCreatedEventLocalAttestationConsumedEvent { attestator, consumer, data_hash, rent_refund }LocalAttestationRentDeferredEvent { local_attestation, rent_refund }—— 已消费,但由于rent_refund是可执行账户,租金被暂时保留LocalAttestationRentReclaimedEvent { local_attestation, rent_refund }AttestationCreatedEventChainIdConfiguredEventChainMapConfiguredEvent
input_settler_escrow——OpenEvent、FinalisedEvent(两条完结路径都是)、RefundedEventoutput_settler_simple——OutputFilledEvent、OutputNotFilledEvent、FillRecordClosedEvent { order_id, output_hash, rent_refund }polymer和wormhole——OutputProvenEvent
常见陷阱
- 32 字节标识符填错。 每个体系都有自己的 32 字节表示方式:Solana 的
Pubkey::to_bytes()、左侧补零到 32 字节的 EVM 地址、Wormhole 的 emitter 地址。把它们弄混,会得到一个验证方永远找不到的 PDA。 - 每个身份字段在不同阶段失败。 在填充之前,请对照身份矩阵核对全部三者:
- 如果
MandateOutput.settler填错了,填充会被拒绝。该订单会一直无法填充,直到过期退款。 - 如果
StandardOrder.input_oracle填错了,完结逻辑会用错误的认证方派生出预期的Attestation地址,因此永远找不到预言机实际创建的那个认证。 - 如果
MandateOutput.oracle填错了,填充仍会成功,因为这个字段在填充时不会被检查。求解器的资产会立即离开;但之后的证明提交会因认证被拒绝而失败,求解器也就得不到偿付。
- 如果
- 时间戳不一致。 同链验证要求
LocalAttestation.timestamp == SolveParams.timestamp。如果求解器传入的时间戳与输出结算器存储的不一致,完结就会失败。 - 缺少租金退还槽位。
finalise和refund_on_non_fill要求为每个认证提供一个租金退还槽位,即使该槽位会被忽略。只传入认证会以WrongAttestationCount失败。
同链意图
Solana 到 Solana 的意图完全跳过远程证明。每一笔同链订单都会将输出结算器的状态 PDA 填入全部三个身份槽位(settler、oracle、inputOracle)——见哪个身份对应哪个字段。这样的订单有两条结算路径:
- 标准路径:
fill创建一个LocalAttestation,之后的finalise直接验证它——不涉及任何预言机程序。该认证不会被消费,因此其租金不会被退还。 - 先释放输入: 先释放输入的结算会在交付输出的同一笔交易中向求解器付款,因此求解器可以直接用用户的输入来置办这笔输出,而不必动用自己的余额。紧凑填充不会创建任何认证。该订单还必须满足:
callback_data为空,且 context 为空或为独占限价。EVM 通过finalise的回调达到同样的效果;Solana 没有回调机制,而是需要一条特定的后置填充指令。

预言机
对于跨链订单,预言机程序负责将填充证明从输出链带到输入链,并将其注册为Attestation 账户。Polymer 和 Wormhole 的部署地址见部署概览。请使用订单和路由所配置的预言机。这些实现如何验证证明,见预言机系统;它们所写入的账户模型,见上文的认证。
扩展该系统
这些程序是可组合的。每个具体的程序都封装了一个共享的基础 crate——input_settler_base、output_settler_base 和 oracle_base——由它承载验证逻辑。你可以在同样的基础之上构建新的结算器或预言机,从而使其与系统的其余部分保持兼容。
对于自定义预言机的作者,oracle_base 提供了共享的辅助函数。在输出侧,consume_and_encode_payloads 接收 (LocalAttestation, rent_refund) 对,使用预言机传入的消费权限(Polymer 的 local_consumer PDA,或 Wormhole 自己的状态 PDA)验证并消费每一个认证,再将这些负载编码进一条跨链消息中;consume_payload 处理单个证明。在输入侧,decode_and_attest_payloads 会解码一条预言机已经验证过的消息,并为每个负载创建一个 Attestation 账户。Wormhole 使用它;Polymer 的 receive_attest 则自行解析已证明的 EVM 事件。
程序 IDL
每个程序的 Anchor IDL 都是机器可读的接口参考:包含每一条指令、每一个账户、每一种类型(含 Borsh 布局)、事件,以及该程序自身的错误码。跨程序共享的错误位于单独的目录文件errors.json 中。请在此下载,并用 Anchor 的 TypeScript 客户端加载它们:
上表中四个已部署程序的 IDL 内嵌了前文列出的程序 id。Wormhole 的 IDL 是源码接口参考,其内嵌地址与已公布的主网部署不同。本页未确认该 IDL 与主网部署的兼容性;不要用其内嵌地址替换已部署的程序 ID。
这些是 LI.FI 经过验证的生产 IDL。Solana IDL 包页面提供了该包的其余部分——共享错误目录、来源记录、生成的 TypeScript 类型——以及 IDL 无法表达的规则:哪些 PDA 需要自行派生,以及每条指令确切的
remaining_accounts 布局。
后续步骤
预言机系统
Polymer 如何验证和中继证明
Solana vs EVM
与 EVM 实现的差异及其原因

