jito_bundle/client/
jito_bundler.rs1use 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}