vaea-flash-sdk 0.1.0

VAEA Flash — Universal Flash Loan SDK for Solana. Borrow any SPL token atomically in one call.
Documentation
/// VAEA Flash — Jito Bundle Integration (Rust)
///
/// Send flash loan transactions via Jito Block Engine for:
/// - Bundle privacy (not in public mempool)
/// - Auto-calculated tips based on current tip floor
/// - Atomic execution guarantees
///
/// Pure reqwest, zero external dependencies.

#[allow(deprecated)]
use solana_sdk::{
    instruction::Instruction,
    pubkey::Pubkey,
    system_instruction,
    transaction::VersionedTransaction,
};
use std::collections::HashMap;

// ═══════════════════════════════════════════════════════════
//  Constants
// ═══════════════════════════════════════════════════════════

/// Jito Block Engine URLs by region.
pub fn block_engine_urls() -> HashMap<&'static str, &'static str> {
    let mut m = HashMap::new();
    m.insert("mainnet",   "https://mainnet.block-engine.jito.wtf");
    m.insert("amsterdam", "https://amsterdam.mainnet.block-engine.jito.wtf");
    m.insert("frankfurt", "https://frankfurt.mainnet.block-engine.jito.wtf");
    m.insert("ny",        "https://ny.mainnet.block-engine.jito.wtf");
    m.insert("tokyo",     "https://tokyo.mainnet.block-engine.jito.wtf");
    m.insert("slc",       "https://slc.mainnet.block-engine.jito.wtf");
    m
}

/// Hardcoded Jito tip accounts (from getTipAccounts).
pub const JITO_TIP_ACCOUNTS: [&str; 8] = [
    "96gYZGLnJYVFmbjzopPSU6QiEV5fGqZNyN9nmNhvrZU5",
    "HFqU5x63VTqvQss8hp11i4wVV8bD44PvwucfZ2bU7gRe",
    "Cw8CFyM9FkoMi7K7Crf6HNQqf4uEMzpKw6QNghXLvLkY",
    "ADaUMid9yfUytqMBgopwjb2DTLSokTSzL1zt6iGPaS49",
    "DfXygSm4jCyNCzbzYAKhb58Pi6BteBuKVjBJhZSLQndT",
    "ADuUkR4vqLUMWXxW9gh6D6L8pMSawimctcNZ5pGwDcEt",
    "DttWaMuVvTiduCN3AwnFnBbEG9HshVEy7BkH6V1RB2oz",
    "3AVi9Tg9Uo68tJfuvoKvqKNWKkC5wPdSSdeBnizKZ6jT",
];

// ═══════════════════════════════════════════════════════════
//  Types
// ═══════════════════════════════════════════════════════════

/// Tip strategy for Jito bundles.
#[derive(Debug, Clone)]
pub enum TipStrategy {
    /// Tip floor (~1,000-5,000 lamports). Cheapest, lowest priority.
    Min,
    /// Tip floor × 3 (~10k-50k lamports). Recommended.
    Competitive,
    /// 100,000+ lamports. For high-value opportunities.
    Aggressive,
    /// Exact tip in lamports. Full control.
    Exact(u64),
}

/// Jito configuration.
#[derive(Debug, Clone)]
pub struct JitoConfig {
    /// Tip strategy (default: Competitive)
    pub tip: TipStrategy,
    /// Block Engine region or full URL (default: "mainnet")
    pub region: String,
}

impl Default for JitoConfig {
    fn default() -> Self {
        Self {
            tip: TipStrategy::Competitive,
            region: "mainnet".to_string(),
        }
    }
}

/// Result of sending a Jito bundle.
#[derive(Debug, Clone)]
pub struct JitoBundleResult {
    pub bundle_id: String,
    pub signature: Option<String>,
}

// ═══════════════════════════════════════════════════════════
//  URL Resolution
// ═══════════════════════════════════════════════════════════

/// Resolve a region name to a full Block Engine URL.
pub fn resolve_block_engine_url(region: &str) -> String {
    if region.starts_with("http") {
        return region.to_string();
    }
    let urls = block_engine_urls();
    urls.get(region)
        .unwrap_or(&"https://mainnet.block-engine.jito.wtf")
        .to_string()
}

// ═══════════════════════════════════════════════════════════
//  Tip Calculation
// ═══════════════════════════════════════════════════════════

const MIN_TIP_LAMPORTS: u64 = 1_000;
const COMPETITIVE_MULTIPLIER: u64 = 3;
const AGGRESSIVE_TIP_LAMPORTS: u64 = 100_000;

/// Calculate tip amount in lamports based on strategy.
pub fn calculate_tip(strategy: &TipStrategy, floor_lamports: u64) -> u64 {
    match strategy {
        TipStrategy::Exact(tip) => (*tip).max(MIN_TIP_LAMPORTS),
        TipStrategy::Min => floor_lamports.max(MIN_TIP_LAMPORTS),
        TipStrategy::Competitive => (floor_lamports * COMPETITIVE_MULTIPLIER).max(MIN_TIP_LAMPORTS * 10),
        TipStrategy::Aggressive => AGGRESSIVE_TIP_LAMPORTS.max(floor_lamports * 5),
    }
}

/// Build a tip instruction — SystemProgram::transfer to a random Jito tip account.
pub fn build_tip_instruction(payer: &Pubkey, tip_lamports: u64) -> Instruction {
    let idx = (std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap()
        .subsec_nanos() as usize) % JITO_TIP_ACCOUNTS.len();
    let tip_account: Pubkey = JITO_TIP_ACCOUNTS[idx].parse().unwrap();
    system_instruction::transfer(payer, &tip_account, tip_lamports)
}

// ═══════════════════════════════════════════════════════════
//  Block Engine API — pure reqwest
// ═══════════════════════════════════════════════════════════

/// Fetch current tip accounts from Jito Block Engine.
/// Falls back to hardcoded JITO_TIP_ACCOUNTS on failure.
pub async fn fetch_tip_accounts(block_engine_url: &str) -> Vec<Pubkey> {
    let client = reqwest::Client::new();
    let body = serde_json::json!({
        "jsonrpc": "2.0",
        "id": 1,
        "method": "getTipAccounts",
        "params": []
    });

    match client.post(&format!("{}/api/v1/bundles", block_engine_url))
        .json(&body)
        .send()
        .await
    {
        Ok(res) => {
            if let Ok(data) = res.json::<serde_json::Value>().await {
                if let Some(result) = data.get("result").and_then(|r| r.as_array()) {
                    return result.iter()
                        .filter_map(|v| v.as_str()?.parse::<Pubkey>().ok())
                        .collect();
                }
            }
        }
        Err(_) => {}
    }

    // Fallback
    JITO_TIP_ACCOUNTS.iter()
        .filter_map(|s| s.parse::<Pubkey>().ok())
        .collect()
}

/// Send a bundle of signed transactions to the Jito Block Engine.
pub async fn send_jito_bundle(
    block_engine_url: &str,
    transactions: &[VersionedTransaction],
) -> Result<String, String> {
    use base64::Engine;

    let encoded_txs: Vec<String> = transactions.iter()
        .map(|tx| {
            let serialized = bincode::serialize(tx).map_err(|e| e.to_string())?;
            Ok(base64::engine::general_purpose::STANDARD.encode(&serialized))
        })
        .collect::<Result<Vec<_>, String>>()?;

    let body = serde_json::json!({
        "jsonrpc": "2.0",
        "id": 1,
        "method": "sendBundle",
        "params": [encoded_txs, {"encoding": "base64"}]
    });

    let client = reqwest::Client::new();
    let res = client.post(&format!("{}/api/v1/bundles", block_engine_url))
        .json(&body)
        .send()
        .await
        .map_err(|e| format!("Jito send error: {}", e))?;

    let data: serde_json::Value = res.json().await
        .map_err(|e| format!("Jito parse error: {}", e))?;

    if let Some(error) = data.get("error") {
        let msg = error.get("message").and_then(|m| m.as_str()).unwrap_or("unknown");
        return Err(format!("Jito sendBundle error: {}", msg));
    }

    data.get("result")
        .and_then(|r| r.as_str())
        .map(|s| s.to_string())
        .ok_or_else(|| "No bundle_id in response".to_string())
}

/// Poll Jito Block Engine for bundle landing status.
pub async fn poll_bundle_status(
    block_engine_url: &str,
    bundle_id: &str,
    timeout_ms: u64,
) -> Result<String, String> {
    let client = reqwest::Client::new();
    let start = std::time::Instant::now();
    let poll_interval = std::time::Duration::from_millis(500);

    while start.elapsed().as_millis() < timeout_ms as u128 {
        let body = serde_json::json!({
            "jsonrpc": "2.0",
            "id": 1,
            "method": "getBundleStatuses",
            "params": [[bundle_id]]
        });

        if let Ok(res) = client.post(&format!("{}/api/v1/bundles", block_engine_url))
            .json(&body)
            .send()
            .await
        {
            if let Ok(data) = res.json::<serde_json::Value>().await {
                if let Some(status) = data
                    .get("result")
                    .and_then(|r| r.get("value"))
                    .and_then(|v| v.as_array())
                    .and_then(|arr| arr.first())
                {
                    let conf = status.get("confirmation_status")
                        .and_then(|s| s.as_str())
                        .unwrap_or("");

                    if conf == "confirmed" || conf == "finalized" {
                        let sig = status.get("transactions")
                            .and_then(|t| t.as_array())
                            .and_then(|arr| arr.first())
                            .and_then(|s| s.as_str())
                            .unwrap_or(bundle_id);
                        return Ok(sig.to_string());
                    }

                    if status.get("err").is_some() {
                        return Err(format!("Jito bundle failed: {}", status));
                    }
                }
            }
        }

        tokio::time::sleep(poll_interval).await;
    }

    Err(format!("Jito bundle {} did not land within {}ms", bundle_id, timeout_ms))
}