Tip floor stream

Stream real-time tip floor data from NextBlock to optimize your transaction tips dynamically. This example shows a complete implementation including connection setup, authentication, and tip calculation.

Prerequisites

First, install the required dependencies and generate the gRPC client:

go mod init nextblock-tip-floor-example
go get google.golang.org/grpc
go get github.com/gagliardetto/solana-go
# Clone and generate the proto client
git clone https://github.com/nextblock-ag/nextblock-proto
# Follow the Go generation instructions in the proto repo

Example

package main

import (
    "context"
    "crypto/x509"
    "fmt"
    "log"
    "sync"
    "time"

    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials"
    "google.golang.org/grpc/credentials/insecure"
    "google.golang.org/grpc/keepalive"
    // Import your generated proto client here
    // "path/to/your/generated/api"
)

// Available NextBlock endpoints
const (
    FrankfurtEndpoint = "frankfurt.nextblock.io:443"
    AmsterdamEndpoint = "amsterdam.nextblock.io:443"
    LondonEndpoint    = "london.nextblock.io:443"
    SingaporeEndpoint = "singapore.nextblock.io:443"
    NewYorkEndpoint   = "ny.nextblock.io:443"
    SaltLakeEndpoint  = "slc.nextblock.io:443"
    TokyoEndpoint     = "tokyo.nextblock.io:443"
)

// API key credentials for authentication
type ApiKeyCredentials struct {
    apiKey string
}

func NewApiKeyCredentials(apiKey string) *ApiKeyCredentials {
    return &ApiKeyCredentials{apiKey: apiKey}
}

func (a *ApiKeyCredentials) RequireTransportSecurity() bool {
    return false
}

func (a *ApiKeyCredentials) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
    return map[string]string{
        "authorization": a.apiKey,
    }, nil
}

// Tip floor data structure
type TipFloorData struct {
    Time                         string
    LandedTips25thPercentile     float64
    LandedTips50thPercentile     float64
    LandedTips75thPercentile     float64
    LandedTips95thPercentile     float64
    LandedTips99thPercentile     float64
    EMALandedTips50thPercentile  float64
}

// Tip strategy for dynamic tip calculation
type TipStrategy struct {
    mu             sync.RWMutex
    conservativeTip uint64    // 25th percentile in lamports
    normalTip      uint64    // 50th percentile in lamports
    aggressiveTip  uint64    // 75th percentile in lamports
    priorityTip    uint64    // 95th percentile in lamports
    lastUpdated    time.Time
}

func NewTipStrategy() *TipStrategy {
    return &TipStrategy{
        conservativeTip: 0,   // Will be set from tip floor data
        normalTip:      0,    // Will be set from tip floor data
        aggressiveTip:  0,    // Will be set from tip floor data
        priorityTip:    0,    // Will be set from tip floor data
        lastUpdated:    time.Now(),
    }
}

func (ts *TipStrategy) UpdateFromTipFloor(tipFloor *TipFloorData) {
    ts.mu.Lock()
    defer ts.mu.Unlock()
    
    ts.conservativeTip = solToLamports(tipFloor.LandedTips25thPercentile)
    ts.normalTip = solToLamports(tipFloor.LandedTips50thPercentile)
    ts.aggressiveTip = solToLamports(tipFloor.LandedTips75thPercentile)
    ts.priorityTip = solToLamports(tipFloor.LandedTips95thPercentile)
    ts.lastUpdated = time.Now()
}

func (ts *TipStrategy) GetTip(priority string) uint64 {
    ts.mu.RLock()
    defer ts.mu.RUnlock()
    
    switch priority {
    case "conservative":
        return ts.conservativeTip
    case "normal":
        return ts.normalTip
    case "aggressive":
        return ts.aggressiveTip
    case "priority":
        return ts.priorityTip
    default:
        return ts.normalTip
    }
}

func solToLamports(sol float64) uint64 {
    return uint64(sol * 1_000_000_000)
}

// Connection setup with authentication
func connectToNextblock(endpoint, apiKey string, useTLS bool) (*grpc.ClientConn, error) {
    var opts []grpc.DialOption
    
    // Configure transport credentials
    if useTLS {
        pool, err := x509.SystemCertPool()
        if err != nil {
            return nil, fmt.Errorf("failed to get system cert pool: %w", err)
        }
        creds := credentials.NewClientTLSFromCert(pool, "")
        opts = append(opts, grpc.WithTransportCredentials(creds))
    } else {
        opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials()))
    }

    // Keep-alive parameters
    kacp := keepalive.ClientParameters{
        Time:                time.Minute,
        Timeout:             15 * time.Second,
        PermitWithoutStream: true,
    }
    opts = append(opts, grpc.WithKeepaliveParams(kacp))
    
    // Add API key authentication
    opts = append(opts, grpc.WithPerRPCCredentials(NewApiKeyCredentials(apiKey)))

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

    return conn, nil
}

// Stream tip floor data with complete error handling
func streamTipFloor(endpoint, apiKey string, tipStrategy *TipStrategy) error {
    fmt.Printf("Connecting to NextBlock at %s...\n", endpoint)
    
    // Establish connection
    conn, err := connectToNextblock(endpoint, apiKey, false)
    if err != nil {
        return fmt.Errorf("connection failed: %w", err)
    }
    defer conn.Close()

    // Create API client
    // nextblockApiClient := api.NewApiClient(conn)
    
    fmt.Println("Successfully connected! Starting tip floor stream...")

    /* Uncomment when you have the generated API client
    // Create streaming request
    sub, err := nextblockApiClient.StreamTipFloor(context.TODO(), &api.TipFloorStreamRequest{
        UpdateFrequency: "1m", // Update every minute
    })
    if err != nil {
        return fmt.Errorf("failed to start tip floor stream: %w", err)
    }

    fmt.Println("Streaming tip floor data (Ctrl+C to stop):")
    
    for {
        tipFloorResponse, err := sub.Recv()
        if err != nil {
            return fmt.Errorf("stream error: %w", err)
        }

        // Convert protobuf response to our struct
        tipFloor := &TipFloorData{
            Time:                         tipFloorResponse.Time,
            LandedTips25thPercentile:     tipFloorResponse.LandedTips25ThPercentile,
            LandedTips50thPercentile:     tipFloorResponse.LandedTips50ThPercentile,
            LandedTips75thPercentile:     tipFloorResponse.LandedTips75ThPercentile,
            LandedTips95thPercentile:     tipFloorResponse.LandedTips95ThPercentile,
            LandedTips99thPercentile:     tipFloorResponse.LandedTips99ThPercentile,
            EMALandedTips50thPercentile:  tipFloorResponse.EmaLandedTips50ThPercentile,
        }

        // Process the tip floor update
        processTipFloorUpdate(tipFloor, tipStrategy)
    }
    */
    
    // Mock streaming for demonstration
    fmt.Println("Mock tip floor streaming (Ctrl+C to stop):")
    ticker := time.NewTicker(time.Minute)
    defer ticker.Stop()
    
    for {
        select {
        case <-ticker.C:
            // Generate mock tip floor data
            mockTipFloor := &TipFloorData{
                Time:                         time.Now().Format(time.RFC3339),
                LandedTips25thPercentile:     0.0011,
                LandedTips50thPercentile:     0.005000001,
                LandedTips75thPercentile:     0.01555,
                LandedTips95thPercentile:     0.09339195639999975,
                LandedTips99thPercentile:     0.4846427910400001,
                EMALandedTips50thPercentile:  0.005989477267191758,
            }
            
            processTipFloorUpdate(mockTipFloor, tipStrategy)
        }
    }
}

// Process tip floor updates and update strategy
func processTipFloorUpdate(tipFloor *TipFloorData, tipStrategy *TipStrategy) {
    fmt.Printf("\n=== Tip Floor Update at %s ===\n", tipFloor.Time)
    fmt.Printf("25th percentile: %.6f SOL\n", tipFloor.LandedTips25thPercentile)
    fmt.Printf("50th percentile: %.6f SOL\n", tipFloor.LandedTips50thPercentile)
    fmt.Printf("75th percentile: %.6f SOL\n", tipFloor.LandedTips75thPercentile)
    fmt.Printf("95th percentile: %.6f SOL\n", tipFloor.LandedTips95thPercentile)
    fmt.Printf("99th percentile: %.6f SOL\n", tipFloor.LandedTips99thPercentile)
    fmt.Printf("EMA 50th percentile: %.6f SOL\n", tipFloor.EMALandedTips50thPercentile)
    
    // Update tip strategy
    tipStrategy.UpdateFromTipFloor(tipFloor)
    
    // Display updated tip recommendations
    fmt.Printf("\n--- Updated Tip Recommendations ---\n")
    fmt.Printf("Conservative: %d lamports (low priority)\n", 
        tipStrategy.GetTip("conservative"))
    fmt.Printf("Normal:       %d lamports (standard priority)\n", 
        tipStrategy.GetTip("normal"))
    fmt.Printf("Aggressive:   %d lamports (high priority)\n", 
        tipStrategy.GetTip("aggressive"))
    fmt.Printf("Priority:     %d lamports (highest priority)\n", 
        tipStrategy.GetTip("priority"))
    fmt.Println("=====================================")
}

// Example of using dynamic tips in transaction submission
func exampleTransactionWithDynamicTip(tipStrategy *TipStrategy) {
    // Get current optimal tip based on desired priority level
    currentTip := tipStrategy.GetTip("normal")
    
    fmt.Printf("\nExample: Using dynamic tip based on current tip floor data\n")
    fmt.Printf("Current tip for normal priority: %d lamports\n", currentTip)
    fmt.Printf("Tip automatically adapts to network conditions\n")
    fmt.Printf("Higher tips = higher transaction priority\n")
    
    // Here you would use this tip amount in your transaction building
    // See submit-single-transactions.md for complete transaction examples
}

// Main function demonstrating complete usage
func main() {
    // Configuration
    apiKey := "<your-api-key-here>"  // Replace with your actual API key
    endpoint := FrankfurtEndpoint              // Choose your preferred endpoint
    
    if apiKey == "<your-api-key-here>" {
        log.Fatal("Please set your API key in the code")
    }
    
    // Initialize tip strategy
    tipStrategy := NewTipStrategy()
    
    // Start a goroutine to demonstrate using dynamic tips
    go func() {
        ticker := time.NewTicker(2 * time.Minute)
        defer ticker.Stop()
        
        for range ticker.C {
            exampleTransactionWithDynamicTip(tipStrategy)
        }
    }()
    
    // Start streaming tip floor data
    if err := streamTipFloor(endpoint, apiKey, tipStrategy); err != nil {
        log.Fatalf("Tip floor streaming failed: %v", err)
    }
}

Advanced Usage with Multiple Endpoints

// Connect to multiple endpoints for redundancy
func streamFromMultipleEndpoints(apiKey string, tipStrategy *TipStrategy) {
    endpoints := []string{
        FrankfurtEndpoint,
        AmsterdamEndpoint,
        NewYorkEndpoint,
    }
    
    var wg sync.WaitGroup
    
    for _, endpoint := range endpoints {
        wg.Add(1)
        go func(ep string) {
            defer wg.Done()
            
            fmt.Printf("Starting stream from %s\n", ep)
            if err := streamTipFloor(ep, apiKey, tipStrategy); err != nil {
                log.Printf("Stream from %s failed: %v", ep, err)
            }
        }(endpoint)
    }
    
    wg.Wait()
}

// Tip history tracking for trend analysis
type TipHistory struct {
    mu      sync.RWMutex
    history []TipFloorData
    maxSize int
}

func NewTipHistory(maxSize int) *TipHistory {
    return &TipHistory{
        history: make([]TipFloorData, 0, maxSize),
        maxSize: maxSize,
    }
}

func (th *TipHistory) Add(tipFloor TipFloorData) {
    th.mu.Lock()
    defer th.mu.Unlock()
    
    if len(th.history) >= th.maxSize {
        th.history = th.history[1:]
    }
    th.history = append(th.history, tipFloor)
}

func (th *TipHistory) GetTrend() float64 {
    th.mu.RLock()
    defer th.mu.RUnlock()
    
    if len(th.history) < 2 {
        return 0.0
    }
    
    recent := th.history[len(th.history)-1]
    older := th.history[len(th.history)-2]
    
    return recent.LandedTips50thPercentile - older.LandedTips50thPercentile
}

// Smart tip calculation with trend analysis
func (ts *TipStrategy) GetSmartTip(priority string, tipHistory *TipHistory) uint64 {
    baseTip := ts.GetTip(priority)
    trend := tipHistory.GetTrend()
    
    // Adjust tip based on trend
    adjustmentFactor := 1.0
    if trend > 0.001 { // Tips increasing
        adjustmentFactor = 1.2
        fmt.Printf("Tips trending up (+%.6f), increasing tip by 20%%\n", trend)
    } else if trend < -0.001 { // Tips decreasing
        adjustmentFactor = 0.9
        fmt.Printf("Tips trending down (%.6f), decreasing tip by 10%%\n", trend)
    }
    
    smartTip := uint64(float64(baseTip) * adjustmentFactor)
    return smartTip
}

func max(a, b uint64) uint64 {
    if a > b {
        return a
    }
    return b
}

Available Endpoints

Choose the endpoint closest to your location for optimal latency:

  • Frankfurt: frankfurt.nextblock.io:443 (Europe)

  • Amsterdam: amsterdam.nextblock.io:443 (Europe)

  • London: london.nextblock.io:443 (Europe)

  • Singapore: singapore.nextblock.io:443 (Asia)

  • Tokyo: tokyo.nextblock.io:443 (Asia)

  • New York: ny.nextblock.io:443 (US East)

  • Salt Lake City: slc.nextblock.io:443 (US West)

Best Practices

  1. Choose optimal endpoint: Use the endpoint closest to your location for best performance

  2. Handle connection failures: Implement retry logic and fallback endpoints

  3. Monitor tip trends: Use historical data to make smarter tip decisions

  4. Update frequently: Stream tip floor data continuously for best results

  5. Adapt tips dynamically: Adjust your tip amounts based on current network conditions - higher tips yield higher priority

  6. Match priority to urgency: Use appropriate tip levels based on your transaction urgency needs

  7. Implement graceful shutdown: Handle interruption signals properly

  8. Log important events: Track tip floor updates and connection issues

Last updated