Skip to main content

txgate_chain/
tokens.rs

1//! Token registry for ERC-20 tokens.
2//!
3//! Provides metadata about known tokens for policy enrichment, including:
4//! - Token symbol and name
5//! - Decimal places
6//! - Risk classification
7//!
8//! # Example
9//!
10//! ```rust
11//! use txgate_chain::tokens::{TokenRegistry, TokenInfo, RiskLevel};
12//!
13//! // Create registry with built-in mainnet tokens
14//! let registry = TokenRegistry::with_builtins();
15//!
16//! // Look up USDC by address
17//! let usdc_addr = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48".parse().unwrap();
18//! if let Some(info) = registry.get(&usdc_addr) {
19//!     assert_eq!(info.symbol, "USDC");
20//!     assert_eq!(info.decimals, 6);
21//!     assert_eq!(info.risk_level, RiskLevel::Low);
22//! }
23//!
24//! // Unknown tokens get default high-risk classification
25//! let unknown = "0x0000000000000000000000000000000000000001".parse().unwrap();
26//! let info = registry.get_or_default(&unknown);
27//! assert_eq!(info.risk_level, RiskLevel::High);
28//! ```
29
30use alloy_primitives::Address;
31use serde::{Deserialize, Serialize};
32use std::collections::HashMap;
33
34/// Risk level classification for tokens.
35///
36/// Used by the policy engine to apply different rules based on token risk.
37/// Unknown tokens default to `High` risk as a security measure.
38#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
39#[serde(rename_all = "lowercase")]
40pub enum RiskLevel {
41    /// Low risk - Major stablecoins, wrapped native assets (USDC, WETH, etc.)
42    Low,
43    /// Medium risk - Established defi tokens (UNI, AAVE, etc.)
44    Medium,
45    /// High risk - Unknown tokens, newly deployed contracts
46    #[default]
47    High,
48}
49
50impl std::fmt::Display for RiskLevel {
51    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52        match self {
53            Self::Low => write!(f, "low"),
54            Self::Medium => write!(f, "medium"),
55            Self::High => write!(f, "high"),
56        }
57    }
58}
59
60/// Information about an ERC-20 token.
61#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
62pub struct TokenInfo {
63    /// Token symbol (e.g., "USDC", "WETH")
64    pub symbol: String,
65
66    /// Number of decimals (typically 6 for USDC, 18 for most tokens)
67    pub decimals: u8,
68
69    /// Risk classification
70    pub risk_level: RiskLevel,
71
72    /// Optional token name (e.g., "USD Coin")
73    #[serde(default, skip_serializing_if = "Option::is_none")]
74    pub name: Option<String>,
75}
76
77impl TokenInfo {
78    /// Create a new `TokenInfo`.
79    #[must_use]
80    pub fn new(symbol: impl Into<String>, decimals: u8, risk_level: RiskLevel) -> Self {
81        Self {
82            symbol: symbol.into(),
83            decimals,
84            risk_level,
85            name: None,
86        }
87    }
88
89    /// Create a `TokenInfo` with a name.
90    #[must_use]
91    pub fn with_name(mut self, name: impl Into<String>) -> Self {
92        self.name = Some(name.into());
93        self
94    }
95}
96
97/// Registry of known ERC-20 tokens.
98///
99/// Provides lookup of token metadata by contract address for policy enrichment.
100/// Unknown tokens are assigned `RiskLevel::High` by default.
101#[derive(Debug, Clone, Default)]
102pub struct TokenRegistry {
103    tokens: HashMap<Address, TokenInfo>,
104}
105
106impl TokenRegistry {
107    /// Create an empty token registry.
108    #[must_use]
109    pub fn new() -> Self {
110        Self {
111            tokens: HashMap::new(),
112        }
113    }
114
115    /// Create a registry with built-in tokens (mainnet addresses).
116    ///
117    /// Includes major stablecoins and wrapped assets:
118    /// - USDC (`0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48`)
119    /// - USDT (`0xdAC17F958D2ee523a2206206994597C13D831ec7`)
120    /// - DAI  (`0x6B175474E89094C44Da98b954EedfcE8F7e08E8A`)
121    /// - WETH (`0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2`)
122    /// - WBTC (`0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599`)
123    #[must_use]
124    pub fn with_builtins() -> Self {
125        let mut registry = Self::new();
126
127        // Major stablecoins (Low risk)
128        // USDC - USD Coin
129        if let Ok(addr) = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48".parse::<Address>() {
130            registry.register(
131                addr,
132                TokenInfo::new("USDC", 6, RiskLevel::Low).with_name("USD Coin"),
133            );
134        }
135
136        // USDT - Tether USD
137        if let Ok(addr) = "0xdAC17F958D2ee523a2206206994597C13D831ec7".parse::<Address>() {
138            registry.register(
139                addr,
140                TokenInfo::new("USDT", 6, RiskLevel::Low).with_name("Tether USD"),
141            );
142        }
143
144        // DAI - Dai Stablecoin
145        if let Ok(addr) = "0x6B175474E89094C44Da98b954EedfcE8F7e08E8A".parse::<Address>() {
146            registry.register(
147                addr,
148                TokenInfo::new("DAI", 18, RiskLevel::Low).with_name("Dai Stablecoin"),
149            );
150        }
151
152        // Wrapped assets (Low risk)
153        // WETH - Wrapped Ether
154        if let Ok(addr) = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2".parse::<Address>() {
155            registry.register(
156                addr,
157                TokenInfo::new("WETH", 18, RiskLevel::Low).with_name("Wrapped Ether"),
158            );
159        }
160
161        // WBTC - Wrapped BTC
162        if let Ok(addr) = "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599".parse::<Address>() {
163            registry.register(
164                addr,
165                TokenInfo::new("WBTC", 8, RiskLevel::Low).with_name("Wrapped BTC"),
166            );
167        }
168
169        registry
170    }
171
172    /// Register a token in the registry.
173    pub fn register(&mut self, address: Address, info: TokenInfo) {
174        self.tokens.insert(address, info);
175    }
176
177    /// Look up token info by address.
178    ///
179    /// # Returns
180    /// * `Some(&TokenInfo)` if the token is known
181    /// * `None` if the token is unknown
182    #[must_use]
183    pub fn get(&self, address: &Address) -> Option<&TokenInfo> {
184        self.tokens.get(address)
185    }
186
187    /// Look up token info, returning default for unknown tokens.
188    ///
189    /// Unknown tokens are assigned:
190    /// - Symbol: Contract address (shortened, e.g., "0xA0b8...eB48")
191    /// - Decimals: 18 (assumed)
192    /// - Risk: High
193    #[must_use]
194    pub fn get_or_default(&self, address: &Address) -> TokenInfo {
195        self.tokens.get(address).cloned().unwrap_or_else(|| {
196            let addr_str = format!("{address:?}");
197            // Create shortened address symbol: 0xXXXX...XXXX
198            let symbol = if addr_str.len() >= 42 {
199                format!(
200                    "{}...{}",
201                    addr_str.get(..6).unwrap_or("0x????"),
202                    addr_str.get(38..42).unwrap_or("????")
203                )
204            } else {
205                addr_str
206            };
207            TokenInfo::new(symbol, 18, RiskLevel::High)
208        })
209    }
210
211    /// Check if a token is known.
212    #[must_use]
213    pub fn contains(&self, address: &Address) -> bool {
214        self.tokens.contains_key(address)
215    }
216
217    /// Get all registered token addresses.
218    pub fn addresses(&self) -> impl Iterator<Item = &Address> {
219        self.tokens.keys()
220    }
221
222    /// Get the number of registered tokens.
223    #[must_use]
224    pub fn len(&self) -> usize {
225        self.tokens.len()
226    }
227
228    /// Check if the registry is empty.
229    #[must_use]
230    pub fn is_empty(&self) -> bool {
231        self.tokens.is_empty()
232    }
233
234    /// Load tokens from a JSON string.
235    ///
236    /// Expected format:
237    /// ```json
238    /// {
239    ///   "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48": {
240    ///     "symbol": "USDC",
241    ///     "decimals": 6,
242    ///     "risk_level": "low",
243    ///     "name": "USD Coin"
244    ///   }
245    /// }
246    /// ```
247    ///
248    /// # Errors
249    ///
250    /// Returns an error if the JSON is malformed or cannot be parsed.
251    pub fn load_json(&mut self, json: &str) -> Result<usize, serde_json::Error> {
252        let tokens: HashMap<String, TokenInfo> = serde_json::from_str(json)?;
253        let mut count = 0;
254        for (addr_str, info) in tokens {
255            if let Ok(address) = addr_str.parse::<Address>() {
256                self.register(address, info);
257                count += 1;
258            }
259        }
260        Ok(count)
261    }
262
263    /// Export registry to JSON string.
264    ///
265    /// # Errors
266    ///
267    /// Returns an error if serialization fails.
268    pub fn to_json(&self) -> Result<String, serde_json::Error> {
269        let map: HashMap<String, &TokenInfo> = self
270            .tokens
271            .iter()
272            .map(|(addr, info)| (format!("{addr:?}"), info))
273            .collect();
274        serde_json::to_string_pretty(&map)
275    }
276}
277
278#[cfg(test)]
279mod tests {
280    #![allow(
281        clippy::expect_used,
282        clippy::unwrap_used,
283        clippy::panic,
284        clippy::indexing_slicing,
285        clippy::similar_names,
286        clippy::redundant_clone,
287        clippy::manual_string_new,
288        clippy::needless_raw_string_hashes,
289        clippy::needless_collect,
290        clippy::unreadable_literal
291    )]
292
293    use super::*;
294
295    #[test]
296    fn test_empty_registry() {
297        let registry = TokenRegistry::new();
298        assert!(registry.is_empty());
299        assert_eq!(registry.len(), 0);
300    }
301
302    #[test]
303    fn test_with_builtins_has_expected_tokens() {
304        let registry = TokenRegistry::with_builtins();
305
306        // Should have 5 built-in tokens
307        assert_eq!(registry.len(), 5);
308        assert!(!registry.is_empty());
309
310        // Check USDC
311        let usdc_addr: Address = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
312            .parse()
313            .expect("valid address");
314        let usdc = registry.get(&usdc_addr).expect("USDC should be registered");
315        assert_eq!(usdc.symbol, "USDC");
316        assert_eq!(usdc.decimals, 6);
317        assert_eq!(usdc.risk_level, RiskLevel::Low);
318        assert_eq!(usdc.name, Some("USD Coin".to_string()));
319
320        // Check USDT
321        let usdt_addr: Address = "0xdAC17F958D2ee523a2206206994597C13D831ec7"
322            .parse()
323            .expect("valid address");
324        assert!(registry.contains(&usdt_addr));
325
326        // Check DAI
327        let dai_addr: Address = "0x6B175474E89094C44Da98b954EedfcE8F7e08E8A"
328            .parse()
329            .expect("valid address");
330        let dai = registry.get(&dai_addr).expect("DAI should be registered");
331        assert_eq!(dai.symbol, "DAI");
332        assert_eq!(dai.decimals, 18);
333
334        // Check WETH
335        let weth_addr: Address = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"
336            .parse()
337            .expect("valid address");
338        let weth = registry.get(&weth_addr).expect("WETH should be registered");
339        assert_eq!(weth.symbol, "WETH");
340        assert_eq!(weth.decimals, 18);
341
342        // Check WBTC
343        let wbtc_addr: Address = "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599"
344            .parse()
345            .expect("valid address");
346        let wbtc = registry.get(&wbtc_addr).expect("WBTC should be registered");
347        assert_eq!(wbtc.symbol, "WBTC");
348        assert_eq!(wbtc.decimals, 8);
349    }
350
351    #[test]
352    fn test_lookup_found() {
353        let registry = TokenRegistry::with_builtins();
354        let usdc_addr: Address = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
355            .parse()
356            .expect("valid address");
357
358        let info = registry.get(&usdc_addr);
359        assert!(info.is_some());
360        assert_eq!(info.expect("checked above").symbol, "USDC");
361    }
362
363    #[test]
364    fn test_lookup_not_found() {
365        let registry = TokenRegistry::with_builtins();
366        let unknown_addr: Address = "0x0000000000000000000000000000000000000001"
367            .parse()
368            .expect("valid address");
369
370        assert!(registry.get(&unknown_addr).is_none());
371        assert!(!registry.contains(&unknown_addr));
372    }
373
374    #[test]
375    fn test_get_or_default_for_unknown_tokens() {
376        let registry = TokenRegistry::with_builtins();
377        let unknown_addr: Address = "0x1234567890123456789012345678901234567890"
378            .parse()
379            .expect("valid address");
380
381        let info = registry.get_or_default(&unknown_addr);
382
383        // Unknown tokens get high risk and 18 decimals
384        assert_eq!(info.risk_level, RiskLevel::High);
385        assert_eq!(info.decimals, 18);
386        // Symbol should be shortened address
387        assert!(info.symbol.contains("0x1234"));
388        assert!(info.symbol.contains("7890"));
389    }
390
391    #[test]
392    fn test_get_or_default_for_known_tokens() {
393        let registry = TokenRegistry::with_builtins();
394        let usdc_addr: Address = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
395            .parse()
396            .expect("valid address");
397
398        let info = registry.get_or_default(&usdc_addr);
399
400        // Should return actual token info, not default
401        assert_eq!(info.symbol, "USDC");
402        assert_eq!(info.decimals, 6);
403        assert_eq!(info.risk_level, RiskLevel::Low);
404    }
405
406    #[test]
407    fn test_register() {
408        let mut registry = TokenRegistry::new();
409
410        let addr: Address = "0x1234567890123456789012345678901234567890"
411            .parse()
412            .expect("valid address");
413        let info = TokenInfo::new("TEST", 18, RiskLevel::Medium).with_name("Test Token");
414
415        registry.register(addr, info);
416
417        assert_eq!(registry.len(), 1);
418        assert!(registry.contains(&addr));
419
420        let retrieved = registry.get(&addr).expect("should be registered");
421        assert_eq!(retrieved.symbol, "TEST");
422        assert_eq!(retrieved.decimals, 18);
423        assert_eq!(retrieved.risk_level, RiskLevel::Medium);
424        assert_eq!(retrieved.name, Some("Test Token".to_string()));
425    }
426
427    #[test]
428    fn test_register_overwrites() {
429        let mut registry = TokenRegistry::new();
430
431        let addr: Address = "0x1234567890123456789012345678901234567890"
432            .parse()
433            .expect("valid address");
434
435        registry.register(addr, TokenInfo::new("OLD", 18, RiskLevel::High));
436        registry.register(addr, TokenInfo::new("NEW", 6, RiskLevel::Low));
437
438        assert_eq!(registry.len(), 1);
439        let info = registry.get(&addr).expect("should be registered");
440        assert_eq!(info.symbol, "NEW");
441        assert_eq!(info.decimals, 6);
442        assert_eq!(info.risk_level, RiskLevel::Low);
443    }
444
445    #[test]
446    fn test_load_json() {
447        let mut registry = TokenRegistry::new();
448
449        let json = r#"{
450            "0x1234567890123456789012345678901234567890": {
451                "symbol": "TEST",
452                "decimals": 18,
453                "risk_level": "medium",
454                "name": "Test Token"
455            },
456            "0xabcdef0123456789abcdef0123456789abcdef01": {
457                "symbol": "ABC",
458                "decimals": 6,
459                "risk_level": "low"
460            }
461        }"#;
462
463        let count = registry.load_json(json).expect("valid JSON");
464        assert_eq!(count, 2);
465        assert_eq!(registry.len(), 2);
466
467        let test_addr: Address = "0x1234567890123456789012345678901234567890"
468            .parse()
469            .expect("valid address");
470        let test_info = registry.get(&test_addr).expect("should be registered");
471        assert_eq!(test_info.symbol, "TEST");
472        assert_eq!(test_info.risk_level, RiskLevel::Medium);
473        assert_eq!(test_info.name, Some("Test Token".to_string()));
474
475        let abc_addr: Address = "0xabcdef0123456789abcdef0123456789abcdef01"
476            .parse()
477            .expect("valid address");
478        let abc_info = registry.get(&abc_addr).expect("should be registered");
479        assert_eq!(abc_info.symbol, "ABC");
480        assert_eq!(abc_info.decimals, 6);
481        assert_eq!(abc_info.risk_level, RiskLevel::Low);
482        assert_eq!(abc_info.name, None);
483    }
484
485    #[test]
486    fn test_load_json_invalid() {
487        let mut registry = TokenRegistry::new();
488
489        let result = registry.load_json("not valid json");
490        assert!(result.is_err());
491    }
492
493    #[test]
494    fn test_load_json_skips_invalid_addresses() {
495        let mut registry = TokenRegistry::new();
496
497        let json = r#"{
498            "not-an-address": {
499                "symbol": "SKIP",
500                "decimals": 18,
501                "risk_level": "high"
502            },
503            "0x1234567890123456789012345678901234567890": {
504                "symbol": "VALID",
505                "decimals": 18,
506                "risk_level": "low"
507            }
508        }"#;
509
510        let count = registry.load_json(json).expect("valid JSON");
511        // Only the valid address should be loaded
512        assert_eq!(count, 1);
513        assert_eq!(registry.len(), 1);
514    }
515
516    #[test]
517    fn test_risk_level_serialization() {
518        // Test Low
519        let json = serde_json::to_string(&RiskLevel::Low).expect("serialize");
520        assert_eq!(json, r#""low""#);
521        let deserialized: RiskLevel = serde_json::from_str(&json).expect("deserialize");
522        assert_eq!(deserialized, RiskLevel::Low);
523
524        // Test Medium
525        let json = serde_json::to_string(&RiskLevel::Medium).expect("serialize");
526        assert_eq!(json, r#""medium""#);
527        let deserialized: RiskLevel = serde_json::from_str(&json).expect("deserialize");
528        assert_eq!(deserialized, RiskLevel::Medium);
529
530        // Test High
531        let json = serde_json::to_string(&RiskLevel::High).expect("serialize");
532        assert_eq!(json, r#""high""#);
533        let deserialized: RiskLevel = serde_json::from_str(&json).expect("deserialize");
534        assert_eq!(deserialized, RiskLevel::High);
535    }
536
537    #[test]
538    fn test_risk_level_default() {
539        assert_eq!(RiskLevel::default(), RiskLevel::High);
540    }
541
542    #[test]
543    fn test_risk_level_display() {
544        assert_eq!(format!("{}", RiskLevel::Low), "low");
545        assert_eq!(format!("{}", RiskLevel::Medium), "medium");
546        assert_eq!(format!("{}", RiskLevel::High), "high");
547    }
548
549    #[test]
550    fn test_token_info_new() {
551        let info = TokenInfo::new("TEST", 18, RiskLevel::Medium);
552        assert_eq!(info.symbol, "TEST");
553        assert_eq!(info.decimals, 18);
554        assert_eq!(info.risk_level, RiskLevel::Medium);
555        assert_eq!(info.name, None);
556    }
557
558    #[test]
559    fn test_token_info_with_name() {
560        let info = TokenInfo::new("TEST", 18, RiskLevel::Medium).with_name("Test Token");
561        assert_eq!(info.symbol, "TEST");
562        assert_eq!(info.name, Some("Test Token".to_string()));
563    }
564
565    #[test]
566    fn test_token_info_serialization() {
567        let info = TokenInfo::new("TEST", 18, RiskLevel::Medium).with_name("Test Token");
568        let json = serde_json::to_string(&info).expect("serialize");
569
570        // Verify it contains expected fields
571        assert!(json.contains(r#""symbol":"TEST""#));
572        assert!(json.contains(r#""decimals":18"#));
573        assert!(json.contains(r#""risk_level":"medium""#));
574        assert!(json.contains(r#""name":"Test Token""#));
575
576        // Round-trip
577        let deserialized: TokenInfo = serde_json::from_str(&json).expect("deserialize");
578        assert_eq!(deserialized, info);
579    }
580
581    #[test]
582    fn test_token_info_serialization_without_name() {
583        let info = TokenInfo::new("TEST", 18, RiskLevel::Low);
584        let json = serde_json::to_string(&info).expect("serialize");
585
586        // Should not contain name field when None
587        assert!(!json.contains("name"));
588
589        // Round-trip
590        let deserialized: TokenInfo = serde_json::from_str(&json).expect("deserialize");
591        assert_eq!(deserialized, info);
592    }
593
594    #[test]
595    fn test_addresses_iterator() {
596        let registry = TokenRegistry::with_builtins();
597        let addresses: Vec<_> = registry.addresses().collect();
598        assert_eq!(addresses.len(), 5);
599    }
600
601    #[test]
602    fn test_to_json() {
603        let mut registry = TokenRegistry::new();
604        let addr: Address = "0x1234567890123456789012345678901234567890"
605            .parse()
606            .expect("valid address");
607        registry.register(addr, TokenInfo::new("TEST", 18, RiskLevel::Low));
608
609        let json = registry.to_json().expect("serialize");
610        assert!(json.contains("TEST"));
611        assert!(json.contains("0x1234567890123456789012345678901234567890"));
612    }
613
614    // ========================================================================
615    // Phase 2: Display/Debug and Serialization Edge Cases
616    // ========================================================================
617
618    #[test]
619    fn should_display_all_risk_level_variants_correctly() {
620        // Arrange & Act & Assert: Low
621        let low = RiskLevel::Low;
622        assert_eq!(format!("{low}"), "low");
623        assert_eq!(low.to_string(), "low");
624
625        // Arrange & Act & Assert: Medium
626        let medium = RiskLevel::Medium;
627        assert_eq!(format!("{medium}"), "medium");
628        assert_eq!(medium.to_string(), "medium");
629
630        // Arrange & Act & Assert: High
631        let high = RiskLevel::High;
632        assert_eq!(format!("{high}"), "high");
633        assert_eq!(high.to_string(), "high");
634    }
635
636    #[test]
637    fn should_serialize_and_deserialize_token_info_with_special_characters() {
638        // Arrange: TokenInfo with special characters in symbol and name
639        let info = TokenInfo::new("TEST-123_v2.0", 18, RiskLevel::Medium)
640            .with_name("Test Token (Beta) \"Official\" 'Version'");
641
642        // Act: Serialize to JSON
643        let json = serde_json::to_string(&info).expect("serialize");
644
645        // Assert: Round-trip preserves data
646        let deserialized: TokenInfo = serde_json::from_str(&json).expect("deserialize");
647        assert_eq!(deserialized.symbol, "TEST-123_v2.0");
648        assert_eq!(
649            deserialized.name,
650            Some("Test Token (Beta) \"Official\" 'Version'".to_string())
651        );
652        assert_eq!(deserialized.decimals, 18);
653        assert_eq!(deserialized.risk_level, RiskLevel::Medium);
654        assert_eq!(deserialized, info);
655    }
656
657    #[test]
658    fn should_serialize_and_deserialize_token_info_with_unicode() {
659        // Arrange: TokenInfo with Unicode characters
660        let info = TokenInfo::new("币", 6, RiskLevel::Low).with_name("中文代币 🚀");
661
662        // Act: Serialize and deserialize
663        let json = serde_json::to_string(&info).expect("serialize");
664        let deserialized: TokenInfo = serde_json::from_str(&json).expect("deserialize");
665
666        // Assert: Unicode preserved
667        assert_eq!(deserialized.symbol, "币");
668        assert_eq!(deserialized.name, Some("中文代币 🚀".to_string()));
669        assert_eq!(deserialized, info);
670    }
671
672    #[test]
673    fn should_serialize_token_info_with_zero_decimals() {
674        // Arrange: Token with 0 decimals (edge case, some NFTs do this)
675        let info = TokenInfo::new("NFT", 0, RiskLevel::High);
676
677        // Act: Serialize and deserialize
678        let json = serde_json::to_string(&info).expect("serialize");
679        let deserialized: TokenInfo = serde_json::from_str(&json).expect("deserialize");
680
681        // Assert: Zero decimals preserved
682        assert_eq!(deserialized.decimals, 0);
683        assert_eq!(deserialized, info);
684    }
685
686    #[test]
687    fn should_serialize_token_info_with_max_decimals() {
688        // Arrange: Token with 255 decimals (u8::MAX, theoretical edge case)
689        let info = TokenInfo::new("MAXDEC", 255, RiskLevel::High);
690
691        // Act: Serialize and deserialize
692        let json = serde_json::to_string(&info).expect("serialize");
693        let deserialized: TokenInfo = serde_json::from_str(&json).expect("deserialize");
694
695        // Assert: Max decimals preserved
696        assert_eq!(deserialized.decimals, 255);
697        assert_eq!(deserialized, info);
698    }
699
700    #[test]
701    fn should_serialize_token_info_with_empty_symbol() {
702        // Arrange: Token with empty symbol (edge case)
703        let info = TokenInfo::new("", 18, RiskLevel::High);
704
705        // Act: Serialize and deserialize
706        let json = serde_json::to_string(&info).expect("serialize");
707        let deserialized: TokenInfo = serde_json::from_str(&json).expect("deserialize");
708
709        // Assert: Empty symbol preserved
710        assert_eq!(deserialized.symbol, "");
711        assert_eq!(deserialized, info);
712    }
713
714    #[test]
715    fn should_serialize_token_info_with_very_long_name() {
716        // Arrange: Token with very long name (edge case)
717        let long_name = "A".repeat(1000);
718        let info = TokenInfo::new("LONG", 18, RiskLevel::Medium).with_name(&long_name);
719
720        // Act: Serialize and deserialize
721        let json = serde_json::to_string(&info).expect("serialize");
722        let deserialized: TokenInfo = serde_json::from_str(&json).expect("deserialize");
723
724        // Assert: Long name preserved
725        assert_eq!(deserialized.name, Some(long_name));
726        assert_eq!(deserialized, info);
727    }
728}