Skip to main content

gRPC Migration Cookbook

Each recipe below pairs a JSON-RPC call with its gRPC replacement and explains the key differences. For the full method mapping and decision criteria, see the JSON-RPC Migration Guide. For gRPC concepts and setup, see What is gRPC? and Querying Data with gRPC.

info

JSON-RPC is deprecated. Migrate to either gRPC or GraphQL RPC before the week of July 27, 2026, when JSON-RPC is disabled on Sui Foundation mainnet full nodes. Full decommission, including code removal, is planned for mid-October 2026. For a method mapping, decision criteria, and the full timeline, see the JSON-RPC Migration Guide.

Refer to the list of RPC or data providers that have enabled gRPC on their full nodes or offer GraphQL RPC. Contact a provider directly to request access. If your RPC or data provider doesn’t yet support these data access methods, ask them to enable support or contact the Sui Foundation team on Discord, Telegram, or Slack for help.

Before you start

All gRPC examples use the @mysten/sui TypeScript SDK. Install it with:

npm install @mysten/sui

Create a client once and reuse it:

import { SuiGrpcClient } from '@mysten/sui/grpc';

const client = new SuiGrpcClient({
baseUrl: 'https://fullnode.mainnet.sui.io:443',
network: 'mainnet',
});

For grpcurl and Buf CLI examples, replace <full node URL:port> with your provider's gRPC endpoint. See Querying Data with gRPC for setup details and language-specific client instructions for Go and Python.

Get a single object

Replace sui_getObject with LedgerService.GetObject. Use a field mask to request only the fields you need.

curl -X POST https://fullnode.mainnet.sui.io:443 \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "sui_getObject",
"params": [
"0xOBJECT_ID",
{ "showContent": true, "showOwner": true }
]
}'

Key differences:

  • JSON-RPC uses showContent, showOwner booleans. gRPC uses a read_mask with field paths, or the SDK's include parameter.
  • The SDK's getObject returns { object } directly. Use getObjects (plural) for batch lookups, which returns { objects: (Object | Error)[] }.

Batch-fetch objects

Replace sui_multiGetObjects with LedgerService.BatchGetObjects. The top-level read_mask applies to all objects in the batch.

curl -X POST https://fullnode.mainnet.sui.io:443 \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "sui_multiGetObjects",
"params": [
["0xOBJECT_A", "0xOBJECT_B", "0xOBJECT_C"],
{ "showContent": true }
]
}'

Key differences:

  • Only the top-level read_mask is respected. Any mask inside individual sub-requests is ignored.
  • The response returns objects in the same order as the request.

Get a transaction

Replace sui_getTransactionBlock with LedgerService.GetTransaction.

curl -X POST https://fullnode.mainnet.sui.io:443 \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "sui_getTransactionBlock",
"params": [
"J4NvV5iQZQFm1xKPYv9ffDCCPW6cZ4yFKsCqFUiDX5L4",
{ "showEffects": true, "showEvents": true }
]
}'

Key differences:

  • JSON-RPC uses showEffects, showEvents booleans. gRPC uses read_mask paths or the SDK's include.
  • Digests are Base58-encoded strings in both APIs.

Batch-fetch transactions

Replace sui_multiGetTransactionBlocks with LedgerService.BatchGetTransactions.

curl -X POST https://fullnode.mainnet.sui.io:443 \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "sui_multiGetTransactionBlocks",
"params": [
["DIGEST_A", "DIGEST_B"],
{ "showEffects": true, "showEvents": true }
]
}'

Key differences:

  • Same read_mask rules as batch objects: only the top-level mask is respected.

Query events

JSON-RPC offered suix_queryEvents with filters like MoveModule, MoveEventType, Sender, and Transaction. gRPC does not have a direct filtered-event query. Instead, read events from transaction data or stream them from checkpoints.

Events from a known transaction

If you know the transaction digest, fetch the transaction with events included.

curl -X POST https://fullnode.mainnet.sui.io:443 \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "suix_queryEvents",
"params": [
{ "Transaction": "8NB8sXb4m9PJhCyLB7eVH4onqQWoFFzVUrqPoYUhcQe2" },
null, 50, false
]
}'

Live event streaming from checkpoints

Replace sui_subscribeEvent (WebSocket) with SubscriptionService.SubscribeCheckpoints. Stream finalized checkpoints, then filter events client-side by type, module, or sender.

// Old WebSocket subscription — no longer supported
const ws = new WebSocket('wss://fullnode.mainnet.sui.io/websocket');
ws.send(JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'sui_subscribeEvent',
params: [{ MoveEventType: '0xPACKAGE::module::MyEvent' }],
}));
ws.onmessage = (msg) => console.log(JSON.parse(msg.data));

Key differences:

  • JSON-RPC WebSocket subscriptions filtered events server-side. gRPC streams entire checkpoints and you filter client-side.
  • The gRPC stream is ordered and gapless. If your stream disconnects, resume from the last processed checkpoint sequence number. Backfill any missed range with LedgerService.GetCheckpoint before resubscribing. See Subscriptions for streaming data.
  • For filtered historical event queries over a range, use GraphQL Query.events instead.

Stream checkpoints

Replace sui_getCheckpoints polling with SubscriptionService.SubscribeCheckpoints for a real-time, ordered stream.

// Old polling pattern — inefficient and deprecated
let cursor = null;
while (true) {
const res = await fetch('https://fullnode.mainnet.sui.io:443', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0', id: 1,
method: 'sui_getCheckpoints',
params: [cursor, 10, false],
}),
});
const { result } = await res.json();
for (const cp of result.data) {
console.log('Checkpoint:', cp.sequenceNumber);
cursor = cp.sequenceNumber;
}
if (!result.hasNextPage) await new Promise((r) => setTimeout(r, 1000));
}

Key differences:

  • No polling loop needed. The gRPC stream pushes checkpoints as they finalize.
  • Checkpoints arrive in order with no gaps. Track the last processed sequenceNumber to resume after a disconnect.
  • grpcurl does not support server-side streaming. Use the Buf CLI or an SDK client instead.

Look up a single checkpoint

Replace sui_getCheckpoint with LedgerService.GetCheckpoint.

info

A full node only serves checkpoints within its retention window. If you query an old sequence number and get NOT_FOUND, either use a more recent checkpoint or query the Archival Service instead.

curl -X POST https://fullnode.mainnet.sui.io:443 \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "sui_getCheckpoint",
"params": ["<CHECKPOINT_SEQUENCE_NUMBER>"]
}'

Get balances

Replace suix_getBalance and suix_getAllBalances with StateService.GetBalance and StateService.ListBalances.

# Single coin type
curl -X POST https://fullnode.mainnet.sui.io:443 \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "suix_getBalance",
"params": ["0xADDRESS", "0x2::sui::SUI"]
}'

# All coin types
curl -X POST https://fullnode.mainnet.sui.io:443 \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "suix_getAllBalances",
"params": ["0xADDRESS"]
}'

Key differences:

  • gRPC separates coinBalance (coin objects) and addressBalance (accumulator). The balance field is the combined total. See Using Address Balances for reconciliation details.

List owned coins

Replace suix_getCoins and suix_getAllCoins with StateService.ListOwnedObjects filtered by coin type.

curl -X POST https://fullnode.mainnet.sui.io:443 \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "suix_getCoins",
"params": ["0xADDRESS", "0x2::sui::SUI", null, 50]
}'

Key differences:

List dynamic fields

Replace suix_getDynamicFields with StateService.ListDynamicFields.

curl -X POST https://fullnode.mainnet.sui.io:443 \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "suix_getDynamicFields",
"params": ["0xPARENT_OBJECT_ID", null, 50]
}'

Key differences:

  • For suix_getDynamicFieldObject, derive the dynamic field's object ID locally from the parent ID and field name, then call LedgerService.GetObject. You do not need ListDynamicFields when you already know the field name.

Execute a transaction

Replace sui_executeTransactionBlock with TransactionExecutionService.ExecuteTransaction.

curl -X POST https://fullnode.mainnet.sui.io:443 \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "sui_executeTransactionBlock",
"params": [
"<BASE64_TX_BYTES>",
["<BASE64_SIGNATURE>"],
{ "showEffects": true }
]
}'

Key differences:

Simulate a transaction (dry run)

Replace sui_dryRunTransactionBlock and sui_devInspectTransactionBlock with TransactionExecutionService.SimulateTransaction.

curl -X POST https://fullnode.mainnet.sui.io:443 \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "sui_dryRunTransactionBlock",
"params": ["<BASE64_TX_BYTES>"]
}'

Get reference gas price

Replace suix_getReferenceGasPrice with getReferenceGasPrice on the SDK, or LedgerService.GetEpoch via grpcurl and read the reference_gas_price field.

curl -X POST https://fullnode.mainnet.sui.io:443 \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "suix_getReferenceGasPrice",
"params": []
}'

Resolve a SuiNS name

Replace JSON-RPC name resolution with NameService.LookupName and NameService.ReverseLookupName.

# Name to address
curl -X POST https://fullnode.mainnet.sui.io:443 \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "suix_resolveNameServiceAddress",
"params": ["example.sui"]
}'

Common patterns

Reconnecting a checkpoint stream

If the gRPC stream disconnects, backfill any missed checkpoints before resubscribing:

let lastProcessed = 0n;

async function startStream() {
const { responses } = client.subscriptionService.subscribeCheckpoints({
readMask: { paths: ['sequenceNumber', 'transactions'] },
});

try {
for await (const response of responses) {
const seq = response.checkpoint?.sequenceNumber ?? 0n;

// Detect and backfill gaps
if (seq > lastProcessed + 1n && lastProcessed > 0n) {
for (let i = lastProcessed + 1n; i < seq; i++) {
const { response: missed } = await client.ledgerService.getCheckpoint({
checkpointId: { oneofKind: 'sequenceNumber', sequenceNumber: i },
readMask: { paths: ['transactions'] },
});
processCheckpoint(missed.checkpoint);
}
}

processCheckpoint(response.checkpoint);
lastProcessed = seq;
}
} catch (err) {
console.error('Stream disconnected, reconnecting...', err);
startStream(); // reconnect
}
}

Paginating through all owned objects

import type { SuiClientTypes } from '@mysten/sui';

let cursor: string | null = null;

do {
const page: SuiClientTypes.ListOwnedObjectsResponse = await client.core.listOwnedObjects({
owner: '0xADDRESS',
limit: 50,
cursor,
});

for (const obj of page.objects) {
console.log(obj.objectId);
}

cursor = page.hasNextPage ? page.cursor : null;
} while (cursor);

Falling back to Archival for pruned data

A full node returns NOT_FOUND for data outside its retention window. Fall back to the Archival Service for historical lookups:

import { SuiGrpcClient } from '@mysten/sui/grpc';

const fullNode = new SuiGrpcClient({
baseUrl: 'https://fullnode.mainnet.sui.io:443',
network: 'mainnet',
});

const archive = new SuiGrpcClient({
baseUrl: 'https://archive.mainnet.sui.io:443',
network: 'mainnet',
});

async function getTransaction(digest: string) {
const result = await fullNode.core.getTransaction({
digest,
include: { effects: true },
});

if (result.$kind === 'Transaction') {
return result.Transaction;
}

// Data was pruned — fall back to archival
const archiveResult = await archive.core.getTransaction({
digest,
include: { effects: true },
});

if (archiveResult.$kind === 'Transaction') {
return archiveResult.Transaction;
}

throw new Error(`Transaction ${digest} not found in full node or archive`);
}