#[allow(deprecated)]
use solana_sdk::{
instruction::Instruction,
pubkey::Pubkey,
system_instruction,
transaction::VersionedTransaction,
};
use std::collections::HashMap;
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
}
pub const JITO_TIP_ACCOUNTS: [&str; 8] = [
"96gYZGLnJYVFmbjzopPSU6QiEV5fGqZNyN9nmNhvrZU5",
"HFqU5x63VTqvQss8hp11i4wVV8bD44PvwucfZ2bU7gRe",
"Cw8CFyM9FkoMi7K7Crf6HNQqf4uEMzpKw6QNghXLvLkY",
"ADaUMid9yfUytqMBgopwjb2DTLSokTSzL1zt6iGPaS49",
"DfXygSm4jCyNCzbzYAKhb58Pi6BteBuKVjBJhZSLQndT",
"ADuUkR4vqLUMWXxW9gh6D6L8pMSawimctcNZ5pGwDcEt",
"DttWaMuVvTiduCN3AwnFnBbEG9HshVEy7BkH6V1RB2oz",
"3AVi9Tg9Uo68tJfuvoKvqKNWKkC5wPdSSdeBnizKZ6jT",
];
#[derive(Debug, Clone)]
pub enum TipStrategy {
Min,
Competitive,
Aggressive,
Exact(u64),
}
#[derive(Debug, Clone)]
pub struct JitoConfig {
pub tip: TipStrategy,
pub region: String,
}
impl Default for JitoConfig {
fn default() -> Self {
Self {
tip: TipStrategy::Competitive,
region: "mainnet".to_string(),
}
}
}
#[derive(Debug, Clone)]
pub struct JitoBundleResult {
pub bundle_id: String,
pub signature: Option<String>,
}
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()
}
const MIN_TIP_LAMPORTS: u64 = 1_000;
const COMPETITIVE_MULTIPLIER: u64 = 3;
const AGGRESSIVE_TIP_LAMPORTS: u64 = 100_000;
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),
}
}
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)
}
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(_) => {}
}
JITO_TIP_ACCOUNTS.iter()
.filter_map(|s| s.parse::<Pubkey>().ok())
.collect()
}
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())
}
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))
}