diff --git a/src/chain.rs b/src/chain.rs index 047b64b..4931470 100644 --- a/src/chain.rs +++ b/src/chain.rs @@ -11,140 +11,63 @@ use proptest::{ use strum::{EnumCount, IntoEnumIterator}; /// Either a known [`NamedChain`] or a EIP-155 chain ID. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Clone, Copy, PartialEq, Eq, Hash)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub enum Chain { +pub struct Chain(ChainKind); + +/// The kind of chain. Returned by [`Chain::kind`]. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub enum ChainKind { /// Known chain. Named(NamedChain), /// EIP-155 chain ID. Id(u64), } -impl Chain { - /// Returns the mainnet chain. - pub const fn mainnet() -> Self { - Chain::Named(NamedChain::Mainnet) - } - - /// Returns the goerli chain. - pub const fn goerli() -> Self { - Chain::Named(NamedChain::Goerli) - } - - /// Returns the sepolia chain. - pub const fn sepolia() -> Self { - Chain::Named(NamedChain::Sepolia) - } - - /// Returns the holesky chain. - pub const fn holesky() -> Self { - Chain::Named(NamedChain::Holesky) - } - - /// Returns the optimism goerli chain. - pub const fn optimism_goerli() -> Self { - Chain::Named(NamedChain::OptimismGoerli) - } - - /// Returns the optimism mainnet chain. - pub const fn optimism_mainnet() -> Self { - Chain::Named(NamedChain::Optimism) - } - - /// Returns the base goerli chain. - pub const fn base_goerli() -> Self { - Chain::Named(NamedChain::BaseGoerli) - } - - /// Returns the base mainnet chain. - pub const fn base_mainnet() -> Self { - Chain::Named(NamedChain::Base) - } - - /// Returns the dev chain. - pub const fn dev() -> Self { - Chain::Named(NamedChain::Dev) - } - - /// Returns true if the chain contains Optimism configuration. - pub fn is_optimism(self) -> bool { - self.named().map_or(false, |c| { - matches!( - c, - NamedChain::Optimism - | NamedChain::OptimismGoerli - | NamedChain::OptimismKovan - | NamedChain::Base - | NamedChain::BaseGoerli - ) - }) - } - - /// Attempts to convert the chain into a named chain. - pub fn named(&self) -> Option { - match self { - Chain::Named(chain) => Some(*chain), - Chain::Id(id) => NamedChain::try_from(*id).ok(), - } - } - - /// The ID of the chain. - pub const fn id(&self) -> u64 { - match self { - Chain::Named(chain) => *chain as u64, - Chain::Id(id) => *id, - } - } - - /// Returns the address of the public DNS node list for the given chain. - /// - /// See also - pub fn public_dns_network_protocol(self) -> Option { - use NamedChain as C; - const DNS_PREFIX: &str = "enrtree://AKA3AM6LPBYEUDMVNU3BSVQJ5AD45Y7YPOHJLEF6W26QOE4VTUDPE@"; - - let named: NamedChain = self.try_into().ok()?; - if matches!(named, C::Mainnet | C::Goerli | C::Sepolia | C::Ropsten | C::Rinkeby) { - return Some(format!("{DNS_PREFIX}all.{}.ethdisco.net", named.as_ref().to_lowercase())); - } - None +impl fmt::Debug for Chain { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("Chain::")?; + self.kind().fmt(f) } } impl Default for Chain { + #[inline] fn default() -> Self { - NamedChain::Mainnet.into() + Self::from_named(NamedChain::Mainnet) } } impl From for Chain { + #[inline] fn from(id: NamedChain) -> Self { - Chain::Named(id) + Self::from_named(id) } } impl From for Chain { + #[inline] fn from(id: u64) -> Self { - NamedChain::try_from(id).map(Chain::Named).unwrap_or_else(|_| Chain::Id(id)) + Self::from_id(id) } } impl From for u64 { - fn from(c: Chain) -> Self { - match c { - Chain::Named(c) => c as u64, - Chain::Id(id) => id, - } + #[inline] + fn from(chain: Chain) -> Self { + chain.id() } } impl TryFrom for NamedChain { type Error = >::Error; + #[inline] fn try_from(chain: Chain) -> Result { - match chain { - Chain::Named(chain) => Ok(chain), - Chain::Id(id) => id.try_into(), + match *chain.kind() { + ChainKind::Named(chain) => Ok(chain), + ChainKind::Id(id) => id.try_into(), } } } @@ -154,47 +77,38 @@ impl FromStr for Chain { fn from_str(s: &str) -> Result { if let Ok(chain) = NamedChain::from_str(s) { - Ok(Chain::Named(chain)) + Ok(Self::from_named(chain)) } else { - s.parse::().map(Chain::Id) + s.parse::().map(Self::from_id) } } } impl fmt::Display for Chain { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Chain::Named(chain) => chain.fmt(f), - Chain::Id(id) => { - if let Ok(chain) = NamedChain::try_from(*id) { - chain.fmt(f) - } else { - id.fmt(f) - } - } + match self.kind() { + ChainKind::Named(chain) => chain.fmt(f), + ChainKind::Id(id) => id.fmt(f), } } } #[cfg(feature = "rlp")] impl alloy_rlp::Encodable for Chain { + #[inline] fn encode(&self, out: &mut dyn alloy_rlp::BufMut) { - match self { - Self::Named(chain) => chain.encode(out), - Self::Id(id) => id.encode(out), - } + self.id().encode(out) } + #[inline] fn length(&self) -> usize { - match self { - Self::Named(chain) => chain.length(), - Self::Id(id) => id.length(), - } + self.id().length() } } #[cfg(feature = "rlp")] impl alloy_rlp::Decodable for Chain { + #[inline] fn decode(buf: &mut &[u8]) -> alloy_rlp::Result { u64::decode(buf).map(Self::from) } @@ -206,10 +120,10 @@ impl<'a> arbitrary::Arbitrary<'a> for Chain { if u.ratio(1, 2)? { let chain = u.int_in_range(0..=(NamedChain::COUNT - 1))?; - return Ok(Chain::Named(NamedChain::iter().nth(chain).expect("in range"))); + return Ok(Self::from_named(NamedChain::iter().nth(chain).expect("in range"))); } - Ok(Self::Id(u64::arbitrary(u)?)) + Ok(Self::from_id(u64::arbitrary(u)?)) } } @@ -224,41 +138,198 @@ impl proptest::arbitrary::Arbitrary for Chain { fn arbitrary_with((): ()) -> Self::Strategy { use proptest::prelude::*; prop_oneof![ - any::().prop_map(move |sel| Chain::Named(sel.select(NamedChain::iter()))), - any::().prop_map(Chain::Id), + any::().prop_map(move |sel| Self::from_named(sel.select(NamedChain::iter()))), + any::().prop_map(Self::from_id), ] } } +impl Chain { + #[allow(non_snake_case)] + #[doc(hidden)] + #[deprecated(since = "0.1.0", note = "use `Self::from_named()` instead")] + #[inline] + pub const fn Named(named: NamedChain) -> Self { + Self::from_named(named) + } + + #[allow(non_snake_case)] + #[doc(hidden)] + #[deprecated(since = "0.1.0", note = "use `Self::from_id()` instead")] + #[inline] + pub const fn Id(id: u64) -> Self { + Self::from_id_unchecked(id) + } + + /// Creates a new [`Chain`] by wrapping a [`NamedChain`]. + #[inline] + pub const fn from_named(named: NamedChain) -> Self { + Self(ChainKind::Named(named)) + } + + /// Creates a new [`Chain`] by wrapping a [`NamedChain`]. + #[inline] + pub fn from_id(id: u64) -> Self { + if let Ok(named) = NamedChain::try_from(id) { + Self::from_named(named) + } else { + Self::from_id_unchecked(id) + } + } + + /// Creates a new [`Chain`] from the given ID, without checking if an associated [`NamedChain`] + /// exists. + /// + /// This is discouraged, as other methods assume that the chain ID is not known, but it is not + /// unsafe. + #[inline] + pub const fn from_id_unchecked(id: u64) -> Self { + Self(ChainKind::Id(id)) + } + + /// Returns the mainnet chain. + #[inline] + pub const fn mainnet() -> Self { + Self::from_named(NamedChain::Mainnet) + } + + /// Returns the goerli chain. + #[inline] + pub const fn goerli() -> Self { + Self::from_named(NamedChain::Goerli) + } + + /// Returns the sepolia chain. + #[inline] + pub const fn sepolia() -> Self { + Self::from_named(NamedChain::Sepolia) + } + + /// Returns the holesky chain. + #[inline] + pub const fn holesky() -> Self { + Self::from_named(NamedChain::Holesky) + } + + /// Returns the optimism goerli chain. + #[inline] + pub const fn optimism_goerli() -> Self { + Self::from_named(NamedChain::OptimismGoerli) + } + + /// Returns the optimism mainnet chain. + #[inline] + pub const fn optimism_mainnet() -> Self { + Self::from_named(NamedChain::Optimism) + } + + /// Returns the base goerli chain. + #[inline] + pub const fn base_goerli() -> Self { + Self::from_named(NamedChain::BaseGoerli) + } + + /// Returns the base mainnet chain. + #[inline] + pub const fn base_mainnet() -> Self { + Self::from_named(NamedChain::Base) + } + + /// Returns the dev chain. + #[inline] + pub const fn dev() -> Self { + Self::from_named(NamedChain::Dev) + } + + /// Returns the kind of this chain. + #[inline] + pub const fn kind(&self) -> &ChainKind { + &self.0 + } + + /// Returns the kind of this chain. + #[inline] + pub const fn into_kind(self) -> ChainKind { + self.0 + } + + /// Returns true if the chain contains Optimism configuration. + #[inline] + pub fn is_optimism(self) -> bool { + matches!( + self.kind(), + ChainKind::Named( + NamedChain::Optimism + | NamedChain::OptimismGoerli + | NamedChain::OptimismKovan + | NamedChain::OptimismSepolia + | NamedChain::Base + | NamedChain::BaseGoerli + ) + ) + } + + /// Attempts to convert the chain into a named chain. + #[inline] + pub const fn named(self) -> Option { + match *self.kind() { + ChainKind::Named(named) => Some(named), + ChainKind::Id(_) => None, + } + } + + /// The ID of the chain. + #[inline] + pub const fn id(self) -> u64 { + match *self.kind() { + ChainKind::Named(named) => named as u64, + ChainKind::Id(id) => id, + } + } + + /// Returns the address of the public DNS node list for the given chain. + /// + /// See also + pub fn public_dns_network_protocol(self) -> Option { + use NamedChain as C; + const DNS_PREFIX: &str = "enrtree://AKA3AM6LPBYEUDMVNU3BSVQJ5AD45Y7YPOHJLEF6W26QOE4VTUDPE@"; + + let named: NamedChain = self.try_into().ok()?; + if matches!(named, C::Mainnet | C::Goerli | C::Sepolia | C::Ropsten | C::Rinkeby) { + return Some(format!("{DNS_PREFIX}all.{}.ethdisco.net", named.as_ref().to_lowercase())); + } + None + } +} + #[cfg(test)] mod tests { use super::*; - use alloc::string::ToString; #[test] fn test_id() { - assert_eq!(Chain::Id(1234).id(), 1234); + assert_eq!(Chain::from_id(1234).id(), 1234); } #[test] fn test_named_id() { - assert_eq!(Chain::Named(NamedChain::Goerli).id(), 5); + assert_eq!(Chain::from_named(NamedChain::Goerli).id(), 5); } #[test] fn test_display_named_chain() { - assert_eq!(Chain::Named(NamedChain::Mainnet).to_string(), "mainnet"); + assert_eq!(Chain::from_named(NamedChain::Mainnet).to_string(), "mainnet"); } #[test] fn test_display_id_chain() { - assert_eq!(Chain::Id(1234).to_string(), "1234"); + assert_eq!(Chain::from_id(1234).to_string(), "1234"); } #[test] fn test_from_str_named_chain() { let result = Chain::from_str("mainnet"); - let expected = Chain::Named(NamedChain::Mainnet); + let expected = Chain::from_named(NamedChain::Mainnet); assert_eq!(result.unwrap(), expected); } @@ -271,14 +342,14 @@ mod tests { #[test] fn test_from_str_id_chain() { let result = Chain::from_str("1234"); - let expected = Chain::Id(1234); + let expected = Chain::from_id(1234); assert_eq!(result.unwrap(), expected); } #[test] fn test_default() { let default = Chain::default(); - let expected = Chain::Named(NamedChain::Mainnet); + let expected = Chain::from_named(NamedChain::Mainnet); assert_eq!(default, expected); } @@ -287,7 +358,7 @@ mod tests { fn test_id_chain_encodable_length() { use alloy_rlp::Encodable; - let chain = Chain::Id(1234); + let chain = Chain::from_id(1234); assert_eq!(chain.length(), 3); }