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

Sui RPC Transaction Effects: Parsing objectChanges and balanceChanges

A practical guide to reading Sui transaction effects over JSON-RPC, parsing objectChanges and balanceChanges, and reconciling a digest against what actually happened on-chain.

TL;DR

Sui transaction effects are the protocol's authoritative record of what a transaction actually did: its status, gas usage, and the set of object and balance changes it produced. To read them over JSON-RPC, call sui_getTransactionBlock with options.showEffects, showObjectChanges, showBalanceChanges, and showEvents, then treat effects.status.status as the success/failure signal rather than the submission response. objectChanges distinguishes created, mutated, deleted, wrapped, unwrapped, and published objects, and each entry carries objectId, objectType, owner, version, and previousVersion; version is your optimistic-concurrency token. balanceChanges reports coinType, owner, and amount as signed decimal strings, so net deltas must be computed with integer or decimal arithmetic, never floating point. This article walks the response shape, the owner model, a runnable Node.js parser, a results table you fill against your own endpoint, and the failure modes that make effects null or misleading.

What Sui transaction effects represent in the protocol

In Sui, a transaction is submitted, but its effects are what the network actually committed. The effects object is the protocol's record of the transaction's outcome: whether it succeeded or failed, how much gas it consumed, and the exact set of objects and balances that changed. This is why reading effects is the correct way to confirm a transaction, rather than trusting the submission response, which only tells you that a fullnode accepted the request for execution.

The distinction matters because Sui separates execution from finality. A digest can be returned by a fullnode before the transaction is checkpointed, and a transaction can fail during execution while still consuming gas. The effects object, retrieved after execution, is the canonical answer to "what happened?" The Sui JSON-RPC reference documents the effects and change schemas at docs.sui.io/sui-api-ref, and the JSON-RPC 2.0 envelope that carries the request is specified at jsonrpc.org/specification.

For teams building indexers, wallets, or reconciliation jobs, effects are the join point between a submitted digest and the on-chain state you must update. If you are new to the network, start with the Sui network overview and the Sui RPC guide before wiring effects into production.

  • Effects are the committed outcome, not the submission acknowledgement.
  • They carry status, gas, object changes, balance changes, and events.
  • They are the correct source for reconciliation and indexing.

The SuiTransactionBlockResponse shape and the options that populate it

sui_getTransactionBlock returns a SuiTransactionBlockResponse. The fields you will use most are digest, effects, events, objectChanges, balanceChanges, checkpoint, and timestampMs. Crucially, several of these are only populated when you request them: options.showEffects, options.showObjectChanges, options.showBalanceChanges, and options.showEvents. If you omit them, the fields come back null or empty and you may wrongly conclude a transaction had no consequences.

The response is wrapped in a standard JSON-RPC 2.0 result envelope, so the payload lives under result. The Sui TypeScript SDK types this response as SuiTransactionBlockResponse and exposes getTransactionBlock, which is documented at sdk.mystenlabs.com. The SDK types the effect unions for you, but the semantics of each change type still come from the protocol documentation.

A minimal request therefore looks like a JSON-RPC call with a params array containing the digest and an options object. The next section shows a curl form you can run immediately against any Sui fullnode endpoint.

curl -s https://your-sui-endpoint.example \
  -H 'Content-Type: application/json' \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "sui_getTransactionBlock",
    "params": [
      "0xYOUR_TRANSACTION_DIGEST",
      {
        "showEffects": true,
        "showObjectChanges": true,
        "showBalanceChanges": true,
        "showEvents": true
      }
    ]
  }'

Reading effects.status as the authoritative success or failure signal

The field effects.status.status is either "success" or "failure". When it is "failure", effects.status.error carries a structured error describing what went wrong. This is the authoritative signal: a digest that exists on-chain is not the same as a transaction that succeeded. A failed transaction still consumes gas, and its effects still record the gas cost and any partial state that the protocol committed.

A subtle but important case is a transaction that succeeds while individual commands produce no object changes. For example, a Move call that reads state and returns a value, or a command whose effects are entirely internal, can succeed with an empty objectChanges array. Do not treat an empty change set as a failure; check effects.status.status first, then interpret the change set.

Because failure still consumes gas, reconciliation logic must handle failed digests explicitly. If your job only processes successful transactions, filter on effects.status.status === "success" and record the failure reason separately for observability.

  • effects.status.status is "success" or "failure".
  • effects.status.error explains failures.
  • A successful digest can still be a failed transaction.
  • An empty objectChanges array is valid on a successful transaction.

objectChanges: change types and the fields that matter

objectChanges is an array of ObjectChange entries. Each entry has a type field that is one of created, mutated, deleted, wrapped, unwrapped, or published. The fields that matter for reconciliation are objectId, objectType, owner, version, previousVersion, and digest. For published entries, the packageId and modules fields identify the new package.

To detect a mutation versus a create, inspect the type field directly rather than inferring from the presence of previousVersion. A created object has no previousVersion; a mutated object has both version and previousVersion. The Sui documentation on object ownership, versions, and the effect model at docs.sui.io/concepts is authoritative for why these distinctions exist.

version is your optimistic-concurrency token. When you later mutate an object, you must reference the version you last observed. If another transaction has since mutated it, your version is stale and the transaction will fail. Storing the version from objectChanges is therefore how you keep your local state consistent with the chain.

  • Types: created, mutated, deleted, wrapped, unwrapped, published.
  • Key fields: objectId, objectType, owner, version, previousVersion, digest.
  • Use type, not field presence, to classify a change.
  • version is the optimistic-concurrency token for follow-up writes.

balanceChanges and computing net deltas without floating point

balanceChanges is an array of BalanceChange entries with coinType, owner, and amount. The amount is a signed decimal string, not a number. Positive amounts are credits and negative amounts are debits, and the gas cost appears as a negative balance change for the gas payer. Because amounts are strings, you must parse them with a big-integer or decimal library, never with JavaScript Number, which loses precision on large values.

To compute a net balance delta per coin type and per owner, group the entries by the pair (coinType, owner) and sum the parsed integer amounts. The result is the net change for that owner in that coin type, including gas. This is the correct way to answer "how much did this address gain or lose?" without floating-point drift.

A common mistake is to sum only the positive entries and ignore gas, which overstates the received amount. Always include the negative gas entry for the payer. If you need the gross transfer amount separately from gas, sum the non-gas entries and report gas as its own line item.

  • amount is a signed decimal string; parse it as an integer.
  • Group by (coinType, owner) and sum to get net deltas.
  • Gas appears as a negative balance change for the payer.
  • Never use floating point for Sui amounts.

The owner model and what each owner type implies for follow-up reads

The owner field on an object change tells you who controls the object and therefore how you must read it later. AddressOwner means a single address owns it; you can read it with sui_getObject and mutate it by signing with that address. ObjectOwner means another object owns it, which is common for dynamic fields and child objects; you typically reach it through its parent.

Shared means the object is shared and can be accessed by many transactions, often with consensus ordering. Immutable means the object can never be mutated again, which is typical for published packages and frozen objects. Each type changes your follow-up read strategy, so record the owner type alongside the objectId.

For a deeper treatment of reading objects and their dynamic fields, see Reading Sui objects over RPC. That article covers the read path that complements the change path described here.

  • AddressOwner: single-address control, direct reads and writes.
  • ObjectOwner: owned by another object, often a dynamic field.
  • Shared: accessible by many transactions, consensus-ordered.
  • Immutable: frozen forever, typical for packages.

A runnable Node.js parser for effects, object changes, and balance deltas

The following example uses @mysten/sui to fetch a digest, assert effects.status, then print a table of object changes, balance deltas, and published package ids. It parses amounts with BigInt so no precision is lost. Replace the endpoint and digest with your own values.

The script groups balance changes by coin type and owner, sums them as BigInt, and prints the net delta. It also collects published package ids from objectChanges entries of type published. Run it against a fullnode that has the transaction indexed; if effects is null, the node has not indexed or has pruned the transaction, which the troubleshooting section covers.

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

const client = new SuiClient({ url: getFullnodeUrl('mainnet') });
const digest = process.env.SUI_DIGEST;

const tx = await client.getTransactionBlock({
  digest,
  options: {
    showEffects: true,
    showObjectChanges: true,
    showBalanceChanges: true,
    showEvents: true,
  },
});

if (!tx.effects) {
  throw new Error('effects is null: node not indexed or pruned');
}

const status = tx.effects.status.status;
console.log('status:', status);
if (status === 'failure') {
  console.error('error:', tx.effects.status.error);
}

console.log('\nObject changes:');
for (const c of tx.objectChanges ?? []) {
  console.log([c.type, c.objectId, c.objectType, c.owner, c.version].join(' | '));
}

const deltas = new Map();
for (const b of tx.balanceChanges ?? []) {
  const key = b.coinType + '|' + JSON.stringify(b.owner);
  const prev = deltas.get(key) ?? 0n;
  deltas.set(key, prev + BigInt(b.amount));
}

console.log('\nNet balance deltas:');
for (const [key, amount] of deltas) {
  console.log(key, amount.toString());
}

const packages = (tx.objectChanges ?? [])
  .filter((c) => c.type === 'published')
  .map((c) => c.packageId);
console.log('\nPublished packages:', packages);

A results table to fill against your own endpoint

Because effects availability and latency vary by provider, the only reliable measurement is one you run against your own endpoint. Use the table below as a template. For each digest, record whether effects was present, the status, the number of object changes, the number of balance changes, and the wall-clock time to fetch. Repeat across several digests and at least two endpoints to compare.

Do not treat any single row as a benchmark. The purpose is to characterize your provider's indexing behavior and to detect gaps before they reach production. If effects is null for a digest you know is finalized, that is a signal to investigate the node's indexing or pruning configuration.

  • Columns: digest, effects present (yes/no), status, objectChanges count, balanceChanges count, fetch ms.
  • Run at least 10 digests per endpoint for a meaningful sample.
  • Compare a recent digest against an older one to probe pruning.
  • Record the endpoint URL and timestamp for each row.

Failure modes and limitations of effects-based reconciliation

The most common failure mode is effects being null on an unindexed or pruned fullnode. A fullnode that has not indexed the transaction, or that has pruned historical state, will return null for effects even though the transaction is finalized. This is a provider configuration issue, not a protocol issue, and it varies by provider. If you need historical effects, verify your provider's retention policy before relying on it.

A second failure mode is confusing events with effects. Events are emitted by Move code and are not the same as object changes; a transaction can emit events without changing objects, and can change objects without emitting events. Use Querying Sui events over RPC for the event path and keep effects for state reconciliation.

A third failure mode is decimal-string arithmetic. Summing amounts as floating point silently corrupts large values. Always parse to BigInt or a decimal library. Finally, large change sets can be paginated or truncated by some providers; if you expect many changes, verify the full array length and consider streaming via Sui checkpoint streaming with the gRPC ledger service for high-volume indexing.

  • effects null: unindexed or pruned fullnode, provider-dependent.
  • Events are not effects; do not substitute one for the other.
  • Floating-point sums corrupt decimal-string amounts.
  • Large change sets may be paginated or truncated.

Troubleshooting common effects parsing problems

If effects is null, first confirm the digest is correct and the transaction is finalized by checking a block explorer or a second endpoint. If a second endpoint returns effects, the first node has an indexing or pruning gap. If both return null, the digest may be malformed or the transaction may not exist.

If objectChanges is empty on a successful transaction, that is valid; the transaction may have only read state or produced internal effects. If balanceChanges is empty but you expected a transfer, check that showBalanceChanges was set to true in the request options. If amounts look wrong, verify you are parsing them as strings and not as numbers.

If you see a version mismatch when you later try to mutate an object, you are using a stale version. Re-fetch the object with Reading Sui objects over RPC and use the latest version. For pre-send validation, Simulating Sui transactions with devInspectTransaction lets you inspect effects before committing.

  • Cross-check the digest on a second endpoint.
  • Confirm showBalanceChanges and showObjectChanges are true.
  • Parse amounts as strings, not numbers.
  • Re-fetch objects to refresh stale versions.

Next steps for production effects pipelines

For production reconciliation, combine effects parsing with a durable queue and idempotent writes keyed on digest. Use Sui RPC WebSocket subscriptions for low-latency notifications, and fall back to polling sui_getTransactionBlock for backfill. For high-volume indexing, the gRPC ledger service is often a better fit than per-digest RPC calls.

If you need managed endpoints with predictable retention, review the Sui network page, RPC pricing, and the API service. The OnFinality Learn hub collects the related guides, and the Sui RPC guide covers the broader method surface.

Start by running the Node.js parser above against a handful of digests, fill in the results table, and only then wire effects into your reconciliation job. That sequence keeps you from building on an endpoint whose indexing behavior you have not measured.

  • Key idempotent writes on digest.
  • Use WebSocket subscriptions for low latency, polling for backfill.
  • Consider gRPC for high-volume indexing.
  • Measure your endpoint before committing to it.

Never Worry about Infrastructure Again

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

Get Started