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}