From 44fad40b39b29f745bf10b6becfb9bda29a662e0 Mon Sep 17 00:00:00 2001 From: abhijeetbhagat Date: Thu, 1 May 2025 12:58:30 +0530 Subject: [PATCH] feat: add ens crate from foundry --- crates/ens/CHANGELOG.md | 6 + crates/ens/Cargo.toml | 31 +++++ crates/ens/README.md | 3 + crates/ens/src/ens.rs | 247 ++++++++++++++++++++++++++++++++++++++++ crates/ens/src/lib.rs | 9 ++ 5 files changed, 296 insertions(+) create mode 100644 crates/ens/CHANGELOG.md create mode 100644 crates/ens/Cargo.toml create mode 100644 crates/ens/README.md create mode 100644 crates/ens/src/ens.rs create mode 100644 crates/ens/src/lib.rs diff --git a/crates/ens/CHANGELOG.md b/crates/ens/CHANGELOG.md new file mode 100644 index 00000000000..751d2d4e5f3 --- /dev/null +++ b/crates/ens/CHANGELOG.md @@ -0,0 +1,6 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). \ No newline at end of file diff --git a/crates/ens/Cargo.toml b/crates/ens/Cargo.toml new file mode 100644 index 00000000000..5f246677db2 --- /dev/null +++ b/crates/ens/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "alloy-ens" +description = "Ethereum Name Service utilities" + +version.workspace = true +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +exclude.workspace = true + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = [ + "-Zunstable-options", + "--generate-link-to-definition", + "--show-type-layout", +] + +[lints] +workspace = true + +[dependencies] +alloy-contract.workspace = true +alloy-primitives.workspace = true +alloy-provider.workspace = true +alloy-sol-types.workspace = true +async-trait.workspace = true +thiserror.workspace = true diff --git a/crates/ens/README.md b/crates/ens/README.md new file mode 100644 index 00000000000..3bff44e9af8 --- /dev/null +++ b/crates/ens/README.md @@ -0,0 +1,3 @@ +# alloy-ens + +Ethereum Name Service utilities like namehash, forward & reverse lookups. diff --git a/crates/ens/src/ens.rs b/crates/ens/src/ens.rs new file mode 100644 index 00000000000..da9d3864726 --- /dev/null +++ b/crates/ens/src/ens.rs @@ -0,0 +1,247 @@ +//! ENS Name resolving utilities. + +#![allow(missing_docs)] + +use self::EnsResolver::EnsResolverInstance; +use alloy_primitives::{address, Address, Keccak256, B256}; +use alloy_provider::{Network, Provider}; +use alloy_sol_types::sol; +use async_trait::async_trait; +use std::{borrow::Cow, str::FromStr}; + +// ENS Registry and Resolver contracts. +sol! { + /// ENS Registry contract. + #[sol(rpc)] + contract EnsRegistry { + /// Returns the resolver for the specified node. + function resolver(bytes32 node) view returns (address); + } + + /// ENS Resolver interface. + #[sol(rpc)] + contract EnsResolver { + /// Returns the address associated with the specified node. + function addr(bytes32 node) view returns (address); + + /// Returns the name associated with an ENS node, for reverse records. + function name(bytes32 node) view returns (string); + } +} + +/// ENS registry address (`0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e`) +pub const ENS_ADDRESS: Address = address!("0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e"); + +pub const ENS_REVERSE_REGISTRAR_DOMAIN: &str = "addr.reverse"; + +/// Error type for ENS resolution. +#[derive(Debug, thiserror::Error)] +pub enum EnsError { + /// Failed to get resolver from the ENS registry. + #[error("Failed to get resolver from the ENS registry: {0}")] + Resolver(alloy_contract::Error), + /// Failed to get resolver from the ENS registry. + #[error("ENS resolver not found for name {0:?}")] + ResolverNotFound(String), + /// Failed to lookup ENS name from an address. + #[error("Failed to lookup ENS name from an address: {0}")] + Lookup(alloy_contract::Error), + /// Failed to resolve ENS name to an address. + #[error("Failed to resolve ENS name to an address: {0}")] + Resolve(alloy_contract::Error), +} + +/// ENS name or Ethereum Address. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum NameOrAddress { + /// An ENS Name (format does not get checked) + Name(String), + /// An Ethereum Address + Address(Address), +} + +impl NameOrAddress { + /// Resolves the name to an Ethereum Address. + pub async fn resolve>( + &self, + provider: &P, + ) -> Result { + match self { + Self::Name(name) => provider.resolve_name(name).await, + Self::Address(addr) => Ok(*addr), + } + } +} + +impl From 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
for NameOrAddress { + fn from(addr: Address) -> Self { + Self::Address(addr) + } +} + +impl FromStr for NameOrAddress { + type Err =
::Err; + + fn from_str(s: &str) -> Result { + 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) + } + } + } + } +} + +/// Extension trait for ENS contract calls. +#[async_trait] +pub trait ProviderEnsExt> { + /// Returns the resolver for the specified node. The `&str` is only used for error messages. + async fn get_resolver( + &self, + node: B256, + error_name: &str, + ) -> Result, EnsError>; + + /// Performs a forward lookup of an ENS name to an address. + async fn resolve_name(&self, name: &str) -> Result { + let node = namehash(name); + let resolver = self.get_resolver(node, name).await?; + let addr = resolver.addr(node).call().await.map_err(EnsError::Resolve)?; + + Ok(addr) + } + + /// Performs a reverse lookup of an address to an ENS name. + async fn lookup_address(&self, address: &Address) -> Result { + 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_trait] +impl ProviderEnsExt for P +where + P: Provider, + N: Network, +{ + async fn get_resolver( + &self, + node: B256, + error_name: &str, + ) -> Result, 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)) + } +} + +/// Returns the ENS namehash as specified in [EIP-137](https://eips.ethereum.org/EIPS/eip-137) +pub fn namehash(name: &str) -> B256 { + if name.is_empty() { + return B256::ZERO; + } + + // Remove the variation selector `U+FE0F` if present. + const VARIATION_SELECTOR: char = '\u{fe0f}'; + let name = if name.contains(VARIATION_SELECTOR) { + Cow::Owned(name.replace(VARIATION_SELECTOR, "")) + } else { + Cow::Borrowed(name) + }; + + // Generate the node starting from the right. + // This buffer is `[node @ [u8; 32], label_hash @ [u8; 32]]`. + let mut buffer = [0u8; 64]; + for label in name.rsplit('.') { + // node = keccak256([node, keccak256(label)]) + + // Hash the label. + let mut label_hasher = Keccak256::new(); + label_hasher.update(label.as_bytes()); + label_hasher.finalize_into(&mut buffer[32..]); + + // Hash both the node and the label hash, writing into the node. + let mut buffer_hasher = Keccak256::new(); + buffer_hasher.update(buffer.as_slice()); + buffer_hasher.finalize_into(&mut buffer[..32]); + } + buffer[..32].try_into().unwrap() +} + +/// Returns the reverse-registrar name of an address. +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_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", // 41 + "0x00000000000000000000000000000000000000000", // 43 + "0x28679A1a632125fbBf7A68d850E50623194A709E123", // 44 + ] { + assert!(NameOrAddress::from_str(addr).is_err()); + } + } +} diff --git a/crates/ens/src/lib.rs b/crates/ens/src/lib.rs new file mode 100644 index 00000000000..fa326b6ddc1 --- /dev/null +++ b/crates/ens/src/lib.rs @@ -0,0 +1,9 @@ +#![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, doc_auto_cfg))] + +pub mod ens;