use solana_sdk::{
compute_budget::ComputeBudgetInstruction,
instruction::Instruction,
message::{v0, VersionedMessage},
signature::Keypair,
signer::Signer,
transaction::VersionedTransaction,
address_lookup_table::AddressLookupTableAccount,
commitment_config::CommitmentConfig,
};
use solana_client::nonblocking::rpc_client::RpcClient;
use crate::jito::{JitoConfig, resolve_block_engine_url, calculate_tip, build_tip_instruction, send_jito_bundle, poll_bundle_status};
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum RetryReason {
Expired,
Congestion,
ProgramError,
}
#[derive(Debug, Clone)]
pub struct RetryConfig {
pub max_attempts: u32,
pub strategy: RetryStrategy,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum RetryStrategy {
None,
Adaptive,
}
impl Default for RetryConfig {
fn default() -> Self {
Self {
max_attempts: 3,
strategy: RetryStrategy::Adaptive,
}
}
}
#[derive(Debug, Clone)]
pub enum SendMode {
Rpc,
Jito(JitoConfig),
}
pub async fn send_with_retry(
rpc: &RpcClient,
wallet: &Keypair,
instructions: Vec<Instruction>,
lookup_tables: &[AddressLookupTableAccount],
config: RetryConfig,
initial_priority_micro_lamports: Option<u64>,
send_mode: SendMode,
) -> Result<String, String> {
if config.strategy == RetryStrategy::None {
return send_once(rpc, wallet, &instructions, lookup_tables,
initial_priority_micro_lamports.unwrap_or(0)).await;
}
let mut last_error = String::new();
let mut priority = initial_priority_micro_lamports.unwrap_or(1_000);
for attempt in 1..=config.max_attempts {
let result = match &send_mode {
SendMode::Rpc => {
send_once(rpc, wallet, &instructions, lookup_tables, priority).await
}
SendMode::Jito(jito_config) => {
send_once_via_jito(rpc, wallet, &instructions, lookup_tables, priority, jito_config).await
}
};
match result {
Ok(sig) => return Ok(sig),
Err(err) => {
let reason = classify_error(&err);
last_error = err;
if reason == RetryReason::ProgramError {
return Err(last_error);
}
if attempt >= config.max_attempts {
return Err(last_error);
}
if reason == RetryReason::Congestion {
priority = (priority as f64 * 1.5) as u64;
let backoff = 400 * 2u64.pow(attempt - 1);
tokio::time::sleep(std::time::Duration::from_millis(backoff)).await;
}
}
}
}
Err(last_error)
}
async fn send_once(
rpc: &RpcClient,
wallet: &Keypair,
instructions: &[Instruction],
lookup_tables: &[AddressLookupTableAccount],
priority_micro_lamports: u64,
) -> Result<String, String> {
let (blockhash, _last_valid_block_height) = rpc
.get_latest_blockhash_with_commitment(CommitmentConfig::confirmed())
.await
.map_err(|e| e.to_string())?;
let mut all_ixs: Vec<Instruction> = Vec::new();
if priority_micro_lamports > 0 {
all_ixs.push(ComputeBudgetInstruction::set_compute_unit_price(priority_micro_lamports));
}
all_ixs.extend_from_slice(instructions);
let msg = v0::Message::try_compile(
&wallet.pubkey(), &all_ixs, lookup_tables, blockhash,
).map_err(|e| e.to_string())?;
let tx = VersionedTransaction::try_new(VersionedMessage::V0(msg), &[wallet])
.map_err(|e| e.to_string())?;
let sig = rpc.send_transaction(&tx)
.await
.map_err(|e| e.to_string())?;
rpc.confirm_transaction_with_spinner(&sig, &blockhash, CommitmentConfig::confirmed())
.await
.map_err(|e| e.to_string())?;
Ok(sig.to_string())
}
async fn send_once_via_jito(
rpc: &RpcClient,
wallet: &Keypair,
instructions: &[Instruction],
lookup_tables: &[AddressLookupTableAccount],
priority_micro_lamports: u64,
jito_config: &JitoConfig,
) -> Result<String, String> {
let block_engine_url = resolve_block_engine_url(&jito_config.region);
let tip_lamports = calculate_tip(&jito_config.tip, 10_000);
let mut all_ixs: Vec<Instruction> = Vec::new();
if priority_micro_lamports > 0 {
all_ixs.push(ComputeBudgetInstruction::set_compute_unit_price(priority_micro_lamports));
}
all_ixs.extend_from_slice(instructions);
all_ixs.push(build_tip_instruction(&wallet.pubkey(), tip_lamports));
let (blockhash, _) = rpc
.get_latest_blockhash_with_commitment(CommitmentConfig::confirmed())
.await
.map_err(|e| e.to_string())?;
let msg = v0::Message::try_compile(
&wallet.pubkey(), &all_ixs, lookup_tables, blockhash,
).map_err(|e| e.to_string())?;
let tx = VersionedTransaction::try_new(VersionedMessage::V0(msg), &[wallet])
.map_err(|e| e.to_string())?;
let bundle_id = send_jito_bundle(&block_engine_url, &[tx]).await?;
poll_bundle_status(&block_engine_url, &bundle_id, 30_000).await
}
fn classify_error(err: &str) -> RetryReason {
if err.contains("Blockhash") || err.contains("expired") || err.contains("block height exceeded") {
RetryReason::Expired
} else if err.contains("InstructionError") || err.contains("Custom(") || err.contains("custom program error") {
RetryReason::ProgramError
} else {
RetryReason::Congestion
}
}