1use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
26use std::collections::HashSet;
27use std::fmt;
28use std::str::FromStr;
29
30use crate::networks;
31
32#[derive(Debug, Clone, PartialEq, Eq, Hash)]
54pub struct ChainId {
55 pub namespace: String,
57 pub reference: String,
59}
60
61impl ChainId {
62 pub fn new<N: Into<String>, R: Into<String>>(namespace: N, reference: R) -> Self {
74 Self {
75 namespace: namespace.into(),
76 reference: reference.into(),
77 }
78 }
79
80 pub fn namespace(&self) -> &str {
82 &self.namespace
83 }
84
85 pub fn reference(&self) -> &str {
87 &self.reference
88 }
89
90 pub fn from_network_name(network_name: &str) -> Option<Self> {
106 networks::chain_id_by_network_name(network_name).cloned()
107 }
108
109 pub fn as_network_name(&self) -> Option<&'static str> {
125 networks::network_name_by_chain_id(self)
126 }
127}
128
129impl fmt::Display for ChainId {
130 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
131 write!(f, "{}:{}", self.namespace, self.reference)
132 }
133}
134
135impl From<ChainId> for String {
136 fn from(value: ChainId) -> Self {
137 value.to_string()
138 }
139}
140
141#[derive(Debug, thiserror::Error)]
146#[error("Invalid chain id format {0}")]
147pub struct ChainIdFormatError(String);
148
149impl FromStr for ChainId {
150 type Err = ChainIdFormatError;
151
152 fn from_str(s: &str) -> Result<Self, Self::Err> {
153 let parts: Vec<&str> = s.splitn(2, ':').collect();
154 if parts.len() != 2 {
155 return Err(ChainIdFormatError(s.into()));
156 }
157 Ok(ChainId {
158 namespace: parts[0].into(),
159 reference: parts[1].into(),
160 })
161 }
162}
163
164impl Serialize for ChainId {
165 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
166 where
167 S: Serializer,
168 {
169 serializer.serialize_str(&self.to_string())
170 }
171}
172
173impl<'de> Deserialize<'de> for ChainId {
174 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
175 where
176 D: Deserializer<'de>,
177 {
178 let s = String::deserialize(deserializer)?;
179 ChainId::from_str(&s).map_err(de::Error::custom)
180 }
181}
182
183#[derive(Debug, Clone)]
215pub enum ChainIdPattern {
216 Wildcard {
218 namespace: String,
220 },
221 Exact {
223 namespace: String,
225 reference: String,
227 },
228 Set {
230 namespace: String,
232 references: HashSet<String>,
234 },
235}
236
237impl ChainIdPattern {
238 pub fn wildcard<S: Into<String>>(namespace: S) -> Self {
250 Self::Wildcard {
251 namespace: namespace.into(),
252 }
253 }
254
255 pub fn exact<N: Into<String>, R: Into<String>>(namespace: N, reference: R) -> Self {
267 Self::Exact {
268 namespace: namespace.into(),
269 reference: reference.into(),
270 }
271 }
272
273 pub fn set<N: Into<String>>(namespace: N, references: HashSet<String>) -> Self {
287 Self::Set {
288 namespace: namespace.into(),
289 references,
290 }
291 }
292
293 pub fn matches(&self, chain_id: &ChainId) -> bool {
299 match self {
300 ChainIdPattern::Wildcard { namespace } => chain_id.namespace == *namespace,
301 ChainIdPattern::Exact {
302 namespace,
303 reference,
304 } => chain_id.namespace == *namespace && chain_id.reference == *reference,
305 ChainIdPattern::Set {
306 namespace,
307 references,
308 } => chain_id.namespace == *namespace && references.contains(&chain_id.reference),
309 }
310 }
311
312 #[allow(dead_code)]
314 pub fn namespace(&self) -> &str {
315 match self {
316 ChainIdPattern::Wildcard { namespace } => namespace,
317 ChainIdPattern::Exact { namespace, .. } => namespace,
318 ChainIdPattern::Set { namespace, .. } => namespace,
319 }
320 }
321}
322
323impl fmt::Display for ChainIdPattern {
324 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
325 match self {
326 ChainIdPattern::Wildcard { namespace } => write!(f, "{}:*", namespace),
327 ChainIdPattern::Exact {
328 namespace,
329 reference,
330 } => write!(f, "{}:{}", namespace, reference),
331 ChainIdPattern::Set {
332 namespace,
333 references,
334 } => {
335 let refs: Vec<&str> = references.iter().map(|s| s.as_ref()).collect();
336 write!(f, "{}:{{{}}}", namespace, refs.join(","))
337 }
338 }
339 }
340}
341
342impl FromStr for ChainIdPattern {
343 type Err = ChainIdFormatError;
344
345 fn from_str(s: &str) -> Result<Self, Self::Err> {
346 let (namespace, rest) = s.split_once(':').ok_or(ChainIdFormatError(s.into()))?;
347
348 if namespace.is_empty() {
349 return Err(ChainIdFormatError(s.into()));
350 }
351
352 if rest == "*" {
354 return Ok(ChainIdPattern::wildcard(namespace));
355 }
356
357 if let Some(inner) = rest.strip_prefix('{').and_then(|r| r.strip_suffix('}')) {
359 let mut references = HashSet::new();
360
361 for item in inner.split(',') {
362 let item = item.trim();
363 if item.is_empty() {
364 return Err(ChainIdFormatError(s.into()));
365 }
366 references.insert(item.into());
367 }
368
369 if references.is_empty() {
370 return Err(ChainIdFormatError(s.into()));
371 }
372
373 return Ok(ChainIdPattern::set(namespace, references));
374 }
375
376 if rest.is_empty() {
378 return Err(ChainIdFormatError(s.into()));
379 }
380
381 Ok(ChainIdPattern::exact(namespace, rest))
382 }
383}
384
385impl Serialize for ChainIdPattern {
386 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
387 where
388 S: Serializer,
389 {
390 serializer.serialize_str(&self.to_string())
391 }
392}
393
394impl<'de> Deserialize<'de> for ChainIdPattern {
395 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
396 where
397 D: Deserializer<'de>,
398 {
399 let s = String::deserialize(deserializer)?;
400 ChainIdPattern::from_str(&s).map_err(de::Error::custom)
401 }
402}
403
404impl From<ChainId> for ChainIdPattern {
405 fn from(chain_id: ChainId) -> Self {
406 ChainIdPattern::exact(chain_id.namespace, chain_id.reference)
407 }
408}
409
410impl From<ChainIdPattern> for Vec<ChainIdPattern> {
411 fn from(value: ChainIdPattern) -> Self {
412 vec![value]
413 }
414}
415
416impl From<ChainId> for Vec<ChainId> {
417 fn from(value: ChainId) -> Self {
418 vec![value]
419 }
420}
421
422#[cfg(test)]
423mod tests {
424 use super::*;
425 use crate::networks::{chain_id_by_network_name, network_name_by_chain_id};
426
427 #[test]
428 fn test_chain_id_serialize_eip155() {
429 let chain_id = ChainId::new("eip155", "1");
430 let serialized = serde_json::to_string(&chain_id).unwrap();
431 assert_eq!(serialized, "\"eip155:1\"");
432 }
433
434 #[test]
435 fn test_chain_id_serialize_solana() {
436 let chain_id = ChainId::new("solana", "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp");
437 let serialized = serde_json::to_string(&chain_id).unwrap();
438 assert_eq!(serialized, "\"solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp\"");
439 }
440
441 #[test]
442 fn test_chain_id_deserialize_eip155() {
443 let chain_id: ChainId = serde_json::from_str("\"eip155:1\"").unwrap();
444 assert_eq!(chain_id.namespace, "eip155");
445 assert_eq!(chain_id.reference, "1");
446 }
447
448 #[test]
449 fn test_chain_id_deserialize_solana() {
450 let chain_id: ChainId =
451 serde_json::from_str("\"solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp\"").unwrap();
452 assert_eq!(chain_id.namespace, "solana");
453 assert_eq!(chain_id.reference, "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp");
454 }
455
456 #[test]
457 fn test_chain_id_roundtrip_eip155() {
458 let original = ChainId::new("eip155", "8453");
459 let serialized = serde_json::to_string(&original).unwrap();
461 let deserialized: ChainId = serde_json::from_str(&serialized).unwrap();
462 assert_eq!(original, deserialized);
463 }
464
465 #[test]
466 fn test_chain_id_roundtrip_solana() {
467 let original = ChainId::new("solana", "devnet");
468 let serialized = serde_json::to_string(&original).unwrap();
469 let deserialized: ChainId = serde_json::from_str(&serialized).unwrap();
470 assert_eq!(original, deserialized);
471 }
472
473 #[test]
474 fn test_chain_id_deserialize_invalid_format() {
475 let result: Result<ChainId, _> = serde_json::from_str("\"invalid\"");
476 assert!(result.is_err());
477 }
478
479 #[test]
480 fn test_chain_id_deserialize_unknown_namespace() {
481 let result: Result<ChainId, _> = serde_json::from_str("\"unknown:1\"");
482 assert!(result.is_ok());
483 }
484
485 #[test]
486 fn test_pattern_wildcard_matches() {
487 let pattern = ChainIdPattern::wildcard("eip155");
488 assert!(pattern.matches(&ChainId::new("eip155", "1")));
489 assert!(pattern.matches(&ChainId::new("eip155", "8453")));
490 assert!(pattern.matches(&ChainId::new("eip155", "137")));
491 assert!(!pattern.matches(&ChainId::new("solana", "mainnet")));
492 }
493
494 #[test]
495 fn test_pattern_exact_matches() {
496 let pattern = ChainIdPattern::exact("eip155", "1");
497 assert!(pattern.matches(&ChainId::new("eip155", "1")));
498 assert!(!pattern.matches(&ChainId::new("eip155", "8453")));
499 assert!(!pattern.matches(&ChainId::new("solana", "1")));
500 }
501
502 #[test]
503 fn test_pattern_set_matches() {
504 let references: HashSet<String> = vec!["1", "8453", "137"]
505 .into_iter()
506 .map(String::from)
507 .collect();
508 let pattern = ChainIdPattern::set("eip155", references);
509 assert!(pattern.matches(&ChainId::new("eip155", "1")));
510 assert!(pattern.matches(&ChainId::new("eip155", "8453")));
511 assert!(pattern.matches(&ChainId::new("eip155", "137")));
512 assert!(!pattern.matches(&ChainId::new("eip155", "42")));
513 assert!(!pattern.matches(&ChainId::new("solana", "1")));
514 }
515
516 #[test]
517 fn test_pattern_namespace() {
518 let wildcard = ChainIdPattern::wildcard("eip155");
519 assert_eq!(wildcard.namespace(), "eip155");
520
521 let exact = ChainIdPattern::exact("solana", "mainnet");
522 assert_eq!(exact.namespace(), "solana");
523
524 let references: HashSet<String> = vec!["1"].into_iter().map(String::from).collect();
525 let set = ChainIdPattern::set("eip155", references);
526 assert_eq!(set.namespace(), "eip155");
527 }
528
529 #[test]
530 fn test_chain_id_from_network_name() {
531 let base = chain_id_by_network_name("base").unwrap();
532 assert_eq!(base.namespace, "eip155");
533 assert_eq!(base.reference, "8453");
534
535 let base_sepolia = chain_id_by_network_name("base-sepolia").unwrap();
536 assert_eq!(base_sepolia.namespace, "eip155");
537 assert_eq!(base_sepolia.reference, "84532");
538
539 let polygon = chain_id_by_network_name("polygon").unwrap();
540 assert_eq!(polygon.namespace, "eip155");
541 assert_eq!(polygon.reference, "137");
542
543 let celo = chain_id_by_network_name("celo").unwrap();
544 assert_eq!(celo.namespace, "eip155");
545 assert_eq!(celo.reference, "42220");
546
547 let solana = chain_id_by_network_name("solana").unwrap();
548 assert_eq!(solana.namespace, "solana");
549 assert_eq!(solana.reference, "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp");
550
551 assert!(chain_id_by_network_name("unknown").is_none());
552 }
553
554 #[test]
555 fn test_network_name_by_chain_id() {
556 let chain_id = ChainId::new("eip155", "8453");
557 let network_name = network_name_by_chain_id(&chain_id).unwrap();
558 assert_eq!(network_name, "base");
559
560 let celo_chain_id = ChainId::new("eip155", "42220");
561 let network_name = network_name_by_chain_id(&celo_chain_id).unwrap();
562 assert_eq!(network_name, "celo");
563
564 let celo_sepolia_chain_id = ChainId::new("eip155", "11142220");
565 let network_name = network_name_by_chain_id(&celo_sepolia_chain_id).unwrap();
566 assert_eq!(network_name, "celo-sepolia");
567
568 let solana_chain_id = ChainId::new("solana", "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp");
569 let network_name = network_name_by_chain_id(&solana_chain_id).unwrap();
570 assert_eq!(network_name, "solana");
571
572 let unknown_chain_id = ChainId::new("eip155", "999999");
573 assert!(network_name_by_chain_id(&unknown_chain_id).is_none());
574 }
575
576 #[test]
577 fn test_chain_id_as_network_name() {
578 let chain_id = ChainId::new("eip155", "8453");
579 assert_eq!(chain_id.as_network_name(), Some("base"));
580
581 let celo_chain_id = ChainId::new("eip155", "42220");
582 assert_eq!(celo_chain_id.as_network_name(), Some("celo"));
583
584 let solana_chain_id = ChainId::new("solana", "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp");
585 assert_eq!(solana_chain_id.as_network_name(), Some("solana"));
586
587 let unknown_chain_id = ChainId::new("eip155", "999999");
588 assert!(unknown_chain_id.as_network_name().is_none());
589 }
590}