1use solana_sdk::{
11 compute_budget::ComputeBudgetInstruction,
12 instruction::Instruction,
13 message::{v0, VersionedMessage},
14 signature::Keypair,
15 signer::Signer,
16 transaction::VersionedTransaction,
17 address_lookup_table::AddressLookupTableAccount,
18 commitment_config::CommitmentConfig,
19};
20use solana_client::nonblocking::rpc_client::RpcClient;
21use crate::jito::{JitoConfig, resolve_block_engine_url, calculate_tip, build_tip_instruction, send_jito_bundle, poll_bundle_status};
22
23#[derive(Debug, Clone, Copy, PartialEq)]
29pub enum RetryReason {
30 Expired,
31 Congestion,
32 ProgramError,
33}
34
35#[derive(Debug, Clone)]
37pub struct RetryConfig {
38 pub max_attempts: u32,
40 pub strategy: RetryStrategy,
42}
43
44#[derive(Debug, Clone, Copy, PartialEq)]
45pub enum RetryStrategy {
46 None,
48 Adaptive,
50}
51
52impl Default for RetryConfig {
53 fn default() -> Self {
54 Self {
55 max_attempts: 3,
56 strategy: RetryStrategy::Adaptive,
57 }
58 }
59}
60
61#[derive(Debug, Clone)]
63pub enum SendMode {
64 Rpc,
65 Jito(JitoConfig),
66}
67
68pub async fn send_with_retry(
88 rpc: &RpcClient,
89 wallet: &Keypair,
90 instructions: Vec<Instruction>,
91 lookup_tables: &[AddressLookupTableAccount],
92 config: RetryConfig,
93 initial_priority_micro_lamports: Option<u64>,
94 send_mode: SendMode,
95) -> Result<String, String> {
96 if config.strategy == RetryStrategy::None {
97 return send_once(rpc, wallet, &instructions, lookup_tables,
98 initial_priority_micro_lamports.unwrap_or(0)).await;
99 }
100
101 let mut last_error = String::new();
102 let mut priority = initial_priority_micro_lamports.unwrap_or(1_000);
103
104 for attempt in 1..=config.max_attempts {
105 let result = match &send_mode {
106 SendMode::Rpc => {
107 send_once(rpc, wallet, &instructions, lookup_tables, priority).await
108 }
109 SendMode::Jito(jito_config) => {
110 send_once_via_jito(rpc, wallet, &instructions, lookup_tables, priority, jito_config).await
111 }
112 };
113
114 match result {
115 Ok(sig) => return Ok(sig),
116 Err(err) => {
117 let reason = classify_error(&err);
118 last_error = err;
119
120 if reason == RetryReason::ProgramError {
122 return Err(last_error);
123 }
124 if attempt >= config.max_attempts {
125 return Err(last_error);
126 }
127
128 if reason == RetryReason::Congestion {
130 priority = (priority as f64 * 1.5) as u64;
131 let backoff = 400 * 2u64.pow(attempt - 1);
132 tokio::time::sleep(std::time::Duration::from_millis(backoff)).await;
133 }
134 }
136 }
137 }
138
139 Err(last_error)
140}
141
142async fn send_once(
143 rpc: &RpcClient,
144 wallet: &Keypair,
145 instructions: &[Instruction],
146 lookup_tables: &[AddressLookupTableAccount],
147 priority_micro_lamports: u64,
148) -> Result<String, String> {
149 let (blockhash, _last_valid_block_height) = rpc
150 .get_latest_blockhash_with_commitment(CommitmentConfig::confirmed())
151 .await
152 .map_err(|e| e.to_string())?;
153
154 let mut all_ixs: Vec<Instruction> = Vec::new();
155 if priority_micro_lamports > 0 {
156 all_ixs.push(ComputeBudgetInstruction::set_compute_unit_price(priority_micro_lamports));
157 }
158 all_ixs.extend_from_slice(instructions);
159
160 let msg = v0::Message::try_compile(
161 &wallet.pubkey(), &all_ixs, lookup_tables, blockhash,
162 ).map_err(|e| e.to_string())?;
163
164 let tx = VersionedTransaction::try_new(VersionedMessage::V0(msg), &[wallet])
165 .map_err(|e| e.to_string())?;
166
167 let sig = rpc.send_transaction(&tx)
168 .await
169 .map_err(|e| e.to_string())?;
170
171 rpc.confirm_transaction_with_spinner(&sig, &blockhash, CommitmentConfig::confirmed())
172 .await
173 .map_err(|e| e.to_string())?;
174
175 Ok(sig.to_string())
176}
177
178async fn send_once_via_jito(
179 rpc: &RpcClient,
180 wallet: &Keypair,
181 instructions: &[Instruction],
182 lookup_tables: &[AddressLookupTableAccount],
183 priority_micro_lamports: u64,
184 jito_config: &JitoConfig,
185) -> Result<String, String> {
186 let block_engine_url = resolve_block_engine_url(&jito_config.region);
187 let tip_lamports = calculate_tip(&jito_config.tip, 10_000);
188
189 let mut all_ixs: Vec<Instruction> = Vec::new();
190 if priority_micro_lamports > 0 {
191 all_ixs.push(ComputeBudgetInstruction::set_compute_unit_price(priority_micro_lamports));
192 }
193 all_ixs.extend_from_slice(instructions);
194 all_ixs.push(build_tip_instruction(&wallet.pubkey(), tip_lamports));
195
196 let (blockhash, _) = rpc
197 .get_latest_blockhash_with_commitment(CommitmentConfig::confirmed())
198 .await
199 .map_err(|e| e.to_string())?;
200
201 let msg = v0::Message::try_compile(
202 &wallet.pubkey(), &all_ixs, lookup_tables, blockhash,
203 ).map_err(|e| e.to_string())?;
204
205 let tx = VersionedTransaction::try_new(VersionedMessage::V0(msg), &[wallet])
206 .map_err(|e| e.to_string())?;
207
208 let bundle_id = send_jito_bundle(&block_engine_url, &[tx]).await?;
209 poll_bundle_status(&block_engine_url, &bundle_id, 30_000).await
210}
211
212fn classify_error(err: &str) -> RetryReason {
213 if err.contains("Blockhash") || err.contains("expired") || err.contains("block height exceeded") {
214 RetryReason::Expired
215 } else if err.contains("InstructionError") || err.contains("Custom(") || err.contains("custom program error") {
216 RetryReason::ProgramError
217 } else {
218 RetryReason::Congestion
219 }
220}