fuels-ts 合约交互四法:`get`、`dryRun`、`simulate`、`call` 与只读判断的完整解析
本文围绕 Fuel TypeScript SDK(fuels-ts)中 ContractFunctionInvocationScope 提供的四种合约交互方式——get、dryRun、simulate、call——展开,并补充 isReadOnly 工具方法的用法。读完本篇,你将清楚每种方法是否消耗链上资源、是否需要已充资钱包、返回值形态(同步结果 vs transactionId + waitForResult)的差异,并能在 fuels-ts 源码 层面理解这些方法背后的调用链,从而在实际项目中正确选择"读取、预演还是上链"。
四种交互方式总览
在 官方文档 Interacting With Contracts 中明确指出,与 Fuel 链上合约交互共有 4 种方式:get、dryRun、simulate、call。它们的核心区别在于是否消耗真实资源、是否需要已充资钱包以及是否真正将交易写入链:
| 方法 | 是否真实执行上链 | 是否消耗 Gas | 是否需要已充资钱包 | 典型用途 |
|---|---|---|---|---|
get |
否(本地求值) | 否 | 否,甚至可以不传钱包 | 读取链上只读数据 |
dryRun |
否(本地预演) | 否 | 否,甚至可以不传钱包 | 预演一次写操作的结果 |
simulate |
否(节点模拟) | 否,但会校验 Gas 是否足够 | 是,钱包必须有足够余额 | 上链前确认交易可行 |
call |
是 | 是,真实支付 Gas 费 | 是 | 真正执行并持久化状态变更 |
这四者都通过同一个入口调用:contract.functions.<函数名>(...参数) 返回的调用范围对象。在 SDK 源码中,该入口由 Contract 类 构造——它通过 Object.defineProperty(funcInvocationScopeCreator, 'isReadOnly', ...) 把 ABI 函数映射为可调用的函数式接口,其中 isReadOnly 被定义为一个闭包属性,直接代理到 ABI 函数对象的 func.isReadOnly() 判断(见 contract.ts)。
get:零成本读取链上数据
get 方法用于在不消耗任何资源的情况下从区块链读取数据。它可以配合未充资的钱包使用,甚至完全不传钱包。适合场景是:调用只读函数(如计数器查询、余额查询)获取链上最新状态。
以下示例来自文档片段 get.ts,使用本地网络部署 Counter 合约后读取计数值:
import { Provider, Wallet } from 'fuels';
import { LOCAL_NETWORK_URL, WALLET_PVT_KEY } from '../../../../env';
import { CounterFactory } from '../../../../typegend';
const provider = new Provider(LOCAL_NETWORK_URL);
const deployer = Wallet.fromPrivateKey(WALLET_PVT_KEY, provider);
const deployContract = await CounterFactory.deploy(deployer);
const { contract } = await deployContract.waitForResult();
// Read from the blockchain
const { value } = await contract.functions.get_count().get();
// 0
要点说明:
Provider负责连接节点(示例中为本地网络LOCAL_NETWORK_URL),Wallet.fromPrivateKey用私钥创建部署钱包;CounterFactory是 typegen 生成的工厂类,CounterFactory.deploy(deployer)返回的部署交易需waitForResult()才能获得最终的contract实例;get()返回的value是BN类型,可用.toNumber()转换为 JS 数字断言0。
由于 get 由本地直接对链上状态求值,它只对只读函数有意义——若对非只读函数调用 get,无法反映状态变更。
dryRun:本地预演写操作,不动 Gas
dryRun 方法用于干跑(dry-run)一次合约调用:它不花费任何资源,同样可以使用未充资的钱包,甚至不传钱包。适合在真正上链前先看看函数返回什么,而不实际提交交易。
示例来自文档片段 dry-run.ts:
import { Provider, Wallet } from 'fuels';
import { LOCAL_NETWORK_URL, WALLET_PVT_KEY } from '../../../../env';
import { CounterFactory } from '../../../../typegend';
const provider = new Provider(LOCAL_NETWORK_URL);
const deployer = Wallet.fromPrivateKey(WALLET_PVT_KEY, provider);
const deployContract = await CounterFactory.deploy(deployer);
const { contract } = await deployContract.waitForResult();
// Perform a dry-run of the transaction
const { value } = await contract.functions.increment_count(1).dryRun();
注意示例中对 increment_count(1)(一个写操作)执行 dryRun() 后得到 value = 1——它预演了增量逻辑的返回值,但链上状态并未被修改,也不扣费。文档片段末尾的断言 value.toNumber() === 1 验证了这一点。
simulate:校验资金充足性的模拟执行
simulate 方法同样是对合约调用做干跑,但它在节点侧模拟执行,目的是确保所用钱包有足够资金覆盖该交易的手续费,同时不真正消耗任何资源。
与 get、dryRun 的关键区别:simulate 要求传入一个已充资的钱包(funded wallet),因为需要用它来评估 Gas 费用是否付得起。
示例来自文档片段 simulate.ts:
import { Provider, Wallet } from 'fuels';
import { LOCAL_NETWORK_URL, WALLET_PVT_KEY } from '../../../../env';
import { CounterFactory } from '../../../../typegend';
const provider = new Provider(LOCAL_NETWORK_URL);
const deployer = Wallet.fromPrivateKey(WALLET_PVT_KEY, provider);
const deployContract = await CounterFactory.deploy(deployer);
const { contract } = await deployContract.waitForResult();
// Simulate the transaction
const { value } = await contract.functions.increment_count(10).simulate();
increment_count(10) 模拟执行后返回 value = 10,验证逻辑与 dryRun 类似,但模拟过程中会走节点的交易模拟路径,能提前暴露余额不足等问题。在真实项目中的典型用法是:先 simulate 确认交易可行,再 call 提交上链。
call:真实上链,异步确认结果
call 方法向节点提交一次真实的合约调用交易。它提交后立即 resolve,返回一个 transactionId 以及一个 waitForResult 回调,用于等待交易最终执行完成。文档中特别指出,这种行为与区块链的自然特性一致——交易可能需要几秒钟才会被记录上链,因此 SDK 采用"提交即返回 + 异步等待"的模型。
call 会消耗真实资源:合约函数执行的所有操作都会真正处理上链,钱包需支付 Gas 费。
示例来自文档片段 call.ts:
import { Provider, Wallet } from 'fuels';
import { LOCAL_NETWORK_URL, WALLET_PVT_KEY } from '../../../../env';
import { CounterFactory } from '../../../../typegend';
const provider = new Provider(LOCAL_NETWORK_URL);
const deployer = Wallet.fromPrivateKey(WALLET_PVT_KEY, provider);
const deployContract = await CounterFactory.deploy(deployer);
const { contract } = await deployContract.waitForResult();
// Perform the transaction
const { waitForResult } = await contract.functions.increment_count(10).call();
const { value } = await waitForResult();
与其他三种方法的直接返回 value 不同,call() 的返回值核心是 waitForResult:
await contract.functions.increment_count(10).call()立即得到提交句柄;- 再
await waitForResult()才能拿到交易最终结果(含value)与交易状态。
这一异步模型在 SDK 中由 ContractTransactionResponse 承接,它在 fuel-core 交易响应 基础上封装了合约维度(如 logs 解码、函数返回值解码);部署场景中的 deployContract.waitForResult() 使用的也是同一套响应机制。测试侧可以参见 is-function-readonly.test.ts 中对"先判断只读、再选择 get 或 call"流程的集成验证。
isReadOnly:自动判断该用 get 还是 call
如果你需要程序化地判断某个函数是否只读,可以使用 isReadOnly 方法。它直接查询 ABI 中该函数的标注(Sway 侧 #[storage(read)] 等),返回布尔值,无需任何链上交互。
示例来自文档片段 is-read-only.ts:
import { Provider, Wallet } from 'fuels';
import { LOCAL_NETWORK_URL, WALLET_PVT_KEY } from '../../../../env';
import { CounterFactory } from '../../../../typegend';
const provider = new Provider(LOCAL_NETWORK_URL);
const deployer = Wallet.fromPrivateKey(WALLET_PVT_KEY, provider);
const deployContract = await CounterFactory.deploy(deployer);
const { contract } = await deployContract.waitForResult();
const isReadOnly = contract.functions.get_count.isReadOnly();
if (isReadOnly) {
await contract.functions.get_count().get();
} else {
const { waitForResult } = await contract.functions.get_count().call();
await waitForResult();
}
文档给出的决策规则非常明确:
- 只读函数:用
get获取链上数据,不花 Gas; - 非只读函数:必须用
call提交链上交易,需要支付 Gas 费。
从源码结构看,isReadOnly 并非额外发送的链上请求,而是 Contract 类 在初始化 functions 时通过 Object.defineProperty 挂载到每个函数调用创建器上的属性,其实现委托给 ABI 函数对象的 func.isReadOnly()——即读取的是合约 ABI 元数据。这也解释了为什么它零成本、可同步调用。
实践建议:如何选择
结合上述四种方法与文档规则,一条可落地的选型路径是:
- 读数据:先
contract.functions.fn.isReadOnly(),若为只读则get(),零成本; - 写数据前预演:用
dryRun()(不关心费用)或simulate()(需已充资钱包,确认费用付得起); - 正式上链:
call(),记住它是异步模型——拿到transactionId后必须waitForResult()才能得到最终结果; - 所有方法都遵循统一入口
contract.functions.<fn>(...args),参数与返回值类型由 typegen 生成的工厂类(如示例中的CounterFactory)提供类型约束。
以上示例统一基于本地网络(LOCAL_NETWORK_URL)与 typegen 生成的 Counter 合约工厂;在接入其他网络时,仅需替换 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