Skip to main content

jito_bundle/client/
send.rs

1use crate::bundler::bundle::Bundle;
2use crate::client::jito_bundler::JitoBundler;
3use crate::constants::JITO_EXPLORER_URL;
4use crate::error::JitoError;
5use crate::types::{BundleResult, JsonRpcRequest, JsonRpcResponse};
6use base64::Engine;
7use serde::Serialize;
8use solana_sdk::transaction::VersionedTransaction;
9
10impl JitoBundler {
11    pub fn get_jito_explorer_url(bundle_id: &str) -> String {
12        format!("{JITO_EXPLORER_URL}/{bundle_id}")
13    }
14
15    pub fn encode_transactions(
16        transactions: &[VersionedTransaction],
17    ) -> Result<Vec<String>, JitoError> {
18        transactions
19            .iter()
20            .map(|tx| {
21                let serialized = bincode::serialize(tx).map_err(|e| JitoError::Serialization {
22                    reason: e.to_string(),
23                })?;
24                Ok(base64::engine::general_purpose::STANDARD.encode(serialized))
25            })
26            .collect()
27    }
28
29    pub fn extract_signatures(transactions: &[VersionedTransaction]) -> Vec<String> {
30        transactions
31            .iter()
32            .map(|tx| bs58::encode(&tx.signatures[0]).into_string())
33            .collect()
34    }
35
36    pub async fn send_bundle(&self, bundle: &Bundle<'_>) -> Result<BundleResult, JitoError> {
37        let encoded_txs = Self::encode_transactions(&bundle.versioned_transaction)?;
38        let signatures = Self::extract_signatures(&bundle.versioned_transaction);
39        let endpoints = self.config.network.send_endpoints();
40        let mut last_error = String::from("no endpoints available");
41        for endpoint in &endpoints {
42            match self.send_bundle_to_endpoint(endpoint, &encoded_txs).await {
43                Ok(bundle_id) => {
44                    let explorer_url = Self::get_jito_explorer_url(&bundle_id);
45                    return Ok(BundleResult {
46                        success: true,
47                        bundle_id: Some(bundle_id),
48                        error: None,
49                        signatures,
50                        explorer_url: Some(explorer_url),
51                    });
52                }
53                Err(e) => {
54                    tracing::warn!("endpoint {endpoint} failed: {e}");
55                    last_error = e.to_string();
56                }
57            }
58        }
59
60        Err(JitoError::AllEndpointsFailed {
61            count: endpoints.len(),
62            last_error,
63        })
64    }
65
66    async fn send_bundle_to_endpoint(
67        &self,
68        endpoint: &str,
69        encoded_txs: &[String],
70    ) -> Result<String, JitoError> {
71        #[derive(Serialize)]
72        struct BundleParams<'a> {
73            #[serde(skip_serializing_if = "Option::is_none")]
74            encoding: Option<&'a str>,
75        }
76
77        let request = JsonRpcRequest {
78            jsonrpc: "2.0",
79            id: 1,
80            method: "sendBundle",
81            params: (
82                encoded_txs,
83                BundleParams {
84                    encoding: Some("base64"),
85                },
86            ),
87        };
88        let response = self
89            .jito_post(endpoint)
90            .json(&request)
91            .send()
92            .await
93            .map_err(|e| JitoError::Network {
94                reason: e.to_string(),
95            })?;
96
97        let status = response.status();
98        let response_text = response.text().await.map_err(|e| JitoError::Network {
99            reason: format!("failed to read response: {e}"),
100        })?;
101
102        if !status.is_success() {
103            return Err(JitoError::Network {
104                reason: format!("HTTP {status}: {response_text}"),
105            });
106        }
107
108        let parsed: JsonRpcResponse<String> =
109            serde_json::from_str(&response_text).map_err(|e| JitoError::Serialization {
110                reason: format!("failed to parse response: {e}, body: {response_text}"),
111            })?;
112
113        if let Some(error) = parsed.error {
114            return Err(JitoError::JsonRpc {
115                code: error.code,
116                message: error.message,
117            });
118        }
119
120        parsed.result.ok_or_else(|| JitoError::JsonRpc {
121            code: -1,
122            message: "no bundle_id in response".to_string(),
123        })
124    }
125}