1use std::{str::FromStr, sync::LazyLock};
2
3use regex::Regex;
4use serde::{Deserialize, Deserializer, Serialize};
5use thiserror::Error;
6#[cfg(feature = "ts-bindings")]
7use ts_rs::TS;
8use utoipa::ToSchema;
9
10#[cfg_attr(feature = "ts-bindings", derive(TS))]
18#[cfg_attr(feature = "ts-bindings", ts(export, type = "string"))]
19#[derive(
20 Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, ToSchema, bincode::Decode, bincode::Encode,
21)]
22pub struct ChainKey {
23 pub namespace: ChainKeyNamespace,
24 pub id: ChainKeyId,
25}
26
27type ChainKeyResult<T> = Result<T, ChainKeyError>;
28
29static CHAIN_KEY_REGEX: LazyLock<Regex> =
30 LazyLock::new(|| Regex::new(r"^([-a-z0-9]{1,32}):([-_a-zA-Z0-9]{1,32})$").unwrap());
31
32static NAMESPACE_REGEX: LazyLock<Regex> =
33 LazyLock::new(|| Regex::new(r"^[-a-z0-9]{1,32}$").unwrap());
34
35static ID_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^[-_a-zA-Z0-9]{1,32}$").unwrap());
36
37impl ChainKey {
38 pub fn validate(s: impl AsRef<str>) -> ChainKeyResult<()> {
40 let s = s.as_ref();
41 if !CHAIN_KEY_REGEX.is_match(s) {
42 Err(ChainKeyError::FormatError)
43 } else {
44 Ok(())
45 }
46 }
47
48 pub fn new(s: impl Into<String>) -> ChainKeyResult<Self> {
50 let s = s.into();
51 let captures = CHAIN_KEY_REGEX
52 .captures(&s)
53 .ok_or(ChainKeyError::FormatError)?;
54
55 let namespace = ChainKeyNamespace::new(&captures[1])?;
56 let id = ChainKeyId::new(&captures[2])?;
57
58 Ok(Self { namespace, id })
59 }
60}
61
62impl<'de> Deserialize<'de> for ChainKey {
63 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
64 where
65 D: Deserializer<'de>,
66 {
67 let s = String::deserialize(deserializer)?;
68 Self::new(s).map_err(serde::de::Error::custom)
69 }
70}
71
72impl Serialize for ChainKey {
73 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
74 where
75 S: serde::Serializer,
76 {
77 serializer.serialize_str(&self.to_string())
78 }
79}
80
81impl std::fmt::Display for ChainKey {
82 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83 write!(f, "{}:{}", self.namespace, self.id)
84 }
85}
86
87impl TryFrom<&str> for ChainKey {
88 type Error = ChainKeyError;
89 fn try_from(s: &str) -> Result<Self, Self::Error> {
90 s.parse()
91 }
92}
93
94impl FromStr for ChainKey {
95 type Err = ChainKeyError;
96 fn from_str(s: &str) -> Result<Self, Self::Err> {
97 Self::new(s)
98 }
99}
100
101impl From<ChainKey> for layer_climb_config::ChainId {
102 fn from(key: ChainKey) -> Self {
103 layer_climb_config::ChainId::new(key.id.into_inner())
105 }
106}
107
108#[cfg_attr(feature = "ts-bindings", derive(TS))]
112#[derive(
113 Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, ToSchema, bincode::Decode, bincode::Encode,
114)]
115pub struct ChainKeyNamespace(String);
116
117impl ChainKeyNamespace {
118 pub const EVM: &str = "evm";
119 pub const COSMOS: &str = "cosmos";
120 pub const DEV: &str = "dev";
121
122 pub fn new(s: impl Into<String>) -> ChainKeyResult<Self> {
123 let s = s.into();
124 if NAMESPACE_REGEX.is_match(&s) {
125 Ok(Self(s))
126 } else {
127 Err(ChainKeyError::InvalidNamespace)
128 }
129 }
130 pub fn as_str(&self) -> &str {
131 &self.0
132 }
133 pub fn into_inner(self) -> String {
134 self.0
135 }
136}
137
138impl FromStr for ChainKeyNamespace {
139 type Err = ChainKeyError;
140 fn from_str(s: &str) -> Result<Self, Self::Err> {
141 Self::new(s.to_string())
142 }
143}
144
145impl TryFrom<&str> for ChainKeyNamespace {
146 type Error = ChainKeyError;
147 fn try_from(s: &str) -> Result<Self, Self::Error> {
148 s.parse()
149 }
150}
151
152impl std::fmt::Display for ChainKeyNamespace {
153 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
154 f.write_str(&self.0)
155 }
156}
157
158impl<'de> Deserialize<'de> for ChainKeyNamespace {
159 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
160 where
161 D: Deserializer<'de>,
162 {
163 let s = String::deserialize(deserializer)?;
164 Self::new(s).map_err(serde::de::Error::custom)
165 }
166}
167
168impl Serialize for ChainKeyNamespace {
169 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
170 where
171 S: serde::Serializer,
172 {
173 serializer.serialize_str(&self.0)
174 }
175}
176
177#[cfg_attr(feature = "ts-bindings", derive(TS))]
181#[derive(
182 Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, ToSchema, bincode::Decode, bincode::Encode,
183)]
184pub struct ChainKeyId(String);
185
186impl ChainKeyId {
187 pub fn new(s: impl Into<String>) -> ChainKeyResult<Self> {
188 let s = s.into();
189 if ID_REGEX.is_match(&s) {
190 Ok(Self(s))
191 } else {
192 Err(ChainKeyError::InvalidId)
193 }
194 }
195 pub fn as_str(&self) -> &str {
196 &self.0
197 }
198 pub fn into_inner(self) -> String {
199 self.0
200 }
201}
202
203impl FromStr for ChainKeyId {
204 type Err = ChainKeyError;
205 fn from_str(s: &str) -> Result<Self, Self::Err> {
206 Self::new(s.to_string())
207 }
208}
209
210impl TryFrom<&str> for ChainKeyId {
211 type Error = ChainKeyError;
212 fn try_from(s: &str) -> Result<Self, Self::Error> {
213 s.parse()
214 }
215}
216
217impl std::fmt::Display for ChainKeyId {
218 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
219 f.write_str(&self.0)
220 }
221}
222
223impl<'de> Deserialize<'de> for ChainKeyId {
224 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
225 where
226 D: Deserializer<'de>,
227 {
228 let s = String::deserialize(deserializer)?;
229 Self::new(s).map_err(serde::de::Error::custom)
230 }
231}
232
233impl Serialize for ChainKeyId {
234 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
235 where
236 S: serde::Serializer,
237 {
238 serializer.serialize_str(&self.0)
239 }
240}
241
242impl From<ChainKeyId> for layer_climb_config::ChainId {
243 fn from(id: ChainKeyId) -> Self {
244 layer_climb_config::ChainId::new(id.into_inner())
245 }
246}
247
248#[derive(Error, Debug, PartialEq, Eq, Clone)]
252pub enum ChainKeyError {
253 #[error("ChainKey must follow the CAIP-2-like format of 'namespace:id'")]
254 FormatError,
255 #[error("Invalid namespace component")]
256 InvalidNamespace,
257 #[error("Invalid id component")]
258 InvalidId,
259}
260
261#[cfg(test)]
262mod tests {
263 use super::*;
264
265 #[test]
266 fn test_valid_chain_ids() {
267 assert!(ChainKey::new("eip155:1").is_ok());
268 assert!(ChainKey::new("cosmos:cosmoshub-4").is_ok());
269 assert!(ChainKey::new("polkadot:b0a8d493285c2df73290dfb7e61f870f").is_ok());
270 assert!(ChainKey::new("abc:X_Y-Z123").is_ok());
271 assert!(ChainKey::new("abc-123:ABC_xyz-123").is_ok());
272 }
273
274 #[test]
275 fn test_invalid_chain_ids() {
276 assert!(ChainKey::new("no-colon").is_err());
278 assert!(ChainKey::new("too:many:colons").is_err());
279 assert!(ChainKey::new(":empty-namespace").is_err());
280 assert!(ChainKey::new("empty-id:").is_err());
281
282 assert!(ChainKey::new("thisiswaytoolongtobeavalidnamespace:ref").is_err()); assert!(ChainKey::new("ABC:ref").is_err()); assert!(ChainKey::new("ab_c:ref").is_err()); assert!(ChainKey::new("ab.c:ref").is_err()); assert!(ChainKey::new("abc:").is_err()); assert!(ChainKey::new("abc:this-is-way-too-long-to-be-a-valid-idt").is_err()); assert!(ChainKey::new("abc:id.with.dots").is_err()); assert!(ChainKey::new("abc:id@123").is_err()); assert!(ChainKey::new("abc:id space").is_err()); }
295
296 #[test]
297 fn test_accessors() {
298 let chain_key = ChainKey::new("eip155:1").unwrap();
299 assert_eq!(chain_key.to_string(), "eip155:1");
300 assert_eq!(chain_key.namespace.as_str(), "eip155");
301 assert_eq!(chain_key.id.as_str(), "1");
302 }
303
304 #[test]
305 fn test_edge_cases() {
306 assert!(ChainKey::new("abc:x").is_ok());
308
309 assert!(ChainKey::new("abcd1234:ABCD1234_abcd1234-ABCD1234").is_ok());
311
312 let chain_key = ChainKey::new("eip155:MyChain_123").unwrap();
314 assert_eq!(chain_key.id.as_str(), "MyChain_123");
315 }
316}