#![doc = include_str!("../README.md")]
#![doc(
html_logo_url = "https://raw.githubusercontent.com/alloy-rs/core/main/assets/alloy.jpg",
html_favicon_url = "https://raw.githubusercontent.com/alloy-rs/core/main/assets/favicon.ico"
)]
#![cfg_attr(not(test), warn(unused_crate_dependencies))]
#![cfg_attr(docsrs, feature(doc_cfg))]
use alloy_primitives::{address, Address, Keccak256, B256};
use std::{borrow::Cow, str::FromStr};
pub const ENS_ADDRESS: Address = address!("0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e");
pub const UNIVERSAL_RESOLVER_ADDRESS: Address =
address!("0xeeeeeeee14d718c2b47d9923deab1335e144eeee");
pub const ENS_REVERSE_REGISTRAR_DOMAIN: &str = "addr.reverse";
#[cfg(feature = "contract")]
pub use contract::*;
#[cfg(feature = "provider")]
pub use provider::*;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum NameOrAddress {
Name(String),
Address(Address),
}
impl NameOrAddress {
#[cfg(feature = "provider")]
pub async fn resolve<N: alloy_provider::Network, P: alloy_provider::Provider<N>>(
&self,
provider: &P,
) -> Result<Address, EnsError> {
match self {
Self::Name(name) => provider.resolve_name(name).await,
Self::Address(addr) => Ok(*addr),
}
}
}
impl From<String> for NameOrAddress {
fn from(name: String) -> Self {
Self::Name(name)
}
}
impl From<&String> for NameOrAddress {
fn from(name: &String) -> Self {
Self::Name(name.clone())
}
}
impl From<Address> for NameOrAddress {
fn from(addr: Address) -> Self {
Self::Address(addr)
}
}
impl FromStr for NameOrAddress {
type Err = <Address as FromStr>::Err;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match Address::from_str(s) {
Ok(addr) => Ok(Self::Address(addr)),
Err(err) => {
if s.contains('.') {
Ok(Self::Name(s.to_string()))
} else {
Err(err)
}
}
}
}
}
#[cfg(feature = "contract")]
mod contract {
use alloy_sol_types::sol;
sol! {
#[sol(rpc)]
contract EnsRegistry {
function resolver(bytes32 node) view returns (address);
function owner(bytes32 node) view returns (address);
}
#[sol(rpc)]
contract EnsResolver {
function addr(bytes32 node) view returns (address);
function name(bytes32 node) view returns (string);
function text(bytes32 node,string calldata key) view virtual returns (string memory);
}
#[sol(rpc)]
contract UniversalResolver {
function resolve(bytes calldata name, bytes calldata data) external view returns (bytes memory, address);
function reverse(bytes calldata reverseName) external view returns (string memory, address, address, address);
}
#[sol(rpc)]
contract ReverseRegistrar {}
}
#[derive(Debug, thiserror::Error)]
pub enum EnsError {
#[error("Failed to get resolver from the ENS registry: {0}")]
Resolver(alloy_contract::Error),
#[error("ENS resolver not found for name {0:?}")]
ResolverNotFound(String),
#[error("Failed to get reverse registrar from the ENS registry: {0}")]
RevRegistrar(alloy_contract::Error),
#[error("ENS reverse registrar not found for addr.reverse")]
ReverseRegistrarNotFound,
#[error("Failed to lookup ENS name from an address: {0}")]
Lookup(alloy_contract::Error),
#[error("Failed to resolve ENS name to an address: {0}")]
Resolve(alloy_contract::Error),
#[error("Failed to resolve txt record: {0}")]
ResolveTxtRecord(alloy_contract::Error),
}
}
#[cfg(feature = "provider")]
mod provider {
use crate::{
dns_encode, namehash, reverse_address, EnsError, EnsRegistry, EnsResolver,
EnsResolver::EnsResolverInstance, ReverseRegistrar::ReverseRegistrarInstance,
UniversalResolver, ENS_ADDRESS, ENS_REVERSE_REGISTRAR_DOMAIN, UNIVERSAL_RESOLVER_ADDRESS,
};
use alloy_primitives::{Address, Bytes, B256};
use alloy_provider::{Network, Provider};
use alloy_sol_types::SolCall;
#[cfg_attr(target_family = "wasm", async_trait::async_trait(?Send))]
#[cfg_attr(not(target_family = "wasm"), async_trait::async_trait)]
pub trait ProviderEnsExt<N: alloy_provider::Network, P: Provider<N>> {
async fn get_resolver(
&self,
node: B256,
error_name: &str,
) -> Result<EnsResolverInstance<&P, N>, EnsError>;
async fn get_reverse_registrar(&self) -> Result<ReverseRegistrarInstance<&P, N>, EnsError>;
async fn resolve_name(&self, name: &str) -> Result<Address, EnsError>;
async fn lookup_address(&self, address: &Address) -> Result<String, EnsError>;
async fn lookup_txt(&self, name: &str, key: &str) -> Result<String, EnsError>;
}
#[cfg_attr(target_family = "wasm", async_trait::async_trait(?Send))]
#[cfg_attr(not(target_family = "wasm"), async_trait::async_trait)]
impl<N, P> ProviderEnsExt<N, P> for P
where
P: Provider<N>,
N: Network,
{
async fn get_resolver(
&self,
node: B256,
error_name: &str,
) -> Result<EnsResolverInstance<&P, N>, EnsError> {
let registry = EnsRegistry::new(ENS_ADDRESS, self);
let address = registry.resolver(node).call().await.map_err(EnsError::Resolver)?;
if address == Address::ZERO {
return Err(EnsError::ResolverNotFound(error_name.to_string()));
}
Ok(EnsResolverInstance::new(address, self))
}
async fn get_reverse_registrar(&self) -> Result<ReverseRegistrarInstance<&P, N>, EnsError> {
let registry = EnsRegistry::new(ENS_ADDRESS, self);
let address = registry
.owner(namehash(ENS_REVERSE_REGISTRAR_DOMAIN))
.call()
.await
.map_err(EnsError::RevRegistrar)?;
if address == Address::ZERO {
return Err(EnsError::ReverseRegistrarNotFound);
}
Ok(ReverseRegistrarInstance::new(address, self))
}
async fn resolve_name(&self, name: &str) -> Result<Address, EnsError> {
let dns_name = dns_encode(name);
let node = namehash(name);
let addr_call = EnsResolver::addrCall { node };
let call_data = Bytes::from(EnsResolver::addrCall::abi_encode(&addr_call));
let ur = UniversalResolver::new(UNIVERSAL_RESOLVER_ADDRESS, self);
let result = ur
.resolve(Bytes::from(dns_name), call_data)
.call()
.await
.map_err(EnsError::Resolve)?;
let result_bytes = result._0;
if result_bytes.len() < 32 {
return Err(EnsError::ResolverNotFound(name.to_string()));
}
let addr = Address::from_slice(&result_bytes[result_bytes.len() - 20..]);
Ok(addr)
}
async fn lookup_address(&self, address: &Address) -> Result<String, EnsError> {
let name = reverse_address(address);
let node = namehash(&name);
let resolver = self.get_resolver(node, &name).await?;
let name = resolver.name(node).call().await.map_err(EnsError::Lookup)?;
Ok(name)
}
async fn lookup_txt(&self, name: &str, key: &str) -> Result<String, EnsError> {
let node = namehash(name);
let resolver = self.get_resolver(node, name).await?;
let txt_value = resolver
.text(node, key.to_string())
.call()
.await
.map_err(EnsError::ResolveTxtRecord)?;
Ok(txt_value)
}
}
}
pub fn namehash(name: &str) -> B256 {
if name.is_empty() {
return B256::ZERO;
}
const VARIATION_SELECTOR: char = '\u{fe0f}';
let name = if name.contains(VARIATION_SELECTOR) {
Cow::Owned(name.replace(VARIATION_SELECTOR, ""))
} else {
Cow::Borrowed(name)
};
let mut buffer = [0u8; 64];
for label in name.rsplit('.') {
let mut label_hasher = Keccak256::new();
label_hasher.update(label.as_bytes());
label_hasher.finalize_into(&mut buffer[32..]);
let mut buffer_hasher = Keccak256::new();
buffer_hasher.update(buffer.as_slice());
buffer_hasher.finalize_into(&mut buffer[..32]);
}
buffer[..32].try_into().unwrap()
}
pub fn dns_encode(name: &str) -> Vec<u8> {
let mut result = Vec::with_capacity(name.len() + 2);
for label in name.split('.') {
result.push(label.len() as u8);
result.extend_from_slice(label.as_bytes());
}
result.push(0);
result
}
pub fn reverse_address(addr: &Address) -> String {
format!("{addr:x}.{ENS_REVERSE_REGISTRAR_DOMAIN}")
}
#[cfg(test)]
mod test {
use super::*;
use alloy_primitives::hex;
fn assert_hex(hash: B256, val: &str) {
assert_eq!(hash.0[..], hex::decode(val).unwrap()[..]);
}
#[test]
fn test_namehash() {
for (name, expected) in &[
("", "0x0000000000000000000000000000000000000000000000000000000000000000"),
("eth", "0x93cdeb708b7545dc668eb9280176169d1c33cfd8ed6f04690a0bcc88a93fc4ae"),
("foo.eth", "0xde9b09fd7c5f901e23a3f19fecc54828e9c848539801e86591bd9801b019f84f"),
("alice.eth", "0x787192fc5378cc32aa956ddfdedbf26b24e8d78e40109add0eea2c1a012c3dec"),
("ret↩️rn.eth", "0x3de5f4c02db61b221e7de7f1c40e29b6e2f07eb48d65bf7e304715cd9ed33b24"),
] {
assert_hex(namehash(name), expected);
}
}
#[test]
fn test_dns_encode() {
assert_eq!(dns_encode("eth"), vec![3, b'e', b't', b'h', 0]);
assert_eq!(
dns_encode("vitalik.eth"),
vec![7, b'v', b'i', b't', b'a', b'l', b'i', b'k', 3, b'e', b't', b'h', 0]
);
}
#[test]
fn test_reverse_address() {
for (addr, expected) in [
(
"0x314159265dd8dbb310642f98f50c066173c1259b",
"314159265dd8dbb310642f98f50c066173c1259b.addr.reverse",
),
(
"0x28679A1a632125fbBf7A68d850E50623194A709E",
"28679a1a632125fbbf7a68d850e50623194a709e.addr.reverse",
),
] {
assert_eq!(reverse_address(&addr.parse().unwrap()), expected, "{addr}");
}
}
#[test]
fn test_invalid_address() {
for addr in [
"0x314618",
"0x000000000000000000000000000000000000000", "0x00000000000000000000000000000000000000000", "0x28679A1a632125fbBf7A68d850E50623194A709E123", ] {
assert!(NameOrAddress::from_str(addr).is_err());
}
}
}
#[cfg(all(test, feature = "provider"))]
mod tests {
use super::*;
use alloy_primitives::address;
use alloy_provider::ProviderBuilder;
#[tokio::test]
async fn test_reverse_registrar_fetching_mainnet() {
let provider = ProviderBuilder::new()
.connect_http("https://reth-ethereum.ithaca.xyz/rpc".parse().unwrap());
let res = provider.get_reverse_registrar().await;
assert_eq!(*res.unwrap().address(), address!("0xa58E81fe9b61B5c3fE2AFD33CF304c454AbFc7Cb"));
}
#[tokio::test]
async fn test_pub_resolver_fetching_mainnet() {
let provider = ProviderBuilder::new()
.connect_http("https://reth-ethereum.ithaca.xyz/rpc".parse().unwrap());
let name = "vitalik.eth";
let node = namehash(name);
let res = provider.get_resolver(node, name).await;
assert_eq!(*res.unwrap().address(), address!("0x231b0Ee14048e9dCcD1d247744d114a4EB5E8E63"));
}
#[tokio::test]
async fn test_resolve_name_via_universal_resolver() {
let provider = ProviderBuilder::new()
.connect_http("https://reth-ethereum.ithaca.xyz/rpc".parse().unwrap());
let addr = provider.resolve_name("ur.integration-tests.eth").await.unwrap();
assert_eq!(addr, address!("0x2222222222222222222222222222222222222222"));
}
#[tokio::test]
async fn test_lookup_address_via_universal_resolver() {
let provider = ProviderBuilder::new()
.connect_http("https://reth-ethereum.ithaca.xyz/rpc".parse().unwrap());
let name = provider
.lookup_address(&address!("0xeE9eeaAB0Bb7D9B969D701f6f8212609EDeA252E"))
.await
.unwrap();
assert_eq!(name, "devrel.enslabs.eth");
}
#[tokio::test]
async fn test_lookup_txt_via_universal_resolver() {
let provider = ProviderBuilder::new()
.connect_http("https://reth-ethereum.ithaca.xyz/rpc".parse().unwrap());
let avatar = provider.lookup_txt("integration-tests.eth", "avatar").await.unwrap();
assert_eq!(
avatar,
"https://raw.githubusercontent.com/ensdomains/resolution-tests/refs/heads/main/assets/avatar.svg"
);
}
#[tokio::test]
async fn test_pub_resolver_text() {
let provider = ProviderBuilder::new()
.connect_http("http://reth-ethereum.ithaca.xyz/rpc".parse().unwrap());
let name = "vitalik.eth";
let node = namehash(name);
let res = provider.get_resolver(node, name).await.unwrap();
let txt = res.text(node, "avatar".to_string()).call().await.unwrap();
assert_eq!(txt, "https://euc.li/vitalik.eth")
}
#[tokio::test]
async fn test_pub_resolver_fetching_txt() {
let provider = ProviderBuilder::new()
.connect_http("http://reth-ethereum.ithaca.xyz/rpc".parse().unwrap());
let name = "vitalik.eth";
let res = provider.lookup_txt(name, "avatar").await.unwrap();
assert_eq!(res, "https://euc.li/vitalik.eth")
}
}