1use crate::error::OwsError;
2use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
3use std::fmt;
4use std::hash::{Hash, Hasher};
5use std::str::FromStr;
6
7#[derive(Debug, Clone, Eq)]
9pub struct ChainId {
10 pub namespace: String,
11 pub reference: String,
12}
13
14impl ChainId {
15 fn validate_namespace(ns: &str) -> Result<(), OwsError> {
16 if ns.len() < 3 || ns.len() > 8 {
17 return Err(OwsError::CaipParseError {
18 message: format!(
19 "namespace must be 3-8 characters, got {} ('{}')",
20 ns.len(),
21 ns
22 ),
23 });
24 }
25 if !ns
26 .chars()
27 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit())
28 {
29 return Err(OwsError::CaipParseError {
30 message: format!("namespace must be [a-z0-9], got '{}'", ns),
31 });
32 }
33 Ok(())
34 }
35
36 fn validate_reference(reference: &str) -> Result<(), OwsError> {
37 if reference.is_empty() || reference.len() > 64 {
38 return Err(OwsError::CaipParseError {
39 message: format!(
40 "reference must be 1-64 characters, got {} ('{}')",
41 reference.len(),
42 reference
43 ),
44 });
45 }
46 if !reference
47 .chars()
48 .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
49 {
50 return Err(OwsError::CaipParseError {
51 message: format!("reference contains invalid characters: '{}'", reference),
52 });
53 }
54 Ok(())
55 }
56}
57
58impl FromStr for ChainId {
59 type Err = OwsError;
60
61 fn from_str(s: &str) -> Result<Self, Self::Err> {
62 let parts: Vec<&str> = s.splitn(2, ':').collect();
63 if parts.len() != 2 {
64 return Err(OwsError::CaipParseError {
65 message: format!("expected 'namespace:reference', got '{}'", s),
66 });
67 }
68 let namespace = parts[0].to_string();
69 let reference = parts[1].to_string();
70 Self::validate_namespace(&namespace)?;
71 Self::validate_reference(&reference)?;
72 Ok(ChainId {
73 namespace,
74 reference,
75 })
76 }
77}
78
79impl fmt::Display for ChainId {
80 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81 write!(f, "{}:{}", self.namespace, self.reference)
82 }
83}
84
85impl PartialEq for ChainId {
86 fn eq(&self, other: &Self) -> bool {
87 self.namespace == other.namespace && self.reference == other.reference
88 }
89}
90
91impl Hash for ChainId {
92 fn hash<H: Hasher>(&self, state: &mut H) {
93 self.namespace.hash(state);
94 self.reference.hash(state);
95 }
96}
97
98impl Serialize for ChainId {
99 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
100 serializer.serialize_str(&self.to_string())
101 }
102}
103
104impl<'de> Deserialize<'de> for ChainId {
105 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
106 let s = String::deserialize(deserializer)?;
107 ChainId::from_str(&s).map_err(de::Error::custom)
108 }
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114
115 #[test]
116 fn test_parse_evm_chain_id() {
117 let id: ChainId = "eip155:1".parse().unwrap();
118 assert_eq!(id.namespace, "eip155");
119 assert_eq!(id.reference, "1");
120 }
121
122 #[test]
123 fn test_parse_solana_chain_id() {
124 let id: ChainId = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp".parse().unwrap();
125 assert_eq!(id.namespace, "solana");
126 }
127
128 #[test]
129 fn test_parse_cosmos_chain_id() {
130 let id: ChainId = "cosmos:cosmoshub-4".parse().unwrap();
131 assert_eq!(id.namespace, "cosmos");
132 assert_eq!(id.reference, "cosmoshub-4");
133 }
134
135 #[test]
136 fn test_parse_bitcoin_chain_id() {
137 let id: ChainId = "bip122:000000000019d6689c085ae165831e93".parse().unwrap();
138 assert_eq!(id.namespace, "bip122");
139 }
140
141 #[test]
142 fn test_parse_tron_chain_id() {
143 let id: ChainId = "tron:mainnet".parse().unwrap();
144 assert_eq!(id.namespace, "tron");
145 assert_eq!(id.reference, "mainnet");
146 }
147
148 #[test]
149 fn test_reject_empty_namespace() {
150 assert!("".parse::<ChainId>().is_err());
151 }
152
153 #[test]
154 fn test_reject_short_namespace() {
155 assert!("ab:1".parse::<ChainId>().is_err());
156 }
157
158 #[test]
159 fn test_reject_long_namespace() {
160 assert!("abcdefghi:1".parse::<ChainId>().is_err());
161 }
162
163 #[test]
164 fn test_reject_uppercase_namespace() {
165 assert!("EIP155:1".parse::<ChainId>().is_err());
166 }
167
168 #[test]
169 fn test_reject_no_colon() {
170 assert!("eip1551".parse::<ChainId>().is_err());
171 }
172
173 #[test]
174 fn test_display_roundtrip() {
175 let id: ChainId = "eip155:1".parse().unwrap();
176 assert_eq!(id.to_string(), "eip155:1");
177 let id2: ChainId = id.to_string().parse().unwrap();
178 assert_eq!(id, id2);
179 }
180
181 #[test]
182 fn test_serde_roundtrip() {
183 let id: ChainId = "eip155:1".parse().unwrap();
184 let json = serde_json::to_string(&id).unwrap();
185 assert_eq!(json, "\"eip155:1\"");
186 let id2: ChainId = serde_json::from_str(&json).unwrap();
187 assert_eq!(id, id2);
188 }
189
190 #[test]
191 fn test_chain_id_hash() {
192 use std::collections::HashSet;
193 let mut set = HashSet::new();
194 let id1: ChainId = "eip155:1".parse().unwrap();
195 let id2: ChainId = "eip155:1".parse().unwrap();
196 set.insert(id1);
197 assert!(set.contains(&id2));
198 }
199}