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 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#[derive(Clone, Debug, PartialEq, Eq)]
28pub enum NameOrAddress {
29 Name(String),
31 Address(Address),
33}
34
35impl NameOrAddress {
36 #[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 sol! {
90 #[sol(rpc)]
92 contract EnsRegistry {
93 function resolver(bytes32 node) view returns (address);
95
96 function owner(bytes32 node) view returns (address);
98 }
99
100 #[sol(rpc)]
102 contract EnsResolver {
103 function addr(bytes32 node) view returns (address);
105
106 function name(bytes32 node) view returns (string);
108
109 function text(bytes32 node,string calldata key) view virtual returns (string memory);
111 }
112
113 #[sol(rpc)]
115 contract ReverseRegistrar {}
116 }
117
118 #[derive(Debug, thiserror::Error)]
120 pub enum EnsError {
121 #[error("Failed to get resolver from the ENS registry: {0}")]
123 Resolver(alloy_contract::Error),
124 #[error("ENS resolver not found for name {0:?}")]
126 ResolverNotFound(String),
127 #[error("Failed to get reverse registrar from the ENS registry: {0}")]
129 RevRegistrar(alloy_contract::Error),
130 #[error("ENS reverse registrar not found for addr.reverse")]
132 ReverseRegistrarNotFound,
133 #[error("Failed to lookup ENS name from an address: {0}")]
135 Lookup(alloy_contract::Error),
136 #[error("Failed to resolve ENS name to an address: {0}")]
138 Resolve(alloy_contract::Error),
139 #[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 #[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 async fn get_resolver(
160 &self,
161 node: B256,
162 error_name: &str,
163 ) -> Result<EnsResolverInstance<&P, N>, EnsError>;
164
165 async fn get_reverse_registrar(&self) -> Result<ReverseRegistrarInstance<&P, N>, EnsError>;
167
168 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 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 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
234pub fn namehash(name: &str) -> B256 {
236 if name.is_empty() {
237 return B256::ZERO;
238 }
239
240 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 let mut buffer = [0u8; 64];
251 for label in name.rsplit('.') {
252 let mut label_hasher = Keccak256::new();
256 label_hasher.update(label.as_bytes());
257 label_hasher.finalize_into(&mut buffer[32..]);
258
259 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
267pub 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", "0x00000000000000000000000000000000000000000", "0x28679A1a632125fbBf7A68d850E50623194A709E123", ] {
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}