1#[allow(deprecated)]
11use solana_sdk::{
12 instruction::Instruction,
13 pubkey::Pubkey,
14 system_instruction,
15 transaction::VersionedTransaction,
16};
17use std::collections::HashMap;
18
19pub fn block_engine_urls() -> HashMap<&'static str, &'static str> {
25 let mut m = HashMap::new();
26 m.insert("mainnet", "https://mainnet.block-engine.jito.wtf");
27 m.insert("amsterdam", "https://amsterdam.mainnet.block-engine.jito.wtf");
28 m.insert("frankfurt", "https://frankfurt.mainnet.block-engine.jito.wtf");
29 m.insert("ny", "https://ny.mainnet.block-engine.jito.wtf");
30 m.insert("tokyo", "https://tokyo.mainnet.block-engine.jito.wtf");
31 m.insert("slc", "https://slc.mainnet.block-engine.jito.wtf");
32 m
33}
34
35pub const JITO_TIP_ACCOUNTS: [&str; 8] = [
37 "96gYZGLnJYVFmbjzopPSU6QiEV5fGqZNyN9nmNhvrZU5",
38 "HFqU5x63VTqvQss8hp11i4wVV8bD44PvwucfZ2bU7gRe",
39 "Cw8CFyM9FkoMi7K7Crf6HNQqf4uEMzpKw6QNghXLvLkY",
40 "ADaUMid9yfUytqMBgopwjb2DTLSokTSzL1zt6iGPaS49",
41 "DfXygSm4jCyNCzbzYAKhb58Pi6BteBuKVjBJhZSLQndT",
42 "ADuUkR4vqLUMWXxW9gh6D6L8pMSawimctcNZ5pGwDcEt",
43 "DttWaMuVvTiduCN3AwnFnBbEG9HshVEy7BkH6V1RB2oz",
44 "3AVi9Tg9Uo68tJfuvoKvqKNWKkC5wPdSSdeBnizKZ6jT",
45];
46
47#[derive(Debug, Clone)]
53pub enum TipStrategy {
54 Min,
56 Competitive,
58 Aggressive,
60 Exact(u64),
62}
63
64#[derive(Debug, Clone)]
66pub struct JitoConfig {
67 pub tip: TipStrategy,
69 pub region: String,
71}
72
73impl Default for JitoConfig {
74 fn default() -> Self {
75 Self {
76 tip: TipStrategy::Competitive,
77 region: "mainnet".to_string(),
78 }
79 }
80}
81
82#[derive(Debug, Clone)]
84pub struct JitoBundleResult {
85 pub bundle_id: String,
86 pub signature: Option<String>,
87}
88
89pub fn resolve_block_engine_url(region: &str) -> String {
95 if region.starts_with("http") {
96 return region.to_string();
97 }
98 let urls = block_engine_urls();
99 urls.get(region)
100 .unwrap_or(&"https://mainnet.block-engine.jito.wtf")
101 .to_string()
102}
103
104const MIN_TIP_LAMPORTS: u64 = 1_000;
109const COMPETITIVE_MULTIPLIER: u64 = 3;
110const AGGRESSIVE_TIP_LAMPORTS: u64 = 100_000;
111
112pub fn calculate_tip(strategy: &TipStrategy, floor_lamports: u64) -> u64 {
114 match strategy {
115 TipStrategy::Exact(tip) => (*tip).max(MIN_TIP_LAMPORTS),
116 TipStrategy::Min => floor_lamports.max(MIN_TIP_LAMPORTS),
117 TipStrategy::Competitive => (floor_lamports * COMPETITIVE_MULTIPLIER).max(MIN_TIP_LAMPORTS * 10),
118 TipStrategy::Aggressive => AGGRESSIVE_TIP_LAMPORTS.max(floor_lamports * 5),
119 }
120}
121
122pub fn build_tip_instruction(payer: &Pubkey, tip_lamports: u64) -> Instruction {
124 let idx = (std::time::SystemTime::now()
125 .duration_since(std::time::UNIX_EPOCH)
126 .unwrap()
127 .subsec_nanos() as usize) % JITO_TIP_ACCOUNTS.len();
128 let tip_account: Pubkey = JITO_TIP_ACCOUNTS[idx].parse().unwrap();
129 system_instruction::transfer(payer, &tip_account, tip_lamports)
130}
131
132pub async fn fetch_tip_accounts(block_engine_url: &str) -> Vec<Pubkey> {
139 let client = reqwest::Client::new();
140 let body = serde_json::json!({
141 "jsonrpc": "2.0",
142 "id": 1,
143 "method": "getTipAccounts",
144 "params": []
145 });
146
147 match client.post(&format!("{}/api/v1/bundles", block_engine_url))
148 .json(&body)
149 .send()
150 .await
151 {
152 Ok(res) => {
153 if let Ok(data) = res.json::<serde_json::Value>().await {
154 if let Some(result) = data.get("result").and_then(|r| r.as_array()) {
155 return result.iter()
156 .filter_map(|v| v.as_str()?.parse::<Pubkey>().ok())
157 .collect();
158 }
159 }
160 }
161 Err(_) => {}
162 }
163
164 JITO_TIP_ACCOUNTS.iter()
166 .filter_map(|s| s.parse::<Pubkey>().ok())
167 .collect()
168}
169
170pub async fn send_jito_bundle(
172 block_engine_url: &str,
173 transactions: &[VersionedTransaction],
174) -> Result<String, String> {
175 use base64::Engine;
176
177 let encoded_txs: Vec<String> = transactions.iter()
178 .map(|tx| {
179 let serialized = bincode::serialize(tx).map_err(|e| e.to_string())?;
180 Ok(base64::engine::general_purpose::STANDARD.encode(&serialized))
181 })
182 .collect::<Result<Vec<_>, String>>()?;
183
184 let body = serde_json::json!({
185 "jsonrpc": "2.0",
186 "id": 1,
187 "method": "sendBundle",
188 "params": [encoded_txs, {"encoding": "base64"}]
189 });
190
191 let client = reqwest::Client::new();
192 let res = client.post(&format!("{}/api/v1/bundles", block_engine_url))
193 .json(&body)
194 .send()
195 .await
196 .map_err(|e| format!("Jito send error: {}", e))?;
197
198 let data: serde_json::Value = res.json().await
199 .map_err(|e| format!("Jito parse error: {}", e))?;
200
201 if let Some(error) = data.get("error") {
202 let msg = error.get("message").and_then(|m| m.as_str()).unwrap_or("unknown");
203 return Err(format!("Jito sendBundle error: {}", msg));
204 }
205
206 data.get("result")
207 .and_then(|r| r.as_str())
208 .map(|s| s.to_string())
209 .ok_or_else(|| "No bundle_id in response".to_string())
210}
211
212pub async fn poll_bundle_status(
214 block_engine_url: &str,
215 bundle_id: &str,
216 timeout_ms: u64,
217) -> Result<String, String> {
218 let client = reqwest::Client::new();
219 let start = std::time::Instant::now();
220 let poll_interval = std::time::Duration::from_millis(500);
221
222 while start.elapsed().as_millis() < timeout_ms as u128 {
223 let body = serde_json::json!({
224 "jsonrpc": "2.0",
225 "id": 1,
226 "method": "getBundleStatuses",
227 "params": [[bundle_id]]
228 });
229
230 if let Ok(res) = client.post(&format!("{}/api/v1/bundles", block_engine_url))
231 .json(&body)
232 .send()
233 .await
234 {
235 if let Ok(data) = res.json::<serde_json::Value>().await {
236 if let Some(status) = data
237 .get("result")
238 .and_then(|r| r.get("value"))
239 .and_then(|v| v.as_array())
240 .and_then(|arr| arr.first())
241 {
242 let conf = status.get("confirmation_status")
243 .and_then(|s| s.as_str())
244 .unwrap_or("");
245
246 if conf == "confirmed" || conf == "finalized" {
247 let sig = status.get("transactions")
248 .and_then(|t| t.as_array())
249 .and_then(|arr| arr.first())
250 .and_then(|s| s.as_str())
251 .unwrap_or(bundle_id);
252 return Ok(sig.to_string());
253 }
254
255 if status.get("err").is_some() {
256 return Err(format!("Jito bundle failed: {}", status));
257 }
258 }
259 }
260 }
261
262 tokio::time::sleep(poll_interval).await;
263 }
264
265 Err(format!("Jito bundle {} did not land within {}ms", bundle_id, timeout_ms))
266}