首页
/ fuels-rs 交易策略实战指南:用 TxPolicies 定制 Fuel 交易的 Gas、手续费与生效窗口

fuels-rs 交易策略实战指南:用 TxPolicies 定制 Fuel 交易的 Gas、手续费与生效窗口

2026-09-05 16:37:41作者:裘晴惠Vivianne

本文围绕 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 策略使用) 由构建器按输入所有者数量推导

两点值得特别注意(原文档中明确强调的缺省行为):

  1. 当 Script Gas Limit 未设置时,Rust SDK 会在后台估算 gas 消耗,并把估算结果(含容差)设置为该交易的上限;
  2. 当 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.rstx_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_policiescall() 发起异步调用。同样的示例文件(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_defaultexamples/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_limittransaction_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.rspackages/fuels-core/src/types/wrappers/transaction.rsexamples/contracts/src/lib.rs 中查证。
登录后查看全文
热门项目推荐
相关项目推荐