Skip to main content

alloy_ens/
lib.rs

1#![doc = include_str!("../README.md")]
2#![doc(
3    html_logo_url = "https://raw.githubusercontent.com/alloy-rs/core/main/assets/alloy.jpg",
4    html_favicon_url = "https://raw.githubusercontent.com/alloy-rs/core/main/assets/favicon.ico"
5)]
6#![cfg_attr(not(test), warn(unused_crate_dependencies))]
7#![cfg_attr(docsrs, feature(doc_cfg))]
8
9//! ENS Name resolving utilities.
10
11use alloy_primitives::{address, Address, Keccak256, B256};
12use std::{borrow::Cow, str::FromStr};
13
14/// ENS registry address (`0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e`)
15pub const ENS_ADDRESS: Address = address!("0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e");
16
17/// ENS const for registrar domain
18pub const ENS_REVERSE_REGISTRAR_DOMAIN: &str = "addr.reverse";
19
20#[cfg(feature = "contract")]
21pub use contract::*;
22
23#[cfg(feature = "provider")]
24pub use provider::*;
25
26/// ENS name or Ethereum Address.
27#[derive(Clone, Debug, PartialEq, Eq)]
28pub enum NameOrAddress {
29    /// An ENS Name (format does not get checked)
30    Name(String),
31    /// An Ethereum Address
32    Address(Address),
33}
34
35impl NameOrAddress {
36    /// Resolves the name to an Ethereum Address.
37    #[cfg(feature = "provider")]
38    pub async fn resolve<N: alloy_provider::Network, P: alloy_provider::Provider<N>>(
39        &self,
40        provider: &P,
41    ) -> Result<Address, EnsError> {
42        match self {
43            Self::Name(name) => provider.resolve_name(name).await,
44            Self::Address(addr) => Ok(*addr),
45        }
46    }
47}
48
49impl From<String> for NameOrAddress {
50    fn from(name: String) -> Self {
51        Self::Name(name)
52    }
53}
54
55impl From<&String> for NameOrAddress {
56    fn from(name: &String) -> Self {
57        Self::Name(name.clone())
58    }
59}
60
61impl From<Address> for NameOrAddress {
62    fn from(addr: Address) -> Self {
63        Self::Address(addr)
64    }
65}
66
67impl FromStr for NameOrAddress {
68    type Err = <Address as FromStr>::Err;
69
70    fn from_str(s: &str) -> Result<Self, Self::Err> {
71        match Address::from_str(s) {
72            Ok(addr) => Ok(Self::Address(addr)),
73            Err(err) => {
74                if s.contains('.') {
75                    Ok(Self::Name(s.to_string()))
76                } else {
77                    Err(err)
78                }
79            }
80        }
81    }
82}
83
84#[cfg(feature = "contract")]
85mod contract {
86    use alloy_sol_types::sol;
87
88    // ENS Registry and Resolver contracts.
89    sol! {
90        /// ENS Registry contract.
91        #[sol(rpc)]
92        contract EnsRegistry {
93            /// Returns the resolver for the specified node.
94            function resolver(bytes32 node) view returns (address);
95
96            /// returns the owner of this node
97            function owner(bytes32 node) view returns (address);
98        }
99
100        /// ENS Resolver interface.
101        #[sol(rpc)]
102        contract EnsResolver {
103            /// Returns the address associated with the specified node.
104            function addr(bytes32 node) view returns (address);
105
106            /// Returns the name associated with an ENS node, for reverse records.
107            function name(bytes32 node) view returns (string);
108
109            /// Returns the txt associated with an ENS node
110            function text(bytes32 node,string calldata key) view virtual returns (string memory);
111        }
112
113        /// ENS Reverse Registrar contract
114        #[sol(rpc)]
115        contract ReverseRegistrar {}
116    }
117
118    /// Error type for ENS resolution.
119    #[derive(Debug, thiserror::Error)]
120    pub enum EnsError {
121        /// Failed to get resolver from the ENS registry.
122        #[error("Failed to get resolver from the ENS registry: {0}")]
123        Resolver(alloy_contract::Error),
124        /// Failed to get resolver from the ENS registry.
125        #[error("ENS resolver not found for name {0:?}")]
126        ResolverNotFound(String),
127        /// Failed to get reverse registrar from the ENS registry.
128        #[error("Failed to get reverse registrar from the ENS registry: {0}")]
129        RevRegistrar(alloy_contract::Error),
130        /// Failed to get reverse registrar from the ENS registry.
131        #[error("ENS reverse registrar not found for addr.reverse")]
132        ReverseRegistrarNotFound,
133        /// Failed to lookup ENS name from an address.
134        #[error("Failed to lookup ENS name from an address: {0}")]
135        Lookup(alloy_contract::Error),
136        /// Failed to resolve ENS name to an address.
137        #[error("Failed to resolve ENS name to an address: {0}")]
138        Resolve(alloy_contract::Error),
139        /// Failed to get txt records of ENS name.
140        #[error("Failed to resolve txt record: {0}")]
141        ResolveTxtRecord(alloy_contract::Error),
142    }
143}
144
145#[cfg(feature = "provider")]
146mod provider {
147    use crate::{
148        namehash, reverse_address, EnsError, EnsRegistry, EnsResolver::EnsResolverInstance,
149        ReverseRegistrar::ReverseRegistrarInstance, ENS_ADDRESS, ENS_REVERSE_REGISTRAR_DOMAIN,
150    };
151    use alloy_primitives::{Address, B256};
152    use alloy_provider::{Network, Provider};
153
154    /// Extension trait for ENS contract calls.
155    #[cfg_attr(target_family = "wasm", async_trait::async_trait(?Send))]
156    #[cfg_attr(not(target_family = "wasm"), async_trait::async_trait)]
157    pub trait ProviderEnsExt<N: alloy_provider::Network, P: Provider<N>> {
158        /// Returns the resolver for the specified node. The `&str` is only used for error messages.
159        async fn get_resolver(
160            &self,
161            node: B256,
162            error_name: &str,
163        ) -> Result<EnsResolverInstance<&P, N>, EnsError>;
164
165        /// Returns the reverse registrar for the specified node.
166        async fn get_reverse_registrar(&self) -> Result<ReverseRegistrarInstance<&P, N>, EnsError>;
167
168        /// Performs a forward lookup of an ENS name to an address.
169        async fn resolve_name(&self, name: &str) -> Result<Address, EnsError> {
170            let node = namehash(name);
171            let resolver = self.get_resolver(node, name).await?;
172            let addr = resolver.addr(node).call().await.map_err(EnsError::Resolve)?;
173
174            Ok(addr)
175        }
176
177        /// Performs a reverse lookup of an address to an ENS name.
178        async fn lookup_address(&self, address: &Address) -> Result<String, EnsError> {
179            let name = reverse_address(address);
180            let node = namehash(&name);
181            let resolver = self.get_resolver(node, &name).await?;
182            let name = resolver.name(node).call().await.map_err(EnsError::Lookup)?;
183            Ok(name)
184        }
185
186        /// Performs a txt lookup of an address to an ENS name.
187        async fn lookup_txt(&self, name: &str, key: &str) -> Result<String, EnsError> {
188            let node = namehash(name);
189            let resolver = self.get_resolver(node, name).await?;
190            let txt_value = resolver
191                .text(node, key.to_string())
192                .call()
193                .await
194                .map_err(EnsError::ResolveTxtRecord)?;
195            Ok(txt_value)
196        }
197    }
198
199    #[cfg_attr(target_family = "wasm", async_trait::async_trait(?Send))]
200    #[cfg_attr(not(target_family = "wasm"), async_trait::async_trait)]
201    impl<N, P> ProviderEnsExt<N, P> for P
202    where
203        P: Provider<N>,
204        N: Network,
205    {
206        async fn get_resolver(
207            &self,
208            node: B256,
209            error_name: &str,
210        ) -> Result<EnsResolverInstance<&P, N>, EnsError> {
211            let registry = EnsRegistry::new(ENS_ADDRESS, self);
212            let address = registry.resolver(node).call().await.map_err(EnsError::Resolver)?;
213            if address == Address::ZERO {
214                return Err(EnsError::ResolverNotFound(error_name.to_string()));
215            }
216            Ok(EnsResolverInstance::new(address, self))
217        }
218
219        async fn get_reverse_registrar(&self) -> Result<ReverseRegistrarInstance<&P, N>, EnsError> {
220            let registry = EnsRegistry::new(ENS_ADDRESS, self);
221            let address = registry
222                .owner(namehash(ENS_REVERSE_REGISTRAR_DOMAIN))
223                .call()
224                .await
225                .map_err(EnsError::RevRegistrar)?;
226            if address == Address::ZERO {
227                return Err(EnsError::ReverseRegistrarNotFound);
228            }
229            Ok(ReverseRegistrarInstance::new(address, self))
230        }
231    }
232}
233
234/// Returns the ENS namehash as specified in [EIP-137](https://eips.ethereum.org/EIPS/eip-137)
235pub fn namehash(name: &str) -> B256 {
236    if name.is_empty() {
237        return B256::ZERO;
238    }
239
240    // Remove the variation selector `U+FE0F` if present.
241    const VARIATION_SELECTOR: char = '\u{fe0f}';
242    let name = if name.contains(VARIATION_SELECTOR) {
243        Cow::Owned(name.replace(VARIATION_SELECTOR, ""))
244    } else {
245        Cow::Borrowed(name)
246    };
247
248    // Generate the node starting from the right.
249    // This buffer is `[node @ [u8; 32], label_hash @ [u8; 32]]`.
250    let mut buffer = [0u8; 64];
251    for label in name.rsplit('.') {
252        // node = keccak256([node, keccak256(label)])
253
254        // Hash the label.
255        let mut label_hasher = Keccak256::new();
256        label_hasher.update(label.as_bytes());
257        label_hasher.finalize_into(&mut buffer[32..]);
258
259        // Hash both the node and the label hash, writing into the node.
260        let mut buffer_hasher = Keccak256::new();
261        buffer_hasher.update(buffer.as_slice());
262        buffer_hasher.finalize_into(&mut buffer[..32]);
263    }
264    buffer[..32].try_into().unwrap()
265}
266
267/// Returns the reverse-registrar name of an address.
268pub fn reverse_address(addr: &Address) -> String {
269    format!("{addr:x}.{ENS_REVERSE_REGISTRAR_DOMAIN}")
270}
271
272#[cfg(test)]
273mod test {
274    use super::*;
275    use alloy_primitives::hex;
276
277    fn assert_hex(hash: B256, val: &str) {
278        assert_eq!(hash.0[..], hex::decode(val).unwrap()[..]);
279    }
280
281    #[test]
282    fn test_namehash() {
283        for (name, expected) in &[
284            ("", "0x0000000000000000000000000000000000000000000000000000000000000000"),
285            ("eth", "0x93cdeb708b7545dc668eb9280176169d1c33cfd8ed6f04690a0bcc88a93fc4ae"),
286            ("foo.eth", "0xde9b09fd7c5f901e23a3f19fecc54828e9c848539801e86591bd9801b019f84f"),
287            ("alice.eth", "0x787192fc5378cc32aa956ddfdedbf26b24e8d78e40109add0eea2c1a012c3dec"),
288            ("ret↩️rn.eth", "0x3de5f4c02db61b221e7de7f1c40e29b6e2f07eb48d65bf7e304715cd9ed33b24"),
289        ] {
290            assert_hex(namehash(name), expected);
291        }
292    }
293
294    #[test]
295    fn test_reverse_address() {
296        for (addr, expected) in [
297            (
298                "0x314159265dd8dbb310642f98f50c066173c1259b",
299                "314159265dd8dbb310642f98f50c066173c1259b.addr.reverse",
300            ),
301            (
302                "0x28679A1a632125fbBf7A68d850E50623194A709E",
303                "28679a1a632125fbbf7a68d850e50623194a709e.addr.reverse",
304            ),
305        ] {
306            assert_eq!(reverse_address(&addr.parse().unwrap()), expected, "{addr}");
307        }
308    }
309
310    #[test]
311    fn test_invalid_address() {
312        for addr in [
313            "0x314618",
314            "0x000000000000000000000000000000000000000", // 41
315            "0x00000000000000000000000000000000000000000", // 43
316            "0x28679A1a632125fbBf7A68d850E50623194A709E123", // 44
317        ] {
318            assert!(NameOrAddress::from_str(addr).is_err());
319        }
320    }
321}
322
323#[cfg(all(test, feature = "provider"))]
324mod tests {
325    use super::*;
326    use alloy_primitives::address;
327    use alloy_provider::ProviderBuilder;
328
329    #[tokio::test]
330    async fn test_reverse_registrar_fetching_mainnet() {
331        let provider = ProviderBuilder::new()
332            .connect_http("https://reth-ethereum.ithaca.xyz/rpc".parse().unwrap());
333
334        let res = provider.get_reverse_registrar().await;
335        assert_eq!(*res.unwrap().address(), address!("0xa58E81fe9b61B5c3fE2AFD33CF304c454AbFc7Cb"));
336    }
337
338    #[tokio::test]
339    async fn test_pub_resolver_fetching_mainnet() {
340        let provider = ProviderBuilder::new()
341            .connect_http("https://reth-ethereum.ithaca.xyz/rpc".parse().unwrap());
342
343        let name = "vitalik.eth";
344        let node = namehash(name);
345        let res = provider.get_resolver(node, name).await;
346        assert_eq!(*res.unwrap().address(), address!("0x231b0Ee14048e9dCcD1d247744d114a4EB5E8E63"));
347    }
348    #[tokio::test]
349    async fn test_pub_resolver_text() {
350        let provider = ProviderBuilder::new()
351            .connect_http("http://reth-ethereum.ithaca.xyz/rpc".parse().unwrap());
352
353        let name = "vitalik.eth";
354        let node = namehash(name);
355        let res = provider.get_resolver(node, name).await.unwrap();
356        let txt = res.text(node, "avatar".to_string()).call().await.unwrap();
357        assert_eq!(txt, "https://euc.li/vitalik.eth")
358    }
359
360    #[tokio::test]
361    async fn test_pub_resolver_fetching_txt() {
362        let provider = ProviderBuilder::new()
363            .connect_http("http://reth-ethereum.ithaca.xyz/rpc".parse().unwrap());
364
365        let name = "vitalik.eth";
366        let res = provider.lookup_txt(name, "avatar").await.unwrap();
367        assert_eq!(res, "https://euc.li/vitalik.eth")
368    }
369}