Skip to main content

gRPC Migration Cookbook

Each recipe 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 the SDK with the following command:

$ npm install @mysten/sui

Create a client once and reuse it:

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

const client = new SuiGrpcClient({
baseUrl: '<FULL_NODE_URL>',
network: 'mainnet',
});

For grpcurl and Buf CLI examples, replace <FULL_NODE_URL> 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 <FULL_NODE_URL> \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "sui_getObject",
"params": [
"0xOBJECT_ID",
{ "showContent": true, "showOwner": true }
]
}'

The gRPC call differs from the JSON-RPC call in the following ways:

  • JSON-RPC uses the showContent and 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 <FULL_NODE_URL> \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "sui_multiGetObjects",
"params": [
["0xOBJECT_A", "0xOBJECT_B", "0xOBJECT_C"],
{ "showContent": true }
]
}'

The gRPC call differs from the JSON-RPC call in the following ways:

  • The service respects only the top-level read_mask. The service ignores any mask inside individual sub-requests.
  • The response returns objects in the same order as the request.

Get a transaction

Replace sui_getTransactionBlock with LedgerService.GetTransaction.

$ curl -X POST <FULL_NODE_URL> \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "sui_getTransactionBlock",
"params": [
"J4NvV5iQZQFm1xKPYv9ffDCCPW6cZ4yFKsCqFUiDX5L4",
{ "showEffects": true, "showEvents": true }
]
}'

The gRPC call differs from the JSON-RPC call in the following ways:

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

Batch-fetch transactions

Replace sui_multiGetTransactionBlocks with LedgerService.BatchGetTransactions.

$ curl -X POST <FULL_NODE_URL> \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "sui_multiGetTransactionBlocks",
"params": [
["DIGEST_A", "DIGEST_B"],
{ "showEffects": true, "showEvents": true }
]
}'

The same read_mask rules apply as for batch objects: the service respects only the top-level mask.

Query events

JSON-RPC uses suix_queryEvents with filters such as MoveModule, MoveEventType, Sender, and Transaction. gRPC provides LedgerService.ListEvents for paginated, filtered historical queries and SubscriptionService.SubscribeEvents for live streaming with the same filters. You can also read events from a specific transaction.

Events from a known transaction

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

$ curl -X POST <FULL_NODE_URL> \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "suix_queryEvents",
"params": [
{ "Transaction": "8NB8sXb4m9PJhCyLB7eVH4onqQWoFFzVUrqPoYUhcQe2" },
null, 50, false
]
}'

Live event streaming

Replace sui_subscribeEvent (WebSocket) with SubscriptionService.SubscribeEvents. The JSON-RPC WebSocket subscription APIs have been deprecated since mid-2024, so migrate any remaining WebSocket subscriptions to gRPC streaming. The server filters events before it sends them, so your client receives only matching events.

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

The gRPC subscription differs from the JSON-RPC subscription in the following ways:

  • JSON-RPC WebSocket subscriptions filtered events server-side, and have been deprecated since mid-2024. gRPC SubscribeEvents also filters server-side using an EventFilter, so your client receives only matching events.
  • Subscriptions always begin at the current tip of the chain and do not accept a resume point. To avoid missing data between a backfill and a subscription, open the subscription first, note the first Watermark from SubscribeEvents, then backfill with LedgerService.ListEvents up to that watermark. After the backfill completes, begin consuming from the already-open subscription. See Subscriptions for streaming data.
  • For filtered historical event queries over a range, use LedgerService.ListEvents or GraphQL Query.events.

Query events over a checkpoint range

Use LedgerService.ListEvents to retrieve events matching an EventFilter across a range of checkpoints. This method replaces the paginated suix_queryEvents pattern.

info

The @mysten/sui TypeScript SDK does not yet expose ListEvents. Use the generated proto client directly until the SDK adds a wrapper, or use the Rust SDK, which supports ListEvents. The grpcurl example in the next tab works today.

// Use the generated proto client directly
const stream = client.ledgerService.listEvents({
startCheckpoint: BigInt(1000000),
endCheckpoint: BigInt(1000100),
filter: {
terms: [{
literals: [{
eventType: { eventType: '<PACKAGE>::module::MyEvent' },
}],
}],
},
options: { limit: 50 },
});

for await (const response of stream.responses) {
console.log('Event:', response.event);
}

Stream checkpoints

Replace sui_getCheckpoints polling with SubscriptionService.SubscribeCheckpoints for a real-time, ordered stream, or LedgerService.ListCheckpoints for paginated queries over a checkpoint range.

// Old polling pattern (inefficient and deprecated)
let cursor = null;
while (true) {
const res = await fetch('<FULL_NODE_URL>', {
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));
}

The gRPC stream differs from the JSON-RPC polling pattern in the following ways:

  • You do not need a polling loop. The gRPC stream pushes checkpoints as they finalize.
  • Use LedgerService.ListCheckpoints to query a historical range of checkpoints, optionally filtered by a TransactionFilter. Use SubscriptionService.SubscribeCheckpoints for live streaming from the current tip.
  • Subscriptions do not support resumption. To avoid gaps, open the subscription first, note the first cursor from SubscribeCheckpoints, then backfill missed checkpoints with ListCheckpoints up to that cursor before you consume from the already-open stream.
  • grpcurl supports server-side streaming for the LedgerService.List* calls on this page. For SubscriptionService subscriptions, which are indefinite streams, the Buf CLI or an SDK client provides better control over timeouts and reconnection.

Look up a single checkpoint

Replace sui_getCheckpoint with LedgerService.GetCheckpoint.

info

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

$ curl -X POST <FULL_NODE_URL> \
-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.

To query a single coin type, run the following command:

$ curl -X POST <FULL_NODE_URL> \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "suix_getBalance",
"params": ["<ADDRESS>", "0x2::sui::SUI"]
}'

To query all coin types, run the following command:

$ curl -X POST <FULL_NODE_URL> \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "suix_getAllBalances",
"params": ["<ADDRESS>"]
}'

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 <FULL_NODE_URL> \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "suix_getCoins",
"params": ["<ADDRESS>", "0x2::sui::SUI", null, 50]
}'

The gRPC approach differs from the JSON-RPC approach in the following ways:

  • There is no dedicated coin-listing RPC. Use ListOwnedObjects with a Coin<T> type filter.
  • To list coins of every type, filter by 0x2::coin::Coin without a type parameter.
  • For transaction building, prefer gas smashing or address-balance gas payments over manual coin selection.

List dynamic fields

Replace suix_getDynamicFields with StateService.ListDynamicFields.

$ curl -X POST <FULL_NODE_URL> \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "suix_getDynamicFields",
"params": ["0xPARENT_OBJECT_ID", null, 50]
}'

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 <FULL_NODE_URL> \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "sui_executeTransactionBlock",
"params": [
"BASE64_TX_BYTES",
["BASE64_SIGNATURE"],
{ "showEffects": true }
]
}'

The JSON-RPC unsafe_* builder methods, such as unsafe_moveCall and unsafe_transferObject, have no gRPC equivalent. Build transactions with programmable transaction blocks (PTBs) using the SDK, then submit the bytes. See Building PTBs and Signing and Sending Transactions.

Simulate a transaction (dry run)

Replace sui_dryRunTransactionBlock and sui_devInspectTransactionBlock with TransactionExecutionService.SimulateTransaction.

$ curl -X POST <FULL_NODE_URL> \
-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 call LedgerService.GetEpoch through grpcurl and read the reference_gas_price field.

$ curl -X POST <FULL_NODE_URL> \
-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.

To resolve a name to an address, run the following command:

$ curl -X POST <FULL_NODE_URL> \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "suix_resolveNameServiceAddress",
"params": ["example.sui"]
}'

Common patterns

Reconnect a checkpoint stream

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

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
}
}

Paginate through all owned objects

Use the cursor from each response to request the next page:

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

let cursor: string | null = null;

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

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

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

Fall back to the Archival Store for pruned data

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

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

const fullNode = new SuiGrpcClient({
baseUrl: '<FULL_NODE_URL>',
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, so fall back to the Archival Store.
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`);
}