Fuel SDK 实战:使用 Predicate 发送与花费链上资产(Send and Spend Funds From Predicates)
【免费下载链接】fuels-tsFuel Network Typescript SDK项目地址: https://gitcode.com/GitHub_Trending/fu/fuels-ts
Predicate(谓词)是 Sway 中一类返回布尔值的特殊程序,它像一道"规则锁"一样守护链上资产——只有当交易满足谓词预设条件时,资产才允许被转移。本文以 fuels-ts 仓库中 send-and-spend-funds-from-predicates.md 为主线,结合仓库内真实 Sway 源码与 TS 测试片段,完整演示"向谓词地址转账、由谓词条件验证后花费资产、预构建交易、异常与验证失败处理"的整条链路。读完本文你将掌握:如何用forc build产物实例化谓词、如何为谓词充值并执行转移、如何用createTransfer预取交易 ID,以及两类典型失败场景的排查方法。
Predicate 如何"锁定"与"解锁"资产
在 Fuel 网络中,资产可以像发送到普通地址一样发送到 Predicate 的地址(该地址由谓词字节码与数据计算出的根root派生而来)。区别在于:要花掉这些资产,交易必须附上谓词字节码与数据,让链下先执行谓词逻辑,返回true才放行,返回false则整笔交易被拒绝。这一特性在 Predicates 概览 中有清晰阐述:谓词是纯函数、不依赖链上存储,只在收到参数后做出布尔决策,因此校验可以在"上链之前"完成,既降低网络拥堵,也让交易更便宜。
当谓词校验成功后资产才可用;否则 SDK 会抛出验证错误(见下文"Predicate 验证失败"一节)。
第一步:编写一个受控地址的 Sway Predicate
仓库中的示例谓词位于 apps/docs/sway/simple-predicate/src/main.sw,完整逻辑如下:
predicate; fn main(input_address: b256) -> bool { let valid_address = 0xfc05c23a8f7f66222377170ddcbfea9c543dff0dd2d2ba4d0478a4521423a9d4; input_address == valid_address }它的含义非常直接:
- 入口函数
main接收一个b256类型的参数input_address; - 函数体内硬编码了一个"合法地址"
valid_address; - 只有当
input_address与该valid_address完全相等时返回true,否则返回false。
也就是说,只有持有正确地址数据的人,才能触发这笔受控资产的转移。
第二步:编译并收集两大关键产物
在项目目录执行forc build编译上述谓词后,会得到两个必需工件:
- JSON ABI:描述
main函数签名与参数类型,供 SDK 正确编码谓词数据; - 谓词字节码(binary):需要附带在交易中供节点/链下执行。
此外,fuels-ts 的类型生成工具会根据 ABI 自动生成类型安全的谓词类(本仓库文档示例中为SimplePredicate),这样无需手写 ABI 与字节码的加载逻辑即可实例化。
第三步:在 SDK 中实例化谓词并传入数据
关键点:本示例main需要一个名为input_address、类型为B256的参数。该参数正是谓词数据(predicate data),需要在构造谓词时一并传入。代码见 transferring-assets.ts:
import { Provider, Wallet } from 'fuels'; 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 实现 中,构造函数接收
bytecode、abi、provider,以及可选的data与configurableConstants;data会被编码成predicateData字节,供交易携带。 - 谓词地址(
predicate.address)由字节码 + 谓词数据共同推导:数据一旦改变,能花费资金的地址也随之改变。 - 因此原文档特别提示:若想在实例化之后再修改谓词数据,或使用与构造时不同的数据,必须重新创建新的
Predicate实例。这也正是示例代码把data: [inputAddress]放在构造函数里的原因。从源码结构看,谓词数据参与了predicateId/根的计算,所以"改数据 = 换一把新锁",必须新建实例。
关于钱包的创建与资助,可以参考仓库的 钱包使用指南 与 谓词实例化完整说明。
第四步:向谓词地址转入资产
有了可用的钱包(sender)后,就可以像给普通地址转账一样,向predicate.address转入资产,为后续的"条件花费"储备资金(见 transferring-assets.ts#L23-L39):
// 打算发送给谓词的金额 const amountToPredicate = 10_000_000; // 从 sender 钱包向谓词地址转入资产 const fundPredicateTx = await sender.transfer( predicate.address, amountToPredicate, baseAssetId, { gasLimit: 1000, } ); // 等待交易上链 await fundPredicateTx.waitForResult();transfer的三个核心参数是:接收方地址(此处为谓词地址)、转账金额、以及资产 ID(通过provider.getBaseAssetId()获取的基础资产)。
第五步:让谓词"亲自"转出资产
谓词持有资金后,即可用它来验证一笔新的转账交易:直接调用predicate.transfer,把资金转给目标钱包(receiver),见 transferring-assets.ts#L46-L59:
// 从谓词转给接收钱包的金额 const amountToReceiver = 200; // 由谓词发起转账:此时谓词条件会被求值 const transferFromPredicateTx = await predicate.transfer( receiver.address, amountToReceiver, baseAssetId ); await transferFromPredicateTx.waitForResult();同样地,transfer接收两个核心参数:接收方地址与转账金额。SDK 在提交前会把谓词字节码与数据填充进交易输入(对应 predicate.ts 中的输入填充逻辑),链下执行谓词main:
- 若传入的
input_address等于硬编码的valid_address,谓词返回true,资金成功转到receiver; - 若不等,谓词返回
false,SDK 抛出PredicateVerificationFailed错误(详见下文)。
实现层面,
Predicate重写了sendTransaction并在发送前用populateTransactionPredicates把字节码、谓词数据与 witness 写入交易请求,且发送时关闭了自动依赖估算(estimateTxDependencies: false)。transfer/createTransfer则定义在 Account 基类 中,供普通钱包与谓词共用同一套转账 API。
进阶:用createTransfer预构建交易并预取交易 ID
predicate.transfer是一步到位的便捷写法。若需要"先搭好交易、确认无误再提交",可以使用createTransfer。它返回一个ScriptTransactionRequest,随后用sendTransaction提交。预构建的最大好处是:在真正提交之前就能获知交易 ID。仓库 pre-stage.ts 演示了这一流程:
// 预创建一笔从谓词转出的交易 const transactionRequest = await predicate.createTransfer( receiver.address, amountToReceiver, baseAssetId, { gasLimit: 1000, } ); // 提交前即可获得交易 ID const chainId = await provider.getChainId(); const transactionId = transactionRequest.getTransactionId(chainId); // 提交交易并等待结果 const submitTransaction = await predicate.sendTransaction(transactionRequest); await submitTransaction.waitForResult();示例后半段还会校验transactionId === submitTransaction.id,验证"预取 ID 与最终上链交易 ID 一致",这在需要先行签名、多步编排或离线审计的场景下非常实用。Predicate与Account的其他常用方法(如transfer、createTransfer、sendTransaction、simulateTransaction等)可进一步参考 Predicate 方法指南。
失败场景一:试图花费谓词的全部余额
直接转出谓词地址上的全部余额会失败,因为没有剩余资产去支付交易手续费(gas)。此时 SDK 会抛出类似如下报错(见 failure-not-enough-funds.ts):
Insufficient funds or too many small value coins. Consider combining UTXOs. For the following asset ID: '<baseAssetId>'.这个报错提示了两种排查方向:要么为交易预留足够支付手续费的部分资产(即不要转出 100% 余额),要么合并零碎的 UTXO,避免因小额硬币过多而无法凑出可用的资金与手续费组合。示例中该错误通过safeExec(来自fuels/test-utils)捕获后与期望文案比对,以验证错误确实发生。
失败场景二:Predicate 条件不满足(验证失败)
回顾我们的谓词:只有当input_address与硬编码的valid_address相等时才返回true。因此,若在实例化时传入一个随机生成的、不同于valid_address的B256地址,谓词校验必然失败,整笔转账被拒绝。仓库 failure-returns-false.ts 展示了该场景的错误特征:
const inputAddress = getRandomB256(); // 故意用与 valid_address 不同的地址 // ...先给谓词充值,再尝试转账... // 捕获到的错误信息以如下片段开头: const errorMessage = `PredicateVerificationFailed`;也就是说,SDK 抛出的错误以PredicateVerificationFailed开头。只要在测试或日志中看到这一关键字,即可确认是谓词条件未满足(返回false)导致交易被拒,而不是网络或余额问题——这能显著加速排查。
小结与延伸阅读
围绕"发送与花费谓词资产",核心链路可概括为四步:
- 编写 Sway 谓词(
main返回布尔值),执行forc build得到 ABI 与字节码; - 构造
SimplePredicate(或任意生成的谓词类),把main所需的参数通过data传入; - 用普通钱包向
predicate.address转账充值; - 调用
predicate.transfer(或createTransfer+sendTransaction两步式),由谓词条件放行资产;注意留出手续费余额,否则会触发资金不足错误。
谓词机制的边界条件(数据不可变、纯函数、链下校验)使其天然适合做时间锁、多签、白名单地址、条件支付等"可编程资产锁"场景。想深入了解的朋友还可以继续阅读仓库中同一专题下的 Predicate 概览与调试技巧、实例化谓词、部署谓词、可配置常量 以及 自定义交易;对应 Sway 源码与 TypeScript 片段则分别位于 docs/sway/simple-predicate/src/main.sw 与 predicates/snippets/cookbook 目录下,可直接对照运行验证。
【免费下载链接】fuels-tsFuel Network Typescript SDK项目地址: https://gitcode.com/GitHub_Trending/fu/fuels-ts
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考