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
9use alloy_primitives::{address, Address, Keccak256, B256};
12use std::{borrow::Cow, str::FromStr};
13
14pub const ENS_ADDRESS: Address = address!("0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e");
16
17pub const UNIVERSAL_RESOLVER_ADDRESS: Address =
23 address!("0xeeeeeeee14d718c2b47d9923deab1335e144eeee");
24
25pub 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#[derive(Clone, Debug, PartialEq, Eq)]
40pub enum NameOrAddress {
41 Name(String),
43 Address(Address),
45}
46
47impl NameOrAddress {
48 #[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 sol! {
102 #[sol(rpc)]
104 contract EnsRegistry {
105 function resolver(bytes32 node) view returns (address);
107
108 function owner(bytes32 node) view returns (address);
110 }
111
112 #[sol(rpc)]
114 contract EnsResolver {
115 function addr(bytes32 node) view returns (address);
117
118 function name(bytes32 node) view returns (string);
120
121 function text(bytes32 node,string calldata key) view virtual returns (string memory);
123 }
124
125 #[sol(rpc)]
136 contract UniversalResolver {
137 function resolve(bytes calldata name, bytes calldata data) external view returns (bytes memory, address);
139
140 function reverse(bytes calldata reverseName) external view returns (string memory, address, address, address);
145 }
146
147 #[sol(rpc)]
149 contract ReverseRegistrar {}
150 }
151
152 #[derive(Debug, thiserror::Error)]
154 pub enum EnsError {
155 #[error("Failed to get resolver from the ENS registry: {0}")]
157 Resolver(alloy_contract::Error),
158 #[error("ENS resolver not found for name {0:?}")]
160 ResolverNotFound(String),
161 #[error("Failed to get reverse registrar from the ENS registry: {0}")]
163 RevRegistrar(alloy_contract::Error),
164 #[error("ENS reverse registrar not found for addr.reverse")]
166 ReverseRegistrarNotFound,
167 #[error("Failed to lookup ENS name from an address: {0}")]
169 Lookup(alloy_contract::Error),
170 #[error("Failed to resolve ENS name to an address: {0}")]
172 Resolve(alloy_contract::Error),
173 #[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 #[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 async fn get_resolver(
200 &self,
201 node: B256,
202 error_name: &str,
203 ) -> Result<EnsResolverInstance<&P, N>, EnsError>;
204
205 async fn get_reverse_registrar(&self) -> Result<ReverseRegistrarInstance<&P, N>, EnsError>;
207
208 async fn resolve_name(&self, name: &str) -> Result<Address, EnsError>;
216
217 async fn lookup_address(&self, address: &Address) -> Result<String, EnsError>;
223
224 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
303pub fn namehash(name: &str) -> B256 {
308 if name.is_empty() {
309 return B256::ZERO;
310 }
311
312 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 let mut buffer = [0u8; 64];
323 for label in name.rsplit('.') {
324 let mut label_hasher = Keccak256::new();
328 label_hasher.update(label.as_bytes());
329 label_hasher.finalize_into(&mut buffer[32..]);
330
331 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
339pub 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
371pub 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", "0x00000000000000000000000000000000000000000", "0x28679A1a632125fbBf7A68d850E50623194A709E123", ] {
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}