riglr_config/
network.rs

1//! Network and blockchain configuration
2
3use crate::{ConfigError, ConfigResult};
4use once_cell::sync::Lazy;
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7
8/// Trait for validating blockchain addresses
9///
10/// This trait allows different blockchain address validation logic to be plugged into
11/// the configuration system without creating tight coupling to specific blockchain crates.
12pub trait AddressValidator: Send + Sync {
13    /// Validate an address string
14    ///
15    /// # Arguments
16    /// * `address` - The address string to validate
17    /// * `contract_name` - The name of the contract (for error messages)
18    ///
19    /// # Returns
20    /// * `Ok(())` if the address is valid
21    /// * `Err(ConfigError)` with details if the address is invalid
22    fn validate(&self, address: &str, contract_name: &str) -> ConfigResult<()>;
23}
24
25const RIGLR_CHAINS_CONFIG: &str = "RIGLR_CHAINS_CONFIG";
26
27// Test environment variable constants
28#[cfg(test)]
29mod test_env_vars {
30    pub const RPC_URL_1: &str = "RPC_URL_1";
31    pub const RPC_URL_137: &str = "RPC_URL_137";
32    pub const RPC_URL_INVALID: &str = "RPC_URL_INVALID";
33    pub const NOT_RPC_URL_1: &str = "NOT_RPC_URL_1";
34    pub const ROUTER_1: &str = "ROUTER_1";
35    pub const QUOTER_1: &str = "QUOTER_1";
36    pub const FACTORY_137: &str = "FACTORY_137";
37
38    /// Helper function to set environment variables in tests without using string literals
39    pub fn set_test_env_var(key: &'static str, value: &str) {
40        std::env::set_var(key, value);
41    }
42
43    /// Helper function to remove environment variables in tests without using string literals  
44    pub fn remove_test_env_var(key: &'static str) {
45        std::env::remove_var(key);
46    }
47}
48
49/// Network configuration
50#[derive(Debug, Clone, Deserialize, Serialize)]
51pub struct NetworkConfig {
52    /// Solana RPC URL
53    pub solana_rpc_url: String,
54
55    /// Solana WebSocket URL (optional)
56    #[serde(default)]
57    pub solana_ws_url: Option<String>,
58
59    /// EVM RPC URLs using RPC_URL_{CHAIN_ID} convention
60    /// This is populated dynamically from environment variables
61    #[serde(default, skip_serializing)]
62    pub rpc_urls: HashMap<String, String>,
63
64    /// Chain-specific contract addresses
65    #[serde(default, skip_serializing)]
66    pub chains: HashMap<u64, ChainConfig>,
67
68    /// Default chain ID to use
69    #[serde(default = "default_chain_id")]
70    pub default_chain_id: u64,
71
72    /// Network timeouts
73    #[serde(flatten)]
74    pub timeouts: NetworkTimeouts,
75}
76
77/// Chain-specific configuration
78#[derive(Debug, Clone, Deserialize, Serialize)]
79pub struct ChainConfig {
80    /// Chain ID
81    pub id: u64,
82
83    /// Human-readable chain name
84    pub name: String,
85
86    /// RPC URL (overrides global RPC_URL_{CHAIN_ID} if set)
87    #[serde(default)]
88    pub rpc_url: Option<String>,
89
90    /// Contract addresses for this chain
91    #[serde(flatten)]
92    pub contracts: ChainContract,
93
94    /// Block explorer URL
95    #[serde(default)]
96    pub explorer_url: Option<String>,
97
98    /// Native token symbol
99    #[serde(default)]
100    pub native_token: Option<String>,
101
102    /// Whether this is a testnet
103    #[serde(default)]
104    pub is_testnet: bool,
105}
106
107/// Contract addresses for a chain
108#[derive(Debug, Clone, Default, Deserialize, Serialize)]
109pub struct ChainContract {
110    /// Uniswap V3 router address
111    #[serde(default)]
112    pub router: Option<String>,
113
114    /// Uniswap V3 quoter address
115    #[serde(default)]
116    pub quoter: Option<String>,
117
118    /// Uniswap V3 factory address
119    #[serde(default)]
120    pub factory: Option<String>,
121
122    /// WETH/WNATIVE address
123    #[serde(default)]
124    pub weth: Option<String>,
125
126    /// USDC address
127    #[serde(default)]
128    pub usdc: Option<String>,
129
130    /// USDT address
131    #[serde(default)]
132    pub usdt: Option<String>,
133
134    /// SushiSwap router address
135    #[serde(default)]
136    pub sushiswap_router: Option<String>,
137
138    /// SushiSwap factory address
139    #[serde(default)]
140    pub sushiswap_factory: Option<String>,
141
142    /// Aave V3 pool address
143    #[serde(default)]
144    pub aave_v3_pool: Option<String>,
145
146    /// Aave V3 pool data provider address
147    #[serde(default)]
148    pub aave_v3_pool_data_provider: Option<String>,
149
150    /// Aave V3 oracle address
151    #[serde(default)]
152    pub aave_v3_oracle: Option<String>,
153
154    /// Compound V3 USDC comet address
155    #[serde(default)]
156    pub compound_v3_usdc: Option<String>,
157
158    /// Curve registry address
159    #[serde(default)]
160    pub curve_registry: Option<String>,
161
162    /// 1inch aggregation router address
163    #[serde(default)]
164    pub oneinch_aggregation_router: Option<String>,
165
166    /// Balancer V2 vault address
167    #[serde(default)]
168    pub balancer_vault: Option<String>,
169
170    /// QuickSwap router address (Polygon-specific)
171    #[serde(default)]
172    pub quickswap_router: Option<String>,
173
174    /// GMX router address (Arbitrum-specific)
175    #[serde(default)]
176    pub gmx_router: Option<String>,
177
178    /// MakerDAO DAI token address
179    #[serde(default)]
180    pub maker_dai: Option<String>,
181
182    /// Additional custom contracts
183    #[serde(default)]
184    pub custom: HashMap<String, String>,
185}
186
187/// Network timeout configuration
188#[derive(Debug, Clone, Deserialize, Serialize)]
189pub struct NetworkTimeouts {
190    /// RPC request timeout in seconds
191    #[serde(default = "default_rpc_timeout")]
192    pub rpc_timeout_secs: u64,
193
194    /// WebSocket connection timeout in seconds
195    #[serde(default = "default_ws_timeout")]
196    pub ws_timeout_secs: u64,
197
198    /// HTTP request timeout in seconds
199    #[serde(default = "default_http_timeout")]
200    pub http_timeout_secs: u64,
201}
202
203/// Network name to chain ID mapping
204static NETWORK_NAME_MAP: Lazy<HashMap<&'static str, u64>> = Lazy::new(|| {
205    let mut map = HashMap::new();
206
207    // Ethereum networks
208    map.insert("ethereum", 1);
209    map.insert("mainnet", 1);
210    map.insert("eth", 1);
211    map.insert("goerli", 5);
212    map.insert("sepolia", 11155111);
213
214    // Layer 2 networks
215    map.insert("optimism", 10);
216    map.insert("op", 10);
217    map.insert("arbitrum", 42161);
218    map.insert("arb", 42161);
219    map.insert("arbitrum_goerli", 421613);
220    map.insert("base", 8453);
221    map.insert("base_goerli", 84531);
222
223    // Other EVM chains
224    map.insert("polygon", 137);
225    map.insert("matic", 137);
226    map.insert("polygon_mumbai", 80001);
227    map.insert("mumbai", 80001);
228    map.insert("bsc", 56);
229    map.insert("binance", 56);
230    map.insert("bnb", 56);
231    map.insert("bsc_testnet", 97);
232    map.insert("avalanche", 43114);
233    map.insert("avax", 43114);
234    map.insert("avalanche_fuji", 43113);
235    map.insert("fuji", 43113);
236    map.insert("fantom", 250);
237    map.insert("ftm", 250);
238    map.insert("fantom_testnet", 4002);
239    map.insert("gnosis", 100);
240    map.insert("xdai", 100);
241    map.insert("celo", 42220);
242    map.insert("moonbeam", 1284);
243    map.insert("moonriver", 1285);
244    map.insert("aurora", 1313161554);
245    map.insert("harmony", 1666600000);
246    map.insert("metis", 1088);
247    map.insert("cronos", 25);
248    map.insert("kava", 2222);
249    map.insert("scroll", 534352);
250    map.insert("scroll_sepolia", 534351);
251    map.insert("zksync", 324);
252    map.insert("zksync_era", 324);
253    map.insert("zksync_testnet", 280);
254    map.insert("linea", 59144);
255    map.insert("linea_goerli", 59140);
256    map.insert("mantle", 5000);
257    map.insert("mantle_testnet", 5001);
258
259    map
260});
261
262/// Resolve network name aliases to chain IDs
263fn resolve_network_name(name: &str) -> Option<u64> {
264    NETWORK_NAME_MAP.get(&name.to_lowercase().as_str()).copied()
265}
266
267impl NetworkConfig {
268    /// Extract RPC URLs from environment using RPC_URL_{CHAIN_ID} or RPC_URL_{NETWORK_NAME} convention
269    pub fn extract_rpc_urls(&mut self) {
270        for (key, value) in std::env::vars() {
271            if let Some(chain_identifier) = key.strip_prefix("RPC_URL_") {
272                // First try to parse as a numeric chain ID
273                if let Ok(chain_id) = chain_identifier.parse::<u64>() {
274                    self.rpc_urls.insert(chain_id.to_string(), value);
275                } else if let Some(chain_id) = resolve_network_name(chain_identifier) {
276                    // If not numeric, try to resolve as a network name
277                    self.rpc_urls.insert(chain_id.to_string(), value);
278                }
279            }
280        }
281    }
282
283    /// Load chain contracts from chains.toml file
284    pub fn load_chain_contracts(&mut self) -> ConfigResult<()> {
285        let chains_path =
286            std::env::var(RIGLR_CHAINS_CONFIG).unwrap_or_else(|_| "chains.toml".to_string());
287
288        // Only try to load if file exists
289        if !std::path::Path::new(&chains_path).exists() {
290            tracing::debug!("chains.toml not found at {}, using defaults", chains_path);
291            return Ok(());
292        }
293
294        let content = std::fs::read_to_string(&chains_path).map_err(|e| {
295            ConfigError::io(format!(
296                "Failed to read chains config from {}: {}",
297                chains_path, e
298            ))
299        })?;
300
301        let chains_file: ChainsFile = toml::from_str(&content)
302            .map_err(|e| ConfigError::parse(format!("Failed to parse chains.toml: {}", e)))?;
303
304        // Convert from TOML structure to our internal structure
305        for (_name, toml_chain) in chains_file.chains {
306            let mut chain: ChainConfig = toml_chain.into();
307
308            // Apply environment variable overrides
309            if let Ok(router) = std::env::var(format!("ROUTER_{}", chain.id)) {
310                chain.contracts.router = Some(router);
311            }
312            if let Ok(quoter) = std::env::var(format!("QUOTER_{}", chain.id)) {
313                chain.contracts.quoter = Some(quoter);
314            }
315            if let Ok(factory) = std::env::var(format!("FACTORY_{}", chain.id)) {
316                chain.contracts.factory = Some(factory);
317            }
318            if let Ok(weth) = std::env::var(format!("WETH_{}", chain.id)) {
319                chain.contracts.weth = Some(weth);
320            }
321            if let Ok(usdc) = std::env::var(format!("USDC_{}", chain.id)) {
322                chain.contracts.usdc = Some(usdc);
323            }
324            if let Ok(usdt) = std::env::var(format!("USDT_{}", chain.id)) {
325                chain.contracts.usdt = Some(usdt);
326            }
327            // Apply overrides for new DeFi protocol fields
328            if let Ok(sushiswap_router) = std::env::var(format!("SUSHISWAP_ROUTER_{}", chain.id)) {
329                chain.contracts.sushiswap_router = Some(sushiswap_router);
330            }
331            if let Ok(sushiswap_factory) = std::env::var(format!("SUSHISWAP_FACTORY_{}", chain.id))
332            {
333                chain.contracts.sushiswap_factory = Some(sushiswap_factory);
334            }
335            if let Ok(aave_v3_pool) = std::env::var(format!("AAVE_V3_POOL_{}", chain.id)) {
336                chain.contracts.aave_v3_pool = Some(aave_v3_pool);
337            }
338            if let Ok(aave_v3_pool_data_provider) =
339                std::env::var(format!("AAVE_V3_POOL_DATA_PROVIDER_{}", chain.id))
340            {
341                chain.contracts.aave_v3_pool_data_provider = Some(aave_v3_pool_data_provider);
342            }
343            if let Ok(aave_v3_oracle) = std::env::var(format!("AAVE_V3_ORACLE_{}", chain.id)) {
344                chain.contracts.aave_v3_oracle = Some(aave_v3_oracle);
345            }
346            if let Ok(compound_v3_usdc) = std::env::var(format!("COMPOUND_V3_USDC_{}", chain.id)) {
347                chain.contracts.compound_v3_usdc = Some(compound_v3_usdc);
348            }
349            if let Ok(curve_registry) = std::env::var(format!("CURVE_REGISTRY_{}", chain.id)) {
350                chain.contracts.curve_registry = Some(curve_registry);
351            }
352            if let Ok(oneinch_aggregation_router) =
353                std::env::var(format!("ONEINCH_AGGREGATION_ROUTER_{}", chain.id))
354            {
355                chain.contracts.oneinch_aggregation_router = Some(oneinch_aggregation_router);
356            }
357            if let Ok(balancer_vault) = std::env::var(format!("BALANCER_VAULT_{}", chain.id)) {
358                chain.contracts.balancer_vault = Some(balancer_vault);
359            }
360            if let Ok(quickswap_router) = std::env::var(format!("QUICKSWAP_ROUTER_{}", chain.id)) {
361                chain.contracts.quickswap_router = Some(quickswap_router);
362            }
363            if let Ok(gmx_router) = std::env::var(format!("GMX_ROUTER_{}", chain.id)) {
364                chain.contracts.gmx_router = Some(gmx_router);
365            }
366            if let Ok(maker_dai) = std::env::var(format!("MAKER_DAI_{}", chain.id)) {
367                chain.contracts.maker_dai = Some(maker_dai);
368            }
369
370            self.chains.insert(chain.id, chain);
371        }
372
373        Ok(())
374    }
375
376    /// Get RPC URL for a specific chain ID or network name
377    pub fn get_rpc_url(&self, chain_identifier: &str) -> Option<String> {
378        // First try to parse as a numeric chain ID
379        let chain_id = if let Ok(id) = chain_identifier.parse::<u64>() {
380            id
381        } else if let Some(id) = resolve_network_name(chain_identifier) {
382            // If not numeric, try to resolve as a network name
383            id
384        } else {
385            // Unknown identifier
386            return None;
387        };
388
389        // First check chain-specific config
390        if let Some(chain) = self.chains.get(&chain_id) {
391            if let Some(ref url) = chain.rpc_url {
392                return Some(url.clone());
393            }
394        }
395
396        // Then check dynamic RPC URLs
397        self.rpc_urls.get(&chain_id.to_string()).cloned()
398    }
399
400    /// Get RPC URL for a specific numeric chain ID (backward compatibility)
401    pub fn get_rpc_url_by_id(&self, chain_id: u64) -> Option<String> {
402        self.get_rpc_url(&chain_id.to_string())
403    }
404
405    /// Get chain configuration
406    pub fn get_chain(&self, chain_id: u64) -> Option<&ChainConfig> {
407        self.chains.get(&chain_id)
408    }
409
410    /// Get all supported chain IDs
411    pub fn get_supported_chains(&self) -> Vec<u64> {
412        let mut chains: Vec<u64> = self
413            .rpc_urls
414            .keys()
415            .filter_map(|k| k.parse::<u64>().ok())
416            .collect();
417
418        // Add chains from config
419        chains.extend(self.chains.keys());
420
421        // Deduplicate
422        chains.sort_unstable();
423        chains.dedup();
424
425        chains
426    }
427
428    /// Validates the network configuration
429    ///
430    /// Checks that all URLs are properly formatted and optionally validates contract addresses
431    /// if an address validator is provided.
432    ///
433    /// # Arguments
434    /// * `address_validator` - Optional validator for blockchain addresses. If None, address validation is skipped.
435    pub fn validate_config(
436        &self,
437        address_validator: Option<&dyn AddressValidator>,
438    ) -> ConfigResult<()> {
439        self.validate_solana_rpc_url()?;
440        self.validate_rpc_urls()?;
441        self.validate_chain_configs(address_validator)?;
442        Ok(())
443    }
444
445    /// Validates the Solana RPC URL
446    fn validate_solana_rpc_url(&self) -> ConfigResult<()> {
447        if !self.solana_rpc_url.starts_with("http://")
448            && !self.solana_rpc_url.starts_with("https://")
449        {
450            return Err(ConfigError::validation(
451                "SOLANA_RPC_URL must be a valid HTTP(S) URL",
452            ));
453        }
454        Ok(())
455    }
456
457    /// Validates all RPC URLs
458    fn validate_rpc_urls(&self) -> ConfigResult<()> {
459        for (chain_id, url) in &self.rpc_urls {
460            if !Self::is_valid_rpc_url(url) {
461                return Err(ConfigError::validation(format!(
462                    "Invalid RPC URL for chain {}: {}",
463                    chain_id, url
464                )));
465            }
466        }
467        Ok(())
468    }
469
470    /// Checks if a URL is a valid RPC URL
471    fn is_valid_rpc_url(url: &str) -> bool {
472        url.starts_with("http://")
473            || url.starts_with("https://")
474            || url.starts_with("wss://")
475            || url.starts_with("ws://")
476    }
477
478    /// Validates all chain configurations
479    fn validate_chain_configs(
480        &self,
481        address_validator: Option<&dyn AddressValidator>,
482    ) -> ConfigResult<()> {
483        for (chain_id, chain) in &self.chains {
484            self.validate_chain_id_consistency(*chain_id, chain)?;
485            if let Some(validator) = address_validator {
486                self.validate_chain_contracts(validator, chain)?;
487            }
488        }
489        Ok(())
490    }
491
492    /// Validates that chain ID in map matches chain config
493    fn validate_chain_id_consistency(
494        &self,
495        chain_id: u64,
496        chain: &ChainConfig,
497    ) -> ConfigResult<()> {
498        if chain.id != chain_id {
499            return Err(ConfigError::validation(format!(
500                "Chain ID mismatch: {} vs {}",
501                chain_id, chain.id
502            )));
503        }
504        Ok(())
505    }
506
507    /// Validates all contract addresses for a chain
508    fn validate_chain_contracts(
509        &self,
510        validator: &dyn AddressValidator,
511        chain: &ChainConfig,
512    ) -> ConfigResult<()> {
513        self.validate_core_contracts(validator, chain)?;
514        self.validate_defi_protocol_contracts(validator, chain)?;
515        Ok(())
516    }
517
518    /// Validates core contract addresses (router, quoter, factory, tokens)
519    fn validate_core_contracts(
520        &self,
521        validator: &dyn AddressValidator,
522        chain: &ChainConfig,
523    ) -> ConfigResult<()> {
524        let core_contracts = [
525            (&chain.contracts.router, "router"),
526            (&chain.contracts.quoter, "quoter"),
527            (&chain.contracts.factory, "factory"),
528            (&chain.contracts.weth, "weth"),
529            (&chain.contracts.usdc, "usdc"),
530            (&chain.contracts.usdt, "usdt"),
531        ];
532
533        for (address_opt, contract_name) in core_contracts {
534            if let Some(addr) = address_opt {
535                validator.validate(addr, contract_name)?;
536            }
537        }
538        Ok(())
539    }
540
541    /// Validates DeFi protocol contract addresses
542    fn validate_defi_protocol_contracts(
543        &self,
544        validator: &dyn AddressValidator,
545        chain: &ChainConfig,
546    ) -> ConfigResult<()> {
547        let defi_contracts = [
548            (&chain.contracts.sushiswap_router, "sushiswap_router"),
549            (&chain.contracts.sushiswap_factory, "sushiswap_factory"),
550            (&chain.contracts.aave_v3_pool, "aave_v3_pool"),
551            (
552                &chain.contracts.aave_v3_pool_data_provider,
553                "aave_v3_pool_data_provider",
554            ),
555            (&chain.contracts.aave_v3_oracle, "aave_v3_oracle"),
556            (&chain.contracts.compound_v3_usdc, "compound_v3_usdc"),
557            (&chain.contracts.curve_registry, "curve_registry"),
558            (
559                &chain.contracts.oneinch_aggregation_router,
560                "oneinch_aggregation_router",
561            ),
562            (&chain.contracts.balancer_vault, "balancer_vault"),
563            (&chain.contracts.quickswap_router, "quickswap_router"),
564            (&chain.contracts.gmx_router, "gmx_router"),
565            (&chain.contracts.maker_dai, "maker_dai"),
566        ];
567
568        for (address_opt, contract_name) in defi_contracts {
569            if let Some(addr) = address_opt {
570                validator.validate(addr, contract_name)?;
571            }
572        }
573        Ok(())
574    }
575}
576
577/// Solana-specific network configuration
578#[derive(Debug, Clone, Deserialize, Serialize)]
579pub struct SolanaNetworkConfig {
580    /// Network name (mainnet, devnet, testnet)
581    pub name: String,
582
583    /// RPC endpoint URL
584    pub rpc_url: String,
585
586    /// Optional WebSocket URL
587    #[serde(default)]
588    pub ws_url: Option<String>,
589
590    /// Optional block explorer URL
591    #[serde(default)]
592    pub explorer_url: Option<String>,
593}
594
595impl SolanaNetworkConfig {
596    /// Create a new Solana network configuration
597    pub fn new(name: impl Into<String>, rpc_url: impl Into<String>) -> Self {
598        Self {
599            name: name.into(),
600            rpc_url: rpc_url.into(),
601            ws_url: None,
602            explorer_url: None,
603        }
604    }
605
606    /// Create mainnet configuration
607    pub fn mainnet() -> Self {
608        Self::new("mainnet", "https://api.mainnet-beta.solana.com")
609    }
610
611    /// Create devnet configuration
612    pub fn devnet() -> Self {
613        Self::new("devnet", "https://api.devnet.solana.com")
614    }
615
616    /// Create testnet configuration
617    pub fn testnet() -> Self {
618        Self::new("testnet", "https://api.testnet.solana.com")
619    }
620}
621
622/// EVM-specific network configuration
623#[derive(Debug, Clone, Deserialize, Serialize)]
624pub struct EvmNetworkConfig {
625    /// Network name (ethereum, polygon, arbitrum, etc.)
626    pub name: String,
627
628    /// Chain ID
629    pub chain_id: u64,
630
631    /// RPC endpoint URL
632    pub rpc_url: String,
633
634    /// Optional block explorer URL
635    #[serde(default)]
636    pub explorer_url: Option<String>,
637
638    /// Native token symbol
639    #[serde(default)]
640    pub native_token: Option<String>,
641}
642
643impl EvmNetworkConfig {
644    /// Create a new EVM network configuration
645    pub fn new(name: impl Into<String>, chain_id: u64, rpc_url: impl Into<String>) -> Self {
646        Self {
647            name: name.into(),
648            chain_id,
649            rpc_url: rpc_url.into(),
650            explorer_url: None,
651            native_token: None,
652        }
653    }
654
655    /// Create Ethereum mainnet configuration
656    pub fn ethereum_mainnet() -> Self {
657        let mut config = Self::new("ethereum", 1, "https://eth.llamarpc.com");
658        config.native_token = Some("ETH".to_string());
659        config.explorer_url = Some("https://etherscan.io".to_string());
660        config
661    }
662
663    /// Create Polygon configuration
664    pub fn polygon() -> Self {
665        let mut config = Self::new("polygon", 137, "https://polygon-rpc.com");
666        config.native_token = Some("MATIC".to_string());
667        config.explorer_url = Some("https://polygonscan.com".to_string());
668        config
669    }
670
671    /// Generate CAIP-2 identifier
672    pub fn caip2(&self) -> String {
673        format!("eip155:{}", self.chain_id)
674    }
675}
676
677/// Structure for parsing chains.toml file
678#[derive(Debug, Deserialize)]
679struct ChainsFile {
680    chains: HashMap<String, ChainFromToml>,
681}
682
683/// Chain configuration as parsed from TOML
684#[derive(Debug, Deserialize)]
685struct ChainFromToml {
686    id: u64,
687    name: String,
688    #[serde(default)]
689    router: Option<String>,
690    #[serde(default)]
691    quoter: Option<String>,
692    #[serde(default)]
693    factory: Option<String>,
694    #[serde(default)]
695    weth: Option<String>,
696    #[serde(default)]
697    usdc: Option<String>,
698    #[serde(default)]
699    usdt: Option<String>,
700    #[serde(default)]
701    sushiswap_router: Option<String>,
702    #[serde(default)]
703    sushiswap_factory: Option<String>,
704    #[serde(default)]
705    aave_v3_pool: Option<String>,
706    #[serde(default)]
707    aave_v3_pool_data_provider: Option<String>,
708    #[serde(default)]
709    aave_v3_oracle: Option<String>,
710    #[serde(default)]
711    compound_v3_usdc: Option<String>,
712    #[serde(default)]
713    curve_registry: Option<String>,
714    #[serde(default)]
715    oneinch_aggregation_router: Option<String>,
716    #[serde(default)]
717    balancer_vault: Option<String>,
718    #[serde(default)]
719    quickswap_router: Option<String>,
720    #[serde(default)]
721    gmx_router: Option<String>,
722    #[serde(default)]
723    maker_dai: Option<String>,
724    #[serde(default)]
725    explorer_url: Option<String>,
726    #[serde(default)]
727    native_token: Option<String>,
728    #[serde(default)]
729    is_testnet: bool,
730}
731
732impl From<ChainFromToml> for ChainConfig {
733    fn from(toml: ChainFromToml) -> Self {
734        Self {
735            id: toml.id,
736            name: toml.name,
737            rpc_url: None,
738            contracts: ChainContract {
739                router: toml.router,
740                quoter: toml.quoter,
741                factory: toml.factory,
742                weth: toml.weth,
743                usdc: toml.usdc,
744                usdt: toml.usdt,
745                sushiswap_router: toml.sushiswap_router,
746                sushiswap_factory: toml.sushiswap_factory,
747                aave_v3_pool: toml.aave_v3_pool,
748                aave_v3_pool_data_provider: toml.aave_v3_pool_data_provider,
749                aave_v3_oracle: toml.aave_v3_oracle,
750                compound_v3_usdc: toml.compound_v3_usdc,
751                curve_registry: toml.curve_registry,
752                oneinch_aggregation_router: toml.oneinch_aggregation_router,
753                balancer_vault: toml.balancer_vault,
754                quickswap_router: toml.quickswap_router,
755                gmx_router: toml.gmx_router,
756                maker_dai: toml.maker_dai,
757                custom: HashMap::new(),
758            },
759            explorer_url: toml.explorer_url,
760            native_token: toml.native_token,
761            is_testnet: toml.is_testnet,
762        }
763    }
764}
765
766// Default value functions
767fn default_chain_id() -> u64 {
768    1
769} // Ethereum mainnet
770fn default_rpc_timeout() -> u64 {
771    30
772}
773fn default_ws_timeout() -> u64 {
774    60
775}
776fn default_http_timeout() -> u64 {
777    30
778}
779
780impl Default for NetworkConfig {
781    fn default() -> Self {
782        Self {
783            solana_rpc_url: "https://api.mainnet-beta.solana.com".to_string(),
784            solana_ws_url: None,
785            rpc_urls: HashMap::new(),
786            chains: HashMap::new(),
787            default_chain_id: default_chain_id(),
788            timeouts: NetworkTimeouts::default(),
789        }
790    }
791}
792
793impl Default for NetworkTimeouts {
794    fn default() -> Self {
795        Self {
796            rpc_timeout_secs: default_rpc_timeout(),
797            ws_timeout_secs: default_ws_timeout(),
798            http_timeout_secs: default_http_timeout(),
799        }
800    }
801}
802
803#[cfg(test)]
804mod tests {
805    use super::*;
806    use serial_test::serial;
807    use std::fs;
808    use tempfile::TempDir;
809
810    // Helper function to create a temporary test directory
811    fn create_temp_dir() -> TempDir {
812        tempfile::tempdir().unwrap()
813    }
814
815    // Helper function to create a test chains.toml content
816    fn create_test_chains_toml() -> String {
817        r#"
818[chains.ethereum]
819id = 1
820name = "Ethereum Mainnet"
821router = "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45"
822quoter = "0xb27308f9F90D607463bb33eA8e66e3e6e63a3f75"
823factory = "0x1F98431c8aD98523631AE4a59f267346ea31F984"
824weth = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"
825usdc = "0xA0b86a33E6417efE3CF1AA5bAdC34a6a2C2d0BE0"
826usdt = "0xdAC17F958D2ee523a2206206994597C13D831ec7"
827explorer_url = "https://etherscan.io"
828native_token = "ETH"
829is_testnet = false
830
831[chains.polygon]
832id = 137
833name = "Polygon"
834router = "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45"
835is_testnet = false
836"#
837        .to_string()
838    }
839
840    #[test]
841    fn test_default_functions() {
842        assert_eq!(default_chain_id(), 1);
843        assert_eq!(default_rpc_timeout(), 30);
844        assert_eq!(default_ws_timeout(), 60);
845        assert_eq!(default_http_timeout(), 30);
846    }
847
848    #[test]
849    fn test_network_config_default() {
850        let config = NetworkConfig::default();
851
852        assert_eq!(config.solana_rpc_url, "https://api.mainnet-beta.solana.com");
853        assert_eq!(config.solana_ws_url, None);
854        assert!(config.rpc_urls.is_empty());
855        assert!(config.chains.is_empty());
856        assert_eq!(config.default_chain_id, 1);
857        assert_eq!(config.timeouts.rpc_timeout_secs, 30);
858        assert_eq!(config.timeouts.ws_timeout_secs, 60);
859        assert_eq!(config.timeouts.http_timeout_secs, 30);
860    }
861
862    #[test]
863    fn test_network_timeouts_default() {
864        let timeouts = NetworkTimeouts::default();
865
866        assert_eq!(timeouts.rpc_timeout_secs, 30);
867        assert_eq!(timeouts.ws_timeout_secs, 60);
868        assert_eq!(timeouts.http_timeout_secs, 30);
869    }
870
871    #[test]
872    fn test_chain_contract_default() {
873        let contract = ChainContract::default();
874
875        assert_eq!(contract.router, None);
876        assert_eq!(contract.quoter, None);
877        assert_eq!(contract.factory, None);
878        assert_eq!(contract.weth, None);
879        assert_eq!(contract.usdc, None);
880        assert_eq!(contract.usdt, None);
881        assert!(contract.custom.is_empty());
882    }
883
884    #[test]
885    fn test_chain_from_toml_conversion() {
886        let toml_chain = ChainFromToml {
887            id: 1,
888            name: "Ethereum".to_string(),
889            router: Some("0x123".to_string()),
890            quoter: Some("0x456".to_string()),
891            factory: Some("0x789".to_string()),
892            weth: Some("0xabc".to_string()),
893            usdc: Some("0xdef".to_string()),
894            usdt: Some("0x012".to_string()),
895            sushiswap_router: None,
896            sushiswap_factory: None,
897            aave_v3_pool: None,
898            aave_v3_pool_data_provider: None,
899            aave_v3_oracle: None,
900            compound_v3_usdc: None,
901            curve_registry: None,
902            oneinch_aggregation_router: None,
903            balancer_vault: None,
904            quickswap_router: None,
905            gmx_router: None,
906            maker_dai: None,
907            explorer_url: Some("https://etherscan.io".to_string()),
908            native_token: Some("ETH".to_string()),
909            is_testnet: false,
910        };
911
912        let chain_config: ChainConfig = toml_chain.into();
913
914        assert_eq!(chain_config.id, 1);
915        assert_eq!(chain_config.name, "Ethereum");
916        assert_eq!(chain_config.rpc_url, None);
917        assert_eq!(chain_config.contracts.router, Some("0x123".to_string()));
918        assert_eq!(chain_config.contracts.quoter, Some("0x456".to_string()));
919        assert_eq!(chain_config.contracts.factory, Some("0x789".to_string()));
920        assert_eq!(chain_config.contracts.weth, Some("0xabc".to_string()));
921        assert_eq!(chain_config.contracts.usdc, Some("0xdef".to_string()));
922        assert_eq!(chain_config.contracts.usdt, Some("0x012".to_string()));
923        assert_eq!(
924            chain_config.explorer_url,
925            Some("https://etherscan.io".to_string())
926        );
927        assert_eq!(chain_config.native_token, Some("ETH".to_string()));
928        assert!(!chain_config.is_testnet);
929        assert!(chain_config.contracts.custom.is_empty());
930    }
931
932    #[test]
933    fn test_chain_from_toml_conversion_with_defaults() {
934        let toml_chain = ChainFromToml {
935            id: 2,
936            name: "Test Chain".to_string(),
937            router: None,
938            quoter: None,
939            factory: None,
940            weth: None,
941            usdc: None,
942            usdt: None,
943            sushiswap_router: None,
944            sushiswap_factory: None,
945            aave_v3_pool: None,
946            aave_v3_pool_data_provider: None,
947            aave_v3_oracle: None,
948            compound_v3_usdc: None,
949            curve_registry: None,
950            oneinch_aggregation_router: None,
951            balancer_vault: None,
952            quickswap_router: None,
953            gmx_router: None,
954            maker_dai: None,
955            explorer_url: None,
956            native_token: None,
957            is_testnet: true,
958        };
959
960        let chain_config: ChainConfig = toml_chain.into();
961
962        assert_eq!(chain_config.id, 2);
963        assert_eq!(chain_config.name, "Test Chain");
964        assert_eq!(chain_config.rpc_url, None);
965        assert_eq!(chain_config.contracts.router, None);
966        assert_eq!(chain_config.contracts.quoter, None);
967        assert_eq!(chain_config.contracts.factory, None);
968        assert_eq!(chain_config.contracts.weth, None);
969        assert_eq!(chain_config.contracts.usdc, None);
970        assert_eq!(chain_config.contracts.usdt, None);
971        assert_eq!(chain_config.explorer_url, None);
972        assert_eq!(chain_config.native_token, None);
973        assert!(chain_config.is_testnet);
974    }
975
976    #[test]
977    #[serial]
978    fn test_extract_rpc_urls() {
979        use test_env_vars::*;
980        // Set up test environment variables
981        set_test_env_var(RPC_URL_1, "https://mainnet.infura.io");
982        set_test_env_var(RPC_URL_137, "https://polygon-rpc.com");
983        set_test_env_var(RPC_URL_INVALID, "https://invalid.com"); // Should be ignored
984        set_test_env_var(NOT_RPC_URL_1, "https://should-be-ignored.com"); // Should be ignored
985
986        let mut config = NetworkConfig::default();
987        config.extract_rpc_urls();
988
989        assert_eq!(
990            config.rpc_urls.get("1"),
991            Some(&"https://mainnet.infura.io".to_string())
992        );
993        assert_eq!(
994            config.rpc_urls.get("137"),
995            Some(&"https://polygon-rpc.com".to_string())
996        );
997        assert!(!config.rpc_urls.contains_key("INVALID"));
998        assert!(!config.rpc_urls.contains_key("NOT_RPC_URL_1"));
999
1000        // Clean up
1001        remove_test_env_var(RPC_URL_1);
1002        remove_test_env_var(RPC_URL_137);
1003        remove_test_env_var(RPC_URL_INVALID);
1004        remove_test_env_var(NOT_RPC_URL_1);
1005    }
1006
1007    #[test]
1008    fn test_extract_rpc_urls_empty_environment() {
1009        let mut config = NetworkConfig::default();
1010        config.extract_rpc_urls();
1011
1012        // Should not crash with empty environment and rpc_urls should remain empty
1013        // (assuming no RPC_URL_* vars are set in test environment)
1014    }
1015
1016    #[test]
1017    fn test_get_rpc_url_from_chain_config() {
1018        let mut config = NetworkConfig::default();
1019
1020        let chain_config = ChainConfig {
1021            id: 1,
1022            name: "Ethereum".to_string(),
1023            rpc_url: Some("https://custom-rpc.com".to_string()),
1024            contracts: ChainContract::default(),
1025            explorer_url: None,
1026            native_token: None,
1027            is_testnet: false,
1028        };
1029
1030        config.chains.insert(1, chain_config);
1031
1032        assert_eq!(
1033            config.get_rpc_url("1"),
1034            Some("https://custom-rpc.com".to_string())
1035        );
1036    }
1037
1038    #[test]
1039    fn test_get_rpc_url_from_rpc_urls() {
1040        let mut config = NetworkConfig::default();
1041        config
1042            .rpc_urls
1043            .insert("137".to_string(), "https://polygon-rpc.com".to_string());
1044
1045        assert_eq!(
1046            config.get_rpc_url("137"),
1047            Some("https://polygon-rpc.com".to_string())
1048        );
1049    }
1050
1051    #[test]
1052    fn test_get_rpc_url_chain_config_overrides_rpc_urls() {
1053        let mut config = NetworkConfig::default();
1054        config
1055            .rpc_urls
1056            .insert("1".to_string(), "https://fallback-rpc.com".to_string());
1057
1058        let chain_config = ChainConfig {
1059            id: 1,
1060            name: "Ethereum".to_string(),
1061            rpc_url: Some("https://priority-rpc.com".to_string()),
1062            contracts: ChainContract::default(),
1063            explorer_url: None,
1064            native_token: None,
1065            is_testnet: false,
1066        };
1067
1068        config.chains.insert(1, chain_config);
1069
1070        assert_eq!(
1071            config.get_rpc_url("1"),
1072            Some("https://priority-rpc.com".to_string())
1073        );
1074    }
1075
1076    #[test]
1077    fn test_get_rpc_url_not_found() {
1078        let config = NetworkConfig::default();
1079        assert_eq!(config.get_rpc_url("999"), None);
1080    }
1081
1082    #[test]
1083    fn test_get_rpc_url_chain_config_without_rpc_url() {
1084        let mut config = NetworkConfig::default();
1085        config
1086            .rpc_urls
1087            .insert("1".to_string(), "https://fallback-rpc.com".to_string());
1088
1089        let chain_config = ChainConfig {
1090            id: 1,
1091            name: "Ethereum".to_string(),
1092            rpc_url: None, // No RPC URL in chain config
1093            contracts: ChainContract::default(),
1094            explorer_url: None,
1095            native_token: None,
1096            is_testnet: false,
1097        };
1098
1099        config.chains.insert(1, chain_config);
1100
1101        assert_eq!(
1102            config.get_rpc_url("1"),
1103            Some("https://fallback-rpc.com".to_string())
1104        );
1105    }
1106
1107    #[test]
1108    fn test_get_chain_exists() {
1109        let mut config = NetworkConfig::default();
1110
1111        let chain_config = ChainConfig {
1112            id: 1,
1113            name: "Ethereum".to_string(),
1114            rpc_url: None,
1115            contracts: ChainContract::default(),
1116            explorer_url: None,
1117            native_token: None,
1118            is_testnet: false,
1119        };
1120
1121        config.chains.insert(1, chain_config);
1122
1123        let result = config.get_chain(1);
1124        assert!(result.is_some());
1125        assert_eq!(result.unwrap().id, 1);
1126        assert_eq!(result.unwrap().name, "Ethereum");
1127    }
1128
1129    #[test]
1130    fn test_get_chain_not_exists() {
1131        let config = NetworkConfig::default();
1132        assert!(config.get_chain(999).is_none());
1133    }
1134
1135    #[test]
1136    fn test_get_supported_chains_from_rpc_urls_only() {
1137        let mut config = NetworkConfig::default();
1138        config
1139            .rpc_urls
1140            .insert("1".to_string(), "https://eth.com".to_string());
1141        config
1142            .rpc_urls
1143            .insert("137".to_string(), "https://polygon.com".to_string());
1144        config
1145            .rpc_urls
1146            .insert("invalid".to_string(), "https://invalid.com".to_string()); // Should be ignored
1147
1148        let chains = config.get_supported_chains();
1149        assert_eq!(chains.len(), 2);
1150        assert!(chains.contains(&1));
1151        assert!(chains.contains(&137));
1152    }
1153
1154    #[test]
1155    fn test_get_supported_chains_from_chains_only() {
1156        let mut config = NetworkConfig::default();
1157
1158        let chain1 = ChainConfig {
1159            id: 1,
1160            name: "Ethereum".to_string(),
1161            rpc_url: None,
1162            contracts: ChainContract::default(),
1163            explorer_url: None,
1164            native_token: None,
1165            is_testnet: false,
1166        };
1167
1168        let chain2 = ChainConfig {
1169            id: 56,
1170            name: "BSC".to_string(),
1171            rpc_url: None,
1172            contracts: ChainContract::default(),
1173            explorer_url: None,
1174            native_token: None,
1175            is_testnet: false,
1176        };
1177
1178        config.chains.insert(1, chain1);
1179        config.chains.insert(56, chain2);
1180
1181        let chains = config.get_supported_chains();
1182        assert_eq!(chains.len(), 2);
1183        assert!(chains.contains(&1));
1184        assert!(chains.contains(&56));
1185    }
1186
1187    #[test]
1188    fn test_get_supported_chains_mixed_sources_with_duplicates() {
1189        let mut config = NetworkConfig::default();
1190
1191        // Add RPC URLs
1192        config
1193            .rpc_urls
1194            .insert("1".to_string(), "https://eth.com".to_string());
1195        config
1196            .rpc_urls
1197            .insert("137".to_string(), "https://polygon.com".to_string());
1198
1199        // Add chain configs (including duplicate chain ID 1)
1200        let chain1 = ChainConfig {
1201            id: 1,
1202            name: "Ethereum".to_string(),
1203            rpc_url: None,
1204            contracts: ChainContract::default(),
1205            explorer_url: None,
1206            native_token: None,
1207            is_testnet: false,
1208        };
1209
1210        let chain56 = ChainConfig {
1211            id: 56,
1212            name: "BSC".to_string(),
1213            rpc_url: None,
1214            contracts: ChainContract::default(),
1215            explorer_url: None,
1216            native_token: None,
1217            is_testnet: false,
1218        };
1219
1220        config.chains.insert(1, chain1);
1221        config.chains.insert(56, chain56);
1222
1223        let chains = config.get_supported_chains();
1224        assert_eq!(chains.len(), 3); // 1, 56, 137 (deduplicated)
1225        assert!(chains.contains(&1));
1226        assert!(chains.contains(&56));
1227        assert!(chains.contains(&137));
1228    }
1229
1230    #[test]
1231    fn test_get_supported_chains_empty() {
1232        let config = NetworkConfig::default();
1233        let chains = config.get_supported_chains();
1234        assert!(chains.is_empty());
1235    }
1236
1237    #[test]
1238    fn test_validate_valid_config() {
1239        let mut config = NetworkConfig::default();
1240        config.solana_rpc_url = "https://api.mainnet-beta.solana.com".to_string();
1241        config
1242            .rpc_urls
1243            .insert("1".to_string(), "https://mainnet.infura.io".to_string());
1244        config
1245            .rpc_urls
1246            .insert("137".to_string(), "wss://polygon-rpc.com".to_string());
1247
1248        let chain_config = ChainConfig {
1249            id: 1,
1250            name: "Ethereum".to_string(),
1251            rpc_url: None,
1252            contracts: ChainContract {
1253                router: Some("0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45".to_string()),
1254                quoter: Some("0xb27308f9F90D607463bb33eA8e66e3e6e63a3f75".to_string()),
1255                factory: Some("0x1F98431c8aD98523631AE4a59f267346ea31F984".to_string()),
1256                ..ChainContract::default()
1257            },
1258            explorer_url: None,
1259            native_token: None,
1260            is_testnet: false,
1261        };
1262
1263        config.chains.insert(1, chain_config);
1264
1265        assert!(config.validate_config(None).is_ok());
1266    }
1267
1268    #[test]
1269    fn test_validate_invalid_solana_rpc_url_no_protocol() {
1270        let mut config = NetworkConfig::default();
1271        config.solana_rpc_url = "api.mainnet-beta.solana.com".to_string();
1272
1273        let result = config.validate_config(None);
1274        assert!(result.is_err());
1275        assert!(result
1276            .unwrap_err()
1277            .to_string()
1278            .contains("SOLANA_RPC_URL must be a valid HTTP(S) URL"));
1279    }
1280
1281    #[test]
1282    fn test_validate_invalid_solana_rpc_url_ftp_protocol() {
1283        let mut config = NetworkConfig::default();
1284        config.solana_rpc_url = "ftp://api.mainnet-beta.solana.com".to_string();
1285
1286        let result = config.validate_config(None);
1287        assert!(result.is_err());
1288        assert!(result
1289            .unwrap_err()
1290            .to_string()
1291            .contains("SOLANA_RPC_URL must be a valid HTTP(S) URL"));
1292    }
1293
1294    #[test]
1295    fn test_validate_invalid_rpc_url() {
1296        let mut config = NetworkConfig::default();
1297        config
1298            .rpc_urls
1299            .insert("1".to_string(), "ftp://invalid-protocol.com".to_string());
1300
1301        let result = config.validate_config(None);
1302        assert!(result.is_err());
1303        assert!(result
1304            .unwrap_err()
1305            .to_string()
1306            .contains("Invalid RPC URL for chain 1"));
1307    }
1308
1309    #[test]
1310    fn test_validate_valid_rpc_url_protocols() {
1311        let mut config = NetworkConfig::default();
1312        config
1313            .rpc_urls
1314            .insert("1".to_string(), "http://localhost:8545".to_string());
1315        config
1316            .rpc_urls
1317            .insert("2".to_string(), "https://mainnet.infura.io".to_string());
1318        config
1319            .rpc_urls
1320            .insert("3".to_string(), "ws://localhost:8546".to_string());
1321        config
1322            .rpc_urls
1323            .insert("4".to_string(), "wss://mainnet.infura.io/ws".to_string());
1324
1325        assert!(config.validate_config(None).is_ok());
1326    }
1327
1328    #[test]
1329    fn test_validate_chain_id_mismatch() {
1330        let mut config = NetworkConfig::default();
1331
1332        let chain_config = ChainConfig {
1333            id: 2, // Different from the key (1)
1334            name: "Ethereum".to_string(),
1335            rpc_url: None,
1336            contracts: ChainContract::default(),
1337            explorer_url: None,
1338            native_token: None,
1339            is_testnet: false,
1340        };
1341
1342        config.chains.insert(1, chain_config);
1343
1344        let result = config.validate_config(None);
1345        assert!(result.is_err());
1346        assert!(result
1347            .unwrap_err()
1348            .to_string()
1349            .contains("Chain ID mismatch: 1 vs 2"));
1350    }
1351
1352    #[test]
1353    #[serial]
1354    fn test_load_chain_contracts_file_not_exists() {
1355        // Test with non-existent file (should not error)
1356        std::env::set_var(RIGLR_CHAINS_CONFIG, "/non/existent/path/chains.toml");
1357
1358        let mut config = NetworkConfig::default();
1359        let result = config.load_chain_contracts();
1360
1361        assert!(result.is_ok());
1362        assert!(config.chains.is_empty());
1363
1364        std::env::remove_var(RIGLR_CHAINS_CONFIG);
1365    }
1366
1367    #[test]
1368    fn test_load_chain_contracts_default_path_not_exists() {
1369        // Test with default path when environment variable is not set
1370        std::env::remove_var(RIGLR_CHAINS_CONFIG);
1371
1372        let mut config = NetworkConfig::default();
1373        let result = config.load_chain_contracts();
1374
1375        // Should succeed even if chains.toml doesn't exist
1376        assert!(result.is_ok());
1377    }
1378
1379    #[test]
1380    #[serial]
1381    fn test_load_chain_contracts_valid_file() {
1382        let temp_dir = create_temp_dir();
1383        let chains_path = temp_dir.path().join("chains.toml");
1384
1385        // Write test chains.toml
1386        fs::write(&chains_path, create_test_chains_toml()).unwrap();
1387
1388        std::env::set_var(RIGLR_CHAINS_CONFIG, chains_path.to_str().unwrap());
1389
1390        let mut config = NetworkConfig::default();
1391        let result = config.load_chain_contracts();
1392
1393        assert!(result.is_ok());
1394        assert_eq!(config.chains.len(), 2);
1395
1396        let eth_chain = config.chains.get(&1).unwrap();
1397        assert_eq!(eth_chain.name, "Ethereum Mainnet");
1398        assert_eq!(
1399            eth_chain.contracts.router,
1400            Some("0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45".to_string())
1401        );
1402        assert_eq!(eth_chain.native_token, Some("ETH".to_string()));
1403        assert!(!eth_chain.is_testnet);
1404
1405        let polygon_chain = config.chains.get(&137).unwrap();
1406        assert_eq!(polygon_chain.name, "Polygon");
1407        assert_eq!(
1408            polygon_chain.contracts.router,
1409            Some("0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45".to_string())
1410        );
1411        assert_eq!(polygon_chain.contracts.quoter, None); // Not specified in TOML
1412        assert!(!polygon_chain.is_testnet);
1413
1414        std::env::remove_var(RIGLR_CHAINS_CONFIG);
1415    }
1416
1417    #[test]
1418    #[serial]
1419    fn test_load_chain_contracts_invalid_toml() {
1420        let temp_dir = create_temp_dir();
1421        let chains_path = temp_dir.path().join("chains.toml");
1422
1423        // Write invalid TOML
1424        fs::write(&chains_path, "invalid toml content [[[").unwrap();
1425
1426        std::env::set_var(RIGLR_CHAINS_CONFIG, chains_path.to_str().unwrap());
1427
1428        let mut config = NetworkConfig::default();
1429        let result = config.load_chain_contracts();
1430
1431        assert!(result.is_err());
1432        assert!(result
1433            .unwrap_err()
1434            .to_string()
1435            .contains("Failed to parse chains.toml"));
1436
1437        std::env::remove_var(RIGLR_CHAINS_CONFIG);
1438    }
1439
1440    #[test]
1441    #[serial]
1442    fn test_load_chain_contracts_with_environment_overrides() {
1443        let temp_dir = create_temp_dir();
1444        let chains_path = temp_dir.path().join("chains.toml");
1445
1446        // Write test chains.toml
1447        fs::write(&chains_path, create_test_chains_toml()).unwrap();
1448
1449        use test_env_vars::*;
1450        // Set environment overrides
1451        set_test_env_var(ROUTER_1, "0x1111111111111111111111111111111111111111");
1452        set_test_env_var(QUOTER_1, "0x2222222222222222222222222222222222222222");
1453        set_test_env_var(FACTORY_137, "0x3333333333333333333333333333333333333333");
1454
1455        std::env::set_var(RIGLR_CHAINS_CONFIG, chains_path.to_str().unwrap());
1456
1457        let mut config = NetworkConfig::default();
1458        let result = config.load_chain_contracts();
1459
1460        assert!(result.is_ok());
1461
1462        let eth_chain = config.chains.get(&1).unwrap();
1463        assert_eq!(
1464            eth_chain.contracts.router,
1465            Some("0x1111111111111111111111111111111111111111".to_string())
1466        );
1467        assert_eq!(
1468            eth_chain.contracts.quoter,
1469            Some("0x2222222222222222222222222222222222222222".to_string())
1470        );
1471        // Factory should remain from TOML since no override for chain 1
1472        assert_eq!(
1473            eth_chain.contracts.factory,
1474            Some("0x1F98431c8aD98523631AE4a59f267346ea31F984".to_string())
1475        );
1476
1477        let polygon_chain = config.chains.get(&137).unwrap();
1478        // Router should remain from TOML since no override for chain 137
1479        assert_eq!(
1480            polygon_chain.contracts.router,
1481            Some("0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45".to_string())
1482        );
1483        assert_eq!(polygon_chain.contracts.quoter, None); // No quoter in TOML and no env override
1484        assert_eq!(
1485            polygon_chain.contracts.factory,
1486            Some("0x3333333333333333333333333333333333333333".to_string())
1487        );
1488
1489        // Clean up
1490        remove_test_env_var(ROUTER_1);
1491        remove_test_env_var(QUOTER_1);
1492        remove_test_env_var(FACTORY_137);
1493        std::env::remove_var(RIGLR_CHAINS_CONFIG);
1494    }
1495
1496    #[test]
1497    #[serial]
1498    fn test_load_chain_contracts_read_error() {
1499        // Test with a directory instead of a file to trigger read error
1500        let temp_dir = create_temp_dir();
1501        let chains_path = temp_dir.path().join("chains_dir");
1502        fs::create_dir(&chains_path).unwrap();
1503
1504        std::env::set_var(RIGLR_CHAINS_CONFIG, chains_path.to_str().unwrap());
1505
1506        let mut config = NetworkConfig::default();
1507        let result = config.load_chain_contracts();
1508
1509        assert!(result.is_err());
1510        assert!(result
1511            .unwrap_err()
1512            .to_string()
1513            .contains("Failed to read chains config"));
1514
1515        std::env::remove_var(RIGLR_CHAINS_CONFIG);
1516    }
1517}