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 servicetransaction.proto- Transaction servicesignature_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"]
Related Resources#
- gRPC Overview - Complete API introduction
- TypeScript Setup - TypeScript client
- Python Setup - Python client
- grpcurl Setup - Command-line testing
Need help? Contact support@dwellir.com