fuels-ts 跨合约调用实战:从 Sway 合约设计到 SDK addContracts 的源码剖析
【免费下载链接】fuels-ts Fuel Network Typescript SDK 项目地址: 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 侧跨合约调用的两个核心机制:
两个合约的依赖关系从它们的 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();
流程分四步理解:
原文档特别强调的一句话值得单独记住:
注意 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;
}
它做了两件互补的事:
从源码结构看,外部 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 项目地址: https://gitcode.com/GitHub_Trending/fu/fuels-ts
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考




