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.
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.
- Prerequisites
- Install the
@mysten/suiTypeScript SDK by runningnpm install @mysten/sui. - Obtain access to a gRPC-enabled full node and note its endpoint.
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.
- JSON-RPC (deprecated)
- gRPC (TypeScript SDK)
- gRPC (grpcurl)
$ 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 }
]
}'
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> sui.rpc.v2.LedgerService/GetObject
The gRPC call differs from the JSON-RPC call in the following ways:
- JSON-RPC uses the
showContentandshowOwnerbooleans. 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 <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 }
]
}'
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> sui.rpc.v2.LedgerService/BatchGetObjects
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.
- JSON-RPC (deprecated)
- gRPC (TypeScript SDK)
- gRPC (grpcurl)
$ 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 }
]
}'
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> sui.rpc.v2.LedgerService/GetTransaction
The gRPC call differs from the JSON-RPC call in the following ways:
- JSON-RPC uses the
showEffectsandshowEventsbooleans. gRPC usesread_maskpaths or the SDK'sincludeparameter. - 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 <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 }
]
}'
// 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> sui.rpc.v2.LedgerService/BatchGetTransactions
The same read_mask rules apply as for batch objects: the service respects only the top-level mask.
List and paginate transactions
Replace suix_queryTransactionBlocks with LedgerService.ListTransactions for filtered, paginated queries over a checkpoint range. Unlike StateService list APIs (which return a single page), ListTransactions returns a server-side stream. Each response frame carries a watermark with a resume cursor, and the final frame includes a QueryEnd with the reason the stream ended.
The @mysten/sui TypeScript SDK (v2.23.1+) exposes client.listTransactions() as a high-level wrapper. You can also use the proto client directly through client.ledgerService.listTransactions(). The grpcurl example below shows the raw protobuf JSON request shape.
- gRPC (grpcurl)
$ grpcurl -d '{
"start_checkpoint": "1000000",
"filter": {
"terms": [{
"literals": [{
"sender": { "address": "<ADDRESS>" }
}]
}]
},
"options": { "limit": 50 },
"read_mask": { "paths": ["digest", "effects"] }
}' <FULL_NODE_URL> sui.rpc.v2.LedgerService/ListTransactions
The List APIs differ from JSON-RPC pagination in the following ways:
ListTransactionsis a server-side stream. Each response frame contains atransaction(the matched item), awatermark(progress marker), and optionally aQueryEnd(on the final frame).- The
watermark.cursoris an opaque binary token. Pass it asoptions.afterto resume ascending pagination, oroptions.beforefor descending. - The
watermark.checkpointfield tells you how much of the checkpoint range the server scanned, which is useful for sparse filters that produce few results across many checkpoints. - The
QueryEnd.reasontells you why the stream ended:ITEM_LIMIT: returned the requested number of items. Use the cursor to fetch the next page.SCAN_LIMIT: the server's internal scan budget was exhausted before finding enough matches. Resume from the cursor to continue scanning.LEDGER_TIP: reached the current tip of the chain (noend_checkpointwas specified).CHECKPOINT_BOUND: reached the specifiedend_checkpoint.CURSOR_BOUND: reached a cursor-derived boundary (for example, theoptions.beforecursor used during backfill).
ListCheckpoints and ListEvents follow the same streaming pattern with the same watermark and QueryEnd semantics. ListCheckpoints accepts a TransactionFilter (filtering checkpoints that contain matching transactions), while ListEvents accepts an EventFilter.
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.
- JSON-RPC (deprecated)
- gRPC (TypeScript SDK)
$ 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
]
}'
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
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.
- JSON-RPC WebSocket (deprecated)
- gRPC (proto client)
- gRPC (Buf CLI)
// 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 @mysten/sui TypeScript SDK (v2.23.1+) exposes SubscribeEvents through the proto client at client.subscriptionService.subscribeEvents(). The Buf CLI example below shows the raw protobuf JSON request shape.
// Use the generated proto client directly
const stream = client.subscriptionService.subscribeEvents({
filter: {
terms: [{
literals: [{
negated: false,
predicate: {
oneofKind: 'eventType',
eventType: { eventType: '<PACKAGE>::module::MyEvent' },
},
}],
}],
},
});
for await (const response of stream.responses) {
console.log('Matched event:', response.event);
}
$ buf curl --protocol grpc \
https://<FULL_NODE_URL>/sui.rpc.v2.SubscriptionService/SubscribeEvents \
-d '{
"filter": {
"terms": [{
"literals": [{
"event_type": { "event_type": "<PACKAGE>::module::MyEvent" }
}]
}]
}
}' \
--timeout 5m
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
SubscribeEventsalso filters server-side using anEventFilter, so your client receives only matching events. - Subscriptions begin at the current tip and do not accept a resume point. Filtered subscriptions start with a progress-only frame whose cursor marks the point immediately before live delivery; an unfiltered subscription can start with a payload, so never discard the first frame. Immediately consume live frames into durable, capacity-managed storage while
LedgerService.ListEventsbackfills to that boundary. If the spool fills or the stream drops, restart from the last durably processed cursor and backfill the gap. See Subscriptions for streaming data. - For filtered historical event queries over a range, use
LedgerService.ListEventsor GraphQLQuery.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.
- gRPC (proto client)
- gRPC (grpcurl)
The @mysten/sui TypeScript SDK (v2.23.1+) exposes client.listEvents() as a high-level wrapper. You can also use the proto client directly through client.ledgerService.listEvents(). The grpcurl example in the next tab shows the raw protobuf JSON request shape.
// Use the generated proto client directly
const stream = client.ledgerService.listEvents({
startCheckpoint: BigInt(1000000),
endCheckpoint: BigInt(1000100),
filter: {
terms: [{
literals: [{
negated: false,
predicate: {
oneofKind: 'eventType',
eventType: { eventType: '<PACKAGE>::module::MyEvent' },
},
}],
}],
},
options: { limit: 50 },
});
for await (const response of stream.responses) {
console.log('Event:', response.event);
}
$ grpcurl -d '{
"start_checkpoint": "1000000",
"end_checkpoint": "1000100",
"filter": {
"terms": [{
"literals": [{
"event_type": { "event_type": "<PACKAGE>::module::MyEvent" }
}]
}]
}
}' <FULL_NODE_URL> sui.rpc.v2.LedgerService/ListEvents
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.
- 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('<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));
}
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
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.ListCheckpointsto query a historical range of checkpoints, optionally filtered by aTransactionFilter. UseSubscriptionService.SubscribeCheckpointsfor live streaming from the current tip. - Subscriptions do not support resumption. Filtered subscriptions start with a progress-only frame whose cursor marks the point immediately before live delivery; an unfiltered subscription can start with a payload, so never discard the first frame. Open the subscription first and immediately drain live frames into durable, bounded storage while
ListCheckpointsbackfills to the starting boundary. If the spool fills or the stream drops, restart from the last durably processed cursor and backfill the gap. grpcurlsupports server-side streaming for theLedgerService.List*calls on this page. ForSubscriptionServicesubscriptions, which are indefinite streams, the Buf CLI or an SDK client provides better control over timeouts and reconnection.
Stream transactions
Use SubscriptionService.SubscribeTransactions for a real-time, filtered stream of transactions. This replaces both JSON-RPC WebSocket subscriptions and polling patterns.
The @mysten/sui TypeScript SDK (v2.23.1+) exposes SubscribeTransactions through the proto client at client.subscriptionService.subscribeTransactions(). The Buf CLI example below shows the raw protobuf JSON request shape.
- gRPC (Buf CLI)
$ buf curl --protocol grpc \
https://<FULL_NODE_URL>/sui.rpc.v2.SubscriptionService/SubscribeTransactions \
-d '{
"filter": {
"terms": [{
"literals": [{
"sender": { "address": "<ADDRESS>" }
}]
}]
},
"read_mask": { "paths": ["digest", "effects"] }
}' \
--timeout 5m
Subscriptions begin at the current tip of the chain. A filtered subscription starts with a progress-only frame (no transaction payload) that establishes the stream's start position. An unfiltered subscription can start with a transaction payload, so do not discard its first frame. To avoid gaps between historical data and the live stream, see Backfill and subscribe without gaps.
Look up a single checkpoint
Replace sui_getCheckpoint with LedgerService.GetCheckpoint.
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.
- JSON-RPC (deprecated)
- gRPC (TypeScript SDK)
- gRPC (grpcurl)
$ curl -X POST <FULL_NODE_URL> \
-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 exists 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> 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)
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>"]
}'
// Single coin type
const { balance } = await client.core.getBalance({
owner: '<ADDRESS>',
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: '<ADDRESS>',
});
for (const b of balances) {
console.log(b.coinType, b.balance);
}
To query a single coin type, run the following command:
$ grpcurl -d '{
"owner": "<ADDRESS>",
"coin_type": "0x2::sui::SUI"
}' <FULL_NODE_URL> sui.rpc.v2.StateService/GetBalance
To query all coin types, run the following command:
$ grpcurl -d '{
"owner": "<ADDRESS>"
}' <FULL_NODE_URL> sui.rpc.v2.StateService/ListBalances
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.
- JSON-RPC (deprecated)
- gRPC (TypeScript SDK)
- gRPC (grpcurl)
$ 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]
}'
// List SUI coin objects
const page = await client.core.listOwnedObjects({
owner: '<ADDRESS>',
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: '<ADDRESS>',
type: '0x2::coin::Coin<0x2::sui::SUI>',
limit: 50,
cursor: page.cursor,
});
}
$ grpcurl -d '{
"owner": "<ADDRESS>",
"object_type": "0x2::coin::Coin<0x2::sui::SUI>"
}' <FULL_NODE_URL> sui.rpc.v2.StateService/ListOwnedObjects
The gRPC approach differs from the JSON-RPC approach in the following ways:
- There is no dedicated coin-listing RPC. Use
ListOwnedObjectswith aCoin<T>type filter. - To list coins of every type, filter by
0x2::coin::Coinwithout 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.
- JSON-RPC (deprecated)
- gRPC (TypeScript SDK)
- gRPC (grpcurl)
$ 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]
}'
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> sui.rpc.v2.StateService/ListDynamicFields
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.
- JSON-RPC (deprecated)
- gRPC (TypeScript SDK)
$ 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 }
]
}'
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);
}
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.
Sign and execute a transaction (end-to-end)
Replace the JSON-RPC signAndExecuteTransactionBlock pattern with signAndExecuteTransaction on the gRPC client. This method builds the transaction (including gas resolution), signs it, and executes it in a single call.
- JSON-RPC (deprecated)
- gRPC (TypeScript SDK)
// Old JSON-RPC pattern
import { SuiClient, getFullnodeUrl } from '@mysten/sui/client';
import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519';
import { Transaction } from '@mysten/sui/transactions';
const client = new SuiClient({ url: getFullnodeUrl('mainnet') });
const keypair = new Ed25519Keypair();
const tx = new Transaction();
const [coin] = tx.splitCoins(tx.gas, [1_000_000]);
tx.transferObjects([coin], '<RECIPIENT_ADDRESS>');
const result = await client.signAndExecuteTransaction({
signer: keypair,
transaction: tx,
});
console.log('Digest:', result.digest);
import { SuiGrpcClient } from '@mysten/sui/grpc';
import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519';
import { Transaction } from '@mysten/sui/transactions';
const client = new SuiGrpcClient({
baseUrl: '<FULL_NODE_URL>',
network: 'mainnet',
});
const keypair = new Ed25519Keypair();
// Build a PTB: split 0.001 SUI and transfer to a recipient
const tx = new Transaction();
const [coin] = tx.splitCoins(tx.gas, [1_000_000]); // 0.001 SUI in MIST
tx.transferObjects([coin], '<RECIPIENT_ADDRESS>');
// signAndExecuteTransaction handles build, gas resolution, signing, and execution
const result = await client.signAndExecuteTransaction({
signer: keypair,
transaction: tx,
include: { effects: true, events: true, balanceChanges: true },
});
if (result.$kind === 'Transaction') {
console.log('Digest:', result.Transaction.digest);
console.log('Status:', result.Transaction.effects?.status);
console.log('Balance changes:', result.Transaction.balanceChanges);
}
The gRPC call differs from the JSON-RPC call in the following ways:
- Both use
signAndExecuteTransaction, but the gRPC version acceptssigner(a keypair) andtransaction(aTransactionobject or raw bytes). - The gRPC version uses
includeto select which fields the response contains, instead of request options. - The response is a discriminated union: check
result.$kindfor'Transaction'(success) or'FailedTransaction'(failure). - Gas resolution happens automatically during
build. The SDK callsSimulateTransactionwithdoGasSelection: trueto select gas coins and set the gas budget.
Manual gas resolution
For sponsored transactions or when you need to select specific gas coins, set gas parameters before building:
// Get the current reference gas price
const { referenceGasPrice } = await client.core.getReferenceGasPrice();
const tx = new Transaction();
tx.setSender(keypair.toSuiAddress());
tx.setGasPrice(BigInt(referenceGasPrice));
tx.setGasBudget(10_000_000);
// Select specific gas coins
tx.setGasPayment([{
objectId: '0xGAS_COIN_ID',
version: '1',
digest: 'COIN_DIGEST',
}]);
// Add your commands
const [coin] = tx.splitCoins(tx.gas, [1_000_000]);
tx.transferObjects([coin], '<RECIPIENT_ADDRESS>');
// Build and sign manually
const bytes = await tx.build({ client });
const { signature } = await keypair.signTransaction(bytes);
const result = await client.core.executeTransaction({
transaction: bytes,
signatures: [signature],
include: { effects: true },
});
For sponsored transactions, both the sender and sponsor must sign the same full transaction bytes (including gas fields). The sponsor sets setGasPayment and setGasOwner, then both parties sign the complete built transaction. See Sponsored Transactions for the full pattern.
Simulate a transaction (dry run)
Replace sui_dryRunTransactionBlock and sui_devInspectTransactionBlock with TransactionExecutionService.SimulateTransaction.
- JSON-RPC (deprecated)
- gRPC (TypeScript SDK)
$ curl -X POST <FULL_NODE_URL> \
-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 call LedgerService.GetEpoch through grpcurl and read the reference_gas_price field.
- JSON-RPC (deprecated)
- gRPC (TypeScript SDK)
- gRPC (grpcurl)
$ curl -X POST <FULL_NODE_URL> \
-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> 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)
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"]
}'
To resolve a name to an address, run the following command:
$ grpcurl -d '{ "name": "example.sui" }' \
<FULL_NODE_URL> sui.rpc.v2.NameService/LookupName
To resolve an address to a name, run the following command:
$ grpcurl -d '{ "address": "<ADDRESS>" }' \
<FULL_NODE_URL> sui.rpc.v2.NameService/ReverseLookupName
Get validator APY
JSON-RPC provided suix_getValidatorsApy as a convenience method. Neither gRPC nor GraphQL has a direct APY field or method. Validator APY is derived from staking pool exchange rate history stored in dynamic fields on each validator's system-state object. The legacy suix_getValidatorsApy used a compounding calculation across up to 30 epoch samples with outlier filtering, so a simple two-epoch ratio does not reproduce the same values.
To compute validator APY after migration:
- Query the validator set through
Epoch.validatorSetin GraphQL orLedgerService.GetEpochin gRPC. - Read each validator's staking pool exchange rate history from the dynamic fields on the pool's
exchange_ratestable. - Apply a compounding formula across multiple epoch samples.
See issue #23832 for the recommended computation and the legacy implementation as a reference.
Common patterns
Backfill and subscribe without gaps
To combine historical backfill with a live subscription without missing data:
- Open the subscription and immediately consume frames into durable, capacity-managed storage. Do not leave the stream unread during backfill.
- Use the first frame's cursor as the live boundary. Filtered subscriptions begin with a progress-only frame whose cursor is the checkpoint immediately before live delivery; unfiltered subscriptions can begin with a payload, which you must also spool.
- Backfill from the last durably processed cursor with the matching
List*API. Keep the initialbeforecursor fixed while advancingafterfrom each response watermark until the server returnsCURSOR_BOUND. Checkpoint bounds are exclusive at the end, so for a filtered progress cursor at checkpointC - 1, useendCheckpoint: C. - Process the durable live spool after backfill catches up, deduplicating by cursor. Atomically persist each cursor with the corresponding application updates.
- If the spool reaches its capacity or the subscription ends, restart from the last durable cursor and backfill before resuming live processing.
This pattern works with all three subscription and list API pairs: SubscribeCheckpoints with ListCheckpoints, SubscribeTransactions with ListTransactions, and SubscribeEvents with ListEvents. Reuse the same filter for the subscription and historical query.
The server's per-subscriber buffer holds 256 frames and evicts a slow consumer when it fills. A client-side in-memory queue only moves the limit; use a durable bounded spool with explicit overflow recovery for long backfills.
Reconnect a checkpoint stream
If the gRPC stream disconnects, reconnect and drain the new stream into durable storage while a separate worker backfills the gap. Do not await backfill in the subscription loop, because doing so stops the client from reading live frames.
let lastProcessed = await loadLastProcessedCheckpoint();
async function consumeSubscription() {
let attempt = 0;
for (;;) {
try {
const { responses } = client.subscriptionService.subscribeCheckpoints({
readMask: { paths: ['sequenceNumber', 'transactions'] },
});
for await (const response of responses) {
if (response.checkpoint) {
// This durable, bounded spool applies backpressure before its limit
// and signals the backfill worker when the first live frame arrives.
await liveSpool.append(response.checkpoint);
}
}
throw new Error('checkpoint subscription ended');
} catch (err) {
const delay = Math.min(30_000, 500 * 2 ** Math.min(attempt, 6));
await new Promise((resolve) =>
setTimeout(resolve, delay + Math.random() * delay * 0.4),
);
attempt++;
}
}
}
async function processWithBackfill() {
for (;;) {
const firstLive = await liveSpool.peek();
await backfillCheckpoints(lastProcessed + 1n, firstLive.sequenceNumber);
for await (const checkpoint of liveSpool.drain()) {
if (checkpoint.sequenceNumber > lastProcessed + 1n) {
// A reconnect can move the live stream past checkpoints that were not
// spooled. Backfill this gap before committing the newer checkpoint.
await backfillCheckpoints(lastProcessed + 1n, checkpoint.sequenceNumber);
}
if (checkpoint.sequenceNumber > lastProcessed) {
// Process idempotently and atomically persist this cursor.
await processAndPersistCheckpoint(checkpoint);
lastProcessed = checkpoint.sequenceNumber;
}
}
}
}
await Promise.all([consumeSubscription(), processWithBackfill()]);
Implement backfillCheckpoints with ListCheckpoints, treating its end checkpoint as exclusive and resuming partial responses from the latest watermark cursor. It must process and durably persist every returned checkpoint before resolving. If the durable spool reaches its configured capacity, stop both workers and restart from lastProcessed rather than dropping live frames. Reset the reconnect attempt only after the stream remains healthy for an application-defined interval; a single frame is not sufficient evidence of recovery.
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`);
}
Troubleshoot streaming connections
gRPC streams can terminate for several reasons. Understanding the server's connection lifecycle helps you build reliable streaming clients.
Connection age limits (GOAWAY): The Sui full node closes connections after a configurable maximum age (default: 4 hours). When a connection reaches this limit, the server sends a GOAWAY frame, stops accepting new streams, and gives in-flight requests a grace period (default: 10 minutes) to complete. Clients observe this as a stream termination. Implement automatic reconnection with backfill from your last processed checkpoint, as shown in Reconnect a checkpoint stream.
Slow consumer eviction: The server buffers up to 256 items per subscriber. If your client cannot keep up and the buffer fills, the server drops the subscription immediately without a graceful close. To avoid this:
- Process items asynchronously. Write to a queue and process from the queue in a separate worker.
- Use a
read_maskto request only the fields you need, reducing frame size. - Use server-side filters to reduce the volume of matching items.
Source lag: If the subscription service falls behind the checkpoint broadcast (for example, due to high server load), the server drops all subscribers. Reconnect and backfill from your last processed checkpoint.
Server-side timeout: Unary requests use the general gRPC timeout, which defaults to 60 seconds. Each streaming List* API has its own configurable timeout-ms; the full node configuration examples use 5 seconds. This deadline covers the entire list stream, so a long backfill can end even after returning frames. Resume from the latest watermark.cursor while preserving the original upper bound. Network, proxy, and GOAWAY interruptions require the same cursor-based recovery.
Public endpoint limitations: The public URLs at https://fullnode.<network>.sui.io are behind load balancers that impose their own connection timeouts and rate limits beyond what the Sui node configures. Long-running streams are more likely to be interrupted on public endpoints. For production streaming workloads, run your own Sui full node or use a provider with a dedicated gRPC endpoint.