Docs

Go Client for Sui gRPC

Complete guide to building production Go applications with Sui gRPC API on Dwellir. Includes project setup, type safety, connection pooling, and production patterns.

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

Bash
# 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

Bash
# 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

Bash
# 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

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

Bash
#!/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:

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

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

Project Structure

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

Bash
# .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:

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:

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:

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:

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

Go
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

Go
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

Go
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

Go
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

Go
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

Go
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

Go
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

dockerfile
# 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