fuels-ts Predicates 入门:链下校验的 Sway 布尔程序如何守护与花费链上资产
Predicates(谓词)是 Fuel 上一种特殊的 Sway 程序,它以 bool 作为返回值,本质上是一组交易必须满足的"规则"。本文基于 fuels-ts 仓库文档 apps/docs/src/guide/predicates/index.md 展开,系统讲解 Predicates 的概念模型、与普通账户的差异、"先链下校验、后链上记账"的执行流程,并结合仓库源码与 docs/sway 下的真实 Sway 示例,给出从编译、实例化、充值到花费资金与排错调试的完整实战路径。
Predicates 是什么:返回布尔值的 Sway 程序
在 Sway 中,Predicates 是一种特定类型的程序,其核心特征是返回一个布尔值(true/false)。也就是说,它们的功能相当于一组"规则",一笔交易必须遵守这些规则才能被视为有效。
Predicates 有两个与普通智能合约截然不同的重要特性:
- 无法访问链上信息:它们不能读取区块链上已写入的状态或数据,只能纯粹基于接收到的参数做出判断;
- 是纯函数:执行过程不会产生任何非预期副作用。
正因为如此,Predicates 天然适合做"条件门槛"类校验——例如"只有当 input_address 与预设地址一致时才放行资产"、"只有当传入的 PIN 码正确时才解锁资金"等。
核心设计:先在链下校验,再上链记账
Predicates 与传统"链上检查规则"模式的关键区别在于执行位置与时机:
不是直接在区块链上检查这些规则,而是先在链外(off the blockchain)校验;只有确信校验通过后,才把交易记录到链上。
这种"先本地验证、再提交上链"的做法带来了两方面的收益:
- 更高效:减少了链上重复计算的负担;
- 更便宜、防拥堵:由于把大量重复性的校验逻辑搬到了链下,链上资源占用下降,交易成本随之降低,也缓解了网络拥堵。
从燃料费的角度理解:Predicate 的校验失败不会浪费链上 gas——SDK 在本地/模拟阶段就能发现验证不通过并抛出错误,交易根本不会被作为有效交易打包。
与 Predicates 交互的基本工作方式
存款:像给普通地址转账一样简单
用户可以向 Predicate 的地址发送资产,就像向区块链上的任何其他地址转账一样,无需任何特殊处理。
花费:需要提供原始字节码与 Predicate Data
要花费存放在 Predicate 地址上的资金,用户必须提供:
- Predicate 的原始字节码(byte code);
- Predicate data(如果要求的话)。
Predicate data 与 Predicate main 函数接收的参数一一对应,它会在字节码执行过程中发挥作用。也就是说,data 就是你把"规则需要判断的输入"作为参数传给 main 的方式。
这里有一个重要推论:如果 main 函数没有任何参数,那么也就没有所谓的 data 需要提供。比如下面这个极简 Predicate(源码见 apps/docs/sway/return-true-predicate/src/main.sw):
predicate;
fn main() -> bool {
true
}
它无条件返回 true,main 不接受参数,因此实例化时不需要传入 data。在 TS SDK 中(来自 apps/docs/src/guide/predicates/snippets/instantiation/simple.ts):
import { Provider } from 'fuels';
import { LOCAL_NETWORK_URL } from '../../../../env';
import { ReturnTruePredicate } from '../../../../typegend';
const provider = new Provider(LOCAL_NETWORK_URL);
const predicate = new ReturnTruePredicate({
provider,
});
校验结果:通过则可花费,失败则 SDK 抛错
如果 Predicate 校验成功,资金将被解锁、可以正常花费;反之,如果校验失败,SDK 会抛出一个验证错误(详见后文"校验失败"一节的报错实例)。
Predicate 实例的三个关键属性与"地址即字节码哈希"
一个 Predicate 实例(继承自 Account)除继承的方法外,有三个重要属性:
bytes:predicate 的字节码;chainId:当前网络链 ID;address:由字节码推导出的 Predicate 地址。
关键点在于这个 address 的生成方式——它由字节码哈希而来,对应比特币中 Pay-to-Script-Hash(P2SH)地址的概念。在源码中,构造器正是用处理后的字节码调用 getPredicateRoot(predicateBytes) 得到根哈希、再包装为 Address(见 packages/account/src/predicate/predicate.ts#L86 与 packages/account/src/predicate/utils/getPredicateRoot.ts):
const address = new Address(getPredicateRoot(predicateBytes));
super(address, provider);
这一设计带来一个后续会反复遇到的推论:任何对字节码的改动(例如改变 configurable 常量)都会产生新的哈希与新地址,即生成一个新的 Predicate,而不是修改原有 Predicate 的行为。
实战:让一个 Predicate 校验你的交易
第一步:编写并编译 Sway Predicate
Predicate 与合约一样用 Sway 编写。以 docs 中用于演示"发送与花费资金"的 Predicate 为例(源码见 apps/docs/sway/simple-predicate/src/main.sw):
predicate;
fn main(input_address: b256) -> bool {
let valid_address = 0xfc05c23a8f7f66222377170ddcbfea9c543dff0dd2d2ba4d0478a4521423a9d4;
input_address == valid_address
}
它接收一个 b256 类型的地址参数,与硬编码的 valid_address 比较:相等返回 true,否则返回 false。
编写完成后,使用 forc build 编译。编译产物有两个关键文件:
- JSON ABI(Application Binary Interface);
- Predicate 的二进制代码(bytecode)。
第二步:带 data 实例化 Predicate
在 TypeScript 中,将上述 ABI 与字节码(通常经 TypeGen 生成类型化类)结合,并在构造时传入 main 所需的参数(即 predicate data)。上面的 SimplePredicate 要求传入 b256 类型的 input_address,因此(见 apps/docs/src/guide/predicates/snippets/cookbook/transferring-assets.ts):
import { Provider, Wallet } from 'fuels';
import { LOCAL_NETWORK_URL, WALLET_PVT_KEY } from '../../../../env';
import { SimplePredicate } from '../../../../typegend';
const provider = new Provider(LOCAL_NETWORK_URL);
const baseAssetId = await provider.getBaseAssetId();
const sender = Wallet.fromPrivateKey(WALLET_PVT_KEY, provider);
const receiver = Wallet.generate({ provider });
const inputAddress =
'0xfc05c23a8f7f66222377170ddcbfea9c543dff0dd2d2ba4d0478a4521423a9d4';
const predicate = new SimplePredicate({
provider,
data: [inputAddress],
});
提示:如果你希望在 Predicate 实例化之后才传入 data,或者想使用与构造时不同的 data,就需要新建一个 Predicate 实例(源码也提供了
toNewInstance便捷方法)。data 是实例化时确定并编码进交易输入的。
第三步:向 Predicate 地址充值
用持有足够资金的普通钱包向 predicate.address 转账即可:
// The amount of coins to send to the predicate
const amountToPredicate = 10_000_000;
// Fund the predicate with some funds from our wallet (sender)
const fundPredicateTx = await sender.transfer(
predicate.address,
amountToPredicate,
baseAssetId,
{
gasLimit: 1000,
}
);
// Wait for the transaction
await fundPredicateTx.waitForResult();
第四步:由 Predicate 校验并发起花费
现在 Predicate 持有资金,我们让它验证一笔转账——把资产转给接收者:
// The amount of coins to send from the predicate, to our receiver wallet.
const amountToReceiver = 200;
// Transfer funds from the predicate, to our receiver wallet
const transferFromPredicateTx = await predicate.transfer(
receiver.address,
amountToReceiver,
baseAssetId
);
// Wait for the transaction
await transferFromPredicateTx.waitForResult();
注意 transfer 方法有两个参数:接收者地址与转账金额。当 Predicate 依据其预设条件返回 true 时,花费即成功完成。
花光余额会失败:别忘了交易费
如果试图把 Predicate 持有的全部金额都转出去,会因为没有余额支付交易手续费而失败。SDK 抛出的错误信息形如(见 apps/docs/src/guide/predicates/snippets/cookbook/failure-not-enough-funds.ts):
const errorMessage = `Insufficient funds or too many small value coins. Consider combining UTXOs.\nFor the following asset ID: '${baseAssetId}'.`;
校验失败的情形
回顾我们的 Predicate:只有 input_address 等于硬编码的 valid_address 才会放行。因此,如果传入一个不同的地址,Predicate 将校验失败,SDK 抛出的错误以如下关键词开头(见 apps/docs/src/guide/predicates/snippets/cookbook/failure-returns-false.ts):
const errorMessage = `PredicateVerificationFailed`;
这也对应上文 index 文档所说"若校验未通过,SDK 将抛出验证错误"。
更丰富的交互方式:Predicate 方法全景
Predicate 类继承自 Account 类(见 packages/account/src/account.ts),因而继承了其全部方法。除此之外,Predicate 自带的关键方法与用途可归为三类:查询余额、交易、转账。
查询余额
getBalances:返回 Predicate 拥有的全部资产余额;getResourcesToSpend:返回 Predicate 拥有的、可加入某笔交易请求的资源(UTXO)。该方法在transfer与createTransfer内部被调用,当你需要把 Predicate 塞进一笔已有的交易请求时,可以主动调用它获取资源(getResourcesToSpend 在 predicate.ts 的实现)。
交易相关方法
setData(data):在 Predicate 已实例化后更新 predicate data(即参数)。注意:它只更新 Predicate 实例内部的 data,不会影响已经嵌入了 predicate UTXO 的交易请求——因为每个 predicate UTXO 都携带自己的一份 data 副本。若需修改交易请求内的 predicate data,应随后调用populateTransactionPredicateData(实现见 predicate.ts#L104-L127),该方法会遍历请求中归属于该 Predicate 地址的输入并写入新的字节码、data 与 witness 索引;sendTransaction:向节点发送交易(Predicate 覆盖实现会关闭依赖估算,见 predicate.ts#L135-L141);simulateTransaction:对 Predicate 调用做干跑(dry-run),不消耗真实资源。典型用途是校验余额是否足以支付交易费(predicate.ts#L149-L154)。
转账相关方法
createTransfer:创建包含全部转账细节的交易请求,并通过干跑自动估算成本、以所需 Predicate 资源为请求注资。返回后可先检查/修改请求属性,再提交上链,从而提高成功率;transfer:直接向另一地址发送资金,是最直接的转账入口。
关于 Predicate data 的内部编码:getPredicateData() 会通过 interface.functions.main 的 encodeArguments(this.predicateData) 将传入的参数数组编码为字节;当没有 data 时返回空字节数组(见 predicate.ts#L161-L168)。
可配置常量(Configurable Constants):同一逻辑、不同配置
Predicate 与合约、脚本一样支持 configurable constants,这让同一个 Predicate 可以适配不同的使用场景。典型例子是白名单资产转移校验:只有当接收方地址在预先批准的白名单中,转账才被执行。
对应的 Sway 实现(见 apps/docs/sway/whitelisted-address-predicate/src/main.sw):
predicate;
configurable {
WHITELISTED: b256 = 0xa703b26833939dabc41d3fcaefa00e62cee8e1ac46db37e0fa5d4c9fe30b4132,
}
fn main(address: b256) -> bool {
WHITELISTED == address
}
WHITELISTED 拥有一个代表默认批准地址的默认值。当需要把另一个地址加入白名单时,可在 TS 端更新该常量并让 Predicate 执行转账(见 configurable-set-data.ts):
const configurable = { WHITELISTED: whitelisted.address.toB256() };
// Instantiate predicate with configurable constants
const predicate = new WhitelistedAddressPredicate({
provider,
data: [configurable.WHITELISTED],
configurableConstants: configurable,
});
// Transferring funds to the predicate
const tx1 = await sender.transfer(predicate.address, 200_000, baseAssetId, {
gasLimit: 1000,
});
await tx1.waitForResult();
const amountToTransfer = 100;
// Transferring funds from the predicate to destination if predicate returns true
const tx2 = await predicate.transfer(recipient.address, amountToTransfer, baseAssetId, {
gasLimit: 1000,
});
await tx2.waitForResult();
只要更新后的 WHITELISTED 与目标接收方地址一致,Predicate 即校验通过。若默认白名单地址恰好就是要转账的接收方,则无需更新常量,直接基于默认值即可(见 configurable-default.ts)。
务必记住上文的地址推论:这些定制并不会直接修改原始 Predicate。改变常量会改变字节码,进而改变哈希与地址——你实际得到的是"新配置的新 Predicate",而非行为被改写的旧 Predicate。
自定义交易:Predicate 与 ScriptTransactionRequest 组合
Predicate 逻辑与自定义交易配合,能解锁更复杂的 dApp 用例:先实例化一个自定义交易,再把 Predicate 资源追加进去,最后经由一个通过校验的 Predicate 提交交易。
自定义交易可通过 ScriptTransactionRequest 实例塑形。以"必须使用 configurable PIN 才能解锁资金"的 Predicate 为例(源码见 apps/docs/sway/configurable-pin/src/main.sw):
predicate;
configurable {
PIN: u64 = 1337,
}
fn main(pin: u64) -> bool {
return PIN == pin;
}
TS 端把它纳入一笔自定义交易(见 custom-transactions.ts):
import { Provider, ScriptTransactionRequest, Wallet } from 'fuels';
// Setup
const provider = new Provider(LOCAL_NETWORK_URL);
const sender = Wallet.fromPrivateKey(WALLET_PVT_KEY, provider);
const receiver = Wallet.generate({ provider });
const assetId = await provider.getBaseAssetId();
const amountToFundPredicate = 300_000;
const amountToReceiver = 100_000;
// Instantiate the predicate using valid predicate data, aka the pin we need
// to send the funds to the receiver
const data: ConfigurablePinInputs = [1337];
const predicate = new ConfigurablePin({ provider, data });
// Fund the predicate, so that we can send these funds via predicate logic
// to the receiver
const fundPredicateTx = await sender.transfer(predicate.address, amountToFundPredicate, assetId);
await fundPredicateTx.waitForResult();
// Instantiate the script request
const request = new ScriptTransactionRequest();
// Adding the OutputCoin that represents the funds that we want to send to the receiver
request.addCoinOutput(receiver.address, amountToReceiver, assetId);
// Estimate the transaction cost and fund accordingly
const { assembledRequest } = await provider.assembleTx({
request,
feePayerAccount: predicate,
accountCoinQuantities: [
{
amount: amountToReceiver,
assetId,
account: predicate,
changeOutputAccount: predicate,
},
],
});
// Submit the transaction and await it's result
const predicateTx = await predicate.sendTransaction(assembledRequest);
await predicateTx.waitForResult();
更完整的自定义交易构造说明可参考 Transaction Request 修改指南 对应章节(该部分位于 apps/docs/src/guide/transactions/ 目录下,具体请查阅仓库内 transactions 指南文档)。
部署 Predicate:把字节码存为 blob,降低重复执行成本
对于需要频繁重复执行的 Predicate,官方建议先进行部署以优化成本。部署后,Predicate 的字节码作为 blob 存储在链上;SDK 会生成一个能按需加载该 blob 的"加载器"字节码,用它来执行原始 Predicate。由于上链的字节码更小、重复执行不再重复携带完整代码,反复执行的成本显著降低。
使用 Fuels CLI 的 fuels deploy 命令部署时,会自动完成以下动作:
- 使用当前
forc版本编译 Predicate; - 把编译好的 Predicate 二进制作为 blob 部署上链;
- 生成一个更小的、用于加载已部署 Predicate blob 的新 Predicate;
- 同时为 Predicate 与加载器生成可在应用中直接使用的 TypeScript 类型。
随后在代码中按生成的类型使用(见 deploying-predicates.ts):
// We can deploy dynamically or via `fuels deploy`
const originalPredicate = new ConfigurablePin({
provider,
data: [1337],
});
const { waitForResult: waitForDeploy } = await originalPredicate.deploy(wallet);
await waitForDeploy();
// First, we will need to instantiate the script via it's loader bytecode.
// This can be imported from the typegen outputs that were created on `fuels deploy`.
// Then we can use the predicate as we would normally, such as overriding the configurables.
const loaderPredicate = new ConfigurablePinLoader({
data: [23],
provider,
configurableConstants: {
PIN: 23,
},
});
// Now, let's fund the predicate
const fundTx = await wallet.transfer(loaderPredicate.address, 100_000, baseAssetId);
await fundTx.waitForResult();
// Then we'll execute the transfer and validate the predicate
const transferTx = await loaderPredicate.transfer(receiver.address, 1000, baseAssetId);
const { isStatusSuccess } = await transferTx.waitForResult();
注意:此处既可以通过 CLI 部署,也可以动态调用 Predicate 的 deploy(account) 方法部署(predicate.ts#L367),二者结果一致。
调试 Predicates:先把逻辑写成 Script
目前尚没有直接调试 Predicate 的手段。文档给出的实用替代方案是:
先把 Predicate 当作 Script 编写、测试与调试(Script 拥有更丰富的调试工具),待其行为符合预期后,再把它改回 Predicate。
由于 Predicate 的 main 本质上是一段可被独立执行的纯校验逻辑,这种方法在工程实践中非常有效,能规避"无法单步调试链上校验"的痛点。
多参数与结构体参数:不止一个入参
Predicate 的 main 同样支持多参数与结构体参数。
例如"两个参数不相等才为真"的 Predicate(见 predicate-multi-args/src/main.sw):
predicate;
fn main(arg1: u64, arg2: u64) -> bool {
return arg1 != arg2;
}
TS 端按数组顺序传参即可(见 multi-args.ts):
const predicate = new PredicateMultiArgs({ provider, data: [20, 30] });
又如期望传入结构体的 Predicate(见 predicate-main-args-struct/src/main.sw):
predicate;
struct Validation {
has_account: bool,
total_complete: u64,
}
fn main(received: Validation) -> bool {
let expected_has_account: bool = true;
let expected_total_complete: u64 = 100;
received.has_account == expected_has_account && received.total_complete == expected_total_complete
}
TS 端把结构体作为对象传入 data 数组(见 struct-arg.ts):
const predicate = new PredicateMainArgsStruct({
provider,
data: [{ has_account: true, total_complete: 100 }],
});
总结:Predicate 编程心智模型
围绕"Predicates"这套机制,可以提炼出如下心智模型:
- 它是规则而非逻辑:Predicate 是纯函数、无副作用、不读链上状态,只凭参数下结论;
- 先校验后记账:校验在链下完成,通过后才上链,这是其"便宜、不堵网络"的根源;
- 地址派生自字节码:向地址存款与普通地址无异;花费必须提供原始字节码 + predicate data,二者共同决定校验结果;
- 改配置 = 新 Predicate:configurable constants 修改会派生出新地址,不会改写旧行为;
- 失败由 SDK 报错:校验失败(如参数不匹配、余额不足付手续费)会以
PredicateVerificationFailed或余额不足等错误抛出; - 调试先走 Script:现阶段把校验逻辑写成 Script 调试,成熟后再迁回 Predicate。
围绕 Predicates 的更多细粒度操作,可继续阅读本仓库中 apps/docs/src/guide/predicates/ 目录下的系列文档:实例化 Predicates、与 Predicates 交互的方法、向 Predicates 发送并花费资金、带 Configurable Constants 的 Predicate、Predicate 与自定义交易以及部署 Predicates。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00