jito_bundle/client/
jito_bundler.rs1use 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
14pub struct JitoBundler {
16 pub config: JitoConfig,
18 pub http_client: Client,
20 pub rpc_client: RpcClient,
22}
23
24impl JitoBundler {
25 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 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 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 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 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 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 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
164pub struct BuildBundleOptions<'a> {
166 pub payer: &'a Keypair,
168 pub transactions_instructions: BundleInstructionSlots,
170 pub lookup_tables: &'a [AddressLookupTableAccount],
172}