Skip to main content

txgate_core/
config.rs

1//! Configuration types for the `TxGate` signing service.
2//!
3//! This module provides the configuration structures for defining the behavior
4//! of the `TxGate` daemon, including server settings, key storage configuration,
5//! and policy rules.
6//!
7//! # Configuration File
8//!
9//! Configuration is stored in TOML format at `~/.txgate/config.toml`.
10//!
11//! # Examples
12//!
13//! ```
14//! use txgate_core::config::{Config, ServerConfig, KeysConfig};
15//!
16//! // Create a default configuration
17//! let config = Config::default();
18//! assert_eq!(config.server.socket_path, "~/.txgate/txgate.sock");
19//! assert_eq!(config.server.timeout_secs, 30);
20//!
21//! // Generate default TOML
22//! let toml_str = Config::default_toml();
23//! println!("{}", toml_str);
24//! ```
25//!
26//! # Default TOML Output
27//!
28//! ```toml
29//! [server]
30//! socket_path = "~/.txgate/txgate.sock"
31//! timeout_secs = 30
32//!
33//! [keys]
34//! directory = "~/.txgate/keys"
35//! default_key = "default"
36//!
37//! [policy]
38//! whitelist_enabled = false
39//! whitelist = []
40//! blacklist = []
41//!
42//! [policy.transaction_limits]
43//! # ETH = "1000000000000000000"  # 1 ETH
44//!
45//! [policy.daily_limits]
46//! # ETH = "10000000000000000000"  # 10 ETH
47//! ```
48
49use crate::error::{ConfigError, PolicyError};
50use alloy_primitives::U256;
51use serde::{Deserialize, Serialize};
52use std::collections::HashMap;
53
54/// Top-level configuration for the `TxGate` signing service.
55///
56/// This struct contains all configuration sections for the `TxGate` daemon:
57///
58/// - **Server**: Unix socket path and request timeout settings
59/// - **Keys**: Key storage directory and default key name
60/// - **Policy**: Transaction approval rules (whitelist, blacklist, limits)
61///
62/// # Examples
63///
64/// ```
65/// use txgate_core::config::Config;
66///
67/// // Load from TOML string
68/// let toml_str = r#"
69/// [server]
70/// socket_path = "/var/run/txgate.sock"
71/// timeout_secs = 60
72///
73/// [keys]
74/// directory = "/var/lib/txgate/keys"
75/// default_key = "main"
76///
77/// [policy]
78/// whitelist_enabled = true
79/// whitelist = ["0x742d35Cc6634C0532925a3b844Bc454e7595f"]
80/// "#;
81///
82/// let config: Config = toml::from_str(toml_str).expect("valid TOML");
83/// assert_eq!(config.server.timeout_secs, 60);
84/// ```
85#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
86pub struct Config {
87    /// Server configuration for the `TxGate` daemon.
88    #[serde(default)]
89    pub server: ServerConfig,
90
91    /// Policy configuration for transaction approval rules.
92    #[serde(default)]
93    pub policy: PolicyConfig,
94
95    /// Key storage configuration.
96    #[serde(default)]
97    pub keys: KeysConfig,
98}
99
100/// Returns the default Unix socket path for the `TxGate` daemon.
101///
102/// The default path is `~/.txgate/txgate.sock`.
103#[must_use]
104fn default_socket_path() -> String {
105    "~/.txgate/txgate.sock".to_string()
106}
107
108/// Returns the default request timeout in seconds.
109///
110/// The default timeout is 30 seconds.
111#[must_use]
112const fn default_timeout() -> u64 {
113    30
114}
115
116/// Server configuration for the `TxGate` daemon.
117///
118/// This struct defines how the `TxGate` daemon listens for requests:
119///
120/// - **Socket path**: The Unix domain socket for IPC communication
121/// - **Timeout**: Maximum time to wait for a signing request to complete
122///
123/// # Security Considerations
124///
125/// The Unix socket should be protected with appropriate file permissions.
126/// By default, only the owning user should have read/write access.
127///
128/// # Examples
129///
130/// ```
131/// use txgate_core::config::ServerConfig;
132///
133/// let config = ServerConfig::default();
134/// assert_eq!(config.socket_path, "~/.txgate/txgate.sock");
135/// assert_eq!(config.timeout_secs, 30);
136/// ```
137#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
138pub struct ServerConfig {
139    /// Unix socket path for the `TxGate` daemon.
140    ///
141    /// The daemon listens on this socket for incoming signing requests.
142    /// The path supports `~` expansion for the home directory.
143    ///
144    /// Default: `~/.txgate/txgate.sock`
145    #[serde(default = "default_socket_path")]
146    pub socket_path: String,
147
148    /// Request timeout in seconds.
149    ///
150    /// Maximum time the daemon will wait for a signing request to complete.
151    /// This includes policy evaluation, key loading, and cryptographic operations.
152    ///
153    /// Default: 30 seconds
154    #[serde(default = "default_timeout")]
155    pub timeout_secs: u64,
156}
157
158impl Default for ServerConfig {
159    fn default() -> Self {
160        Self {
161            socket_path: default_socket_path(),
162            timeout_secs: default_timeout(),
163        }
164    }
165}
166
167/// Returns the default directory for encrypted key files.
168///
169/// The default directory is `~/.txgate/keys`.
170#[must_use]
171fn default_keys_dir() -> String {
172    "~/.txgate/keys".to_string()
173}
174
175/// Returns the default key name for signing operations.
176///
177/// The default key name is `"default"`.
178#[must_use]
179fn default_key_name() -> String {
180    "default".to_string()
181}
182
183/// Key storage configuration for the `TxGate` signing service.
184///
185/// This struct defines where encrypted keys are stored and which key
186/// to use by default for signing operations.
187///
188/// # Directory Structure
189///
190/// Keys are stored as encrypted JSON files in the configured directory:
191/// ```text
192/// ~/.txgate/keys/
193/// ├── default.json
194/// ├── backup.json
195/// └── production.json
196/// ```
197///
198/// # Security Considerations
199///
200/// - The keys directory should have restricted permissions (0700)
201/// - Key files are encrypted with a password-derived key
202/// - Key material is zeroed from memory after use
203///
204/// # Examples
205///
206/// ```
207/// use txgate_core::config::KeysConfig;
208///
209/// let config = KeysConfig::default();
210/// assert_eq!(config.directory, "~/.txgate/keys");
211/// assert_eq!(config.default_key, "default");
212/// ```
213#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
214pub struct KeysConfig {
215    /// Directory for encrypted key files.
216    ///
217    /// The path supports `~` expansion for the home directory.
218    ///
219    /// Default: `~/.txgate/keys`
220    #[serde(default = "default_keys_dir")]
221    pub directory: String,
222
223    /// Default key name to use for signing.
224    ///
225    /// This key is used when no specific key is requested.
226    ///
227    /// Default: `"default"`
228    #[serde(default = "default_key_name")]
229    pub default_key: String,
230}
231
232impl Default for KeysConfig {
233    fn default() -> Self {
234        Self {
235            directory: default_keys_dir(),
236            default_key: default_key_name(),
237        }
238    }
239}
240
241/// Policy configuration for transaction approval rules.
242///
243/// This struct defines the rules that govern which transactions are allowed
244/// or denied by the policy engine. It supports:
245///
246/// - **Whitelist**: Addresses that are always allowed (when enabled)
247/// - **Blacklist**: Addresses that are always denied
248/// - **Transaction limits**: Maximum amount per single transaction
249/// - **Daily limits**: Maximum total amount per 24-hour period
250///
251/// # Address Handling
252///
253/// Address comparisons are case-insensitive for Ethereum-style addresses.
254/// This ensures that `0xABC` and `0xabc` are treated as the same address.
255///
256/// # Security Considerations
257///
258/// - An address cannot be in both whitelist and blacklist
259/// - Blacklist takes precedence if both checks are enabled
260/// - Transaction limits are enforced per token/currency
261///
262/// # Examples
263///
264/// ```
265/// use txgate_core::config::PolicyConfig;
266///
267/// let config = PolicyConfig {
268///     whitelist_enabled: true,
269///     whitelist: vec!["0x1234...".to_string()],
270///     blacklist: vec!["0xdead...".to_string()],
271///     ..Default::default()
272/// };
273///
274/// assert!(config.validate().is_ok());
275/// ```
276#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
277pub struct PolicyConfig {
278    /// Addresses that are always allowed (if whitelist is enabled).
279    ///
280    /// When `whitelist_enabled` is `true`, only addresses in this list
281    /// are permitted as transaction recipients.
282    #[serde(default)]
283    pub whitelist: Vec<String>,
284
285    /// Addresses that are always denied.
286    ///
287    /// Transactions to any address in this list will be rejected,
288    /// regardless of whitelist status.
289    #[serde(default)]
290    pub blacklist: Vec<String>,
291
292    /// Per-token transaction limits (max amount per single tx).
293    ///
294    /// Key: token address or "ETH" for native token.
295    /// Value: maximum amount in the token's smallest unit (wei, etc.).
296    ///
297    /// # Examples
298    ///
299    /// ```
300    /// use txgate_core::config::PolicyConfig;
301    /// use alloy_primitives::U256;
302    /// use std::collections::HashMap;
303    ///
304    /// let mut limits = HashMap::new();
305    /// limits.insert("ETH".to_string(), U256::from(5_000_000_000_000_000_000u64)); // 5 ETH
306    ///
307    /// let config = PolicyConfig {
308    ///     transaction_limits: limits,
309    ///     ..Default::default()
310    /// };
311    /// ```
312    #[serde(default)]
313    pub transaction_limits: HashMap<String, U256>,
314
315    /// Per-token daily limits (max total amount per 24h).
316    ///
317    /// Key: token address or "ETH" for native token.
318    /// Value: maximum daily total in the token's smallest unit (wei, etc.).
319    ///
320    /// Note: Actual daily limit tracking requires a stateful tracker component
321    /// that is not part of this configuration struct.
322    #[serde(default)]
323    pub daily_limits: HashMap<String, U256>,
324
325    /// Whether whitelist is enabled.
326    ///
327    /// - `true`: Only addresses in `whitelist` are allowed
328    /// - `false`: All addresses except those in `blacklist` are allowed
329    #[serde(default)]
330    pub whitelist_enabled: bool,
331}
332
333impl PolicyConfig {
334    /// Creates a new empty policy configuration.
335    ///
336    /// All lists are empty and whitelist is disabled by default.
337    ///
338    /// # Examples
339    ///
340    /// ```
341    /// use txgate_core::config::PolicyConfig;
342    ///
343    /// let config = PolicyConfig::new();
344    /// assert!(!config.whitelist_enabled);
345    /// assert!(config.whitelist.is_empty());
346    /// assert!(config.blacklist.is_empty());
347    /// ```
348    #[must_use]
349    pub fn new() -> Self {
350        Self::default()
351    }
352
353    /// Checks if an address is in the whitelist.
354    ///
355    /// Address comparison is case-insensitive.
356    ///
357    /// # Arguments
358    ///
359    /// * `address` - The address to check
360    ///
361    /// # Returns
362    ///
363    /// `true` if the address is in the whitelist, `false` otherwise.
364    ///
365    /// # Examples
366    ///
367    /// ```
368    /// use txgate_core::config::PolicyConfig;
369    ///
370    /// let config = PolicyConfig::new()
371    ///     .with_whitelist(vec!["0xABC123".to_string()]);
372    ///
373    /// assert!(config.is_whitelisted("0xABC123"));
374    /// assert!(config.is_whitelisted("0xabc123")); // Case insensitive
375    /// assert!(!config.is_whitelisted("0xDEF456"));
376    /// ```
377    #[must_use]
378    pub fn is_whitelisted(&self, address: &str) -> bool {
379        let address_lower = address.to_lowercase();
380        self.whitelist
381            .iter()
382            .any(|a| a.to_lowercase() == address_lower)
383    }
384
385    /// Checks if an address is in the blacklist.
386    ///
387    /// Address comparison is case-insensitive.
388    ///
389    /// # Arguments
390    ///
391    /// * `address` - The address to check
392    ///
393    /// # Returns
394    ///
395    /// `true` if the address is in the blacklist, `false` otherwise.
396    ///
397    /// # Examples
398    ///
399    /// ```
400    /// use txgate_core::config::PolicyConfig;
401    ///
402    /// let config = PolicyConfig::new()
403    ///     .with_blacklist(vec!["0xDEAD".to_string()]);
404    ///
405    /// assert!(config.is_blacklisted("0xDEAD"));
406    /// assert!(config.is_blacklisted("0xdead")); // Case insensitive
407    /// assert!(!config.is_blacklisted("0xABC123"));
408    /// ```
409    #[must_use]
410    pub fn is_blacklisted(&self, address: &str) -> bool {
411        let address_lower = address.to_lowercase();
412        self.blacklist
413            .iter()
414            .any(|a| a.to_lowercase() == address_lower)
415    }
416
417    /// Gets the per-transaction limit for a token.
418    ///
419    /// Token key comparison is case-insensitive.
420    ///
421    /// # Arguments
422    ///
423    /// * `token` - The token identifier (e.g., "ETH" or a contract address)
424    ///
425    /// # Returns
426    ///
427    /// The transaction limit if configured, `None` if no limit is set.
428    ///
429    /// # Examples
430    ///
431    /// ```
432    /// use txgate_core::config::PolicyConfig;
433    /// use alloy_primitives::U256;
434    ///
435    /// let config = PolicyConfig::new()
436    ///     .with_transaction_limit("ETH", U256::from(5_000_000_000_000_000_000u64));
437    ///
438    /// assert_eq!(
439    ///     config.get_transaction_limit("ETH"),
440    ///     Some(U256::from(5_000_000_000_000_000_000u64))
441    /// );
442    /// assert_eq!(config.get_transaction_limit("eth"), Some(U256::from(5_000_000_000_000_000_000u64))); // Case insensitive
443    /// assert!(config.get_transaction_limit("BTC").is_none());
444    /// ```
445    #[must_use]
446    pub fn get_transaction_limit(&self, token: &str) -> Option<U256> {
447        let token_lower = token.to_lowercase();
448        self.transaction_limits
449            .iter()
450            .find(|(k, _)| k.to_lowercase() == token_lower)
451            .map(|(_, v)| *v)
452    }
453
454    /// Gets the daily limit for a token.
455    ///
456    /// Token key comparison is case-insensitive.
457    ///
458    /// # Arguments
459    ///
460    /// * `token` - The token identifier (e.g., "ETH" or a contract address)
461    ///
462    /// # Returns
463    ///
464    /// The daily limit if configured, `None` if no limit is set.
465    ///
466    /// # Examples
467    ///
468    /// ```
469    /// use txgate_core::config::PolicyConfig;
470    /// use alloy_primitives::U256;
471    ///
472    /// let config = PolicyConfig::new()
473    ///     .with_daily_limit("ETH", U256::from(10_000_000_000_000_000_000u64));
474    ///
475    /// assert_eq!(
476    ///     config.get_daily_limit("ETH"),
477    ///     Some(U256::from(10_000_000_000_000_000_000u64))
478    /// );
479    /// assert!(config.get_daily_limit("BTC").is_none());
480    /// ```
481    #[must_use]
482    pub fn get_daily_limit(&self, token: &str) -> Option<U256> {
483        let token_lower = token.to_lowercase();
484        self.daily_limits
485            .iter()
486            .find(|(k, _)| k.to_lowercase() == token_lower)
487            .map(|(_, v)| *v)
488    }
489
490    /// Validates the policy configuration.
491    ///
492    /// Checks for configuration errors such as:
493    /// - Addresses appearing in both whitelist and blacklist
494    ///
495    /// # Returns
496    ///
497    /// `Ok(())` if the configuration is valid, or a `PolicyError` describing the issue.
498    ///
499    /// # Errors
500    ///
501    /// Returns [`PolicyError::InvalidConfiguration`] if:
502    /// - An address appears in both the whitelist and blacklist (case-insensitive)
503    ///
504    /// # Examples
505    ///
506    /// ```
507    /// use txgate_core::config::PolicyConfig;
508    ///
509    /// // Valid config - no overlap
510    /// let valid = PolicyConfig::new()
511    ///     .with_whitelist(vec!["0xAAA".to_string()])
512    ///     .with_blacklist(vec!["0xBBB".to_string()]);
513    /// assert!(valid.validate().is_ok());
514    ///
515    /// // Invalid config - same address in both lists
516    /// let invalid = PolicyConfig::new()
517    ///     .with_whitelist(vec!["0xAAA".to_string()])
518    ///     .with_blacklist(vec!["0xAAA".to_string()]);
519    /// assert!(invalid.validate().is_err());
520    /// ```
521    pub fn validate(&self) -> Result<(), PolicyError> {
522        // Check for overlapping addresses between whitelist and blacklist
523        for whitelist_addr in &self.whitelist {
524            let whitelist_lower = whitelist_addr.to_lowercase();
525            for blacklist_addr in &self.blacklist {
526                if blacklist_addr.to_lowercase() == whitelist_lower {
527                    return Err(PolicyError::invalid_configuration(format!(
528                        "address '{whitelist_addr}' appears in both whitelist and blacklist"
529                    )));
530                }
531            }
532        }
533
534        Ok(())
535    }
536
537    /// Builder method to set the whitelist.
538    ///
539    /// This enables the whitelist automatically.
540    ///
541    /// # Arguments
542    ///
543    /// * `addresses` - List of addresses to whitelist
544    ///
545    /// # Examples
546    ///
547    /// ```
548    /// use txgate_core::config::PolicyConfig;
549    ///
550    /// let config = PolicyConfig::new()
551    ///     .with_whitelist(vec!["0xAAA".to_string(), "0xBBB".to_string()]);
552    ///
553    /// assert!(config.whitelist_enabled);
554    /// assert_eq!(config.whitelist.len(), 2);
555    /// ```
556    #[must_use]
557    pub fn with_whitelist(mut self, addresses: Vec<String>) -> Self {
558        self.whitelist = addresses;
559        self.whitelist_enabled = true;
560        self
561    }
562
563    /// Builder method to set the blacklist.
564    ///
565    /// # Arguments
566    ///
567    /// * `addresses` - List of addresses to blacklist
568    ///
569    /// # Examples
570    ///
571    /// ```
572    /// use txgate_core::config::PolicyConfig;
573    ///
574    /// let config = PolicyConfig::new()
575    ///     .with_blacklist(vec!["0xDEAD".to_string()]);
576    ///
577    /// assert_eq!(config.blacklist.len(), 1);
578    /// ```
579    #[must_use]
580    pub fn with_blacklist(mut self, addresses: Vec<String>) -> Self {
581        self.blacklist = addresses;
582        self
583    }
584
585    /// Builder method to add a per-transaction limit for a token.
586    ///
587    /// # Arguments
588    ///
589    /// * `token` - Token identifier (e.g., "ETH" or contract address)
590    /// * `limit` - Maximum amount per transaction
591    ///
592    /// # Examples
593    ///
594    /// ```
595    /// use txgate_core::config::PolicyConfig;
596    /// use alloy_primitives::U256;
597    ///
598    /// let config = PolicyConfig::new()
599    ///     .with_transaction_limit("ETH", U256::from(5_000_000_000_000_000_000u64))
600    ///     .with_transaction_limit("USDC", U256::from(10_000_000_000u64)); // 10k USDC
601    ///
602    /// assert!(config.get_transaction_limit("ETH").is_some());
603    /// assert!(config.get_transaction_limit("USDC").is_some());
604    /// ```
605    #[must_use]
606    pub fn with_transaction_limit(mut self, token: &str, limit: U256) -> Self {
607        self.transaction_limits.insert(token.to_string(), limit);
608        self
609    }
610
611    /// Builder method to add a daily limit for a token.
612    ///
613    /// # Arguments
614    ///
615    /// * `token` - Token identifier (e.g., "ETH" or contract address)
616    /// * `limit` - Maximum total amount per 24-hour period
617    ///
618    /// # Examples
619    ///
620    /// ```
621    /// use txgate_core::config::PolicyConfig;
622    /// use alloy_primitives::U256;
623    ///
624    /// let config = PolicyConfig::new()
625    ///     .with_daily_limit("ETH", U256::from(10_000_000_000_000_000_000u64));
626    ///
627    /// assert!(config.get_daily_limit("ETH").is_some());
628    /// ```
629    #[must_use]
630    pub fn with_daily_limit(mut self, token: &str, limit: U256) -> Self {
631        self.daily_limits.insert(token.to_string(), limit);
632        self
633    }
634
635    /// Builder method to enable or disable whitelist mode.
636    ///
637    /// # Arguments
638    ///
639    /// * `enabled` - Whether whitelist mode should be enabled
640    ///
641    /// # Examples
642    ///
643    /// ```
644    /// use txgate_core::config::PolicyConfig;
645    ///
646    /// let config = PolicyConfig::new()
647    ///     .with_whitelist_enabled(false);
648    ///
649    /// assert!(!config.whitelist_enabled);
650    /// ```
651    #[must_use]
652    pub const fn with_whitelist_enabled(mut self, enabled: bool) -> Self {
653        self.whitelist_enabled = enabled;
654        self
655    }
656}
657
658impl Config {
659    /// Creates a new configuration with default values.
660    ///
661    /// # Examples
662    ///
663    /// ```
664    /// use txgate_core::config::Config;
665    ///
666    /// let config = Config::new();
667    /// assert_eq!(config.server.timeout_secs, 30);
668    /// ```
669    #[must_use]
670    pub fn new() -> Self {
671        Self::default()
672    }
673
674    /// Validates the configuration.
675    ///
676    /// This method checks for configuration errors such as:
677    /// - Invalid socket path (empty)
678    /// - Invalid timeout (zero)
679    /// - Invalid keys directory (empty)
680    /// - Invalid default key name (empty)
681    /// - Invalid policy configuration (whitelist/blacklist overlap)
682    ///
683    /// # Returns
684    ///
685    /// `Ok(())` if the configuration is valid, or a `ConfigError` describing the issue.
686    ///
687    /// # Errors
688    ///
689    /// Returns [`ConfigError::InvalidValue`] if:
690    /// - `server.socket_path` is empty
691    /// - `server.timeout_secs` is zero
692    /// - `keys.directory` is empty
693    /// - `keys.default_key` is empty
694    ///
695    /// Returns a wrapped [`PolicyError`] (via [`ConfigError::ParseFailed`]) if:
696    /// - An address appears in both the whitelist and blacklist
697    ///
698    /// # Examples
699    ///
700    /// ```
701    /// use txgate_core::config::Config;
702    ///
703    /// let config = Config::default();
704    /// assert!(config.validate().is_ok());
705    ///
706    /// // Invalid: empty socket path
707    /// let mut invalid_config = Config::default();
708    /// invalid_config.server.socket_path = String::new();
709    /// assert!(invalid_config.validate().is_err());
710    /// ```
711    ///
712    /// [`PolicyError`]: crate::error::PolicyError
713    pub fn validate(&self) -> Result<(), ConfigError> {
714        // Validate server configuration
715        if self.server.socket_path.is_empty() {
716            return Err(ConfigError::invalid_value("server.socket_path", "<empty>"));
717        }
718
719        if self.server.timeout_secs == 0 {
720            return Err(ConfigError::invalid_value("server.timeout_secs", "0"));
721        }
722
723        // Validate keys configuration
724        if self.keys.directory.is_empty() {
725            return Err(ConfigError::invalid_value("keys.directory", "<empty>"));
726        }
727
728        if self.keys.default_key.is_empty() {
729            return Err(ConfigError::invalid_value("keys.default_key", "<empty>"));
730        }
731
732        // Validate policy configuration
733        self.policy
734            .validate()
735            .map_err(|e| ConfigError::parse_failed(format!("policy validation failed: {e}")))?;
736
737        Ok(())
738    }
739
740    /// Generates the default configuration as a TOML string.
741    ///
742    /// This method produces a well-formatted TOML configuration file with
743    /// comments explaining each section. It can be used to create an initial
744    /// configuration file for new installations.
745    ///
746    /// # Returns
747    ///
748    /// A TOML-formatted string containing the default configuration.
749    ///
750    /// # Examples
751    ///
752    /// ```
753    /// use txgate_core::config::Config;
754    ///
755    /// let toml = Config::default_toml();
756    /// assert!(toml.contains("[server]"));
757    /// assert!(toml.contains("[keys]"));
758    /// assert!(toml.contains("[policy]"));
759    /// ```
760    #[must_use]
761    pub fn default_toml() -> String {
762        r#"[server]
763socket_path = "~/.txgate/txgate.sock"
764timeout_secs = 30
765
766[keys]
767directory = "~/.txgate/keys"
768default_key = "default"
769
770[policy]
771whitelist_enabled = false
772whitelist = []
773blacklist = []
774
775[policy.transaction_limits]
776# ETH = "1000000000000000000"  # 1 ETH
777
778[policy.daily_limits]
779# ETH = "10000000000000000000"  # 10 ETH
780"#
781        .to_string()
782    }
783
784    /// Creates a configuration builder for customizing values.
785    ///
786    /// # Examples
787    ///
788    /// ```
789    /// use txgate_core::config::Config;
790    ///
791    /// let config = Config::builder()
792    ///     .socket_path("/var/run/txgate.sock")
793    ///     .timeout_secs(60)
794    ///     .build();
795    ///
796    /// assert_eq!(config.server.socket_path, "/var/run/txgate.sock");
797    /// ```
798    #[must_use]
799    pub fn builder() -> ConfigBuilder {
800        ConfigBuilder::new()
801    }
802}
803
804/// Builder for creating customized [`Config`] instances.
805///
806/// This builder provides a fluent API for constructing configuration
807/// objects with non-default values.
808///
809/// # Examples
810///
811/// ```
812/// use txgate_core::config::{Config, ConfigBuilder};
813///
814/// let config = ConfigBuilder::new()
815///     .socket_path("/custom/path.sock")
816///     .timeout_secs(120)
817///     .keys_directory("/custom/keys")
818///     .default_key("production")
819///     .build();
820///
821/// assert_eq!(config.server.socket_path, "/custom/path.sock");
822/// assert_eq!(config.server.timeout_secs, 120);
823/// assert_eq!(config.keys.directory, "/custom/keys");
824/// assert_eq!(config.keys.default_key, "production");
825/// ```
826#[derive(Debug, Clone, Default)]
827pub struct ConfigBuilder {
828    config: Config,
829}
830
831impl ConfigBuilder {
832    /// Creates a new configuration builder with default values.
833    #[must_use]
834    pub fn new() -> Self {
835        Self {
836            config: Config::default(),
837        }
838    }
839
840    /// Sets the Unix socket path.
841    ///
842    /// # Arguments
843    ///
844    /// * `path` - The socket path to use
845    #[must_use]
846    pub fn socket_path(mut self, path: impl Into<String>) -> Self {
847        self.config.server.socket_path = path.into();
848        self
849    }
850
851    /// Sets the request timeout in seconds.
852    ///
853    /// # Arguments
854    ///
855    /// * `secs` - The timeout value in seconds
856    #[must_use]
857    pub const fn timeout_secs(mut self, secs: u64) -> Self {
858        self.config.server.timeout_secs = secs;
859        self
860    }
861
862    /// Sets the keys directory.
863    ///
864    /// # Arguments
865    ///
866    /// * `dir` - The directory path for key storage
867    #[must_use]
868    pub fn keys_directory(mut self, dir: impl Into<String>) -> Self {
869        self.config.keys.directory = dir.into();
870        self
871    }
872
873    /// Sets the default key name.
874    ///
875    /// # Arguments
876    ///
877    /// * `name` - The default key name
878    #[must_use]
879    pub fn default_key(mut self, name: impl Into<String>) -> Self {
880        self.config.keys.default_key = name.into();
881        self
882    }
883
884    /// Sets the policy configuration.
885    ///
886    /// # Arguments
887    ///
888    /// * `policy` - The policy configuration to use
889    #[must_use]
890    pub fn policy(mut self, policy: PolicyConfig) -> Self {
891        self.config.policy = policy;
892        self
893    }
894
895    /// Builds the final configuration.
896    ///
897    /// # Returns
898    ///
899    /// The configured [`Config`] instance.
900    #[must_use]
901    pub fn build(self) -> Config {
902        self.config
903    }
904}
905
906#[cfg(test)]
907mod tests {
908    #![allow(
909        clippy::expect_used,
910        clippy::unwrap_used,
911        clippy::panic,
912        clippy::indexing_slicing,
913        clippy::similar_names,
914        clippy::redundant_clone,
915        clippy::manual_string_new,
916        clippy::needless_raw_string_hashes,
917        clippy::needless_collect,
918        clippy::unreadable_literal
919    )]
920
921    use super::*;
922
923    // -------------------------------------------------------------------------
924    // Config basic tests
925    // -------------------------------------------------------------------------
926
927    #[test]
928    fn test_config_default() {
929        let config = Config::default();
930
931        assert_eq!(config.server.socket_path, "~/.txgate/txgate.sock");
932        assert_eq!(config.server.timeout_secs, 30);
933        assert_eq!(config.keys.directory, "~/.txgate/keys");
934        assert_eq!(config.keys.default_key, "default");
935        assert!(!config.policy.whitelist_enabled);
936        assert!(config.policy.whitelist.is_empty());
937        assert!(config.policy.blacklist.is_empty());
938    }
939
940    #[test]
941    fn test_config_new() {
942        let config = Config::new();
943        assert_eq!(config, Config::default());
944    }
945
946    // -------------------------------------------------------------------------
947    // ServerConfig tests
948    // -------------------------------------------------------------------------
949
950    #[test]
951    fn test_server_config_default() {
952        let config = ServerConfig::default();
953
954        assert_eq!(config.socket_path, "~/.txgate/txgate.sock");
955        assert_eq!(config.timeout_secs, 30);
956    }
957
958    #[test]
959    fn test_server_config_custom() {
960        let config = ServerConfig {
961            socket_path: "/var/run/txgate.sock".to_string(),
962            timeout_secs: 60,
963        };
964
965        assert_eq!(config.socket_path, "/var/run/txgate.sock");
966        assert_eq!(config.timeout_secs, 60);
967    }
968
969    // -------------------------------------------------------------------------
970    // KeysConfig tests
971    // -------------------------------------------------------------------------
972
973    #[test]
974    fn test_keys_config_default() {
975        let config = KeysConfig::default();
976
977        assert_eq!(config.directory, "~/.txgate/keys");
978        assert_eq!(config.default_key, "default");
979    }
980
981    #[test]
982    fn test_keys_config_custom() {
983        let config = KeysConfig {
984            directory: "/var/lib/txgate/keys".to_string(),
985            default_key: "production".to_string(),
986        };
987
988        assert_eq!(config.directory, "/var/lib/txgate/keys");
989        assert_eq!(config.default_key, "production");
990    }
991
992    // -------------------------------------------------------------------------
993    // PolicyConfig tests
994    // -------------------------------------------------------------------------
995
996    #[test]
997    fn test_policy_config_default() {
998        let config = PolicyConfig::default();
999
1000        assert!(config.whitelist.is_empty());
1001        assert!(config.blacklist.is_empty());
1002        assert!(config.transaction_limits.is_empty());
1003        assert!(config.daily_limits.is_empty());
1004        assert!(!config.whitelist_enabled);
1005    }
1006
1007    #[test]
1008    fn test_policy_config_new() {
1009        let config = PolicyConfig::new();
1010        assert_eq!(config, PolicyConfig::default());
1011    }
1012
1013    #[test]
1014    fn test_policy_is_whitelisted() {
1015        let config = PolicyConfig::new().with_whitelist(vec!["0xABC123".to_string()]);
1016
1017        assert!(config.is_whitelisted("0xABC123"));
1018        assert!(config.is_whitelisted("0xabc123")); // Case insensitive
1019        assert!(!config.is_whitelisted("0xDEF456"));
1020    }
1021
1022    #[test]
1023    fn test_policy_is_blacklisted() {
1024        let config = PolicyConfig::new().with_blacklist(vec!["0xDEAD".to_string()]);
1025
1026        assert!(config.is_blacklisted("0xDEAD"));
1027        assert!(config.is_blacklisted("0xdead")); // Case insensitive
1028        assert!(!config.is_blacklisted("0xABC123"));
1029    }
1030
1031    #[test]
1032    fn test_policy_transaction_limit() {
1033        let config = PolicyConfig::new()
1034            .with_transaction_limit("ETH", U256::from(5_000_000_000_000_000_000u64));
1035
1036        assert_eq!(
1037            config.get_transaction_limit("ETH"),
1038            Some(U256::from(5_000_000_000_000_000_000u64))
1039        );
1040        assert_eq!(
1041            config.get_transaction_limit("eth"),
1042            Some(U256::from(5_000_000_000_000_000_000u64))
1043        );
1044        assert!(config.get_transaction_limit("BTC").is_none());
1045    }
1046
1047    #[test]
1048    fn test_policy_daily_limit() {
1049        let config =
1050            PolicyConfig::new().with_daily_limit("ETH", U256::from(10_000_000_000_000_000_000u64));
1051
1052        assert_eq!(
1053            config.get_daily_limit("ETH"),
1054            Some(U256::from(10_000_000_000_000_000_000u64))
1055        );
1056        assert!(config.get_daily_limit("BTC").is_none());
1057    }
1058
1059    #[test]
1060    fn test_policy_validate_passes() {
1061        let config = PolicyConfig::new()
1062            .with_whitelist(vec!["0xAAA".to_string()])
1063            .with_blacklist(vec!["0xBBB".to_string()]);
1064
1065        assert!(config.validate().is_ok());
1066    }
1067
1068    #[test]
1069    fn test_policy_validate_fails_overlap() {
1070        let config = PolicyConfig::new()
1071            .with_whitelist(vec!["0xAAA".to_string()])
1072            .with_blacklist(vec!["0xAAA".to_string()]);
1073
1074        assert!(config.validate().is_err());
1075    }
1076
1077    #[test]
1078    fn test_policy_validate_fails_case_insensitive_overlap() {
1079        let config = PolicyConfig::new()
1080            .with_whitelist(vec!["0xABC".to_string()])
1081            .with_blacklist(vec!["0xabc".to_string()]);
1082
1083        assert!(config.validate().is_err());
1084    }
1085
1086    // -------------------------------------------------------------------------
1087    // Validation tests
1088    // -------------------------------------------------------------------------
1089
1090    #[test]
1091    fn test_validate_passes_for_default_config() {
1092        let config = Config::default();
1093        assert!(config.validate().is_ok());
1094    }
1095
1096    #[test]
1097    fn test_validate_fails_for_empty_socket_path() {
1098        let mut config = Config::default();
1099        config.server.socket_path = String::new();
1100
1101        let result = config.validate();
1102        assert!(result.is_err());
1103
1104        let err = result.expect_err("should have error");
1105        assert!(matches!(err, ConfigError::InvalidValue { .. }));
1106        assert!(err.to_string().contains("socket_path"));
1107    }
1108
1109    #[test]
1110    fn test_validate_fails_for_zero_timeout() {
1111        let mut config = Config::default();
1112        config.server.timeout_secs = 0;
1113
1114        let result = config.validate();
1115        assert!(result.is_err());
1116
1117        let err = result.expect_err("should have error");
1118        assert!(matches!(err, ConfigError::InvalidValue { .. }));
1119        assert!(err.to_string().contains("timeout_secs"));
1120    }
1121
1122    #[test]
1123    fn test_validate_fails_for_empty_keys_directory() {
1124        let mut config = Config::default();
1125        config.keys.directory = String::new();
1126
1127        let result = config.validate();
1128        assert!(result.is_err());
1129
1130        let err = result.expect_err("should have error");
1131        assert!(matches!(err, ConfigError::InvalidValue { .. }));
1132        assert!(err.to_string().contains("directory"));
1133    }
1134
1135    #[test]
1136    fn test_validate_fails_for_empty_default_key() {
1137        let mut config = Config::default();
1138        config.keys.default_key = String::new();
1139
1140        let result = config.validate();
1141        assert!(result.is_err());
1142
1143        let err = result.expect_err("should have error");
1144        assert!(matches!(err, ConfigError::InvalidValue { .. }));
1145        assert!(err.to_string().contains("default_key"));
1146    }
1147
1148    #[test]
1149    fn test_validate_fails_for_invalid_policy() {
1150        let mut config = Config::default();
1151        config.policy.whitelist = vec!["0xAAA".to_string()];
1152        config.policy.blacklist = vec!["0xAAA".to_string()];
1153
1154        let result = config.validate();
1155        assert!(result.is_err());
1156
1157        let err = result.expect_err("should have error");
1158        assert!(matches!(err, ConfigError::ParseFailed { .. }));
1159        assert!(err.to_string().contains("policy validation failed"));
1160    }
1161
1162    // -------------------------------------------------------------------------
1163    // TOML serialization tests
1164    // -------------------------------------------------------------------------
1165
1166    #[test]
1167    fn test_toml_serialization_roundtrip() {
1168        let original = Config::default();
1169
1170        let toml_str = toml::to_string(&original).expect("TOML serialization failed");
1171        let deserialized: Config = toml::from_str(&toml_str).expect("TOML deserialization failed");
1172
1173        assert_eq!(original, deserialized);
1174    }
1175
1176    #[test]
1177    fn test_toml_deserialization_with_defaults() {
1178        let toml_str = r#"
1179            [server]
1180            socket_path = "/custom/path.sock"
1181        "#;
1182
1183        let config: Config = toml::from_str(toml_str).expect("TOML deserialization failed");
1184
1185        assert_eq!(config.server.socket_path, "/custom/path.sock");
1186        assert_eq!(config.server.timeout_secs, 30); // default
1187        assert_eq!(config.keys.directory, "~/.txgate/keys"); // default
1188        assert_eq!(config.keys.default_key, "default"); // default
1189    }
1190
1191    #[test]
1192    fn test_toml_deserialization_empty_config() {
1193        let toml_str = "";
1194        let config: Config = toml::from_str(toml_str).expect("TOML deserialization failed");
1195
1196        assert_eq!(config, Config::default());
1197    }
1198
1199    #[test]
1200    fn test_toml_deserialization_missing_sections() {
1201        // Only server section present
1202        let toml_str = r#"
1203            [server]
1204            socket_path = "/var/run/txgate.sock"
1205            timeout_secs = 60
1206        "#;
1207
1208        let config: Config = toml::from_str(toml_str).expect("TOML deserialization failed");
1209
1210        assert_eq!(config.server.socket_path, "/var/run/txgate.sock");
1211        assert_eq!(config.server.timeout_secs, 60);
1212        // Keys and policy should use defaults
1213        assert_eq!(config.keys, KeysConfig::default());
1214        assert_eq!(config.policy, PolicyConfig::default());
1215    }
1216
1217    #[test]
1218    fn test_toml_deserialization_partial_sections() {
1219        let toml_str = r#"
1220            [server]
1221            timeout_secs = 120
1222
1223            [keys]
1224            default_key = "prod"
1225        "#;
1226
1227        let config: Config = toml::from_str(toml_str).expect("TOML deserialization failed");
1228
1229        // Only specified values should override defaults
1230        assert_eq!(config.server.socket_path, "~/.txgate/txgate.sock"); // default
1231        assert_eq!(config.server.timeout_secs, 120);
1232        assert_eq!(config.keys.directory, "~/.txgate/keys"); // default
1233        assert_eq!(config.keys.default_key, "prod");
1234    }
1235
1236    #[test]
1237    fn test_toml_deserialization_full_config() {
1238        let toml_str = r#"
1239            [server]
1240            socket_path = "/var/run/txgate.sock"
1241            timeout_secs = 60
1242
1243            [keys]
1244            directory = "/var/lib/txgate/keys"
1245            default_key = "production"
1246
1247            [policy]
1248            whitelist_enabled = true
1249            whitelist = ["0x742d35Cc6634C0532925a3b844Bc454e7595f"]
1250            blacklist = ["0xDEADBEEF"]
1251        "#;
1252
1253        let config: Config = toml::from_str(toml_str).expect("TOML deserialization failed");
1254
1255        assert_eq!(config.server.socket_path, "/var/run/txgate.sock");
1256        assert_eq!(config.server.timeout_secs, 60);
1257        assert_eq!(config.keys.directory, "/var/lib/txgate/keys");
1258        assert_eq!(config.keys.default_key, "production");
1259        assert!(config.policy.whitelist_enabled);
1260        assert_eq!(
1261            config.policy.whitelist,
1262            vec!["0x742d35Cc6634C0532925a3b844Bc454e7595f"]
1263        );
1264        assert_eq!(config.policy.blacklist, vec!["0xDEADBEEF"]);
1265    }
1266
1267    #[test]
1268    fn test_toml_with_policy_limits() {
1269        let toml_str = r#"
1270            [policy]
1271            whitelist_enabled = false
1272
1273            [policy.transaction_limits]
1274            ETH = "1000000000000000000"
1275
1276            [policy.daily_limits]
1277            ETH = "10000000000000000000"
1278        "#;
1279
1280        let config: Config = toml::from_str(toml_str).expect("TOML deserialization failed");
1281
1282        assert_eq!(
1283            config.policy.get_transaction_limit("ETH"),
1284            Some(U256::from(1_000_000_000_000_000_000u64))
1285        );
1286        assert_eq!(
1287            config.policy.get_daily_limit("ETH"),
1288            Some(U256::from(10_000_000_000_000_000_000u64))
1289        );
1290    }
1291
1292    // -------------------------------------------------------------------------
1293    // default_toml() tests
1294    // -------------------------------------------------------------------------
1295
1296    #[test]
1297    fn test_default_toml_contains_all_sections() {
1298        let toml = Config::default_toml();
1299
1300        assert!(toml.contains("[server]"));
1301        assert!(toml.contains("[keys]"));
1302        assert!(toml.contains("[policy]"));
1303        assert!(toml.contains("[policy.transaction_limits]"));
1304        assert!(toml.contains("[policy.daily_limits]"));
1305    }
1306
1307    #[test]
1308    fn test_default_toml_contains_default_values() {
1309        let toml = Config::default_toml();
1310
1311        assert!(toml.contains("socket_path = \"~/.txgate/txgate.sock\""));
1312        assert!(toml.contains("timeout_secs = 30"));
1313        assert!(toml.contains("directory = \"~/.txgate/keys\""));
1314        assert!(toml.contains("default_key = \"default\""));
1315        assert!(toml.contains("whitelist_enabled = false"));
1316    }
1317
1318    #[test]
1319    fn test_default_toml_is_parseable() {
1320        let toml_str = Config::default_toml();
1321        let config: Config = toml::from_str(&toml_str).expect("default TOML should be parseable");
1322
1323        // Verify it produces the expected default config
1324        assert_eq!(config.server.socket_path, "~/.txgate/txgate.sock");
1325        assert_eq!(config.server.timeout_secs, 30);
1326    }
1327
1328    // -------------------------------------------------------------------------
1329    // Builder tests
1330    // -------------------------------------------------------------------------
1331
1332    #[test]
1333    fn test_builder_default() {
1334        let config = ConfigBuilder::new().build();
1335        assert_eq!(config, Config::default());
1336    }
1337
1338    #[test]
1339    fn test_builder_socket_path() {
1340        let config = Config::builder().socket_path("/custom/path.sock").build();
1341
1342        assert_eq!(config.server.socket_path, "/custom/path.sock");
1343    }
1344
1345    #[test]
1346    fn test_builder_timeout_secs() {
1347        let config = Config::builder().timeout_secs(120).build();
1348
1349        assert_eq!(config.server.timeout_secs, 120);
1350    }
1351
1352    #[test]
1353    fn test_builder_keys_directory() {
1354        let config = Config::builder().keys_directory("/custom/keys").build();
1355
1356        assert_eq!(config.keys.directory, "/custom/keys");
1357    }
1358
1359    #[test]
1360    fn test_builder_default_key() {
1361        let config = Config::builder().default_key("production").build();
1362
1363        assert_eq!(config.keys.default_key, "production");
1364    }
1365
1366    #[test]
1367    fn test_builder_policy() {
1368        let policy = PolicyConfig::new()
1369            .with_whitelist(vec!["0xAAA".to_string()])
1370            .with_transaction_limit("ETH", U256::from(1_000_000u64));
1371
1372        let config = Config::builder().policy(policy.clone()).build();
1373
1374        assert_eq!(config.policy, policy);
1375    }
1376
1377    #[test]
1378    fn test_builder_chain() {
1379        let config = Config::builder()
1380            .socket_path("/var/run/txgate.sock")
1381            .timeout_secs(60)
1382            .keys_directory("/var/lib/txgate/keys")
1383            .default_key("main")
1384            .build();
1385
1386        assert_eq!(config.server.socket_path, "/var/run/txgate.sock");
1387        assert_eq!(config.server.timeout_secs, 60);
1388        assert_eq!(config.keys.directory, "/var/lib/txgate/keys");
1389        assert_eq!(config.keys.default_key, "main");
1390    }
1391
1392    // -------------------------------------------------------------------------
1393    // Clone and equality tests
1394    // -------------------------------------------------------------------------
1395
1396    #[test]
1397    fn test_config_clone() {
1398        let original = Config::builder()
1399            .socket_path("/custom/path.sock")
1400            .timeout_secs(60)
1401            .build();
1402
1403        let cloned = original.clone();
1404        assert_eq!(original, cloned);
1405    }
1406
1407    #[test]
1408    fn test_config_equality() {
1409        let config1 = Config::builder()
1410            .socket_path("/path1.sock")
1411            .timeout_secs(30)
1412            .build();
1413
1414        let config2 = Config::builder()
1415            .socket_path("/path1.sock")
1416            .timeout_secs(30)
1417            .build();
1418
1419        assert_eq!(config1, config2);
1420    }
1421
1422    #[test]
1423    fn test_config_inequality() {
1424        let config1 = Config::builder().socket_path("/path1.sock").build();
1425
1426        let config2 = Config::builder().socket_path("/path2.sock").build();
1427
1428        assert_ne!(config1, config2);
1429    }
1430
1431    // -------------------------------------------------------------------------
1432    // Debug format tests
1433    // -------------------------------------------------------------------------
1434
1435    #[test]
1436    fn test_config_debug() {
1437        let config = Config::default();
1438        let debug_str = format!("{config:?}");
1439
1440        assert!(debug_str.contains("Config"));
1441        assert!(debug_str.contains("server"));
1442        assert!(debug_str.contains("policy"));
1443        assert!(debug_str.contains("keys"));
1444    }
1445
1446    #[test]
1447    fn test_server_config_debug() {
1448        let config = ServerConfig::default();
1449        let debug_str = format!("{config:?}");
1450
1451        assert!(debug_str.contains("ServerConfig"));
1452        assert!(debug_str.contains("socket_path"));
1453        assert!(debug_str.contains("timeout_secs"));
1454    }
1455
1456    #[test]
1457    fn test_keys_config_debug() {
1458        let config = KeysConfig::default();
1459        let debug_str = format!("{config:?}");
1460
1461        assert!(debug_str.contains("KeysConfig"));
1462        assert!(debug_str.contains("directory"));
1463        assert!(debug_str.contains("default_key"));
1464    }
1465
1466    #[test]
1467    fn test_policy_config_debug() {
1468        let config = PolicyConfig::default();
1469        let debug_str = format!("{config:?}");
1470
1471        assert!(debug_str.contains("PolicyConfig"));
1472        assert!(debug_str.contains("whitelist"));
1473        assert!(debug_str.contains("blacklist"));
1474    }
1475
1476    // -------------------------------------------------------------------------
1477    // JSON serialization tests (for API responses)
1478    // -------------------------------------------------------------------------
1479
1480    #[test]
1481    fn test_json_serialization_roundtrip() {
1482        let original = Config::builder()
1483            .socket_path("/test.sock")
1484            .timeout_secs(45)
1485            .build();
1486
1487        let json_str = serde_json::to_string(&original).expect("JSON serialization failed");
1488        let deserialized: Config =
1489            serde_json::from_str(&json_str).expect("JSON deserialization failed");
1490
1491        assert_eq!(original, deserialized);
1492    }
1493
1494    // -------------------------------------------------------------------------
1495    // PolicyConfig builder tests
1496    // -------------------------------------------------------------------------
1497
1498    #[test]
1499    fn test_policy_builder_chain() {
1500        let config = PolicyConfig::new()
1501            .with_whitelist(vec!["0xWhite".to_string()])
1502            .with_blacklist(vec!["0xBlack".to_string()])
1503            .with_transaction_limit("ETH", U256::from(100))
1504            .with_daily_limit("ETH", U256::from(1000))
1505            .with_whitelist_enabled(true);
1506
1507        assert!(config.is_whitelisted("0xWhite"));
1508        assert!(config.is_blacklisted("0xBlack"));
1509        assert_eq!(config.get_transaction_limit("ETH"), Some(U256::from(100)));
1510        assert_eq!(config.get_daily_limit("ETH"), Some(U256::from(1000)));
1511        assert!(config.whitelist_enabled);
1512    }
1513
1514    #[test]
1515    fn test_policy_with_whitelist_enables_whitelist() {
1516        let config = PolicyConfig::new().with_whitelist(vec!["0xTest".to_string()]);
1517
1518        assert!(config.whitelist_enabled);
1519    }
1520
1521    #[test]
1522    fn test_policy_with_whitelist_enabled_can_disable() {
1523        let config = PolicyConfig::new()
1524            .with_whitelist(vec!["0xTest".to_string()])
1525            .with_whitelist_enabled(false);
1526
1527        assert!(!config.whitelist_enabled);
1528    }
1529
1530    // -------------------------------------------------------------------------
1531    // Edge case tests
1532    // -------------------------------------------------------------------------
1533
1534    #[test]
1535    fn test_policy_empty_address_handling() {
1536        let config = PolicyConfig::new()
1537            .with_whitelist(vec!["".to_string()])
1538            .with_blacklist(vec!["".to_string()]);
1539
1540        // Empty string in both lists should still cause validation to fail
1541        assert!(config.validate().is_err());
1542    }
1543
1544    #[test]
1545    fn test_policy_zero_limits() {
1546        let config = PolicyConfig::new()
1547            .with_transaction_limit("ETH", U256::ZERO)
1548            .with_daily_limit("ETH", U256::ZERO);
1549
1550        assert_eq!(config.get_transaction_limit("ETH"), Some(U256::ZERO));
1551        assert_eq!(config.get_daily_limit("ETH"), Some(U256::ZERO));
1552    }
1553
1554    #[test]
1555    fn test_policy_large_u256_limits() {
1556        let large_value = U256::MAX;
1557        let config = PolicyConfig::new()
1558            .with_transaction_limit("ETH", large_value)
1559            .with_daily_limit("ETH", large_value);
1560
1561        assert_eq!(config.get_transaction_limit("ETH"), Some(large_value));
1562        assert_eq!(config.get_daily_limit("ETH"), Some(large_value));
1563    }
1564
1565    #[test]
1566    fn test_policy_token_address_as_key() {
1567        let token_address = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
1568        let config =
1569            PolicyConfig::new().with_transaction_limit(token_address, U256::from(1_000_000u64));
1570
1571        assert!(config.get_transaction_limit(token_address).is_some());
1572        assert!(config
1573            .get_transaction_limit(&token_address.to_lowercase())
1574            .is_some());
1575    }
1576
1577    #[test]
1578    fn test_policy_multiple_transaction_limits() {
1579        let config = PolicyConfig::new()
1580            .with_transaction_limit("ETH", U256::from(5_000_000_000_000_000_000u64))
1581            .with_transaction_limit("USDC", U256::from(10_000_000_000u64));
1582
1583        assert_eq!(
1584            config.get_transaction_limit("ETH"),
1585            Some(U256::from(5_000_000_000_000_000_000u64))
1586        );
1587        assert_eq!(
1588            config.get_transaction_limit("USDC"),
1589            Some(U256::from(10_000_000_000u64))
1590        );
1591    }
1592}