Skip to main content

alloy_chains/
named.rs

1use alloc::string::String;
2use core::{cmp::Ordering, fmt, str::FromStr, time::Duration};
3
4pub use crate::generated::named::NamedChain;
5
6impl NamedChain {
7    /// Returns an iterator over all named chains.
8    #[inline]
9    pub fn iter() -> NamedChainIter {
10        NamedChainIter { inner: Self::VARIANTS.iter().copied() }
11    }
12
13    /// Returns the chain's average blocktime, if applicable.
14    #[inline]
15    pub const fn average_blocktime_hint(self) -> Option<Duration> {
16        let millis = self.average_blocktime_millis();
17        if millis == 0 { None } else { Some(Duration::from_millis(millis as u64)) }
18    }
19
20    /// Returns the chain's blockchain explorer API key from the environment.
21    #[cfg(feature = "std")]
22    pub fn etherscan_api_key(self) -> Option<String> {
23        self.etherscan_api_key_name().and_then(|name| std::env::var(name).ok())
24    }
25
26    /// Returns the address of the public DNS node list for the given chain.
27    pub fn public_dns_network_protocol(self) -> Option<String> {
28        const DNS_PREFIX: &str = "enrtree://AKA3AM6LPBYEUDMVNU3BSVQJ5AD45Y7YPOHJLEF6W26QOE4VTUDPE@";
29        if matches!(
30            self,
31            Self::Mainnet
32                | Self::Goerli
33                | Self::Sepolia
34                | Self::Ropsten
35                | Self::Rinkeby
36                | Self::Holesky
37                | Self::Hoodi
38        ) {
39            let mut s = String::with_capacity(DNS_PREFIX.len() + 32);
40            s.push_str(DNS_PREFIX);
41            s.push_str("all.");
42            let chain_str = self.as_ref();
43            s.push_str(chain_str);
44            let l = s.len();
45            s[l - chain_str.len()..].make_ascii_lowercase();
46            s.push_str(".ethdisco.net");
47            Some(s)
48        } else {
49            None
50        }
51    }
52}
53
54/// Error returned when parsing a named chain from a string fails.
55#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
56pub struct ParseNamedChainError;
57
58impl fmt::Display for ParseNamedChainError {
59    #[inline]
60    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61        f.write_str("matching variant not found")
62    }
63}
64
65#[cfg(feature = "std")]
66impl std::error::Error for ParseNamedChainError {}
67
68/// Iterator over all named chains.
69#[derive(Clone, Debug)]
70pub struct NamedChainIter {
71    inner: core::iter::Copied<core::slice::Iter<'static, NamedChain>>,
72}
73
74impl Default for NamedChainIter {
75    #[inline]
76    fn default() -> Self {
77        NamedChain::iter()
78    }
79}
80
81impl Iterator for NamedChainIter {
82    type Item = NamedChain;
83
84    #[inline]
85    fn next(&mut self) -> Option<Self::Item> {
86        self.inner.next()
87    }
88
89    #[inline]
90    fn size_hint(&self) -> (usize, Option<usize>) {
91        self.inner.size_hint()
92    }
93}
94
95impl DoubleEndedIterator for NamedChainIter {
96    #[inline]
97    fn next_back(&mut self) -> Option<Self::Item> {
98        self.inner.next_back()
99    }
100}
101
102impl ExactSizeIterator for NamedChainIter {}
103impl core::iter::FusedIterator for NamedChainIter {}
104
105impl From<NamedChain> for &'static str {
106    #[inline]
107    fn from(chain: NamedChain) -> Self {
108        (&chain).into()
109    }
110}
111
112impl From<&NamedChain> for &'static str {
113    #[inline]
114    fn from(chain: &NamedChain) -> Self {
115        chain.as_str()
116    }
117}
118
119impl Default for NamedChain {
120    #[inline]
121    fn default() -> Self {
122        Self::Mainnet
123    }
124}
125
126macro_rules! impl_into_numeric {
127    ($($t:ty)+) => {$(
128        impl From<NamedChain> for $t {
129            #[inline]
130            fn from(chain: NamedChain) -> Self {
131                chain as $t
132            }
133        }
134    )+};
135}
136
137impl_into_numeric!(u64 i64 u128 i128);
138#[cfg(target_pointer_width = "64")]
139impl_into_numeric!(usize isize);
140
141impl num_enum::TryFromPrimitive for NamedChain {
142    type Primitive = u64;
143    type Error = num_enum::TryFromPrimitiveError<Self>;
144
145    const NAME: &'static str = "NamedChain";
146
147    fn try_from_primitive(number: Self::Primitive) -> Result<Self, Self::Error> {
148        Self::from_chain_id(number).ok_or_else(|| num_enum::TryFromPrimitiveError::new(number))
149    }
150}
151
152impl TryFrom<u64> for NamedChain {
153    type Error = num_enum::TryFromPrimitiveError<Self>;
154
155    #[inline]
156    fn try_from(value: u64) -> Result<Self, Self::Error> {
157        num_enum::TryFromPrimitive::try_from_primitive(value)
158    }
159}
160
161macro_rules! impl_try_from_numeric {
162    ($($native:ty)+) => {
163        $(
164            impl TryFrom<$native> for NamedChain {
165                type Error = num_enum::TryFromPrimitiveError<NamedChain>;
166
167                #[inline]
168                fn try_from(value: $native) -> Result<Self, Self::Error> {
169                    (value as u64).try_into()
170                }
171            }
172        )+
173    };
174}
175
176impl_try_from_numeric!(u8 i8 u16 i16 u32 i32 usize isize);
177
178impl fmt::Display for NamedChain {
179    #[inline]
180    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
181        self.as_str().fmt(f)
182    }
183}
184
185impl AsRef<str> for NamedChain {
186    #[inline]
187    fn as_ref(&self) -> &str {
188        self.as_str()
189    }
190}
191
192impl FromStr for NamedChain {
193    type Err = ParseNamedChainError;
194
195    fn from_str(s: &str) -> Result<Self, Self::Err> {
196        NamedChain::from_parse_str(s).ok_or(ParseNamedChainError)
197    }
198}
199
200impl TryFrom<&str> for NamedChain {
201    type Error = ParseNamedChainError;
202
203    #[inline]
204    fn try_from(value: &str) -> Result<Self, Self::Error> {
205        value.parse()
206    }
207}
208
209impl Ord for NamedChain {
210    #[inline]
211    fn cmp(&self, other: &Self) -> Ordering {
212        (*self as u64).cmp(&(*other as u64))
213    }
214}
215
216impl PartialOrd for NamedChain {
217    #[inline]
218    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
219        Some(self.cmp(other))
220    }
221}
222
223impl PartialEq<u64> for NamedChain {
224    #[inline]
225    fn eq(&self, other: &u64) -> bool {
226        (*self as u64) == *other
227    }
228}
229
230impl PartialOrd<u64> for NamedChain {
231    #[inline]
232    fn partial_cmp(&self, other: &u64) -> Option<Ordering> {
233        (*self as u64).partial_cmp(other)
234    }
235}
236
237#[cfg(feature = "serde")]
238impl serde::Serialize for NamedChain {
239    #[inline]
240    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
241        s.serialize_str(self.as_ref())
242    }
243}
244
245#[cfg(feature = "serde")]
246impl<'de> serde::Deserialize<'de> for NamedChain {
247    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
248        struct NamedChainVisitor;
249
250        impl serde::de::Visitor<'_> for NamedChainVisitor {
251            type Value = NamedChain;
252
253            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
254                formatter.write_str("a named chain")
255            }
256
257            fn visit_str<E: serde::de::Error>(self, value: &str) -> Result<Self::Value, E> {
258                NamedChain::from_serde_str(value).ok_or_else(|| {
259                    serde::de::Error::unknown_variant(value, NamedChain::VARIANT_NAMES)
260                })
261            }
262        }
263
264        deserializer.deserialize_str(NamedChainVisitor)
265    }
266}
267
268#[cfg(feature = "rlp")]
269impl alloy_rlp::Encodable for NamedChain {
270    #[inline]
271    fn encode(&self, out: &mut dyn alloy_rlp::BufMut) {
272        (*self as u64).encode(out)
273    }
274
275    #[inline]
276    fn length(&self) -> usize {
277        (*self as u64).length()
278    }
279}
280
281#[cfg(feature = "rlp")]
282impl alloy_rlp::Decodable for NamedChain {
283    #[inline]
284    fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
285        let n = u64::decode(buf)?;
286        Self::try_from(n).map_err(|_| alloy_rlp::Error::Overflow)
287    }
288}
289
290#[cfg(feature = "arbitrary")]
291impl<'a> arbitrary::Arbitrary<'a> for NamedChain {
292    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
293        let idx = u.choose_index(Self::COUNT)?;
294        Ok(Self::VARIANTS[idx])
295    }
296}
297
298#[cfg(test)]
299mod tests {
300    use super::*;
301    use crate::generated::named::PARSE_ALIASES;
302    #[cfg(feature = "serde")]
303    use crate::generated::named::SERDE_ALIASES;
304
305    #[allow(unused_imports)]
306    use alloc::string::ToString;
307
308    #[test]
309    #[cfg(feature = "serde")]
310    fn default() {
311        assert_eq!(serde_json::to_string(&NamedChain::default()).unwrap(), "\"mainnet\"");
312    }
313
314    #[test]
315    fn enum_iter() {
316        assert_eq!(NamedChain::COUNT, NamedChain::iter().size_hint().0);
317        assert_eq!(NamedChain::COUNT, NamedChain::VARIANTS.len());
318        assert_eq!(NamedChain::COUNT, NamedChain::VARIANT_NAMES.len());
319    }
320
321    #[test]
322    fn roundtrip_string() {
323        for chain in NamedChain::iter() {
324            let chain_string = chain.to_string();
325            assert_eq!(chain_string, format!("{chain}"));
326            assert_eq!(chain_string.as_str(), chain.as_ref());
327            #[cfg(feature = "serde")]
328            assert_eq!(serde_json::to_string(&chain).unwrap(), format!("\"{chain_string}\""));
329
330            assert_eq!(chain_string.parse::<NamedChain>().unwrap(), chain);
331        }
332    }
333
334    #[test]
335    #[cfg(feature = "serde")]
336    fn roundtrip_serde() {
337        for chain in NamedChain::iter() {
338            let chain_string = serde_json::to_string(&chain).unwrap();
339            let chain_string = chain_string.replace('-', "_");
340            assert_eq!(serde_json::from_str::<'_, NamedChain>(&chain_string).unwrap(), chain);
341        }
342    }
343
344    #[test]
345    #[cfg(feature = "arbitrary")]
346    fn test_arbitrary_named_chain() {
347        use arbitrary::{Arbitrary, Unstructured};
348        let data = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 255];
349        let mut unstructured = Unstructured::new(&data);
350
351        for _ in 0..10 {
352            let _chain = NamedChain::arbitrary(&mut unstructured).unwrap();
353        }
354    }
355
356    #[test]
357    fn aliases() {
358        for &(chain, alias) in PARSE_ALIASES {
359            assert_eq!(alias.parse::<NamedChain>().unwrap(), chain);
360
361            #[cfg(feature = "serde")]
362            assert_eq!(serde_json::from_str::<NamedChain>(&format!("\"{alias}\"")).unwrap(), chain);
363        }
364
365        #[cfg(feature = "serde")]
366        for &(chain, alias) in SERDE_ALIASES {
367            assert_eq!(serde_json::from_str::<NamedChain>(&format!("\"{alias}\"")).unwrap(), chain);
368        }
369    }
370
371    #[test]
372    #[cfg(feature = "serde")]
373    fn serde_to_string_match() {
374        for chain in NamedChain::iter() {
375            let chain_serde = serde_json::to_string(&chain).unwrap();
376            let chain_string = format!("\"{chain}\"");
377            assert_eq!(chain_serde, chain_string);
378        }
379    }
380
381    #[test]
382    fn test_dns_network() {
383        let s = "enrtree://AKA3AM6LPBYEUDMVNU3BSVQJ5AD45Y7YPOHJLEF6W26QOE4VTUDPE@all.mainnet.ethdisco.net";
384        assert_eq!(NamedChain::Mainnet.public_dns_network_protocol().unwrap(), s);
385    }
386
387    #[test]
388    fn ensure_no_trailing_etherscan_url_separator() {
389        for chain in NamedChain::iter() {
390            if let Some((api, base)) = chain.etherscan_urls() {
391                assert!(!api.ends_with('/'), "{chain:?} api url has trailing /");
392                assert!(!base.ends_with('/'), "{chain:?} base url has trailing /");
393            }
394        }
395    }
396}