Tip Floor Stream

Stream real-time tip floor data from NextBlock using JavaScript/TypeScript.

// Tip floor data interface
interface TipFloorData {
  time: string;
  landed_tips_25th_percentile: number;
  landed_tips_50th_percentile: number;
  landed_tips_75th_percentile: number;
  landed_tips_95th_percentile: number;
  landed_tips_99th_percentile: number;
  ema_landed_tips_50th_percentile: number;
}

// Tip strategy management
class TipStrategy {
  conservativeTip: number = 0;     // Will be set from tip floor data
  normalTip: number = 0;          // Will be set from tip floor data
  aggressiveTip: number = 0;      // Will be set from tip floor data
  priorityTip: number = 0;        // Will be set from tip floor data
  lastUpdated?: Date;

  updateFromTipFloor(tipFloor: TipFloorData): void {
    this.conservativeTip = this.solToLamports(tipFloor.landed_tips_25th_percentile);
    this.normalTip = this.solToLamports(tipFloor.landed_tips_50th_percentile);
    this.aggressiveTip = this.solToLamports(tipFloor.landed_tips_75th_percentile);
    this.priorityTip = this.solToLamports(tipFloor.landed_tips_95th_percentile);
    this.lastUpdated = new Date();
  }

  private solToLamports(sol: number): number {
    return Math.floor(sol * 1_000_000_000);
  }

  getTipForPriority(priority: 'conservative' | 'normal' | 'aggressive' | 'priority'): number {
    switch (priority) {
      case 'conservative': return this.conservativeTip;
      case 'normal': return this.normalTip;
      case 'aggressive': return this.aggressiveTip;
      case 'priority': return this.priorityTip;
      default: return this.normalTip;
    }
  }
}

// Global tip strategy instance
const globalTipStrategy = new TipStrategy();

// Stream tip floor data
async function streamTipFloor(
  // nextblockClient: any, // Your generated gRPC client
  updateFrequency: string = '1m',
  callback?: (tipFloor: TipFloorData) => Promise<void> | void
): Promise<void> {
  console.log(`Starting tip floor stream with frequency: ${updateFrequency}`);

  /* Uncomment when you have the generated gRPC client
  try {
    const request = { updateFrequency };
    const stream = nextblockClient.streamTipFloor(request);

    console.log('Streaming tip floor data:');

    for await (const tipFloorResponse of stream) {
      try {
        const tipFloor: TipFloorData = {
          time: tipFloorResponse.time,
          landed_tips_25th_percentile: tipFloorResponse.landed_tips_25th_percentile,
          landed_tips_50th_percentile: tipFloorResponse.landed_tips_50th_percentile,
          landed_tips_75th_percentile: tipFloorResponse.landed_tips_75th_percentile,
          landed_tips_95th_percentile: tipFloorResponse.landed_tips_95th_percentile,
          landed_tips_99th_percentile: tipFloorResponse.landed_tips_99th_percentile,
          ema_landed_tips_50th_percentile: tipFloorResponse.ema_landed_tips_50th_percentile,
        };

        console.log('Received tip floor update:');
        console.log(`  Time: ${tipFloor.time}`);
        console.log(`  25th percentile: ${tipFloor.landed_tips_25th_percentile.toFixed(6)} SOL`);
        console.log(`  50th percentile: ${tipFloor.landed_tips_50th_percentile.toFixed(6)} SOL`);
        console.log(`  75th percentile: ${tipFloor.landed_tips_75th_percentile.toFixed(6)} SOL`);
        console.log(`  95th percentile: ${tipFloor.landed_tips_95th_percentile.toFixed(6)} SOL`);
        console.log(`  EMA 50th percentile: ${tipFloor.ema_landed_tips_50th_percentile.toFixed(6)} SOL`);
        console.log('  ---');

        // Update global tip strategy
        await processTipFloorUpdate(tipFloor);

        // Call custom callback if provided
        if (callback) {
          await callback(tipFloor);
        }

      } catch (error) {
        console.error('Error processing tip floor update:', error);
      }
    }

  } catch (error) {
    console.error('Tip floor stream error:', error);
    // Implement reconnection logic here
  }
  */

  // Mock streaming for demonstration
  console.log('Mock tip floor streaming started...');

  const mockStream = setInterval(async () => {
    // Generate mock tip floor data
    const mockTipFloor: TipFloorData = {
      time: new Date().toISOString(),
      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,
    };

    console.log('Mock tip floor update:', mockTipFloor);
    await processTipFloorUpdate(mockTipFloor);

    if (callback) {
      await callback(mockTipFloor);
    }
  }, 60000); // Update every minute

  // Return a promise that never resolves (keeps streaming)
  return new Promise(() => {
    // Keep the interval running
    process.on('SIGINT', () => {
      clearInterval(mockStream);
      console.log('Tip floor streaming stopped');
      process.exit(0);
    });
  });
}

// Process tip floor updates
async function processTipFloorUpdate(tipFloor: TipFloorData): Promise<void> {
  // Update global strategy
  globalTipStrategy.updateFromTipFloor(tipFloor);

  console.log('Updated tip strategy:');
  console.log(`  Conservative: ${globalTipStrategy.conservativeTip} lamports`);
  console.log(`  Normal: ${globalTipStrategy.normalTip} lamports`);
  console.log(`  Aggressive: ${globalTipStrategy.aggressiveTip} lamports`);
  console.log(`  Priority: ${globalTipStrategy.priorityTip} lamports`);

  // Store historical data
  await storeTipFloorData(tipFloor);

  // Trigger any pending transactions
  await triggerPendingTransactions();
}

// Historical data management
class TipFloorHistory {
  private data: TipFloorData[] = [];
  private readonly maxSize: number;

  constructor(maxSize: number = 1000) {
    this.maxSize = maxSize;
  }

  add(tipFloor: TipFloorData): void {
    if (this.data.length >= this.maxSize) {
      this.data.shift(); // Remove oldest
    }
    this.data.push(tipFloor);
  }

  getTrend(percentile: '25th' | '50th' | '75th' | '95th' = '50th', window: number = 10): number {
    if (this.data.length < 2) return 0;

    const recentData = this.data.slice(-window);
    if (recentData.length < 2) return 0;

    const key = `landed_tips_${percentile}_percentile` as keyof TipFloorData;
    const startValue = recentData[0][key] as number;
    const endValue = recentData[recentData.length - 1][key] as number;

    return endValue - startValue;
  }

  getAverage(percentile: '25th' | '50th' | '75th' | '95th' = '50th', window: number = 10): number {
    if (this.data.length === 0) return 0;

    const recentData = this.data.slice(-window);
    const key = `landed_tips_${percentile}_percentile` as keyof TipFloorData;
    const values = recentData.map(d => d[key] as number);

    return values.reduce((sum, val) => sum + val, 0) / values.length;
  }

  get length(): number {
    return this.data.length;
  }
}

// Global history tracker
const tipFloorHistory = new TipFloorHistory();

// Smart tip calculation with trend analysis
async function getSmartTip(
  basePriority: 'conservative' | 'normal' | 'aggressive' | 'priority' = 'normal',
  considerTrend: boolean = true
): Promise<number> {
  const baseTip = globalTipStrategy.getTipForPriority(basePriority);

  if (!considerTrend || tipFloorHistory.length < 2) {
    return baseTip;
  }

  // Analyze trend
  const trend = tipFloorHistory.getTrend('50th', 5);

  // Adjust tip based on trend
  let adjustmentFactor = 1.0;
  if (trend > 0.001) {
    adjustmentFactor = 1.2;
    console.log(`Tips trending up (+${trend.toFixed(6)}), increasing tip by 20%`);
  } else if (trend < -0.001) {
    adjustmentFactor = 0.9;
    console.log(`Tips trending down (${trend.toFixed(6)}), decreasing tip by 10%`);
  } else {
    console.log(`Tips stable (${trend.toFixed(6)}), no adjustment`);
  }

  const smartTip = Math.floor(baseTip * adjustmentFactor);
  return Math.max(smartTip, 100_000); // Minimum tip of 0.0001 SOL
}

// Store tip floor data
async function storeTipFloorData(tipFloor: TipFloorData): Promise<void> {
  // Add to history
  tipFloorHistory.add(tipFloor);

  // Optionally save to file
  const fs = require('fs').promises;
  const filename = `tip_data_${new Date().toISOString().split('T')[0]}.jsonl`;

  try {
    await fs.appendFile(filename, JSON.stringify(tipFloor) + '\n');
  } catch (error) {
    console.error('Failed to store tip floor data:', error);
  }
}

// Trigger pending transactions
async function triggerPendingTransactions(): Promise<void> {
  console.log('Checking for pending transactions to trigger...');
  // Implementation would check your pending transaction queue
  // and submit them with updated tip amounts
}

// Get current optimal tips
function getCurrentOptimalTips(): {
  conservative: number;
  normal: number;
  aggressive: number;
  priority: number;
} {
  return {
    conservative: globalTipStrategy.getTipForPriority('conservative'),
    normal: globalTipStrategy.getTipForPriority('normal'),
    aggressive: globalTipStrategy.getTipForPriority('aggressive'),
    priority: globalTipStrategy.getTipForPriority('priority'),
  };
}

Usage Examples

async function tipFloorExample() {
  // Connect to NextBlock (see connection.md)
  // const config = configFromEnv();
  // const manager = new NextBlockConnectionManager(config);
  // await manager.connect();
  // const nextblockClient = manager.grpcClient;

  // Custom callback for tip floor updates
  const onTipFloorUpdate = async (tipFloor: TipFloorData) => {
    console.log(`Custom handler: Received update at ${tipFloor.time}`);

    // Example: Trigger high-priority transactions when tips are low
    if (tipFloor.landed_tips_50th_percentile < 0.002) { // Less than 0.002 SOL
      console.log('Tips are low - good time for high-priority transactions!');
      // await submitPriorityTransactions();
    }
  };

  // Start streaming in background
  const streamPromise = streamTipFloor(
    // nextblockClient,
    '1m',
    onTipFloorUpdate
  );

  // Example usage of dynamic tips
  setTimeout(async () => {
    // Get current optimal tips
    const currentTips = getCurrentOptimalTips();
    console.log('Current optimal tips:', currentTips);

    // Get smart tip with trend analysis
    const smartTip = await getSmartTip('normal', true);
    console.log(`Smart tip: ${smartTip} lamports`);

    // Example: Use tips in transaction submission
    // await submitTransactionWithTip(smartTip);

  }, 5000); // Wait for initial data

  // Keep streaming
  try {
    await streamPromise;
  } catch (error) {
    console.error('Streaming error:', error);
  }
}

// Advanced tip management example
async function advancedTipManagement() {
  // Monitor tip trends and adjust strategy
  const monitorTrends = setInterval(async () => {
    if (tipFloorHistory.length >= 10) {
      const trend5min = tipFloorHistory.getTrend('50th', 5);
      const trend10min = tipFloorHistory.getTrend('50th', 10);
      const average = tipFloorHistory.getAverage('50th', 10);

      console.log('Tip Analysis:');
      console.log(`  5-min trend: ${trend5min.toFixed(6)} SOL`);
      console.log(`  10-min trend: ${trend10min.toFixed(6)} SOL`);
      console.log(`  10-min average: ${average.toFixed(6)} SOL`);

      // Adjust strategy based on trends
      if (trend5min > 0.002 && trend10min > 0.001) {
        console.log('Strong upward trend detected - consider higher tips');
      } else if (trend5min < -0.002 && trend10min < -0.001) {
        console.log('Strong downward trend detected - can use lower tips');
      }
    }
  }, 300000); // Check every 5 minutes

  // Clean up on exit
  process.on('SIGINT', () => {
    clearInterval(monitorTrends);
    console.log('Advanced tip management stopped');
    process.exit(0);
  });
}

// Main example runner
async function main() {
  console.log('NextBlock Tip Floor Streaming Examples');

  try {
    // Start tip floor streaming
    console.log('Starting tip floor streaming...');
    
    // Run both examples concurrently
    await Promise.all([
      tipFloorExample(),
      advancedTipManagement()
    ]);

  } catch (error) {
    console.error('Error in main:', error);
  }
}

if (require.main === module) {
  main().catch(console.error);
}

export {
  TipFloorData,
  TipStrategy,
  TipFloorHistory,
  streamTipFloor,
  getSmartTip,
  getCurrentOptimalTips,
  globalTipStrategy,
};

Last updated