首页
/ fuels-rs 多钱包合约调用实战:用 with_account 为同一合约实例切换调用钱包

fuels-rs 多钱包合约调用实战:用 with_account 为同一合约实例切换调用钱包

2026-09-05 21:58:56作者:昌雅子Ethen

在 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 不变),也不改变任何链上状态,只是替换了实例中持有“谁来出钱、谁来签名”的账号字段。因此它有两个典型应用场景:

  1. 费用分担:合约由钱包 A 部署,但希望钱包 B 为某次调用支付 gas 费;
  2. 多角色交互:同一份合约代码被多方使用,每方用自己的身份账户执行方法调用(合约内可通过 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, // 编码器配置保留
    }
}

从生成代码可以确认三件事:

  1. contract_id 随实例迁移,切换钱包不会影响“调用哪个合约”;
  2. log_decoderencoder_config 一并迁移,所以先调用 with_encoder_config()with_account(),编码限制(如 max_depth/max_tokens)不会丢失;
  3. 对新账号有 trait 约束U: fuels::accounts::Account。该 trait 定义于 packages/fuels-accounts/src/account.rs,继承自 ViewOnlyAccount,要求账号能提供 address()try_provider()get_asset_inputs_for_amount()(按余额构造交易输入)以及 add_witnesses()(为交易添加签名 witness)等能力。WalletPredicate 均满足该约束,因此都可以作为新账号传入。

脚本一侧的对应实现在 packages/fuels-code-gen/src/program_bindings/abigen/bindings/script.rs:同样按值消费旧实例并构造新类型 #name<U>,同时携带 unconfigured_binaryconfigurables 等字段,行为语义与合约一致。生成的代码样例可参考 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.rsmax_fee_estimation_respects_tolerance 测试展示了另一种组合:由 deploy_wallet 部署合约后,contract_instance.with_account(call_wallet.clone()) 交给一个只持有 1000 枚小额币的 call_wallet 来发起调用并验证费用估算容差,说明 with_account 可以自然地接入完整的交易构建器(transaction_builder())流程。

注意事项与边界条件

使用 with_account() 时,文档与源码共同揭示了以下边界:

  1. provider 归属规则(文档明确说明的 Note):将不同的钱包连接到已有实例时,实例上原来设置的 provider 会被忽略,取而代之的是用于部署合约的那个钱包所使用的 provider。也就是说,如果两个钱包分别连接到两个不同的 fuel-core 节点,合约调用仍然走部署钱包所在的那个节点。文档指出:该行为只在同时存在多个 provider(即多个 fuel-core 实例)时才有意义,单节点场景下可以忽略。
  2. 新账号必须实现 Account trait:传入的账号要能获取资产输入并添加 witness(见 packages/fuels-accounts/src/account.rsAccount 定义)。这意味着对真实的 .call(),新钱包里必须有足够的 UTXO 支付费用,否则会因余额不足而失败。
  3. 例外:模拟(simulate)场景不消耗真实资产e2e/tests/scripts.rssimulations_can_be_made_without_coins 测试中,脚本实例通过 with_account() 换成了一个空钱包,然后执行 .simulate(Execution::state_read_only()) 仍然成功——因为模拟不实际扣费。这说明在只读模拟/状态查询场景下,切换到的钱包是否持有资金并不影响调用。
  4. 与原实例的关系with_account() 按值消费旧实例并返回新实例;由于 Wallet 等类型实现了 Clone,实践中通常是克隆原实例或直接链式使用,不影响已有绑定。
  5. 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 归属的说明,在多节点环境下也能建立正确的心智模型。对于涉及多角色交互、费用分摊或测试中模拟不同调用方余额的场景,这是 调用合约 流程中最直接的配套工具。

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