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 =
22 address!("0xeeeeeeee14d718c2b47d9923deab1335e144eeee");
23
24pub 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#[derive(Clone, Debug, PartialEq, Eq)]
35pub enum NameOrAddress {
36 Name(String),
38 Address(Address),
40}
41
42impl NameOrAddress {
43 #[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 sol! {
97 #[sol(rpc)]
99 contract EnsRegistry {
100 function resolver(bytes32 node) view returns (address);
102
103 function owner(bytes32 node) view returns (address);
105 }
106
107 #[sol(rpc)]
109 contract EnsResolver {
110 function addr(bytes32 node) view returns (address);
112
113 function name(bytes32 node) view returns (string);
115
116 function text(bytes32 node,string calldata key) view virtual returns (string memory);
118 }
119
120 #[sol(rpc)]
126 contract UniversalResolver {
127 function resolve(bytes calldata name, bytes calldata data) external view returns (bytes memory, address);
129
130 function reverse(bytes calldata reverseName) external view returns (string memory, address, address, address);
132 }
133
134 #[sol(rpc)]
136 contract ReverseRegistrar {}
137 }
138
139 #[derive(Debug, thiserror::Error)]
141 pub enum EnsError {
142 #[error("Failed to get resolver from the ENS registry: {0}")]
144 Resolver(alloy_contract::Error),
145 #[error("ENS resolver not found for name {0:?}")]
147 ResolverNotFound(String),
148 #[error("Failed to get reverse registrar from the ENS registry: {0}")]
150 RevRegistrar(alloy_contract::Error),
151 #[error("ENS reverse registrar not found for addr.reverse")]
153 ReverseRegistrarNotFound,
154 #[error("Failed to lookup ENS name from an address: {0}")]
156 Lookup(alloy_contract::Error),
157 #[error("Failed to resolve ENS name to an address: {0}")]
159 Resolve(alloy_contract::Error),
160 #[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 #[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 async fn get_resolver(
183 &self,
184 node: B256,
185 error_name: &str,
186 ) -> Result<EnsResolverInstance<&P, N>, EnsError>;
187
188 async fn get_reverse_registrar(&self) -> Result<ReverseRegistrarInstance<&P, N>, EnsError>;
190
191 async fn resolve_name(&self, name: &str) -> Result<Address, EnsError>;
193
194 async fn lookup_address(&self, address: &Address) -> Result<String, EnsError>;
196
197 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
276pub fn namehash(name: &str) -> B256 {
278 if name.is_empty() {
279 return B256::ZERO;
280 }
281
282 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 let mut buffer = [0u8; 64];
293 for label in name.rsplit('.') {
294 let mut label_hasher = Keccak256::new();
298 label_hasher.update(label.as_bytes());
299 label_hasher.finalize_into(&mut buffer[32..]);
300
301 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
309pub 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
335pub 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", "0x00000000000000000000000000000000000000000", "0x28679A1a632125fbBf7A68d850E50623194A709E123", ] {
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}