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