Migrating from JSON-RPC to gRPC
A practical guide to migrating your Sui application from JSON-RPC to gRPC.
Why Migrate?
Sui has deprecated JSON-RPC in favor of gRPC, with full deprecation planned for April 2026. Here's why the migration is worth it:
- Performance: gRPC's binary Protocol Buffers are 3-10x faster than JSON
- Streaming: Subscribe to real-time updates instead of polling
- Type safety: Catch errors at compile time with generated types
- New features: Some Sui capabilities are only available via gRPC
📌 Inodra Gives You Breathing Room
You'll eventually need to migrate, but it's not urgent if you're using Inodra. We'll continue supporting JSON-RPC on a best-effort basis even after Sui removes it by translating requests behind the scenes. Plan your migration when it makes sense for your team.
Migration Checklist
- [ ] Get an Inodra API key (same key works for both protocols)
- [ ] Install gRPC dependencies
- [ ] Decide: use server reflection for quick testing, or compile protos for production
- [ ] Map your JSON-RPC calls to gRPC equivalents (see table below)
- [ ] Update authentication (same
x-api-key, different header format) - [ ] Test with grpcurl before changing code
- [ ] Migrate incrementally - start with read operations
Method Mapping
Here's how common JSON-RPC methods map to gRPC services and methods:
Checkpoint & Transaction Queries
| JSON-RPC Method | gRPC Service.Method |
|---|---|
sui_getLatestCheckpointSequenceNumber | LedgerService/GetLatestCheckpoint |
sui_getCheckpoint | LedgerService/GetCheckpoint |
sui_getCheckpoints | LedgerService/GetCheckpoint (batch) |
sui_getTransactionBlock | LedgerService/GetTransaction |
sui_multiGetTransactionBlocks | LedgerService/BatchGetTransactions |
Object Queries
| JSON-RPC Method | gRPC Service.Method |
|---|---|
sui_getObject | LedgerService/GetObject |
sui_multiGetObjects | LedgerService/BatchGetObjects |
sui_tryGetPastObject | LedgerService/GetObject (with version) |
Balance & Coin Queries
| JSON-RPC Method | gRPC Service.Method |
|---|---|
sui_getBalance | StateService/GetBalance |
sui_getAllBalances | StateService/ListBalances |
sui_getCoins | StateService/ListCoins |
sui_getAllCoins | StateService/ListCoins |
suix_getCoinMetadata | StateService/GetCoinInfo |
Transaction Execution
| JSON-RPC Method | gRPC Service.Method |
|---|---|
sui_executeTransactionBlock | TransactionExecutionService/ExecuteTransaction |
sui_dryRunTransactionBlock | TransactionExecutionService/SimulateTransaction |
Move & Package Queries
| JSON-RPC Method | gRPC Service.Method |
|---|---|
sui_getNormalizedMoveModule | MovePackageService/GetPackage |
sui_getNormalizedMoveFunction | MovePackageService/GetFunction |
sui_getNormalizedMoveStruct | MovePackageService/GetDatatype |
Name Service
| JSON-RPC Method | gRPC Service.Method |
|---|---|
suix_resolveNameServiceAddress | NameService/LookupName |
suix_resolveNameServiceNames | NameService/ReverseLookupName |
Before & After: Code Examples
The Sui SDK now includes SuiGrpcClient with full type safety, making migration straightforward.
Getting the Latest Checkpoint
Before (JSON-RPC with Sui SDK):
import { SuiClient, SuiHTTPTransport } from '@mysten/sui/client'
const transport = new SuiHTTPTransport({
url: 'https://api.inodra.com/v1/jsonrpc',
rpc: {
headers: { 'x-api-key': process.env.INODRA_API_KEY! }
}
})
const client = new SuiClient({ transport })
const checkpoint = await client.getLatestCheckpointSequenceNumber()
console.log('Latest checkpoint:', checkpoint)After (gRPC with Sui SDK):
import { SuiGrpcClient } from '@mysten/sui/grpc'
import { GrpcWebFetchTransport } from '@protobuf-ts/grpcweb-transport'
const transport = new GrpcWebFetchTransport({
baseUrl: 'https://grpc.inodra.com',
meta: { 'x-api-key': process.env.INODRA_API_KEY! }
})
const client = new SuiGrpcClient({
network: 'mainnet',
transport
})
const { checkpoint } = await client.ledger.getLatestCheckpoint({})
console.log('Latest checkpoint:', checkpoint?.sequenceNumber)What changed:
- Same SDK pattern (
@mysten/sui), different imports - Transport switched from
SuiHTTPTransporttoGrpcWebFetchTransport - Client changed from
SuiClienttoSuiGrpcClient - Typed service clients (
client.ledger,client.state) replace method calls
Note: This example uses gRPC-Web which works in browsers and Node.js. For server-side only code, you can also use native gRPC with
GrpcTransportfor full bidirectional streaming support. See the gRPC Gateway docs.
Getting an Object
Before (JSON-RPC):
const object = await client.getObject({
id: '0x123...',
options: { showContent: true, showOwner: true }
})
console.log('Object:', object.data)After (gRPC):
const { object } = await client.ledger.getObject({
objectId: '0x123...'
})
console.log('Object:', object)Getting Account Balance
Before (JSON-RPC):
const balance = await client.getBalance({
owner: '0xabc...',
coinType: '0x2::sui::SUI'
})
console.log('Balance:', balance.totalBalance)After (gRPC):
const { balance } = await client.state.getBalance({
address: '0xabc...',
coinType: '0x2::sui::SUI'
})
console.log('Balance:', balance?.totalBalance)Subscribing to Updates (New in gRPC!)
This is where gRPC really shines. No more polling - get real-time updates as they happen.
Before (JSON-RPC - requires polling):
// Had to poll for new checkpoints
setInterval(async () => {
const latest = await client.getLatestCheckpointSequenceNumber()
console.log('Polled checkpoint:', latest)
}, 1000)After (gRPC - real-time streaming):
// Subscribe to new checkpoints in real-time
const stream = client.subscription.subscribeCheckpoints({
startSequenceNumber: BigInt(0)
})
for await (const checkpoint of stream.responses) {
console.log('New checkpoint:', checkpoint.sequenceNumber)
// Process each checkpoint as it arrives - no polling delay
}Why streaming matters:
- Lower latency: Data arrives as soon as it's available
- Less server load: One connection vs repeated polling
- Cost efficient: Each streamed message costs only 2 CU
Common Gotchas
1. Response Format Differences
gRPC responses use Protocol Buffers, which have slightly different structures:
// JSON-RPC response
{
"result": {
"data": { ... },
"error": null
}
}
// gRPC response (direct object)
{
checkpoint: { sequenceNumber: "123", ... }
}2. Error Handling
JSON-RPC returns errors in the response body. gRPC throws exceptions with status codes:
// JSON-RPC error handling
if (response.error) {
console.error(response.error.message)
}
// gRPC error handling with SuiGrpcClient
import { RpcError } from '@protobuf-ts/runtime-rpc'
try {
const { checkpoint } = await client.ledger.getCheckpoint({
sequenceNumber: BigInt(999999999)
})
} catch (err) {
if (err instanceof RpcError) {
switch (err.code) {
case 'NOT_FOUND':
console.error('Checkpoint not found')
break
case 'UNAUTHENTICATED':
console.error('Invalid API key')
break
default:
console.error('Error:', err.message)
}
}
}3. Field Masks for Performance
gRPC supports field masks to request only specific fields - use them to reduce response size:
const request = {
objectId: '0x123...',
fieldMask: {
paths: ['content', 'owner'] // Only fetch these fields
}
}4. Numbers are Strings
Protocol Buffers handle large numbers as strings to avoid precision loss:
// JSON-RPC might return
{
balance: 1000000000
}
// gRPC returns
{
balance: '1000000000'
}
// Always parse explicitly
const balance = BigInt(response.balance)Testing Your Migration
Step 1: Test with grpcurl First
Before changing code, verify calls work with grpcurl:
# Test the endpoint works
grpcurl -H "x-api-key: YOUR_API_KEY" \
grpc.inodra.com:443 \
sui.rpc.v2.LedgerService/GetLatestCheckpoint
# Compare with your existing JSON-RPC call
curl -X POST https://api.inodra.com/v1/jsonrpc \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{"jsonrpc":"2.0","id":1,"method":"sui_getLatestCheckpointSequenceNumber","params":[]}'Tip: Server reflection is enabled, so you can explore all available services with:
bashgrpcurl -H "x-api-key: YOUR_API_KEY" grpc.inodra.com:443 list
Step 2: Run Both in Parallel
During migration, run both implementations and compare results:
async function compareCheckpoint() {
// JSON-RPC
const jsonRpcResult = await suiClient.getLatestCheckpointSequenceNumber()
// gRPC
const { checkpoint } = await grpcClient.ledger.getLatestCheckpoint({})
const grpcResult = checkpoint?.sequenceNumber
console.log('JSON-RPC:', jsonRpcResult)
console.log('gRPC:', grpcResult)
console.log('Match:', jsonRpcResult === grpcResult)
}Step 3: Migrate Incrementally
- Start with read-only operations (checkpoints, objects, balances)
- Move to transaction simulation
- Finally migrate transaction execution
- Keep JSON-RPC as fallback until confident
Need Help?
- gRPC Gateway documentation for detailed setup
- Sui gRPC API reference for method details
- Discord community for migration support
🚀 Ready to start?
Get your API key - the same key works for both JSON-RPC and gRPC.