oxidized_builder/
config.rs1use crate::common::constants;
2use crate::common::error::AppError;
3use alloy::primitives::Address;
4use config::{Config, Environment, File};
5use serde::{Deserialize, Deserializer};
6use std::collections::HashMap;
7use std::str::FromStr;
8
9#[derive(Debug, Deserialize, Clone)]
10pub struct GlobalSettings {
11 #[serde(default = "default_debug")]
13 pub debug: bool,
14 #[serde(
15 default = "default_chain",
16 deserialize_with = "deserialize_chain_list"
17 )]
18 pub chains: Vec<u64>,
19
20 pub wallet_key: String,
22 pub wallet_address: Address,
23 pub profit_receiver_address: Option<Address>,
24
25 #[serde(default = "default_max_gas")]
27 pub max_gas_price_gwei: u64,
28 #[serde(default = "default_sim_backend")]
29 pub simulation_backend: String, #[serde(default = "default_true")]
33 pub flashloan_enabled: bool,
34 #[serde(default = "default_true")]
35 pub sandwich_attacks_enabled: bool,
36
37 pub rpc_urls: Option<HashMap<String, String>>,
42 pub ws_urls: Option<HashMap<String, String>>,
43 pub chainlink_feeds: Option<HashMap<String, String>>, pub flashbots_relay_url: Option<String>,
45 pub bundle_signer_key: Option<String>,
46 #[serde(default = "default_metrics_port")]
47 pub metrics_port: u16,
48 #[serde(default = "default_true")]
49 pub strategy_enabled: bool,
50 #[serde(default = "default_slippage_bps")]
51 pub slippage_bps: u64,
52 pub gas_caps_gwei: Option<HashMap<String, u64>>,
53 #[serde(default = "default_mev_share_url")]
54 pub mev_share_stream_url: String,
55 #[serde(default = "default_mev_share_history_limit")]
56 pub mev_share_history_limit: u32,
57 #[serde(default = "default_true")]
58 pub mev_share_enabled: bool,
59
60 pub router_allowlist_by_chain: Option<HashMap<String, HashMap<String, String>>>,
62 pub chainlink_feeds_by_chain: Option<HashMap<String, HashMap<String, String>>>,
63}
64
65fn default_debug() -> bool {
67 false
68}
69fn default_chain() -> Vec<u64> {
70 vec![1]
71}
72fn default_max_gas() -> u64 {
73 200
74}
75fn default_true() -> bool {
76 true
77}
78fn default_metrics_port() -> u16 {
79 9000
80}
81fn default_slippage_bps() -> u64 {
82 50
83}
84fn default_sim_backend() -> String {
85 "revm".to_string()
86}
87fn default_mev_share_url() -> String {
88 "https://mev-share.flashbots.net".to_string()
89}
90fn default_mev_share_history_limit() -> u32 {
91 200
92}
93
94fn deserialize_chain_list<'de, D>(deserializer: D) -> Result<Vec<u64>, D::Error>
95where
96 D: Deserializer<'de>,
97{
98 use serde::de::{Error, SeqAccess, Visitor};
99 use std::fmt;
100
101 struct ChainVisitor;
102
103 impl<'de> Visitor<'de> for ChainVisitor {
104 type Value = Vec<u64>;
105
106 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
107 formatter.write_str("a sequence of chain ids or a string with comma-separated ids")
108 }
109
110 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
111 where
112 E: Error,
113 {
114 parse_chain_list(v).map_err(E::custom)
115 }
116
117 fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
118 where
119 A: SeqAccess<'de>,
120 {
121 let mut out = Vec::new();
122 while let Some(elem) = seq.next_element::<u64>()? {
123 out.push(elem);
124 }
125 Ok(out)
126 }
127 }
128
129 deserializer.deserialize_any(ChainVisitor)
130}
131
132impl GlobalSettings {
133 pub fn load_with_path(path: Option<&str>) -> Result<Self, AppError> {
134 dotenvy::dotenv().ok();
136
137 let mut builder = Config::builder();
138
139 if let Some(path) = path {
140 builder = builder.add_source(File::with_name(path).required(true));
141 } else {
142 builder = builder.add_source(File::with_name("config").required(false));
143 }
144
145 builder = builder
146 .add_source(Environment::default());
148
149 let mut settings: GlobalSettings = builder.build()?.try_deserialize()?;
150
151 if let Ok(chains_str) = std::env::var("CHAINS") {
153 settings.chains = parse_chain_list(&chains_str)?;
154 }
155
156 if settings.wallet_key.is_empty() {
158 return Err(AppError::Config("WALLET_KEY is missing".to_string()));
159 }
160
161 Ok(settings)
162 }
163
164 pub fn load() -> Result<Self, AppError> {
165 Self::load_with_path(None)
166 }
167
168 pub fn get_rpc_url(&self, chain_id: u64) -> Result<String, AppError> {
170 if let Some(urls) = &self.rpc_urls {
172 if let Some(url) = urls.get(&chain_id.to_string()) {
173 return Ok(url.clone());
174 }
175 }
176
177 let env_key = format!("RPC_URL_{}", chain_id);
179 std::env::var(&env_key)
180 .map_err(|_| AppError::Config(format!("No RPC URL found for chain {}", chain_id)))
181 }
182
183 pub fn get_ws_url(&self, chain_id: u64) -> Result<String, AppError> {
185 if let Some(urls) = &self.ws_urls {
186 if let Some(url) = urls.get(&chain_id.to_string()) {
187 return Ok(url.clone());
188 }
189 }
190
191 let candidates = [
192 format!("WS_URL_{}", chain_id),
193 format!("WEBSOCKET_URL_{}", chain_id),
194 ];
195
196 for key in candidates {
197 if let Ok(v) = std::env::var(&key) {
198 return Ok(v);
199 }
200 }
201
202 Err(AppError::Config(format!(
203 "No WS URL found for chain {}",
204 chain_id
205 )))
206 }
207
208 pub fn get_chainlink_feed(&self, symbol: &str) -> Option<String> {
209 self.chainlink_feeds
210 .as_ref()
211 .and_then(|m| m.get(&symbol.to_uppercase()).cloned())
212 }
213
214 pub fn flashbots_relay_url(&self) -> String {
215 self.flashbots_relay_url
216 .clone()
217 .or_else(|| std::env::var("FLASHBOTS_RELAY_URL").ok())
218 .unwrap_or_else(|| "https://relay.flashbots.net".to_string())
219 }
220
221 pub fn bundle_signer_key(&self) -> String {
222 self.bundle_signer_key
223 .clone()
224 .or_else(|| std::env::var("BUNDLE_SIGNER_KEY").ok())
225 .unwrap_or_else(|| self.wallet_key.clone())
226 }
227
228 pub fn gas_cap_for_chain(&self, chain_id: u64) -> Option<u64> {
229 self.gas_caps_gwei
230 .as_ref()
231 .and_then(|m| m.get(&chain_id.to_string()).cloned())
232 }
233
234 pub fn routers_for_chain(
235 &self,
236 chain_id: u64,
237 ) -> Result<HashMap<String, Address>, AppError> {
238 if let Some(map) = self
239 .router_allowlist_by_chain
240 .as_ref()
241 .and_then(|m| m.get(&chain_id.to_string()))
242 {
243 return parse_address_map(map, "router_allowlist_by_chain");
244 }
245
246 Ok(constants::default_routers_for_chain(chain_id))
247 }
248
249 pub fn chainlink_feeds_for_chain(
250 &self,
251 chain_id: u64,
252 ) -> Result<HashMap<String, Address>, AppError> {
253 if let Some(map) = self
254 .chainlink_feeds_by_chain
255 .as_ref()
256 .and_then(|m| m.get(&chain_id.to_string()))
257 {
258 return parse_address_map(map, "chainlink_feeds_by_chain");
259 }
260
261 if let Some(map) = &self.chainlink_feeds {
262 return parse_address_map(map, "chainlink_feeds");
263 }
264
265 Ok(constants::default_chainlink_feeds(chain_id))
266 }
267}
268
269fn parse_chain_list(raw: &str) -> Result<Vec<u64>, AppError> {
270 let cleaned = raw.trim_matches(|c| c == '`' || c == '"' || c == '\'');
271 let mut out = Vec::new();
272 for part in cleaned.split(|c: char| c == ',' || c.is_whitespace()) {
273 let p = part.trim();
274 if p.is_empty() {
275 continue;
276 }
277 let id: u64 = p
278 .parse()
279 .map_err(|_| AppError::Config(format!("Invalid chain id '{}'", p)))?;
280 out.push(id);
281 }
282 if out.is_empty() {
283 return Err(AppError::Config("CHAINS env is empty".into()));
284 }
285 Ok(out)
286}
287
288fn parse_address_map(
289 raw: &HashMap<String, String>,
290 field: &str,
291) -> Result<HashMap<String, Address>, AppError> {
292 raw.iter()
293 .map(|(k, v)| {
294 Address::from_str(v)
295 .map(|addr| (k.to_uppercase(), addr))
296 .map_err(|_| AppError::InvalidAddress(format!("{field}:{k} -> {v}")))
297 })
298 .collect()
299}