1use crate::error::Error;
2use once_cell::sync::Lazy;
3use regex::Regex;
4use serde::{Deserialize, Deserializer, Serialize, Serializer};
5use std::str::FromStr;
6
7static 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#[derive(Debug, Clone, PartialEq, Eq, Hash)]
23pub struct ChainId {
24 namespace: String,
25 reference: String,
26}
27
28impl ChainId {
29 pub fn new(namespace: &str, reference: &str) -> Result<Self, Error> {
40 Self::validate_namespace(namespace)?;
42 Self::validate_reference(reference)?;
43
44 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 pub fn namespace(&self) -> &str {
58 &self.namespace
59 }
60
61 pub fn reference(&self) -> &str {
63 &self.reference
64 }
65
66 fn validate_namespace(namespace: &str) -> Result<(), Error> {
68 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 Ok(())
80 }
81
82 fn validate_reference(reference: &str) -> Result<(), Error> {
84 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 Ok(())
96 }
97}
98
99impl FromStr for ChainId {
100 type Err = Error;
101
102 fn from_str(s: &str) -> Result<Self, Self::Err> {
112 if !CHAIN_ID_REGEX.is_match(s) {
114 return Err(Error::InvalidChainId(s.to_string()));
115 }
116
117 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
127impl 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 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 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 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 assert!(ChainId::from_str("").is_err());
188
189 assert!(ChainId::from_str("eip1551").is_err());
191
192 assert!(ChainId::from_str(":1").is_err());
194
195 assert!(ChainId::from_str("eip155:").is_err());
197
198 assert!(ChainId::from_str("ei:1").is_err());
200
201 assert!(ChainId::from_str("eip155toolong:1").is_err());
203
204 assert!(ChainId::from_str("EIP155:1").is_err()); assert!(ChainId::from_str("eip_155:1").is_err()); 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}