Sui Client V2 (Unified JSON-RPC / gRPC Contract)
March 13, 2026 ยท View on GitHub
sui/v2 exposes one public client contract on top of two transport backends:
- JSON-RPC via
HttpConn - gRPC via
grpcconn.SuiGrpcClient
The core value of v2 is that callers use the same request/response model regardless of the underlying transport. The backend is selected once at client construction time; the rest of the application code can stay unchanged.
Design Goals
- Provide one stable public API for both JSON-RPC and gRPC.
- Reuse the same request option structs and response structs across transports.
- Make backend switching, fallback, and A/B comparison easy.
- Normalize transport-specific payload shape differences where practical.
- Keep remaining incompatibilities explicit instead of pretending the two backends are byte-for-byte identical.
Backend Selection
NewClient accepts either a gRPC client or an HTTP JSON-RPC connection:
package main
import (
"context"
"github.com/block-vision/sui-go-sdk/common/grpcconn"
"github.com/block-vision/sui-go-sdk/common/httpconn"
suiV2 "github.com/block-vision/sui-go-sdk/sui/v2"
)
func newGrpcClient() (suiV2.Client, error) {
gc := grpcconn.NewSuiGrpcClient("your-grpc-endpoint")
return suiV2.NewClient(suiV2.ClientOptions{
GrpcClient: gc,
})
}
func newJSONRPCClient() (suiV2.Client, error) {
hc := httpconn.NewHttpConn("your-json-rpc-endpoint", nil)
return suiV2.NewClient(suiV2.ClientOptions{
HttpConn: hc,
})
}
func main() {
var client suiV2.Client
var err error
client, err = newGrpcClient()
// client, err = newJSONRPCClient()
if err != nil {
panic(err)
}
_, _ = client.GetReferenceGasPrice(context.Background())
}
Switching transports should only require changing ClientOptions. Call sites keep
using the same suiV2.Client interface.
Public Contract
The following parts of the v2 API are intended to be transport-agnostic:
- The
suiV2.Clientmethod set. - Request option types such as
GetObjectsOptions,ListCoinsOptions,GetTransactionOptions, andSimulateTransactionOptions. - Shared response types re-exported from
sui/v2/types. - The meaning of
Includeflags for optional transaction/object fields.
In practice, this means callers can rely on the same top-level shapes for:
- Objects, owned objects, coins, balances, dynamic fields, system state, and Move metadata.
- Transaction responses via
TransactionResult/SimulateTransactionResult. - Parsed transaction sub-structures such as
TransactionData,TransactionEffects,ChangedObject,Event, andBalanceChange.
v2 also normalizes several payload-shape differences for you:
- JSON-RPC and gRPC both map into the same exported Go structs.
- Transaction command keys are exposed in v2 naming (for example
typeArguments,result,subresult). - Coin/object type strings are normalized where the JSON-RPC source uses shorter or differently formatted type strings.
- Raw transaction bytes are exposed through
transaction.bcswhen requested and available from the selected backend.
Example
tx, err := client.GetTransaction(ctx, suiV2.GetTransactionOptions{
Digest: "3CK7Fv9CUp3QetDhszToqwnDHgzpYPauP1H2N5iyuijh",
Include: suiV2.TransactionInclude{
Transaction: true,
Effects: true,
Events: true,
BalanceChanges: true,
Bcs: true,
},
})
if err != nil {
panic(err)
}
_ = tx.Transaction.TransactionData
_ = tx.Transaction.Effects
_ = tx.Transaction.Events
_ = tx.Transaction.BCS
Typed Errors
v2 exposes a public typed error model through sui/v2:
*suiV2.SDKErrorsuiV2.ErrBackendRequiredsuiV2.ErrGrpcClientRequiredsuiV2.ErrHttpConnRequiredsuiV2.ErrTransactionNotFoundsuiV2.ErrSystemStateNotFoundsuiV2.ErrChainIdentifierNotFound
Use errors.Is for category checks and errors.As when you want extra context:
resp, err := client.GetTransaction(ctx, suiV2.GetTransactionOptions{
Digest: "some-digest",
})
if err != nil {
if errors.Is(err, suiV2.ErrTransactionNotFound) {
// handle not found
}
var sdkErr *suiV2.SDKError
if errors.As(err, &sdkErr) {
fmt.Printf("code=%s transport=%s method=%s operation=%s\n",
sdkErr.Code, sdkErr.Transport, sdkErr.Method, sdkErr.Operation)
}
return
}
_ = resp
Transport-level failures are wrapped as SDKError with:
Code = transport_errorTransport = grpcorjson-rpcMethod = public v2 method nameOperation = underlying backend operation name
This lets callers keep one error-handling path even when they switch transports.
Running Examples
The repository includes v2_examples/main.go, which now supports both backends.
Run with gRPC:
V2_TRANSPORT=grpc \
BLOCKVISION_API_KEY=YOUR_API_KEY \
go run ./v2_examples
Run with JSON-RPC:
V2_TRANSPORT=json-rpc \
go run ./v2_examples GetBalance
Optional environment variables:
V2_GRPC_ENDPOINTV2_RPC_URLBLOCKVISION_API_KEY(required forgrpc)
Known Differences / Compatibility Notes
v2 targets semantic compatibility across transports, not guaranteed byte-for-byte identity of every upstream payload. Consumers should rely on the normalized field meaning, and treat the following differences as intentional compatibility notes:
GetChainIdentifier: the two backends currently expose different upstream notions of chain identity. gRPC returns the genesis checkpoint digest from service info, while JSON-RPC returns the result ofsui_getChainIdentifier. The value is surfaced through the same response field, but should not be assumed to be string-identical across transports.transaction.bcs: both backends expose raw transaction bytes attransaction.bcswheninclude.bcs = true, but they come from different upstream fields. gRPC uses transaction BCS from the protobuf response; JSON-RPC usesrawTransaction. Presence depends on whether the selected backend returns raw bytes for that request.effects.eventsDigest: gRPC can populateeffects.eventsDigestdirectly. JSON-RPC does not currently provide an equivalent field in the normalized v2 response, so callers must treat it as optional.effects.changedObjects[].inputDigest: gRPC can provideinputDigestdirectly for changed objects. JSON-RPC supplements it when it can derive the value from shared objects or gas payment inputs, but owned-objectinputDigestis not always recoverable from JSON-RPC data and may be absent.transaction.transaction.commands[].*.typeArguments: v2 normalizes the field name itself totypeArguments, but the type argument strings may still differ in address formatting between backends, such as short addresses like0x2::...versus zero-padded canonical addresses. If you compare type arguments across transports, canonicalize the address portion first.transaction.transaction.inputspure input representation: JSON-RPC historically exposes pure inputs as{type, value, valueType}and v2 normalizes them toward raw BCS bytes when possible. gRPC may expose either a typed literal form (for examplePureU64Input,PureAddressInput,PureVecInput) or fall back toPureBCSInputdepending on what metadata the upstream protobuf response carries. These representations are semantically equivalent PTB inputs, but they are not guaranteed to serialize identically.
Scope
v2 is a contract-normalization layer, not a promise that JSON-RPC and gRPC are implemented by the Sui network as identical wire protocols. When a transport difference cannot be losslessly removed, the v2 API prefers:
- one shared exported field name,
- best-effort normalization,
- and explicit documentation of the remaining difference.