use std::{future::Future, time::Duration};
use alloy::{contract::Error as ContractError, primitives::U256, providers::Provider, rpc::types::TransactionReceipt};
use eigensdk::common::get_provider;
use tokio::time::sleep;
use tracing::{error, info, warn};
use crate::error::ChainIoError;
const MAX_RETRY_ATTEMPTS: usize = 2;
const RETRY_DELAY_MS: u64 = 300;
const TX_SUBMISSION_TIMEOUT_SECS: u64 = 60;
const REPLACEMENT_UNDERPRICED_FEE_INCREASE_PCT: u64 = 125;
#[derive(Debug, Clone, Copy)]
pub struct Eip1559Fees {
pub max_fee_per_gas: u128,
pub max_priority_fee_per_gas: u128,
}
pub async fn send_with_retries<F, Fut>(
rpc_url: String,
chain_id: u64,
mut transaction_fn: F,
) -> Result<TransactionReceipt, ChainIoError>
where
F: FnMut(Option<Eip1559Fees>) -> Fut,
Fut: Future<Output = Result<TransactionReceipt, ChainIoError>>,
{
let mut fee_multiplier = U256::from(100); let mut last_error_msg = String::new();
for attempt in 1..=MAX_RETRY_ATTEMPTS {
let fee_override = if fee_multiplier > U256::from(100) {
let provider = get_provider(&rpc_url);
if let Ok(estimate) = provider.estimate_eip1559_fees().await {
let base_max_fee = U256::from(estimate.max_fee_per_gas);
let base_priority_fee = U256::from(estimate.max_priority_fee_per_gas);
let adjusted_max_fee = base_max_fee * fee_multiplier / U256::from(100);
let adjusted_priority_fee = base_priority_fee * fee_multiplier / U256::from(100);
let fees = Eip1559Fees {
max_fee_per_gas: adjusted_max_fee.to::<u128>(),
max_priority_fee_per_gas: adjusted_priority_fee.to::<u128>(),
};
info!(
"Retry attempt {} with increased EIP-1559 fees: max_fee {} -> {}, priority_fee {} -> {} (multiplier: {}%)",
attempt,
estimate.max_fee_per_gas,
fees.max_fee_per_gas,
estimate.max_priority_fee_per_gas,
fees.max_priority_fee_per_gas,
fee_multiplier
);
Some(fees)
} else if let Ok(current_gas_price) = provider.get_gas_price().await {
let current_gas_price_u256 = U256::from(current_gas_price);
let adjusted_gas_price = current_gas_price_u256 * fee_multiplier / U256::from(100);
let adjusted_u128 = adjusted_gas_price.to::<u128>();
let fees = Eip1559Fees {
max_fee_per_gas: adjusted_u128,
max_priority_fee_per_gas: adjusted_u128 / 10, };
info!(
"Retry attempt {} with legacy gas price converted to EIP-1559: {} -> max_fee {}, priority_fee {} (multiplier: {}%)",
attempt, current_gas_price, fees.max_fee_per_gas, fees.max_priority_fee_per_gas, fee_multiplier
);
Some(fees)
} else {
None
}
} else {
None
};
let timeout_duration = Duration::from_secs(TX_SUBMISSION_TIMEOUT_SECS);
let tx_result = tokio::time::timeout(timeout_duration, transaction_fn(fee_override)).await;
match tx_result {
Ok(Ok(receipt)) => {
info!(
"Successfully sent transaction on attempt {}, tx hash: {}",
attempt, receipt.transaction_hash
);
newton_metric::record_tx_submission_attempts(chain_id, attempt as u64);
return Ok(receipt);
}
Ok(Err(error)) => {
error!("Transaction failed on attempt {}: {:?}", attempt, error);
last_error_msg = format!("{:?}", error);
let contract_error = match &error {
ChainIoError::ContractError(e) => Some(e),
ChainIoError::ContractErrorWithTx { source, .. } => Some(source),
_ => None,
};
if let Some(contract_err) = contract_error {
if !is_retryable_error(contract_err) {
error!("Non-retryable contract error encountered: {:?}", error);
newton_metric::inc_tx_submission_exhausted(chain_id, "non_retryable");
newton_metric::record_tx_submission_attempts(chain_id, attempt as u64);
return Err(error);
}
if is_replacement_underpriced_error(contract_err) {
fee_multiplier =
fee_multiplier * U256::from(REPLACEMENT_UNDERPRICED_FEE_INCREASE_PCT) / U256::from(100);
newton_metric::inc_tx_submission_retry(chain_id, "replacement_underpriced");
warn!(
"Replacement underpriced error detected, increasing fees to {}% for next attempt",
fee_multiplier
);
} else {
newton_metric::inc_tx_submission_retry(chain_id, "other_retryable");
}
} else {
newton_metric::inc_tx_submission_retry(chain_id, "network_error");
warn!("Non-contract error, retrying: {:?}", error);
}
if attempt == MAX_RETRY_ATTEMPTS {
newton_metric::inc_tx_submission_exhausted(chain_id, "retries_exhausted");
newton_metric::record_tx_submission_attempts(chain_id, attempt as u64);
error!(
"Transaction submission failed after {} attempts. Last error: {}",
MAX_RETRY_ATTEMPTS, last_error_msg
);
return Err(ChainIoError::RetriesExhausted {
attempts: MAX_RETRY_ATTEMPTS,
last_error: last_error_msg,
});
}
warn!(
"Retrying in {}ms due to error (attempt {}/{})",
RETRY_DELAY_MS, attempt, MAX_RETRY_ATTEMPTS
);
sleep(Duration::from_millis(RETRY_DELAY_MS)).await;
}
Err(_elapsed) => {
error!(
"Transaction attempt {} timed out after {} seconds",
attempt, TX_SUBMISSION_TIMEOUT_SECS
);
newton_metric::inc_tx_submission_timeout(chain_id);
last_error_msg = format!("timeout after {} seconds", TX_SUBMISSION_TIMEOUT_SECS);
if attempt == MAX_RETRY_ATTEMPTS {
newton_metric::inc_tx_submission_exhausted(chain_id, "timeout");
newton_metric::record_tx_submission_attempts(chain_id, attempt as u64);
error!("Transaction submission timed out after {} attempts", MAX_RETRY_ATTEMPTS);
return Err(ChainIoError::TransactionTimeout {
timeout_secs: TX_SUBMISSION_TIMEOUT_SECS,
});
}
warn!(
"Retrying after timeout in {}ms (attempt {}/{})",
RETRY_DELAY_MS, attempt, MAX_RETRY_ATTEMPTS
);
sleep(Duration::from_millis(RETRY_DELAY_MS)).await;
}
}
}
newton_metric::inc_tx_submission_exhausted(chain_id, "unexpected");
Err(ChainIoError::RetriesExhausted {
attempts: MAX_RETRY_ATTEMPTS,
last_error: last_error_msg,
})
}
fn is_retryable_error(error: &ContractError) -> bool {
if !matches!(
error,
ContractError::TransportError(_) | ContractError::PendingTransactionError(_)
) {
return false;
}
let error_str = error.to_string().to_lowercase();
error_str.contains("timeout")
|| error_str.contains("network")
|| error_str.contains("connection")
|| error_str.contains("nonce too low")
|| error_str.contains("replacement transaction underpriced")
|| error_str.contains("server error")
|| error_str.contains("internal error")
}
fn is_replacement_underpriced_error(error: &ContractError) -> bool {
let error_str = error.to_string().to_lowercase();
error_str.contains("replacement transaction underpriced")
}