1use alloy_primitives::Address;
31use serde::{Deserialize, Serialize};
32use std::collections::HashMap;
33
34#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
39#[serde(rename_all = "lowercase")]
40pub enum RiskLevel {
41 Low,
43 Medium,
45 #[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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
62pub struct TokenInfo {
63 pub symbol: String,
65
66 pub decimals: u8,
68
69 pub risk_level: RiskLevel,
71
72 #[serde(default, skip_serializing_if = "Option::is_none")]
74 pub name: Option<String>,
75}
76
77impl TokenInfo {
78 #[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 #[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#[derive(Debug, Clone, Default)]
102pub struct TokenRegistry {
103 tokens: HashMap<Address, TokenInfo>,
104}
105
106impl TokenRegistry {
107 #[must_use]
109 pub fn new() -> Self {
110 Self {
111 tokens: HashMap::new(),
112 }
113 }
114
115 #[must_use]
124 pub fn with_builtins() -> Self {
125 let mut registry = Self::new();
126
127 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 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 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 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 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 pub fn register(&mut self, address: Address, info: TokenInfo) {
174 self.tokens.insert(address, info);
175 }
176
177 #[must_use]
183 pub fn get(&self, address: &Address) -> Option<&TokenInfo> {
184 self.tokens.get(address)
185 }
186
187 #[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 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 #[must_use]
213 pub fn contains(&self, address: &Address) -> bool {
214 self.tokens.contains_key(address)
215 }
216
217 pub fn addresses(&self) -> impl Iterator<Item = &Address> {
219 self.tokens.keys()
220 }
221
222 #[must_use]
224 pub fn len(&self) -> usize {
225 self.tokens.len()
226 }
227
228 #[must_use]
230 pub fn is_empty(&self) -> bool {
231 self.tokens.is_empty()
232 }
233
234 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 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 assert_eq!(registry.len(), 5);
308 assert!(!registry.is_empty());
309
310 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 let usdt_addr: Address = "0xdAC17F958D2ee523a2206206994597C13D831ec7"
322 .parse()
323 .expect("valid address");
324 assert!(registry.contains(&usdt_addr));
325
326 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 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 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 assert_eq!(info.risk_level, RiskLevel::High);
385 assert_eq!(info.decimals, 18);
386 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 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 assert_eq!(count, 1);
513 assert_eq!(registry.len(), 1);
514 }
515
516 #[test]
517 fn test_risk_level_serialization() {
518 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 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 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 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 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 assert!(!json.contains("name"));
588
589 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 #[test]
619 fn should_display_all_risk_level_variants_correctly() {
620 let low = RiskLevel::Low;
622 assert_eq!(format!("{low}"), "low");
623 assert_eq!(low.to_string(), "low");
624
625 let medium = RiskLevel::Medium;
627 assert_eq!(format!("{medium}"), "medium");
628 assert_eq!(medium.to_string(), "medium");
629
630 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 let info = TokenInfo::new("TEST-123_v2.0", 18, RiskLevel::Medium)
640 .with_name("Test Token (Beta) \"Official\" 'Version'");
641
642 let json = serde_json::to_string(&info).expect("serialize");
644
645 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 let info = TokenInfo::new("币", 6, RiskLevel::Low).with_name("中文代币 🚀");
661
662 let json = serde_json::to_string(&info).expect("serialize");
664 let deserialized: TokenInfo = serde_json::from_str(&json).expect("deserialize");
665
666 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 let info = TokenInfo::new("NFT", 0, RiskLevel::High);
676
677 let json = serde_json::to_string(&info).expect("serialize");
679 let deserialized: TokenInfo = serde_json::from_str(&json).expect("deserialize");
680
681 assert_eq!(deserialized.decimals, 0);
683 assert_eq!(deserialized, info);
684 }
685
686 #[test]
687 fn should_serialize_token_info_with_max_decimals() {
688 let info = TokenInfo::new("MAXDEC", 255, RiskLevel::High);
690
691 let json = serde_json::to_string(&info).expect("serialize");
693 let deserialized: TokenInfo = serde_json::from_str(&json).expect("deserialize");
694
695 assert_eq!(deserialized.decimals, 255);
697 assert_eq!(deserialized, info);
698 }
699
700 #[test]
701 fn should_serialize_token_info_with_empty_symbol() {
702 let info = TokenInfo::new("", 18, RiskLevel::High);
704
705 let json = serde_json::to_string(&info).expect("serialize");
707 let deserialized: TokenInfo = serde_json::from_str(&json).expect("deserialize");
708
709 assert_eq!(deserialized.symbol, "");
711 assert_eq!(deserialized, info);
712 }
713
714 #[test]
715 fn should_serialize_token_info_with_very_long_name() {
716 let long_name = "A".repeat(1000);
718 let info = TokenInfo::new("LONG", 18, RiskLevel::Medium).with_name(&long_name);
719
720 let json = serde_json::to_string(&info).expect("serialize");
722 let deserialized: TokenInfo = serde_json::from_str(&json).expect("deserialize");
723
724 assert_eq!(deserialized.name, Some(long_name));
726 assert_eq!(deserialized, info);
727 }
728}