1use crate::{ConfigError, ConfigResult};
4use once_cell::sync::Lazy;
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7
8pub trait AddressValidator: Send + Sync {
13 fn validate(&self, address: &str, contract_name: &str) -> ConfigResult<()>;
23}
24
25const RIGLR_CHAINS_CONFIG: &str = "RIGLR_CHAINS_CONFIG";
26
27#[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 pub fn set_test_env_var(key: &'static str, value: &str) {
40 std::env::set_var(key, value);
41 }
42
43 pub fn remove_test_env_var(key: &'static str) {
45 std::env::remove_var(key);
46 }
47}
48
49#[derive(Debug, Clone, Deserialize, Serialize)]
51pub struct NetworkConfig {
52 pub solana_rpc_url: String,
54
55 #[serde(default)]
57 pub solana_ws_url: Option<String>,
58
59 #[serde(default, skip_serializing)]
62 pub rpc_urls: HashMap<String, String>,
63
64 #[serde(default, skip_serializing)]
66 pub chains: HashMap<u64, ChainConfig>,
67
68 #[serde(default = "default_chain_id")]
70 pub default_chain_id: u64,
71
72 #[serde(flatten)]
74 pub timeouts: NetworkTimeouts,
75}
76
77#[derive(Debug, Clone, Deserialize, Serialize)]
79pub struct ChainConfig {
80 pub id: u64,
82
83 pub name: String,
85
86 #[serde(default)]
88 pub rpc_url: Option<String>,
89
90 #[serde(flatten)]
92 pub contracts: ChainContract,
93
94 #[serde(default)]
96 pub explorer_url: Option<String>,
97
98 #[serde(default)]
100 pub native_token: Option<String>,
101
102 #[serde(default)]
104 pub is_testnet: bool,
105}
106
107#[derive(Debug, Clone, Default, Deserialize, Serialize)]
109pub struct ChainContract {
110 #[serde(default)]
112 pub router: Option<String>,
113
114 #[serde(default)]
116 pub quoter: Option<String>,
117
118 #[serde(default)]
120 pub factory: Option<String>,
121
122 #[serde(default)]
124 pub weth: Option<String>,
125
126 #[serde(default)]
128 pub usdc: Option<String>,
129
130 #[serde(default)]
132 pub usdt: Option<String>,
133
134 #[serde(default)]
136 pub sushiswap_router: Option<String>,
137
138 #[serde(default)]
140 pub sushiswap_factory: Option<String>,
141
142 #[serde(default)]
144 pub aave_v3_pool: Option<String>,
145
146 #[serde(default)]
148 pub aave_v3_pool_data_provider: Option<String>,
149
150 #[serde(default)]
152 pub aave_v3_oracle: Option<String>,
153
154 #[serde(default)]
156 pub compound_v3_usdc: Option<String>,
157
158 #[serde(default)]
160 pub curve_registry: Option<String>,
161
162 #[serde(default)]
164 pub oneinch_aggregation_router: Option<String>,
165
166 #[serde(default)]
168 pub balancer_vault: Option<String>,
169
170 #[serde(default)]
172 pub quickswap_router: Option<String>,
173
174 #[serde(default)]
176 pub gmx_router: Option<String>,
177
178 #[serde(default)]
180 pub maker_dai: Option<String>,
181
182 #[serde(default)]
184 pub custom: HashMap<String, String>,
185}
186
187#[derive(Debug, Clone, Deserialize, Serialize)]
189pub struct NetworkTimeouts {
190 #[serde(default = "default_rpc_timeout")]
192 pub rpc_timeout_secs: u64,
193
194 #[serde(default = "default_ws_timeout")]
196 pub ws_timeout_secs: u64,
197
198 #[serde(default = "default_http_timeout")]
200 pub http_timeout_secs: u64,
201}
202
203static NETWORK_NAME_MAP: Lazy<HashMap<&'static str, u64>> = Lazy::new(|| {
205 let mut map = HashMap::new();
206
207 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 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 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
262fn resolve_network_name(name: &str) -> Option<u64> {
264 NETWORK_NAME_MAP.get(&name.to_lowercase().as_str()).copied()
265}
266
267impl NetworkConfig {
268 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 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 self.rpc_urls.insert(chain_id.to_string(), value);
278 }
279 }
280 }
281 }
282
283 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 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 for (_name, toml_chain) in chains_file.chains {
306 let mut chain: ChainConfig = toml_chain.into();
307
308 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 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 pub fn get_rpc_url(&self, chain_identifier: &str) -> Option<String> {
378 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 id
384 } else {
385 return None;
387 };
388
389 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 self.rpc_urls.get(&chain_id.to_string()).cloned()
398 }
399
400 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 pub fn get_chain(&self, chain_id: u64) -> Option<&ChainConfig> {
407 self.chains.get(&chain_id)
408 }
409
410 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 chains.extend(self.chains.keys());
420
421 chains.sort_unstable();
423 chains.dedup();
424
425 chains
426 }
427
428 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 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 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 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 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 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 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 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 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#[derive(Debug, Clone, Deserialize, Serialize)]
579pub struct SolanaNetworkConfig {
580 pub name: String,
582
583 pub rpc_url: String,
585
586 #[serde(default)]
588 pub ws_url: Option<String>,
589
590 #[serde(default)]
592 pub explorer_url: Option<String>,
593}
594
595impl SolanaNetworkConfig {
596 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 pub fn mainnet() -> Self {
608 Self::new("mainnet", "https://api.mainnet-beta.solana.com")
609 }
610
611 pub fn devnet() -> Self {
613 Self::new("devnet", "https://api.devnet.solana.com")
614 }
615
616 pub fn testnet() -> Self {
618 Self::new("testnet", "https://api.testnet.solana.com")
619 }
620}
621
622#[derive(Debug, Clone, Deserialize, Serialize)]
624pub struct EvmNetworkConfig {
625 pub name: String,
627
628 pub chain_id: u64,
630
631 pub rpc_url: String,
633
634 #[serde(default)]
636 pub explorer_url: Option<String>,
637
638 #[serde(default)]
640 pub native_token: Option<String>,
641}
642
643impl EvmNetworkConfig {
644 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 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 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 pub fn caip2(&self) -> String {
673 format!("eip155:{}", self.chain_id)
674 }
675}
676
677#[derive(Debug, Deserialize)]
679struct ChainsFile {
680 chains: HashMap<String, ChainFromToml>,
681}
682
683#[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
766fn default_chain_id() -> u64 {
768 1
769} fn 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 fn create_temp_dir() -> TempDir {
812 tempfile::tempdir().unwrap()
813 }
814
815 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_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"); set_test_env_var(NOT_RPC_URL_1, "https://should-be-ignored.com"); 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 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 }
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, 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()); 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 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 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); 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, 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 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 std::env::remove_var(RIGLR_CHAINS_CONFIG);
1371
1372 let mut config = NetworkConfig::default();
1373 let result = config.load_chain_contracts();
1374
1375 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 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); 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 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 fs::write(&chains_path, create_test_chains_toml()).unwrap();
1448
1449 use test_env_vars::*;
1450 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 assert_eq!(
1473 eth_chain.contracts.factory,
1474 Some("0x1F98431c8aD98523631AE4a59f267346ea31F984".to_string())
1475 );
1476
1477 let polygon_chain = config.chains.get(&137).unwrap();
1478 assert_eq!(
1480 polygon_chain.contracts.router,
1481 Some("0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45".to_string())
1482 );
1483 assert_eq!(polygon_chain.contracts.quoter, None); assert_eq!(
1485 polygon_chain.contracts.factory,
1486 Some("0x3333333333333333333333333333333333333333".to_string())
1487 );
1488
1489 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 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}