Tip Floor Stream

Stream real-time tip floor data from NextBlock to optimize your transaction tips dynamically.

Streaming Tip Floor Data

use tokio_stream::StreamExt;
use std::time::Duration;
use serde::{Deserialize, Serialize};

// Tip floor response structure
#[derive(Debug, Deserialize, Serialize)]
pub struct TipFloorData {
    pub time: String,
    pub landed_tips_25th_percentile: f64,
    pub landed_tips_50th_percentile: f64,
    pub landed_tips_75th_percentile: f64,
    pub landed_tips_95th_percentile: f64,
    pub landed_tips_99th_percentile: f64,
    pub ema_landed_tips_50th_percentile: f64,
}

// Stream tip floor updates
async fn stream_tip_floor(
    // nextblock_client: &mut YourGeneratedApiClient,
    update_frequency: String,
) -> Result<(), Box<dyn std::error::Error>> {
    println!("Starting tip floor stream with frequency: {}", update_frequency);
    
    // Create streaming request
    /* Uncomment when you have the generated API client
    let request = tonic::Request::new(TipFloorStreamRequest {
        update_frequency,
    });
    
    let mut stream = nextblock_client
        .stream_tip_floor(request)
        .await?
        .into_inner();
    
    println!("Streaming tip floor data:");
    
    while let Some(tip_floor_response) = stream.next().await {
        match tip_floor_response {
            Ok(tip_floor) => {
                println!("Received tip floor update:");
                println!("  Time: {}", tip_floor.time);
                println!("  25th percentile: {:.6} SOL", tip_floor.landed_tips_25th_percentile);
                println!("  50th percentile: {:.6} SOL", tip_floor.landed_tips_50th_percentile);
                println!("  75th percentile: {:.6} SOL", tip_floor.landed_tips_75th_percentile);
                println!("  95th percentile: {:.6} SOL", tip_floor.landed_tips_95th_percentile);
                println!("  99th percentile: {:.6} SOL", tip_floor.landed_tips_99th_percentile);
                println!("  EMA 50th percentile: {:.6} SOL", tip_floor.ema_landed_tips_50th_percentile);
                println!("  ---");
                
                // Process the tip floor data
                process_tip_floor_update(&tip_floor).await?;
            }
            Err(e) => {
                eprintln!("Stream error: {}", e);
                // Implement reconnection logic here
                break;
            }
        }
    }
    */
    
    // Mock streaming for demonstration
    println!("Mock tip floor streaming started...");
    let mut interval = tokio::time::interval(Duration::from_secs(60));
    
    loop {
        interval.tick().await;
        let mock_tip_floor = TipFloorData {
            time: chrono::Utc::now().to_rfc3339(),
            landed_tips_25th_percentile: 0.0011,
            landed_tips_50th_percentile: 0.005000001,
            landed_tips_75th_percentile: 0.01555,
            landed_tips_95th_percentile: 0.09339195639999975,
            landed_tips_99th_percentile: 0.4846427910400001,
            ema_landed_tips_50th_percentile: 0.005989477267191758,
        };
        
        println!("Mock tip floor update: {:#?}", mock_tip_floor);
        process_tip_floor_update(&mock_tip_floor).await?;
    }
}

// Process tip floor updates
async fn process_tip_floor_update(
    tip_floor: &TipFloorData,
) -> Result<(), Box<dyn std::error::Error>> {
    // Update global tip strategy
    update_tip_strategy(tip_floor).await?;
    
    // Optionally store historical data
    store_tip_floor_data(tip_floor).await?;
    
    // Trigger any pending transactions with updated tips
    trigger_pending_transactions().await?;
    
    Ok(())
}

Dynamic Tip Calculation

use std::sync::Arc;
use tokio::sync::RwLock;

// Global tip strategy state
#[derive(Debug, Clone)]
pub struct TipStrategy {
    pub conservative_tip: u64,  // 25th percentile
    pub normal_tip: u64,        // 50th percentile  
    pub aggressive_tip: u64,    // 75th percentile
    pub priority_tip: u64,      // 95th percentile
    pub last_updated: chrono::DateTime<chrono::Utc>,
}

impl Default for TipStrategy {
    fn default() -> Self {
        Self {
            conservative_tip: 500_000,   // 0.0005 SOL
            normal_tip: 1_000_000,      // 0.001 SOL
            aggressive_tip: 2_000_000,  // 0.002 SOL
            priority_tip: 5_000_000,    // 0.005 SOL
            last_updated: chrono::Utc::now(),
        }
    }
}

// Global tip strategy instance
static TIP_STRATEGY: once_cell::sync::Lazy<Arc<RwLock<TipStrategy>>> = 
    once_cell::sync::Lazy::new(|| Arc::new(RwLock::new(TipStrategy::default())));

// Convert SOL to lamports
fn sol_to_lamports(sol: f64) -> u64 {
    (sol * 1_000_000_000.0) as u64
}

// Update tip strategy based on tip floor data
async fn update_tip_strategy(
    tip_floor: &TipFloorData,
) -> Result<(), Box<dyn std::error::Error>> {
    let mut strategy = TIP_STRATEGY.write().await;
    
    strategy.conservative_tip = sol_to_lamports(tip_floor.landed_tips_25th_percentile);
    strategy.normal_tip = sol_to_lamports(tip_floor.landed_tips_50th_percentile);
    strategy.aggressive_tip = sol_to_lamports(tip_floor.landed_tips_75th_percentile);
    strategy.priority_tip = sol_to_lamports(tip_floor.landed_tips_95th_percentile);
    strategy.last_updated = chrono::Utc::now();
    
    println!("Updated tip strategy:");
    println!("  Conservative: {} lamports ({:.6} SOL)", 
             strategy.conservative_tip, tip_floor.landed_tips_25th_percentile);
    println!("  Normal: {} lamports ({:.6} SOL)", 
             strategy.normal_tip, tip_floor.landed_tips_50th_percentile);
    println!("  Aggressive: {} lamports ({:.6} SOL)", 
             strategy.aggressive_tip, tip_floor.landed_tips_75th_percentile);
    println!("  Priority: {} lamports ({:.6} SOL)", 
             strategy.priority_tip, tip_floor.landed_tips_95th_percentile);
    
    Ok(())
}

// Get optimal tip for transaction priority
pub async fn get_optimal_tip(priority: TipPriority) -> u64 {
    let strategy = TIP_STRATEGY.read().await;
    
    match priority {
        TipPriority::Conservative => strategy.conservative_tip,
        TipPriority::Normal => strategy.normal_tip,
        TipPriority::Aggressive => strategy.aggressive_tip,
        TipPriority::Priority => strategy.priority_tip,
    }
}

// Tip priority levels
#[derive(Debug, Clone, Copy)]
pub enum TipPriority {
    Conservative, // 25th percentile - cheapest option
    Normal,       // 50th percentile - balanced option
    Aggressive,   // 75th percentile - faster execution
    Priority,     // 95th percentile - highest priority
}

Advanced Tip Management

use std::collections::VecDeque;

// Historical tip data for trend analysis
#[derive(Debug)]
pub struct TipHistory {
    data: VecDeque<TipFloorData>,
    max_size: usize,
}

impl TipHistory {
    pub fn new(max_size: usize) -> Self {
        Self {
            data: VecDeque::with_capacity(max_size),
            max_size,
        }
    }
    
    pub fn add(&mut self, tip_floor: TipFloorData) {
        if self.data.len() >= self.max_size {
            self.data.pop_front();
        }
        self.data.push_back(tip_floor);
    }
    
    // Calculate trend (increasing/decreasing tips)
    pub fn calculate_trend(&self) -> f64 {
        if self.data.len() < 2 {
            return 0.0;
        }
        
        let recent = &self.data[self.data.len() - 1];
        let older = &self.data[self.data.len() - 2];
        
        recent.landed_tips_50th_percentile - older.landed_tips_50th_percentile
    }
    
    // Get average tip over time window
    pub fn get_average_tip(&self, percentile: &str) -> f64 {
        if self.data.is_empty() {
            return 0.0;
        }
        
        let sum: f64 = self.data.iter().map(|d| {
            match percentile {
                "25th" => d.landed_tips_25th_percentile,
                "50th" => d.landed_tips_50th_percentile,
                "75th" => d.landed_tips_75th_percentile,
                "95th" => d.landed_tips_95th_percentile,
                _ => d.landed_tips_50th_percentile,
            }
        }).sum();
        
        sum / self.data.len() as f64
    }
}

// Smart tip calculation with trend analysis
pub async fn get_smart_tip(
    base_priority: TipPriority,
    tip_history: &TipHistory,
) -> u64 {
    let base_tip = get_optimal_tip(base_priority).await;
    let trend = tip_history.calculate_trend();
    
    // Adjust tip based on trend
    let adjustment_factor = if trend > 0.001 {
        1.2 // Tips are increasing, be more aggressive
    } else if trend < -0.001 {
        0.9 // Tips are decreasing, can be more conservative
    } else {
        1.0 // No significant trend
    };
    
    ((base_tip as f64) * adjustment_factor) as u64
}

Store Historical Data

// Store tip floor data for analysis
async fn store_tip_floor_data(
    tip_floor: &TipFloorData,
) -> Result<(), Box<dyn std::error::Error>> {
    // Example: Store to file (in production, use a database)
    let data_dir = std::path::Path::new("./tip_data");
    if !data_dir.exists() {
        tokio::fs::create_dir_all(data_dir).await?;
    }
    
    let filename = format!("tip_floor_{}.json", 
                          chrono::Utc::now().format("%Y%m%d"));
    let filepath = data_dir.join(filename);
    
    // Append to daily file
    let json_line = format!("{}\n", serde_json::to_string(tip_floor)?);
    tokio::fs::write(&filepath, json_line).await?;
    
    println!("Stored tip floor data to {:?}", filepath);
    Ok(())
}

// Trigger pending transactions with updated tips
async fn trigger_pending_transactions() -> Result<(), Box<dyn std::error::Error>> {
    // This would check for any pending transactions that are waiting
    // for better tip conditions and submit them with updated tips
    println!("Checking for pending transactions to trigger...");
    
    // Example: Check if any transactions are waiting for lower tips
    // and submit them now if conditions are favorable
    
    Ok(())
}

Usage Example

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Connect to NextBlock (see connection.md)
    // let config = NextBlockConfig::default();
    // let mut nextblock_client = create_nextblock_client(config).await?;
    
    // Start tip floor streaming in background
    let stream_handle = tokio::spawn(async move {
        if let Err(e) = stream_tip_floor(
            // &mut nextblock_client,
            "1m".to_string(), // Update every minute
        ).await {
            eprintln!("Tip floor streaming error: {}", e);
        }
    });
    
    // Example: Use dynamic tips in transaction submission
    tokio::time::sleep(Duration::from_secs(5)).await; // Wait for initial data
    
    let conservative_tip = get_optimal_tip(TipPriority::Conservative).await;
    let normal_tip = get_optimal_tip(TipPriority::Normal).await;
    let aggressive_tip = get_optimal_tip(TipPriority::Aggressive).await;
    
    println!("Current optimal tips:");
    println!("  Conservative: {} lamports", conservative_tip);
    println!("  Normal: {} lamports", normal_tip);
    println!("  Aggressive: {} lamports", aggressive_tip);
    
    // Use these tips in your transaction submissions
    // submit_transaction_with_tip(normal_tip).await?;
    
    // Keep the stream running
    stream_handle.await??;
    
    Ok(())
}

Best Practices

  1. Update frequency: Use 1-5 minute intervals for most applications

  2. Trend analysis: Consider tip trends when calculating optimal amounts

  3. Fallback values: Always have default tip values in case streaming fails

  4. Historical data: Store tip floor data for analysis and optimization

  5. Priority levels: Use different tip strategies for different transaction types

  6. Error handling: Implement reconnection logic for stream interruptions

  7. Resource management: Limit historical data storage to prevent memory issues

Last updated