Skip to content
Alchemy Logo

Sui JSON-RPC Migration Guide

Sui is retiring JSON-RPC across the network. Please migrate your application to Sui gRPC by September 25, 2026 to avoid any disruption.

Sui gRPC is a Protocol Buffers API that covers reads, streaming, and transaction execution. This guide maps every JSON-RPC method to its gRPC equivalent and calls out the behavior differences that break a naive port.

NetworkEndpoint
Mainnetsui-mainnet.g.alchemy.com:443
Testnetsui-testnet.g.alchemy.com:443

gRPC uses a port-style host with TLS on 443, not an HTTPS URL path. Authentication uses a Bearer token in the request header:

-H "Authorization: Bearer <YOUR_API_KEY>"

Every call must supply the .proto files or a compiled descriptor set — grpcurl -import-path/-proto, or -protoset. Reflection-based commands such as grpcurl … list and grpcurl … describe do not work, and will hang until they time out rather than returning an error. Start from the protos below.

  1. Clone the proto definitions from MystenLabs/sui-apis. Generated clients and grpcurl both need the .proto files.
  2. Generate a typed client for your language, or use grpcurl for CLI testing.
  3. Confirm connectivity with GetServiceInfo before porting anything else.
grpcurl \
  -H "Authorization: Bearer <YOUR_API_KEY>" \
  -import-path proto \
  -proto sui/rpc/v2/ledger_service.proto \
  -d '{}' \
  sui-mainnet.g.alchemy.com:443 \
  sui.rpc.v2.LedgerService/GetServiceInfo

ServiceProto filePurpose
LedgerServicesui/rpc/v2/ledger_service.protoPoint lookups for checkpoints, transactions, and objects, plus server-streaming historical queries over checkpoints, transactions, and events
StateServicesui/rpc/v2/state_service.protoLive state: balances, coin metadata, owned objects, dynamic fields
TransactionExecutionServicesui/rpc/v2/transaction_execution_service.protoExecute and simulate transactions
SubscriptionServicesui/rpc/v2/subscription_service.protoServer-streaming checkpoints, transactions, and events
MovePackageServicesui/rpc/v2/move_package_service.protoMove package, module, function, and datatype introspection
NameServicesui/rpc/v2/name_service.protoSuiNS forward and reverse resolution
SignatureVerificationServicesui/rpc/v2/signature_verification_service.protoVerify signatures outside transaction execution

JSON-RPCgRPCNotes
sui_getChainIdentifierLedgerService.GetServiceInfoAlso returns chain, epoch, and checkpoint height. The value format changes: JSON-RPC returned the first 4 bytes of the genesis digest as hex (35834a8a on mainnet); chain_id is the full digest in base58
sui_getCheckpoint, sui_getLatestCheckpointSequenceNumberLedgerService.GetCheckpointOmit the sequence number to get the latest checkpoint
sui_getCheckpointsLedgerService.ListCheckpointsServer-streaming. Use SubscribeCheckpoints for an ordered live stream
sui_getTotalTransactionBlocksLedgerService.GetCheckpointRead summary.total_network_transactions from the latest checkpoint
suix_getReferenceGasPriceLedgerService.GetEpochRead reference_gas_price from the response

JSON-RPCgRPCNotes
sui_getTransactionBlockLedgerService.GetTransactionUse read_mask instead of the options object
sui_multiGetTransactionBlocksLedgerService.BatchGetTransactionsOnly the top-level read_mask applies
suix_queryTransactionBlocksLedgerService.ListTransactionsServer-streaming, takes a TransactionFilter over a checkpoint range
suix_subscribeTransactionSubscriptionService.SubscribeTransactionsSame filter type as ListTransactions, so backfill and live stream share one filter
sui_executeTransactionBlockTransactionExecutionService.ExecuteTransactionSubmit signed transaction bytes
sui_dryRunTransactionBlock, sui_devInspectTransactionBlockTransactionExecutionService.SimulateTransactionOne method replaces both. Moved out of LiveDataService in v2
All unsafe_* buildersTransactionExecutionService.ExecuteTransactionNo direct equivalent. Build a PTB with the TypeScript or Rust SDK, then submit the bytes

The unsafe_* family (unsafe_paySui, unsafe_pay, unsafe_payAllSui, unsafe_transferSui, unsafe_transferObject, unsafe_moveCall, unsafe_splitCoin, unsafe_splitCoinEqual, unsafe_mergeCoins, unsafe_batchTransaction, unsafe_publish, unsafe_requestAddStake, unsafe_requestWithdrawStake) has no server-side replacement. Transaction construction moves entirely client-side to programmable transaction blocks.

JSON-RPCgRPCNotes
sui_getObject, sui_tryGetPastObjectLedgerService.GetObjectPass version for a historical read, within the retention window
sui_multiGetObjectsLedgerService.BatchGetObjectsPer-item field masks are ignored, only the top-level read_mask applies
suix_getOwnedObjectsStateService.ListOwnedObjectsOptional object_type filter, paginated with page_token
suix_getDynamicFieldsStateService.ListDynamicFieldsPaginated by parent object ID
suix_getDynamicFieldObjectLedgerService.GetObjectDerive the field object ID locally from the parent ID and field name, then fetch it. ListDynamicFields is unnecessary when you already know the name

JSON-RPCgRPCNotes
suix_getBalanceStateService.GetBalanceRequires owner and coin_type
suix_getAllBalancesStateService.ListBalancesPaginated across all coin types held by the owner
suix_getCoins, suix_getAllCoinsStateService.ListOwnedObjectsFilter by the full coin type for one coin, or the bare 0x2::coin::Coin type for all coins
suix_getCoinMetadata, suix_getTotalSupplyStateService.GetCoinInfoReturns metadata, regulated metadata, and treasury in one response

JSON-RPCgRPCNotes
suix_queryEventsLedgerService.ListEventsServer-streaming with an EventFilter over a checkpoint range
suix_subscribeEventSubscriptionService.SubscribeEventsSame EventFilter type as ListEvents
Events for one known transactionLedgerService.GetTransactionAdd events to the read_mask instead of a separate query

JSON-RPCgRPCNotes
sui_getNormalizedMoveModulesByPackage, sui_getNormalizedMoveModuleMovePackageService.GetPackageReturns the package with its modules
sui_getNormalizedMoveFunction, sui_getMoveFunctionArgTypesMovePackageService.GetFunctionTyped signature replaces the loose arg-type list
sui_getNormalizedMoveStructMovePackageService.GetDatatypeCovers structs and enums
No equivalentMovePackageService.ListPackageVersionsNew in gRPC. Lists published versions of a package

JSON-RPCgRPCNotes
suix_resolveNameServiceAddressNameService.LookupNameSuiNS name to address
suix_resolveNameServiceNamesNameService.ReverseLookupNameAddress to linked SuiNS name

Beyond the unsafe_* builders above, these two read paths also require client-side work rather than a method swap.

JSON-RPCWorkaround
suix_getStakes, suix_getStakesByIdsCall StateService.ListOwnedObjects filtered by type 0x3::staking_pool::StakedSui, then fetch full contents with LedgerService.GetObject
suix_getValidatorsApyNo APY field exists in gRPC. Read validator-set data and compute APY client-side. See sui issue #23832 for the recommended computation

JSON-RPC takes a positional array of arguments. gRPC takes typed protobuf messages. Do not translate positional indices directly, and do not port the JSON-RPC options object. Pass each value by name and use a FieldMask in read_mask to select the response shape.

Field masks are also the main latency lever. Requesting every field on a large object or transaction is significantly slower than requesting the three fields you actually use.

JSON-RPC cursors are opaque strings and are not portable into gRPC. Fetch a fresh cursor or token from the new API and persist that as your resume point.

gRPC uses two different paging mechanisms depending on the method. Check which one you are calling.

Unary methods — page tokens. StateService.ListOwnedObjects, StateService.ListBalances, StateService.ListDynamicFields, and MovePackageService.ListPackageVersions take page_size and page_token, and return next_page_token. The server can return fewer items than the requested page_size, so follow next_page_token rather than assuming a full page means more data and a short page means the end. Iterate until next_page_token is absent.

Server-streaming methods — bounds and cursors. LedgerService.ListCheckpoints, ListTransactions, and ListEvents have no page_token. They stream results and are controlled by these fields instead.

FieldPurpose
start_checkpoint, end_checkpointBound the checkpoint range to scan
options.limitCap the number of items returned
options.after, options.beforeResume from, or stop at, a cursor
options.orderingORDERING_ASCENDING (default) or ORDERING_DESCENDING

Each response frame carries watermark.cursor. Persist it as your resume point and pass it back as options.after on an ascending scan, or options.before on a descending one. The stream ends with a QueryEnd frame whose reason states why it stopped: QUERY_END_REASON_ITEM_LIMIT, QUERY_END_REASON_SCAN_LIMIT, QUERY_END_REASON_CHECKPOINT_BOUND, QUERY_END_REASON_CURSOR_BOUND, or QUERY_END_REASON_LEDGER_TIP. JSON responses carry these full enum names; typed clients expose them as generated enum constants. Do not treat stream termination as "all data retrieved" — check the reason and resume from the last cursor if it stopped on a limit rather than a bound.

suix_subscribeEvent and suix_subscribeTransaction are replaced by server-streaming RPCs on SubscriptionService. Three differences matter in production:

  • Subscriptions begin at the current tip and do not support resumption. There is no cursor you can pass to start a stream from the past.
  • To avoid gaps on reconnect, open the subscription first and immediately drain live frames into durable, bounded storage while a separate worker backfills with the paired list API (ListEvents, ListTransactions, or ListCheckpoints). Do not leave the subscription unread during backfill.
  • Process the durable spool after backfill reaches the subscription boundary, and restart from the last durably processed cursor if the spool fills or the stream ends.

Filtered streams emit progress-only frames where the payload is unset but watermark is present. Track watermark.cursor on every frame, not just on frames carrying data.

SubscribeCheckpoints differs: its frames carry a plain cursor (a checkpoint sequence number) instead of a watermark message. Backfill checkpoints by passing cursor + 1 as start_checkpoint on ListCheckpoints, not options.after.

JSON-RPC sometimes resolved older data through fallback paths that were invisible to the caller. gRPC does not. A full node serves only data inside its retention window (7 epochs / ~7 days) and returns NOT_FOUND for anything older.

If your workload does replays, audits, or deep historical reads, plan for this explicitly rather than discovering it as intermittent NOT_FOUND responses in production.

JSON-RPC object queries supported MatchAll, MatchAny, and MatchNone combinators. gRPC object queries have no equivalent: ListOwnedObjects takes a single positive object_type, so combine results from multiple calls or filter client-side. Transaction and event queries are where negation lives — TransactionFilter and EventFilter use a disjunctive normal form (DNF) filter with per-literal negation.

TransactionFilter and EventFilter are structured in three levels:

  • terms are ORed together. A present filter must contain at least one term. An absent filter matches everything.
  • literals within a single term are ANDed together.
  • Each literal sets exactly one predicate, plus an optional negated: true to invert it.

Match transactions not sent by a given address:

{
  "filter": {
    "terms": [
      { "literals": [ { "negated": true, "sender": { "address": "0x..." } } ] }
    ]
  }
}

Set the predicate field directly on the literal, as above. Some generated clients (notably protobuf-ts) represent oneofs internally as { "oneofKind": "sender", ... }; that is a client-side artifact and is not valid protobuf JSON. Sending it fails with message type sui.rpc.v2.TransactionLiteral has no known field named oneofKind. If you instead see TransactionFilter has no known field named negated, you have set the literal's fields directly on the filter — nest them under terms[].literals[].

Transaction predicates: sender, affected_address, affected_object, move_call, emit_module, event_type, event_stream_head, package_write. Event predicates: sender, emit_module, event_type, event_stream_head.

Write addresses in filter literals fully padded to 64 hex characters. Short forms are rejected with INVALID_ARGUMENT: invalid address — for example 0x3::validator::StakingRequestEvent fails while 0x0000…0003::validator::StakingRequestEvent works. (ListOwnedObjects's object_type accepts short forms; filters do not.)

Sui tracks value both in coin objects and in the address-balance accumulator. GetBalance returns a Balance message with three numeric fields.

Field (JSON)Meaning
balanceTotal across both representations
coinBalanceValue held in coin objects
addressBalanceValue held in the address-balance accumulator

Read balance for the combined total. Read coinBalance and addressBalance separately when you need to reconcile against your own ledger. There is no totalBalance field.

These fields use proto3 presence, so a field that is unset is omitted from the JSON response rather than returned as 0. Treat an absent addressBalance as zero rather than assuming the field is always present.

For transaction building, prefer gas smashing or address-balance gas payments over hand-assembling coin lists from ListOwnedObjects.

In proto3, marking a field optional enables presence detection, meaning the server can tell an explicitly set value from a default. It does not mean you can omit the field. Follow the API contract for required inputs.

ToolAction
Sui CLINo changes needed. The CLI already uses gRPC internally
@mysten/dapp-kitThe published client factory requires SuiJsonRpcClient and rejects SuiGrpcClient. Wait for a release that adds transport-agnostic client support
@mysten/kioskDoes not accept SuiGrpcClient. Migrate only after a Kiosk release adds gRPC support
Walrus site-builderManages its own connection through sites-config.yaml. Transport is controlled by the Walrus team

grpcurl \
  -H "Authorization: Bearer <YOUR_API_KEY>" \
  -import-path proto \
  -proto sui/rpc/v2/ledger_service.proto \
  -d '{
    "digest": "YOUR_TX_DIGEST",
    "read_mask": {"paths": ["digest", "effects", "events"]}
  }' \
  sui-mainnet.g.alchemy.com:443 \
  sui.rpc.v2.LedgerService/GetTransaction

grpcurl \
  -H "Authorization: Bearer <YOUR_API_KEY>" \
  -import-path proto \
  -proto sui/rpc/v2/state_service.proto \
  -d '{
    "owner": "0xYOUR_ADDRESS",
    "coin_type": "0x2::sui::SUI"
  }' \
  sui-mainnet.g.alchemy.com:443 \
  sui.rpc.v2.StateService/GetBalance

Replaces suix_getCoins. Drop the type parameter from the filter to cover the suix_getAllCoins case.

grpcurl \
  -H "Authorization: Bearer <YOUR_API_KEY>" \
  -import-path proto \
  -proto sui/rpc/v2/state_service.proto \
  -d '{
    "owner": "0xYOUR_ADDRESS",
    "object_type": "0x2::coin::Coin<0x2::sui::SUI>",
    "read_mask": {"paths": ["object_id", "version", "balance"]}
  }' \
  sui-mainnet.g.alchemy.com:443 \
  sui.rpc.v2.StateService/ListOwnedObjects

Replaces suix_subscribeEvent. Pair it with LedgerService/ListEvents using the same filter to backfill.

grpcurl \
  -H "Authorization: Bearer <YOUR_API_KEY>" \
  -import-path proto \
  -proto sui/rpc/v2/subscription_service.proto \
  -d '{}' \
  sui-mainnet.g.alchemy.com:443 \
  sui.rpc.v2.SubscriptionService/SubscribeEvents

  • Generate a typed client from the v2 protos, not v2beta2
  • Swap every JSON-RPC call using the mapping tables above
  • Replace options objects with read_mask field masks, requesting only the fields you use
  • Replace stored JSON-RPC cursors with fresh gRPC page tokens
  • Move unsafe_* transaction building to PTBs client-side
  • Rebuild staking reads on ListOwnedObjects plus GetObject, and move APY to a client-side computation
  • Rework WebSocket subscribers into stream plus backfill, with a durable spool and persisted watermark cursor
  • Handle NOT_FOUND for reads outside the retention window
  • Load test against the new response shapes before cutover

Was this page helpful?