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.
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.
- JSON-RPC (deprecated)
- gRPC (TypeScript SDK)
- gRPC (grpcurl)
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 }
]
}'
const { object } = await client.core.getObject({
objectId: '0xOBJECT_ID',
include: { content: true },
});
console.log('Owner:', object.owner);
console.log('Type:', object.type);
console.log('Content:', object.content);
grpcurl -d '{
"object_id": "0xOBJECT_ID",
"read_mask": { "paths": ["content", "owner"] }
}' <full node URL:port> sui.rpc.v2.LedgerService/GetObject
Key differences:
- JSON-RPC uses
showContent,showOwnerbooleans. gRPC uses aread_maskwith field paths, or the SDK'sincludeparameter. - The SDK's
getObjectreturns{ object }directly. UsegetObjects(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.
- JSON-RPC (deprecated)
- gRPC (TypeScript SDK)
- gRPC (grpcurl)
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 }
]
}'
const { objects } = await client.core.getObjects({
objectIds: ['0xOBJECT_A', '0xOBJECT_B', '0xOBJECT_C'],
include: { content: true },
});
for (const obj of objects) {
if (obj instanceof Error) {
console.error('Failed to fetch:', obj.message);
} else {
console.log(obj.objectId, obj.content);
}
}
grpcurl -d '{
"requests": [
{ "object_id": "0xOBJECT_A" },
{ "object_id": "0xOBJECT_B" },
{ "object_id": "0xOBJECT_C" }
],
"read_mask": { "paths": ["content"] }
}' <full node URL:port> sui.rpc.v2.LedgerService/BatchGetObjects
Key differences:
- Only the top-level
read_maskis 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.
- JSON-RPC (deprecated)
- gRPC (TypeScript SDK)
- gRPC (grpcurl)
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 }
]
}'
const result = await client.core.getTransaction({
digest: 'J4NvV5iQZQFm1xKPYv9ffDCCPW6cZ4yFKsCqFUiDX5L4',
include: { effects: true, events: true },
});
if (result.$kind === 'Transaction') {
console.log('Status:', result.Transaction.effects?.status);
console.log('Events:', result.Transaction.events);
}
grpcurl -d '{
"digest": "J4NvV5iQZQFm1xKPYv9ffDCCPW6cZ4yFKsCqFUiDX5L4",
"read_mask": { "paths": ["effects", "events"] }
}' <full node URL:port> sui.rpc.v2.LedgerService/GetTransaction
Key differences:
- JSON-RPC uses
showEffects,showEventsbooleans. gRPC usesread_maskpaths or the SDK'sinclude. - Digests are
Base58-encoded strings in both APIs.
Batch-fetch transactions
Replace sui_multiGetTransactionBlocks with LedgerService.BatchGetTransactions.
- JSON-RPC (deprecated)
- gRPC (TypeScript SDK)
- gRPC (grpcurl)
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 }
]
}'
// Use the proto client directly for batch transaction lookups
const { response } = await client.ledgerService.batchGetTransactions({
digests: ['DIGEST_A', 'DIGEST_B'],
readMask: { paths: ['effects', 'events'] },
});
for (const tx of response.transactions) {
if (tx.result.oneofKind === 'transaction') {
const executed = tx.result.transaction;
console.log(executed.digest, executed.effects);
}
}
grpcurl -d '{
"digests": ["DIGEST_A", "DIGEST_B"],
"read_mask": { "paths": ["effects", "events"] }
}' <full node URL:port> sui.rpc.v2.LedgerService/BatchGetTransactions
Key differences:
- Same
read_maskrules 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.
- JSON-RPC (deprecated)
- gRPC (TypeScript SDK)
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
]
}'
const result = await client.core.getTransaction({
digest: '8NB8sXb4m9PJhCyLB7eVH4onqQWoFFzVUrqPoYUhcQe2',
include: { events: true },
});
if (result.$kind === 'Transaction') {
for (const event of result.Transaction.events ?? []) {
console.log('Type:', event.eventType);
console.log('JSON:', event.json);
}
}
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.
- JSON-RPC WebSocket (deprecated)
- gRPC (TypeScript SDK)
- gRPC (Buf CLI)
// 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));
import { SuiGrpcClient } from '@mysten/sui/grpc';
const client = new SuiGrpcClient({
baseUrl: 'https://fullnode.mainnet.sui.io:443',
network: 'mainnet',
});
const TARGET_EVENT_TYPE = '0xPACKAGE::module::MyEvent';
// Stream checkpoints and filter events client-side
const { responses } = client.subscriptionService.subscribeCheckpoints({
readMask: {
paths: ['transactions'],
},
});
for await (const response of responses) {
for (const tx of response.checkpoint?.transactions ?? []) {
for (const event of tx.events?.events ?? []) {
if (event.eventType === TARGET_EVENT_TYPE) {
console.log('Matched event:', event);
}
}
}
}
buf curl --protocol grpc \
https://<full node URL>/sui.rpc.v2.SubscriptionService/SubscribeCheckpoints \
-d '{ "readMask": "transactions" }' \
--timeout 5m
Filter the output with jq for a specific event type:
buf curl --protocol grpc \
https://<full node URL>/sui.rpc.v2.SubscriptionService/SubscribeCheckpoints \
-d '{ "readMask": "transactions" }' \
--timeout 5m \
| jq '.checkpoint.transactions[]?.events.events[]? | select(.eventType == "0xPACKAGE::module::MyEvent")'
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.GetCheckpointbefore resubscribing. See Subscriptions for streaming data. - For filtered historical event queries over a range, use GraphQL
Query.eventsinstead.
Stream checkpoints
Replace sui_getCheckpoints polling with SubscriptionService.SubscribeCheckpoints for a real-time, ordered stream.
- JSON-RPC polling (deprecated)
- gRPC (TypeScript SDK)
- gRPC (Buf CLI)
// 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));
}
const { responses } = client.subscriptionService.subscribeCheckpoints({
readMask: {
paths: ['sequenceNumber', 'digest', 'summary'],
},
});
for await (const response of responses) {
const cp = response.checkpoint;
console.log('Checkpoint:', cp?.sequenceNumber);
console.log('Timestamp:', cp?.summary?.timestamp);
console.log('Tx count:', cp?.summary?.totalNetworkTransactions);
}
buf curl --protocol grpc \
https://<full node URL>/sui.rpc.v2.SubscriptionService/SubscribeCheckpoints \
-d '{ "readMask": "sequenceNumber,digest,summary.timestamp" }' \
--timeout 5m
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
sequenceNumberto resume after a disconnect. grpcurldoes 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.
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.
- JSON-RPC (deprecated)
- gRPC (TypeScript SDK)
- gRPC (grpcurl)
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>"]
}'
// Use the proto client directly — no Core API wrapper for checkpoint lookups
const { response } = await client.ledgerService.getCheckpoint({
checkpointId: {
oneofKind: 'sequenceNumber',
sequenceNumber: BigInt('<CHECKPOINT_SEQUENCE_NUMBER>'),
},
readMask: { paths: ['digest', 'summary'] },
});
console.log('Digest:', response.checkpoint?.digest);
console.log('Timestamp:', response.checkpoint?.summary?.timestamp);
console.log('Total transactions:', response.checkpoint?.summary?.totalNetworkTransactions);
grpcurl -d '{
"sequence_number": "<CHECKPOINT_SEQUENCE_NUMBER>",
"read_mask": { "paths": ["digest", "summary"] }
}' <full node URL:port> sui.rpc.v2.LedgerService/GetCheckpoint
Get balances
Replace suix_getBalance and suix_getAllBalances with StateService.GetBalance and StateService.ListBalances.
- JSON-RPC (deprecated)
- gRPC (TypeScript SDK)
- gRPC (grpcurl)
# 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"]
}'
// Single coin type
const { balance } = await client.core.getBalance({
owner: '0xADDRESS',
coinType: '0x2::sui::SUI',
});
console.log('Total balance:', balance.balance);
console.log('Coin balance:', balance.coinBalance);
console.log('Address balance:', balance.addressBalance);
// All coin types
const { balances } = await client.core.listBalances({
owner: '0xADDRESS',
});
for (const b of balances) {
console.log(b.coinType, b.balance);
}
# Single coin type
grpcurl -d '{
"owner": "0xADDRESS",
"coin_type": "0x2::sui::SUI"
}' <full node URL:port> sui.rpc.v2.StateService/GetBalance
# All coin types
grpcurl -d '{
"owner": "0xADDRESS"
}' <full node URL:port> sui.rpc.v2.StateService/ListBalances
Key differences:
- gRPC separates
coinBalance(coin objects) andaddressBalance(accumulator). Thebalancefield 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.
- JSON-RPC (deprecated)
- gRPC (TypeScript SDK)
- gRPC (grpcurl)
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]
}'
// List SUI coin objects
const page = await client.core.listOwnedObjects({
owner: '0xADDRESS',
type: '0x2::coin::Coin<0x2::sui::SUI>',
limit: 50,
});
for (const obj of page.objects) {
console.log('Coin:', obj.objectId);
}
// Paginate with the cursor from the previous response
if (page.hasNextPage) {
const nextPage = await client.core.listOwnedObjects({
owner: '0xADDRESS',
type: '0x2::coin::Coin<0x2::sui::SUI>',
limit: 50,
cursor: page.cursor,
});
}
grpcurl -d '{
"owner": "0xADDRESS",
"object_type": "0x2::coin::Coin<0x2::sui::SUI>"
}' <full node URL:port> sui.rpc.v2.StateService/ListOwnedObjects
To list coins of every type, filter by 0x2::coin::Coin without a type parameter.
Key differences:
- There is no dedicated coin-listing RPC. Use
ListOwnedObjectswith aCoin<T>type filter. - For transaction building, prefer gas smashing or address-balance gas payments over manual coin selection.
List dynamic fields
Replace suix_getDynamicFields with StateService.ListDynamicFields.
- JSON-RPC (deprecated)
- gRPC (TypeScript SDK)
- gRPC (grpcurl)
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]
}'
const page = await client.core.listDynamicFields({
parentId: '0xPARENT_OBJECT_ID',
limit: 50,
});
for (const field of page.dynamicFields) {
console.log('Name:', field.name);
console.log('Field ID:', field.fieldId);
}
grpcurl -d '{
"parent": "0xPARENT_OBJECT_ID"
}' <full node URL:port> sui.rpc.v2.StateService/ListDynamicFields
Key differences:
- For
suix_getDynamicFieldObject, derive the dynamic field's object ID locally from the parent ID and field name, then callLedgerService.GetObject. You do not needListDynamicFieldswhen you already know the field name.
Execute a transaction
Replace sui_executeTransactionBlock with TransactionExecutionService.ExecuteTransaction.
- JSON-RPC (deprecated)
- gRPC (TypeScript SDK)
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 }
]
}'
import { Transaction } from '@mysten/sui/transactions';
// Build and sign a transaction (see PTB docs for details)
const tx = new Transaction();
// ... add commands ...
const bytes = await tx.build({ client });
const { signature } = await keypair.signTransaction(bytes);
// Execute
const result = await client.core.executeTransaction({
transaction: bytes,
signatures: [signature],
include: { effects: true },
});
if (result.$kind === 'Transaction') {
console.log('Digest:', result.Transaction.digest);
console.log('Status:', result.Transaction.effects?.status);
}
Key differences:
- The JSON-RPC
unsafe_*builder methods (unsafe_moveCall,unsafe_transferObject, and others) 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.
- JSON-RPC (deprecated)
- gRPC (TypeScript SDK)
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>"]
}'
const result = await client.core.simulateTransaction({
transaction: txBytes,
include: { effects: true, events: true },
});
if (result.$kind === 'Transaction') {
console.log('Simulated status:', result.Transaction.effects?.status);
console.log('Simulated events:', result.Transaction.events);
}
Get reference gas price
Replace suix_getReferenceGasPrice with getReferenceGasPrice on the SDK, or LedgerService.GetEpoch via grpcurl and read the reference_gas_price field.
- JSON-RPC (deprecated)
- gRPC (TypeScript SDK)
- gRPC (grpcurl)
curl -X POST https://fullnode.mainnet.sui.io:443 \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "suix_getReferenceGasPrice",
"params": []
}'
const { referenceGasPrice } = await client.core.getReferenceGasPrice();
console.log('Reference gas price:', referenceGasPrice);
grpcurl -d '{ "read_mask": { "paths": ["reference_gas_price"] } }' \
<full node URL:port> sui.rpc.v2.LedgerService/GetEpoch
Resolve a SuiNS name
Replace JSON-RPC name resolution with NameService.LookupName and NameService.ReverseLookupName.
- JSON-RPC (deprecated)
- gRPC (grpcurl)
# 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"]
}'
# Name to address
grpcurl -d '{ "name": "example.sui" }' \
<full node URL:port> sui.rpc.v2.NameService/LookupName
# Address to name (reverse lookup)
grpcurl -d '{ "address": "0xADDRESS" }' \
<full node URL:port> sui.rpc.v2.NameService/ReverseLookupName
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`);
}
Related resources
- JSON-RPC Migration Guide: full method mapping and decision criteria
- What is gRPC?: concepts, services, and best practices
- Querying Data with gRPC: client setup in TypeScript, Go, and Python
- Emitting Events: event struct definitions, querying, and filtering
- Archival Store: high-retention lookups for pruned data
- Building PTBs: transaction construction for the
ExecuteTransactionpath - Sui Full Node gRPC API reference: complete protobuf service and message definitions