欢迎光临
我们一直在努力

fuels-ts 跨合约调用实战:从 Sway 合约设计到 SDK `addContracts` 的源码剖析

fuels-ts 跨合约调用实战:从 Sway 合约设计到 SDK addContracts 的源码剖析

【免费下载链接】fuels-ts Fuel Network Typescript SDK 【免费下载链接】fuels-ts 项目地址: https://gitcode.com/GitHub_Trending/fu/fuels-ts

本文基于 Fuel Network TypeScript SDK(fuels-ts)官方文档 apps/docs/src/guide/contracts/inter-contract-calls.md,完整讲解“一个合约调用另一个合约”的跨合约调用(Inter-Contract Call)场景:先用 SimpleToken 与 TokenDepositor 两个 Sway 合约搭建真实业务场景,再通过 SDK 完成部署与调用,最后从 SDK 源码层面解释为什么调用外部合约前必须执行 addContracts,以及它究竟为交易补充了哪些输入、输出与 ABI 信息。

场景概述:SimpleToken 与 TokenDepositor

文档选用的示例场景是:SimpleToken 合约代表一个基础的代币合约,可以为不同地址持有余额;TokenDepositor 合约则负责向 SimpleToken 合约“存入”代币。两个合约通过 FuelVM 的合约 ID 机制互相引用,SDK 侧则通过 addContracts 方法让 TokenDepositor 对 SimpleToken 的调用成功执行。

这两个合约的 Sway 源码位于仓库的文档配套 Sw 目录中:

  • SimpleToken 合约源码
  • TokenDepositor 合约源码

合约一:SimpleToken

SimpleToken 使用 StorageMap<b256, u64> 存储各地址余额,提供两个函数:

contract;

use std::hash::*;
use simple_token_abi::SimpleToken;

storage {
balances: StorageMap<b256, u64> = StorageMap {},
}

impl SimpleToken for Contract {
#[storage(read, write)]
fn deposit(address: b256, amount: u64) {
let current_balance = storage.balances.get(address).try_read().unwrap_or(0);
storage.balances.insert(address, current_balance + amount);
}
#[storage(read)]
fn get_balance(address: b256) -> u64 {
let balance = storage.balances.get(address).try_read().unwrap_or(0);
balance
}
}

要点:

  • deposit 带有 #[storage(read, write)] 注解,会累加目标地址的余额;
  • get_balance 只读,用于调用前后核对余额变化;
  • use simple_token_abi::SimpleToken; 引入的是 simple_token_abi 这个库(library)crate 导出的 ABI 接口,而不是合约实现本身。

合约二:TokenDepositor

TokenDepositor 导入同一个 SimpleToken ABI,并在其方法内部对目标合约发起跨合约调用:

contract;

use std::auth::msg_sender;

use simple_token_abi::SimpleToken;

abi TokenDepositor {
fn deposit_to_simple_token(contract_id: b256, amount: u64);
}

impl TokenDepositor for Contract {
fn deposit_to_simple_token(contract_id: b256, amount: u64) {
let simple_token_contract = abi(SimpleToken, contract_id);

let sender = msg_sender().unwrap();

let address: b256 = match sender {
Identity::Address(sender_param) => sender_param.bits(),
_ => revert(0),
};

simple_token_contract.deposit(address, amount);
}
}

这段代码体现了 Sway 侧跨合约调用的两个核心机制:

  • abi(SimpleToken, contract_id):FuelVM 的 abi 关键字。它不导入对方合约的代码,而是以“ABI 接口 + 目标合约 ID”构造一个可远程调用的合约句柄。调用 simple_token_contract.deposit(…) 时,实际发生的是对 contract_id 指向的已部署合约的一次真实合约调用;
  • msg_sender():获取当前消息的发送者身份。deposit_to_simple_token 的签名者(外部钱包)是真正的消息发起方,而 SimpleToken 是被 TokenDepositor 间接调用的,所以这里显式取出调用者地址,把代币记在钱包地址上而非合约地址上。若发送者不是普通 Address 身份,则直接 revert(0)。
  • 两个合约的依赖关系从它们的 Forc 配置 中可以清楚看到:simple-token 与 token-depositor 两个项目都声明了同一个库依赖 simple_token_abi = { path = "../simple-token-abi" }。也就是说,“调用方”与“被调用方”共享的是一份独立编译的 ABI 库 crate——这正是 Sway 实现跨合约调用的标准做法:接口定义与实现分离。

    SDK 侧:完整的跨合约调用流程

    文档给出的可运行示例位于 inter-contract-calls.ts。它假设 SimpleTokenFactory、TokenDepositorFactory 已通过 typegen 生成(导入自 typegend 目录),完整代码如下:

    import { Provider, Wallet } from 'fuels';

    import { LOCAL_NETWORK_URL, WALLET_PVT_KEY } from '../../../env';
    import { SimpleTokenFactory, TokenDepositorFactory } from '../../../typegend';

    const provider = new Provider(LOCAL_NETWORK_URL);
    const wallet = Wallet.fromPrivateKey(WALLET_PVT_KEY, provider);

    const { waitForResult: waitForSimpleToken } =
    await SimpleTokenFactory.deploy(wallet);

    const { contract: simpleToken } = await waitForSimpleToken();

    const { waitForResult: waitForTokenDepositor } =
    await TokenDepositorFactory.deploy(wallet);

    const { contract: tokenDepositor } = await waitForTokenDepositor();

    const amountToDeposit = 70;
    const call1 = await simpleToken.functions
    .get_balance(wallet.address.toB256())
    .call();

    const { value: initialBalance } = await call1.waitForResult();

    const call2 = await tokenDepositor.functions
    .deposit_to_simple_token(simpleToken.id.toB256(), amountToDeposit)
    .addContracts([simpleToken])
    .call();

    await call2.waitForResult();

    const call3 = await simpleToken.functions
    .get_balance(wallet.address.toB256())
    .call();

    const { value: finalBalance } = await call3.waitForResult();

    流程分四步理解:

  • 部署两个合约:SimpleTokenFactory.deploy(wallet) 与 TokenDepositorFactory.deploy(wallet) 均返回 waitForResult,await 之后得到 Contract 实例 simpleToken 与 tokenDepositor。跨合约调用要求“被调用方先部署”,因为调用方在 Sway 侧传入的是运行时已存在的合约 ID;
  • 记录初始余额:直接调用 simpleToken.functions.get_balance(…),此时钱包地址应无任何余额(initialBalance.toNumber() === 0);
  • 发起跨合约调用:关键点在 tokenDepositor.functions.deposit_to_simple_token(…) 之后的 .addContracts([simpleToken])——把被调用的 SimpleToken 合约实例加入本次调用的合约清单,然后 .call() 提交交易;
  • 校验结果:交易确认后再次查询余额,finalBalance.toNumber() 应等于 amountToDeposit(示例中为 70)。示例文件末尾的两行 console.log 正是对“初始余额为 0”“最终余额等于存入金额”两项断言的输出。
  • 原文档特别强调的一句话值得单独记住:

    注意 TokenDepositor 合约调用的 addContracts 方法。它接收一个已部署合约实例的数组。如果不调用这个方法,跨合约调用将无法正常工作。

    为什么 addContracts 必不可少:源码级解析

    addContracts 定义在 SDK 的通用调用作用域基类 BaseInvocationScope 中,见 base-invocation-scope.ts:

    addContracts(contracts: Array<AbstractContract | string>) {
    contracts.forEach((contract) => {
    if (typeof contract === 'string') {
    this.transactionRequest.addContractInputAndOutput(new Address(contract));
    } else {
    this.transactionRequest.addContractInputAndOutput(contract.id);
    this.externalAbis[contract.id.toB256()] = contract.interface.jsonAbi;
    }
    });
    return this;
    }

    它做了两件互补的事:

  • 补充 ContractInput / ContractOutput:对传入的每个合约(支持传 Contract 实例或合约 ID 字符串),调用 transactionRequest.addContractInputAndOutput(…),把该合约 ID 同时写入交易的输入与输出。FuelVM 执行交易时,合约 A 要调用合约 B,B 必须作为 ContractInput 出现在交易中(提供存储地址与 ID),执行产生的状态变化也需要对应的 ContractOutput 承载。不调用 addContracts 时,交易中根本不存在 SimpleToken 的输入/输出,VM 无法完成对它的寻址与调用——这正是文档中“不调用则跨合约调用无法工作”的根本原因;
  • 登记外部合约的 JSON ABI:当传入的是 Contract 实例时,SDK 还会执行 this.externalAbis[contract.id.toB256()] = contract.interface.jsonAbi,把外部合约的 ABI 记入 externalAbis 表(字段定义见 base-invocation-scope.ts)。这个信息随后被 createContractCall 挂到每个 ContractCall 的 externalContractsAbis 字段上(base-invocation-scope.ts),并在 updateContractInputAndOutput(base-invocation-scope.ts)中再次确认这些外部合约的 ContractInput/Output 已加入交易。
  • 从源码结构看,外部 ABI 的最终用途是结果与日志的解码:在 prepareTransaction 中,SDK 会执行 this.transactionRequest.abis = getAbisFromAllCalls(this.functionInvocationScopes)(base-invocation-scope.ts),把本次调用涉及的所有合约(含外部合约)的 ABI 挂到交易请求上。这样 waitForResult 在解析回执(receipt)时,即便日志产生于 SimpleToken 这样的外部合约,SDK 也能依据登记的 ABI 正确解码跨合约调用路径上的日志与返回值。

    综合起来,addContracts 是“业务层一行代码,事务层两重保障”:既让交易结构满足 VM 的寻址要求,又让 SDK 具备解码外部合约产出的能力。

    在仓库测试中的实际用法

    addContracts 不只是文档示例的写法,SDK 的集成测试套件(fuel-gauge)中同样如此使用。例如 contract.test.ts 中出现了如下模式:

    const scope = contract.multiCall(calls).addContracts([otherContract]);

    即先通过 multiCall 构造多个调用,再 addContracts 补上被调用的外部合约,最后统一提交。这说明“调用方 + 被调用方合约清单”是该 SDK 处理多合约交易的固定范式,跨合约调用、多调用(multi-call)场景共用同一套机制。

    关键要点回顾

    • Sway 侧:跨合约调用 = 共享的 ABI 库 crate(simple_token_abi)+ abi(Abi, contract_id) 构造远程句柄 + msg_sender() 识别真实调用者;调用双方通过各自的 Forc.toml 以 path 依赖引用同一 ABI crate;
    • SDK 侧:被调用合约必须先部署;调用外部合约的链式调用中必须追加 .addContracts([被调用合约实例]),否则交易中缺少对应 ContractInput/ContractOutput,调用无法完成;
    • 源码层面:addContracts 在 BaseInvocationScope 中完成“补交易输入输出 + 登记外部 ABI”两件事,外部 ABI 进一步用于交易结果的日志解码;
    • 延伸阅读:仓库中同目录的 logs 分组文档 也涉及跨合约调用(inter-contract call)场景下的日志处理,可作为理解回执解码的配套资料。

    【免费下载链接】fuels-ts Fuel Network Typescript SDK 【免费下载链接】fuels-ts 项目地址: https://gitcode.com/GitHub_Trending/fu/fuels-ts

    创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

    赞(0)
    未经允许不得转载:171主机测评 » fuels-ts 跨合约调用实战:从 Sway 合约设计到 SDK `addContracts` 的源码剖析
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址