1use crate::config::tip_strategy::TipStrategy;
2use crate::constants::{JITO_TIP_ACCOUNTS, SYSTEM_PROGRAM_ID};
3use crate::error::JitoError;
4use crate::types::JitoTipFloorResponse;
5use rand::Rng;
6use reqwest::Client;
7use solana_instruction::{AccountMeta, Instruction};
8use solana_pubkey::Pubkey;
9
10pub struct TipHelper;
11
12impl TipHelper {
13 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(
22 payer: &Pubkey,
23 tip_account: &Pubkey,
24 tip_lamports: u64,
25 ) -> Instruction {
26 let mut data = vec![2, 0, 0, 0];
27 data.extend_from_slice(&tip_lamports.to_le_bytes());
28 Instruction {
29 program_id: SYSTEM_PROGRAM_ID,
30 accounts: vec![
31 AccountMeta::new(*payer, true),
32 AccountMeta::new(*tip_account, false),
33 ],
34 data,
35 }
36 }
37
38 pub async fn fetch_tip_floor(
41 client: &Client,
42 tip_floor_url: &str,
43 ) -> Result<(u64, JitoTipFloorResponse), JitoError> {
44 let response = client
45 .get(tip_floor_url)
46 .header("Content-Type", "application/json")
47 .send()
48 .await
49 .map_err(|e| JitoError::TipFloorFetchFailed {
50 reason: e.to_string(),
51 })?;
52
53 if !response.status().is_success() {
54 return Err(JitoError::TipFloorFetchFailed {
55 reason: format!("HTTP {}", response.status()),
56 });
57 }
58
59 let data: Vec<JitoTipFloorResponse> =
60 response
61 .json()
62 .await
63 .map_err(|e| JitoError::TipFloorFetchFailed {
64 reason: format!("failed to parse response: {e}"),
65 })?;
66
67 let tip_data = data.first().ok_or_else(|| JitoError::TipFloorFetchFailed {
68 reason: "tip_floor returned an empty array".to_string(),
69 })?;
70
71 let tip_in_lamports = Self::compute_tip_floor_lamports(tip_data);
72
73 Ok((tip_in_lamports, tip_data.clone()))
74 }
75
76 pub async fn resolve_tip(
78 client: &Client,
79 tip_floor_url: &str,
80 strategy: &TipStrategy,
81 ) -> Result<u64, JitoError> {
82 match strategy {
83 TipStrategy::Fixed(lamports) => Ok(*lamports),
84 TipStrategy::FetchFloor => {
85 let (tip, _) = Self::fetch_tip_floor(client, tip_floor_url).await?;
86 Ok(Self::apply_floor_strategy(tip, strategy))
87 }
88 TipStrategy::FetchFloorWithCap { min, max } => {
89 let (tip, _) = Self::fetch_tip_floor(client, tip_floor_url).await?;
90 Ok(Self::apply_floor_strategy(
91 tip,
92 &TipStrategy::FetchFloorWithCap {
93 min: *min,
94 max: *max,
95 },
96 ))
97 }
98 }
99 }
100
101 fn compute_tip_floor_lamports(tip_data: &JitoTipFloorResponse) -> u64 {
103 let tip_float = (tip_data.ema_landed_tips_50th_percentile * 1e9).ceil();
104 if tip_float.is_sign_negative() || tip_float.is_nan() {
105 0u64
106 } else {
107 tip_float as u64
108 }
109 }
110
111 fn apply_floor_strategy(tip: u64, strategy: &TipStrategy) -> u64 {
113 match strategy {
114 TipStrategy::Fixed(lamports) => *lamports,
115 TipStrategy::FetchFloor => tip,
116 TipStrategy::FetchFloorWithCap { min, max } => tip.clamp(*min, *max),
117 }
118 }
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124 use crate::constants::{DEFAULT_TIP_LAMPORTS, MAX_TIP_LAMPORTS};
125
126 fn make_tip_floor(ema_50th: f64) -> JitoTipFloorResponse {
128 JitoTipFloorResponse {
129 time: "2024-01-01T00:00:00Z".to_string(),
130 landed_tips_25th_percentile: 0.0,
131 landed_tips_50th_percentile: 0.0,
132 landed_tips_75th_percentile: 0.0,
133 landed_tips_95th_percentile: 0.0,
134 landed_tips_99th_percentile: 0.0,
135 ema_landed_tips_50th_percentile: ema_50th,
136 }
137 }
138
139 #[test]
141 fn random_tip_account_is_valid() {
142 for _ in 0..100 {
143 let account = TipHelper::get_random_tip_account();
144 assert!(JITO_TIP_ACCOUNTS.contains(&account));
145 }
146 }
147
148 #[test]
150 fn fetch_floor_does_not_clamp_by_default() {
151 let tip_data = make_tip_floor(20.0);
152 let tip = TipHelper::compute_tip_floor_lamports(&tip_data);
153 assert_eq!(tip, 20_000_000_000);
154 }
155
156 #[test]
158 fn fetch_floor_negative_or_nan_returns_zero() {
159 let negative = make_tip_floor(-0.1);
160 let tip = TipHelper::compute_tip_floor_lamports(&negative);
161 assert_eq!(tip, 0);
162
163 let nan = make_tip_floor(f64::NAN);
164 let tip = TipHelper::compute_tip_floor_lamports(&nan);
165 assert_eq!(tip, 0);
166 }
167
168 #[test]
170 fn fetch_floor_with_cap_applies_min_max() {
171 let tip = 20_000_000_000;
172 let clamped = TipHelper::apply_floor_strategy(
173 tip,
174 &TipStrategy::FetchFloorWithCap {
175 min: DEFAULT_TIP_LAMPORTS,
176 max: MAX_TIP_LAMPORTS,
177 },
178 );
179 assert_eq!(clamped, MAX_TIP_LAMPORTS);
180
181 let small_tip = 50_000;
182 let clamped = TipHelper::apply_floor_strategy(
183 small_tip,
184 &TipStrategy::FetchFloorWithCap {
185 min: DEFAULT_TIP_LAMPORTS,
186 max: MAX_TIP_LAMPORTS,
187 },
188 );
189 assert_eq!(clamped, DEFAULT_TIP_LAMPORTS);
190 }
191}