|
| 1 | +use std::collections::BTreeMap; |
| 2 | +use std::path::PathBuf; |
| 3 | + |
| 4 | +use serde::Deserialize; |
| 5 | + |
| 6 | +/// Serializable TOML representation of DevkitConfig. |
| 7 | +#[derive(Debug, Deserialize, Default)] |
| 8 | +struct DevkitConfigToml { |
| 9 | + db_path: Option<String>, |
| 10 | + scenario: Option<String>, |
| 11 | + port: Option<u16>, |
| 12 | + verbose: Option<bool>, |
| 13 | + horizon_url: Option<String>, |
| 14 | + poll_interval_secs: Option<u64>, |
| 15 | + retry_attempts: Option<u32>, |
| 16 | + base_retry_delay_ms: Option<u64>, |
| 17 | + simulation_duration: Option<u64>, |
| 18 | + simulation_base_fee: Option<u64>, |
| 19 | + simulation_spike_prob: Option<f64>, |
| 20 | + sandbox_time_offset_secs: Option<i64>, |
| 21 | + analysis_window_hours: Option<u32>, |
| 22 | +} |
| 23 | + |
| 24 | +/// Master configuration struct for the Stellar fee tracker devkit. |
| 25 | +/// |
| 26 | +/// Aggregates all configuration options across CLI, simulation, sandbox, |
| 27 | +/// and analysis modules. Can be loaded from a TOML file, environment |
| 28 | +/// variables, or constructed with defaults. |
| 29 | +#[derive(Debug, Clone)] |
| 30 | +pub struct DevkitConfig { |
| 31 | + /// Path to the fee database (SQLite). |
| 32 | + pub db_path: PathBuf, |
| 33 | + /// Default scenario file for mock data. |
| 34 | + pub scenario: String, |
| 35 | + /// Mock server port. |
| 36 | + pub port: u16, |
| 37 | + /// Whether to show detailed output. |
| 38 | + pub verbose: bool, |
| 39 | + /// Horizon API base URL. |
| 40 | + pub horizon_url: String, |
| 41 | + /// Polling interval in seconds for fee data collection. |
| 42 | + pub poll_interval_secs: u64, |
| 43 | + /// Maximum number of retry attempts for failed requests. |
| 44 | + pub retry_attempts: u32, |
| 45 | + /// Base delay in milliseconds between retries. |
| 46 | + pub base_retry_delay_ms: u64, |
| 47 | + /// Number of ledgers to simulate. |
| 48 | + pub simulation_duration: u64, |
| 49 | + /// Base fee floor in stroops for simulation. |
| 50 | + pub simulation_base_fee: u64, |
| 51 | + /// Probability of a fee spike on any given ledger [0.0, 1.0]. |
| 52 | + pub simulation_spike_prob: f64, |
| 53 | + /// Sandbox time travel offset in seconds. |
| 54 | + pub sandbox_time_offset_secs: i64, |
| 55 | + /// Analysis window size in hours. |
| 56 | + pub analysis_window_hours: u32, |
| 57 | + /// Custom key-value overrides. |
| 58 | + pub overrides: BTreeMap<String, String>, |
| 59 | +} |
| 60 | + |
| 61 | +impl Default for DevkitConfig { |
| 62 | + fn default() -> Self { |
| 63 | + Self { |
| 64 | + db_path: PathBuf::from("stellar_fees.db"), |
| 65 | + scenario: String::from("normal"), |
| 66 | + port: 8090, |
| 67 | + verbose: false, |
| 68 | + horizon_url: String::from("https://horizon-testnet.stellar.org"), |
| 69 | + poll_interval_secs: 10, |
| 70 | + retry_attempts: 3, |
| 71 | + base_retry_delay_ms: 1000, |
| 72 | + simulation_duration: 1000, |
| 73 | + simulation_base_fee: 100, |
| 74 | + simulation_spike_prob: 0.05, |
| 75 | + sandbox_time_offset_secs: 0, |
| 76 | + analysis_window_hours: 24, |
| 77 | + overrides: BTreeMap::new(), |
| 78 | + } |
| 79 | + } |
| 80 | +} |
| 81 | + |
| 82 | +impl DevkitConfig { |
| 83 | + /// Load configuration from a TOML file. |
| 84 | + pub fn from_toml_file(path: &PathBuf) -> Result<Self, String> { |
| 85 | + let content = |
| 86 | + std::fs::read_to_string(path).map_err(|e| format!("Failed to read config file: {}", e))?; |
| 87 | + let toml_cfg: DevkitConfigToml = |
| 88 | + toml::from_str(&content).map_err(|e| format!("Failed to parse config file: {}", e))?; |
| 89 | + |
| 90 | + let mut cfg = Self::default(); |
| 91 | + if let Some(v) = toml_cfg.db_path { |
| 92 | + cfg.db_path = PathBuf::from(v); |
| 93 | + } |
| 94 | + if let Some(v) = toml_cfg.scenario { |
| 95 | + cfg.scenario = v; |
| 96 | + } |
| 97 | + if let Some(v) = toml_cfg.port { |
| 98 | + cfg.port = v; |
| 99 | + } |
| 100 | + if let Some(v) = toml_cfg.verbose { |
| 101 | + cfg.verbose = v; |
| 102 | + } |
| 103 | + if let Some(v) = toml_cfg.horizon_url { |
| 104 | + cfg.horizon_url = v; |
| 105 | + } |
| 106 | + if let Some(v) = toml_cfg.poll_interval_secs { |
| 107 | + cfg.poll_interval_secs = v; |
| 108 | + } |
| 109 | + if let Some(v) = toml_cfg.retry_attempts { |
| 110 | + cfg.retry_attempts = v; |
| 111 | + } |
| 112 | + if let Some(v) = toml_cfg.base_retry_delay_ms { |
| 113 | + cfg.base_retry_delay_ms = v; |
| 114 | + } |
| 115 | + if let Some(v) = toml_cfg.simulation_duration { |
| 116 | + cfg.simulation_duration = v; |
| 117 | + } |
| 118 | + if let Some(v) = toml_cfg.simulation_base_fee { |
| 119 | + cfg.simulation_base_fee = v; |
| 120 | + } |
| 121 | + if let Some(v) = toml_cfg.simulation_spike_prob { |
| 122 | + cfg.simulation_spike_prob = v; |
| 123 | + } |
| 124 | + if let Some(v) = toml_cfg.sandbox_time_offset_secs { |
| 125 | + cfg.sandbox_time_offset_secs = v; |
| 126 | + } |
| 127 | + if let Some(v) = toml_cfg.analysis_window_hours { |
| 128 | + cfg.analysis_window_hours = v; |
| 129 | + } |
| 130 | + |
| 131 | + Ok(cfg) |
| 132 | + } |
| 133 | + |
| 134 | + /// Load configuration from environment variables. |
| 135 | + pub fn from_env() -> Self { |
| 136 | + let mut cfg = Self::default(); |
| 137 | + cfg.apply_env(); |
| 138 | + cfg |
| 139 | + } |
| 140 | + |
| 141 | + /// Apply environment variable overrides on top of the current config. |
| 142 | + pub fn apply_env(&mut self) { |
| 143 | + if let Ok(v) = std::env::var("DEVKIT_DB_PATH") { |
| 144 | + self.db_path = PathBuf::from(v); |
| 145 | + } |
| 146 | + if let Ok(v) = std::env::var("DEVKIT_SCENARIO") { |
| 147 | + self.scenario = v; |
| 148 | + } |
| 149 | + if let Ok(v) = std::env::var("DEVKIT_PORT") { |
| 150 | + self.port = v.parse().unwrap_or(self.port); |
| 151 | + } |
| 152 | + if let Ok(v) = std::env::var("DEVKIT_VERBOSE") { |
| 153 | + self.verbose = v == "true" || v == "1"; |
| 154 | + } |
| 155 | + if let Ok(v) = std::env::var("DEVKIT_HORIZON_URL") { |
| 156 | + self.horizon_url = v; |
| 157 | + } |
| 158 | + if let Ok(v) = std::env::var("DEVKIT_POLL_INTERVAL_SECS") { |
| 159 | + self.poll_interval_secs = v.parse().unwrap_or(self.poll_interval_secs); |
| 160 | + } |
| 161 | + if let Ok(v) = std::env::var("DEVKIT_RETRY_ATTEMPTS") { |
| 162 | + self.retry_attempts = v.parse().unwrap_or(self.retry_attempts); |
| 163 | + } |
| 164 | + if let Ok(v) = std::env::var("DEVKIT_BASE_RETRY_DELAY_MS") { |
| 165 | + self.base_retry_delay_ms = v.parse().unwrap_or(self.base_retry_delay_ms); |
| 166 | + } |
| 167 | + if let Ok(v) = std::env::var("DEVKIT_SIMULATION_DURATION") { |
| 168 | + self.simulation_duration = v.parse().unwrap_or(self.simulation_duration); |
| 169 | + } |
| 170 | + if let Ok(v) = std::env::var("DEVKIT_SIMULATION_BASE_FEE") { |
| 171 | + self.simulation_base_fee = v.parse().unwrap_or(self.simulation_base_fee); |
| 172 | + } |
| 173 | + if let Ok(v) = std::env::var("DEVKIT_SIMULATION_SPIKE_PROB") { |
| 174 | + self.simulation_spike_prob = v.parse().unwrap_or(self.simulation_spike_prob); |
| 175 | + } |
| 176 | + if let Ok(v) = std::env::var("DEVKIT_SANDBOX_TIME_OFFSET_SECS") { |
| 177 | + self.sandbox_time_offset_secs = v.parse().unwrap_or(self.sandbox_time_offset_secs); |
| 178 | + } |
| 179 | + if let Ok(v) = std::env::var("DEVKIT_ANALYSIS_WINDOW_HOURS") { |
| 180 | + self.analysis_window_hours = v.parse().unwrap_or(self.analysis_window_hours); |
| 181 | + } |
| 182 | + } |
| 183 | + |
| 184 | + /// Display the full configuration as a formatted key/value report. |
| 185 | + pub fn display(&self) -> String { |
| 186 | + let mut out = String::new(); |
| 187 | + out.push_str("devkit configuration\n"); |
| 188 | + out.push_str("====================\n"); |
| 189 | + |
| 190 | + let fields = [ |
| 191 | + ("db_path", &self.db_path.display().to_string()), |
| 192 | + ("scenario", &self.scenario), |
| 193 | + ("port", &self.port.to_string()), |
| 194 | + ("verbose", &self.verbose.to_string()), |
| 195 | + ("horizon_url", &self.horizon_url), |
| 196 | + ("poll_interval_secs", &self.poll_interval_secs.to_string()), |
| 197 | + ("retry_attempts", &self.retry_attempts.to_string()), |
| 198 | + ("base_retry_delay_ms", &self.base_retry_delay_ms.to_string()), |
| 199 | + ("simulation_duration", &self.simulation_duration.to_string()), |
| 200 | + ("simulation_base_fee", &self.simulation_base_fee.to_string()), |
| 201 | + ("simulation_spike_prob", &self.simulation_spike_prob.to_string()), |
| 202 | + ( |
| 203 | + "sandbox_time_offset_secs", |
| 204 | + &self.sandbox_time_offset_secs.to_string(), |
| 205 | + ), |
| 206 | + ( |
| 207 | + "analysis_window_hours", |
| 208 | + &self.analysis_window_hours.to_string(), |
| 209 | + ), |
| 210 | + ]; |
| 211 | + |
| 212 | + out.push_str(&format!( |
| 213 | + "{:<12} {:<30} {}\n", |
| 214 | + "key", "value", "source" |
| 215 | + )); |
| 216 | + for (key, value) in &fields { |
| 217 | + let source = if std::env::var(format!("DEVKIT_{}", key.to_uppercase())).is_ok() { |
| 218 | + "env" |
| 219 | + } else { |
| 220 | + "default" |
| 221 | + }; |
| 222 | + out.push_str(&format!("{:<12} {:<30} {}\n", key, value, source)); |
| 223 | + } |
| 224 | + |
| 225 | + if !self.overrides.is_empty() { |
| 226 | + out.push_str("overrides:\n"); |
| 227 | + for (k, v) in &self.overrides { |
| 228 | + out.push_str(&format!(" {} = {}\n", k, v)); |
| 229 | + } |
| 230 | + } |
| 231 | + out |
| 232 | + } |
| 233 | +} |
0 commit comments