From cd706aa67e1fe3e6386a3722a5b5b43e27cf7234 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Tue, 24 Sep 2024 18:23:08 +0100 Subject: [PATCH] extracted common functionalities and implemented remaining skimmed routes --- nym-api/nym-api-requests/src/nym_nodes.rs | 21 + nym-api/src/node_describe_cache/mod.rs | 6 + .../src/node_describe_cache/query_helpers.rs | 2 +- .../nym_nodes/handlers/unstable/helpers.rs | 52 +- .../src/nym_nodes/handlers/unstable/mod.rs | 14 +- .../nym_nodes/handlers/unstable/skimmed.rs | 630 ++++++++---------- nym-api/src/support/http/state.rs | 6 +- 7 files changed, 367 insertions(+), 364 deletions(-) diff --git a/nym-api/nym-api-requests/src/nym_nodes.rs b/nym-api/nym-api-requests/src/nym_nodes.rs index bb7e9abe6c..27d3e6ba3d 100644 --- a/nym-api/nym-api-requests/src/nym_nodes.rs +++ b/nym-api/nym-api-requests/src/nym_nodes.rs @@ -6,6 +6,7 @@ use crate::pagination::{PaginatedResponse, Pagination}; use nym_crypto::asymmetric::ed25519::serde_helpers::bs58_ed25519_pubkey; use nym_crypto::asymmetric::x25519::serde_helpers::bs58_x25519_pubkey; use nym_crypto::asymmetric::{ed25519, x25519}; +use nym_mixnet_contract_common::nym_node::Role; use nym_mixnet_contract_common::reward_params::Performance; use nym_mixnet_contract_common::NodeId; use serde::{Deserialize, Serialize}; @@ -90,6 +91,26 @@ pub enum NodeRole { Inactive, } +impl NodeRole { + pub fn is_inactive(&self) -> bool { + matches!(self, NodeRole::Inactive) + } +} + +impl From> for NodeRole { + fn from(role: Option) -> Self { + match role { + Some(Role::EntryGateway) => NodeRole::EntryGateway, + Some(Role::Layer1) => NodeRole::Mixnode { layer: 1 }, + Some(Role::Layer2) => NodeRole::Mixnode { layer: 2 }, + Some(Role::Layer3) => NodeRole::Mixnode { layer: 3 }, + Some(Role::ExitGateway) => NodeRole::ExitGateway, + Some(Role::Standby) => NodeRole::Standby, + None => NodeRole::Inactive, + } + } +} + #[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] pub struct BasicEntryInformation { pub hostname: Option, diff --git a/nym-api/src/node_describe_cache/mod.rs b/nym-api/src/node_describe_cache/mod.rs index 28d231639a..e92a3ebe5b 100644 --- a/nym-api/src/node_describe_cache/mod.rs +++ b/nym-api/src/node_describe_cache/mod.rs @@ -162,6 +162,12 @@ impl DescribedNodes { self.nodes.values() } + pub fn all_nym_nodes(&self) -> impl Iterator { + self.nodes + .values() + .filter(|n| n.contract_node_type == DescribedNodeType::NymNode) + } + pub fn mixing_nym_nodes(&self) -> impl Iterator { self.nodes .values() diff --git a/nym-api/src/node_describe_cache/query_helpers.rs b/nym-api/src/node_describe_cache/query_helpers.rs index 3e6c5027b9..1752345f2c 100644 --- a/nym-api/src/node_describe_cache/query_helpers.rs +++ b/nym-api/src/node_describe_cache/query_helpers.rs @@ -12,7 +12,7 @@ use nym_bin_common::build_information::BinaryBuildInformationOwned; use nym_config::defaults::mainnet; use nym_mixnet_contract_common::NodeId; use nym_node_requests::api::client::{NymNodeApiClientError, NymNodeApiClientExt}; -use nym_node_requests::api::v1::node::models::{AuxiliaryDetails, NodeRoles}; +use nym_node_requests::api::v1::node::models::AuxiliaryDetails; use nym_node_requests::api::Client; use pin_project::pin_project; use std::future::Future; diff --git a/nym-api/src/nym_nodes/handlers/unstable/helpers.rs b/nym-api/src/nym_nodes/handlers/unstable/helpers.rs index 60309114d5..4f7a20155e 100644 --- a/nym-api/src/nym_nodes/handlers/unstable/helpers.rs +++ b/nym-api/src/nym_nodes/handlers/unstable/helpers.rs @@ -1,10 +1,60 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use nym_api_requests::models::OffsetDateTimeJsonSchemaWrapper; +use nym_api_requests::models::{ + GatewayBondAnnotated, MalformedNodeBond, MixNodeBondAnnotated, OffsetDateTimeJsonSchemaWrapper, +}; +use nym_api_requests::nym_nodes::{NodeRole, SkimmedNode}; use nym_bin_common::version_checker; +use nym_mixnet_contract_common::reward_params::Performance; use time::OffsetDateTime; +pub(crate) trait LegacyAnnotation { + fn version(&self) -> &str; + + fn performance(&self) -> Performance; + + fn identity(&self) -> &str; + + fn try_to_skimmed_node(&self, role: NodeRole) -> Result; +} + +impl LegacyAnnotation for MixNodeBondAnnotated { + fn version(&self) -> &str { + self.version() + } + + fn performance(&self) -> Performance { + self.node_performance.last_24h + } + + fn identity(&self) -> &str { + self.identity_key() + } + + fn try_to_skimmed_node(&self, role: NodeRole) -> Result { + self.try_to_skimmed_node(role) + } +} + +impl LegacyAnnotation for GatewayBondAnnotated { + fn version(&self) -> &str { + self.version() + } + + fn performance(&self) -> Performance { + self.node_performance.last_24h + } + + fn identity(&self) -> &str { + self.identity() + } + + fn try_to_skimmed_node(&self, role: NodeRole) -> Result { + self.try_to_skimmed_node(role) + } +} + pub(crate) fn refreshed_at( iter: impl IntoIterator, ) -> OffsetDateTimeJsonSchemaWrapper { diff --git a/nym-api/src/nym_nodes/handlers/unstable/mod.rs b/nym-api/src/nym_nodes/handlers/unstable/mod.rs index a3f50283ac..add55ec427 100644 --- a/nym-api/src/nym_nodes/handlers/unstable/mod.rs +++ b/nym-api/src/nym_nodes/handlers/unstable/mod.rs @@ -23,10 +23,9 @@ use crate::nym_nodes::handlers::unstable::full_fat::nodes_detailed; use crate::nym_nodes::handlers::unstable::semi_skimmed::nodes_expanded; use crate::nym_nodes::handlers::unstable::skimmed::{ - deprecated_gateways_basic, deprecated_mixnodes_basic, entry_gateways_basic_active, - entry_gateways_basic_all, exit_gateways_basic_active, exit_gateways_basic_all, - mixnodes_basic_active, mixnodes_basic_all, nodes_basic, nodes_basic_active, - nodes_basic_standby, + entry_gateways_basic_active, entry_gateways_basic_all, exit_gateways_basic_active, + exit_gateways_basic_all, mixnodes_basic_active, mixnodes_basic_all, nodes_basic_active, + nodes_basic_all, }; use crate::support::http::helpers::PaginationRequest; use crate::support::http::state::AppState; @@ -46,9 +45,8 @@ pub(crate) fn nym_node_routes_unstable() -> Router { .nest( "/skimmed", Router::new() - .route("/", get(nodes_basic)) + .route("/", get(nodes_basic_all)) .route("/active", get(nodes_basic_active)) - .route("/standby", get(nodes_basic_standby)) .nest( "/mixnodes", Router::new() @@ -73,8 +71,8 @@ pub(crate) fn nym_node_routes_unstable() -> Router { Router::new().route("/", get(nodes_expanded)), ) .nest("/full-fat", Router::new().route("/", get(nodes_detailed))) - .route("/gateways/skimmed", get(deprecated_gateways_basic)) - .route("/mixnodes/skimmed", get(deprecated_mixnodes_basic)) + .route("/gateways/skimmed", get(skimmed::deprecated_gateways_basic)) + .route("/mixnodes/skimmed", get(skimmed::deprecated_mixnodes_basic)) } #[derive(Debug, Deserialize, utoipa::IntoParams)] diff --git a/nym-api/src/nym_nodes/handlers/unstable/skimmed.rs b/nym-api/src/nym_nodes/handlers/unstable/skimmed.rs index 6904d265cc..df263d2e06 100644 --- a/nym-api/src/nym_nodes/handlers/unstable/skimmed.rs +++ b/nym-api/src/nym_nodes/handlers/unstable/skimmed.rs @@ -1,19 +1,188 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use crate::node_describe_cache::DescribedNodes; use crate::node_status_api::models::{AxumErrorResponse, AxumResult}; -use crate::nym_nodes::handlers::unstable::helpers::{refreshed_at, semver}; +use crate::nym_contract_cache::cache::CachedRewardedSet; +use crate::nym_nodes::handlers::unstable::helpers::{refreshed_at, semver, LegacyAnnotation}; use crate::nym_nodes::handlers::unstable::{NodesParams, NodesParamsWithRole}; +use crate::support::caching::Cache; use crate::support::http::state::AppState; use axum::extract::{Query, State}; use axum::Json; +use nym_api_requests::models::{NodeAnnotation, NymNodeDescription}; use nym_api_requests::nym_nodes::{ CachedNodesResponse, NodeRole, NodeRoleQueryParam, PaginatedCachedNodesResponse, SkimmedNode, }; +use nym_mixnet_contract_common::NodeId; +use std::collections::HashMap; +use std::future::Future; +use tokio::sync::RwLockReadGuard; use tracing::trace; pub type PaginatedSkimmedNodes = AxumResult>>; +/// Given all relevant caches, build part of response for JUST Nym Nodes +fn build_nym_nodes_response<'a, NI>( + rewarded_set: &CachedRewardedSet, + required_semver: &Option, + nym_nodes_subset: NI, + annotations: &HashMap, + active_only: bool, +) -> Vec +where + NI: Iterator + 'a, +{ + let mut nodes = Vec::new(); + for nym_node in nym_nodes_subset { + let node_id = nym_node.node_id; + + // if we have wrong version, ignore + if !semver(required_semver, nym_node.version()) { + continue; + } + + let role: NodeRole = rewarded_set.role(node_id).into(); + + // if the role is inactive, see if our filter allows it + if active_only && role.is_inactive() { + continue; + } + + // honestly, not sure under what exact circumstances this value could be missing, + // but in that case just use 0 performance + let annotation = annotations.get(&node_id).copied().unwrap_or_default(); + + nodes.push(nym_node.to_skimmed_node(role, annotation.last_24h_performance)); + } + nodes +} + +/// Given all relevant caches, add appropriate legacy nodes to the part of the response +fn add_legacy( + nodes: &mut Vec, + required_semver: &Option, + rewarded_set: &CachedRewardedSet, + describe_cache: &DescribedNodes, + annotated_legacy_nodes: &HashMap, + active_only: bool, +) where + LN: LegacyAnnotation, +{ + for (node_id, legacy) in annotated_legacy_nodes.iter() { + // if we have wrong version, ignore + if !semver(required_semver, legacy.version()) { + continue; + } + + let role: NodeRole = rewarded_set.role(*node_id).into(); + + // if the role is inactive, see if our filter allows it + if active_only && role.is_inactive() { + continue; + } + + // if we have self-described info, prefer it over contract data + if let Some(described) = describe_cache.get_node(node_id) { + nodes.push(described.to_skimmed_node(role, legacy.performance())) + } else { + match legacy.try_to_skimmed_node(role) { + Ok(node) => nodes.push(node), + Err(err) => { + let id = legacy.identity(); + trace!("node {id} is malformed: {err}") + } + } + } + } +} + +// hehe, what an abomination, but it's used in multiple different places and I hate copy-pasting code, +// especially if it has multiple loops, etc +async fn build_skimmed_nodes_response<'a, NI, LG, Fut, LN>( + state: &'a AppState, + Query(query_params): Query, + nym_nodes_subset: NI, + annotated_legacy_nodes_getter: LG, + active_only: bool, +) -> PaginatedSkimmedNodes +where + // iterator returning relevant subset of nym-nodes (like mixing nym-nodes, entries, etc.) + NI: Iterator + 'a, + + // async function that returns cache of appropriate legacy nodes (mixnodes or gateways) + LG: Fn(&'a AppState) -> Fut, + Fut: + Future>>, AxumErrorResponse>>, + + // the legacy node (MixNodeBondAnnotated or GatewayBondAnnotated) + LN: LegacyAnnotation + 'a, +{ + // TODO: implement it + let _ = query_params.per_page; + let _ = query_params.page; + let semver_compatibility = query_params.semver_compatibility; + + // 1. get the rewarded set + let rewarded_set = state.rewarded_set().await?; + + // 2. grab all annotations so that we could attach scores to the [nym] nodes + let annotations = state.node_annotations().await?; + + // 3. implicitly grab the relevant described nodes + // (ideally it'd be tied directly to the NI iterator, but I couldn't defeat the compiler) + let describe_cache = state.describe_nodes_cache_data().await?; + + // 4. start building the response + let mut nodes = build_nym_nodes_response( + &rewarded_set, + &semver_compatibility, + nym_nodes_subset, + &annotations, + active_only, + ); + + // 5. if we allow legacy nodes, repeat the procedure for them, otherwise return just nym-nodes + if query_params.no_legacy { + // min of all caches + let refreshed_at = refreshed_at([ + rewarded_set.timestamp(), + annotations.timestamp(), + describe_cache.timestamp(), + ]); + + return Ok(Json(PaginatedCachedNodesResponse::new_full( + refreshed_at, + nodes, + ))); + } + + // 6. grab relevant legacy nodes + // (due to the existence of the legacy endpoints, we already have fully annotated data on them) + let annotated_legacy_nodes = annotated_legacy_nodes_getter(state).await?; + add_legacy( + &mut nodes, + &semver_compatibility, + &rewarded_set, + &describe_cache, + &annotated_legacy_nodes, + active_only, + ); + + // min of all caches + let refreshed_at = refreshed_at([ + rewarded_set.timestamp(), + annotations.timestamp(), + describe_cache.timestamp(), + annotated_legacy_nodes.timestamp(), + ]); + + Ok(Json(PaginatedCachedNodesResponse::new_full( + refreshed_at, + nodes, + ))) +} + /// Deprecated query that gets ALL gateways #[utoipa::path( tag = "Unstable Nym Nodes", @@ -68,6 +237,66 @@ pub(super) async fn deprecated_mixnodes_basic( })) } +async fn nodes_basic( + state: State, + Query(query_params): Query, + active_only: bool, +) -> PaginatedSkimmedNodes { + // unfortunately we have to build the response semi-manually here as we need to add two sources of legacy nodes + + // 1. grab all relevant described nym-nodes + let rewarded_set = state.rewarded_set().await?; + let semver_compatibility = &query_params.semver_compatibility; + + let describe_cache = state.describe_nodes_cache_data().await?; + let all_nym_nodes = describe_cache.all_nym_nodes(); + let annotations = state.node_annotations().await?; + let legacy_mixnodes = state.legacy_mixnode_annotations().await?; + let legacy_gateways = state.legacy_gateways_annotations().await?; + + let mut nodes = build_nym_nodes_response( + &rewarded_set, + semver_compatibility, + all_nym_nodes, + &annotations, + active_only, + ); + + // add legacy gateways to the response + add_legacy( + &mut nodes, + semver_compatibility, + &rewarded_set, + &describe_cache, + &legacy_gateways, + active_only, + ); + + // add legacy mixnodes to the response + add_legacy( + &mut nodes, + semver_compatibility, + &rewarded_set, + &describe_cache, + &legacy_mixnodes, + active_only, + ); + + // min of all caches + let refreshed_at = refreshed_at([ + rewarded_set.timestamp(), + annotations.timestamp(), + describe_cache.timestamp(), + legacy_mixnodes.timestamp(), + legacy_gateways.timestamp(), + ]); + + Ok(Json(PaginatedCachedNodesResponse::new_full( + refreshed_at, + nodes, + ))) +} + /// Return all Nym Nodes and optionally legacy mixnodes/gateways (if `no-legacy` flag is not used) /// that are currently bonded. #[utoipa::path( @@ -80,7 +309,7 @@ pub(super) async fn deprecated_mixnodes_basic( (status = 200, body = PaginatedCachedNodesResponse) ) )] -pub(super) async fn nodes_basic( +pub(super) async fn nodes_basic_all( state: State, Query(query_params): Query, ) -> PaginatedSkimmedNodes { @@ -98,23 +327,9 @@ pub(super) async fn nodes_basic( }; } - // TODO: implement pagination - - let semver = query_params.semver_compatibility; - let no_legacy = query_params.no_legacy; - - // TODO: - /* - - `/v1/unstable/nym-nodes/skimmed` - now works with `exit` parameter - - `/v1/unstable/nym-nodes/skimmed` - introduced `no-legacy` flag to ignore legacy mixnodes/gateways (where applicable) - - `/v1/unstable/nym-nodes/skimmed` - will now return **ALL** nodes if no query parameter is provided - - */ - - Err(AxumErrorResponse::not_implemented()) + nodes_basic(state, Query(query_params.into()), false).await } -// - `/v1/unstable/nym-nodes/skimmed/active` - returns all Nym Nodes **AND** legacy mixnodes **AND** legacy gateways that are currently in the active set, unless `no-legacy` parameter is used /// Return Nym Nodes and optionally legacy mixnodes/gateways (if `no-legacy` flag is not used) /// that are currently bonded and are in the **active set** #[utoipa::path( @@ -129,145 +344,42 @@ pub(super) async fn nodes_basic( )] pub(super) async fn nodes_basic_active( state: State, - query_params: Query, + Query(query_params): Query, ) -> PaginatedSkimmedNodes { - // TODO: implement it - let _ = query_params.per_page; - let _ = query_params.page; - todo!() -} + if let Some(role) = query_params.role { + return match role { + NodeRoleQueryParam::ActiveMixnode => { + mixnodes_basic_active(state, Query(query_params.into())).await + } + NodeRoleQueryParam::EntryGateway => { + entry_gateways_basic_active(state, Query(query_params.into())).await + } + NodeRoleQueryParam::ExitGateway => { + exit_gateways_basic_active(state, Query(query_params.into())).await + } + }; + } -/// Return Nym Nodes and optionally legacy mixnodes/gateways (if `no-legacy` flag is not used) -/// that are currently bonded and are in the **standby set** -#[utoipa::path( - tag = "Unstable Nym Nodes", - get, - params(NodesParamsWithRole), - path = "/skimmed/standby", - context_path = "/v1/unstable/nym-nodes/skimmed", - responses( - (status = 200, body = PaginatedCachedNodesResponse) - ) -)] -pub(super) async fn nodes_basic_standby( - state: State, - query_params: Query, -) -> PaginatedSkimmedNodes { - // TODO: implement it - let _ = query_params.per_page; - let _ = query_params.page; - todo!() + nodes_basic(state, Query(query_params.into()), true).await } async fn mixnodes_basic( state: State, - Query(query_params): Query, + query_params: Query, active_only: bool, ) -> PaginatedSkimmedNodes { - // TODO: implement it - let _ = query_params.per_page; - let _ = query_params.page; - let semver_compatibility = query_params.semver_compatibility; - - // 1. get the rewarded set - let rewarded_set = state.rewarded_set().await?; - - // 2. grab all annotations so that we could attach scores to the [nym] nodes - let annotations = state.node_annotations().await?; - - // 3. grab all relevant described nym-nodes + // 1. grab all relevant described nym-nodes let describe_cache = state.describe_nodes_cache_data().await?; let mixing_nym_nodes = describe_cache.mixing_nym_nodes(); - // 4. start building the response - let mut nodes = Vec::new(); - - for nym_node in mixing_nym_nodes { - let node_id = nym_node.node_id; - - // if this node is not an active mixnode, ignore it - if active_only && !rewarded_set.is_active_mixnode(&node_id) { - continue; - } - - // if we have wrong version, ignore - if !semver(&semver_compatibility, nym_node.version()) { - continue; - } - - let role = match rewarded_set.try_get_mix_layer(&node_id) { - Some(layer) => NodeRole::Mixnode { layer }, - None => NodeRole::Inactive, - }; - - // honestly, not sure under what exact circumstances this value could be missing, - // but in that case just use 0 performance - let annotation = annotations.get(&node_id).copied().unwrap_or_default(); - - nodes.push(nym_node.to_skimmed_node(role, annotation.last_24h_performance)); - } - - // 5. if we allow legacy mixnodes, repeat the procedure for mixnodes, otherwise return just nym-nodes - if query_params.no_legacy { - // min of all caches - let refreshed_at = refreshed_at([ - rewarded_set.timestamp(), - annotations.timestamp(), - describe_cache.timestamp(), - ]); - - return Ok(Json(PaginatedCachedNodesResponse::new_full( - refreshed_at, - nodes, - ))); - } - - // 6. grab all legacy mixnodes - // due to legacy endpoints we already have fully annotated data on them - let annotated_legacy_mixnodes = state.legacy_mixnode_annotations().await?; - - for (mix_id, legacy) in annotated_legacy_mixnodes.iter() { - // if this node is not an active mixnode, ignore it - if active_only && !rewarded_set.is_active_mixnode(mix_id) { - continue; - } - - // if we have wrong version, ignore - if !semver(&semver_compatibility, legacy.version()) { - continue; - } - - let role = match rewarded_set.try_get_mix_layer(mix_id) { - Some(layer) => NodeRole::Mixnode { layer }, - None => NodeRole::Inactive, - }; - - // if we have self-described info, prefer it over contract data - if let Some(described) = describe_cache.get_node(mix_id) { - nodes.push(described.to_skimmed_node(role, legacy.node_performance.last_24h)) - } else { - match legacy.try_to_skimmed_node(role) { - Ok(node) => nodes.push(node), - Err(err) => { - let id = legacy.identity_key(); - trace!("node {id} is malformed: {err}") - } - } - } - } - - // min of all caches - let refreshed_at = refreshed_at([ - rewarded_set.timestamp(), - annotations.timestamp(), - describe_cache.timestamp(), - annotated_legacy_mixnodes.timestamp(), - ]); - - Ok(Json(PaginatedCachedNodesResponse::new_full( - refreshed_at, - nodes, - ))) + build_skimmed_nodes_response( + &state.0, + query_params, + mixing_nym_nodes, + |state| state.legacy_mixnode_annotations(), + active_only, + ) + .await } /// Returns Nym Nodes and optionally legacy mixnodes (if `no-legacy` flag is not used) @@ -310,113 +422,21 @@ pub(super) async fn mixnodes_basic_active( async fn entry_gateways_basic( state: State, - Query(query_params): Query, + query_params: Query, active_only: bool, ) -> PaginatedSkimmedNodes { - // TODO: implement it - let _ = query_params.per_page; - let _ = query_params.page; - let semver_compatibility = query_params.semver_compatibility; - - // 1. get the rewarded set - let rewarded_set = state.rewarded_set().await?; - - // 2. grab all annotations so that we could attach scores to the [nym] nodes - let annotations = state.node_annotations().await?; - - // 3. grab all relevant described nym-nodes + // 1. grab all relevant described nym-nodes let describe_cache = state.describe_nodes_cache_data().await?; - let gateway_capable_nym_nodes = describe_cache.entry_capable_nym_nodes(); + let mixing_nym_nodes = describe_cache.entry_capable_nym_nodes(); - // 4. start building the response - let mut nodes = Vec::new(); - - for nym_node in gateway_capable_nym_nodes { - let node_id = nym_node.node_id; - - // if this node is not an active gateway, ignore it - if active_only && !rewarded_set.is_entry(&node_id) { - continue; - } - - // if we have wrong version, ignore - if !semver(&semver_compatibility, nym_node.version()) { - continue; - } - - let role = match rewarded_set.is_entry(&node_id) { - true => NodeRole::EntryGateway, - false => NodeRole::Inactive, - }; - - // honestly, not sure under what exact circumstances this value could be missing, - // but in that case just use 0 performance - let annotation = annotations.get(&node_id).copied().unwrap_or_default(); - - nodes.push(nym_node.to_skimmed_node(role, annotation.last_24h_performance)); - } - - // 5. if we allow legacy gateways, repeat the procedure for gateways, otherwise return just nym-nodes - if query_params.no_legacy { - // min of all caches - let refreshed_at = refreshed_at([ - rewarded_set.timestamp(), - annotations.timestamp(), - describe_cache.timestamp(), - ]); - - return Ok(Json(PaginatedCachedNodesResponse::new_full( - refreshed_at, - nodes, - ))); - } - - // 6. grab all legacy gateways - // due to legacy endpoints we already have fully annotated data on them - let annotated_legacy_gateways = state.legacy_gateways_annotations().await?; - - for (node_id, legacy) in annotated_legacy_gateways.iter() { - // if this node is not an active gateway, ignore it - if active_only && !rewarded_set.is_entry(&node_id) { - continue; - } - - // if we have wrong version, ignore - if !semver(&semver_compatibility, legacy.version()) { - continue; - } - - let role = match rewarded_set.is_entry(&node_id) { - true => NodeRole::EntryGateway, - false => NodeRole::Inactive, - }; - - // if we have self-described info, prefer it over contract data - if let Some(described) = describe_cache.get_node(node_id) { - nodes.push(described.to_skimmed_node(role, legacy.node_performance.last_24h)) - } else { - match legacy.try_to_skimmed_node(role) { - Ok(node) => nodes.push(node), - Err(err) => { - let id = legacy.gateway_bond.identity(); - trace!("node {id} is malformed: {err}") - } - } - } - } - - // min of all caches - let refreshed_at = refreshed_at([ - rewarded_set.timestamp(), - annotations.timestamp(), - describe_cache.timestamp(), - annotated_legacy_gateways.timestamp(), - ]); - - Ok(Json(PaginatedCachedNodesResponse::new_full( - refreshed_at, - nodes, - ))) + build_skimmed_nodes_response( + &state.0, + query_params, + mixing_nym_nodes, + |state| state.legacy_gateways_annotations(), + active_only, + ) + .await } /// Returns Nym Nodes and optionally legacy gateways (if `no-legacy` flag is not used) @@ -459,113 +479,21 @@ pub(super) async fn entry_gateways_basic_all( async fn exit_gateways_basic( state: State, - Query(query_params): Query, + query_params: Query, active_only: bool, ) -> PaginatedSkimmedNodes { - // TODO: implement it - let _ = query_params.per_page; - let _ = query_params.page; - let semver_compatibility = query_params.semver_compatibility; - - // 1. get the rewarded set - let rewarded_set = state.rewarded_set().await?; - - // 2. grab all annotations so that we could attach scores to the [nym] nodes - let annotations = state.node_annotations().await?; - - // 3. grab all relevant described nym-nodes + // 1. grab all relevant described nym-nodes let describe_cache = state.describe_nodes_cache_data().await?; - let gateway_capable_nym_nodes = describe_cache.exit_capable_nym_nodes(); + let mixing_nym_nodes = describe_cache.exit_capable_nym_nodes(); - // 4. start building the response - let mut nodes = Vec::new(); - - for nym_node in gateway_capable_nym_nodes { - let node_id = nym_node.node_id; - - // if this node is not an active gateway, ignore it - if active_only && !rewarded_set.is_exit(&node_id) { - continue; - } - - // if we have wrong version, ignore - if !semver(&semver_compatibility, nym_node.version()) { - continue; - } - - let role = match rewarded_set.is_exit(&node_id) { - true => NodeRole::ExitGateway, - false => NodeRole::Inactive, - }; - - // honestly, not sure under what exact circumstances this value could be missing, - // but in that case just use 0 performance - let annotation = annotations.get(&node_id).copied().unwrap_or_default(); - - nodes.push(nym_node.to_skimmed_node(role, annotation.last_24h_performance)); - } - - // 5. if we allow legacy gateways, repeat the procedure for gateways, otherwise return just nym-nodes - if query_params.no_legacy { - // min of all caches - let refreshed_at = refreshed_at([ - rewarded_set.timestamp(), - annotations.timestamp(), - describe_cache.timestamp(), - ]); - - return Ok(Json(PaginatedCachedNodesResponse::new_full( - refreshed_at, - nodes, - ))); - } - - // 6. grab all legacy gateways - // due to legacy endpoints we already have fully annotated data on them - let annotated_legacy_gateways = state.legacy_gateways_annotations().await?; - - for (node_id, legacy) in annotated_legacy_gateways.iter() { - // if this node is not an active gateway, ignore it - if active_only && !rewarded_set.is_exit(&node_id) { - continue; - } - - // if we have wrong version, ignore - if !semver(&semver_compatibility, legacy.version()) { - continue; - } - - let role = match rewarded_set.is_exit(&node_id) { - true => NodeRole::ExitGateway, - false => NodeRole::Inactive, - }; - - // if we have self-described info, prefer it over contract data - if let Some(described) = describe_cache.get_node(node_id) { - nodes.push(described.to_skimmed_node(role, legacy.node_performance.last_24h)) - } else { - match legacy.try_to_skimmed_node(role) { - Ok(node) => nodes.push(node), - Err(err) => { - let id = legacy.gateway_bond.identity(); - trace!("node {id} is malformed: {err}") - } - } - } - } - - // min of all caches - let refreshed_at = refreshed_at([ - rewarded_set.timestamp(), - annotations.timestamp(), - describe_cache.timestamp(), - annotated_legacy_gateways.timestamp(), - ]); - - Ok(Json(PaginatedCachedNodesResponse::new_full( - refreshed_at, - nodes, - ))) + build_skimmed_nodes_response( + &state.0, + query_params, + mixing_nym_nodes, + |state| state.legacy_gateways_annotations(), + active_only, + ) + .await } /// Returns Nym Nodes and optionally legacy gateways (if `no-legacy` flag is not used) diff --git a/nym-api/src/support/http/state.rs b/nym-api/src/support/http/state.rs index f0803d6677..c20f18436b 100644 --- a/nym-api/src/support/http/state.rs +++ b/nym-api/src/support/http/state.rs @@ -111,9 +111,9 @@ impl AppState { // handler helpers to easily get data or return error response impl AppState { - pub(crate) async fn describe_nodes_cache_data( - &self, - ) -> Result>, AxumErrorResponse> { + pub(crate) async fn describe_nodes_cache_data<'a>( + &'a self, + ) -> Result>, AxumErrorResponse> { Ok(self.described_nodes_cache().get().await?) }