tap_caip/
chain_id.rs

1use crate::error::Error;
2use once_cell::sync::Lazy;
3use regex::Regex;
4use serde::{Deserialize, Deserializer, Serialize, Serializer};
5use std::str::FromStr;
6
7/// Regular expression pattern for CAIP-2 chain ID validation
8/// The pattern is modified to support longer references for Bitcoin block hashes (64 chars)
9static CHAIN_ID_REGEX: Lazy<Regex> = Lazy::new(|| {
10    Regex::new(r"^[-a-z0-9]{3,8}:[-a-zA-Z0-9]{1,64}$").expect("Failed to compile CHAIN_ID_REGEX")
11});
12
13/// CAIP-2 Chain ID implementation
14///
15/// A Chain ID is a string that identifies a blockchain and follows the format:
16/// `<namespace>:<reference>`
17///
18/// - `namespace`: Identifies the blockchain standard (e.g., eip155 for Ethereum)
19/// - `reference`: Chain-specific identifier (e.g., 1 for Ethereum mainnet)
20///
21/// Example: "eip155:1" for Ethereum mainnet
22#[derive(Debug, Clone, PartialEq, Eq, Hash)]
23pub struct ChainId {
24    namespace: String,
25    reference: String,
26}
27
28impl ChainId {
29    /// Create a new ChainId from namespace and reference
30    ///
31    /// # Arguments
32    ///
33    /// * `namespace` - The blockchain namespace (e.g., "eip155")
34    /// * `reference` - The chain-specific reference (e.g., "1")
35    ///
36    /// # Returns
37    ///
38    /// * `Result<ChainId, Error>` - A ChainId or an error if validation fails
39    pub fn new(namespace: &str, reference: &str) -> Result<Self, Error> {
40        // Validate namespace and reference individually
41        Self::validate_namespace(namespace)?;
42        Self::validate_reference(reference)?;
43
44        // Construct and validate the full chain ID
45        let chain_id = format!("{}:{}", namespace, reference);
46        if !CHAIN_ID_REGEX.is_match(&chain_id) {
47            return Err(Error::InvalidChainId(chain_id));
48        }
49
50        Ok(Self {
51            namespace: namespace.to_string(),
52            reference: reference.to_string(),
53        })
54    }
55
56    /// Get the namespace component
57    pub fn namespace(&self) -> &str {
58        &self.namespace
59    }
60
61    /// Get the reference component
62    pub fn reference(&self) -> &str {
63        &self.reference
64    }
65
66    /// Validate the namespace component
67    fn validate_namespace(namespace: &str) -> Result<(), Error> {
68        // Namespace must be 3-8 characters, lowercase alphanumeric with possible hyphens
69        if !Regex::new(r"^[-a-z0-9]{3,8}$")
70            .expect("Failed to compile namespace regex")
71            .is_match(namespace)
72        {
73            return Err(Error::InvalidNamespace(namespace.to_string()));
74        }
75
76        // Additional validation could be added here for known namespaces
77        // For now, we allow any namespace that matches the format
78
79        Ok(())
80    }
81
82    /// Validate the reference component
83    fn validate_reference(reference: &str) -> Result<(), Error> {
84        // Reference must be 1-64 characters, alphanumeric with possible hyphens
85        if !Regex::new(r"^[-a-zA-Z0-9]{1,64}$")
86            .expect("Failed to compile reference regex")
87            .is_match(reference)
88        {
89            return Err(Error::InvalidReference(reference.to_string()));
90        }
91
92        // Additional validation could be added here for specific references
93        // For now, we allow any reference that matches the format
94
95        Ok(())
96    }
97}
98
99impl FromStr for ChainId {
100    type Err = Error;
101
102    /// Parse a string into a ChainId
103    ///
104    /// # Arguments
105    ///
106    /// * `s` - A string in the format "namespace:reference"
107    ///
108    /// # Returns
109    ///
110    /// * `Result<ChainId, Error>` - A ChainId or an error if parsing fails
111    fn from_str(s: &str) -> Result<Self, Self::Err> {
112        // Check the overall format first
113        if !CHAIN_ID_REGEX.is_match(s) {
114            return Err(Error::InvalidChainId(s.to_string()));
115        }
116
117        // Split the chain ID into namespace and reference
118        let parts: Vec<&str> = s.split(':').collect();
119        if parts.len() != 2 {
120            return Err(Error::InvalidChainId(s.to_string()));
121        }
122
123        ChainId::new(parts[0], parts[1])
124    }
125}
126
127// Removed the conflicting ToString implementation
128// Let the default implementation from Display be used
129
130impl std::fmt::Display for ChainId {
131    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
132        write!(f, "{}:{}", self.namespace, self.reference)
133    }
134}
135
136impl Serialize for ChainId {
137    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
138    where
139        S: Serializer,
140    {
141        serializer.serialize_str(&self.to_string())
142    }
143}
144
145impl<'de> Deserialize<'de> for ChainId {
146    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
147    where
148        D: Deserializer<'de>,
149    {
150        let s = String::deserialize(deserializer)?;
151        ChainId::from_str(&s).map_err(serde::de::Error::custom)
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158
159    #[test]
160    fn test_valid_chain_ids() {
161        // Test Ethereum mainnet
162        let eth_mainnet = ChainId::from_str("eip155:1").unwrap();
163        assert_eq!(eth_mainnet.namespace(), "eip155");
164        assert_eq!(eth_mainnet.reference(), "1");
165        assert_eq!(eth_mainnet.to_string(), "eip155:1");
166
167        // Test Bitcoin mainnet
168        let btc_mainnet = ChainId::from_str(
169            "bip122:000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f",
170        )
171        .unwrap();
172        assert_eq!(btc_mainnet.namespace(), "bip122");
173        assert_eq!(
174            btc_mainnet.reference(),
175            "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"
176        );
177
178        // Test direct creation
179        let poly_mainnet = ChainId::new("polkadot", "91b171bb158e2d3848fa23a9f1c25182").unwrap();
180        assert_eq!(poly_mainnet.namespace(), "polkadot");
181        assert_eq!(poly_mainnet.reference(), "91b171bb158e2d3848fa23a9f1c25182");
182    }
183
184    #[test]
185    fn test_invalid_chain_ids() {
186        // Invalid: empty string
187        assert!(ChainId::from_str("").is_err());
188
189        // Invalid: missing separator
190        assert!(ChainId::from_str("eip1551").is_err());
191
192        // Invalid: empty namespace
193        assert!(ChainId::from_str(":1").is_err());
194
195        // Invalid: empty reference
196        assert!(ChainId::from_str("eip155:").is_err());
197
198        // Invalid: namespace too short
199        assert!(ChainId::from_str("ei:1").is_err());
200
201        // Invalid: namespace too long
202        assert!(ChainId::from_str("eip155toolong:1").is_err());
203
204        // Invalid: invalid namespace characters
205        assert!(ChainId::from_str("EIP155:1").is_err()); // uppercase not allowed
206        assert!(ChainId::from_str("eip_155:1").is_err()); // underscore not allowed
207
208        // Invalid: reference too long
209        let long_reference = "a".repeat(65);
210        assert!(ChainId::from_str(&format!("eip155:{}", long_reference)).is_err());
211    }
212
213    #[test]
214    fn test_serialization() {
215        let chain_id = ChainId::from_str("eip155:1").unwrap();
216        let serialized = serde_json::to_string(&chain_id).unwrap();
217        assert_eq!(serialized, r#""eip155:1""#);
218
219        let deserialized: ChainId = serde_json::from_str(&serialized).unwrap();
220        assert_eq!(deserialized, chain_id);
221    }
222
223    #[test]
224    fn test_display_formatting() {
225        let chain_id = ChainId::from_str("eip155:1").unwrap();
226        assert_eq!(format!("{}", chain_id), "eip155:1");
227        assert_eq!(chain_id.to_string(), "eip155:1");
228    }
229}