Skip to main content

jito_bundle/client/
simulate.rs

1use crate::bundler::bundle::Bundle;
2use crate::client::jito_bundler::JitoBundler;
3use crate::error::JitoError;
4use crate::types::{
5    JsonRpcRequest, JsonRpcResponse, SimulateBundleApiResult, SimulateBundleParams,
6    SimulateBundleSummary, SimulateBundleValue,
7};
8use base64::Engine;
9use solana_client::rpc_config::RpcSimulateTransactionConfig;
10use solana_sdk::commitment_config::CommitmentConfig;
11
12impl JitoBundler {
13    pub async fn simulate_per_transaction(&self, bundle: &Bundle<'_>) -> Result<(), JitoError> {
14        for (i, tx) in bundle.versioned_transaction.iter().enumerate() {
15            let sig = bs58::encode(&tx.signatures[0]).into_string();
16            let config = RpcSimulateTransactionConfig {
17                sig_verify: true,
18                replace_recent_blockhash: false,
19                commitment: Some(CommitmentConfig::confirmed()),
20                accounts: None,
21                min_context_slot: None,
22                inner_instructions: false,
23                encoding: None,
24            };
25
26            match self
27                .rpc_client
28                .simulate_transaction_with_config(tx, config)
29                .await
30            {
31                Ok(result) => {
32                    if let Some(err) = result.value.err {
33                        let logs = result.value.logs.unwrap_or_default();
34                        return Err(JitoError::SimulationFailed {
35                            details: format!(
36                                "transaction {i} simulation failed: {err:?}\nsignature: {sig}\nlogs: {logs:?}"
37                            ),
38                        });
39                    }
40                }
41                Err(e) => {
42                    return Err(JitoError::SimulationFailed {
43                        details: format!(
44                            "transaction {i} RPC simulation error: {e}\nsignature: {sig}"
45                        ),
46                    });
47                }
48            }
49        }
50
51        Ok(())
52    }
53
54    pub async fn simulate_bundle_helius(
55        &self,
56        bundle: &Bundle<'_>,
57        helius_rpc_url: &str,
58    ) -> Result<SimulateBundleValue, JitoError> {
59        let encoded_transactions: Vec<String> = bundle
60            .versioned_transaction
61            .iter()
62            .map(|tx| {
63                let serialized = bincode::serialize(tx).map_err(|e| JitoError::Serialization {
64                    reason: e.to_string(),
65                })?;
66                Ok(base64::engine::general_purpose::STANDARD.encode(serialized))
67            })
68            .collect::<Result<Vec<String>, JitoError>>()?;
69        let params = SimulateBundleParams {
70            encoded_transactions,
71        };
72        let request = JsonRpcRequest {
73            jsonrpc: "2.0",
74            id: 1,
75            method: "simulateBundle",
76            params: [params],
77        };
78        let response = self
79            .http_client
80            .post(helius_rpc_url)
81            .header("Content-Type", "application/json")
82            .json(&request)
83            .send()
84            .await
85            .map_err(|e| JitoError::Network {
86                reason: format!("Helius simulateBundle: {e}"),
87            })?;
88        let status = response.status();
89        let response_text = response.text().await.map_err(|e| JitoError::Network {
90            reason: format!("failed to read Helius response: {e}"),
91        })?;
92        if !status.is_success() {
93            return Err(JitoError::Network {
94                reason: format!("Helius simulateBundle HTTP {status}: {response_text}"),
95            });
96        }
97        let parsed: JsonRpcResponse<SimulateBundleApiResult> = serde_json::from_str(&response_text)
98            .map_err(|e| JitoError::Serialization {
99                reason: format!(
100                    "failed to parse Helius simulateBundle response: {e}, body: {response_text}"
101                ),
102            })?;
103        if let Some(error) = parsed.error {
104            return Err(JitoError::JsonRpc {
105                code: error.code,
106                message: error.message,
107            });
108        }
109
110        let result = parsed.result.ok_or_else(|| JitoError::JsonRpc {
111            code: -1,
112            message: "no result in Helius simulateBundle response".to_string(),
113        })?;
114
115        if let SimulateBundleSummary::Failed(failure) = &result.value.summary {
116            let tx_count = bundle.versioned_transaction.len();
117            let results_count = result.value.transaction_results.len();
118            let failed_tx_index = if results_count < tx_count {
119                results_count
120            } else {
121                result
122                    .value
123                    .transaction_results
124                    .iter()
125                    .position(|r| r.err.is_some())
126                    .unwrap_or(0)
127            };
128            let mut error_details = format!(
129                "bundle simulation failed at transaction {failed_tx_index}: {}\n\
130                 failing tx signature: {:?}\n\
131                 bundle size: {tx_count} transactions, results returned: {results_count}",
132                failure.error_message(),
133                failure.tx_signature
134            );
135
136            if results_count < tx_count {
137                error_details.push_str(&format!(
138                    "\nHelius stopped after tx {failed_tx_index} failed — no logs for subsequent transactions"
139                ));
140            }
141
142            for (idx, tx_result) in result.value.transaction_results.iter().enumerate() {
143                let status_str = if tx_result.err.is_some() {
144                    "FAILED"
145                } else {
146                    "OK"
147                };
148                let units = tx_result
149                    .units_consumed
150                    .map_or_else(|| "N/A".to_string(), |u| u.to_string());
151
152                error_details.push_str(&format!("\n\n=== transaction {idx} [{status_str}] ==="));
153                error_details.push_str(&format!("\ncompute units: {units}"));
154
155                if let Some(err) = &tx_result.err {
156                    error_details.push_str(&format!("\nerror: {err}"));
157                }
158
159                if let Some(logs) = &tx_result.logs {
160                    error_details.push_str("\nlogs:");
161                    for log in logs {
162                        error_details.push_str(&format!("\n  {log}"));
163                    }
164                } else {
165                    error_details.push_str("\nlogs: none");
166                }
167            }
168
169            return Err(JitoError::SimulationFailed {
170                details: error_details,
171            });
172        }
173
174        Ok(result.value)
175    }
176}