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
Update frequency: Use 1-5 minute intervals for most applications
Trend analysis: Consider tip trends when calculating optimal amounts
Fallback values: Always have default tip values in case streaming fails
Historical data: Store tip floor data for analysis and optimization
Priority levels: Use different tip strategies for different transaction types
Error handling: Implement reconnection logic for stream interruptions
Resource management: Limit historical data storage to prevent memory issues
Last updated