Skip to main content

jito_bundle/client/
jito_bundler.rs

1use crate::bundler::bundle::{Bundle, BundleBuilderInputs};
2use crate::config::jito::JitoConfig;
3use crate::error::JitoError;
4use crate::tip::TipHelper;
5use crate::types::{BundleResult, BundleStatus, SimulateBundleValue};
6use reqwest::Client;
7use solana_client::nonblocking::rpc_client::RpcClient;
8use solana_instruction::Instruction;
9use solana_sdk::address_lookup_table::AddressLookupTableAccount;
10use solana_sdk::hash::Hash;
11use solana_sdk::signer::keypair::Keypair;
12use std::time::Duration;
13
14pub struct JitoBundler {
15    pub config: JitoConfig,
16    pub http_client: Client,
17    pub rpc_client: RpcClient,
18}
19
20impl JitoBundler {
21    pub fn new(config: JitoConfig) -> Result<Self, JitoError> {
22        let http_client = Client::builder()
23            .timeout(Duration::from_secs(30))
24            .build()
25            .map_err(|e| JitoError::Network {
26                reason: format!("failed to create HTTP client: {e}"),
27            })?;
28        let rpc_client = RpcClient::new(config.rpc_url.clone());
29        Ok(Self {
30            config,
31            http_client,
32            rpc_client,
33        })
34    }
35
36    pub fn jito_post(&self, url: &str) -> reqwest::RequestBuilder {
37        let full_url = if let Some(uuid) = &self.config.uuid
38            && !self.config.network.is_custom()
39        {
40            let separator = if url.contains('?') { "&" } else { "?" };
41            format!("{url}{separator}uuid={uuid}")
42        } else {
43            url.to_string()
44        };
45        let mut builder = self
46            .http_client
47            .post(full_url)
48            .header("Content-Type", "application/json");
49        if let Some(uuid) = &self.config.uuid {
50            builder = builder.header("x-jito-auth", uuid.as_str());
51        }
52        builder
53    }
54
55    pub async fn fetch_tip(&self) -> Result<u64, JitoError> {
56        let tip_floor_url = self.config.network.tip_floor_url();
57        TipHelper::resolve_tip(&self.http_client, tip_floor_url, &self.config.tip_strategy).await
58    }
59
60    pub fn build_bundle<'a>(
61        &'a self,
62        input: BuildBundleOptions<'a>,
63    ) -> Result<Bundle<'a>, JitoError> {
64        let BuildBundleOptions {
65            payer,
66            transactions_instructions,
67            lookup_tables,
68            recent_blockhash,
69            tip_lamports,
70        } = input;
71        let bundle = Bundle::new(BundleBuilderInputs {
72            payer,
73            transactions_instructions,
74            lookup_tables,
75            recent_blockhash,
76            tip_lamports,
77            jitodontfront_pubkey: self.config.jitodontfront_pubkey.as_ref(),
78            compute_unit_limit: self.config.compute_unit_limit,
79        });
80        bundle.build()
81    }
82
83    pub async fn simulate_helius(
84        &self,
85        bundle: &Bundle<'_>,
86    ) -> Result<SimulateBundleValue, JitoError> {
87        let helius_url =
88            self.config
89                .helius_rpc_url
90                .as_deref()
91                .ok_or_else(|| JitoError::Network {
92                    reason: "helius_rpc_url not configured".to_string(),
93                })?;
94        self.simulate_bundle_helius(bundle, helius_url).await
95    }
96
97    pub async fn send_and_confirm(&self, bundle: &Bundle<'_>) -> Result<BundleResult, JitoError> {
98        if let Some(helius_url) = &self.config.helius_rpc_url
99            && let Err(e) = self.simulate_bundle_helius(bundle, helius_url).await
100        {
101            tracing::warn!("Helius simulation failed: {e}");
102            return Err(e);
103        }
104        let result = self.send_bundle(bundle).await?;
105        tracing::info!(
106            "bundle submitted: bundle_id={:?}, signatures={:?}, explorer={:?}",
107            result.bundle_id,
108            result.signatures,
109            result.explorer_url
110        );
111        let status = self.wait_for_landing_on_chain(&result.signatures).await;
112        Self::interpret_landing_status(status, self.config.confirm_policy.max_attempts)?;
113        Ok(result)
114    }
115
116    fn interpret_landing_status(
117        status: Result<BundleStatus, JitoError>,
118        max_attempts: u32,
119    ) -> Result<(), JitoError> {
120        match status {
121            Ok(BundleStatus::Landed { .. }) => Ok(()),
122            Ok(BundleStatus::Failed { error }) => {
123                let reason = error.unwrap_or_else(|| "unknown error".to_string());
124                Err(JitoError::OnChainFailure { reason })
125            }
126            Ok(_) => Err(JitoError::ConfirmationTimeout {
127                attempts: max_attempts,
128            }),
129            Err(e) => Err(e),
130        }
131    }
132}
133
134pub struct BuildBundleOptions<'a> {
135    pub payer: &'a Keypair,
136    pub transactions_instructions: [Option<Vec<Instruction>>; 5],
137    pub lookup_tables: &'a [AddressLookupTableAccount],
138    pub recent_blockhash: Hash,
139    pub tip_lamports: u64,
140}