首页
/ fuels-rs 自定义输入与输出:用 with_inputs / with_outputs / add_signer 定制合约调用交易

fuels-rs 自定义输入与输出:用 with_inputs / with_outputs / add_signer 定制合约调用交易

2026-09-05 11:10:25作者:郁楠烈Hubert

在 Fuel 链上发起合约调用时,fuels-rs 默认会自动根据调用所需的资产与合约推导交易输入(Input)和输出(Output)。当调用场景特殊——例如需要在同一笔交易中消费指定来源的币、引入自定义的 change output,或让第二方钱包对自定义 coin 输入进行签名时——SDK 提供了 with_inputswith_outputs 两个构建方法,以及配套的 add_signer 方法来接管这一推导过程。本文以仓库文档 custom-inputs-outputs.md 为主体,结合 fuels-rs 源码,完整讲清这套 API 的用法、底层数据流与签名机制。

读完本文,你将能够:

  1. 在合约调用链上正确挂载自定义 Vec<Input> / Vec<Output>
  2. 理解自定义输入输出在 SDK 内部从 ContractCall 到最终 ScriptTransaction 的完整流转路径;
  3. 掌握 add_signer 如何让外部签名者对自定义 coin 输入完成 witness 签名。

何时需要自定义 Input / Output

一笔 Fuel 交易由输入(Input:coin、contract、message 等)、输出(Output:coin、contract、change 等)和脚本组成。fuels-rs 在 packages/fuels-programs/src/calls/utils.rstransaction_builder_from_contract_calls 中会自动完成推导:根据 ContractCall 需要的资产向账户取 coin 作为资产输入(asset_inputs),再由 get_transaction_inputs_outputs 生成合约输入、change 输出等。

对于绝大多数简单调用,这条默认路径足够。但存在 SDK 无法替你做决策的场景:

  • 你想指定交易里具体使用哪些输入(例如消费某个特定来源的 UTXO、带特定 predicate 的输入),而不是让 SDK 自动选取;
  • 你想追加自定义的输出(例如自定义 change 流向、额外转账输出);
  • 自定义输入中包含需要签名的 coin,此时必须提供对应的签名者,SDK 才能为这些 coin 输入生成签名 witness。

官方文档对后一种情况给出了明确提示:若自定义输入包含需要签名的 coin,应使用 add_signer 方法添加相应签名者。

基本用法:with_inputs 与 with_outputs

文档给出的核心示例来自 examples/contracts/src/lib.rscustom_assets_example 测试(该测试通过 setup_program_test! 宏准备了两个钱包 wallet / wallet_2,并部署了 MyContract 合约):

let custom_inputs = vec![];
let custom_outputs = vec![];

let _ = contract_instance
    .methods()
    .initialize_counter(42)
    .with_inputs(custom_inputs)     // 附加自定义交易输入 Vec<Input>
    .with_outputs(custom_outputs)   // 附加自定义交易输出 Vec<Output>
    .add_signer(wallet_2.signer().clone()) // 为需要签名的自定义输入提供签名者
    .call()
    .await?;

几个要点:

  • with_inputs / with_outputs构建方法(builder),接收 Vec<Input>Vec<Output>,可任意穿插在 methods()...call() 调用链中;
  • 示例传入了空 vec![],是因为测试目的为验证 API 链路本身;实际使用时你应当构造好需要注入的 Input / Output 值;
  • add_signer(wallet_2.signer().clone()) 传入第二个钱包的签名者,使该钱包能够为自定义 coin 输入签名。

Input / Output 类型由 fuels-core 对外导出,定义在 packages/fuels-core/src/types/input.rs 与 packages/fuels-core/src/types/output.rs(通过 types.rs 聚合导出),它们是 fuel_tx 中交易输入/输出枚举的 SDK 封装。

源码解析:自定义输入输出的流转路径

第一站:ContractCall 结构体

调用链上第一个承载自定义输入输出的位置是 ContractCall 结构体,见 packages/fuels-programs/src/calls/contract_call.rs

pub struct ContractCall {
    pub contract_id: ContractId,
    pub encoded_args: Result<Vec<u8>>,
    pub encoded_selector: Selector,
    pub call_parameters: CallParameters,
    pub external_contracts: Vec<ContractId>,
    pub output_param: ParamType,
    pub is_payable: bool,
    pub custom_assets: HashMap<(AssetId, Option<Address>), u64>,
    pub inputs: Vec<Input>,    // with_inputs 写入这里
    pub outputs: Vec<Output>,  // with_outputs 写入这里
}

对应的两个方法实现非常直接(contract_call.rs#L65-L75):

/// Add custom outputs to the `ContractCall`.
pub fn with_outputs(mut self, outputs: Vec<Output>) -> Self {
    self.outputs = outputs;
    self
}

/// Add custom inputs to the `ContractCall`.
pub fn with_inputs(mut self, inputs: Vec<Input>) -> Self {
    self.inputs = inputs;
    self
}

可以推断,with_inputs / with_outputs整体替换语义:传入的向量直接覆盖 self.inputs / self.outputs,而不是追加。

第二站:CallHandler 的转发与签名者收集

contract_instance.methods().xxx() 返回的调用句柄最终由 CallHandlerpackages/fuels-programs/src/calls/call_handler.rs)统一管理,它同时持有账户(account)、底层调用(call)、交易策略、日志解码器,以及一个关键的字段:

pub struct CallHandler<A, C, T> {
    pub account: A,
    pub call: C,
    pub tx_policies: TxPolicies,
    pub log_decoder: LogDecoder,
    pub datatype: PhantomData<T>,
    decoder_config: DecoderConfig,
    cached_tx_id: Option<Bytes32>,
    variable_output_policy: VariableOutputPolicy,
    unresolved_signers: Vec<Arc<dyn Signer + Send + Sync>>,
}

CallHandler 上再次调用 with_inputs / with_outputs 时,只是把值转发给底层调用(call_handler.rs#L366-L374):

pub fn with_outputs(mut self, outputs: Vec<Output>) -> Self {
    self.call = self.call.with_outputs(outputs);
    self
}

pub fn with_inputs(mut self, inputs: Vec<Input>) -> Self {
    self.call = self.call.with_inputs(inputs);
    self
}

add_signer 并不触碰 call,而是把签名者暂存到 unresolved_signerscall_handler.rs#L84-L87):

pub fn add_signer(mut self, signer: impl Signer + Send + Sync + 'static) -> Self {
    self.unresolved_signers.push(Arc::new(signer));
    self
}

注意“unresolved”这个命名:此刻只是登记签名者身份,真正把它绑定到某个 coin 输入的 witness 索引,发生在后面构建交易的时候。

第三站:TransactionBuilder 与最终交易组装

当执行 .call() 时,CallHandler::transaction_buildercall_handler.rs#L96-L131)完成三件事:

  1. 通过 provider 获取共识参数,并调用 required_assets 计算各资产需求;
  2. 用账户的可花费资源凑出 asset_inputs(默认自动选取的 coin 输入);
  3. 把调用描述转成 ScriptTransactionBuilder,并unresolved_signers 一次性交给 buildertb.add_signers(&self.unresolved_signers)?

ScriptTransactionBuilder 实现了 TransactionBuilder trait(packages/fuels-core/src/types/transaction_builders.rs#L161-L182),其中与本文主题直接相关的接口为:

fn add_signer(&mut self, signer: impl Signer + Send + Sync + 'static) -> Result<&mut Self>;
fn add_signers<'a>(&mut self, signers: ...) -> Result<&mut Self>;
fn with_inputs(self, inputs: Vec<Input>) -> Self;
fn with_outputs(self, outputs: Vec<Output>) -> Self;
fn with_witnesses(self, witnesses: Vec<Witness>) -> Self;
fn inputs(&self) -> &Vec<Input>;
fn outputs(&self) -> &Vec<Output>;

其宏实现(transaction_builders.rs#L268-L276)中,with_inputs / with_outputs 同样是直接覆盖 builder 内部的 inputs / outputs 字段。

最终,ContractCall.inputs / ContractCall.outputs 会汇入交易级输入输出。关键逻辑在 packages/fuels-programs/src/calls/utils.rsget_transaction_inputs_outputs

// Custom `Inputs` and `Outputs` should be placed before other inputs and outputs.
let custom_inputs = calls.iter().flat_map(|c| c.inputs.clone()).collect_vec();
let custom_inputs_len = custom_inputs.len();
let custom_outputs = calls.iter().flat_map(|c| c.outputs.clone()).collect_vec();

let inputs = chain!(
    custom_inputs,
    generate_contract_inputs(contract_ids, custom_outputs.len()),
    asset_inputs
).collect();

let outputs = chain!(
    custom_outputs,
    generate_contract_outputs(num_of_contracts, custom_inputs_len),
    generate_asset_change_outputs(address, asset_ids),
    generate_custom_outputs(calls),
).collect();

由此可以读出两条明确的编排规则:

  1. 自定义输入输出永远排在最前面。输入序列为:custom_inputs → 合约输入(generate_contract_inputs)→ 自动选取的资产输入;输出序列为:custom_outputs → 合约输出 → 资产 change 输出 → custom_assets 产生的 coin 输出。
  2. 索引一致性被显式维护。合约输入通过 output_index 字段引用输出数组中的位置,而 generate_contract_inputs(contract_ids, custom_outputs.len())generate_contract_outputs(num_of_contracts, custom_inputs_len) 都传入了自定义部分的长度作为偏移,从而保证节点侧按 output_index 索引时不错位。源码注释也明确写道:节点收到请求后,会用 output_index 来索引我们发送的 inputs / outputs 数组。

因此,使用 with_inputs / with_outputs 时应当意识到:你注入的元素会整体前移后续自动生成元素的索引位置,SDK 已替你处理了这部分对齐,你不需要(也不应该)手动计算偏移。

add_signer:为自定义 coin 输入提供签名

回到文档的那条 Note。为什么自定义输入需要签名时要 add_signer

TransactionBuilder 的宏实现里,add_signertransaction_builders.rs#L191-L202)做了两件事:

fn add_signer(&mut self, signer: impl Signer + Send + Sync + 'static) -> Result<&mut Self> {
    self.validate_no_signer_available(&signer.address())?;  // 防止重复注册

    let index_offset = self.unresolved_signers.len() as u64;
    self.unresolved_witness_indexes
        .owner_to_idx_offset
        .insert(signer.address().clone(), index_offset);   // 记录签名者 -> witness 偏移
    self.unresolved_signers.push(std::sync::Arc::new(signer));

    Ok(self)
}

从源码结构看,SDK 通过 unresolved_witness_indexes 维护“签名者地址 → witness 索引偏移”的映射:当构建脚本交易时,凡是 coin 输入的拥有者(owner)地址与某个已注册签名者匹配,builder 就会在 witness 列表预留位置,并在签名阶段调用对应 Signer 生成 witness。这也解释了示例中 add_signer(wallet_2.signer().clone()) 的必要性——如果 custom_inputs 里包含属于 wallet_2 且带 owner 的 coin 输入,不注册 wallet_2 的签名者,交易就无法为该输入生成签名。

另外两个值得了解的细节:

  • 重复为同一地址注册签名者会直接报错(validate_no_signer_available),例如默认账户已是主签名者时不能再 add_signer 同一个地址;
  • CallHandler 上可以链式多次 add_signer(多个签名者依次入栈),而 trait 层的 add_signers 支持批量注册。

在脚本调用与交易构建器上的同款 API

with_inputs / with_outputs 并不局限于合约调用。从源码看,同一套 API 也覆盖:

  • 脚本调用ScriptCall 同样实现了 with_inputs / with_outputspackages/fuels-programs/src/calls/script_call.rs),并在构建时将其与 generate_contract_inputs / generate_contract_outputs 组合,索引对齐逻辑与合约调用一致;
  • 底层交易构建器:如果你不经过 methods() 调用链,而是直接操作 ScriptTransactionBuilder 等 builder,也可以调用 trait 上的 with_inputs / with_outputs / with_witnesses 以及只读访问器 inputs() / outputs() 来检查当前输入输出列表(transaction_builders.rs#L172-L178)。

多合约调用(multicall)场景下,CallHandler 的另一组 with_inputs / with_outputs 实现(call_handler.rs#L413-L421)会把输入输出应用到多个 ContractCall 上,随后在 get_transaction_inputs_outputs 中通过 flat_map 汇总所有调用的自定义部分,即每个调用贡献的 inputs / outputs 会被整体拼接。

小结与参考

延伸阅读(均为仓库内相对路径):

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

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.83 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
987
506
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384