Skip to main content

jito_bundle/
tip.rs

1use crate::config::tip_strategy::TipStrategy;
2use crate::constants::{
3    DEFAULT_TIP_LAMPORTS, JITO_TIP_ACCOUNTS, MAX_TIP_LAMPORTS, SYSTEM_PROGRAM_ID,
4};
5use crate::error::JitoError;
6use crate::types::JitoTipFloorResponse;
7use rand::Rng;
8use reqwest::Client;
9use solana_instruction::{AccountMeta, Instruction};
10use solana_pubkey::Pubkey;
11
12pub struct TipHelper;
13
14impl TipHelper {
15    pub fn get_random_tip_account() -> Pubkey {
16        let index = rand::rng().random_range(0..JITO_TIP_ACCOUNTS.len());
17        JITO_TIP_ACCOUNTS[index]
18    }
19
20    pub fn create_tip_instruction_to(
21        payer: &Pubkey,
22        tip_account: &Pubkey,
23        tip_lamports: u64,
24    ) -> Instruction {
25        let mut data = vec![2, 0, 0, 0];
26        data.extend_from_slice(&tip_lamports.to_le_bytes());
27        Instruction {
28            program_id: SYSTEM_PROGRAM_ID,
29            accounts: vec![
30                AccountMeta::new(*payer, true),
31                AccountMeta::new(*tip_account, false),
32            ],
33            data,
34        }
35    }
36
37    pub async fn fetch_tip_floor(
38        client: &Client,
39        tip_floor_url: &str,
40    ) -> Result<(u64, JitoTipFloorResponse), JitoError> {
41        let response = client
42            .get(tip_floor_url)
43            .header("Content-Type", "application/json")
44            .send()
45            .await
46            .map_err(|e| JitoError::TipFloorFetchFailed {
47                reason: e.to_string(),
48            })?;
49
50        if !response.status().is_success() {
51            return Err(JitoError::TipFloorFetchFailed {
52                reason: format!("HTTP {}", response.status()),
53            });
54        }
55
56        let data: Vec<JitoTipFloorResponse> =
57            response
58                .json()
59                .await
60                .map_err(|e| JitoError::TipFloorFetchFailed {
61                    reason: format!("failed to parse response: {e}"),
62                })?;
63
64        let tip_data = data.first().ok_or_else(|| JitoError::TipFloorFetchFailed {
65            reason: "tip_floor returned an empty array".to_string(),
66        })?;
67
68        let tip_float = (tip_data.ema_landed_tips_50th_percentile * 1e9).ceil();
69        let tip_in_lamports = if tip_float.is_sign_negative() || tip_float.is_nan() {
70            0u64
71        } else {
72            tip_float as u64
73        };
74        let final_tip = tip_in_lamports.clamp(DEFAULT_TIP_LAMPORTS, MAX_TIP_LAMPORTS);
75
76        Ok((final_tip, tip_data.clone()))
77    }
78
79    pub async fn resolve_tip(
80        client: &Client,
81        tip_floor_url: &str,
82        strategy: &TipStrategy,
83    ) -> Result<u64, JitoError> {
84        match strategy {
85            TipStrategy::Fixed(lamports) => Ok(*lamports),
86            TipStrategy::FetchFloor => {
87                let (tip, _) = Self::fetch_tip_floor(client, tip_floor_url).await?;
88                Ok(tip)
89            }
90            TipStrategy::FetchFloorWithCap { min, max } => {
91                let (tip, _) = Self::fetch_tip_floor(client, tip_floor_url).await?;
92                Ok(tip.clamp(*min, *max))
93            }
94        }
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101
102    #[test]
103    fn random_tip_account_is_valid() {
104        for _ in 0..100 {
105            let account = TipHelper::get_random_tip_account();
106            assert!(JITO_TIP_ACCOUNTS.contains(&account));
107        }
108    }
109}