1use crate::error::Error;
2use once_cell::sync::Lazy;
3use regex::Regex;
4use serde::{Deserialize, Serialize};
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, Serialize, Deserialize)]
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
136#[cfg(test)]
137mod tests {
138 use super::*;
139
140 #[test]
141 fn test_valid_chain_ids() {
142 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 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 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 assert!(ChainId::from_str("").is_err());
169
170 assert!(ChainId::from_str("eip1551").is_err());
172
173 assert!(ChainId::from_str(":1").is_err());
175
176 assert!(ChainId::from_str("eip155:").is_err());
178
179 assert!(ChainId::from_str("ei:1").is_err());
181
182 assert!(ChainId::from_str("eip155toolong:1").is_err());
184
185 assert!(ChainId::from_str("EIP155:1").is_err()); assert!(ChainId::from_str("eip_155:1").is_err()); 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}