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}