Skip to main content

x402_types/chain/
chain_id.rs

1//! CAIP-2 chain identifier types for blockchain-agnostic identification.
2//!
3//! This module implements the [CAIP-2](https://standards.chainagnostic.org/CAIPs/caip-2) standard
4//! for identifying blockchain networks in a chain-agnostic way. A CAIP-2 chain ID
5//! consists of two parts separated by a colon:
6//!
7//! - **Namespace**: The blockchain ecosystem (e.g., `eip155` for EVM, `solana` for Solana)
8//! - **Reference**: The chain-specific identifier (e.g., `8453` for Base, `137` for Polygon)
9//!
10//! # Examples
11//!
12//! ```
13//! use x402_types::chain::ChainId;
14//!
15//! // Create a chain ID for Base mainnet
16//! let base = ChainId::new("eip155", "8453");
17//! assert_eq!(base.to_string(), "eip155:8453");
18//!
19//! // Parse from string
20//! let polygon: ChainId = "eip155:137".parse().unwrap();
21//! assert_eq!(polygon.namespace, "eip155");
22//! assert_eq!(polygon.reference, "137");
23//! ```
24
25use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
26use std::collections::HashSet;
27use std::fmt;
28use std::str::FromStr;
29
30use crate::networks;
31
32/// A CAIP-2 compliant blockchain identifier.
33///
34/// Chain IDs uniquely identify blockchain networks across different ecosystems.
35/// The format is `namespace:reference` where:
36///
37/// - `namespace` identifies the blockchain family (e.g., `eip155`, `solana`)
38/// - `reference` identifies the specific chain within that family
39///
40/// # Serialization
41///
42/// Serializes to/from a colon-separated string: `"eip155:8453"`
43///
44/// # Example
45///
46/// ```
47/// use x402_types::chain::ChainId;
48///
49/// let chain = ChainId::new("eip155", "8453");
50/// let json = serde_json::to_string(&chain).unwrap();
51/// assert_eq!(json, "\"eip155:8453\"");
52/// ```
53#[derive(Debug, Clone, PartialEq, Eq, Hash)]
54pub struct ChainId {
55    /// The blockchain namespace (e.g., `eip155` for EVM chains, `solana` for Solana).
56    pub namespace: String,
57    /// The chain-specific reference (e.g., `8453` for Base, `137` for Polygon).
58    pub reference: String,
59}
60
61impl ChainId {
62    /// Creates a new chain ID from namespace and reference components.
63    ///
64    /// # Example
65    ///
66    /// ```
67    /// use x402_types::chain::ChainId;
68    ///
69    /// let base = ChainId::new("eip155", "8453");
70    /// assert_eq!(base.namespace, "eip155");
71    /// assert_eq!(base.reference, "8453");
72    /// ```
73    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    /// Returns the namespace component of the chain ID.
81    pub fn namespace(&self) -> &str {
82        &self.namespace
83    }
84
85    /// Returns the reference component of the chain ID.
86    pub fn reference(&self) -> &str {
87        &self.reference
88    }
89
90    /// Creates a chain ID from a well-known network name.
91    ///
92    /// This method looks up the network name in the registry of known networks
93    /// (see [`crate::networks`]) and returns the corresponding chain ID.
94    ///
95    /// # Example
96    ///
97    /// ```
98    /// use x402_types::chain::ChainId;
99    ///
100    /// let base = ChainId::from_network_name("base").unwrap();
101    /// assert_eq!(base.to_string(), "eip155:8453");
102    ///
103    /// assert!(ChainId::from_network_name("unknown").is_none());
104    /// ```
105    pub fn from_network_name(network_name: &str) -> Option<Self> {
106        networks::chain_id_by_network_name(network_name).cloned()
107    }
108
109    /// Returns the well-known network name for this chain ID, if any.
110    ///
111    /// This is the reverse of [`ChainId::from_network_name`].
112    ///
113    /// # Example
114    ///
115    /// ```
116    /// use x402_types::chain::ChainId;
117    ///
118    /// let base = ChainId::new("eip155", "8453");
119    /// assert_eq!(base.as_network_name(), Some("base"));
120    ///
121    /// let unknown = ChainId::new("eip155", "999999");
122    /// assert!(unknown.as_network_name().is_none());
123    /// ```
124    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/// Error returned when parsing an invalid chain ID string.
142///
143/// A valid chain ID must be in the format `namespace:reference` where both
144/// components are non-empty strings.
145#[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/// A pattern for matching chain IDs.
184///
185/// Chain ID patterns allow flexible matching of blockchain networks:
186///
187/// - **Wildcard**: Matches any chain within a namespace (e.g., `eip155:*` matches all EVM chains)
188/// - **Exact**: Matches a specific chain (e.g., `eip155:8453` matches only Base)
189/// - **Set**: Matches any chain from a set (e.g., `eip155:{1,8453,137}` matches Ethereum, Base, or Polygon)
190///
191/// # Serialization
192///
193/// Patterns serialize to human-readable strings:
194/// - Wildcard: `"eip155:*"`
195/// - Exact: `"eip155:8453"`
196/// - Set: `"eip155:{1,8453,137}"`
197///
198/// # Example
199///
200/// ```
201/// use x402_types::chain::{ChainId, ChainIdPattern};
202///
203/// // Match all EVM chains
204/// let all_evm = ChainIdPattern::wildcard("eip155");
205/// assert!(all_evm.matches(&ChainId::new("eip155", "8453")));
206/// assert!(all_evm.matches(&ChainId::new("eip155", "137")));
207/// assert!(!all_evm.matches(&ChainId::new("solana", "mainnet")));
208///
209/// // Match specific chain
210/// let base_only = ChainIdPattern::exact("eip155", "8453");
211/// assert!(base_only.matches(&ChainId::new("eip155", "8453")));
212/// assert!(!base_only.matches(&ChainId::new("eip155", "137")));
213/// ```
214#[derive(Debug, Clone)]
215pub enum ChainIdPattern {
216    /// Matches any chain within the specified namespace.
217    Wildcard {
218        /// The namespace to match (e.g., `eip155`, `solana`).
219        namespace: String,
220    },
221    /// Matches exactly one specific chain.
222    Exact {
223        /// The namespace of the chain.
224        namespace: String,
225        /// The reference of the chain.
226        reference: String,
227    },
228    /// Matches any chain from a set of references within a namespace.
229    Set {
230        /// The namespace of the chains.
231        namespace: String,
232        /// The set of chain references to match.
233        references: HashSet<String>,
234    },
235}
236
237impl ChainIdPattern {
238    /// Creates a wildcard pattern that matches any chain in the given namespace.
239    ///
240    /// # Example
241    ///
242    /// ```
243    /// use x402_types::chain::{ChainId, ChainIdPattern};
244    ///
245    /// let pattern = ChainIdPattern::wildcard("eip155");
246    /// assert!(pattern.matches(&ChainId::new("eip155", "1")));
247    /// assert!(pattern.matches(&ChainId::new("eip155", "8453")));
248    /// ```
249    pub fn wildcard<S: Into<String>>(namespace: S) -> Self {
250        Self::Wildcard {
251            namespace: namespace.into(),
252        }
253    }
254
255    /// Creates an exact pattern that matches only the specified chain.
256    ///
257    /// # Example
258    ///
259    /// ```
260    /// use x402_types::chain::{ChainId, ChainIdPattern};
261    ///
262    /// let pattern = ChainIdPattern::exact("eip155", "8453");
263    /// assert!(pattern.matches(&ChainId::new("eip155", "8453")));
264    /// assert!(!pattern.matches(&ChainId::new("eip155", "137")));
265    /// ```
266    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    /// Creates a set pattern that matches any chain from the given set of references.
274    ///
275    /// # Example
276    ///
277    /// ```
278    /// use x402_types::chain::{ChainId, ChainIdPattern};
279    /// use std::collections::HashSet;
280    ///
281    /// let refs: HashSet<String> = ["1", "8453", "137"].iter().map(|s| s.to_string()).collect();
282    /// let pattern = ChainIdPattern::set("eip155", refs);
283    /// assert!(pattern.matches(&ChainId::new("eip155", "8453")));
284    /// assert!(!pattern.matches(&ChainId::new("eip155", "42")));
285    /// ```
286    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    /// Check if a `ChainId` matches this pattern.
294    ///
295    /// - `Wildcard` matches any chain with the same namespace
296    /// - `Exact` matches only if both namespace and reference are equal
297    /// - `Set` matches if the namespace is equal and the reference is in the set
298    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    /// Returns the namespace of this pattern.
313    #[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        // Wildcard: eip155:*
353        if rest == "*" {
354            return Ok(ChainIdPattern::wildcard(namespace));
355        }
356
357        // Set: eip155:{1,2,3}
358        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        // Exact: eip155:1
377        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 original = ChainId::eip155(8453);
460        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}