Logo
New RPC users get 35% off their first monthView the offer
OnFinality Learn
Network & Protocol Guides12 min read

Simulating Sui Transactions Before Sending: devInspectTransaction and dryRunTransactionBlock

Learn how to simulate Sui transactions via JSON-RPC to catch object version conflicts, gas issues, and command failures before paying gas.

TL;DR

Before broadcasting a Sui transaction, you can simulate it over JSON-RPC using devInspectTransaction or dryRunTransactionBlock. These methods execute the transaction against the current state without committing, returning the TransactionEffects and execution status. This lets you catch object version conflicts, insufficient gas, and command-level failures at zero cost, ensuring your transaction will succeed when actually submitted.

Direct Answer: Simulate First, Pay Later

Sui transactions are not executed in a vacuum: they operate on a set of owned and shared objects, each with a specific version. A transaction that succeeds against one object version may fail against another. To avoid wasting gas on failed transactions, Sui provides two JSON-RPC methods that let you simulate a transaction before broadcasting it: devInspectTransaction and dryRunTransactionBlock. Both return the TransactionEffects that would result, including an execution status of success or failure. By inspecting these effects, you can catch object version conflicts, insufficient gas, and command-level errors before they cost you anything.

This guide explains the mechanism behind these simulation methods, how to decode the returned effects, and provides a reproducible TypeScript script using the official @mysten/sui/client SDK. You'll learn to distinguish between a successful simulation and a failed one, and how to handle the common pitfalls that trip up developers.

Understanding Sui Transaction Execution and Object Versioning

In Sui, a transaction is a TransactionData structure that contains a programmable TransactionBlock. This block consists of commands that operate on inputs, which are either owned objects, shared objects, or pure values. Each object in Sui has a unique ID and a monotonically increasing version number. When a transaction is executed, validators check that the object versions referenced in the transaction match the current versions on-chain. If they don't, the transaction fails with an object version conflict.

This versioning is crucial for simulation: when you simulate a transaction, you must ensure that the object versions you provide are the ones you intend to use. If you fetch an object's reference (ID and version) and then construct a transaction, but the object is mutated by another transaction before you submit, your simulation may succeed while the real submission fails. Therefore, always fetch the latest object references immediately before constructing your transaction.

The two simulation methods differ in their approach. dryRunTransactionBlock executes the transaction as if it were being submitted, requiring a gas object and a gas budget. It returns the exact effects that would be committed, including the gas used. devInspectTransaction, on the other hand, runs the transaction against a set of supplied objects without charging gas and without requiring a gas object. It assumes an infinite gas budget and is intended for development and testing. The results from devInspectTransaction should not be taken as the exact outcome on mainnet, because it uses the objects you provide, not necessarily the current on-chain state.

Both methods are read-only: they do not mutate state. However, dryRunTransactionBlock requires a gas payment and will fail if the gas budget is insufficient, while devInspectTransaction does not. This makes devInspectTransaction ideal for 'what-if' scenarios, such as testing a new command against a hypothetical object state.

The Two Simulation Entry Points: dryRunTransactionBlock vs devInspectTransaction

The Sui JSON-RPC API provides two primary methods for simulating transactions. Note that method names have evolved: devInspectTransactionBlock was renamed to devInspectTransaction in recent SDK versions, and older names are marked deprecated in several references. Always use the current method names as per your SDK and endpoint's API version; the Sui JSON-RPC documentation and the Sui API references for dryRunTransactionBlock and devInspectTransaction describe the current signatures. For example, the @mysten/sui/client SDK exposes client.devInspectTransaction and client.dryRunTransactionBlock.

dryRunTransactionBlock takes a TransactionBlock (or its serialized bytes) and a sender address. It executes the transaction against the current state, using the gas object specified in the transaction. It returns a DryRunTransactionBlockResponse containing the effects and any error. The effects include the status (success or failure), the gasUsed, and the list of created, mutated, and deleted objects.

devInspectTransaction takes a sender address, a TransactionBlock, and optionally a list of gasPrice and epoch. It does not require a gas object; instead, it uses a mock gas coin with an infinite balance. It returns a DevInspectResponse with the effects and results for each command. The effects include a gasUsed summary, but since the gas budget is infinite, the actual gas cost is not representative of a real transaction.

Key difference: dryRunTransactionBlock will fail if the gas budget is insufficient, while devInspectTransaction will not. Therefore, to test whether your transaction will succeed with a specific gas budget, use dryRunTransactionBlock. To test the logic of your transaction without worrying about gas, use devInspectTransaction.

Decoding Execution Status and Effects

The most important part of a simulation response is the effects.status. This is an object with a status field that is either 'success' or 'failure'. If it's 'failure', the error field contains a string describing the error. Common errors include 'MoveAbort', 'MoveModule', 'U64Wrap', and object version conflicts.

A command-level failure occurs when a specific command in the transaction block fails. For example, a MoveAbort error indicates that a Move module aborted, often due to a failed assertion. An object version conflict occurs when the input object's version does not match the current on-chain version. This is a common issue when you construct a transaction using stale object references.

It's crucial to distinguish between an RPC error and a successful RPC with a failure status. If the simulation call itself returns an error (e.g., invalid parameters), that's a client-side issue. If the call succeeds but the effects.status is 'failure', that means the transaction would fail if submitted. Many developers mistakenly treat any error as an RPC failure, but you must check the effects.status field.

The effects also contain a gasUsed object with computationCost, storageCost, and storageRebate. In a dry run, these reflect the actual gas that would be used. In a dev inspect, the gas used is computed assuming an infinite budget, so the gasUsed may be higher than what you'd actually pay. Always use dryRunTransactionBlock to estimate real gas costs.

Reproducible Example: Simulating a Coin Transfer with @mysten/sui/client

The following TypeScript script demonstrates how to simulate a simple coin transfer using devInspectTransaction and dryRunTransactionBlock. It connects to a Sui endpoint, constructs a transaction that transfers a specific amount of SUI from one address to another, and then simulates it. The script prints the execution status, gas summary, and any errors.

To run this script, you'll need Node.js and the @mysten/sui package. Install it with npm install @mysten/sui. Replace the endpoint URL and addresses with your own. The script uses the devInspectTransaction method, but you can easily switch to dryRunTransactionBlock by uncommenting the relevant lines.

Note: The script assumes you have an object (a coin) to transfer. You'll need to provide the object ID and its version. In a real scenario, you'd fetch these using suix_getOwnedObjects or 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);

Simulating Failure Scenarios: Object Version Conflicts and Insufficient Gas

To understand failure shapes, it's instructive to simulate an intentionally wrong object version or an insufficient gas budget. The following script demonstrates how to create a transaction that references an old version of an object, causing a version conflict, and how to simulate a transaction with a gas budget that is too low.

For the object version conflict, you can manually set the version of the coin object to an older version (e.g., version 1) while the current version is higher. This will cause the simulation to fail with an error indicating a version mismatch.

For insufficient gas, you can set a very low gas budget in the transaction and then call dryRunTransactionBlock. The simulation will return a failure status with an error about insufficient gas.

The table below summarizes common status strings and their meanings. Fill in the 'Fix' column based on your scenario.

// 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);
}

Fill-in Decision Table for Simulation Statuses

When you simulate a transaction, you'll encounter various status strings. The table below lists common ones and their meanings. Use it to quickly diagnose issues.

  • Status / ErrorMeaningFix
    successThe transaction would succeed.Submit it.
    failure with MoveAbortA Move module aborted, often due to a failed assertion.Inspect the Move code and the abort code.
    failure with MoveModuleAn error occurred in a Move module, such as a missing function or type mismatch.Check the module's interface and your transaction's commands.
    failure with U64WrapAn unsigned 64-bit integer overflowed.Adjust your calculations to avoid overflow.
    failure with InsufficientGasThe gas budget is too low.Increase the gas budget.
    failure with ObjectVersionConflictThe object version in the transaction does not match the current on-chain version.Fetch the latest object reference and rebuild the transaction.
    failure with ObjectDeletedThe object has been deleted.Use a different object or recreate it.
    failure with CommandArgumentErrorA command argument is invalid.Check the arguments passed to the command.

Common Pitfalls and Troubleshooting Checklist

Simulating transactions is straightforward, but several pitfalls can lead to confusion. Use this checklist to avoid them:

  1. Always fetch the latest object references before constructing your transaction. Use suix_getDynamicField or suix_getOwnedObjects to get the current version and digest. Stale references cause version conflicts.

  1. Check the effects.status field, not just the RPC response. A successful RPC call can still return a failed transaction status.

  1. Understand the difference between devInspectTransaction and dryRunTransactionBlock. The former does not require gas and uses an infinite budget; the latter simulates with your specified gas budget and payment.

  1. For gas estimation, use dryRunTransactionBlock. The gasUsed from devInspectTransaction is not representative because it assumes an infinite budget.

  1. Be aware of method naming changes. devInspectTransactionBlock is deprecated; use devInspectTransaction in current SDKs. Verify the method names supported by your endpoint's API version.

  1. When simulating shared objects, ensure you provide the correct shared object version. Shared objects have a version that increments with each transaction, so you must fetch the latest.

  1. If you're using a programmable transaction block, ensure all commands are valid and the inputs are correctly referenced. A single invalid command will cause the entire simulation to fail.

Limitations and Tradeoffs of Simulation

Simulation is a powerful tool, but it has limitations. devInspectTransaction does not charge gas and uses a mock gas coin, so it cannot detect insufficient gas errors. It also uses the objects you provide, which may not reflect the current on-chain state if you don't fetch them fresh. Therefore, a successful devInspectTransaction does not guarantee that the real transaction will succeed.

dryRunTransactionBlock is more accurate because it uses the actual gas object and current state. However, it still does not guarantee success because the state can change between the simulation and the actual submission. For example, another transaction could mutate an object you're using, causing a version conflict.

Another limitation is that simulation does not execute Move code that has side effects outside of the transaction's scope. For example, if your transaction calls a Move function that emits an event, the event will not be emitted during simulation. This is expected, as simulation is read-only.

Finally, simulation results are only as good as the data you provide. If you use stale object references or incorrect parameters, the simulation will be misleading. Always fetch the latest data before simulating.

Next Steps and Further Reading

Now that you understand how to simulate Sui transactions, you can integrate this into your development workflow to save time and money. To deepen your knowledge, explore the following resources:

  • Sui RPC timeouts to understand timeout behavior when simulating large transactions.

  • API service to get a dedicated endpoint for your development.

  • RPC pricing to understand the cost of RPC calls, including simulations.

Never Worry about Infrastructure Again

OnFinality takes away the heavy lifting of DevOps so you can build smarter and faster.

Get Started