fuels-rs 多钱包合约调用实战:用 with_account 为同一合约实例切换调用钱包
在 fuels-rs(Fuel Network Rust SDK)中,通过 abigen! 生成的合约实例会把一个钱包(账号)与 ContractId 绑定在一起,之后所有调用都由该钱包出资、签名。当业务需要“用不同的钱包轮流调用同一个合约”——例如多角色分摊 gas 费用、不同授权方分别执行操作、或让另一个账户来支付交易费——时,SDK 提供了 with_account() 方法:它接受一个已有的合约实例,返回一个连接到新钱包的新实例,从而支持链式地在不同钱包之间切换调用方。本文基于仓库文档 calls-with-different-wallets.md 及其对应的示例、测试与代码生成源码,完整讲解该能力的用法、边界与底层实现。
核心概念:with_account 是什么、不是什么
with_account() 是 abigen! 宏为合约实例生成的便捷方法。官方文档给出的定义是:
你可以在现有合约实例上使用
with_account()方法,作为“创建一个连接到指定钱包的新实例”的简写形式。这让你可以像链式调用一样,用不同的钱包发起合约调用。
从源码结构看,它并不改变合约本身(ContractId 不变),也不改变任何链上状态,只是替换了实例中持有“谁来出钱、谁来签名”的账号字段。因此它有两个典型应用场景:
- 费用分担:合约由钱包 A 部署,但希望钱包 B 为某次调用支付 gas 费;
- 多角色交互:同一份合约代码被多方使用,每方用自己的身份账户执行方法调用(合约内可通过
msg_sender感知调用方不同)。
一个重要的“不是什么”:with_account() 返回的是一个新实例,原实例保持不变(该方法按值消费 self,但 Rust 的 Wallet/Predicate 均可 Clone,实际使用中不影响原引用)。
实战示例:同一个合约,两个钱包轮流调用
以下代码完整继承自仓库示例 examples/contracts/src/lib.rs 中的 connect_wallet 测试,也是文档中 {{#include ...:connect_wallet}} 指向的锚点代码:
use fuels::prelude::*;
abigen!(Contract(
name = "MyContract",
abi = "e2e/sway/contracts/contract_test/out/release/contract_test-abi.json"
));
// 创建 2 个测试钱包,每个持有 DEFAULT_COIN_AMOUNT 的基础资产
let config = WalletsConfig::new(Some(2), Some(1), Some(DEFAULT_COIN_AMOUNT));
let mut wallets = launch_custom_provider_and_get_wallets(config, None, None).await?;
let wallet_1 = wallets.pop().unwrap();
let wallet_2 = wallets.pop().unwrap();
// 由 wallet_1 负责部署合约
let contract_id = Contract::load_from(
"../../e2e/sway/contracts/contract_test/out/release/contract_test.bin",
LoadConfiguration::default(),
)?
.deploy(&wallet_1, TxPolicies::default())
.await?
.contract_id;
// Create contract instance with wallet_1
let contract_instance = MyContract::new(contract_id, wallet_1.clone());
// Perform contract call with wallet_2
let response = contract_instance
.with_account(wallet_2) // Connect wallet_2
.methods() // Get contract methods
.get_msg_amount() // Our contract method
.call() // Perform the contract call.
.await?; // This is an async call, `.await` for it.
要点解读:
WalletsConfig::new(Some(2), Some(1), Some(DEFAULT_COIN_AMOUNT))一次性启动本地节点并创建 2 个各带 1 枚测试币的钱包;- 合约由
wallet_1部署,实例初始绑定wallet_1; .with_account(wallet_2)之后,后续的get_msg_amount()调用由wallet_2构建交易输入、支付费用并签名——原实例仍然绑定wallet_1,可继续用于其他调用。
同样的模式也适用于 Sway 脚本实例:abigen!(Script(...)) 生成的类型同样带有 with_account(),用于切换执行脚本时出资的账号(详见 docs/src/running-scripts.md)。
源码剖析:abigen 生成的绑定里发生了什么
with_account() 并非运行时动态修改实例,而是由代码生成器 packages/fuels-code-gen/src/program_bindings/abigen/bindings/contract.rs 为每个合约类型展开的固定模板。生成的结构体长这样:
#[derive(Debug, Clone)]
pub struct MyContract<A = ()> {
contract_id: ContractId,
account: A, // 泛型账号,默认 ()
log_decoder: LogDecoder,
encoder_config: EncoderConfig,
}
其上的关键方法(由生成器产出):
new(contract_id, account):创建实例,账号类型由调用方决定;methods():要求A: Clone,返回MyContractMethods<A>,每个合约方法最终都会执行CallHandler::new_contract_call(self.contract_id.clone(), self.account.clone(), ...)——调用方账号正是在这里进入交易构建流程;with_account:
pub fn with_account<U: fuels::accounts::Account>(self, account: U) -> MyContract<U> {
MyContract {
contract_id: self.contract_id, // 合约 ID 原样保留
account, // 换成新账号
log_decoder: self.log_decoder, // 日志解码器保留
encoder_config: self.encoder_config, // 编码器配置保留
}
}
从生成代码可以确认三件事:
contract_id随实例迁移,切换钱包不会影响“调用哪个合约”;log_decoder与encoder_config一并迁移,所以先调用with_encoder_config()再with_account(),编码限制(如max_depth/max_tokens)不会丢失;- 对新账号有 trait 约束:
U: fuels::accounts::Account。该 trait 定义于 packages/fuels-accounts/src/account.rs,继承自ViewOnlyAccount,要求账号能提供address()、try_provider()、get_asset_inputs_for_amount()(按余额构造交易输入)以及add_witnesses()(为交易添加签名 witness)等能力。Wallet、Predicate均满足该约束,因此都可以作为新账号传入。
脚本一侧的对应实现在 packages/fuels-code-gen/src/program_bindings/abigen/bindings/script.rs:同样按值消费旧实例并构造新类型 #name<U>,同时携带 unconfigured_binary、configurables 等字段,行为语义与合约一致。生成的代码样例可参考 examples/rust_bindings/src/rust_bindings_formatted.rs。
测试证据:新钱包真的被扣费了
e2e 测试 e2e/tests/contracts.rs 中的 test_connect_wallet 是对该特性的端到端验证,其断言逻辑值得逐条对照:
// 用 wallet 付款调用一次
contract_instance
.methods()
.initialize_counter(42)
.with_tx_policies(tx_policies)
.call()
.await?;
// 确认 wallet 余额被扣除
let wallet_balance = wallet.get_asset_balance(&Default::default()).await?;
assert!(DEFAULT_COIN_AMOUNT as u128 > wallet_balance);
// 用 wallet_2 付款再调用一次
contract_instance
.with_account(wallet_2.clone())
.methods()
.initialize_counter(42)
.with_tx_policies(tx_policies)
.call()
.await?;
// 确认 wallet 余额不再变化,而 wallet_2 被扣费
let wallet_balance_second_call = wallet.get_asset_balance(&Default::default()).await?;
let wallet_2_balance = wallet_2.get_asset_balance(&Default::default()).await?;
assert_eq!(wallet_balance_second_call, wallet_balance);
assert!(DEFAULT_COIN_AMOUNT as u128 > wallet_2_balance);
这组断言直接证明了:执行 with_account(wallet_2) 后,第二笔调用的 gas 确实由 wallet_2 的 UTXO 支付,而原部署钱包 wallet 的余额保持不变。这正是“切换调用钱包”在费用语义上的确切行为——交易构建时,SDK 通过新账号的 get_asset_inputs_for_amount() 选取其 UTXO 作为输入,再由 add_witnesses() 完成签名。
此外,e2e/tests/contracts.rs 的 max_fee_estimation_respects_tolerance 测试展示了另一种组合:由 deploy_wallet 部署合约后,contract_instance.with_account(call_wallet.clone()) 交给一个只持有 1000 枚小额币的 call_wallet 来发起调用并验证费用估算容差,说明 with_account 可以自然地接入完整的交易构建器(transaction_builder())流程。
注意事项与边界条件
使用 with_account() 时,文档与源码共同揭示了以下边界:
- provider 归属规则(文档明确说明的 Note):将不同的钱包连接到已有实例时,实例上原来设置的 provider 会被忽略,取而代之的是用于部署合约的那个钱包所使用的 provider。也就是说,如果两个钱包分别连接到两个不同的 fuel-core 节点,合约调用仍然走部署钱包所在的那个节点。文档指出:该行为只在同时存在多个 provider(即多个 fuel-core 实例)时才有意义,单节点场景下可以忽略。
- 新账号必须实现
Accounttrait:传入的账号要能获取资产输入并添加 witness(见 packages/fuels-accounts/src/account.rs 的Account定义)。这意味着对真实的.call(),新钱包里必须有足够的 UTXO 支付费用,否则会因余额不足而失败。 - 例外:模拟(simulate)场景不消耗真实资产。e2e/tests/scripts.rs 的
simulations_can_be_made_without_coins测试中,脚本实例通过with_account()换成了一个空钱包,然后执行.simulate(Execution::state_read_only())仍然成功——因为模拟不实际扣费。这说明在只读模拟/状态查询场景下,切换到的钱包是否持有资金并不影响调用。 - 与原实例的关系:
with_account()按值消费旧实例并返回新实例;由于Wallet等类型实现了Clone,实践中通常是克隆原实例或直接链式使用,不影响已有绑定。 - 与
add_signer的区别:如果需要“同一个调用由多个账户共同签名/出资”而非“换一个账户”,应使用调用链上的add_signer()(见 examples/contracts/src/lib.rs 中的add_custom_inputs_outputs用法);with_account解决的是“主调用方是谁”的问题。
小结
with_account() 是 fuels-rs 中成本极低的多钱包切换手段:一行链式调用即可把合约或脚本实例的“调用方账号”换成任意实现 Account trait 的钱包,同时保留合约 ID、日志解码器与编码器配置。结合 e2e/tests/contracts.rs 的余额断言,可以确认它改变的是费用支付者与签名者,而不改变合约本身;再结合文档中关于 provider 归属的说明,在多节点环境下也能建立正确的心智模型。对于涉及多角色交互、费用分摊或测试中模拟不同调用方余额的场景,这是 调用合约 流程中最直接的配套工具。
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 StartedRust0623
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