tap_caip/
chain_id.rs

1use crate::error::Error;
2use once_cell::sync::Lazy;
3use regex::Regex;
4use serde::{Deserialize, Serialize};
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, Serialize, Deserialize)]
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
136#[cfg(test)]
137mod tests {
138    use super::*;
139
140    #[test]
141    fn test_valid_chain_ids() {
142        // Test Ethereum mainnet
143        let eth_mainnet = ChainId::from_str("eip155:1").unwrap();
144        assert_eq!(eth_mainnet.namespace(), "eip155");
145        assert_eq!(eth_mainnet.reference(), "1");
146        assert_eq!(eth_mainnet.to_string(), "eip155:1");
147
148        // Test Bitcoin mainnet
149        let btc_mainnet = ChainId::from_str(
150            "bip122:000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f",
151        )
152        .unwrap();
153        assert_eq!(btc_mainnet.namespace(), "bip122");
154        assert_eq!(
155            btc_mainnet.reference(),
156            "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"
157        );
158
159        // Test direct creation
160        let poly_mainnet = ChainId::new("polkadot", "91b171bb158e2d3848fa23a9f1c25182").unwrap();
161        assert_eq!(poly_mainnet.namespace(), "polkadot");
162        assert_eq!(poly_mainnet.reference(), "91b171bb158e2d3848fa23a9f1c25182");
163    }
164
165    #[test]
166    fn test_invalid_chain_ids() {
167        // Invalid: empty string
168        assert!(ChainId::from_str("").is_err());
169
170        // Invalid: missing separator
171        assert!(ChainId::from_str("eip1551").is_err());
172
173        // Invalid: empty namespace
174        assert!(ChainId::from_str(":1").is_err());
175
176        // Invalid: empty reference
177        assert!(ChainId::from_str("eip155:").is_err());
178
179        // Invalid: namespace too short
180        assert!(ChainId::from_str("ei:1").is_err());
181
182        // Invalid: namespace too long
183        assert!(ChainId::from_str("eip155toolong:1").is_err());
184
185        // Invalid: invalid namespace characters
186        assert!(ChainId::from_str("EIP155:1").is_err()); // uppercase not allowed
187        assert!(ChainId::from_str("eip_155:1").is_err()); // underscore not allowed
188
189        // Invalid: reference too long
190        let long_reference = "a".repeat(65);
191        assert!(ChainId::from_str(&format!("eip155:{}", long_reference)).is_err());
192    }
193
194    #[test]
195    fn test_serialization() {
196        let chain_id = ChainId::from_str("eip155:1").unwrap();
197        let serialized = serde_json::to_string(&chain_id).unwrap();
198        assert_eq!(serialized, r#"{"namespace":"eip155","reference":"1"}"#);
199
200        let deserialized: ChainId = serde_json::from_str(&serialized).unwrap();
201        assert_eq!(deserialized, chain_id);
202    }
203
204    #[test]
205    fn test_display_formatting() {
206        let chain_id = ChainId::from_str("eip155:1").unwrap();
207        assert_eq!(format!("{}", chain_id), "eip155:1");
208        assert_eq!(chain_id.to_string(), "eip155:1");
209    }
210}