Skip to main content

vaea_flash_sdk/
jito.rs

1/// VAEA Flash — Jito Bundle Integration (Rust)
2///
3/// Send flash loan transactions via Jito Block Engine for:
4/// - Bundle privacy (not in public mempool)
5/// - Auto-calculated tips based on current tip floor
6/// - Atomic execution guarantees
7///
8/// Pure reqwest, zero external dependencies.
9
10#[allow(deprecated)]
11use solana_sdk::{
12    instruction::Instruction,
13    pubkey::Pubkey,
14    system_instruction,
15    transaction::VersionedTransaction,
16};
17use std::collections::HashMap;
18
19// ═══════════════════════════════════════════════════════════
20//  Constants
21// ═══════════════════════════════════════════════════════════
22
23/// Jito Block Engine URLs by region.
24pub 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
35/// Hardcoded Jito tip accounts (from getTipAccounts).
36pub 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// ═══════════════════════════════════════════════════════════
48//  Types
49// ═══════════════════════════════════════════════════════════
50
51/// Tip strategy for Jito bundles.
52#[derive(Debug, Clone)]
53pub enum TipStrategy {
54    /// Tip floor (~1,000-5,000 lamports). Cheapest, lowest priority.
55    Min,
56    /// Tip floor × 3 (~10k-50k lamports). Recommended.
57    Competitive,
58    /// 100,000+ lamports. For high-value opportunities.
59    Aggressive,
60    /// Exact tip in lamports. Full control.
61    Exact(u64),
62}
63
64/// Jito configuration.
65#[derive(Debug, Clone)]
66pub struct JitoConfig {
67    /// Tip strategy (default: Competitive)
68    pub tip: TipStrategy,
69    /// Block Engine region or full URL (default: "mainnet")
70    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/// Result of sending a Jito bundle.
83#[derive(Debug, Clone)]
84pub struct JitoBundleResult {
85    pub bundle_id: String,
86    pub signature: Option<String>,
87}
88
89// ═══════════════════════════════════════════════════════════
90//  URL Resolution
91// ═══════════════════════════════════════════════════════════
92
93/// Resolve a region name to a full Block Engine URL.
94pub 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
104// ═══════════════════════════════════════════════════════════
105//  Tip Calculation
106// ═══════════════════════════════════════════════════════════
107
108const MIN_TIP_LAMPORTS: u64 = 1_000;
109const COMPETITIVE_MULTIPLIER: u64 = 3;
110const AGGRESSIVE_TIP_LAMPORTS: u64 = 100_000;
111
112/// Calculate tip amount in lamports based on strategy.
113pub 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
122/// Build a tip instruction — SystemProgram::transfer to a random Jito tip account.
123pub 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
132// ═══════════════════════════════════════════════════════════
133//  Block Engine API — pure reqwest
134// ═══════════════════════════════════════════════════════════
135
136/// Fetch current tip accounts from Jito Block Engine.
137/// Falls back to hardcoded JITO_TIP_ACCOUNTS on failure.
138pub 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    // Fallback
165    JITO_TIP_ACCOUNTS.iter()
166        .filter_map(|s| s.parse::<Pubkey>().ok())
167        .collect()
168}
169
170/// Send a bundle of signed transactions to the Jito Block Engine.
171pub 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
212/// Poll Jito Block Engine for bundle landing status.
213pub 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}