use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use crate::{
common::chain::{
is_supported_chain, ARBITRUM_ONE, ARBITRUM_SEPOLIA, BASE_MAINNET, BASE_SEPOLIA, ETHEREUM_MAINNET, LOCAL_ANVIL,
LOCAL_ANVIL_DESTINATION, OPTIMISM_MAINNET, OPTIMISM_SEPOLIA, POLYGON_AMOY, POLYGON_MAINNET, SEPOLIA,
},
config::error::ConfigError,
};
pub const ANVIL_CHAIN_ID: u64 = LOCAL_ANVIL;
pub const ANVIL_DESTINATION_CHAIN_ID: u64 = LOCAL_ANVIL_DESTINATION;
pub const SEPOLIA_CHAIN_ID: u64 = SEPOLIA;
pub const MAINNET_CHAIN_ID: u64 = ETHEREUM_MAINNET;
pub const ANVIL_HTTP_URL: &str = "http://127.0.0.1:8545";
pub const ANVIL_WS_URL: &str = "ws://127.0.0.1:8545";
pub const ANVIL_DESTINATION_HTTP_URL: &str = "http://127.0.0.1:8546";
pub const ANVIL_DESTINATION_WS_URL: &str = "ws://127.0.0.1:8546";
pub const MAINNET_HTTP_URL: &str = "https://mainnet.gateway.tenderly.co";
pub const MAINNET_WS_URL: &str = "wss://mainnet.gateway.tenderly.co";
pub const SEPOLIA_HTTP_URL: &str = "https://sepolia.gateway.tenderly.co";
pub const SEPOLIA_WS_URL: &str = "wss://sepolia.gateway.tenderly.co";
pub const BASE_MAINNET_HTTP_URL: &str = "https://base.gateway.tenderly.co";
pub const BASE_MAINNET_WS_URL: &str = "wss://base.gateway.tenderly.co";
pub const BASE_SEPOLIA_HTTP_URL: &str = "https://base-sepolia.gateway.tenderly.co";
pub const BASE_SEPOLIA_WS_URL: &str = "wss://base-sepolia.gateway.tenderly.co";
pub const OPTIMISM_MAINNET_HTTP_URL: &str = "https://optimism.gateway.tenderly.co";
pub const OPTIMISM_MAINNET_WS_URL: &str = "wss://optimism.gateway.tenderly.co";
pub const OPTIMISM_SEPOLIA_HTTP_URL: &str = "https://optimism-sepolia.gateway.tenderly.co";
pub const OPTIMISM_SEPOLIA_WS_URL: &str = "wss://optimism-sepolia.gateway.tenderly.co";
pub const ARBITRUM_ONE_HTTP_URL: &str = "https://arbitrum.gateway.tenderly.co";
pub const ARBITRUM_ONE_WS_URL: &str = "wss://arbitrum.gateway.tenderly.co";
pub const ARBITRUM_SEPOLIA_HTTP_URL: &str = "https://arbitrum-sepolia.gateway.tenderly.co";
pub const ARBITRUM_SEPOLIA_WS_URL: &str = "wss://arbitrum-sepolia.gateway.tenderly.co";
pub const POLYGON_MAINNET_HTTP_URL: &str = "https://polygon.gateway.tenderly.co";
pub const POLYGON_MAINNET_WS_URL: &str = "wss://polygon.gateway.tenderly.co";
pub const POLYGON_AMOY_HTTP_URL: &str = "https://polygon-amoy.gateway.tenderly.co";
pub const POLYGON_AMOY_WS_URL: &str = "wss://polygon-amoy.gateway.tenderly.co";
pub const WEBSOCKET_DROPPED_MAX_RETRY: u64 = 6;
pub const WEBSOCKET_DROPPED_RETRY_DELAY: u64 = 10;
#[cfg(any(feature = "rpc", feature = "database", feature = "websocket"))]
pub trait WsReconnectCallbacks: Send + 'static {
fn on_disconnected(&self) {}
fn on_reconnect_attempt(&self) {}
}
#[derive(Debug)]
#[cfg(any(feature = "rpc", feature = "database", feature = "websocket"))]
pub struct NoOpWsCallbacks;
#[cfg(any(feature = "rpc", feature = "database", feature = "websocket"))]
impl WsReconnectCallbacks for NoOpWsCallbacks {}
#[cfg(any(feature = "rpc", feature = "database", feature = "websocket"))]
pub async fn ws_reconnect_loop<F, Fut, C>(
label: &str,
max_retries: u64,
retry_delay: std::time::Duration,
mut listen_fn: F,
callbacks: C,
) -> bool
where
F: FnMut() -> Fut,
Fut: std::future::Future<Output = eyre::Result<()>>,
C: WsReconnectCallbacks,
{
let mut retry_count: u64 = 0;
loop {
match listen_fn().await {
Ok(()) => {
callbacks.on_disconnected();
callbacks.on_reconnect_attempt();
retry_count = 0;
tracing::info!("{label} ws stream ended, reconnecting");
}
Err(e) => {
callbacks.on_disconnected();
if retry_count >= max_retries {
tracing::error!("{label} websocket failed to reconnect after {retry_count} retries: {e}");
return true;
}
callbacks.on_reconnect_attempt();
tracing::error!("{label} websocket error: {e}, retrying");
tokio::time::sleep(retry_delay).await;
retry_count += 1;
}
}
}
}
pub fn is_anvil() -> bool {
match std::env::var("CHAIN_ID") {
Ok(chain_id_str) => {
let chain_id = chain_id_str.parse::<u64>().unwrap_or(0);
chain_id == ANVIL_CHAIN_ID || chain_id == ANVIL_DESTINATION_CHAIN_ID
}
Err(_) => true,
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
pub struct RpcProviderConfig {
pub http: String,
pub ws: String,
}
impl RpcProviderConfig {
pub fn new(http: String, ws: String) -> Self {
Self { http, ws }
}
pub fn load(chain_id: u64) -> Result<Self, std::io::Error> {
let default_config = get_default_rpc_config(chain_id).ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("Unsupported chain id: {}", chain_id),
)
})?;
Ok(Self {
http: get_env_or_default(
&format!("RPC_HTTP_URL_{}", chain_id),
"RPC_HTTP_URL",
&default_config.http,
),
ws: get_env_or_default(&format!("RPC_WS_URL_{}", chain_id), "RPC_WS_URL", &default_config.ws),
})
}
}
#[derive(Debug, Clone)]
pub struct ChainRpcProviderConfig {
configs: HashMap<u64, RpcProviderConfig>,
}
impl ChainRpcProviderConfig {
pub fn load() -> Self {
let mut configs = HashMap::new();
let supported_chains = [
LOCAL_ANVIL,
LOCAL_ANVIL_DESTINATION,
ETHEREUM_MAINNET,
SEPOLIA,
BASE_MAINNET,
BASE_SEPOLIA,
OPTIMISM_MAINNET,
OPTIMISM_SEPOLIA,
ARBITRUM_ONE,
ARBITRUM_SEPOLIA,
POLYGON_MAINNET,
POLYGON_AMOY,
];
for chain_id in supported_chains {
if let Some(default_config) = get_default_rpc_config(chain_id) {
let http = get_env_or_default(
&format!("RPC_HTTP_URL_{}", chain_id),
"RPC_HTTP_URL",
&default_config.http,
);
let ws = get_env_or_default(&format!("RPC_WS_URL_{}", chain_id), "RPC_WS_URL", &default_config.ws);
configs.insert(chain_id, RpcProviderConfig { http, ws });
}
}
Self { configs }
}
pub fn get(&self, chain_id: u64) -> Option<&RpcProviderConfig> {
self.configs.get(&chain_id)
}
pub fn get_or_err(&self, chain_id: u64) -> Result<&RpcProviderConfig, ConfigError> {
self.configs
.get(&chain_id)
.ok_or(ConfigError::MissingRpcForChain { chain_id })
}
pub fn insert(&mut self, chain_id: u64, config: RpcProviderConfig) -> Result<(), std::io::Error> {
if !is_supported_chain(chain_id) {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("Chain ID {} is not supported", chain_id),
));
}
self.configs.insert(chain_id, config);
Ok(())
}
pub fn contains(&self, chain_id: u64) -> bool {
self.configs.contains_key(&chain_id)
}
pub fn chain_ids(&self) -> impl Iterator<Item = &u64> {
self.configs.keys()
}
}
fn get_default_rpc_config(chain_id: u64) -> Option<RpcProviderConfig> {
match chain_id {
LOCAL_ANVIL => Some(RpcProviderConfig {
http: ANVIL_HTTP_URL.to_string(),
ws: ANVIL_WS_URL.to_string(),
}),
LOCAL_ANVIL_DESTINATION => Some(RpcProviderConfig {
http: ANVIL_DESTINATION_HTTP_URL.to_string(),
ws: ANVIL_DESTINATION_WS_URL.to_string(),
}),
ETHEREUM_MAINNET => Some(RpcProviderConfig {
http: MAINNET_HTTP_URL.to_string(),
ws: MAINNET_WS_URL.to_string(),
}),
SEPOLIA => Some(RpcProviderConfig {
http: SEPOLIA_HTTP_URL.to_string(),
ws: SEPOLIA_WS_URL.to_string(),
}),
BASE_MAINNET => Some(RpcProviderConfig {
http: BASE_MAINNET_HTTP_URL.to_string(),
ws: BASE_MAINNET_WS_URL.to_string(),
}),
BASE_SEPOLIA => Some(RpcProviderConfig {
http: BASE_SEPOLIA_HTTP_URL.to_string(),
ws: BASE_SEPOLIA_WS_URL.to_string(),
}),
OPTIMISM_MAINNET => Some(RpcProviderConfig {
http: OPTIMISM_MAINNET_HTTP_URL.to_string(),
ws: OPTIMISM_MAINNET_WS_URL.to_string(),
}),
OPTIMISM_SEPOLIA => Some(RpcProviderConfig {
http: OPTIMISM_SEPOLIA_HTTP_URL.to_string(),
ws: OPTIMISM_SEPOLIA_WS_URL.to_string(),
}),
ARBITRUM_ONE => Some(RpcProviderConfig {
http: ARBITRUM_ONE_HTTP_URL.to_string(),
ws: ARBITRUM_ONE_WS_URL.to_string(),
}),
ARBITRUM_SEPOLIA => Some(RpcProviderConfig {
http: ARBITRUM_SEPOLIA_HTTP_URL.to_string(),
ws: ARBITRUM_SEPOLIA_WS_URL.to_string(),
}),
POLYGON_MAINNET => Some(RpcProviderConfig {
http: POLYGON_MAINNET_HTTP_URL.to_string(),
ws: POLYGON_MAINNET_WS_URL.to_string(),
}),
POLYGON_AMOY => Some(RpcProviderConfig {
http: POLYGON_AMOY_HTTP_URL.to_string(),
ws: POLYGON_AMOY_WS_URL.to_string(),
}),
_ => None,
}
}
fn get_env_or_default(chain_specific_key: &str, global_key: &str, default: &str) -> String {
if let Ok(url) = std::env::var(chain_specific_key) {
if !url.is_empty() {
return url;
}
}
if let Ok(url) = std::env::var(global_key) {
if !url.is_empty() {
return url;
}
}
default.to_string()
}