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 Universal Resolver address on Ethereum Mainnet
18/// (`0xeeeeeeee14d718c2b47d9923deab1335e144eeee`)
19///
20/// The Universal Resolver is the canonical entry point for all ENS resolution.
21pub const UNIVERSAL_RESOLVER_ADDRESS: Address =
22    address!("0xeeeeeeee14d718c2b47d9923deab1335e144eeee");
23
24/// ENS const for registrar domain
25pub const ENS_REVERSE_REGISTRAR_DOMAIN: &str = "addr.reverse";
26
27#[cfg(feature = "contract")]
28pub use contract::*;
29
30#[cfg(feature = "provider")]
31pub use provider::*;
32
33/// ENS name or Ethereum Address.
34#[derive(Clone, Debug, PartialEq, Eq)]
35pub enum NameOrAddress {
36    /// An ENS Name (format does not get checked)
37    Name(String),
38    /// An Ethereum Address
39    Address(Address),
40}
41
42impl NameOrAddress {
43    /// Resolves the name to an Ethereum Address.
44    #[cfg(feature = "provider")]
45    pub async fn resolve<N: alloy_provider::Network, P: alloy_provider::Provider<N>>(
46        &self,
47        provider: &P,
48    ) -> Result<Address, EnsError> {
49        match self {
50            Self::Name(name) => provider.resolve_name(name).await,
51            Self::Address(addr) => Ok(*addr),
52        }
53    }
54}
55
56impl From<String> for NameOrAddress {
57    fn from(name: String) -> Self {
58        Self::Name(name)
59    }
60}
61
62impl From<&String> for NameOrAddress {
63    fn from(name: &String) -> Self {
64        Self::Name(name.clone())
65    }
66}
67
68impl From<Address> for NameOrAddress {
69    fn from(addr: Address) -> Self {
70        Self::Address(addr)
71    }
72}
73
74impl FromStr for NameOrAddress {
75    type Err = <Address as FromStr>::Err;
76
77    fn from_str(s: &str) -> Result<Self, Self::Err> {
78        match Address::from_str(s) {
79            Ok(addr) => Ok(Self::Address(addr)),
80            Err(err) => {
81                if s.contains('.') {
82                    Ok(Self::Name(s.to_string()))
83                } else {
84                    Err(err)
85                }
86            }
87        }
88    }
89}
90
91#[cfg(feature = "contract")]
92mod contract {
93    use alloy_sol_types::sol;
94
95    // ENS Registry and Resolver contracts.
96    sol! {
97        /// ENS Registry contract.
98        #[sol(rpc)]
99        contract EnsRegistry {
100            /// Returns the resolver for the specified node.
101            function resolver(bytes32 node) view returns (address);
102
103            /// returns the owner of this node
104            function owner(bytes32 node) view returns (address);
105        }
106
107        /// ENS Resolver interface.
108        #[sol(rpc)]
109        contract EnsResolver {
110            /// Returns the address associated with the specified node.
111            function addr(bytes32 node) view returns (address);
112
113            /// Returns the name associated with an ENS node, for reverse records.
114            function name(bytes32 node) view returns (string);
115
116            /// Returns the txt associated with an ENS node
117            function text(bytes32 node,string calldata key) view virtual returns (string memory);
118        }
119
120        /// ENS Universal Resolver contract.
121        ///
122        /// The Universal Resolver is the canonical entry point for ENS resolution.
123        /// It handles CCIP-Read (EIP-3668) for offchain/cross-chain names and
124        /// supports all name types including DNS names.
125        #[sol(rpc)]
126        contract UniversalResolver {
127            /// Resolves an ENS name with the given encoded resolver call data.
128            function resolve(bytes calldata name, bytes calldata data) external view returns (bytes memory, address);
129
130            /// Performs reverse resolution for an address.
131            function reverse(bytes calldata reverseName) external view returns (string memory, address, address, address);
132        }
133
134        /// ENS Reverse Registrar contract
135        #[sol(rpc)]
136        contract ReverseRegistrar {}
137    }
138
139    /// Error type for ENS resolution.
140    #[derive(Debug, thiserror::Error)]
141    pub enum EnsError {
142        /// Failed to get resolver from the ENS registry.
143        #[error("Failed to get resolver from the ENS registry: {0}")]
144        Resolver(alloy_contract::Error),
145        /// Failed to get resolver from the ENS registry.
146        #[error("ENS resolver not found for name {0:?}")]
147        ResolverNotFound(String),
148        /// Failed to get reverse registrar from the ENS registry.
149        #[error("Failed to get reverse registrar from the ENS registry: {0}")]
150        RevRegistrar(alloy_contract::Error),
151        /// Failed to get reverse registrar from the ENS registry.
152        #[error("ENS reverse registrar not found for addr.reverse")]
153        ReverseRegistrarNotFound,
154        /// Failed to lookup ENS name from an address.
155        #[error("Failed to lookup ENS name from an address: {0}")]
156        Lookup(alloy_contract::Error),
157        /// Failed to resolve ENS name to an address.
158        #[error("Failed to resolve ENS name to an address: {0}")]
159        Resolve(alloy_contract::Error),
160        /// Failed to get txt records of ENS name.
161        #[error("Failed to resolve txt record: {0}")]
162        ResolveTxtRecord(alloy_contract::Error),
163    }
164}
165
166#[cfg(feature = "provider")]
167mod provider {
168    use crate::{
169        dns_encode, namehash, reverse_address, EnsError, EnsRegistry, EnsResolver,
170        EnsResolver::EnsResolverInstance, ReverseRegistrar::ReverseRegistrarInstance,
171        UniversalResolver, ENS_ADDRESS, ENS_REVERSE_REGISTRAR_DOMAIN, UNIVERSAL_RESOLVER_ADDRESS,
172    };
173    use alloy_primitives::{Address, Bytes, B256};
174    use alloy_provider::{Network, Provider};
175    use alloy_sol_types::SolCall;
176
177    /// Extension trait for ENS contract calls.
178    #[cfg_attr(target_family = "wasm", async_trait::async_trait(?Send))]
179    #[cfg_attr(not(target_family = "wasm"), async_trait::async_trait)]
180    pub trait ProviderEnsExt<N: alloy_provider::Network, P: Provider<N>> {
181        /// Returns the resolver for the specified node. The `&str` is only used for error messages.
182        async fn get_resolver(
183            &self,
184            node: B256,
185            error_name: &str,
186        ) -> Result<EnsResolverInstance<&P, N>, EnsError>;
187
188        /// Returns the reverse registrar for the specified node.
189        async fn get_reverse_registrar(&self) -> Result<ReverseRegistrarInstance<&P, N>, EnsError>;
190
191        /// Performs a forward lookup of an ENS name to an address using the Universal Resolver.
192        async fn resolve_name(&self, name: &str) -> Result<Address, EnsError>;
193
194        /// Performs a reverse lookup of an address to an ENS name.
195        async fn lookup_address(&self, address: &Address) -> Result<String, EnsError>;
196
197        /// Performs a txt lookup of an ENS name.
198        async fn lookup_txt(&self, name: &str, key: &str) -> Result<String, EnsError>;
199    }
200
201    #[cfg_attr(target_family = "wasm", async_trait::async_trait(?Send))]
202    #[cfg_attr(not(target_family = "wasm"), async_trait::async_trait)]
203    impl<N, P> ProviderEnsExt<N, P> for P
204    where
205        P: Provider<N>,
206        N: Network,
207    {
208        async fn get_resolver(
209            &self,
210            node: B256,
211            error_name: &str,
212        ) -> Result<EnsResolverInstance<&P, N>, EnsError> {
213            let registry = EnsRegistry::new(ENS_ADDRESS, self);
214            let address = registry.resolver(node).call().await.map_err(EnsError::Resolver)?;
215            if address == Address::ZERO {
216                return Err(EnsError::ResolverNotFound(error_name.to_string()));
217            }
218            Ok(EnsResolverInstance::new(address, self))
219        }
220
221        async fn get_reverse_registrar(&self) -> Result<ReverseRegistrarInstance<&P, N>, EnsError> {
222            let registry = EnsRegistry::new(ENS_ADDRESS, self);
223            let address = registry
224                .owner(namehash(ENS_REVERSE_REGISTRAR_DOMAIN))
225                .call()
226                .await
227                .map_err(EnsError::RevRegistrar)?;
228            if address == Address::ZERO {
229                return Err(EnsError::ReverseRegistrarNotFound);
230            }
231            Ok(ReverseRegistrarInstance::new(address, self))
232        }
233
234        async fn resolve_name(&self, name: &str) -> Result<Address, EnsError> {
235            let dns_name = dns_encode(name);
236            let node = namehash(name);
237            let addr_call = EnsResolver::addrCall { node };
238            let call_data = Bytes::from(EnsResolver::addrCall::abi_encode(&addr_call));
239
240            let ur = UniversalResolver::new(UNIVERSAL_RESOLVER_ADDRESS, self);
241            let result = ur
242                .resolve(Bytes::from(dns_name), call_data)
243                .call()
244                .await
245                .map_err(EnsError::Resolve)?;
246
247            let result_bytes = result._0;
248            if result_bytes.len() < 32 {
249                return Err(EnsError::ResolverNotFound(name.to_string()));
250            }
251            let addr = Address::from_slice(&result_bytes[result_bytes.len() - 20..]);
252            Ok(addr)
253        }
254
255        async fn lookup_address(&self, address: &Address) -> Result<String, EnsError> {
256            let name = reverse_address(address);
257            let node = namehash(&name);
258            let resolver = self.get_resolver(node, &name).await?;
259            let name = resolver.name(node).call().await.map_err(EnsError::Lookup)?;
260            Ok(name)
261        }
262
263        async fn lookup_txt(&self, name: &str, key: &str) -> Result<String, EnsError> {
264            let node = namehash(name);
265            let resolver = self.get_resolver(node, name).await?;
266            let txt_value = resolver
267                .text(node, key.to_string())
268                .call()
269                .await
270                .map_err(EnsError::ResolveTxtRecord)?;
271            Ok(txt_value)
272        }
273    }
274}
275
276/// Returns the ENS namehash as specified in [EIP-137](https://eips.ethereum.org/EIPS/eip-137)
277pub fn namehash(name: &str) -> B256 {
278    if name.is_empty() {
279        return B256::ZERO;
280    }
281
282    // Remove the variation selector `U+FE0F` if present.
283    const VARIATION_SELECTOR: char = '\u{fe0f}';
284    let name = if name.contains(VARIATION_SELECTOR) {
285        Cow::Owned(name.replace(VARIATION_SELECTOR, ""))
286    } else {
287        Cow::Borrowed(name)
288    };
289
290    // Generate the node starting from the right.
291    // This buffer is `[node @ [u8; 32], label_hash @ [u8; 32]]`.
292    let mut buffer = [0u8; 64];
293    for label in name.rsplit('.') {
294        // node = keccak256([node, keccak256(label)])
295
296        // Hash the label.
297        let mut label_hasher = Keccak256::new();
298        label_hasher.update(label.as_bytes());
299        label_hasher.finalize_into(&mut buffer[32..]);
300
301        // Hash both the node and the label hash, writing into the node.
302        let mut buffer_hasher = Keccak256::new();
303        buffer_hasher.update(buffer.as_slice());
304        buffer_hasher.finalize_into(&mut buffer[..32]);
305    }
306    buffer[..32].try_into().unwrap()
307}
308
309/// Encodes a domain name into DNS wire format as specified in
310/// [RFC 1035](https://datatracker.ietf.org/doc/html/rfc1035).
311///
312/// Each label is prefixed with its length byte, and the name is terminated with a
313/// zero-length label (null byte).
314///
315/// # Examples
316///
317/// ```
318/// use alloy_ens::dns_encode;
319/// assert_eq!(dns_encode("eth"), vec![3, b'e', b't', b'h', 0]);
320/// assert_eq!(
321///     dns_encode("vitalik.eth"),
322///     vec![7, b'v', b'i', b't', b'a', b'l', b'i', b'k', 3, b'e', b't', b'h', 0]
323/// );
324/// ```
325pub fn dns_encode(name: &str) -> Vec<u8> {
326    let mut result = Vec::with_capacity(name.len() + 2);
327    for label in name.split('.') {
328        result.push(label.len() as u8);
329        result.extend_from_slice(label.as_bytes());
330    }
331    result.push(0);
332    result
333}
334
335/// Returns the reverse-registrar name of an address.
336pub fn reverse_address(addr: &Address) -> String {
337    format!("{addr:x}.{ENS_REVERSE_REGISTRAR_DOMAIN}")
338}
339
340#[cfg(test)]
341mod test {
342    use super::*;
343    use alloy_primitives::hex;
344
345    fn assert_hex(hash: B256, val: &str) {
346        assert_eq!(hash.0[..], hex::decode(val).unwrap()[..]);
347    }
348
349    #[test]
350    fn test_namehash() {
351        for (name, expected) in &[
352            ("", "0x0000000000000000000000000000000000000000000000000000000000000000"),
353            ("eth", "0x93cdeb708b7545dc668eb9280176169d1c33cfd8ed6f04690a0bcc88a93fc4ae"),
354            ("foo.eth", "0xde9b09fd7c5f901e23a3f19fecc54828e9c848539801e86591bd9801b019f84f"),
355            ("alice.eth", "0x787192fc5378cc32aa956ddfdedbf26b24e8d78e40109add0eea2c1a012c3dec"),
356            ("ret↩️rn.eth", "0x3de5f4c02db61b221e7de7f1c40e29b6e2f07eb48d65bf7e304715cd9ed33b24"),
357        ] {
358            assert_hex(namehash(name), expected);
359        }
360    }
361
362    #[test]
363    fn test_dns_encode() {
364        assert_eq!(dns_encode("eth"), vec![3, b'e', b't', b'h', 0]);
365        assert_eq!(
366            dns_encode("vitalik.eth"),
367            vec![7, b'v', b'i', b't', b'a', b'l', b'i', b'k', 3, b'e', b't', b'h', 0]
368        );
369    }
370
371    #[test]
372    fn test_reverse_address() {
373        for (addr, expected) in [
374            (
375                "0x314159265dd8dbb310642f98f50c066173c1259b",
376                "314159265dd8dbb310642f98f50c066173c1259b.addr.reverse",
377            ),
378            (
379                "0x28679A1a632125fbBf7A68d850E50623194A709E",
380                "28679a1a632125fbbf7a68d850e50623194a709e.addr.reverse",
381            ),
382        ] {
383            assert_eq!(reverse_address(&addr.parse().unwrap()), expected, "{addr}");
384        }
385    }
386
387    #[test]
388    fn test_invalid_address() {
389        for addr in [
390            "0x314618",
391            "0x000000000000000000000000000000000000000", // 41
392            "0x00000000000000000000000000000000000000000", // 43
393            "0x28679A1a632125fbBf7A68d850E50623194A709E123", // 44
394        ] {
395            assert!(NameOrAddress::from_str(addr).is_err());
396        }
397    }
398}
399
400#[cfg(all(test, feature = "provider"))]
401mod tests {
402    use super::*;
403    use alloy_primitives::address;
404    use alloy_provider::ProviderBuilder;
405
406    #[tokio::test]
407    async fn test_reverse_registrar_fetching_mainnet() {
408        let provider =
409            ProviderBuilder::new().connect_http("https://ethereum.reth.rs/rpc".parse().unwrap());
410
411        let res = provider.get_reverse_registrar().await;
412        assert_eq!(*res.unwrap().address(), address!("0xa58E81fe9b61B5c3fE2AFD33CF304c454AbFc7Cb"));
413    }
414
415    #[tokio::test]
416    async fn test_pub_resolver_fetching_mainnet() {
417        let provider =
418            ProviderBuilder::new().connect_http("https://ethereum.reth.rs/rpc".parse().unwrap());
419
420        let name = "vitalik.eth";
421        let node = namehash(name);
422        let res = provider.get_resolver(node, name).await;
423        assert_eq!(*res.unwrap().address(), address!("0x231b0Ee14048e9dCcD1d247744d114a4EB5E8E63"));
424    }
425
426    #[tokio::test]
427    async fn test_resolve_name_via_universal_resolver() {
428        let provider =
429            ProviderBuilder::new().connect_http("https://ethereum.reth.rs/rpc".parse().unwrap());
430
431        let addr = provider.resolve_name("ur.integration-tests.eth").await.unwrap();
432        assert_eq!(addr, address!("0x2222222222222222222222222222222222222222"));
433    }
434
435    #[tokio::test]
436    async fn test_lookup_address_via_universal_resolver() {
437        let provider =
438            ProviderBuilder::new().connect_http("https://ethereum.reth.rs/rpc".parse().unwrap());
439
440        let name = provider
441            .lookup_address(&address!("0xeE9eeaAB0Bb7D9B969D701f6f8212609EDeA252E"))
442            .await
443            .unwrap();
444        assert_eq!(name, "devrel.enslabs.eth");
445    }
446
447    #[tokio::test]
448    async fn test_lookup_txt_via_universal_resolver() {
449        let provider =
450            ProviderBuilder::new().connect_http("https://ethereum.reth.rs/rpc".parse().unwrap());
451
452        let avatar = provider.lookup_txt("integration-tests.eth", "avatar").await.unwrap();
453        assert_eq!(
454            avatar,
455            "https://raw.githubusercontent.com/ensdomains/resolution-tests/refs/heads/main/assets/avatar.svg"
456        );
457    }
458
459    #[tokio::test]
460    async fn test_pub_resolver_text() {
461        let provider =
462            ProviderBuilder::new().connect_http("http://ethereum.reth.rs/rpc".parse().unwrap());
463
464        let name = "vitalik.eth";
465        let node = namehash(name);
466        let res = provider.get_resolver(node, name).await.unwrap();
467        let txt = res.text(node, "avatar".to_string()).call().await.unwrap();
468        assert_eq!(txt, "https://euc.li/vitalik.eth")
469    }
470
471    #[tokio::test]
472    async fn test_pub_resolver_fetching_txt() {
473        let provider =
474            ProviderBuilder::new().connect_http("http://ethereum.reth.rs/rpc".parse().unwrap());
475
476        let name = "vitalik.eth";
477        let res = provider.lookup_txt(name, "avatar").await.unwrap();
478        assert_eq!(res, "https://euc.li/vitalik.eth")
479    }
480}