use std::collections::HashMap;
use celes::Country;
use rand::seq::SliceRandom;
use tracing::{debug, info, warn};
use nym_crypto::asymmetric::ed25519;
use nym_sphinx::addressing::clients::Recipient;
use nym_validator_client::nym_api::NymApiClientExt;
use crate::ip_packet_client::discovery::create_nym_api_client;
use crate::{Error, NymNetworkDetails};
#[derive(Debug, Clone, Default)]
pub enum NetworkRequesterSelector {
#[default]
Any,
InCountries(Vec<Country>),
Exact(Box<Recipient>),
}
impl NetworkRequesterSelector {
pub fn any() -> Self {
Self::Any
}
#[allow(clippy::result_large_err)]
pub fn in_countries<I, S>(codes: I) -> Result<Self, Error>
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
let countries = codes
.into_iter()
.map(|c| {
Country::from_alpha2(c.as_ref())
.map_err(|_| Error::InvalidCountryCode(c.as_ref().to_string()))
})
.collect::<Result<Vec<_>, _>>()?;
if countries.is_empty() {
return Err(Error::NoCountriesSpecified);
}
Ok(Self::InCountries(countries))
}
#[allow(clippy::result_large_err)]
pub fn exact(address: impl AsRef<str>) -> Result<Self, Error> {
let recipient = address
.as_ref()
.parse()
.map_err(|_| Error::InvalidRecipientAddress(address.as_ref().to_string()))?;
Ok(Self::Exact(Box::new(recipient)))
}
pub async fn resolve(&self) -> Result<Recipient, Error> {
match self {
Self::Exact(addr) => Ok(**addr),
Self::Any => discover(&[]).await,
Self::InCountries(countries) if countries.is_empty() => {
Err(Error::NoCountriesSpecified)
}
Self::InCountries(countries) => discover(countries).await,
}
}
}
async fn discover(countries: &[Country]) -> Result<Recipient, Error> {
let nym_api_urls = NymNetworkDetails::new_mainnet()
.nym_api_urls
.ok_or(Error::NoNymAPIUrl)?;
let client = create_nym_api_client(nym_api_urls)?;
get_best_network_requester_in(client, countries).await
}
struct NetworkRequesterWithPerformance {
address: Recipient,
identity: ed25519::PublicKey,
performance: u8,
country: Option<Country>,
}
async fn retrieve_network_requesters_with_performance(
client: nym_http_api_client::Client,
) -> Result<Vec<NetworkRequesterWithPerformance>, Error> {
let all_nodes = client
.get_all_described_nodes_v2()
.await?
.into_iter()
.map(|described| (described.ed25519_identity_key(), described))
.collect::<HashMap<_, _>>();
let basic_nodes = client.get_all_basic_nodes_with_metadata().await?.nodes;
let mut requesters = Vec::new();
for node_meta in basic_nodes {
let Some(node) = all_nodes.get(&node_meta.ed25519_identity_pubkey) else {
debug!(
"{} has no described-node record; skipping",
node_meta.ed25519_identity_pubkey
);
continue;
};
let Some(nr_info) = node.description.network_requester.clone() else {
continue;
};
match nr_info.address.parse() {
Ok(parsed_address) => requesters.push(NetworkRequesterWithPerformance {
address: parsed_address,
identity: node_meta.ed25519_identity_pubkey,
performance: node_meta.performance.round_to_integer(),
country: node.description.auxiliary_details.location,
}),
Err(err) => warn!(
"{} advertises an unparseable network requester address {:?}: {err}; skipping",
node_meta.ed25519_identity_pubkey, nr_info.address
),
}
}
Ok(requesters)
}
async fn get_best_network_requester_in(
client: nym_http_api_client::Client,
countries: &[Country],
) -> Result<Recipient, Error> {
let requesters = retrieve_network_requesters_with_performance(client).await?;
let total = requesters.len();
let pool: Vec<NetworkRequesterWithPerformance> = if countries.is_empty() {
requesters
} else {
requesters
.into_iter()
.filter(|nr| match nr.country {
Some(c) => countries
.iter()
.any(|want| want.alpha2.eq_ignore_ascii_case(c.alpha2)),
None => false,
})
.collect()
};
info!(
"Found {} network requesters ({} after country filter)",
total,
pool.len()
);
if pool.is_empty() {
return Err(if countries.is_empty() {
Error::NoGatewayAvailable
} else {
Error::NoGatewayInCountries
});
}
let mut rng = rand::thread_rng();
let selected = pool
.choose_weighted(&mut rng, |nr| nr.performance as f64)
.or_else(|_| pool.choose(&mut rng).ok_or(Error::NoGatewayAvailable))?;
info!(
"Using network requester: {} (Gateway: {}, Country: {:?}, Performance: {:?})",
selected.address,
selected.identity,
selected.country.map(|c| c.alpha2),
selected.performance
);
Ok(selected.address)
}