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
Choose optimal endpoint: Use the endpoint closest to your location for best performance
Handle connection failures: Implement retry logic and fallback endpoints
Monitor tip trends: Use historical data to make smarter tip decisions
Update frequently: Stream tip floor data continuously for best results
Adapt tips dynamically: Adjust your tip amounts based on current network conditions - higher tips yield higher priority
Match priority to urgency: Use appropriate tip levels based on your transaction urgency needs
Implement graceful shutdown: Handle interruption signals properly
Log important events: Track tip floor updates and connection issues
Last updated