fuels-rs 交易策略实战指南:用 TxPolicies 定制 Fuel 交易的 Gas、手续费与生效窗口
本文围绕 Fuel Network Rust SDK(fuels-rs)中的交易策略(Transaction Policies)展开,覆盖 TxPolicies 结构的完整字段定义、各参数的默认填充行为,以及如何通过链式方法 with_tx_policies 在合约调用、合约部署和资产转账三类场景中配置这些参数,并结合同仓库源码剖析 SDK 在构建交易时如何将这些策略写入最终的 Fuel 交易对象。读完本文,你能够独立地为任意交易设定 tip、gas 上限、手续费上限与生效/过期区块高度,并理解 SDK 在参数缺省时背后的估算与兜底逻辑。
什么是 TxPolicies
在 Fuel 网络上,一笔交易是否被打包、何时生效、最多能烧多少 gas、最多付多少钱,都由交易中的 policies 字段决定。fuels-rs 将这些参数抽象成了 TxPolicies 结构体,定义于 packages/fuels-core/src/types/wrappers/transaction.rs:
pub struct TxPolicies {
tip: Option<u64>,
witness_limit: Option<u64>,
maturity: Option<u64>,
expiration: Option<u64>,
max_fee: Option<u64>,
script_gas_limit: Option<u64>,
owner: Option<u64>,
}
所有字段都是 Option<u64>,这意味着每一项都是可选的:不提供时,SDK 会在交易构建阶段自动估算或取默认值;提供时,则以你指定的值为准。各字段含义如下:
| 字段 | 含义 | 缺省行为 |
|---|---|---|
| Tip | 付给区块生产者以优先打包交易的小费 | 不参与打包决策,仅写入 policies |
| Witness Limit | 交易允许携带的 witness 数据总量上限 | SDK 自动设为当前交易中全部 witness 与签名的总大小 |
| Maturity | 交易在此区块高度之前不能进入区块(延迟生效) | 不设置 |
| Expiration | 超过此区块高度后交易不能再被打包(过期时间) | 不设置 |
| Max Fee | 本交易允许支付的最高手续费 | SDK 通过 dry run 估算实际费用后填入 |
| Script Gas Limit | 执行脚本代码允许消耗的最大 gas | SDK 后台做 dry run 估算消耗并据此设置 |
| Owner | 交易所有者的索引(配合 Owner 策略使用) |
由构建器按输入所有者数量推导 |
两点值得特别注意(原文档中明确强调的缺省行为):
- 当 Script Gas Limit 未设置时,Rust SDK 会在后台估算 gas 消耗,并把估算结果(含容差)设置为该交易的上限;
- 当 Witness Limit 未设置时,SDK 会把它设置为交易构建器中已定义的所有 witness 和签名的总大小。
这两条不是口号,源码可以逐行印证,见后文“底层机制”一节。
构建 TxPolicies:new 与链式 with_* 方法
TxPolicies 没有公开字段(均为私有),只能通过构造函数或链式 builder 方法构建。同一个 事务包装文件 中提供了:
TxPolicies::new(tip, witness_limit, maturity, expiration, max_fee, script_gas_limit, owner)—— 一次性传入全部 7 个Option<u64>参数;TxPolicies::default()—— 全部字段为None的起点;- 链式方法:
with_tip(u64)、with_witness_limit(u64)、with_maturity(u64)、with_expiration(u64)、with_max_fee(u64)、with_script_gas_limit(u64)、with_owner(u64),每个方法都会消费自身并返回修改后的新实例; - 对应的读取器:
tip()、witness_limit()、maturity()、expiration()、max_fee()、script_gas_limit()、owner(),均返回Option<u64>。
实际使用中,最常见的是 TxPolicies::default() 加链式 with_* 的组合写法,只显式设置自己关心的项。
配置合约调用的交易策略:with_tx_policies
配置交易策略的入口是链式方法 with_tx_policies,它定义在 CallHandler 上(约第 55~64 行):
/// Sets the transaction policies for a given transaction.
/// Note that this is a builder method, i.e. use it as a chain:
/// my_contract_instance.my_method(...).with_tx_policies(tx_policies).call()
pub fn with_tx_policies(mut self, tx_policies: TxPolicies) -> Self {
self.tx_policies = tx_policies;
self
}
官方示例位于 examples/contracts/src/lib.rs 的 tx_policies 代码锚点(约第 300~314 行),完整可参考:
let contract_methods = MyContract::new(contract_id, wallet.clone()).methods();
let tx_policies = TxPolicies::default()
.with_tip(1)
.with_script_gas_limit(1_000_000)
.with_maturity(0)
.with_expiration(10_000);
let response = contract_methods
.initialize_counter(42) // Our contract method
.with_tx_policies(tx_policies) // Chain the tx policies
.call() // Perform the contract call
.await?; // This is an async call, `.await` it.
调用模式是固定的三段式:构造 TxPolicies → 在方法句柄上链式挂接 with_tx_policies → call() 发起异步调用。同样的示例文件(examples/contracts/src/lib.rs)中还能看到交易策略与其他调用参数配合的用法——call_params_gas 锚点(约第 562~575 行):
// Set the transaction `gas_limit` to 1_000_000 and `gas_forwarded` to 4300 to specify that
// the contract call transaction may consume up to 1_000_000 gas, while the actual call may
// only use 4300 gas
let tx_policies = TxPolicies::default().with_script_gas_limit(1_000_000);
let call_params = CallParameters::default().with_gas_forwarded(4300);
let response = contract_methods
.get_msg_amount() // Our contract method.
.with_tx_policies(tx_policies) // Chain the tx policies.
.call_params(call_params)? // Chain the call parameters.
.call() // Perform the contract call.
.await?;
这里体现了两个不同层级的 gas 参数分工:TxPolicies::with_script_gas_limit 约束的是整个脚本交易的 gas 上限,而 CallParameters::with_gas_forwarded 控制的是从脚本转发给合约调用的 gas 额度。
多调用(multi-call)场景下,TxPolicies 挂在 CallHandler::new_multi_call 上,同一示例文件约第 606~611 行:
let multi_call_handler = CallHandler::new_multi_call(wallet.clone())
.add_call(call_handler_1)
.add_call(call_handler_2)
.with_tx_policies(TxPolicies::default());
使用 TxPolicies::default()
如果不需要精细控制,直接传入默认值即可,对应示例锚点 tx_policies_default(examples/contracts/src/lib.rs 约第 316~322 行):
let response = contract_methods
.initialize_counter(42)
.with_tx_policies(TxPolicies::default())
.call()
.await?;
TxPolicies 实现了 Default(推导自全 None 字段),此时所有估算类参数(witness limit、script gas limit、max fee)全部交给 SDK 的后台估算逻辑处理,这也是日常使用中最稳妥的起点——只在需要覆盖估算结果(例如固定 gas 预算、控制最大手续费)时才用 with_* 显式设置。
部署合约与转账资产时传入 TxPolicies
交易策略并不只服务于脚本调用。部署合约和资产转账的接口同样接收 TxPolicies 参数,这与文档末尾的提示一致,源码层面可以确认:
合约部署。Contract 的 deploy 方法(约第 146 行)签名为 deploy(self, account: &impl Account, tx_policies: TxPolicies),内部通过 CreateTransactionBuilder::prepare_contract_deployment(...) 把策略注入 CreateTransactionBuilder。示例(examples/contracts/src/lib.rs deploy_with_parameters 锚点,约第 163~178 行):
let tx_policies = TxPolicies::default()
.with_tip(1)
.with_script_gas_limit(1_000_000)
.with_maturity(0)
.with_expiration(10_000);
let contract_id_2 = Contract::load_from(
"../../e2e/sway/contracts/contract_test/out/release/contract_test.bin",
configuration,
)?
.deploy(&wallet, tx_policies)
.await?
.contract_id;
资产转账。Account::transfer(约第 175~181 行)签名为:
async fn transfer(
&self,
to: Address,
amount: u64,
asset_id: AssetId,
tx_policies: TxPolicies,
) -> Result<TxResponse>
e2e 测试 e2e/tests/aws.rs 中的真实用法印证了这一点:wallet.transfer(address, amount, AssetId::zeroed(), TxPolicies::default()).await。
此外,对于大合约的分片部署路径(Contract::load_from 触发的 Blob 上传 + 部署),loader 的 deploy 实现(约第 159~168 行)会把同一个 TxPolicies 同时传给 upload_blobs(每个 Blob 交易)和最终的 deploy,保证一条部署链路上的所有子交易共用一致的策略。
底层机制:SDK 如何把 TxPolicies 变成链上 policies
理解缺省行为的最佳方式是看交易构建器。核心逻辑集中在 packages/fuels-core/src/types/transaction_builders.rs。
1. generate_fuel_policies:策略字段的落位
每种交易构建器(Script / Create / Blob / Upload / Upgrade)都实现了 generate_fuel_policies()(约第 346~361 行):
fn generate_fuel_policies(&self) -> Result<Policies> {
let witness_limit = match self.tx_policies.witness_limit() {
Some(limit) => limit,
None => self.calculate_witnesses_size()?,
};
let mut policies = Policies::default().with_witness_limit(witness_limit);
// `MaxFee` set to `tip` or `0` for `dry_run`
policies.set(PolicyType::MaxFee, self.tx_policies.tip().or(Some(0)));
policies.set(PolicyType::Maturity, self.tx_policies.maturity());
policies.set(PolicyType::Tip, self.tx_policies.tip());
policies.set(PolicyType::Expiration, self.tx_policies.expiration());
policies.set(PolicyType::Owner, self.tx_policies.owner());
Ok(policies)
}
这段代码揭示了两个文档级事实的实现细节:
- Witness Limit 的兜底:未设置时调用
calculate_witnesses_size()(同文件约第 424~432 行),其结果是 builder 中已有 witness 的总大小,加上为每个未解析签名预留的SIGNATURE_WITNESS_SIZE,再按字长向上取整(padded_len_usize)。witness 大小的基础计算在 packages/fuels-core/src/utils.rs(约第 29~36 行):对每个 witness 累加witness.len() + WITNESS_STATIC_SIZE。 - MaxFee 先被置为 tip 或 0:这是 dry run 阶段的占位值,真正的手续费在签名前另行估算覆盖(见下文第 3 点)。
2. set_script_gas_limit:后台估算 gas 上限
文档说“Script Gas Limit 未设置时 SDK 会后台估算”,对应的正是 ScriptTransactionBuilder::set_script_gas_limit(transaction_builders.rs 约第 815~844 行):
- 若用户显式设置了
script_gas_limit,直接采用用户值,即使该值不足导致交易 revert 也不会调整——这是“用户显式值优先”的明确约定; - 若脚本为空(如纯转账脚本),gas 上限直接为 0;
- 否则执行 dry run(若此前为 variable outputs 估算已经跑过 dry run,则直接复用其结果以省一次运行),取
dry_run.gas_with_tolerance(self.gas_estimation_tolerance)作为上限,即估算 gas 加上一份容差。
这也解释了为何在需要控制 gas 的场景(例如 e2e 测试 e2e/tests/contracts.rs 约第 341~346 行用 .with_tx_policies(TxPolicies::default().with_script_gas_limit(gas_limit)) 配合 estimate_transaction_cost)中显式设置该值是常见做法。
3. Max Fee:估算或手动覆盖
在脚本交易解析阶段(约第 741~748 行),构建器先做 dry run 估算,然后:
if let Some(max_fee) = self.tx_policies.max_fee() {
tx.policies_mut().set(PolicyType::MaxFee, Some(max_fee));
} else {
Self::set_max_fee_policy(&mut tx, &dry_runner, ...);
}
即:设置了 with_max_fee 就直接采用;未设置则基于 dry run 的 gas 消耗、gas 价格估算(含 gas_price_estimation_block_horizon 区块视野)和 max_fee_estimation_tolerance 容差计算出费用上限写入交易。构建器还提供 with_gas_estimation_tolerance / with_max_fee_estimation_tolerance 调节估算松紧,合约调用路径默认使用 DEFAULT_MAX_FEE_ESTIMATION_TOLERANCE(见 packages/fuels-programs/src/calls/utils.rs 约第 58~66 行)。
4. Witness Limit 的运行时校验
witness 上限不是摆设,SDK 在追加签名 witness 时会做校验。事务包装器宏生成的 append_witness(约第 496~516 行)会把新增 witness 后的总大小(含 8 字节头部、字长取整)与 tx.witness_limit() 比较,超限时抛出如下校验错误:
transaction validation: Witness limit exceeded. Consider setting the limit manually with a transaction builder. The new limit should be: '{n}'
单元测试 append_witnesses_returns_error_when_limit_exceeded(约第 697~719 行)验证了该报错行为:向 witness 上限不足的交易追加一个 3 字节 witness 时,SDK 提示应把上限设为 16(3 字节数据 + 头部 + 对齐填充)。实战含义:如果你自定义了较小的 witness_limit 又添加了多个签名者/谓词输入,签名阶段就会触发该错误,错误信息里直接给出了应设置的新上限值,照做即可。
小结与使用建议
TxPolicies的 7 个字段(tip、witness_limit、maturity、expiration、max_fee、script_gas_limit、owner)全部可选,日常建议从TxPolicies::default()出发,仅覆盖有明确预算或时序需求的项。- 调用合约时通过
.with_tx_policies(...)链式挂接;部署合约(deploy/deploy_if_not_exists)和转账(transfer)则将其作为独立参数传入。 - 显式设置
script_gas_limit是“硬承诺”:SDK 不会因估算结果不同而覆盖它;不设置则享受后台 dry run 估算(含容差)。 - 自定义
witness_limit时务必为所有签名 witness 预留空间,否则会在append_witness阶段收到携带建议值的校验错误。 - 本文依据的文档为 docs/src/calling-contracts/tx-policies.md,核心实现可继续在 packages/fuels-core/src/types/transaction_builders.rs、packages/fuels-core/src/types/wrappers/transaction.rs 与 examples/contracts/src/lib.rs 中查证。
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 StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00