fuels-ts 合约调用中的 Variable Outputs:为 Sway 资产转移手动配置 Output Variables
在 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.ts 与 script-transaction-request.ts 的源码,说明 variableOutputs 参数在 SDK 内部如何落到交易请求的 outputs 列表中。读完本文,你能够准确判断哪些合约调用需要 Variable Outputs、如何计算所需数量,并理解 SDK 自动补齐机制的原理及其性能代价。
为什么 Sway 转移函数需要 Output Variable
Sway 提供了功能完备的资产转移函数(例如 std::asset 模块中的 transfer、mint、burn)。在这些 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_address 和 transfer_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_address 与 transfer_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();
逐段解读:
- 部署合约:
TokenFactory.deploy(deployer)部署前面 Sway 合约,并等待部署交易完成拿到contract实例; - 铸造测试资产:先调用
mint_coins(subId, 100)铸造 100 枚代币,得到可转移的余额; - 准备参数:
address是随机生成的收款地址(以{ bits }结构传入 b256),assetId由getMintedAssetId(contract.id.toB256(), subId)推导出该 sub-id 对应的 minted 资产 ID; - 关键一步:
.txParams({ variableOutputs: 1 })—— 由于transfer_to_address内部只调用了一次transfer,因此声明 1 个 Variable Output; - 执行
.call()提交交易,waitForResult等待最终回执。
若一次合约调用内部发生了 N 次转移,则应写 variableOutputs: N。
variableOutputs 参数在 SDK 中的落地链路
variableOutputs 是 TxParams 类型中的一个字段,定义见 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。
实战建议与适用前提
结合文档与源码,实际开发中建议遵循以下规则:
- 先数转移次数,再写
variableOutputs:审查目标合约函数及其调用链中std::asset::transfer(以及类似会向外部账户输出资产的调用)的实际执行次数。若某函数固定执行 3 次转移,就固定传variableOutputs: 3; - 间接调用链同样计入:合约 A 调合约 B、B 再执行转移时,Variable Outputs 依然要按整条链上实际发生的转移总数量声明,因为输出的消耗发生在整个交易执行过程中;
- 动态次数场景依赖自动机制兜底:如果转移次数由运行时状态决定(例如循环转移),无法静态预知数量时,可以省略
variableOutputs,由 SDK 自动补齐;但要接受额外的 dry run 延迟开销; - 本地开发注意环境依赖:官方示例片段依赖
typegen产物(TokenFactory来自typegend)与本地网络环境变量(LOCAL_NETWORK_URL、WALLET_PVT_KEY,见 apps/docs/src/env.ts),在 fuels-ts 文档工程中这些文件由 docs 的构建流程生成;在自己项目中应替换为fuels typegen的输出路径与实际钱包私钥; - 验证方式:提交后通过
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 数量”的线性对应关系,并在可预知的场景下显式声明数量,是兼顾交易成功率与提交性能的正确姿势。
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 StartedRust0624
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