jito-bundle 0.1.5

Rust client for Jito bundle
Documentation
use crate::config::tip_strategy::TipStrategy;
use crate::constants::{JITO_TIP_ACCOUNTS, SYSTEM_PROGRAM_ID};
use crate::error::JitoError;
use crate::types::JitoTipFloorResponse;
use rand::Rng;
use reqwest::Client;
use solana_instruction::{AccountMeta, Instruction};
use solana_pubkey::Pubkey;

pub struct TipHelper;

impl TipHelper {
    const LAMPORTS_PER_SOL: f64 = 1_000_000_000.0;

    // --- Tip Account / Instruction ---
    /// Picks a random Jito tip account from known constants.
    pub fn get_random_tip_account() -> Pubkey {
        let index = rand::rng().random_range(0..JITO_TIP_ACCOUNTS.len());
        JITO_TIP_ACCOUNTS[index]
    }

    /// Creates a system transfer instruction for the tip payment.
    pub fn create_tip_instruction_to(
        payer: &Pubkey,
        tip_account: &Pubkey,
        tip_lamports: u64,
    ) -> Instruction {
        let mut data = vec![2, 0, 0, 0];
        data.extend_from_slice(&tip_lamports.to_le_bytes());
        Instruction {
            program_id: SYSTEM_PROGRAM_ID,
            accounts: vec![
                AccountMeta::new(*payer, true),
                AccountMeta::new(*tip_account, false),
            ],
            data,
        }
    }

    // --- Tip Resolution ---
    /// Fetches current tip floor data and returns computed lamports.
    pub async fn fetch_tip_floor(
        client: &Client,
        tip_floor_url: &str,
    ) -> Result<(u64, JitoTipFloorResponse), JitoError> {
        let response = client
            .get(tip_floor_url)
            .header("Content-Type", "application/json")
            .send()
            .await
            .map_err(|e| JitoError::TipFloorFetchFailed {
                reason: e.to_string(),
            })?;

        if !response.status().is_success() {
            return Err(JitoError::TipFloorFetchFailed {
                reason: format!("HTTP {}", response.status()),
            });
        }

        let data: Vec<JitoTipFloorResponse> =
            response
                .json()
                .await
                .map_err(|e| JitoError::TipFloorFetchFailed {
                    reason: format!("failed to parse response: {e}"),
                })?;

        let tip_data = data.first().ok_or_else(|| JitoError::TipFloorFetchFailed {
            reason: "tip_floor returned an empty array".to_string(),
        })?;

        let tip_in_lamports = Self::compute_tip_floor_lamports(tip_data)?;

        Ok((tip_in_lamports, tip_data.clone()))
    }

    /// Resolves effective tip amount for the provided strategy.
    pub async fn resolve_tip(
        client: &Client,
        tip_floor_url: &str,
        strategy: &TipStrategy,
    ) -> Result<u64, JitoError> {
        match strategy {
            TipStrategy::Fixed(lamports) => Ok(*lamports),
            TipStrategy::FetchFloor => {
                let (tip, _) = Self::fetch_tip_floor(client, tip_floor_url).await?;
                Ok(Self::apply_floor_strategy(tip, strategy))
            }
            TipStrategy::FetchFloorWithCap { min, max } => {
                let (tip, _) = Self::fetch_tip_floor(client, tip_floor_url).await?;
                Ok(Self::apply_floor_strategy(
                    tip,
                    &TipStrategy::FetchFloorWithCap {
                        min: *min,
                        max: *max,
                    },
                ))
            }
        }
    }

    /// Converts EMA 50th percentile SOL tip value into lamports.
    fn compute_tip_floor_lamports(tip_data: &JitoTipFloorResponse) -> Result<u64, JitoError> {
        let ema_50th = tip_data.ema_landed_tips_50th_percentile;
        if !ema_50th.is_finite() {
            return Err(JitoError::TipFloorFetchFailed {
                reason: format!("invalid tip floor value (non-finite): {ema_50th}"),
            });
        }
        if ema_50th < 0.0 {
            return Err(JitoError::TipFloorFetchFailed {
                reason: format!("invalid tip floor value (negative): {ema_50th}"),
            });
        }

        let tip_float = (ema_50th * Self::LAMPORTS_PER_SOL).ceil();
        if !tip_float.is_finite() || tip_float > u64::MAX as f64 {
            return Err(JitoError::TipFloorFetchFailed {
                reason: format!("tip floor value is out of range: {ema_50th} SOL"),
            });
        }

        Ok(tip_float as u64)
    }

    /// Applies strategy transforms such as min/max clamping.
    fn apply_floor_strategy(tip: u64, strategy: &TipStrategy) -> u64 {
        match strategy {
            TipStrategy::Fixed(lamports) => *lamports,
            TipStrategy::FetchFloor => tip,
            TipStrategy::FetchFloorWithCap { min, max } => tip.clamp(*min, *max),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::constants::{DEFAULT_TIP_LAMPORTS, MAX_TIP_LAMPORTS};

    /// Builds a synthetic tip-floor payload for strategy tests.
    fn make_tip_floor(ema_50th: f64) -> JitoTipFloorResponse {
        JitoTipFloorResponse {
            time: "2024-01-01T00:00:00Z".to_string(),
            landed_tips_25th_percentile: 0.0,
            landed_tips_50th_percentile: 0.0,
            landed_tips_75th_percentile: 0.0,
            landed_tips_95th_percentile: 0.0,
            landed_tips_99th_percentile: 0.0,
            ema_landed_tips_50th_percentile: ema_50th,
        }
    }

    /// Verifies random tip account selection always returns known accounts.
    #[test]
    fn random_tip_account_is_valid() {
        for _ in 0..100 {
            let account = TipHelper::get_random_tip_account();
            assert!(JITO_TIP_ACCOUNTS.contains(&account));
        }
    }

    /// Verifies raw floor strategy does not apply min/max clamping.
    #[test]
    fn fetch_floor_does_not_clamp_by_default() {
        let tip_data = make_tip_floor(20.0);
        let tip = TipHelper::compute_tip_floor_lamports(&tip_data);
        assert!(
            tip.is_ok(),
            "expected valid tip floor conversion, got {tip:?}"
        );
        assert_eq!(tip.unwrap_or_default(), 20_000_000_000);
    }

    /// Verifies invalid floor values return typed errors.
    #[test]
    fn fetch_floor_negative_or_nan_return_error() {
        let negative = make_tip_floor(-0.1);
        assert!(
            TipHelper::compute_tip_floor_lamports(&negative).is_err(),
            "expected negative tip floor to be rejected"
        );

        let nan = make_tip_floor(f64::NAN);
        assert!(
            TipHelper::compute_tip_floor_lamports(&nan).is_err(),
            "expected NaN tip floor to be rejected"
        );
    }

    /// Verifies infinite tip floors are rejected.
    #[test]
    fn fetch_floor_infinite_returns_error() {
        let infinite = make_tip_floor(f64::INFINITY);
        assert!(
            TipHelper::compute_tip_floor_lamports(&infinite).is_err(),
            "expected infinite tip floor to be rejected"
        );
    }

    /// Verifies capped floor strategy applies both min and max bounds.
    #[test]
    fn fetch_floor_with_cap_applies_min_max() {
        let tip = 20_000_000_000;
        let clamped = TipHelper::apply_floor_strategy(
            tip,
            &TipStrategy::FetchFloorWithCap {
                min: DEFAULT_TIP_LAMPORTS,
                max: MAX_TIP_LAMPORTS,
            },
        );
        assert_eq!(clamped, MAX_TIP_LAMPORTS);

        let small_tip = 50_000;
        let clamped = TipHelper::apply_floor_strategy(
            small_tip,
            &TipStrategy::FetchFloorWithCap {
                min: DEFAULT_TIP_LAMPORTS,
                max: MAX_TIP_LAMPORTS,
            },
        );
        assert_eq!(clamped, DEFAULT_TIP_LAMPORTS);
    }
}