首页
/ fuels-ts 合约调用中的 Variable Outputs:为 Sway 资产转移手动配置 Output Variables

fuels-ts 合约调用中的 Variable Outputs:为 Sway 资产转移手动配置 Output Variables

2026-09-05 19:26:50作者:温玫谨Lighthearted

在 Fuel 网络上调用 Sway 合约时,凡是涉及资产转移(transfer / mint / burn)的合约函数,都会要求交易(Transaction)携带对应数量的 Variable Outputs(Output Variable)。本文围绕 fuels-ts 官方文档 Variable Outputs 展开,先讲清“为什么转移函数需要 Output Variables”这一 FuelVM 交易格式层面的约束,再给出在 TypeScript SDK 中通过 txParams({ variableOutputs: N }) 手动添加的完整可运行示例,并深入 base-invocation-scope.tsscript-transaction-request.ts 的源码,说明 variableOutputs 参数在 SDK 内部如何落到交易请求的 outputs 列表中。读完本文,你能够准确判断哪些合约调用需要 Variable Outputs、如何计算所需数量,并理解 SDK 自动补齐机制的原理及其性能代价。

为什么 Sway 转移函数需要 Output Variable

Sway 提供了功能完备的资产转移函数(例如 std::asset 模块中的 transfermintburn)。在这些 Sway 项目中调用这些转移函数时,必须注意一个交易格式层面的约束:

  • 每一次转移函数调用,都要求交易 Outputs 列表中存在一个对应的 Output Variable
  • 数量是线性对应的:如果一个合约函数内部调用了 3 次 Sway 转移函数,交易就必须携带 3 个 Output Variables
  • 不仅包括“直接调用”,也包括间接链路——当你的合约函数调用了另一个会(直接或间接)触发这些转移函数的函数时,同样需要按实际发生的转移次数添加相应数量的 Output Variables。

从源码结构看,这类输出在 fuels-ts 中被建模为 OutputType.Variable 类型的交易输出,最终由 script-transaction-request.ts 中的 addVariableOutputs 方法批量推入交易的 outputs 数组,供节点在交易校验阶段使用。

示例:哪些 Sway 函数会要求 Output Variable

官方文档在 apps/docs/sway/token/src/main.sw 中给出了一个典型的 Token 合约,其中 transfer_to_addresstransfer_to_contract 两个函数都会触发 Variable Output 需求:

contract;

use std::asset::{burn, mint, transfer};

abi Token {
    fn transfer_to_address(target: Address, asset_id: AssetId, coins: u64);
    fn transfer_to_contract(recipient: ContractId, asset_id: AssetId, coins: u64);
    fn mint_coins(sub_id: b256, mint_amount: u64);
    fn burn_coins(sub_id: b256, burn_amount: u64);
}

impl Token for Contract {
    fn transfer_to_address(recipient: Address, asset_id: AssetId, amount: u64) {
        transfer(Identity::Address(recipient), asset_id, amount);
    }

    fn transfer_to_contract(target: ContractId, asset_id: AssetId, amount: u64) {
        transfer(Identity::ContractId(target), asset_id, amount);
    }

    fn mint_coins(sub_id: b256, mint_amount: u64) {
        mint(sub_id, mint_amount);
    }

    fn burn_coins(sub_id: b256, burn_amount: u64) {
        burn(sub_id, burn_amount);
    }
}

要点在于:transfer_to_addresstransfer_to_contract 各自调用了一次 std::asset::transfer,因此每调用一次这两个函数中的任一个,就需要一个 Output Variable;若合约逻辑在一个函数里连续执行多次转移,则按次数累加。mint / burn 本身不对外部地址转出资产,不会因此产生 Variable Output 需求(它们的作用体现在合约余额与资产铸造上,可参考同目录下的 minted-token-asset-id 文档示例)。

手动为合约调用添加 Variable Outputs

当你的合约调用了上述转移函数(或经由间接调用链触发转移)时,需要在发起调用时通过 txParams 显式指定 variableOutputs。官方示例位于 variable-outputs.ts,完整流程如下:

import { Provider, Wallet, getMintedAssetId, getRandomB256 } from 'fuels';

import { LOCAL_NETWORK_URL, WALLET_PVT_KEY } from '../../../../env';
import { TokenFactory } from '../../../../typegend';

const provider = new Provider(LOCAL_NETWORK_URL);
const deployer = Wallet.fromPrivateKey(WALLET_PVT_KEY, provider);

const deployContract = await TokenFactory.deploy(deployer);
const { contract } = await deployContract.waitForResult();

const subId = getRandomB256();

const call1 = await contract.functions.mint_coins(subId, 100).call();
await call1.waitForResult();

const address = { bits: Wallet.generate().address.toB256() };
const assetId = { bits: getMintedAssetId(contract.id.toB256(), subId) };

const { waitForResult } = await contract.functions
  .transfer_to_address(address, assetId, 100)
  .txParams({
    variableOutputs: 1,
  })
  .call();

await waitForResult();

逐段解读:

  1. 部署合约TokenFactory.deploy(deployer) 部署前面 Sway 合约,并等待部署交易完成拿到 contract 实例;
  2. 铸造测试资产:先调用 mint_coins(subId, 100) 铸造 100 枚代币,得到可转移的余额;
  3. 准备参数address 是随机生成的收款地址(以 { bits } 结构传入 b256),assetIdgetMintedAssetId(contract.id.toB256(), subId) 推导出该 sub-id 对应的 minted 资产 ID;
  4. 关键一步.txParams({ variableOutputs: 1 }) —— 由于 transfer_to_address 内部只调用了一次 transfer,因此声明 1 个 Variable Output;
  5. 执行 .call() 提交交易,waitForResult 等待最终回执。

若一次合约调用内部发生了 N 次转移,则应写 variableOutputs: N

variableOutputs 参数在 SDK 中的落地链路

variableOutputsTxParams 类型中的一个字段,定义见 types.ts

export type TxParams = Partial<{
  tip: BigNumberish;
  gasLimit: BigNumberish;
  maturity?: number;
  expiration?: number;
  maxFee?: BigNumberish;
  witnessLimit?: BigNumberish;
  variableOutputs: number;
}>;

BaseInvocationScope.txParams 中,调用 txParams 时会将该值直接写入交易请求:

txParams(txParams: TxParams) {
  this.txParameters = txParams;
  const request = this.transactionRequest;

  request.tip = bn(txParams.tip || request.tip);
  request.gasLimit = bn(txParams.gasLimit || request.gasLimit);
  // ...
  request.addVariableOutputs(this.txParameters?.variableOutputs || 0);

  return this;
}

addVariableOutputs 的实际实现位于 script-transaction-request.ts:按给定数量循环向 outputs 数组 push 若干个 OutputType.Variable 类型的输出:

addVariableOutputs(numberOfVariables: number = 1) {
  let outputsNumber = numberOfVariables;

  while (outputsNumber) {
    this.pushOutput({
      type: OutputType.Variable,
    });
    outputsNumber -= 1;
  }

  return this.outputs.length - 1;
}

也就是说,variableOutputs: 1 会精确地在交易请求的 outputs 末尾追加 1 个 Variable Output;如果未指定(undefined),上式中 this.txParameters?.variableOutputs || 0 使追加数量为 0,即不做任何手动添加,交由后续的自动机制处理(见下一节)。

SDK 的自动补齐机制:暴力式 Dry Run

文档明确指出:TypeScript SDK 会自动把 Variable Outputs 添加到交易的 outputs 列表中。其采用的策略是暴力试探——执行顺序的 dry runs,每次递增 Variable Outputs 数量,直到 dry run 不再报错为止,从而确定处理该交易所需的 Output Variables 数量。

在源码中可以印证这一“自动探测”痕迹:base-invocation-scope.ts 中保留的 legacy 资金装配路径会先通过 getTransactionCost() 发起成本估算(内部依赖 dry run),从返回的 txCost 中解构出 outputVariables,再调用 transactionRequest.addVariableOutputs(outputVariables) 补齐所需数量:

const txCost = await this.getTransactionCost();
const { gasUsed, missingContractIds, outputVariables, maxFee } = txCost;
this.setDefaultTxParams(transactionRequest, gasUsed, maxFee);

// Adding required number of OutputVariables
transactionRequest.addVariableOutputs(outputVariables);

这条路径表明:当开发者没有手动指定数量时,SDK 需要额外发起估算/dry run 请求才能确定数量。文档同时给出了明确的性能警告:

这种暴力式策略会显著延迟交易处理,因此强烈建议在提交交易前手动添加正确数量的 Variable Outputs。

实战建议与适用前提

结合文档与源码,实际开发中建议遵循以下规则:

  1. 先数转移次数,再写 variableOutputs:审查目标合约函数及其调用链中 std::asset::transfer(以及类似会向外部账户输出资产的调用)的实际执行次数。若某函数固定执行 3 次转移,就固定传 variableOutputs: 3
  2. 间接调用链同样计入:合约 A 调合约 B、B 再执行转移时,Variable Outputs 依然要按整条链上实际发生的转移总数量声明,因为输出的消耗发生在整个交易执行过程中;
  3. 动态次数场景依赖自动机制兜底:如果转移次数由运行时状态决定(例如循环转移),无法静态预知数量时,可以省略 variableOutputs,由 SDK 自动补齐;但要接受额外的 dry run 延迟开销;
  4. 本地开发注意环境依赖:官方示例片段依赖 typegen 产物(TokenFactory 来自 typegend)与本地网络环境变量(LOCAL_NETWORK_URLWALLET_PVT_KEY,见 apps/docs/src/env.ts),在 fuels-ts 文档工程中这些文件由 docs 的构建流程生成;在自己项目中应替换为 fuels typegen 的输出路径与实际钱包私钥;
  5. 验证方式:提交后通过 waitForResult() 返回的 transactionResult.isStatusSuccess 检查交易状态;若 Variable Outputs 数量不足,交易在节点校验/执行阶段会失败,这正是自动机制反复 dry run 才会收敛的原因。

小结

Variable Outputs 是 Fuel 交易格式对“合约执行期向外部输出资产”这一行为的静态声明:Sway 中每调用一次 std::asset 的转移函数,就需要一个对应的 Output Variable。fuels-ts 通过 txParams({ variableOutputs: N }) 提供了手动声明入口,底层由 BaseInvocationScope.txParams 调用 ScriptTransactionRequest.addVariableOutputs 将指定数量的 OutputType.Variable 输出推入交易请求;不声明时,SDK 会回退到基于顺序 dry run 的暴力探测自动补齐,但会带来可观的延迟。理解“转移次数 ↔ Variable Output 数量”的线性对应关系,并在可预知的场景下显式声明数量,是兼顾交易成功率与提交性能的正确姿势。

登录后查看全文
热门项目推荐
相关项目推荐