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