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