use std::time::Duration;
use alloy_ens::ProviderEnsExt as _;
use alloy_provider::ProviderBuilder;
use tokio::runtime::Runtime;
use crate::error::{Result, XmtpError};
use crate::resolve::Resolver;
const RPC_TIMEOUT: Duration = Duration::from_secs(30);
pub const DEFAULT_RPC: &str = "https://eth.llamarpc.com";
pub struct EnsResolver {
rt: Runtime,
rpc_url: url::Url,
}
impl std::fmt::Debug for EnsResolver {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("EnsResolver")
.field("rpc_url", &self.rpc_url.as_str())
.finish_non_exhaustive()
}
}
impl EnsResolver {
pub fn mainnet() -> Result<Self> {
Self::new(DEFAULT_RPC)
}
pub fn new(rpc_url: &str) -> Result<Self> {
let rpc_url: url::Url = rpc_url
.parse()
.map_err(|e| XmtpError::InvalidArgument(format!("bad RPC URL: {e}")))?;
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.map_err(|e| XmtpError::Resolution(e.to_string()))?;
Ok(Self { rt, rpc_url })
}
}
impl Resolver for EnsResolver {
fn resolve(&self, name: &str) -> Result<String> {
let provider = ProviderBuilder::new().connect_http(self.rpc_url.clone());
let addr = self.rt.block_on(async {
tokio::time::timeout(RPC_TIMEOUT, provider.resolve_name(name))
.await
.map_err(|_| XmtpError::Resolution(format!("{name}: timeout")))?
.map_err(|e| XmtpError::Resolution(format!("{name}: {e}")))
})?;
Ok(addr.to_string().to_lowercase())
}
fn reverse_resolve(&self, address: &str) -> Result<Option<String>> {
let addr: alloy_primitives::Address = address
.parse()
.map_err(|e| XmtpError::Resolution(format!("{address}: {e}")))?;
let provider = ProviderBuilder::new().connect_http(self.rpc_url.clone());
self.rt.block_on(async {
match tokio::time::timeout(RPC_TIMEOUT, provider.lookup_address(&addr)).await {
Ok(Ok(name)) => Ok(Some(name)),
Ok(Err(_)) => Ok(None),
Err(_) => Err(XmtpError::Resolution(format!("{address}: timeout"))),
}
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[ignore = "requires network access to Ethereum RPC"]
fn probe_rpc_endpoints() {
let rpcs = [
("cloudflare", "https://cloudflare-eth.com"),
("llamarpc", "https://eth.llamarpc.com"),
("publicnode", "https://ethereum-rpc.publicnode.com"),
];
for (label, url) in rpcs {
let resolver = EnsResolver::new(url).expect("create resolver");
let t1 = std::time::Instant::now();
let r1 = resolver.resolve("vitalik.eth");
eprintln!("[{label}] resolve: {r1:?} ({:.1?})", t1.elapsed());
let t2 = std::time::Instant::now();
let r2 = resolver.reverse_resolve("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045");
eprintln!("[{label}] reverse: {r2:?} ({:.1?})", t2.elapsed());
}
}
#[test]
#[ignore = "requires network access to Ethereum RPC"]
fn smoke_resolve() {
let resolver = EnsResolver::mainnet().expect("create resolver");
let fwd = resolver.resolve("qntx.eth");
eprintln!("forward: {fwd:?}");
assert!(fwd.is_ok(), "forward failed: {fwd:?}");
let rev = resolver.reverse_resolve("0xE350Ef4E8557a3e2a24D11327d9F25B382Ac93Cb");
eprintln!("reverse: {rev:?}");
assert!(rev.is_ok(), "reverse failed: {rev:?}");
assert_eq!(rev.unwrap().as_deref(), Some("qntx.eth"));
}
}