use crate::eth::EthConfig;
use figment::{
Figment,
providers::{Env, Format, Serialized, Toml},
};
use serde::{Deserialize, Serialize};
use std::path::Path;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum ConfigError {
#[error("Failed to load configuration: {0}")]
LoadError(String),
#[error("Missing required configuration: {0}")]
MissingConfig(String),
#[error("Invalid configuration value: {0}")]
InvalidValue(String),
#[error("Environment error: {0}")]
EnvError(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DatabaseConfig {
pub url: String,
pub max_connections: u32,
pub timeout_seconds: u64,
#[serde(default = "default_store_messages")]
pub store_messages: bool,
#[serde(default = "default_db_batch_size")]
pub batch_size: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RedisConfig {
pub url: String,
#[serde(default = "default_redis_pool_size")]
pub pool_size: u32,
#[serde(default = "default_redis_batch_size")]
pub batch_size: usize,
#[serde(default = "default_enable_dead_letter")]
pub enable_dead_letter: bool,
#[serde(default = "default_consumer_rebalance_interval")]
pub consumer_rebalance_interval_seconds: u64,
#[serde(default = "default_metrics_collection_interval")]
pub metrics_collection_interval_seconds: u64,
#[serde(default = "default_connection_timeout_ms")]
pub connection_timeout_ms: u64,
#[serde(default = "default_idle_timeout_secs")]
pub idle_timeout_secs: u64,
#[serde(default = "default_max_connection_lifetime_secs")]
pub max_connection_lifetime_secs: u64,
}
fn default_redis_pool_size() -> u32 {
50 }
fn default_redis_batch_size() -> usize {
100 }
fn default_connection_timeout_ms() -> u64 {
5000 }
fn default_idle_timeout_secs() -> u64 {
300 }
fn default_max_connection_lifetime_secs() -> u64 {
300 }
fn default_enable_dead_letter() -> bool {
true }
fn default_consumer_rebalance_interval() -> u64 {
300 }
fn default_metrics_collection_interval() -> u64 {
60 }
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HubConfig {
pub url: String,
#[serde(default = "default_hub_max_concurrent_connections")]
pub max_concurrent_connections: u32,
#[serde(default = "default_hub_max_requests_per_second")]
pub max_requests_per_second: u32,
#[serde(default = "default_retry_attempts")]
pub retry_max_attempts: u32,
#[serde(default = "default_retry_base_delay_ms")]
pub retry_base_delay_ms: u64,
#[serde(default = "default_retry_max_delay_ms")]
pub retry_max_delay_ms: u64,
#[serde(default = "default_retry_jitter_factor")]
pub retry_jitter_factor: f32,
#[serde(default = "default_retry_timeout_ms")]
pub retry_timeout_ms: u64,
#[serde(default = "default_conn_timeout_ms")]
pub conn_timeout_ms: u64,
}
fn default_hub_max_concurrent_connections() -> u32 {
5 }
fn default_hub_max_requests_per_second() -> u32 {
10 }
fn default_retry_attempts() -> u32 {
5 }
fn default_retry_base_delay_ms() -> u64 {
100 }
fn default_retry_max_delay_ms() -> u64 {
30000 }
fn default_retry_jitter_factor() -> f32 {
0.25 }
fn default_retry_timeout_ms() -> u64 {
60000 }
fn default_conn_timeout_ms() -> u64 {
30000 }
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LoggingConfig {
pub format: String,
pub default_level: String,
pub dependency_filter: Option<String>,
}
impl Default for LoggingConfig {
fn default() -> Self {
Self {
format: "text".to_string(),
default_level: "info".to_string(),
dependency_filter: Some(
"hyper=warn,h2=warn,tower=info,tokio_util=warn,mio=warn,rustls=warn,tonic=info,want=warn,warp=warn,sqlx=warn".to_string()
),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StatsdConfig {
pub prefix: String,
pub addr: String,
pub use_tags: bool,
pub enabled: bool,
}
impl Default for StatsdConfig {
fn default() -> Self {
Self {
prefix: "way_read".to_string(),
addr: "127.0.0.1:8125".to_string(),
use_tags: false,
enabled: false,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpConfig {
#[serde(default = "default_mcp_enabled")]
pub enabled: bool,
#[serde(default = "default_mcp_bind_address")]
pub bind_address: String,
#[serde(default = "default_mcp_port")]
pub port: u16,
}
impl Default for McpConfig {
fn default() -> Self {
Self {
enabled: default_mcp_enabled(),
bind_address: default_mcp_bind_address(),
port: default_mcp_port(),
}
}
}
fn default_mcp_enabled() -> bool {
true
}
fn default_mcp_bind_address() -> String {
"127.0.0.1".to_string()
}
fn default_mcp_port() -> u16 {
8000
}
fn default_clear_db() -> bool {
false
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Config {
pub database: DatabaseConfig,
pub redis: RedisConfig,
pub hub: HubConfig,
#[serde(default)]
pub logging: LoggingConfig,
#[serde(default)]
pub backfill: BackfillConfig,
#[serde(default)]
pub statsd: StatsdConfig,
#[serde(default)]
pub mcp: McpConfig,
#[serde(default)]
pub eth: EthConfig,
#[serde(default = "default_clear_db")]
pub clear_db: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BackfillConfig {
pub concurrency: Option<usize>,
pub batch_size: Option<usize>,
}
impl Default for BackfillConfig {
fn default() -> Self {
Self { concurrency: Some(40), batch_size: Some(50) } }
}
fn default_store_messages() -> bool {
true }
fn default_db_batch_size() -> usize {
100 }
impl Default for DatabaseConfig {
fn default() -> Self {
Self {
url: "postgresql://localhost/waypoint".to_string(),
max_connections: 60, timeout_seconds: 30, store_messages: default_store_messages(),
batch_size: default_db_batch_size(),
}
}
}
impl Default for RedisConfig {
fn default() -> Self {
Self {
url: "redis://localhost:6379".to_string(),
pool_size: default_redis_pool_size(),
batch_size: default_redis_batch_size(),
enable_dead_letter: default_enable_dead_letter(),
consumer_rebalance_interval_seconds: default_consumer_rebalance_interval(),
metrics_collection_interval_seconds: default_metrics_collection_interval(),
connection_timeout_ms: default_connection_timeout_ms(),
idle_timeout_secs: default_idle_timeout_secs(),
max_connection_lifetime_secs: default_max_connection_lifetime_secs(),
}
}
}
impl Default for HubConfig {
fn default() -> Self {
Self {
url: "snapchain.farcaster.xyz:3383".to_string(),
max_concurrent_connections: default_hub_max_concurrent_connections(),
max_requests_per_second: default_hub_max_requests_per_second(),
retry_max_attempts: default_retry_attempts(),
retry_base_delay_ms: default_retry_base_delay_ms(),
retry_max_delay_ms: default_retry_max_delay_ms(),
retry_jitter_factor: default_retry_jitter_factor(),
retry_timeout_ms: default_retry_timeout_ms(),
conn_timeout_ms: default_conn_timeout_ms(),
}
}
}
impl Config {
pub fn load() -> Result<Self, ConfigError> {
let _ = dotenvy::dotenv().ok();
let mut figment = Figment::new()
.merge(Serialized::defaults(Config::default()))
.merge(Env::prefixed("WAYPOINT_").split("__"));
if let Some(config_path) = std::env::var_os("WAYPOINT_CONFIG") {
if let Some(path_str) = config_path.to_str() {
let path = Path::new(path_str);
if path.exists() {
figment = figment.merge(Toml::file(path));
}
}
}
figment.extract().map_err(|e| ConfigError::LoadError(e.to_string()))
}
pub fn validate(&self) -> Result<(), ConfigError> {
if self.database.url.is_empty() {
return Err(ConfigError::MissingConfig("Database URL is required".to_string()));
}
if self.redis.url.is_empty() {
return Err(ConfigError::MissingConfig("Redis URL is required".to_string()));
}
if self.hub.url.is_empty() {
return Err(ConfigError::MissingConfig("Hub URL is required".to_string()));
}
Ok(())
}
}