Migrating from JSON-RPC
Migrate deprecated JSON-RPC code to shared gRPC and GraphQL top-level methods
JSON-RPC APIs are deprecated in the Sui TypeScript SDK. For most application code, migrate from
SuiJsonRpcClient to SuiGrpcClient and call the gRPC client's top-level
methods. Use SuiGraphQLClient when the code needs custom indexed queries,
historical object versions, or custom GraphQL selection sets. Standard transaction and event queries
are available directly on both clients.
Create one client for the transport you want to use. Application code should usually call
top-level methods like client.getObject() and client.listTransactions(). SDKs and libraries
should accept ClientWithCoreApi and call client.core.<method>().
Choosing a target client
| Client | Use For |
|---|---|
SuiGrpcClient | Standard reads, writes, simulations, transaction/event queries, and streams |
SuiGraphQLClient | The same shared methods plus custom indexed queries and historical object versions |
SuiJsonRpcClient | Maintaining legacy JSON-RPC code while migrating |
Quick migration to gRPC
Replace SuiJsonRpcClient with SuiGrpcClient:
- import { SuiJsonRpcClient, getJsonRpcFullnodeUrl } from '@mysten/sui/jsonRpc';
+ import { SuiGrpcClient } from '@mysten/sui/grpc';
- const client = new SuiJsonRpcClient({
- url: getJsonRpcFullnodeUrl('mainnet'),
- network: 'mainnet',
- });
+ const client = new SuiGrpcClient({
+ baseUrl: 'https://fullnode.mainnet.sui.io:443',
+ network: 'mainnet',
+ });Full node hosts commonly expose both JSON-RPC and gRPC. When migrating, pass the full node endpoint
as baseUrl instead of url, and verify the protocol and port used by your node provider.
Use top-level methods in apps
The gRPC and GraphQL clients expose top-level methods for the common client API. These methods use
the same options and response shapes as client.core, with transport-specific additions where the
transport can expose more data.
This includes transaction and event methods. Use getTransaction, waitForTransaction,
listTransactions, and listEvents directly instead of dropping down to a raw gRPC service or a
custom GraphQL query for standard queries.
const { object } = await client.getObject({
objectId: '0x...',
include: { content: true, display: true },
});
const { balance } = await client.getBalance({
owner: '0x...',
});
const result = await client.signAndExecuteTransaction({
transaction,
signer,
include: { effects: true, balanceChanges: true },
});For SDKs and shared libraries, accept ClientWithCoreApi and use client.core so the caller can
provide SuiGrpcClient, SuiGraphQLClient, or a legacy SuiJsonRpcClient during migration:
import type { ClientWithCoreApi } from '@mysten/sui/client';
export async function readObject(client: ClientWithCoreApi, objectId: string) {
return client.core.getObject({
objectId,
include: { content: true },
});
}Method replacements
Replace legacy JSON-RPC method names with the gRPC top-level method when one exists. In SDK code,
use the same replacement under client.core.
| JSON-RPC Method | App Code Replacement |
|---|---|
getObject | getObject |
multiGetObjects | getObjects |
getOwnedObjects | listOwnedObjects for an exact StructType filter |
getCoins | listCoins |
getAllBalances | Paginate listBalances and map its normalized response |
getBalance | getBalance |
getCoinMetadata | getCoinMetadata |
getDynamicFields | listDynamicFields |
getDynamicFieldObject | getDynamicField or client.core.getDynamicObjectField |
getTransactionBlock | getTransaction |
multiGetTransactionBlocks | Multiple getTransaction calls |
executeTransactionBlock | executeTransaction |
waitForTransaction | waitForTransaction |
dryRunTransactionBlock | simulateTransaction |
devInspectTransactionBlock | simulateTransaction with the sender set and checksEnabled: false |
queryTransactionBlocks | listTransactions |
queryEvents | listEvents |
getNormalizedMoveFunction | getMoveFunction |
getMoveFunctionArgTypes | No direct equivalent; getMoveFunction returns normalized signatures |
resolveNameServiceAddress | nameService.lookupName or GraphQL query |
resolveNameServiceNames | No direct equivalent for listing every name assigned to an address |
getMoveFunction exposes normalized parameter signatures, but it does not reproduce the legacy
Pure, Object, and object-access classifications from getMoveFunctionArgTypes. Likewise,
defaultNameServiceName and raw gRPC nameService.reverseLookupName return only the configured
default name; they do not replace the paginated list returned by resolveNameServiceNames. Keep a
legacy endpoint or use an application indexer when those exact results are required.
Some composed helpers are currently exposed through client.core rather than as top-level gRPC or
GraphQL methods:
const { protocolConfig } = await client.core.getProtocolConfig();
const { systemState } = await client.core.getCurrentSystemState();
const { chainIdentifier } = await client.core.getChainIdentifier();Object and coin reads
Migrating getOwnedObjects
- const { data } = await jsonRpcClient.getOwnedObjects({
- owner: '0xabc...',
- filter: { StructType: '0x2::coin::Coin<0x2::sui::SUI>' },
- options: { showContent: true },
- });
+ const { objects } = await client.listOwnedObjects({
+ owner: '0xabc...',
+ type: '0x2::coin::Coin<0x2::sui::SUI>',
+ include: { content: true },
+ });Only the legacy StructType filter maps directly to listOwnedObjects.type. Legacy package,
module, owner, object ID, version, and boolean-composition filters do not have top-level Core
equivalents. A custom GraphQL ObjectFilter can cover package, module, and owner-kind cases. For
the remaining filters, use an indexer or paginate and filter the normalized results in application
code.
Migrating getCoins
- const coins = await jsonRpcClient.getCoins({
- owner: '0xabc...',
- coinType: '0x2::sui::SUI',
- });
+ const coins = await client.listCoins({
+ owner: '0xabc...',
+ coinType: '0x2::sui::SUI',
+ });Migrating getAllBalances
Unlike getAllBalances, listBalances is paginated. It also returns normalized balance,
coinBalance, and addressBalance fields instead of the legacy CoinBalance shape with
coinObjectCount, totalBalance, and lockedBalance. Follow cursor while hasNextPage is true
and map the result explicitly if existing code depends on the legacy shape.
Migrating object include options
- const object = await jsonRpcClient.getObject({
- id: objectId,
- options: {
- showBcs: true,
- showContent: true,
- showDisplay: true,
- },
- });
+ const { object } = await client.getObject({
+ objectId,
+ include: {
+ content: true,
+ json: true,
+ display: true,
+ },
+ });Use include.content for BCS-encoded Move struct bytes. It is the most stable cross-transport shape
for parsing application data.
Transaction execution and simulation
Legacy JSON-RPC methods accept serialized transaction blocks as base64 strings. executeTransaction
accepts bytes, while simulateTransaction accepts bytes or a Transaction, so decode existing
strings before passing them to the new client:
import { Transaction } from '@mysten/sui/transactions';
import { fromBase64 } from '@mysten/sui/utils';Migrating getTransactionBlock
- const result = await jsonRpcClient.getTransactionBlock({
- digest,
- options: {
- showEffects: true,
- showEvents: true,
- showInput: true,
- },
- });
+ const result = await client.getTransaction({
+ digest,
+ include: {
+ effects: true,
+ events: true,
+ transaction: true,
+ },
+ });Both SuiGrpcClient and SuiGraphQLClient expose getTransaction. SDKs can make the same request
through client.core.getTransaction.
For multiple digests, call the same top-level method for each transaction:
- const results = await jsonRpcClient.multiGetTransactionBlocks({
- digests,
- options: { showEffects: true },
- });
+ const results = await Promise.all(
+ digests.map((digest) =>
+ client.getTransaction({
+ digest,
+ include: { effects: true },
+ }),
+ ),
+ );The raw gRPC ledgerService.batchGetTransactions method remains available when an application
specifically needs the transport's batch wire API, but it is not required for ordinary client code.
Migrating executeTransactionBlock
- const result = await jsonRpcClient.executeTransactionBlock({
- transactionBlock: bytes,
- signature,
- options: {
- showEffects: true,
- showEvents: true,
- },
- });
+ const result = await client.executeTransaction({
+ transaction: fromBase64(bytes),
+ signatures: [signature],
+ include: {
+ effects: true,
+ events: true,
+ },
+ });
- const status = result.effects?.status.status;
+ const tx = result.Transaction ?? result.FailedTransaction;
+ const success = tx.status.success;Migrating signAndExecuteTransaction
- const result = await jsonRpcClient.signAndExecuteTransaction({
- transaction,
- signer,
- options: { showEffects: true },
- });
+ const result = await client.signAndExecuteTransaction({
+ transaction,
+ signer,
+ include: { effects: true },
+ });
+ if (result.$kind === 'FailedTransaction') {
+ throw new Error(result.FailedTransaction.status.error?.message ?? 'Transaction failed');
+ }Migrating waitForTransaction
- const result = await jsonRpcClient.waitForTransaction({
- digest,
- options: { showEffects: true },
- timeout: 60_000,
- pollInterval: 2_000,
- });
+ const result = await client.waitForTransaction({
+ digest,
+ include: { effects: true },
+ timeout: 60_000,
+ pollSchedule: [0, 2_000],
+ });waitForTransaction is a top-level method on both SuiGrpcClient and SuiGraphQLClient. It can
also accept the result of executeTransaction or signAndExecuteTransaction through its result
option.
Migrating dryRunTransactionBlock
- const result = await jsonRpcClient.dryRunTransactionBlock({
- transactionBlock: tx,
- });
+ const result = await client.simulateTransaction({
+ transaction: fromBase64(tx),
+ include: {
+ effects: true,
+ balanceChanges: true,
+ },
+ });Migrating devInspectTransactionBlock
- const result = await jsonRpcClient.devInspectTransactionBlock({
- sender: '0xabc...',
- transactionBlock: tx,
- });
- const returnValues = result.results?.[0]?.returnValues;
+ const transaction = Transaction.fromKind(tx);
+ transaction.setSender('0xabc...');
+ const result = await client.simulateTransaction({
+ transaction,
+ checksEnabled: false,
+ include: { commandResults: true },
+ });
+ const returnValues = result.commandResults?.[0]?.returnValues;Transaction and event queries
listTransactions and listEvents are first-class methods on SuiGrpcClient, SuiGraphQLClient,
and the Core API. Application code should call the top-level method on its chosen client. Reusable
SDK code should call the same method through client.core.
Migrating queryTransactionBlocks
- const result = await jsonRpcClient.queryTransactionBlocks({
- filter: { FromAddress: '0xabc...' },
- options: { showEffects: true },
- limit: 10,
- });
- const digests = result.data.map((tx) => tx.digest);
+ const page = await client.listTransactions({
+ filter: { sender: '0xabc...' },
+ include: { effects: true },
+ limit: 10,
+ order: 'descending',
+ });
+ const digests = page.transactions.map(
+ (tx) => (tx.Transaction ?? tx.FailedTransaction).digest,
+ );Common transaction filter mappings:
| JSON-RPC Filter | gRPC/GraphQL Top-Level Filter |
|---|---|
FromAddress | sender |
MoveFunction | function |
ToAddress, FromOrToAddress, ChangedObject, AffectedObject | Use raw gRPC ledger filters or a custom GraphQL query |
The response contains normalized transaction results plus ledger-position cursors:
for (const result of page.transactions) {
const transaction = result.Transaction ?? result.FailedTransaction;
console.log(transaction.digest, result.$kind);
}
const nextPage = page.hasNextPage
? await client.listTransactions({
filter: { sender: '0xabc...' },
include: { effects: true },
before: page.endCursor,
limit: 10,
})
: null;The legacy JSON-RPC transaction and event queries default to descending order, while
listTransactions and listEvents default to ascending order. Pass order: 'descending' when
preserving the legacy default.
Migrating queryEvents
- const result = await jsonRpcClient.queryEvents({
- query: { MoveEventType: '0x2::coin::CoinCreated' },
- limit: 10,
- order: 'descending',
- });
+ const result = await client.listEvents({
+ filter: { eventType: '0x2::coin::CoinCreated' },
+ limit: 10,
+ order: 'descending',
+ });
+ for (const event of result.events) {
+ console.log(event.eventType, event.transactionDigest, event.json);
+ }Common event filter mappings:
| JSON-RPC Filter | gRPC/GraphQL Top-Level Filter |
|---|---|
Sender | sender |
MoveModule | emitModule: 'package::module' |
MoveEventModule | eventType: 'package::module' |
MoveEventType | eventType: 'package::module::Event' |
The top-level query methods handle pagination and cursor normalization. For richer filters, such as
combined predicates, affected addresses, affected objects, or checkpoint ranges, use the raw
ledgerService on SuiGrpcClient or a custom GraphQL query.
Use after: result.endCursor to continue an ascending query and before: result.endCursor to
continue a descending query. startCursor identifies the first item in a page and can be used with
after to poll for newer transactions or events.
Native gRPC replacements
Some JSON-RPC methods map to gRPC service clients rather than top-level methods:
| JSON-RPC Method | gRPC Replacement |
|---|---|
getCheckpoint | ledgerService.getCheckpoint |
getCheckpoints | ledgerService.listCheckpoints |
getLatestCheckpointSequenceNumber | ledgerService.getServiceInfo and read checkpointHeight |
getCurrentEpoch | ledgerService.getEpoch with no epoch argument |
getCommitteeInfo | ledgerService.getEpoch with committee in the read mask |
getLatestSuiSystemState | client.core.getCurrentSystemState or ledgerService.getEpoch |
getProtocolConfig | client.core.getProtocolConfig or ledgerService.getEpoch |
getTotalSupply | stateService.getCoinInfo and read treasury.totalSupply |
getNormalizedMoveModule | movePackageService.getPackage |
getNormalizedMoveModulesByPackage | movePackageService.getPackage |
getNormalizedMoveStruct | movePackageService.getDatatype |
import { SuiGrpcClient } from '@mysten/sui/grpc';
const client = new SuiGrpcClient({
baseUrl: 'https://fullnode.mainnet.sui.io:443',
network: 'mainnet',
});
const { response: info } = await client.ledgerService.getServiceInfo({});
const latestCheckpoint = info.checkpointHeight;
if (latestCheckpoint == null) {
throw new Error('The server did not return a checkpoint height');
}
const { response } = await client.ledgerService.getCheckpoint({
checkpointId: { oneofKind: 'sequenceNumber', sequenceNumber: latestCheckpoint },
readMask: { paths: ['sequence_number', 'digest', 'summary.timestamp'] },
});
console.log(response.checkpoint?.sequenceNumber, response.checkpoint?.summary?.timestamp);Subscriptions
Replace deprecated JSON-RPC websocket subscriptions with the gRPC subscriptionService:
| JSON-RPC Method | gRPC Service Replacement |
|---|---|
subscribeTransaction | subscriptionService.subscribeTransactions |
subscribeEvent | subscriptionService.subscribeEvents |
const stream = client.subscriptionService.subscribeEvents({
filter: {
terms: [
{
literals: [
{
negated: false,
predicate: {
oneofKind: 'eventType',
eventType: { eventType: '0x2::coin::CoinCreated' },
},
},
],
},
],
},
readMask: { paths: ['event_type', 'contents', 'json', 'checkpoint', 'transaction_digest'] },
});
for await (const frame of stream.responses) {
if (frame.event) {
console.log(frame.event.eventType, frame.event.json);
}
}Subscriptions begin at the current tip of the chain and do not resume automatically. For gap
recovery, retain the last frame.watermark.cursor, open the new subscription to establish its
first-frame position, and replay with the paired raw client.ledgerService.listEvents() call using
the same protobuf filter and options.after cursor. Repeat the raw list call as the index advances
until it reaches the new subscription's start position. Do not pass the subscription filter or
cursor to top-level client.listEvents(): its Core filter and base64 cursor are different types.
When to use GraphQL
Use SuiGraphQLClient when the replacement needs a custom indexed query, a historical object
version, or a custom selection set. Standard transaction and event history does not require a custom
GraphQL query; call graphqlClient.listTransactions() or graphqlClient.listEvents() directly.
| JSON-RPC Method | Alternative |
|---|---|
getEpochs | GraphQL epochs query |
tryGetPastObject | GraphQL object(address:, version:) query |
getStakes | No current gRPC/Core/GraphQL equivalent; use a staking indexer |
getStakesByIds | No current gRPC/Core/GraphQL equivalent; use a staking indexer |
getNetworkMetrics | Use an indexer or analytics-specific GraphQL schema |
getAddressMetrics | Use an indexer or analytics-specific GraphQL schema |
getMoveCallMetrics | Use an indexer or analytics-specific GraphQL schema |
import { SuiGraphQLClient } from '@mysten/sui/graphql';
import { graphql } from '@mysten/sui/graphql/schema';
const graphqlClient = new SuiGraphQLClient({
url: 'https://sui-mainnet.mystenlabs.com/graphql',
network: 'mainnet',
});
const historicalObjectQuery = graphql(`
query GetObjectAtVersion($id: SuiAddress!, $version: UInt53!) {
object(address: $id, version: $version) {
address
version
digest
asMoveObject {
contents {
type {
repr
}
bcs
}
}
}
}
`);
const result = await graphqlClient.query({
query: historicalObjectQuery,
variables: {
id: '0x123...',
version: 42,
},
});Validator APY
There is no direct SDK replacement for getValidatorsApy. There is no canonical definition of
validator APY, so compute the metric from validator staking-pool exchange rates or use an
application-specific indexer.
Two reference implementations:
- The
jsonrpc-altimplementation insui-indexer-alt-jsonrpc - A GraphQL approach
Treat whichever formula you adopt as a definition of validator APY, not the definition.
Response format differences
gRPC and GraphQL top-level methods return the Core API response format, which differs from legacy JSON-RPC response shapes.
// Transaction result access
- const status = result.effects?.status?.status;
+ const tx = result.Transaction ?? result.FailedTransaction;
+ const success = tx.status.success;
// Include options
- { showEffects: true, showEvents: true }
+ { effects: true, events: true }See the
@mysten/sui migration guide
for transaction executor response changes.
Client extensions
Client extensions work with SuiGrpcClient and any client that implements ClientWithCoreApi:
import { deepbook } from '@mysten/deepbook-v3';
import { suins } from '@mysten/suins';
import { SuiGrpcClient } from '@mysten/sui/grpc';
const client = new SuiGrpcClient({
baseUrl: 'https://fullnode.mainnet.sui.io:443',
network: 'mainnet',
}).$extend(deepbook({ address: myAddress }), suins());
await client.deepbook.checkManagerBalance(manager, asset);
await client.suins.getNameRecord('example.sui');