Skip to main content

jito_bundle/client/
jito_bundler.rs

1use crate::bundler::builder::types::{BundleBuilder, BundleBuilderInputs};
2use crate::bundler::bundle::types::BuiltBundle;
3use crate::bundler::types::BundleInstructionSlots;
4use crate::config::jito::JitoConfig;
5use crate::error::JitoError;
6use crate::tip::TipHelper;
7use crate::types::{BundleResult, BundleStatus, SimulateBundleValue};
8use reqwest::Client;
9use solana_client::nonblocking::rpc_client::RpcClient;
10use solana_sdk::address_lookup_table::AddressLookupTableAccount;
11use solana_sdk::signer::keypair::Keypair;
12use std::time::Duration;
13
14/// High-level facade for building, simulating, and sending Jito bundles.
15pub struct JitoBundler {
16    /// Runtime bundler configuration.
17    pub config: JitoConfig,
18    /// Shared HTTP client for Jito/Helius requests.
19    pub http_client: Client,
20    /// Solana RPC client for chain reads and simulations.
21    pub rpc_client: RpcClient,
22}
23
24impl JitoBundler {
25    // --- Construction ---
26    /// Creates a bundler client from configuration.
27    pub fn new(config: JitoConfig) -> Result<Self, JitoError> {
28        let http_client = Client::builder()
29            .timeout(Duration::from_secs(30))
30            .build()
31            .map_err(|e| JitoError::Network {
32                reason: format!("failed to create HTTP client: {e}"),
33            })?;
34        let rpc_client = RpcClient::new(config.rpc_url.clone());
35        Ok(Self {
36            config,
37            http_client,
38            rpc_client,
39        })
40    }
41
42    // --- HTTP Helpers ---
43    /// Builds a JSON POST request with Jito auth headers when configured.
44    pub fn jito_post(&self, url: &str) -> reqwest::RequestBuilder {
45        let full_url = if let Some(uuid) = &self.config.uuid
46            && !self.config.network.is_custom()
47        {
48            let separator = if url.contains('?') { "&" } else { "?" };
49            format!("{url}{separator}uuid={uuid}")
50        } else {
51            url.to_string()
52        };
53        let mut builder = self
54            .http_client
55            .post(full_url)
56            .header("Content-Type", "application/json");
57        if let Some(uuid) = &self.config.uuid {
58            builder = builder.header("x-jito-auth", uuid.as_str());
59        }
60        builder
61    }
62
63    // --- Bundle Lifecycle ---
64    /// Resolves the tip amount according to configured strategy.
65    pub async fn fetch_tip(&self) -> Result<u64, JitoError> {
66        let tip_floor_url = self.config.network.tip_floor_url();
67        TipHelper::resolve_tip(&self.http_client, tip_floor_url, &self.config.tip_strategy).await
68    }
69
70    /// Builds a signed `BuiltBundle` from fixed instruction slots.
71    ///
72    /// Steps:
73    /// 1. Resolve the tip amount using configured `TipStrategy`.
74    /// 2. Fetch a fresh recent blockhash from RPC.
75    /// 3. Build and sign transactions via `BundleBuilder::build`.
76    ///
77    /// Returns `JitoError` when tip resolution, blockhash fetch, or compilation fails.
78    pub async fn build_bundle(
79        &self,
80        input: BuildBundleOptions<'_>,
81    ) -> Result<BuiltBundle, JitoError> {
82        let BuildBundleOptions {
83            payer,
84            transactions_instructions,
85            lookup_tables,
86        } = input;
87        let tip_lamports = self.fetch_tip().await?;
88        let recent_blockhash = self.rpc_client.get_latest_blockhash().await.map_err(|e| {
89            JitoError::GetLatestBlockhash {
90                reason: e.to_string(),
91            }
92        })?;
93        let bundle = BundleBuilder::build(BundleBuilderInputs {
94            payer,
95            transactions_instructions,
96            lookup_tables,
97            recent_blockhash,
98            tip_lamports,
99            jitodontfront_pubkey: self.config.jitodontfront_pubkey.as_ref(),
100            compute_unit_limit: self.config.compute_unit_limit,
101        })?;
102        Ok(bundle)
103    }
104
105    /// Simulates the built bundle against configured Helius RPC.
106    pub async fn simulate_helius(
107        &self,
108        bundle: &BuiltBundle,
109    ) -> Result<SimulateBundleValue, JitoError> {
110        let helius_url =
111            self.config
112                .helius_rpc_url
113                .as_deref()
114                .ok_or_else(|| JitoError::Network {
115                    reason: "helius_rpc_url not configured".to_string(),
116                })?;
117        self.simulate_bundle_helius(bundle, helius_url).await
118    }
119
120    /// Optionally simulates, sends, and confirms the bundle on-chain.
121    ///
122    /// Behavior:
123    /// 1. If `helius_rpc_url` is configured, run atomic simulation first.
124    /// 2. Submit bundle with endpoint retry.
125    /// 3. Poll on-chain signatures until landed/failed/timeout.
126    pub async fn send_and_confirm(&self, bundle: &BuiltBundle) -> Result<BundleResult, JitoError> {
127        if let Some(helius_url) = &self.config.helius_rpc_url
128            && let Err(e) = self.simulate_bundle_helius(bundle, helius_url).await
129        {
130            tracing::warn!("Helius simulation failed: {e}");
131            return Err(e);
132        }
133        let result = self.send_bundle(bundle).await?;
134        tracing::info!(
135            "bundle submitted: bundle_id={:?}, signatures={:?}, explorer={:?}",
136            result.bundle_id,
137            result.signatures,
138            result.explorer_url
139        );
140        let status = self.wait_for_landing_on_chain(&result.signatures).await;
141        Self::interpret_landing_status(status, self.config.confirm_policy.max_attempts)?;
142        Ok(result)
143    }
144
145    /// Maps raw landing status into final client-facing success/error.
146    fn interpret_landing_status(
147        status: Result<BundleStatus, JitoError>,
148        max_attempts: u32,
149    ) -> Result<(), JitoError> {
150        match status {
151            Ok(BundleStatus::Landed { .. }) => Ok(()),
152            Ok(BundleStatus::Failed { error }) => {
153                let reason = error.unwrap_or_else(|| "unknown error".to_string());
154                Err(JitoError::OnChainFailure { reason })
155            }
156            Ok(_) => Err(JitoError::ConfirmationTimeout {
157                attempts: max_attempts,
158            }),
159            Err(e) => Err(e),
160        }
161    }
162}
163
164/// Input arguments for `JitoBundler::build_bundle`.
165pub struct BuildBundleOptions<'a> {
166    /// Signing payer used for all transactions.
167    pub payer: &'a Keypair,
168    /// Fixed instruction slots (max 5) used for bundle creation.
169    pub transactions_instructions: BundleInstructionSlots,
170    /// Lookup tables used when compiling transactions.
171    pub lookup_tables: &'a [AddressLookupTableAccount],
172}