首页
/ fuels-ts 合约交互四法:`get`、`dryRun`、`simulate`、`call` 与只读判断的完整解析

fuels-ts 合约交互四法:`get`、`dryRun`、`simulate`、`call` 与只读判断的完整解析

2026-09-05 23:23:02作者:舒璇辛Bertina

本文围绕 Fuel TypeScript SDK(fuels-ts)中 ContractFunctionInvocationScope 提供的四种合约交互方式——getdryRunsimulatecall——展开,并补充 isReadOnly 工具方法的用法。读完本篇,你将清楚每种方法是否消耗链上资源、是否需要已充资钱包、返回值形态(同步结果 vs transactionId + waitForResult)的差异,并能在 fuels-ts 源码 层面理解这些方法背后的调用链,从而在实际项目中正确选择"读取、预演还是上链"。

四种交互方式总览

官方文档 Interacting With Contracts 中明确指出,与 Fuel 链上合约交互共有 4 种方式:getdryRunsimulatecall。它们的核心区别在于是否消耗真实资源是否需要已充资钱包以及是否真正将交易写入链

方法 是否真实执行上链 是否消耗 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() 返回的 valueBN 类型,可用 .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 方法同样是对合约调用做干跑,但它在节点侧模拟执行,目的是确保所用钱包有足够资金覆盖该交易的手续费,同时不真正消耗任何资源

getdryRun 的关键区别: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

  1. await contract.functions.increment_count(10).call() 立即得到提交句柄;
  2. await waitForResult() 才能拿到交易最终结果(含 value)与交易状态。

这一异步模型在 SDK 中由 ContractTransactionResponse 承接,它在 fuel-core 交易响应 基础上封装了合约维度(如 logs 解码、函数返回值解码);部署场景中的 deployContract.waitForResult() 使用的也是同一套响应机制。测试侧可以参见 is-function-readonly.test.ts 中对"先判断只读、再选择 getcall"流程的集成验证。

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 元数据。这也解释了为什么它零成本、可同步调用。

实践建议:如何选择

结合上述四种方法与文档规则,一条可落地的选型路径是:

  1. 读数据:先 contract.functions.fn.isReadOnly(),若为只读则 get(),零成本;
  2. 写数据前预演:用 dryRun()(不关心费用)或 simulate()(需已充资钱包,确认费用付得起);
  3. 正式上链call(),记住它是异步模型——拿到 transactionId 后必须 waitForResult() 才能得到最终结果;
  4. 所有方法都遵循统一入口 contract.functions.<fn>(...args),参数与返回值类型由 typegen 生成的工厂类(如示例中的 CounterFactory)提供类型约束。

以上示例统一基于本地网络(LOCAL_NETWORK_URL)与 typegen 生成的 Counter 合约工厂;在接入其他网络时,仅需替换 Provider 的节点地址,四种交互方式的语义保持不变。

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