Logo
新用户订阅 RPC,首月享 6.5 折优惠查看优惠
OnFinality Learn
网络与协议指南阅读约 12 分钟

发送前模拟 Sui 交易:devInspectTransaction 与 dryRunTransactionBlock

了解如何通过 JSON-RPC 模拟 Sui 交易,在支付 gas 前捕获对象版本冲突、gas 问题和命令失败。

TL;DR

在广播 Sui 交易之前,您可以通过 JSON-RPC 使用 devInspectTransaction 或 dryRunTransactionBlock 进行模拟。这些方法针对当前状态执行交易而不提交,返回交易效果和执行状态。这使您能够以零成本捕获对象版本冲突、gas 不足和命令级失败,确保您的交易在实际提交时能够成功。

直接回答:先模拟,后付费

Sui 交易并非在真空中执行:它们操作一组拥有和共享的对象,每个对象都有特定的版本。针对一个对象版本成功的交易可能针对另一个版本失败。为了避免在失败交易上浪费 gas,Sui 提供了两种 JSON-RPC 方法,让您可以在广播交易前进行模拟:devInspectTransactiondryRunTransactionBlock。两者都返回将产生的交易效果,包括 successfailure 的执行状态。通过检查这些效果,您可以在它们造成任何成本之前捕获对象版本冲突、gas 不足和命令级错误。

本指南解释了这些模拟方法背后的机制、如何解码返回的效果,并提供了一个使用官方 @mysten/sui/client SDK 的可复现 TypeScript 脚本。您将学会区分成功模拟和失败模拟,以及如何处理开发人员常遇到的陷阱。

理解 Sui 交易执行和对象版本控制

在 Sui 中,交易是一个 TransactionData 结构,包含一个可编程的 TransactionBlock。该区块由操作输入的命令组成,输入可以是拥有的对象、共享的对象或纯值。Sui 中的每个对象都有一个唯一的 ID 和一个单调递增的版本号。当交易执行时,验证器会检查交易中引用的对象版本是否与链上的当前版本匹配。如果不匹配,交易将因对象版本冲突而失败。

这种版本控制对模拟至关重要:当您模拟交易时,必须确保提供的对象版本是您打算使用的版本。如果您获取了对象的引用(ID 和版本),然后构建交易,但该对象在您提交之前被另一笔交易修改,您的模拟可能会成功,而实际提交会失败。因此,始终在构建交易之前立即获取最新的对象引用。

这两种模拟方法在方法上有所不同。dryRunTransactionBlock 像提交一样执行交易,需要 gas 对象和 gas 预算。它返回将提交的确切效果,包括使用的 gas。另一方面,devInspectTransaction 针对一组提供的对象运行交易,不收取 gas,也不需要 gas 对象。它假设无限的 gas 预算,专为开发和测试而设计。devInspectTransaction 的结果不应被视为主网上的确切结果,因为它使用您提供的对象,而不一定是当前的链上状态。

这两种方法都是只读的:它们不会改变状态。但是,dryRunTransactionBlock 需要 gas 支付,如果 gas 预算不足则会失败,而 devInspectTransaction 则不会。这使得 devInspectTransaction 非常适合“假设”场景,例如针对假设的对象状态测试新命令。

两个模拟入口:dryRunTransactionBlock 与 devInspectTransaction

Sui JSON-RPC API 提供了两种主要的交易模拟方法。请注意,方法名称已经演变:devInspectTransactionBlock 在最近的 SDK 版本中已重命名为 devInspectTransaction,旧名称在多个参考资料中已被标记为已弃用。始终根据您的 SDK 和端点的 API 版本使用当前的方法名称;Sui JSON-RPC 文档dryRunTransactionBlock / devInspectTransaction 参考 描述了当前签名。例如,@mysten/sui/client SDK 暴露了 client.devInspectTransactionclient.dryRunTransactionBlock

dryRunTransactionBlock 接受一个 TransactionBlock(或其序列化字节)和一个 sender 地址。它针对当前状态执行交易,使用交易中指定的 gas 对象。它返回一个 DryRunTransactionBlockResponse,包含 effects 和任何错误。effects 包括 status(成功或失败)、gasUsed 以及创建、修改和删除的对象列表。

devInspectTransaction 接受一个 sender 地址、一个 TransactionBlock,以及可选的 gasPriceepoch 列表。它不需要 gas 对象;相反,它使用一个余额无限的模拟 gas 币。它返回一个 DevInspectResponse,包含每个命令的 effectsresultseffects 包括 gasUsed 摘要,但由于 gas 预算是无限的,实际的 gas 成本并不代表真实交易。

关键区别:dryRunTransactionBlock 在 gas 预算不足时会失败,而 devInspectTransaction 不会。因此,要测试您的交易在特定 gas 预算下是否会成功,请使用 dryRunTransactionBlock。要测试交易逻辑而不担心 gas,请使用 devInspectTransaction

解码执行状态和效果

模拟响应中最重要的部分是 effects.status。这是一个对象,其 status 字段为 'success''failure'。如果是 'failure'error 字段包含描述错误的字符串。常见错误包括 'MoveAbort''MoveModule''U64Wrap' 和对象版本冲突。

当交易块中的特定命令失败时,会发生命令级失败。例如,MoveAbort 错误表示 Move 模块中止,通常是由于断言失败。对象版本冲突发生在输入对象的版本与当前链上版本不匹配时。当您使用过时的对象引用构建交易时,这是一个常见问题。

区分 RPC 错误和带有失败状态的成功 RPC 至关重要。如果模拟调用本身返回错误(例如,参数无效),那是客户端问题。如果调用成功但 effects.status'failure',则表示如果提交,交易将失败。许多开发人员错误地将任何错误视为 RPC 失败,但您必须检查 effects.status 字段。

effects 还包含一个 gasUsed 对象,其中包含 computationCoststorageCoststorageRebate。在 dry run 中,这些反映了实际将使用的 gas。在 dev inspect 中,使用的 gas 是在假设无限预算的情况下计算的,因此 gasUsed 可能高于您实际支付的费用。始终使用 dryRunTransactionBlock 来估算真实的 gas 成本。

可复现示例:使用 @mysten/sui/client 模拟代币转账

以下 TypeScript 脚本演示了如何使用 devInspectTransactiondryRunTransactionBlock 模拟简单的代币转账。它连接到 Sui 端点,构建一个将特定数量的 SUI 从一个地址转移到另一个地址的交易,然后进行模拟。脚本打印执行状态、gas 摘要和任何错误。

要运行此脚本,您需要 Node.js 和 @mysten/sui 包。使用 npm install @mysten/sui 安装它。将端点 URL 和地址替换为您自己的。该脚本使用 devInspectTransaction 方法,但您可以通过取消注释相关行轻松切换到 dryRunTransactionBlock

注意:脚本假设您有一个要转移的对象(代币)。您需要提供对象 ID 及其版本。在真实场景中,您将使用 suix_getOwnedObjectssuix_getDynamicField 获取这些信息。

import { SuiClient, getFullnodeUrl } from '@mysten/sui/client';
import { Transaction } from '@mysten/sui/transactions';

// Connect to a Sui endpoint (replace with your endpoint)
const client = new SuiClient({ url: getFullnodeUrl('mainnet') });

async function simulateTransfer() {
  const sender = '0xYOUR_SENDER_ADDRESS';
  const recipient = '0xRECIPIENT_ADDRESS';
  const coinObjectId = '0xCOIN_OBJECT_ID';
  const amount = 1000; // in MIST

  // Build a transaction to transfer `amount` from the coin to recipient
  const tx = new Transaction();
  const coin = tx.object(coinObjectId);
  tx.transferObjects([coin], recipient);

  // Simulate using devInspectTransaction (no gas required)
  const devInspectResult = await client.devInspectTransaction({
    sender,
    transactionBlock: tx,
  });

  console.log('DevInspect Status:', devInspectResult.effects.status);
  console.log('DevInspect Gas Used:', devInspectResult.effects.gasUsed);

  // To simulate with a gas budget, use dryRunTransactionBlock
  // You need to set the gas budget and gas payment in the transaction
  // tx.setGasBudget(1000);
  // tx.setGasPayment([{ objectId: gasCoinId, version: gasCoinVersion, digest: gasCoinDigest }]);
  // const dryRunResult = await client.dryRunTransactionBlock({
  //   transactionBlock: tx,
  //   sender,
  // });
  // console.log('DryRun Status:', dryRunResult.effects.status);
}

simulateTransfer().catch(console.error);

模拟失败场景:对象版本冲突和 gas 不足

为了理解失败形态,模拟故意错误的对象版本或不足的 gas 预算是有启发性的。以下脚本演示了如何创建引用旧版本对象的交易,导致版本冲突,以及如何模拟 gas 预算过低的交易。

对于对象版本冲突,您可以手动将代币对象的版本设置为较旧版本(例如,版本 1),而当前版本更高。这将导致模拟失败,并出现指示版本不匹配的错误。

对于 gas 不足,您可以在交易中设置非常低的 gas 预算,然后调用 dryRunTransactionBlock。模拟将返回失败状态,并出现关于 gas 不足的错误。

下表总结了常见的状态字符串及其含义。根据您的场景填写“修复”列。

// Simulate an object version conflict
async function simulateVersionConflict() {
  const sender = '0xYOUR_SENDER_ADDRESS';
  const recipient = '0xRECIPIENT_ADDRESS';
  const coinObjectId = '0xCOIN_OBJECT_ID';
  // Assume the current version is 5, but we use version 1
  const staleVersion = 1;
  const staleDigest = '0xSTALE_DIGEST';

  const tx = new Transaction();
  const coin = tx.objectRef({
    objectId: coinObjectId,
    version: staleVersion,
    digest: staleDigest,
  });
  tx.transferObjects([coin], recipient);

  const result = await client.devInspectTransaction({
    sender,
    transactionBlock: tx,
  });
  console.log('Version Conflict Status:', result.effects.status);
  console.log('Error:', result.effects.status.error);
}

// Simulate insufficient gas using dryRunTransactionBlock
async function simulateInsufficientGas() {
  const sender = '0xYOUR_SENDER_ADDRESS';
  const recipient = '0xRECIPIENT_ADDRESS';
  const coinObjectId = '0xCOIN_OBJECT_ID';
  const gasCoinId = '0xGAS_COIN_ID';
  const gasCoinVersion = 1;
  const gasCoinDigest = '0xGAS_COIN_DIGEST';

  const tx = new Transaction();
  const coin = tx.object(coinObjectId);
  tx.transferObjects([coin], recipient);
  tx.setGasBudget(1); // absurdly low
  tx.setGasPayment([{ objectId: gasCoinId, version: gasCoinVersion, digest: gasCoinDigest }]);

  const result = await client.dryRunTransactionBlock({
    transactionBlock: tx,
    sender,
  });
  console.log('Insufficient Gas Status:', result.effects.status);
  console.log('Error:', result.effects.status.error);
}

模拟状态决策表

当您模拟交易时,会遇到各种状态字符串。下表列出了常见的状态及其含义。使用它快速诊断问题。

  • 状态 / 错误含义修复
    success交易将成功。提交它。
    failure 且带有 MoveAbortMove 模块中止,通常是由于断言失败。检查 Move 代码和中止代码。
    failure 且带有 MoveModuleMove 模块中发生错误,例如缺少函数或类型不匹配。检查模块的接口和交易中的命令。
    failure 且带有 U64Wrap无符号 64 位整数溢出。调整计算以避免溢出。
    failure 且带有 InsufficientGasgas 预算过低。增加 gas 预算。
    failure 且带有 ObjectVersionConflict交易中的对象版本与当前链上版本不匹配。获取最新的对象引用并重新构建交易。
    failure 且带有 ObjectDeleted对象已被删除。使用不同的对象或重新创建它。
    failure 且带有 CommandArgumentError命令参数无效。检查传递给命令的参数。

常见陷阱和故障排除清单

模拟交易很简单,但有几个陷阱可能导致混淆。使用此清单避免它们:

  1. 始终在构建交易之前获取最新的对象引用。使用 suix_getDynamicFieldsuix_getOwnedObjects 获取当前版本和摘要。过时的引用会导致版本冲突。

  1. 检查 effects.status 字段,而不仅仅是 RPC 响应。成功的 RPC 调用仍可能返回失败的交易状态。

  1. 理解 devInspectTransactiondryRunTransactionBlock 之间的区别。前者不需要 gas,并使用无限预算;后者使用您指定的 gas 预算和支付进行模拟。

  1. 对于 gas 估算,使用 dryRunTransactionBlockdevInspectTransaction 中的 gasUsed 不具有代表性,因为它假设无限预算。

  1. 注意方法名称的变化devInspectTransactionBlock 已弃用;在当前 SDK 中使用 devInspectTransaction。验证您的端点 API 版本支持的方法名称。

  1. 模拟共享对象时,确保提供正确的共享对象版本。共享对象的版本会随着每笔交易而递增,因此您必须获取最新版本。

  1. 如果您使用可编程交易块,确保所有命令有效且输入正确引用。单个无效命令将导致整个模拟失败。

模拟的局限性和权衡

模拟是一个强大的工具,但它有局限性。devInspectTransaction 不收取 gas,并使用模拟 gas 币,因此无法检测 gas 不足的错误。它还使用您提供的对象,如果您不新鲜获取,这些对象可能无法反映当前的链上状态。因此,成功的 devInspectTransaction 并不能保证真实交易会成功。

dryRunTransactionBlock 更准确,因为它使用实际的 gas 对象和当前状态。但是,它仍然不能保证成功,因为状态可能在模拟和实际提交之间发生变化。例如,另一笔交易可能会修改您正在使用的对象,导致版本冲突。

另一个限制是模拟不会执行交易范围之外具有副作用的 Move 代码。例如,如果您的交易调用发出事件的 Move 函数,则模拟期间不会发出该事件。这是预期的,因为模拟是只读的。

最后,模拟结果仅与您提供的数据一样好。如果您使用过时的对象引用或不正确的参数,模拟将具有误导性。始终在模拟之前获取最新数据。

后续步骤和进一步阅读

既然您了解了如何模拟 Sui 交易,您可以将其集成到您的开发工作流程中,以节省时间和金钱。为了加深您的知识,请探索以下资源:

  • RPC 定价 了解 RPC 调用(包括模拟)的成本。

永远不用担心基础设施

OnFinality 消除了 DevOps 的繁重工作,让您能够更聪明、更快地构建。

开始