fuels-rs 合约调用中的同交易自定义资产转账:add_custom_asset()用法与底层实现
【免费下载链接】fuels-rsFuel Network Rust SDK项目地址: https://gitcode.com/GitHub_Trending/fu/fuels-rs
本篇文章聚焦 Fuel Network 官方 Rust SDK(fuels-rs)在合约调用(contract call)过程中随调用一同转账自定义资产的能力。核心 API 是add_custom_asset():它允许你在同一笔交易里,为合约调用额外指定“资产 ID + 金额 + 目标地址”,实现合约方法与资产转移的原子性提交。读完本文,你将掌握add_custom_asset()的完整调用形态、to参数与None的语义差别,理解 SDK 是如何把“自定义资产”编译成链上交易的 Coin 输入/输出,并能用仓库自带的端到端测试来验证余额变化。
一、为什么需要“随合约调用转账资产”
在 Fuel 上,资产是“携带在调用(frame/call)中”被转交的。日常的合约调用默认只处理基础资产(base asset,即AssetId::zeroed(),可类比原生燃料币),并通过 CallParameters 配置随调用转发的金额。
但业务场景往往不止于此,例如:
- 在调用某个合约方法的同时,把某种自定义代币(非基础资产)直接支付给第三方地址;
- 在同一笔交易中完成“合约状态更新 + 资产划转”,保证二者要么同时成功、要么同时失败;
- 借助签名聚合,让调用与转账共用同一个交易、同一组输入签名,避免二次签名与竞态。
这正是 docs/src/calling-contracts/custom-asset-transfer.md 所介绍的特性:SDK 允许在发起合约调用时,在同一笔交易内指定要转移的资产(asset ID)、数量(amount)与目标地址(destination address)。实现该能力的方法就是add_custom_asset()。
二、add_custom_asset()快速上手
原文档引用的完整示例位于仓库的 examples/contracts/src/lib.rs,摘录核心调用片段(对应文档中的add_custom_assets锚点):
// ANCHOR: add_custom_assets let amount = 1000; let _ = contract_instance .methods() .initialize_counter(42) .add_custom_asset(AssetId::zeroed(), amount, Some(some_addr)) .call() .await?; // ANCHOR_END: add_custom_assets示例所在测试函数的完整上下文(包含钱包与合约的搭建)如下,便于你直接理解运行前提:
#[tokio::test] async fn custom_assets_example() -> Result<()> { use fuels::prelude::*; setup_program_test!( Wallets("wallet", "wallet_2"), Abigen(Contract( name = "MyContract", project = "e2e/sway/contracts/contract_test" )), Deploy( name = "contract_instance", contract = "MyContract", wallet = "wallet" ) ); let some_addr: Address = thread_rng().r#gen(); let amount = 1000; let _ = contract_instance .methods() .initialize_counter(42) .add_custom_asset(AssetId::zeroed(), amount, Some(some_addr)) .call() .await?; // ... Ok(()) }关键点解读
- 调用链:
contract_instance.methods().<合约方法>(...)先选定要调用的合约方法(此处为initialize_counter(42)),随后在.call()之前链式调用.add_custom_asset(...),即“为这笔即将发送的合约调用附加一次资产转移”。 - 参数三元组:
(asset_id, amount, to),分别是资产 ID、转移数量、目标地址。 - 示例中的
some_addr:通过thread_rng().r#gen()生成的随机地址,演示“把资产转给一个非当前钱包的第三方地址”的用法;示例里用的资产是AssetId::zeroed()(基础资产)。
需要注意的是,示例同时展示了与add_custom_asset相邻但作用不同的两个方法:.with_inputs(custom_inputs)与.with_outputs(custom_outputs)(在 examples/contracts/src/lib.rs 的add_custom_inputs_outputs锚点中)。它们分别对应手工指定完整的交易输入/输出这一更低层的能力,详见 custom-inputs-outputs.md;而add_custom_asset()是更高层的便捷封装。
三、方法签名与参数语义
add_custom_asset()定义在 packages/fuels-programs/src/calls/contract_call.rs:
pub fn add_custom_asset(&mut self, asset_id: AssetId, amount: u64, to: Option<Address>) { *self.custom_assets.entry((asset_id, to)).or_default() += amount; }三个参数的含义如下:
| 参数 | 类型 | 含义 |
|---|---|---|
asset_id | AssetId | 要转移的资产 ID。基础资产为AssetId::zeroed(),自定义资产为其各自的 32 字节 ID。 |
amount | u64 | 转移数量。 |
to | Option<Address> | 目标地址。为Some(addr)时 SDK 会生成发往该地址的 Coin 输出;为None时不会为这笔资产生成发往外部地址的 Coin 输出(资产仍作为调用所需的输入随交易进入,具体业务语义由合约与你的交易结构决定)。 |
可被重复调用并自动累加
注意实现中self.custom_assets.entry((asset_id, to)).or_default() += amount这一行:
- 内部用一个
HashMap<(AssetId, Option<Address>), u64>保存所有自定义资产(字段定义见同文件第 24 行); - 键是
(asset_id, to)的组合; - 对同一
(asset_id, to)多次调用add_custom_asset(),数量会累加而不是覆盖。
因此在同一笔调用中你可以连续声明多笔转账,例如把两种不同资产同时转给同一目标,见下文的端到端测试示例。
四、底层实现:自定义资产如何变成链上交易
理解add_custom_asset需要把它放入调用处理与交易组装的主链路中观察。相关逻辑集中在 packages/fuels-programs/src/calls/utils.rs。
1. 自定义资产被纳入“所需资产总额”的计算
在预估这笔调用需要从钱包中取出多少资产时,calculate_required_asset_amounts()(见 utils.rs)会把两类来源合并统计:
- 每个调用的
CallParameters中通过call_parameters.amount()与asset_id()(未指定时为基础资产)表达的基础转发金额; - 每个调用的
custom_assets表里登记的自定义资产。
两者按asset_id分组求和,得到“这笔交易按资产种类分别需要多少余额”。这一步决定了 SDK 在构造输入([Input])时是否要为某种资产额外挑选未花费的 coin(UTXO),也直接影响了钱包的可用余额校验。
2. 自定义资产驱动 Coin 输出的生成
交易输出([Output])由get_transaction_inputs_outputs()(utils.rs)统一编排,其中自定义资产对应的“汇款”动作由generate_custom_outputs()(utils.rs)完成:
fn generate_custom_outputs(calls: &[ContractCall]) -> Vec<Output> { calls .iter() .flat_map(|call| &call.custom_assets) .group_by(|custom| (custom.0.0, custom.0.1)) .into_iter() .filter_map(|(asset_id_address, groups_w_same_asset_id_address)| { let total_amount_in_group = groups_w_same_asset_id_address .map(|(_, amount)| amount) .sum::<u64>(); asset_id_address .1 .map(|address| Output::coin(address, total_amount_in_group, asset_id_address.0)) }) .collect::<Vec<_>>() }该函数清晰揭示了几个实现事实:
- 先按
(asset_id, 目标地址)分组再求和:即使你在多个调用中声明了相同的“资产 + 地址”,最终也只生成一个Coin 输出,金额取总和,链上更省空间; filter_map过滤掉to == None的条目:只有目标地址为Some(address)的自定义资产才会生成Output::coin(address, amount, asset_id)形式的 Coin 输出,直接把amount个asset_id资产打进address;- 顺序约定:自定义输出被放在交易输出的靠前位置(在函数内部先收集
custom_outputs,随后才拼接合约输出与找零输出),而自定义输入/输出在get_transaction_inputs_outputs()中同样被显式注释为“应置于其他输入输出之前”(见 utils.rs),这与 Fuel 交易中输入输出按类型排序的规范一致。
3. 调用对象的生命周期
每次构造合约方法调用时,ContractCall会以custom_assets: Default::default()初始化空表(见 packages/fuels-programs/src/calls/call_handler.rs 与 packages/fuels-programs/src/calls/utils.rs),随后由你在构建期通过add_custom_asset()逐步登记,最终在交易组装阶段被消费。也就是说,add_custom_asset是**构建期(build-time)**设置,必须发生在.call()之前。
五、用仓库端到端测试验证真实转账
仓库在 e2e/tests/contracts.rs 中提供了test_add_custom_assets端到端测试,可直接用于验证本特性的完整行为。其核心思路与断言如下:
1. 构造多资产钱包
测试为两个钱包分别准备三种资产的 coin(每种num_coins: 1、初始coin_amount: 100_000):基础资产AssetId::zeroed(),以及两种自定义资产AssetId::from([3u8; 32])与AssetId::from([1u8; 32])(见 e2e/tests/contracts.rs)。
2. 在同一调用中附加两笔自定义资产转账
let amount_1 = 5000; let amount_2 = 3000; let response = contract_instance .methods() .get(5, 6) .add_custom_asset(asset_id_1, amount_1, Some(wallet_2.address())) .add_custom_asset(asset_id_2, amount_2, Some(wallet_2.address())) .call() .await?; assert_eq!(response.value, 11);这里清晰地演示了“合约方法照常执行 + 多种自定义资产一并转移”的组合:get(5, 6)返回 11 不受影响,同时两笔不同资产的转账在同一笔交易内完成。
3. 余额断言
let balance_asset_1 = wallet_1.get_asset_balance(&asset_id_1).await?; let balance_asset_2 = wallet_1.get_asset_balance(&asset_id_2).await?; assert_eq!(balance_asset_1, (initial_amount - amount_1) as u128); assert_eq!(balance_asset_2, (initial_amount - amount_2) as u128); let balance_asset_1 = wallet_2.get_asset_balance(&asset_id_1).await?; let balance_asset_2 = wallet_2.get_asset_balance(&asset_id_2).await?; assert_eq!(balance_asset_1, (initial_amount + amount_1) as u128); assert_eq!(balance_asset_2, (initial_amount + amount_2) as u128);转出方wallet_1的两种资产各减少对应数量,转入方wallet_2的两种资产各增加对应数量——这从链上余额角度验证了Output::coin(address, total_amount, asset_id)确实按预期产出并上链。需要补充说明的是,测试通过WalletsConfig::new_multiple_assets(...)让钱包预先持有足够余额,这是因为燃料机制要求交易的每一种输入资产都必须有对应的 UTXO 作为来源。
六、适用边界与相关主题指引
在使用add_custom_asset()时,建议结合以下几点判断是否适用:
- 需要“按交易原子转移给第三方”时优先使用它;若目标只是“给合约本身转发某资产”,通常用
CallParameters更直接(见 call-params.md)。 - 需要完全掌控输入输出结构(例如自定义签名输入、手工输出类型)时,可退而使用 with_inputs/with_outputs,并通过
add_signer为交易补充签名(示例见 examples/contracts/src/lib.rs)。 - 变量数量输出(variable output)是另一种与资产转移相关的输出类型,适合“数量在构造时未知”的转账场景,见 variable-outputs.md;而本特性要求你在构建期就明确指定
amount。
参考路径速查
- 特性文档:docs/src/calling-contracts/custom-asset-transfer.md
- 示例代码:examples/contracts/src/lib.rs
- 核心实现:packages/fuels-programs/src/calls/contract_call.rs
- 输入输出组装:packages/fuels-programs/src/calls/utils.rs
- 调用构建初始化:packages/fuels-programs/src/calls/call_handler.rs
- 端到端验证:e2e/tests/contracts.rs
- 示例所用 Sway 合约:e2e/sway/contracts/contract_test/src/main.sw
以上链接覆盖“文档 → 示例 → SDK 实现 → 合约 → 测试”的完整证据链,你可以顺着任一环节在仓库中继续深入阅读。
【免费下载链接】fuels-rsFuel Network Rust SDK项目地址: https://gitcode.com/GitHub_Trending/fu/fuels-rs
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考