1use std::collections::{HashMap, HashSet};
13use std::fmt;
14use std::str::FromStr;
15use std::sync::Arc;
16
17use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
18
19#[derive(Debug, Clone, PartialEq, Eq, Hash)]
45pub struct ChainId {
46 namespace: String,
47 reference: String,
48}
49
50impl ChainId {
51 pub fn new<N: Into<String>, R: Into<String>>(namespace: N, reference: R) -> Self {
53 Self {
54 namespace: namespace.into(),
55 reference: reference.into(),
56 }
57 }
58
59 #[must_use]
61 pub fn namespace(&self) -> &str {
62 &self.namespace
63 }
64
65 #[must_use]
67 pub fn reference(&self) -> &str {
68 &self.reference
69 }
70
71 #[must_use]
73 pub fn into_parts(self) -> (String, String) {
74 (self.namespace, self.reference)
75 }
76}
77
78impl fmt::Display for ChainId {
79 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80 write!(f, "{}:{}", self.namespace, self.reference)
81 }
82}
83
84impl From<ChainId> for String {
85 fn from(value: ChainId) -> Self {
86 value.to_string()
87 }
88}
89
90#[derive(Debug, thiserror::Error)]
95#[error("Invalid chain id format {0}")]
96pub struct ChainIdFormatError(String);
97
98impl FromStr for ChainId {
99 type Err = ChainIdFormatError;
100
101 fn from_str(s: &str) -> Result<Self, Self::Err> {
102 let (namespace, reference) = s
103 .split_once(':')
104 .ok_or_else(|| ChainIdFormatError(s.into()))?;
105 Ok(Self {
106 namespace: namespace.into(),
107 reference: reference.into(),
108 })
109 }
110}
111
112impl Serialize for ChainId {
113 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
114 where
115 S: Serializer,
116 {
117 serializer.serialize_str(&self.to_string())
118 }
119}
120
121impl<'de> Deserialize<'de> for ChainId {
122 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
123 where
124 D: Deserializer<'de>,
125 {
126 let s = String::deserialize(deserializer)?;
127 Self::from_str(&s).map_err(de::Error::custom)
128 }
129}
130
131#[derive(Debug, Clone)]
146#[non_exhaustive]
147pub enum ChainIdPattern {
148 Wildcard {
150 namespace: String,
152 },
153 Exact {
155 namespace: String,
157 reference: String,
159 },
160 Set {
162 namespace: String,
164 references: HashSet<String>,
166 },
167}
168
169impl ChainIdPattern {
170 pub fn wildcard<S: Into<String>>(namespace: S) -> Self {
172 Self::Wildcard {
173 namespace: namespace.into(),
174 }
175 }
176
177 pub fn exact<N: Into<String>, R: Into<String>>(namespace: N, reference: R) -> Self {
179 Self::Exact {
180 namespace: namespace.into(),
181 reference: reference.into(),
182 }
183 }
184
185 pub fn set<N: Into<String>>(namespace: N, references: HashSet<String>) -> Self {
187 Self::Set {
188 namespace: namespace.into(),
189 references,
190 }
191 }
192
193 #[must_use]
199 pub fn matches(&self, chain_id: &ChainId) -> bool {
200 match self {
201 Self::Wildcard { namespace } => chain_id.namespace == *namespace,
202 Self::Exact {
203 namespace,
204 reference,
205 } => chain_id.namespace == *namespace && chain_id.reference == *reference,
206 Self::Set {
207 namespace,
208 references,
209 } => chain_id.namespace == *namespace && references.contains(&chain_id.reference),
210 }
211 }
212
213 #[must_use]
215 pub fn namespace(&self) -> &str {
216 match self {
217 Self::Wildcard { namespace }
218 | Self::Exact { namespace, .. }
219 | Self::Set { namespace, .. } => namespace,
220 }
221 }
222}
223
224impl fmt::Display for ChainIdPattern {
225 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
226 match self {
227 Self::Wildcard { namespace } => write!(f, "{namespace}:*"),
228 Self::Exact {
229 namespace,
230 reference,
231 } => write!(f, "{namespace}:{reference}"),
232 Self::Set {
233 namespace,
234 references,
235 } => {
236 let refs: Vec<&str> = references.iter().map(AsRef::as_ref).collect();
237 write!(f, "{}:{{{}}}", namespace, refs.join(","))
238 }
239 }
240 }
241}
242
243impl FromStr for ChainIdPattern {
244 type Err = ChainIdFormatError;
245
246 fn from_str(s: &str) -> Result<Self, Self::Err> {
247 let (namespace, rest) = s
248 .split_once(':')
249 .ok_or_else(|| ChainIdFormatError(s.into()))?;
250
251 if namespace.is_empty() {
252 return Err(ChainIdFormatError(s.into()));
253 }
254
255 if rest == "*" {
257 return Ok(Self::wildcard(namespace));
258 }
259
260 if let Some(inner) = rest.strip_prefix('{').and_then(|r| r.strip_suffix('}')) {
262 let items: Vec<&str> = inner.split(',').map(str::trim).collect();
263 if items.is_empty() || items.iter().any(|item| item.is_empty()) {
264 return Err(ChainIdFormatError(s.into()));
265 }
266 let references = items.into_iter().map(Into::into).collect();
267 return Ok(Self::set(namespace, references));
268 }
269
270 if rest.is_empty() {
272 return Err(ChainIdFormatError(s.into()));
273 }
274
275 Ok(Self::exact(namespace, rest))
276 }
277}
278
279impl Serialize for ChainIdPattern {
280 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
281 where
282 S: Serializer,
283 {
284 serializer.serialize_str(&self.to_string())
285 }
286}
287
288impl<'de> Deserialize<'de> for ChainIdPattern {
289 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
290 where
291 D: Deserializer<'de>,
292 {
293 let s = String::deserialize(deserializer)?;
294 Self::from_str(&s).map_err(de::Error::custom)
295 }
296}
297
298impl From<ChainId> for ChainIdPattern {
299 fn from(chain_id: ChainId) -> Self {
300 let (namespace, reference) = chain_id.into_parts();
301 Self::exact(namespace, reference)
302 }
303}
304
305pub trait ChainProvider {
310 fn signer_addresses(&self) -> Vec<String>;
315
316 fn chain_id(&self) -> ChainId;
318}
319
320impl<T: ChainProvider> ChainProvider for Arc<T> {
321 fn signer_addresses(&self) -> Vec<String> {
322 (**self).signer_addresses()
323 }
324 fn chain_id(&self) -> ChainId {
325 (**self).chain_id()
326 }
327}
328
329#[derive(Debug)]
335pub struct ChainRegistry<P>(HashMap<ChainId, P>);
336
337impl<P> ChainRegistry<P> {
338 #[must_use]
340 pub const fn new(providers: HashMap<ChainId, P>) -> Self {
341 Self(providers)
342 }
343}
344
345impl<P> ChainRegistry<P> {
346 #[must_use]
350 pub fn by_chain_id(&self, chain_id: &ChainId) -> Option<&P> {
351 self.0.get(chain_id)
352 }
353
354 #[must_use]
362 pub fn by_chain_id_pattern(&self, pattern: &ChainIdPattern) -> Vec<&P> {
363 self.0
364 .iter()
365 .filter_map(|(chain_id, provider)| pattern.matches(chain_id).then_some(provider))
366 .collect()
367 }
368}
369
370#[derive(Debug, Clone)]
380pub struct DeployedTokenAmount<TAmount, TToken> {
381 pub amount: TAmount,
383 pub token: TToken,
385}
386
387#[derive(Debug, Clone, Copy, PartialEq, Eq)]
389pub struct NetworkInfo {
390 pub name: &'static str,
392 pub namespace: &'static str,
394 pub reference: &'static str,
396}
397
398impl NetworkInfo {
399 #[must_use]
401 pub fn chain_id(&self) -> ChainId {
402 ChainId::new(self.namespace, self.reference)
403 }
404}
405
406#[cfg(test)]
407mod tests {
408 use super::*;
409 #[test]
410 fn test_chain_id_serialize_eip155() {
411 let chain_id = ChainId::new("eip155", "1");
412 let serialized = serde_json::to_string(&chain_id).unwrap();
413 assert_eq!(serialized, "\"eip155:1\"");
414 }
415
416 #[test]
417 fn test_chain_id_serialize_solana() {
418 let chain_id = ChainId::new("solana", "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp");
419 let serialized = serde_json::to_string(&chain_id).unwrap();
420 assert_eq!(serialized, "\"solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp\"");
421 }
422
423 #[test]
424 fn test_chain_id_deserialize_eip155() {
425 let chain_id: ChainId = serde_json::from_str("\"eip155:1\"").unwrap();
426 assert_eq!(chain_id.namespace(), "eip155");
427 assert_eq!(chain_id.reference(), "1");
428 }
429
430 #[test]
431 fn test_chain_id_deserialize_solana() {
432 let chain_id: ChainId =
433 serde_json::from_str("\"solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp\"").unwrap();
434 assert_eq!(chain_id.namespace(), "solana");
435 assert_eq!(chain_id.reference(), "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp");
436 }
437
438 #[test]
439 fn test_chain_id_roundtrip_eip155() {
440 let original = ChainId::new("eip155", "8453");
441 let serialized = serde_json::to_string(&original).unwrap();
442 let deserialized: ChainId = serde_json::from_str(&serialized).unwrap();
443 assert_eq!(original, deserialized);
444 }
445
446 #[test]
447 fn test_chain_id_roundtrip_solana() {
448 let original = ChainId::new("solana", "devnet");
449 let serialized = serde_json::to_string(&original).unwrap();
450 let deserialized: ChainId = serde_json::from_str(&serialized).unwrap();
451 assert_eq!(original, deserialized);
452 }
453
454 #[test]
455 fn test_chain_id_deserialize_invalid_format() {
456 let result: Result<ChainId, _> = serde_json::from_str("\"invalid\"");
457 assert!(result.is_err());
458 }
459
460 #[test]
461 fn test_chain_id_deserialize_unknown_namespace() {
462 let result: Result<ChainId, _> = serde_json::from_str("\"unknown:1\"");
463 assert!(result.is_ok());
464 }
465
466 #[test]
467 fn test_pattern_wildcard_matches() {
468 let pattern = ChainIdPattern::wildcard("eip155");
469 assert!(pattern.matches(&ChainId::new("eip155", "1")));
470 assert!(pattern.matches(&ChainId::new("eip155", "8453")));
471 assert!(pattern.matches(&ChainId::new("eip155", "137")));
472 assert!(!pattern.matches(&ChainId::new("solana", "mainnet")));
473 }
474
475 #[test]
476 fn test_pattern_exact_matches() {
477 let pattern = ChainIdPattern::exact("eip155", "1");
478 assert!(pattern.matches(&ChainId::new("eip155", "1")));
479 assert!(!pattern.matches(&ChainId::new("eip155", "8453")));
480 assert!(!pattern.matches(&ChainId::new("solana", "1")));
481 }
482
483 #[test]
484 fn test_pattern_set_matches() {
485 let references: HashSet<String> = vec!["1", "8453", "137"]
486 .into_iter()
487 .map(String::from)
488 .collect();
489 let pattern = ChainIdPattern::set("eip155", references);
490 assert!(pattern.matches(&ChainId::new("eip155", "1")));
491 assert!(pattern.matches(&ChainId::new("eip155", "8453")));
492 assert!(pattern.matches(&ChainId::new("eip155", "137")));
493 assert!(!pattern.matches(&ChainId::new("eip155", "42")));
494 assert!(!pattern.matches(&ChainId::new("solana", "1")));
495 }
496
497 #[test]
498 fn test_pattern_namespace() {
499 let wildcard = ChainIdPattern::wildcard("eip155");
500 assert_eq!(wildcard.namespace(), "eip155");
501
502 let exact = ChainIdPattern::exact("solana", "mainnet");
503 assert_eq!(exact.namespace(), "solana");
504
505 let references: HashSet<String> = vec!["1"].into_iter().map(String::from).collect();
506 let set = ChainIdPattern::set("eip155", references);
507 assert_eq!(set.namespace(), "eip155");
508 }
509}