Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 86 additions & 18 deletions crates/name_resolver/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ use async_trait::async_trait;
use primitives::chain::Chain;
use primitives::name::{NameProvider, NameRecord};

use crate::error::NameError;

#[async_trait]
pub trait NameClient {
async fn resolve(&self, name: &str, chain: Chain) -> Result<String, Box<dyn Error + Send + Sync>>;
Expand Down Expand Up @@ -44,23 +46,89 @@ impl Client {
}

pub async fn resolve(&self, name: &str, chain: Chain) -> Result<NameRecord, Box<dyn Error + Send + Sync>> {
let name_prefix = name.split('.').clone().next_back().unwrap_or_default();
for provider in self.providers.iter() {
if provider.chains().contains(&chain) && provider.domains().contains(&name_prefix) {
match provider.resolve(name, chain).await {
Ok(address) => {
let record = NameRecord {
provider: provider.provider().as_ref().to_string(),
address,
name: name.to_string(),
chain,
};
return Ok(record);
}
Err(e) => return Err(e),
}
}
}
Err(format!("No provider found for name: {name}").into())
let provider = self.matched_provider(name, chain)?;
let address = provider.resolve(name, chain).await?;

Ok(NameRecord {
provider: provider.provider().as_ref().to_string(),
address,
name: name.to_string(),
chain,
})
}

fn matched_provider(&self, name: &str, chain: Chain) -> Result<&(dyn NameClient + Send + Sync), Box<dyn Error + Send + Sync>> {
self.providers
.iter()
.enumerate()
.filter(|(_, provider)| provider.chains().contains(&chain))
.filter_map(|(index, provider)| {
provider
.domains()
.iter()
.filter_map(|domain| domain_match_len(name, domain))
.max()
.map(|match_len| (match_len, index, provider.as_ref()))
})
.max_by(|left, right| left.0.cmp(&right.0).then(right.1.cmp(&left.1)))
.map(|(_, _, provider)| provider)
.ok_or_else(|| NameError::new(format!("No provider found for name: {name}")).into())
}
}

fn domain_match_len(name: &str, domain: &str) -> Option<usize> {
if domain == "*" {
return Some(0);
}

if name.eq_ignore_ascii_case(domain) {
Some(domain.len())
} else if name.len() > domain.len() {
let offset = name.len() - domain.len();
let has_dot = name.as_bytes().get(offset - 1).is_some_and(|byte| *byte == b'.');
let is_suffix_match = name.get(offset..).is_some_and(|suffix| suffix.eq_ignore_ascii_case(domain));

if has_dot && is_suffix_match { Some(domain.len()) } else { None }
} else {
None
}
}

#[cfg(test)]
mod tests {
use crate::testkit::TestProvider;
use primitives::name::NameProvider;

use super::Client;
use primitives::chain::Chain;

#[tokio::test]
async fn test_resolve_prefers_longer_domain_match() {
let client = Client::new(vec![
TestProvider::new(NameProvider::Ens, vec!["*"], vec![Chain::Base], Ok("0x0000000000000000000000000000000000000001")),
TestProvider::new(
NameProvider::Basenames,
vec!["base.eth"],
vec![Chain::Base],
Ok("0x0000000000000000000000000000000000000002"),
),
]);

let record = client.resolve("alice.base.eth", Chain::Base).await.unwrap();

assert_eq!(record.provider, NameProvider::Basenames.as_ref());
assert_eq!(record.address, "0x0000000000000000000000000000000000000002");
}

#[tokio::test]
async fn test_resolve_returns_more_specific_provider_error() {
let client = Client::new(vec![
TestProvider::new(NameProvider::Ens, vec!["*"], vec![Chain::Base], Ok("0x0000000000000000000000000000000000000003")),
TestProvider::new(NameProvider::Basenames, vec!["base.eth"], vec![Chain::Base], Err("failed")),
]);

let error = client.resolve("alice.base.eth", Chain::Base).await.err().unwrap();

assert_eq!(error.to_string(), "failed");
}
}
2 changes: 1 addition & 1 deletion crates/name_resolver/src/ens/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ impl NameClient for ENSClient {
}

fn domains(&self) -> Vec<&'static str> {
vec!["eth"]
vec!["eth", "com", "xyz", "dev"]
}

fn chains(&self) -> Vec<Chain> {
Expand Down
19 changes: 19 additions & 0 deletions crates/name_resolver/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
use std::error::Error;
use std::fmt::{Display, Formatter, Result as FmtResult};

#[derive(Debug)]
pub struct NameError(String);

impl NameError {
pub fn new(message: impl Into<String>) -> Self {
Self(message.into())
}
}

impl Display for NameError {
fn fmt(&self, formatter: &mut Formatter<'_>) -> FmtResult {
write!(formatter, "{}", self.0)
}
}

impl Error for NameError {}
3 changes: 3 additions & 0 deletions crates/name_resolver/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ pub mod client;
pub mod codec;
pub mod did;
pub mod ens;
pub mod error;
pub mod eths;
pub mod hyperliquid;
pub mod icns;
Expand All @@ -16,6 +17,8 @@ pub mod lens;
pub mod sns;
pub mod spaceid;
pub mod suins;
#[cfg(test)]
pub mod testkit;
pub mod ton;
pub mod ton_codec;
pub mod ud;
Expand Down
53 changes: 53 additions & 0 deletions crates/name_resolver/src/testkit.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
use std::error::Error;

use async_trait::async_trait;
use primitives::chain::Chain;
use primitives::name::NameProvider;

use crate::client::NameClient;
use crate::error::NameError;

pub struct TestProvider {
provider: NameProvider,
domains: Vec<&'static str>,
chains: Vec<Chain>,
response: Result<String, &'static str>,
}

impl TestProvider {
pub fn new(provider: NameProvider, domains: Vec<&'static str>, chains: Vec<Chain>, response: Result<&'static str, &'static str>) -> Box<dyn NameClient + Send + Sync> {
let response = match response {
Ok(address) => Ok(address.to_string()),
Err(error) => Err(error),
};

Box::new(Self {
provider,
domains,
chains,
response,
})
}
}

#[async_trait]
impl NameClient for TestProvider {
async fn resolve(&self, _name: &str, _chain: Chain) -> Result<String, Box<dyn Error + Send + Sync>> {
match &self.response {
Ok(address) => Ok(address.clone()),
Err(error) => Err(Box::new(NameError::new(error.to_string()))),
}
}

fn provider(&self) -> NameProvider {
self.provider.clone()
}

fn domains(&self) -> Vec<&'static str> {
self.domains.clone()
}

fn chains(&self) -> Vec<Chain> {
self.chains.clone()
}
}
8 changes: 8 additions & 0 deletions crates/name_resolver/tests/integration_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ mod tests {
assert_eq!(address.unwrap(), "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045")
}

#[tokio::test]
async fn test_resolver_ens_imported_name() {
let nodes = get_nodes_for_chain(Chain::Ethereum);
let client = name_resolver::client::Client::new(vec![Box::new(ENSClient::new(nodes[0].url.clone()))]);
let address = client.resolve("farcaster.xyz", Chain::Ethereum).await.unwrap().address;
assert_eq!(address, "0xF12E89805E10d96c0CDf22da88aED361eD9329cA");
}

#[tokio::test]
async fn test_resolve_basenames() {
let nodes = get_nodes_for_chain(Chain::Base);
Expand Down
2 changes: 1 addition & 1 deletion crates/primitives/src/name.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ pub struct NameRecord {
pub provider: String,
}

#[derive(Debug, Serialize, AsRefStr, EnumString)]
#[derive(Clone, Debug, Serialize, AsRefStr, EnumString)]
#[typeshare(swift = "Sendable")]
#[serde(rename_all = "lowercase")]
#[strum(serialize_all = "lowercase")]
Expand Down
Loading