Skip to content

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 MethodgRPC Service.Method
sui_getLatestCheckpointSequenceNumberLedgerService/GetLatestCheckpoint
sui_getCheckpointLedgerService/GetCheckpoint
sui_getCheckpointsLedgerService/GetCheckpoint (batch)
sui_getTransactionBlockLedgerService/GetTransaction
sui_multiGetTransactionBlocksLedgerService/BatchGetTransactions

Object Queries

JSON-RPC MethodgRPC Service.Method
sui_getObjectLedgerService/GetObject
sui_multiGetObjectsLedgerService/BatchGetObjects
sui_tryGetPastObjectLedgerService/GetObject (with version)

Balance & Coin Queries

JSON-RPC MethodgRPC Service.Method
sui_getBalanceStateService/GetBalance
sui_getAllBalancesStateService/ListBalances
sui_getCoinsStateService/ListCoins
sui_getAllCoinsStateService/ListCoins
suix_getCoinMetadataStateService/GetCoinInfo

Transaction Execution

JSON-RPC MethodgRPC Service.Method
sui_executeTransactionBlockTransactionExecutionService/ExecuteTransaction
sui_dryRunTransactionBlockTransactionExecutionService/SimulateTransaction

Move & Package Queries

JSON-RPC MethodgRPC Service.Method
sui_getNormalizedMoveModuleMovePackageService/GetPackage
sui_getNormalizedMoveFunctionMovePackageService/GetFunction
sui_getNormalizedMoveStructMovePackageService/GetDatatype

Name Service

JSON-RPC MethodgRPC Service.Method
suix_resolveNameServiceAddressNameService/LookupName
suix_resolveNameServiceNamesNameService/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):

typescript
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):

typescript
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 SuiHTTPTransport to GrpcWebFetchTransport
  • Client changed from SuiClient to SuiGrpcClient
  • 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 GrpcTransport for full bidirectional streaming support. See the gRPC Gateway docs.

Getting an Object

Before (JSON-RPC):

typescript
const object = await client.getObject({
  id: '0x123...',
  options: { showContent: true, showOwner: true }
})

console.log('Object:', object.data)

After (gRPC):

typescript
const { object } = await client.ledger.getObject({
  objectId: '0x123...'
})

console.log('Object:', object)

Getting Account Balance

Before (JSON-RPC):

typescript
const balance = await client.getBalance({
  owner: '0xabc...',
  coinType: '0x2::sui::SUI'
})

console.log('Balance:', balance.totalBalance)

After (gRPC):

typescript
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):

typescript
// Had to poll for new checkpoints
setInterval(async () => {
  const latest = await client.getLatestCheckpointSequenceNumber()
  console.log('Polled checkpoint:', latest)
}, 1000)

After (gRPC - real-time streaming):

typescript
// 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:

typescript
// 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:

typescript
// 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:

typescript
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:

typescript
// 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:

bash
# 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:

bash
grpcurl -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:

typescript
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

  1. Start with read-only operations (checkpoints, objects, balances)
  2. Move to transaction simulation
  3. Finally migrate transaction execution
  4. Keep JSON-RPC as fallback until confident

Need Help?

🚀 Ready to start?

Get your API key - the same key works for both JSON-RPC and gRPC.

Released under the MIT License.