在广播 Sui 交易之前,您可以通过 JSON-RPC 使用 devInspectTransaction 或 dryRunTransactionBlock 进行模拟。这些方法针对当前状态执行交易而不提交,返回交易效果和执行状态。这使您能够以零成本捕获对象版本冲突、gas 不足和命令级失败,确保您的交易在实际提交时能够成功。
直接回答:先模拟,后付费
Sui 交易并非在真空中执行:它们操作一组拥有和共享的对象,每个对象都有特定的版本。针对一个对象版本成功的交易可能针对另一个版本失败。为了避免在失败交易上浪费 gas,Sui 提供了两种 JSON-RPC 方法,让您可以在广播交易前进行模拟:devInspectTransaction 和 dryRunTransactionBlock。两者都返回将产生的交易效果,包括 success 或 failure 的执行状态。通过检查这些效果,您可以在它们造成任何成本之前捕获对象版本冲突、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.devInspectTransaction 和 client.dryRunTransactionBlock。
dryRunTransactionBlock 接受一个 TransactionBlock(或其序列化字节)和一个 sender 地址。它针对当前状态执行交易,使用交易中指定的 gas 对象。它返回一个 DryRunTransactionBlockResponse,包含 effects 和任何错误。effects 包括 status(成功或失败)、gasUsed 以及创建、修改和删除的对象列表。
devInspectTransaction 接受一个 sender 地址、一个 TransactionBlock,以及可选的 gasPrice 和 epoch 列表。它不需要 gas 对象;相反,它使用一个余额无限的模拟 gas 币。它返回一个 DevInspectResponse,包含每个命令的 effects 和 results。effects 包括 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 对象,其中包含 computationCost、storageCost 和 storageRebate。在 dry run 中,这些反映了实际将使用的 gas。在 dev inspect 中,使用的 gas 是在假设无限预算的情况下计算的,因此 gasUsed 可能高于您实际支付的费用。始终使用 dryRunTransactionBlock 来估算真实的 gas 成本。
可复现示例:使用 @mysten/sui/client 模拟代币转账
以下 TypeScript 脚本演示了如何使用 devInspectTransaction 和 dryRunTransactionBlock 模拟简单的代币转账。它连接到 Sui 端点,构建一个将特定数量的 SUI 从一个地址转移到另一个地址的交易,然后进行模拟。脚本打印执行状态、gas 摘要和任何错误。
要运行此脚本,您需要 Node.js 和 @mysten/sui 包。使用 npm install @mysten/sui 安装它。将端点 URL 和地址替换为您自己的。该脚本使用 devInspectTransaction 方法,但您可以通过取消注释相关行轻松切换到 dryRunTransactionBlock。
注意:脚本假设您有一个要转移的对象(代币)。您需要提供对象 ID 及其版本。在真实场景中,您将使用 suix_getOwnedObjects 或 suix_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命令参数无效。 检查传递给命令的参数。
常见陷阱和故障排除清单
模拟交易很简单,但有几个陷阱可能导致混淆。使用此清单避免它们:
- 始终在构建交易之前获取最新的对象引用。使用
suix_getDynamicField或suix_getOwnedObjects获取当前版本和摘要。过时的引用会导致版本冲突。
- 检查
effects.status字段,而不仅仅是 RPC 响应。成功的 RPC 调用仍可能返回失败的交易状态。
- 理解
devInspectTransaction和dryRunTransactionBlock之间的区别。前者不需要 gas,并使用无限预算;后者使用您指定的 gas 预算和支付进行模拟。
- 对于 gas 估算,使用
dryRunTransactionBlock。devInspectTransaction中的gasUsed不具有代表性,因为它假设无限预算。
- 注意方法名称的变化。
devInspectTransactionBlock已弃用;在当前 SDK 中使用devInspectTransaction。验证您的端点 API 版本支持的方法名称。
- 模拟共享对象时,确保提供正确的共享对象版本。共享对象的版本会随着每笔交易而递增,因此您必须获取最新版本。
- 如果您使用可编程交易块,确保所有命令有效且输入正确引用。单个无效命令将导致整个模拟失败。
模拟的局限性和权衡
模拟是一个强大的工具,但它有局限性。devInspectTransaction 不收取 gas,并使用模拟 gas 币,因此无法检测 gas 不足的错误。它还使用您提供的对象,如果您不新鲜获取,这些对象可能无法反映当前的链上状态。因此,成功的 devInspectTransaction 并不能保证真实交易会成功。
dryRunTransactionBlock 更准确,因为它使用实际的 gas 对象和当前状态。但是,它仍然不能保证成功,因为状态可能在模拟和实际提交之间发生变化。例如,另一笔交易可能会修改您正在使用的对象,导致版本冲突。
另一个限制是模拟不会执行交易范围之外具有副作用的 Move 代码。例如,如果您的交易调用发出事件的 Move 函数,则模拟期间不会发出该事件。这是预期的,因为模拟是只读的。
最后,模拟结果仅与您提供的数据一样好。如果您使用过时的对象引用或不正确的参数,模拟将具有误导性。始终在模拟之前获取最新数据。
后续步骤和进一步阅读
既然您了解了如何模拟 Sui 交易,您可以将其集成到您的开发工作流程中,以节省时间和金钱。为了加深您的知识,请探索以下资源:
- Sui RPC 指南(RPC 助手) 了解 Sui RPC 方法的全面概述。
- Sui WebSocket 订阅 了解如何实时订阅交易效果。
- Sui RPC 超时 了解模拟大型交易时的超时行为。
- 通过 RPC 查询 Sui 历史状态 获取过去的对象版本以进行测试。
- 监控 RPC 端点 确保您的端点对于模拟是可靠的。
- OnFinality 学习中心 获取有关 Sui 和其他网络的更多指南。
- Sui 网络概述 获取有关 Sui 的一般信息。
- API 服务 获取专用端点以进行开发。
- RPC 定价 了解 RPC 调用(包括模拟)的成本。