Skip to main content

Go Client for Sui gRPC

Build high-performance, production-ready Go applications for Sui using gRPC. This guide covers project setup, best practices, and production deployment patterns.

Why Go for gRPC?#

  • Native gRPC support - gRPC was originally designed with Go in mind
  • High performance - Compiled binary, efficient concurrency with goroutines
  • Strong typing - Compile-time type safety prevents runtime errors
  • Simple deployment - Single binary, no runtime dependencies
  • Excellent tooling - Great IDE support, testing frameworks, profiling

Prerequisites#

  • Go 1.21 or higher (install Go)
  • Protocol Buffers compiler (protoc)
  • Basic understanding of Go concurrency
  • Dwellir API key (get one here)

Project Setup#

Initialize Go Module#

# Create project directory
mkdir sui-grpc-go
cd sui-grpc-go

# Initialize Go module
go mod init github.com/yourusername/sui-grpc-go

Install Dependencies#

# Core gRPC dependencies
go get google.golang.org/grpc
go get google.golang.org/protobuf

# Code generation tools
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

# Additional useful packages
go get github.com/joho/godotenv # Environment variables
go get github.com/rs/zerolog # Structured logging

Get Proto Files from Sui APIs Repository#

# Clone the Sui APIs repository
git clone https://github.com/MystenLabs/sui-apis.git
cd sui-apis

# Copy proto files to your project
mkdir -p ../sui-grpc-go/protos
cp -r protos/sui ../sui-grpc-go/protos/

The proto files are located in protos/sui/:

  • ledger.proto - Ledger service (checkpoints, objects, transactions)
  • state.proto - State service (balances, objects)
  • move_package.proto - Package service
  • transaction.proto - Transaction service
  • signature_verification.proto - Signature verification

Generate Go Code from Protos#

# Navigate to your project directory
cd sui-grpc-go

# Ensure protoc-gen-go is in PATH
export PATH="$PATH:$(go env GOPATH)/bin"

# Generate Go code from proto files
protoc \
-I./protos \
--go_out=./pkg \
--go_opt=paths=source_relative \
--go-grpc_out=./pkg \
--go-grpc_opt=paths=source_relative \
./protos/sui/*.proto

# This creates in pkg/sui/:
# - *.pb.go files (message definitions)
# - *_grpc.pb.go files (service interfaces and clients)
Auto-Generation Script

Create a generate.sh script:

#!/bin/bash
# generate.sh

set -e

echo "Generating Go code from proto files..."

protoc \
-I./protos \
--go_out=./pkg \
--go_opt=paths=source_relative \
--go-grpc_out=./pkg \
--go-grpc_opt=paths=source_relative \
./protos/sui/*.proto

echo "✓ Code generation complete!"

Make it executable: chmod +x generate.sh

Add to Makefile:

.PHONY: generate
generate:
@./generate.sh

.PHONY: build
build: generate
go build -o bin/app ./cmd/app

Project Structure#

sui-grpc-go/
├── .env # Environment variables (gitignored)
├── .gitignore
├── go.mod
├── go.sum
├── cmd/
│ └── app/
│ └── main.go # Application entrypoint
├── internal/
│ ├── client/
│ │ ├── client.go # gRPC client wrapper
│ │ └── options.go # Client configuration
│ ├── config/
│ │ └── config.go # Configuration management
│ └── models/
│ └── types.go # Application types
├── pkg/
│ └── sui/ # Generated proto code
│ └── v2/
│ ├── *.pb.go
│ └── *_grpc.pb.go
└── examples/
├── get_checkpoint.go
├── get_balance.go
└── monitor_epochs.go

Configuration#

Environment Variables#

Create a .env file:

# .env
DWELLIR_ENDPOINT=api-sui-mainnet-full.n.dwellir.com:443
DWELLIR_API_KEY=your_api_key_here
GRPC_TIMEOUT=30s

Configuration Module#

Create internal/config/config.go:

package config

import (
"fmt"
"os"
"time"

"github.com/joho/godotenv"
)

// Config holds application configuration
type Config struct {
Endpoint string
APIKey string
Timeout time.Duration
}

// Load reads configuration from environment variables
func Load() (*Config, error) {
// Load .env file if it exists
_ = godotenv.Load()

endpoint := os.Getenv("DWELLIR_ENDPOINT")
apiKey := os.Getenv("DWELLIR_API_KEY")

if endpoint == "" {
return nil, fmt.Errorf("DWELLIR_ENDPOINT environment variable is required")
}

if apiKey == "" {
return nil, fmt.Errorf("DWELLIR_API_KEY environment variable is required")
}

timeout := 30 * time.Second
if timeoutStr := os.Getenv("GRPC_TIMEOUT"); timeoutStr != "" {
if parsed, err := time.ParseDuration(timeoutStr); err == nil {
timeout = parsed
}
}

return &Config{
Endpoint: endpoint,
APIKey: apiKey,
Timeout: timeout,
}, nil
}

Client Implementation#

Basic Client#

Create internal/client/client.go:

package client

import (
"context"
"fmt"
"time"

"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/metadata"

pb "github.com/yourusername/sui-grpc-go/pkg/sui/v2"
)

// SuiClient wraps gRPC connection and provides typed service clients
type SuiClient struct {
conn *grpc.ClientConn
apiKey string
timeout time.Duration

// Service clients
Ledger pb.LedgerServiceClient
State pb.StateServiceClient
Package pb.MovePackageServiceClient
TxService pb.TransactionServiceClient
}

// NewClient creates a new Sui gRPC client
func NewClient(endpoint, apiKey string, opts ...Option) (*SuiClient, error) {
// Apply default options
options := &clientOptions{
timeout: 30 * time.Second,
maxMsgSize: 50 * 1024 * 1024, // 50MB
keepalive: 30 * time.Second,
retryPolicy: defaultRetryPolicy(),
}

for _, opt := range opts {
opt(options)
}

// Create TLS credentials
creds := credentials.NewClientTLSFromCert(nil, "")

// Configure gRPC dial options
dialOpts := []grpc.DialOption{
grpc.WithTransportCredentials(creds),
grpc.WithDefaultCallOptions(
grpc.MaxCallRecvMsgSize(options.maxMsgSize),
grpc.MaxCallSendMsgSize(options.maxMsgSize),
),
grpc.WithKeepaliveParams(keepalive.ClientParameters{
Time: options.keepalive,
Timeout: 10 * time.Second,
PermitWithoutStream: true,
}),
}

// Add retry policy if configured
if options.retryPolicy != nil {
dialOpts = append(dialOpts, grpc.WithDefaultServiceConfig(options.retryPolicy))
}

// Establish connection
conn, err := grpc.NewClient(endpoint, dialOpts...)
if err != nil {
return nil, fmt.Errorf("failed to connect: %w", err)
}

client := &SuiClient{
conn: conn,
apiKey: apiKey,
timeout: options.timeout,
Ledger: pb.NewLedgerServiceClient(conn),
State: pb.NewStateServiceClient(conn),
Package: pb.NewMovePackageServiceClient(conn),
TxService: pb.NewTransactionServiceClient(conn),
}

return client, nil
}

// Close closes the gRPC connection
func (c *SuiClient) Close() error {
if c.conn != nil {
return c.conn.Close()
}
return nil
}

// NewContext creates a context with authentication and timeout
func (c *SuiClient) NewContext(parent context.Context) (context.Context, context.CancelFunc) {
// Add authentication metadata
ctx := metadata.AppendToOutgoingContext(parent, "x-api-key", c.apiKey)

// Add timeout
return context.WithTimeout(ctx, c.timeout)
}

Client Options#

Create internal/client/options.go:

package client

import (
"time"

"google.golang.org/grpc/keepalive"
)

type clientOptions struct {
timeout time.Duration
maxMsgSize int
keepalive time.Duration
retryPolicy *string
}

// Option configures client behavior
type Option func(*clientOptions)

// WithTimeout sets the default request timeout
func WithTimeout(timeout time.Duration) Option {
return func(o *clientOptions) {
o.timeout = timeout
}
}

// WithMaxMessageSize sets the maximum message size
func WithMaxMessageSize(size int) Option {
return func(o *clientOptions) {
o.maxMsgSize = size
}
}

// WithKeepalive sets keepalive interval
func WithKeepalive(interval time.Duration) Option {
return func(o *clientOptions) {
o.keepalive = interval
}
}

// WithRetry enables automatic retry with exponential backoff
func WithRetry() Option {
return func(o *clientOptions) {
policy := defaultRetryPolicy()
o.retryPolicy = &policy
}
}

func defaultRetryPolicy() string {
return `{
"methodConfig": [{
"name": [{"service": "sui.rpc.v2"}],
"retryPolicy": {
"maxAttempts": 3,
"initialBackoff": "0.5s",
"maxBackoff": "5s",
"backoffMultiplier": 2.0,
"retryableStatusCodes": ["UNAVAILABLE", "RESOURCE_EXHAUSTED"]
}
}]
}`
}

Basic Usage Examples#

Get Checkpoint#

Create examples/get_checkpoint.go:

package main

import (
"context"
"fmt"
"log"

"github.com/yourusername/sui-grpc-go/internal/client"
"github.com/yourusername/sui-grpc-go/internal/config"
pb "github.com/yourusername/sui-grpc-go/pkg/sui/v2"
"google.golang.org/protobuf/types/known/fieldmaskpb"
)

func main() {
// Load configuration
cfg, err := config.Load()
if err != nil {
log.Fatalf("Failed to load config: %v", err)
}

// Create client
suiClient, err := client.NewClient(
cfg.Endpoint,
cfg.APIKey,
client.WithTimeout(cfg.Timeout),
client.WithRetry(),
)
if err != nil {
log.Fatalf("Failed to create client: %v", err)
}
defer suiClient.Close()

// Create context with authentication
ctx, cancel := suiClient.NewContext(context.Background())
defer cancel()

// Get current checkpoint
request := &pb.GetCheckpointRequest{
ReadMask: &fieldmaskpb.FieldMask{
Paths: []string{
"sequence_number",
"timestamp_ms",
"contents.transactions",
},
},
}

response, err := suiClient.Ledger.GetCheckpoint(ctx, request)
if err != nil {
log.Fatalf("Failed to get checkpoint: %v", err)
}

checkpoint := response.GetCheckpoint()

// Display results
fmt.Println("Current Checkpoint:")
fmt.Printf("Sequence Number: %d\n", checkpoint.GetSequenceNumber())
fmt.Printf("Timestamp: %d\n", checkpoint.GetTimestampMs())
fmt.Printf("Transactions: %d\n", len(checkpoint.GetContents().GetTransactions()))
}

Get Balance#

package main

import (
"context"
"fmt"
"log"
"math/big"

"github.com/yourusername/sui-grpc-go/internal/client"
"github.com/yourusername/sui-grpc-go/internal/config"
pb "github.com/yourusername/sui-grpc-go/pkg/sui/v2"
)

func main() {
cfg, err := config.Load()
if err != nil {
log.Fatalf("Failed to load config: %v", err)
}

suiClient, err := client.NewClient(cfg.Endpoint, cfg.APIKey)
if err != nil {
log.Fatalf("Failed to create client: %v", err)
}
defer suiClient.Close()

// Query balance
address := "0x742d35cc6634c0532925a3b844bc9e7eb503c114a04bd3e02c7681a09e58b01d"
coinType := "0x0000000000000000000000000000000000000000000000000000000000000002::sui::SUI"

ctx, cancel := suiClient.NewContext(context.Background())
defer cancel()

request := &pb.GetBalanceRequest{
Owner: &address,
CoinType: &coinType,
}

response, err := suiClient.State.GetBalance(ctx, request)
if err != nil {
log.Fatalf("Failed to get balance: %v", err)
}

// Convert MIST to SUI
balance := new(big.Int)
balance.SetUint64(response.GetBalance())

divisor := new(big.Int)
divisor.SetUint64(1_000_000_000) // 1 SUI = 10^9 MIST

sui := new(big.Float).Quo(
new(big.Float).SetInt(balance),
new(big.Float).SetInt(divisor),
)

fmt.Printf("Address: %s\n", address)
fmt.Printf("Balance: %s SUI\n", sui.Text('f', 9))
}

Advanced Patterns#

Connection Pool#

package client

import (
"context"
"sync"

"google.golang.org/grpc"
)

// ClientPool manages a pool of gRPC connections
type ClientPool struct {
endpoint string
apiKey string
size int
clients []*SuiClient
nextClient int
mu sync.Mutex
}

// NewClientPool creates a connection pool
func NewClientPool(endpoint, apiKey string, size int) (*ClientPool, error) {
pool := &ClientPool{
endpoint: endpoint,
apiKey: apiKey,
size: size,
clients: make([]*SuiClient, size),
}

// Create initial connections
for i := 0; i < size; i++ {
client, err := NewClient(endpoint, apiKey)
if err != nil {
// Clean up any created clients
for j := 0; j < i; j++ {
pool.clients[j].Close()
}
return nil, err
}
pool.clients[i] = client
}

return pool, nil
}

// Get returns a client from the pool (round-robin)
func (p *ClientPool) Get() *SuiClient {
p.mu.Lock()
defer p.mu.Unlock()

client := p.clients[p.nextClient]
p.nextClient = (p.nextClient + 1) % p.size

return client
}

// Close closes all connections in the pool
func (p *ClientPool) Close() error {
for _, client := range p.clients {
if err := client.Close(); err != nil {
return err
}
}
return nil
}

Concurrent Batch Processing#

package main

import (
"context"
"fmt"
"log"
"sync"

"github.com/yourusername/sui-grpc-go/internal/client"
pb "github.com/yourusername/sui-grpc-go/pkg/sui/v2"
)

// fetchCheckpointsConcurrently fetches multiple checkpoints in parallel
func fetchCheckpointsConcurrently(
client *client.SuiClient,
sequenceNumbers []uint64,
) ([]*pb.Checkpoint, error) {
var wg sync.WaitGroup
results := make([]*pb.Checkpoint, len(sequenceNumbers))
errors := make([]error, len(sequenceNumbers))

// Limit concurrency
concurrency := 10
sem := make(chan struct{}, concurrency)

for i, seqNum := range sequenceNumbers {
wg.Add(1)
go func(index int, seq uint64) {
defer wg.Done()

// Acquire semaphore
sem <- struct{}{}
defer func() { <-sem }()

ctx, cancel := client.NewContext(context.Background())
defer cancel()

request := &pb.GetCheckpointRequest{
SequenceNumber: &seq,
}

response, err := client.Ledger.GetCheckpoint(ctx, request)
if err != nil {
errors[index] = err
return
}

results[index] = response.GetCheckpoint()
}(i, seqNum)
}

wg.Wait()

// Check for errors
for i, err := range errors {
if err != nil {
return nil, fmt.Errorf("failed to fetch checkpoint %d: %w", sequenceNumbers[i], err)
}
}

return results, nil
}

func main() {
cfg, _ := config.Load()
suiClient, _ := client.NewClient(cfg.Endpoint, cfg.APIKey)
defer suiClient.Close()

checkpoints := []uint64{1000, 1001, 1002, 1003, 1004}

results, err := fetchCheckpointsConcurrently(suiClient, checkpoints)
if err != nil {
log.Fatal(err)
}

fmt.Printf("Fetched %d checkpoints successfully\n", len(results))
}

Structured Error Handling#

package client

import (
"errors"
"fmt"

"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)

// Error types
var (
ErrNotFound = errors.New("resource not found")
ErrUnauth enticated = errors.New("authentication failed")
ErrInvalidRequest = errors.New("invalid request")
ErrTimeout = errors.New("request timeout")
ErrUnavailable = errors.New("service unavailable")
)

// WrapGRPCError converts gRPC status errors to application errors
func WrapGRPCError(err error) error {
if err == nil {
return nil
}

st, ok := status.FromError(err)
if !ok {
return fmt.Errorf("unknown error: %w", err)
}

switch st.Code() {
case codes.NotFound:
return fmt.Errorf("%w: %s", ErrNotFound, st.Message())
case codes.Unauthenticated:
return fmt.Errorf("%w: %s", ErrUnauthenticated, st.Message())
case codes.InvalidArgument:
return fmt.Errorf("%w: %s", ErrInvalidRequest, st.Message())
case codes.DeadlineExceeded:
return fmt.Errorf("%w: %s", ErrTimeout, st.Message())
case codes.Unavailable:
return fmt.Errorf("%w: %s", ErrUnavailable, st.Message())
default:
return fmt.Errorf("gRPC error [%s]: %s", st.Code(), st.Message())
}
}

// Example usage
func (c *SuiClient) GetBalanceSafe(ctx context.Context, owner, coinType string) (uint64, error) {
request := &pb.GetBalanceRequest{
Owner: &owner,
CoinType: &coinType,
}

response, err := c.State.GetBalance(ctx, request)
if err != nil {
return 0, WrapGRPCError(err)
}

return response.GetBalance(), nil
}

Production Best Practices#

Health Checks#

package main

import (
"context"
"fmt"
"time"

"github.com/yourusername/sui-grpc-go/internal/client"
"google.golang.org/grpc/health/grpc_health_v1"
)

// HealthChecker periodically checks service health
type HealthChecker struct {
client *client.SuiClient
interval time.Duration
}

func NewHealthChecker(client *client.SuiClient, interval time.Duration) *HealthChecker {
return &HealthChecker{
client: client,
interval: interval,
}
}

func (h *HealthChecker) Start(ctx context.Context) {
ticker := time.NewTicker(h.interval)
defer ticker.Stop()

for {
select {
case <-ctx.Done():
return
case <-ticker.C:
if err := h.check(ctx); err != nil {
fmt.Printf("Health check failed: %v\n", err)
} else {
fmt.Println("Health check OK")
}
}
}
}

func (h *HealthChecker) check(ctx context.Context) error {
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()

// Simple health check: fetch latest checkpoint
_, err := h.client.Ledger.GetCheckpoint(
metadata.AppendToOutgoingContext(ctx, "x-api-key", h.client.apiKey),
&pb.GetCheckpointRequest{},
)

return err
}

Metrics and Observability#

package metrics

import (
"context"
"time"

"google.golang.org/grpc"
"google.golang.org/grpc/status"
)

// MetricsCollector collects client metrics
type MetricsCollector struct {
requestCount map[string]int64
requestLatency map[string]time.Duration
errorCount map[string]int64
}

// UnaryClientInterceptor returns a gRPC interceptor for metrics
func (m *MetricsCollector) UnaryClientInterceptor() grpc.UnaryClientInterceptor {
return func(
ctx context.Context,
method string,
req, reply interface{},
cc *grpc.ClientConn,
invoker grpc.UnaryInvoker,
opts ...grpc.CallOption,
) error {
start := time.Now()

// Call the RPC
err := invoker(ctx, method, req, reply, cc, opts...)

// Record metrics
duration := time.Since(start)
m.requestCount[method]++
m.requestLatency[method] = duration

if err != nil {
st, _ := status.FromError(err)
m.errorCount[st.Code().String()]++
}

return err
}
}

Testing#

Mock Client#

package client_test

import (
"context"
"testing"

pb "github.com/yourusername/sui-grpc-go/pkg/sui/v2"
"google.golang.org/grpc"
)

// MockLedgerClient implements pb.LedgerServiceClient for testing
type MockLedgerClient struct {
GetCheckpointFunc func(context.Context, *pb.GetCheckpointRequest, ...grpc.CallOption) (*pb.GetCheckpointResponse, error)
}

func (m *MockLedgerClient) GetCheckpoint(
ctx context.Context,
req *pb.GetCheckpointRequest,
opts ...grpc.CallOption,
) (*pb.GetCheckpointResponse, error) {
if m.GetCheckpointFunc != nil {
return m.GetCheckpointFunc(ctx, req, opts...)
}
return &pb.GetCheckpointResponse{}, nil
}

// Example test
func TestGetCheckpoint(t *testing.T) {
mockClient := &MockLedgerClient{
GetCheckpointFunc: func(
ctx context.Context,
req *pb.GetCheckpointRequest,
opts ...grpc.CallOption,
) (*pb.GetCheckpointResponse, error) {
return &pb.GetCheckpointResponse{
Checkpoint: &pb.Checkpoint{
SequenceNumber: proto.Uint64(12345),
},
}, nil
},
}

// Use mock in tests
response, err := mockClient.GetCheckpoint(context.Background(), &pb.GetCheckpointRequest{})
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}

if response.GetCheckpoint().GetSequenceNumber() != 12345 {
t.Error("Unexpected sequence number")
}
}

Docker Deployment#

# Build stage
FROM golang:1.21-alpine AS builder

WORKDIR /app

# Copy go mod files
COPY go.mod go.sum ./
RUN go mod download

# Copy source code
COPY . .

# Build binary
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main ./cmd/app

# Runtime stage
FROM alpine:latest

RUN apk --no-cache add ca-certificates

WORKDIR /root/

# Copy binary from builder
COPY --from=builder /app/main .

# Run
CMD ["./main"]

Need help? Contact support@dwellir.com