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