split nym-nodes http handlers
This commit is contained in:
+1
-1
@@ -1,2 +1,2 @@
|
||||
#!/bin/sh
|
||||
sqlite3 -init settings.sql /Users/jedrzej/workspace/nym/target/debug/build/nym-api-6517e259cadeb663/out/nym-api-example.sqlite
|
||||
sqlite3 -init settings.sql /Users/jedrzej/workspace/nym/target/debug/build/nym-api-3a0aa50356183aba/out/nym-api-example.sqlite
|
||||
@@ -19,7 +19,7 @@ use crate::support::http::state::force_refresh::ForcedRefresh;
|
||||
use crate::support::http::state::AppState;
|
||||
use crate::support::nyxd::Client;
|
||||
use crate::support::storage::NymApiStorage;
|
||||
use crate::unstable_routes::account::cache::AddressInfoCache;
|
||||
use crate::unstable_routes::v1::account::cache::AddressInfoCache;
|
||||
use async_trait::async_trait;
|
||||
use axum::Router;
|
||||
use axum_test::http::StatusCode;
|
||||
|
||||
@@ -24,7 +24,6 @@ use tower_http::compression::CompressionLayer;
|
||||
use utoipa::{IntoParams, ToSchema};
|
||||
|
||||
pub(crate) mod legacy;
|
||||
pub(crate) mod unstable;
|
||||
|
||||
pub(crate) fn nym_node_routes() -> Router<AppState> {
|
||||
Router::new()
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use nym_api_requests::models::{
|
||||
GatewayBondAnnotated, MalformedNodeBond, MixNodeBondAnnotated, OffsetDateTimeJsonSchemaWrapper,
|
||||
};
|
||||
use nym_api_requests::nym_nodes::{NodeRole, SkimmedNode};
|
||||
use nym_mixnet_contract_common::reward_params::Performance;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
pub(crate) trait LegacyAnnotation {
|
||||
fn performance(&self) -> Performance;
|
||||
|
||||
fn identity(&self) -> &str;
|
||||
|
||||
fn try_to_skimmed_node(&self, role: NodeRole) -> Result<SkimmedNode, MalformedNodeBond>;
|
||||
}
|
||||
|
||||
impl LegacyAnnotation for MixNodeBondAnnotated {
|
||||
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<SkimmedNode, MalformedNodeBond> {
|
||||
self.try_to_skimmed_node(role)
|
||||
}
|
||||
}
|
||||
|
||||
impl LegacyAnnotation for GatewayBondAnnotated {
|
||||
fn performance(&self) -> Performance {
|
||||
self.node_performance.last_24h
|
||||
}
|
||||
|
||||
fn identity(&self) -> &str {
|
||||
self.identity()
|
||||
}
|
||||
|
||||
fn try_to_skimmed_node(&self, role: NodeRole) -> Result<SkimmedNode, MalformedNodeBond> {
|
||||
self.try_to_skimmed_node(role)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn refreshed_at(
|
||||
iter: impl IntoIterator<Item = OffsetDateTime>,
|
||||
) -> OffsetDateTimeJsonSchemaWrapper {
|
||||
iter.into_iter()
|
||||
.min()
|
||||
.unwrap_or(OffsetDateTime::UNIX_EPOCH)
|
||||
.into()
|
||||
}
|
||||
@@ -1,184 +0,0 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
//! All routes/nodes are split into three tiers:
|
||||
//!
|
||||
//! `/skimmed`
|
||||
//! - used by clients
|
||||
//! - returns the very basic information for routing purposes
|
||||
//!
|
||||
//! `/semi-skimmed`
|
||||
//! - used by other nodes/VPN
|
||||
//! - returns more additional information such noise keys
|
||||
//!
|
||||
//! `/full-fat`
|
||||
//! - used by explorers, et al.
|
||||
//! - returns almost everything there is about the nodes
|
||||
//!
|
||||
//! There's also additional split based on the role:
|
||||
//! - `?role` => filters based on the specific role (mixnode/gateway/(in the future: entry/exit))
|
||||
//! - `/mixnodes/<tier>` => only returns mixnode role data
|
||||
//! - `/gateway/<tier>` => only returns (entry) gateway role data
|
||||
|
||||
use crate::node_status_api::models::{AxumErrorResponse, AxumResult};
|
||||
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::{
|
||||
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;
|
||||
use axum::extract::{Query, State};
|
||||
use axum::routing::{get, post};
|
||||
use axum::{Json, Router};
|
||||
use nym_api_requests::nym_nodes::{
|
||||
NodeRoleQueryParam, NodesByAddressesRequestBody, NodesByAddressesResponse,
|
||||
};
|
||||
use nym_http_api_common::{FormattedResponse, Output, OutputParams};
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
use tower_http::compression::CompressionLayer;
|
||||
|
||||
pub(crate) mod full_fat;
|
||||
mod helpers;
|
||||
pub(crate) mod semi_skimmed;
|
||||
pub(crate) mod skimmed;
|
||||
|
||||
#[allow(deprecated)]
|
||||
pub(crate) fn routes() -> Router<AppState> {
|
||||
Router::new()
|
||||
.nest(
|
||||
"/skimmed",
|
||||
Router::new()
|
||||
.route("/", get(nodes_basic_all))
|
||||
.route("/active", get(nodes_basic_active))
|
||||
.nest(
|
||||
"/mixnodes",
|
||||
Router::new()
|
||||
.route("/active", get(mixnodes_basic_active))
|
||||
.route("/all", get(mixnodes_basic_all)),
|
||||
)
|
||||
.nest(
|
||||
"/entry-gateways",
|
||||
Router::new()
|
||||
.route("/active", get(entry_gateways_basic_active))
|
||||
.route("/all", get(entry_gateways_basic_all)),
|
||||
)
|
||||
.nest(
|
||||
"/exit-gateways",
|
||||
Router::new()
|
||||
.route("/active", get(exit_gateways_basic_active))
|
||||
.route("/all", get(exit_gateways_basic_all)),
|
||||
),
|
||||
)
|
||||
.nest(
|
||||
"/semi-skimmed",
|
||||
Router::new().route("/", get(nodes_expanded)),
|
||||
)
|
||||
.nest("/full-fat", Router::new().route("/", get(nodes_detailed)))
|
||||
.route("/gateways/skimmed", get(skimmed::deprecated_gateways_basic))
|
||||
.route("/mixnodes/skimmed", get(skimmed::deprecated_mixnodes_basic))
|
||||
.route("/by-addresses", post(nodes_by_addresses))
|
||||
.layer(CompressionLayer::new())
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, utoipa::IntoParams)]
|
||||
struct NodesParamsWithRole {
|
||||
#[param(inline)]
|
||||
role: Option<NodeRoleQueryParam>,
|
||||
|
||||
#[allow(dead_code)]
|
||||
semver_compatibility: Option<String>,
|
||||
no_legacy: Option<bool>,
|
||||
page: Option<u32>,
|
||||
per_page: Option<u32>,
|
||||
|
||||
// Identifier for the current epoch of the topology state. When sent by a client we can check if
|
||||
// the client already knows about the latest topology state, allowing a `no-updates` response
|
||||
// instead of wasting bandwidth serving an unchanged topology.
|
||||
epoch_id: Option<u32>,
|
||||
|
||||
output: Option<Output>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, utoipa::IntoParams)]
|
||||
#[into_params(parameter_in = Query)]
|
||||
struct NodesParams {
|
||||
#[allow(dead_code)]
|
||||
semver_compatibility: Option<String>,
|
||||
no_legacy: Option<bool>,
|
||||
page: Option<u32>,
|
||||
per_page: Option<u32>,
|
||||
|
||||
// Identifier for the current epoch of the topology state. When sent by a client we can check if
|
||||
// the client already knows about the latest topology state, allowing a `no-updates` response
|
||||
// instead of wasting bandwidth serving an unchanged topology.
|
||||
epoch_id: Option<u32>,
|
||||
output: Option<Output>,
|
||||
}
|
||||
|
||||
impl From<NodesParamsWithRole> for NodesParams {
|
||||
fn from(params: NodesParamsWithRole) -> Self {
|
||||
NodesParams {
|
||||
semver_compatibility: params.semver_compatibility,
|
||||
no_legacy: params.no_legacy,
|
||||
page: params.page,
|
||||
per_page: params.per_page,
|
||||
epoch_id: params.epoch_id,
|
||||
output: params.output,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a NodesParams> for PaginationRequest {
|
||||
fn from(params: &'a NodesParams) -> Self {
|
||||
PaginationRequest {
|
||||
output: params.output,
|
||||
page: params.page,
|
||||
per_page: params.per_page,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
tag = "Unstable Nym Nodes",
|
||||
post,
|
||||
request_body = NodesByAddressesRequestBody,
|
||||
path = "/by-addresses",
|
||||
context_path = "/v1/unstable/nym-nodes",
|
||||
responses(
|
||||
(status = 200, content(
|
||||
(NodesByAddressesResponse = "application/json"),
|
||||
(NodesByAddressesResponse = "application/yaml"),
|
||||
(NodesByAddressesResponse = "application/bincode")
|
||||
))
|
||||
),
|
||||
params(OutputParams)
|
||||
)]
|
||||
async fn nodes_by_addresses(
|
||||
Query(output): Query<OutputParams>,
|
||||
state: State<AppState>,
|
||||
Json(body): Json<NodesByAddressesRequestBody>,
|
||||
) -> AxumResult<FormattedResponse<NodesByAddressesResponse>> {
|
||||
// if the request is too big, simply reject it
|
||||
if body.addresses.len() > 100 {
|
||||
return Err(AxumErrorResponse::bad_request(
|
||||
"requested too many addresses",
|
||||
));
|
||||
}
|
||||
|
||||
let output = output.output.unwrap_or_default();
|
||||
|
||||
// TODO: perhaps introduce different cache because realistically nym-api will receive
|
||||
// request for the same couple addresses from all nodes in quick succession
|
||||
let describe_cache = state.describe_nodes_cache_data().await?;
|
||||
|
||||
let mut existence = HashMap::new();
|
||||
for address in body.addresses {
|
||||
existence.insert(address, describe_cache.node_with_address(address));
|
||||
}
|
||||
|
||||
Ok(output.to_response(NodesByAddressesResponse { existence }))
|
||||
}
|
||||
@@ -1,643 +0,0 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::node_describe_cache::cache::DescribedNodes;
|
||||
use crate::node_status_api::models::{AxumErrorResponse, AxumResult};
|
||||
use crate::nym_nodes::handlers::unstable::helpers::{refreshed_at, 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 nym_api_requests::models::{
|
||||
NodeAnnotation, NymNodeDescription, OffsetDateTimeJsonSchemaWrapper,
|
||||
};
|
||||
use nym_api_requests::nym_nodes::{
|
||||
CachedNodesResponse, NodeRole, NodeRoleQueryParam, PaginatedCachedNodesResponse, SkimmedNode,
|
||||
};
|
||||
use nym_api_requests::pagination::PaginatedResponse;
|
||||
use nym_http_api_common::{FormattedResponse, Output};
|
||||
use nym_mixnet_contract_common::NodeId;
|
||||
use nym_topology::CachedEpochRewardedSet;
|
||||
use std::collections::HashMap;
|
||||
use std::future::Future;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::RwLockReadGuard;
|
||||
use tracing::trace;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
pub type PaginatedSkimmedNodes =
|
||||
AxumResult<FormattedResponse<PaginatedCachedNodesResponse<SkimmedNode>>>;
|
||||
|
||||
/// Given all relevant caches, build part of response for JUST Nym Nodes
|
||||
fn build_nym_nodes_response<'a, NI>(
|
||||
rewarded_set: &CachedEpochRewardedSet,
|
||||
nym_nodes_subset: NI,
|
||||
annotations: &HashMap<NodeId, NodeAnnotation>,
|
||||
current_key_rotation: u32,
|
||||
active_only: bool,
|
||||
) -> Vec<SkimmedNode>
|
||||
where
|
||||
NI: Iterator<Item = &'a NymNodeDescription> + 'a,
|
||||
{
|
||||
let mut nodes = Vec::new();
|
||||
for nym_node in nym_nodes_subset {
|
||||
let node_id = nym_node.node_id;
|
||||
|
||||
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(
|
||||
current_key_rotation,
|
||||
role,
|
||||
annotation.last_24h_performance,
|
||||
));
|
||||
}
|
||||
nodes
|
||||
}
|
||||
|
||||
/// Given all relevant caches, add appropriate legacy nodes to the part of the response
|
||||
fn add_legacy<LN>(
|
||||
nodes: &mut Vec<SkimmedNode>,
|
||||
rewarded_set: &CachedEpochRewardedSet,
|
||||
describe_cache: &DescribedNodes,
|
||||
annotated_legacy_nodes: &HashMap<NodeId, LN>,
|
||||
current_key_rotation: u32,
|
||||
active_only: bool,
|
||||
) where
|
||||
LN: LegacyAnnotation,
|
||||
{
|
||||
for (node_id, legacy) in annotated_legacy_nodes.iter() {
|
||||
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) {
|
||||
// legacy nodes don't support key rotation
|
||||
nodes.push(described.to_skimmed_node(current_key_rotation, 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<NodesParams>,
|
||||
nym_nodes_subset: NI,
|
||||
annotated_legacy_nodes_getter: LG,
|
||||
active_only: bool,
|
||||
output: Output,
|
||||
) -> PaginatedSkimmedNodes
|
||||
where
|
||||
// iterator returning relevant subset of nym-nodes (like mixing nym-nodes, entries, etc.)
|
||||
NI: Iterator<Item = &'a NymNodeDescription> + 'a,
|
||||
|
||||
// async function that returns cache of appropriate legacy nodes (mixnodes or gateways)
|
||||
LG: Fn(&'a AppState) -> Fut,
|
||||
Fut:
|
||||
Future<Output = Result<RwLockReadGuard<'a, Cache<HashMap<NodeId, LN>>>, AxumErrorResponse>>,
|
||||
|
||||
// the legacy node (MixNodeBondAnnotated or GatewayBondAnnotated)
|
||||
LN: LegacyAnnotation + 'a,
|
||||
{
|
||||
// TODO: implement it
|
||||
let _ = query_params.per_page;
|
||||
let _ = query_params.page;
|
||||
|
||||
// 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?;
|
||||
|
||||
let contract_cache = state.nym_contract_cache();
|
||||
|
||||
let interval = contract_cache.current_interval().await?;
|
||||
let current_key_rotation = contract_cache.current_key_rotation_id().await?;
|
||||
|
||||
// 4.0 If the client indicates that they already know about the current topology send empty response
|
||||
if let Some(client_known_epoch) = query_params.epoch_id {
|
||||
if client_known_epoch == interval.current_epoch_id() {
|
||||
return Ok(output.to_response(PaginatedCachedNodesResponse::no_updates(
|
||||
interval.current_epoch_absolute_id(),
|
||||
current_key_rotation,
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
// 4. start building the response
|
||||
let mut nodes = build_nym_nodes_response(
|
||||
&rewarded_set,
|
||||
nym_nodes_subset,
|
||||
&annotations,
|
||||
current_key_rotation,
|
||||
active_only,
|
||||
);
|
||||
|
||||
// 5. if we allow legacy nodes, repeat the procedure for them, otherwise return just nym-nodes
|
||||
if let Some(true) = query_params.no_legacy {
|
||||
// min of all caches
|
||||
let refreshed_at = refreshed_at([
|
||||
rewarded_set.timestamp(),
|
||||
annotations.timestamp(),
|
||||
describe_cache.timestamp(),
|
||||
]);
|
||||
|
||||
return Ok(output.to_response(
|
||||
PaginatedCachedNodesResponse::new_full(
|
||||
interval.current_epoch_absolute_id(),
|
||||
current_key_rotation,
|
||||
refreshed_at,
|
||||
nodes,
|
||||
)
|
||||
.fresh(interval),
|
||||
));
|
||||
}
|
||||
|
||||
// 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,
|
||||
&rewarded_set,
|
||||
&describe_cache,
|
||||
&annotated_legacy_nodes,
|
||||
current_key_rotation,
|
||||
active_only,
|
||||
);
|
||||
|
||||
// min of all caches
|
||||
let refreshed_at = refreshed_at([
|
||||
rewarded_set.timestamp(),
|
||||
annotations.timestamp(),
|
||||
describe_cache.timestamp(),
|
||||
annotated_legacy_nodes.timestamp(),
|
||||
]);
|
||||
|
||||
let base_response = output.to_response(
|
||||
PaginatedCachedNodesResponse::new_full(
|
||||
interval.current_epoch_absolute_id(),
|
||||
current_key_rotation,
|
||||
refreshed_at,
|
||||
nodes,
|
||||
)
|
||||
.fresh(interval),
|
||||
);
|
||||
|
||||
if !active_only {
|
||||
return Ok(base_response);
|
||||
}
|
||||
|
||||
// if caller requested only active nodes, the response is valid until the epoch changes
|
||||
// (but add 2 minutes due to epoch transition not being instantaneous
|
||||
let epoch_end = interval.current_epoch_end();
|
||||
let expiration = epoch_end + Duration::from_secs(120);
|
||||
Ok(base_response.with_expires_header(expiration))
|
||||
}
|
||||
|
||||
/// Deprecated query that gets ALL gateways
|
||||
#[utoipa::path(
|
||||
tag = "Unstable Nym Nodes",
|
||||
get,
|
||||
params(NodesParams),
|
||||
path = "/gateways/skimmed",
|
||||
context_path = "/v1/unstable/nym-nodes",
|
||||
responses(
|
||||
(status = 200, content(
|
||||
(CachedNodesResponse<SkimmedNode> = "application/json"),
|
||||
(CachedNodesResponse<SkimmedNode> = "application/yaml"),
|
||||
(CachedNodesResponse<SkimmedNode> = "application/bincode")
|
||||
))
|
||||
),
|
||||
)]
|
||||
#[deprecated(note = "use '/v1/unstable/nym-nodes/entry-gateways/skimmed/all' instead")]
|
||||
pub(super) async fn deprecated_gateways_basic(
|
||||
state: State<AppState>,
|
||||
query_params: Query<NodesParams>,
|
||||
) -> AxumResult<FormattedResponse<CachedNodesResponse<SkimmedNode>>> {
|
||||
let output = query_params.output.unwrap_or_default();
|
||||
|
||||
// 1. call '/v1/unstable/skimmed/entry-gateways/all'
|
||||
let all_gateways = entry_gateways_basic_all(state, query_params)
|
||||
.await?
|
||||
.into_inner();
|
||||
|
||||
// 3. return result
|
||||
Ok(output.to_response(CachedNodesResponse {
|
||||
refreshed_at: all_gateways.metadata.refreshed_at,
|
||||
// 2. remove pagination
|
||||
nodes: all_gateways.nodes.data,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Deprecated query that gets ACTIVE-ONLY mixnodes
|
||||
#[utoipa::path(
|
||||
tag = "Unstable Nym Nodes",
|
||||
get,
|
||||
params(NodesParams),
|
||||
path = "/mixnodes/skimmed",
|
||||
context_path = "/v1/unstable/nym-nodes",
|
||||
responses(
|
||||
(status = 200, content(
|
||||
(CachedNodesResponse<SkimmedNode> = "application/json"),
|
||||
(CachedNodesResponse<SkimmedNode> = "application/yaml"),
|
||||
(CachedNodesResponse<SkimmedNode> = "application/bincode")
|
||||
))
|
||||
),
|
||||
)]
|
||||
#[deprecated(note = "use '/v1/unstable/nym-nodes/skimmed/mixnodes/active' instead")]
|
||||
pub(super) async fn deprecated_mixnodes_basic(
|
||||
state: State<AppState>,
|
||||
query_params: Query<NodesParams>,
|
||||
) -> AxumResult<FormattedResponse<CachedNodesResponse<SkimmedNode>>> {
|
||||
let output = query_params.output.unwrap_or_default();
|
||||
|
||||
// 1. call '/v1/unstable/nym-nodes/skimmed/mixnodes/active'
|
||||
let active_mixnodes = mixnodes_basic_active(state, query_params)
|
||||
.await?
|
||||
.into_inner();
|
||||
|
||||
// 3. return result
|
||||
Ok(output.to_response(CachedNodesResponse {
|
||||
refreshed_at: active_mixnodes.metadata.refreshed_at,
|
||||
// 2. remove pagination
|
||||
nodes: active_mixnodes.nodes.data,
|
||||
}))
|
||||
}
|
||||
|
||||
async fn nodes_basic(
|
||||
state: State<AppState>,
|
||||
Query(query_params): Query<NodesParams>,
|
||||
active_only: bool,
|
||||
) -> PaginatedSkimmedNodes {
|
||||
let output = query_params.output.unwrap_or_default();
|
||||
|
||||
// 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 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 interval = state.nym_contract_cache().current_interval().await?;
|
||||
let current_key_rotation = state.nym_contract_cache().current_key_rotation_id().await?;
|
||||
|
||||
let mut nodes = build_nym_nodes_response(
|
||||
&rewarded_set,
|
||||
all_nym_nodes,
|
||||
&annotations,
|
||||
current_key_rotation,
|
||||
active_only,
|
||||
);
|
||||
|
||||
// add legacy gateways to the response
|
||||
add_legacy(
|
||||
&mut nodes,
|
||||
&rewarded_set,
|
||||
&describe_cache,
|
||||
&legacy_gateways,
|
||||
current_key_rotation,
|
||||
active_only,
|
||||
);
|
||||
|
||||
// add legacy mixnodes to the response
|
||||
add_legacy(
|
||||
&mut nodes,
|
||||
&rewarded_set,
|
||||
&describe_cache,
|
||||
&legacy_mixnodes,
|
||||
current_key_rotation,
|
||||
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(output.to_response(PaginatedCachedNodesResponse::new_full(
|
||||
current_key_rotation,
|
||||
interval.current_epoch_absolute_id(),
|
||||
refreshed_at,
|
||||
nodes,
|
||||
)))
|
||||
}
|
||||
|
||||
#[allow(dead_code)] // not dead, used in OpenAPI docs
|
||||
#[derive(ToSchema)]
|
||||
#[schema(title = "PaginatedCachedNodesResponse")]
|
||||
pub struct PaginatedCachedNodesResponseSchema {
|
||||
pub refreshed_at: OffsetDateTimeJsonSchemaWrapper,
|
||||
#[schema(value_type = SkimmedNode)]
|
||||
pub nodes: PaginatedResponse<SkimmedNode>,
|
||||
}
|
||||
|
||||
/// Return all Nym Nodes and optionally legacy mixnodes/gateways (if `no-legacy` flag is not used)
|
||||
/// that are currently bonded.
|
||||
#[utoipa::path(
|
||||
tag = "Unstable Nym Nodes",
|
||||
get,
|
||||
params(NodesParamsWithRole),
|
||||
path = "",
|
||||
context_path = "/v1/unstable/nym-nodes/skimmed",
|
||||
responses(
|
||||
(status = 200, content(
|
||||
(PaginatedCachedNodesResponseSchema = "application/json"),
|
||||
(PaginatedCachedNodesResponseSchema = "application/yaml"),
|
||||
(PaginatedCachedNodesResponseSchema = "application/bincode")
|
||||
))
|
||||
),
|
||||
)]
|
||||
pub(super) async fn nodes_basic_all(
|
||||
state: State<AppState>,
|
||||
Query(query_params): Query<NodesParamsWithRole>,
|
||||
) -> PaginatedSkimmedNodes {
|
||||
if let Some(role) = query_params.role {
|
||||
return match role {
|
||||
NodeRoleQueryParam::ActiveMixnode => {
|
||||
mixnodes_basic_all(state, Query(query_params.into())).await
|
||||
}
|
||||
NodeRoleQueryParam::EntryGateway => {
|
||||
entry_gateways_basic_all(state, Query(query_params.into())).await
|
||||
}
|
||||
NodeRoleQueryParam::ExitGateway => {
|
||||
exit_gateways_basic_all(state, Query(query_params.into())).await
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
nodes_basic(state, Query(query_params.into()), false).await
|
||||
}
|
||||
|
||||
/// 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(
|
||||
tag = "Unstable Nym Nodes",
|
||||
get,
|
||||
params(NodesParams),
|
||||
path = "/active",
|
||||
context_path = "/v1/unstable/nym-nodes/skimmed",
|
||||
responses(
|
||||
(status = 200, content(
|
||||
(PaginatedCachedNodesResponseSchema = "application/json"),
|
||||
(PaginatedCachedNodesResponseSchema = "application/yaml"),
|
||||
(PaginatedCachedNodesResponseSchema = "application/bincode")
|
||||
))
|
||||
),
|
||||
)]
|
||||
pub(super) async fn nodes_basic_active(
|
||||
state: State<AppState>,
|
||||
Query(query_params): Query<NodesParamsWithRole>,
|
||||
) -> PaginatedSkimmedNodes {
|
||||
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
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
nodes_basic(state, Query(query_params.into()), true).await
|
||||
}
|
||||
|
||||
async fn mixnodes_basic(
|
||||
state: State<AppState>,
|
||||
query_params: Query<NodesParams>,
|
||||
active_only: bool,
|
||||
) -> PaginatedSkimmedNodes {
|
||||
let output = query_params.output.unwrap_or_default();
|
||||
|
||||
// 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();
|
||||
|
||||
build_skimmed_nodes_response(
|
||||
&state.0,
|
||||
query_params,
|
||||
mixing_nym_nodes,
|
||||
|state| state.legacy_mixnode_annotations(),
|
||||
active_only,
|
||||
output,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Returns Nym Nodes and optionally legacy mixnodes (if `no-legacy` flag is not used)
|
||||
/// that are currently bonded and support mixing role.
|
||||
#[utoipa::path(
|
||||
tag = "Unstable Nym Nodes",
|
||||
get,
|
||||
params(NodesParams),
|
||||
path = "/mixnodes/all",
|
||||
context_path = "/v1/unstable/nym-nodes/skimmed",
|
||||
responses(
|
||||
(status = 200, content(
|
||||
(PaginatedCachedNodesResponseSchema = "application/json"),
|
||||
(PaginatedCachedNodesResponseSchema = "application/yaml"),
|
||||
(PaginatedCachedNodesResponseSchema = "application/bincode")
|
||||
))
|
||||
),
|
||||
)]
|
||||
pub(super) async fn mixnodes_basic_all(
|
||||
state: State<AppState>,
|
||||
query_params: Query<NodesParams>,
|
||||
) -> PaginatedSkimmedNodes {
|
||||
mixnodes_basic(state, query_params, false).await
|
||||
}
|
||||
|
||||
/// Returns Nym Nodes and optionally legacy mixnodes (if `no-legacy` flag is not used)
|
||||
/// that are currently bonded and are in the active set with one of the mixing roles.
|
||||
#[utoipa::path(
|
||||
tag = "Unstable Nym Nodes",
|
||||
get,
|
||||
params(NodesParams),
|
||||
path = "/mixnodes/active",
|
||||
context_path = "/v1/unstable/nym-nodes/skimmed",
|
||||
responses(
|
||||
(status = 200, content(
|
||||
(PaginatedCachedNodesResponseSchema = "application/json"),
|
||||
(PaginatedCachedNodesResponseSchema = "application/yaml"),
|
||||
(PaginatedCachedNodesResponseSchema = "application/bincode")
|
||||
))
|
||||
),
|
||||
)]
|
||||
pub(super) async fn mixnodes_basic_active(
|
||||
state: State<AppState>,
|
||||
query_params: Query<NodesParams>,
|
||||
) -> PaginatedSkimmedNodes {
|
||||
mixnodes_basic(state, query_params, true).await
|
||||
}
|
||||
|
||||
async fn entry_gateways_basic(
|
||||
state: State<AppState>,
|
||||
query_params: Query<NodesParams>,
|
||||
active_only: bool,
|
||||
) -> PaginatedSkimmedNodes {
|
||||
let output = query_params.output.unwrap_or_default();
|
||||
|
||||
// 1. grab all relevant described nym-nodes
|
||||
let describe_cache = state.describe_nodes_cache_data().await?;
|
||||
let mixing_nym_nodes = describe_cache.entry_capable_nym_nodes();
|
||||
|
||||
build_skimmed_nodes_response(
|
||||
&state.0,
|
||||
query_params,
|
||||
mixing_nym_nodes,
|
||||
|state| state.legacy_gateways_annotations(),
|
||||
active_only,
|
||||
output,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Returns Nym Nodes and optionally legacy gateways (if `no-legacy` flag is not used)
|
||||
/// that are currently bonded and are in the active set with the entry role.
|
||||
#[utoipa::path(
|
||||
tag = "Unstable Nym Nodes",
|
||||
get,
|
||||
params(NodesParams),
|
||||
path = "/entry-gateways/active",
|
||||
context_path = "/v1/unstable/nym-nodes/skimmed",
|
||||
responses(
|
||||
(status = 200, content(
|
||||
(PaginatedCachedNodesResponseSchema = "application/json"),
|
||||
(PaginatedCachedNodesResponseSchema = "application/yaml"),
|
||||
(PaginatedCachedNodesResponseSchema = "application/bincode")
|
||||
))
|
||||
),
|
||||
)]
|
||||
pub(super) async fn entry_gateways_basic_active(
|
||||
state: State<AppState>,
|
||||
query_params: Query<NodesParams>,
|
||||
) -> PaginatedSkimmedNodes {
|
||||
entry_gateways_basic(state, query_params, true).await
|
||||
}
|
||||
|
||||
/// Returns Nym Nodes and optionally legacy gateways (if `no-legacy` flag is not used)
|
||||
/// that are currently bonded and support entry gateway role.
|
||||
#[utoipa::path(
|
||||
tag = "Unstable Nym Nodes",
|
||||
get,
|
||||
params(NodesParams),
|
||||
path = "/entry-gateways/all",
|
||||
context_path = "/v1/unstable/nym-nodes/skimmed",
|
||||
responses(
|
||||
(status = 200, content(
|
||||
(PaginatedCachedNodesResponseSchema = "application/json"),
|
||||
(PaginatedCachedNodesResponseSchema = "application/yaml"),
|
||||
(PaginatedCachedNodesResponseSchema = "application/bincode")
|
||||
))
|
||||
),
|
||||
)]
|
||||
pub(super) async fn entry_gateways_basic_all(
|
||||
state: State<AppState>,
|
||||
query_params: Query<NodesParams>,
|
||||
) -> PaginatedSkimmedNodes {
|
||||
entry_gateways_basic(state, query_params, false).await
|
||||
}
|
||||
|
||||
async fn exit_gateways_basic(
|
||||
state: State<AppState>,
|
||||
query_params: Query<NodesParams>,
|
||||
active_only: bool,
|
||||
) -> PaginatedSkimmedNodes {
|
||||
let output = query_params.output.unwrap_or_default();
|
||||
|
||||
// 1. grab all relevant described nym-nodes
|
||||
let describe_cache = state.describe_nodes_cache_data().await?;
|
||||
let mixing_nym_nodes = describe_cache.exit_capable_nym_nodes();
|
||||
|
||||
build_skimmed_nodes_response(
|
||||
&state.0,
|
||||
query_params,
|
||||
mixing_nym_nodes,
|
||||
|state| state.legacy_gateways_annotations(),
|
||||
active_only,
|
||||
output,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Returns Nym Nodes and optionally legacy gateways (if `no-legacy` flag is not used)
|
||||
/// that are currently bonded and are in the active set with the exit role.
|
||||
#[utoipa::path(
|
||||
tag = "Unstable Nym Nodes",
|
||||
get,
|
||||
params(NodesParams),
|
||||
path = "/exit-gateways/active",
|
||||
context_path = "/v1/unstable/nym-nodes/skimmed",
|
||||
responses(
|
||||
(status = 200, content(
|
||||
(PaginatedCachedNodesResponseSchema = "application/json"),
|
||||
(PaginatedCachedNodesResponseSchema = "application/yaml"),
|
||||
(PaginatedCachedNodesResponseSchema = "application/bincode")
|
||||
))
|
||||
),
|
||||
)]
|
||||
pub(super) async fn exit_gateways_basic_active(
|
||||
state: State<AppState>,
|
||||
query_params: Query<NodesParams>,
|
||||
) -> PaginatedSkimmedNodes {
|
||||
exit_gateways_basic(state, query_params, true).await
|
||||
}
|
||||
|
||||
/// Returns Nym Nodes and optionally legacy gateways (if `no-legacy` flag is not used)
|
||||
/// that are currently bonded and support exit gateway role.
|
||||
#[utoipa::path(
|
||||
tag = "Unstable Nym Nodes",
|
||||
get,
|
||||
params(NodesParams),
|
||||
path = "/exit-gateways/all",
|
||||
context_path = "/v1/unstable/nym-nodes/skimmed",
|
||||
responses(
|
||||
(status = 200, content(
|
||||
(PaginatedCachedNodesResponseSchema = "application/json"),
|
||||
(PaginatedCachedNodesResponseSchema = "application/yaml"),
|
||||
(PaginatedCachedNodesResponseSchema = "application/bincode")
|
||||
))
|
||||
),
|
||||
)]
|
||||
pub(super) async fn exit_gateways_basic_all(
|
||||
state: State<AppState>,
|
||||
query_params: Query<NodesParams>,
|
||||
) -> PaginatedSkimmedNodes {
|
||||
exit_gateways_basic(state, query_params, false).await
|
||||
}
|
||||
@@ -28,7 +28,7 @@ use crate::support::http::{RouterBuilder, ShutdownHandles, TASK_MANAGER_TIMEOUT_
|
||||
use crate::support::nyxd;
|
||||
use crate::support::storage::runtime_migrations::m001_directory_services_v2_1::migrate_to_directory_services_v2_1;
|
||||
use crate::support::storage::NymApiStorage;
|
||||
use crate::unstable_routes::account::cache::AddressInfoCache;
|
||||
use crate::unstable_routes::v1::account::cache::AddressInfoCache;
|
||||
use crate::{
|
||||
circulating_supply_api, ecash, epoch_operations, network_monitor, node_describe_cache,
|
||||
node_status_api, nym_contract_cache,
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
// Copyright 2022-2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use nym_task::TaskManager;
|
||||
use tokio::task::JoinHandle;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
pub(crate) mod helpers;
|
||||
pub(crate) mod openapi;
|
||||
pub(crate) mod router;
|
||||
pub(crate) mod state;
|
||||
|
||||
use nym_task::TaskManager;
|
||||
pub(crate) use router::RouterBuilder;
|
||||
use tokio::task::JoinHandle;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::unstable_routes;
|
||||
|
||||
pub(crate) const TASK_MANAGER_TIMEOUT_S: u64 = 10;
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ use crate::nym_nodes::handlers::nym_node_routes;
|
||||
use crate::status;
|
||||
use crate::support::http::openapi::ApiDoc;
|
||||
use crate::support::http::state::AppState;
|
||||
use crate::support::http::unstable_routes::unstable_routes;
|
||||
use crate::unstable_routes::v1::unstable_routes_v1;
|
||||
use anyhow::anyhow;
|
||||
use axum::response::Redirect;
|
||||
use axum::routing::get;
|
||||
@@ -64,7 +64,7 @@ impl RouterBuilder {
|
||||
.nest("/api-status", status::handlers::api_status_routes())
|
||||
.nest("/nym-nodes", nym_node_routes())
|
||||
.nest("/ecash", ecash_routes())
|
||||
.nest("/unstable", unstable_routes()), // CORS layer needs to be "outside" of routes
|
||||
.nest("/unstable", unstable_routes_v1()), // CORS layer needs to be "outside" of routes
|
||||
);
|
||||
|
||||
Self {
|
||||
|
||||
@@ -16,8 +16,8 @@ use crate::support::http::state::chain_status::ChainStatusCache;
|
||||
use crate::support::http::state::force_refresh::ForcedRefresh;
|
||||
use crate::support::nyxd::Client;
|
||||
use crate::support::storage;
|
||||
use crate::unstable_routes::account::cache::AddressInfoCache;
|
||||
use crate::unstable_routes::models::NyxAccountDetails;
|
||||
use crate::unstable_routes::v1::account::cache::AddressInfoCache;
|
||||
use crate::unstable_routes::v1::account::models::NyxAccountDetails;
|
||||
use axum::extract::FromRef;
|
||||
use nym_api_requests::models::{GatewayBondAnnotated, MixNodeBondAnnotated, NodeAnnotation};
|
||||
use nym_mixnet_contract_common::NodeId;
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
@@ -1,15 +1,6 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
pub(crate) mod account;
|
||||
pub(crate) mod models;
|
||||
|
||||
use crate::support::http::state::AppState;
|
||||
use axum::Router;
|
||||
|
||||
// as those get stabilised, they should get deprecated and use a redirection instead
|
||||
pub(crate) fn unstable_routes() -> Router<AppState> {
|
||||
Router::new()
|
||||
.nest("/nym-nodes", crate::nym_nodes::handlers::unstable::routes())
|
||||
.nest("/account", account::routes())
|
||||
}
|
||||
pub(crate) mod helpers;
|
||||
pub(crate) mod v1;
|
||||
pub(crate) mod v2;
|
||||
|
||||
+3
-8
@@ -1,11 +1,6 @@
|
||||
use crate::{
|
||||
node_status_api::models::AxumResult,
|
||||
nym_contract_cache::cache::NymContractCache,
|
||||
unstable_routes::{
|
||||
account::data_collector::AddressDataCollector,
|
||||
models::{NyxAccountDelegationDetails, NyxAccountDetails},
|
||||
},
|
||||
};
|
||||
use crate::unstable_routes::v1::account::data_collector::AddressDataCollector;
|
||||
use crate::unstable_routes::v1::account::models::{NyxAccountDelegationDetails, NyxAccountDetails};
|
||||
use crate::{node_status_api::models::AxumResult, nym_contract_cache::cache::NymContractCache};
|
||||
use moka::{future::Cache, Entry};
|
||||
use nym_validator_client::nyxd::AccountId;
|
||||
use std::{sync::Arc, time::Duration};
|
||||
+1
-1
@@ -1,10 +1,10 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::unstable_routes::v1::account::models::NyxAccountDelegationRewardDetails;
|
||||
use crate::{
|
||||
node_status_api::models::{AxumErrorResponse, AxumResult},
|
||||
nym_contract_cache::cache::NymContractCache,
|
||||
unstable_routes::models::NyxAccountDelegationRewardDetails,
|
||||
};
|
||||
use cosmwasm_std::{Coin, Decimal};
|
||||
use nym_mixnet_contract_common::NodeRewarding;
|
||||
+2
-1
@@ -4,13 +4,13 @@
|
||||
use crate::{
|
||||
node_status_api::models::{AxumErrorResponse, AxumResult},
|
||||
support::http::state::AppState,
|
||||
unstable_routes::models::NyxAccountDetails,
|
||||
};
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
routing::get,
|
||||
Json, Router,
|
||||
};
|
||||
use models::NyxAccountDetails;
|
||||
use nym_validator_client::nyxd::AccountId;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::str::FromStr;
|
||||
@@ -19,6 +19,7 @@ use utoipa::ToSchema;
|
||||
|
||||
pub(crate) mod cache;
|
||||
pub(crate) mod data_collector;
|
||||
pub(crate) mod models;
|
||||
|
||||
pub(crate) fn routes() -> Router<AppState> {
|
||||
Router::new().route("/:address", get(address))
|
||||
@@ -0,0 +1,15 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::support::http::state::AppState;
|
||||
use axum::Router;
|
||||
|
||||
pub(crate) mod account;
|
||||
pub(crate) mod nym_nodes;
|
||||
|
||||
// as those get stabilised, they should get deprecated and use a redirection instead
|
||||
pub(crate) fn unstable_routes_v1() -> Router<AppState> {
|
||||
Router::new()
|
||||
.nest("/nym-nodes", nym_nodes::routes())
|
||||
.nest("/account", account::routes())
|
||||
}
|
||||
+4
-4
@@ -1,9 +1,9 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::node_status_api::models::{AxumErrorResponse, AxumResult};
|
||||
use crate::nym_nodes::handlers::unstable::NodesParamsWithRole;
|
||||
use crate::support::http::state::AppState;
|
||||
use crate::unstable_routes::v1::nym_nodes::helpers::NodesParamsWithRole;
|
||||
use axum::extract::{Query, State};
|
||||
use nym_api_requests::nym_nodes::{CachedNodesResponse, FullFatNode};
|
||||
use nym_http_api_common::FormattedResponse;
|
||||
@@ -15,11 +15,11 @@ use nym_http_api_common::FormattedResponse;
|
||||
path = "",
|
||||
context_path = "/v1/unstable/nym-nodes/full-fat",
|
||||
responses(
|
||||
// (status = 200, body = CachedNodesResponse<FullFatNode>)
|
||||
// (status = 200, body = CachedNodesResponse<FullFatNode>)
|
||||
(status = 501)
|
||||
)
|
||||
)]
|
||||
pub(super) async fn nodes_detailed(
|
||||
pub(crate) async fn nodes_detailed(
|
||||
_state: State<AppState>,
|
||||
_query_params: Query<NodesParamsWithRole>,
|
||||
) -> AxumResult<FormattedResponse<CachedNodesResponse<FullFatNode>>> {
|
||||
@@ -0,0 +1,51 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::node_status_api::models::{AxumErrorResponse, AxumResult};
|
||||
use crate::support::http::state::AppState;
|
||||
use axum::extract::{Query, State};
|
||||
use axum::Json;
|
||||
use nym_api_requests::nym_nodes::{NodesByAddressesRequestBody, NodesByAddressesResponse};
|
||||
use nym_http_api_common::{FormattedResponse, OutputParams};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[utoipa::path(
|
||||
tag = "Unstable Nym Nodes",
|
||||
post,
|
||||
request_body = NodesByAddressesRequestBody,
|
||||
path = "/by-addresses",
|
||||
context_path = "/v1/unstable/nym-nodes",
|
||||
responses(
|
||||
(status = 200, content(
|
||||
(NodesByAddressesResponse = "application/json"),
|
||||
(NodesByAddressesResponse = "application/yaml"),
|
||||
(NodesByAddressesResponse = "application/bincode")
|
||||
))
|
||||
),
|
||||
params(OutputParams)
|
||||
)]
|
||||
pub(crate) async fn nodes_by_addresses(
|
||||
Query(output): Query<OutputParams>,
|
||||
state: State<AppState>,
|
||||
Json(body): Json<NodesByAddressesRequestBody>,
|
||||
) -> AxumResult<FormattedResponse<NodesByAddressesResponse>> {
|
||||
// if the request is too big, simply reject it
|
||||
if body.addresses.len() > 100 {
|
||||
return Err(AxumErrorResponse::bad_request(
|
||||
"requested too many addresses",
|
||||
));
|
||||
}
|
||||
|
||||
let output = output.output.unwrap_or_default();
|
||||
|
||||
// TODO: perhaps introduce different cache because realistically nym-api will receive
|
||||
// request for the same couple addresses from all nodes in quick succession
|
||||
let describe_cache = state.describe_nodes_cache_data().await?;
|
||||
|
||||
let mut existence = HashMap::new();
|
||||
for address in body.addresses {
|
||||
existence.insert(address, describe_cache.node_with_address(address));
|
||||
}
|
||||
|
||||
Ok(output.to_response(NodesByAddressesResponse { existence }))
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::support::http::helpers::PaginationRequest;
|
||||
use nym_api_requests::models::{
|
||||
GatewayBondAnnotated, MalformedNodeBond, MixNodeBondAnnotated, OffsetDateTimeJsonSchemaWrapper,
|
||||
};
|
||||
use nym_api_requests::nym_nodes::{NodeRole, NodeRoleQueryParam, SkimmedNode};
|
||||
use nym_http_api_common::Output;
|
||||
use nym_mixnet_contract_common::reward_params::Performance;
|
||||
use serde::Deserialize;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
#[derive(Debug, Deserialize, utoipa::IntoParams)]
|
||||
pub(crate) struct NodesParamsWithRole {
|
||||
#[param(inline)]
|
||||
pub(crate) role: Option<NodeRoleQueryParam>,
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) semver_compatibility: Option<String>,
|
||||
pub(crate) no_legacy: Option<bool>,
|
||||
pub(crate) page: Option<u32>,
|
||||
pub(crate) per_page: Option<u32>,
|
||||
|
||||
// Identifier for the current epoch of the topology state. When sent by a client we can check if
|
||||
// the client already knows about the latest topology state, allowing a `no-updates` response
|
||||
// instead of wasting bandwidth serving an unchanged topology.
|
||||
pub(crate) epoch_id: Option<u32>,
|
||||
|
||||
pub(crate) output: Option<Output>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, utoipa::IntoParams)]
|
||||
#[into_params(parameter_in = Query)]
|
||||
pub(crate) struct NodesParams {
|
||||
#[allow(dead_code)]
|
||||
pub(crate) semver_compatibility: Option<String>,
|
||||
pub(crate) no_legacy: Option<bool>,
|
||||
pub(crate) page: Option<u32>,
|
||||
pub(crate) per_page: Option<u32>,
|
||||
|
||||
// Identifier for the current epoch of the topology state. When sent by a client we can check if
|
||||
// the client already knows about the latest topology state, allowing a `no-updates` response
|
||||
// instead of wasting bandwidth serving an unchanged topology.
|
||||
pub(crate) epoch_id: Option<u32>,
|
||||
pub(crate) output: Option<Output>,
|
||||
}
|
||||
|
||||
impl From<NodesParamsWithRole> for NodesParams {
|
||||
fn from(params: NodesParamsWithRole) -> Self {
|
||||
NodesParams {
|
||||
semver_compatibility: params.semver_compatibility,
|
||||
no_legacy: params.no_legacy,
|
||||
page: params.page,
|
||||
per_page: params.per_page,
|
||||
epoch_id: params.epoch_id,
|
||||
output: params.output,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a NodesParams> for PaginationRequest {
|
||||
fn from(params: &'a NodesParams) -> Self {
|
||||
PaginationRequest {
|
||||
output: params.output,
|
||||
page: params.page,
|
||||
per_page: params.per_page,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait LegacyAnnotation {
|
||||
fn performance(&self) -> Performance;
|
||||
|
||||
fn identity(&self) -> &str;
|
||||
|
||||
fn try_to_skimmed_node(&self, role: NodeRole) -> Result<SkimmedNode, MalformedNodeBond>;
|
||||
}
|
||||
|
||||
impl LegacyAnnotation for MixNodeBondAnnotated {
|
||||
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<SkimmedNode, MalformedNodeBond> {
|
||||
self.try_to_skimmed_node(role)
|
||||
}
|
||||
}
|
||||
|
||||
impl LegacyAnnotation for GatewayBondAnnotated {
|
||||
fn performance(&self) -> Performance {
|
||||
self.node_performance.last_24h
|
||||
}
|
||||
|
||||
fn identity(&self) -> &str {
|
||||
self.identity()
|
||||
}
|
||||
|
||||
fn try_to_skimmed_node(&self, role: NodeRole) -> Result<SkimmedNode, MalformedNodeBond> {
|
||||
self.try_to_skimmed_node(role)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn refreshed_at(
|
||||
iter: impl IntoIterator<Item = OffsetDateTime>,
|
||||
) -> OffsetDateTimeJsonSchemaWrapper {
|
||||
iter.into_iter()
|
||||
.min()
|
||||
.unwrap_or(OffsetDateTime::UNIX_EPOCH)
|
||||
.into()
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//! All routes/nodes are split into three tiers:
|
||||
//!
|
||||
//! `/skimmed`
|
||||
//! - used by clients
|
||||
//! - returns the very basic information for routing purposes
|
||||
//!
|
||||
//! `/semi-skimmed`
|
||||
//! - used by other nodes/VPN
|
||||
//! - returns more additional information such as noise keys
|
||||
//!
|
||||
//! `/full-fat`
|
||||
//! - used by explorers, et al.
|
||||
//! - returns almost everything there is about the nodes
|
||||
//!
|
||||
//! There's also additional split based on the role:
|
||||
//! - `?role` => filters based on the specific role (mixnode/gateway/(in the future: entry/exit))
|
||||
//! - `/mixnodes/<tier>` => only returns mixnode role data
|
||||
//! - `/gateway/<tier>` => only returns (entry) gateway role data
|
||||
|
||||
use crate::support::http::state::AppState;
|
||||
use crate::unstable_routes::v1::nym_nodes::full_fat::nodes_detailed;
|
||||
use crate::unstable_routes::v1::nym_nodes::handlers::nodes_by_addresses;
|
||||
use crate::unstable_routes::v1::nym_nodes::semi_skimmed::nodes_expanded;
|
||||
use crate::unstable_routes::v1::nym_nodes::skimmed::{
|
||||
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 axum::routing::{get, post};
|
||||
use axum::Router;
|
||||
use tower_http::compression::CompressionLayer;
|
||||
|
||||
pub(crate) mod full_fat;
|
||||
pub(crate) mod handlers;
|
||||
pub(crate) mod helpers;
|
||||
pub(crate) mod semi_skimmed;
|
||||
pub(crate) mod skimmed;
|
||||
|
||||
#[allow(deprecated)]
|
||||
pub(crate) fn routes() -> Router<AppState> {
|
||||
Router::new()
|
||||
.nest(
|
||||
"/skimmed",
|
||||
Router::new()
|
||||
.route("/", get(nodes_basic_all))
|
||||
.route("/active", get(nodes_basic_active))
|
||||
.nest(
|
||||
"/mixnodes",
|
||||
Router::new()
|
||||
.route("/active", get(mixnodes_basic_active))
|
||||
.route("/all", get(mixnodes_basic_all)),
|
||||
)
|
||||
.nest(
|
||||
"/entry-gateways",
|
||||
Router::new()
|
||||
.route("/active", get(entry_gateways_basic_active))
|
||||
.route("/all", get(entry_gateways_basic_all)),
|
||||
)
|
||||
.nest(
|
||||
"/exit-gateways",
|
||||
Router::new()
|
||||
.route("/active", get(exit_gateways_basic_active))
|
||||
.route("/all", get(exit_gateways_basic_all)),
|
||||
),
|
||||
)
|
||||
.nest(
|
||||
"/semi-skimmed",
|
||||
Router::new().route("/", get(nodes_expanded)),
|
||||
)
|
||||
.nest("/full-fat", Router::new().route("/", get(nodes_detailed)))
|
||||
.route("/gateways/skimmed", get(skimmed::deprecated_gateways_basic))
|
||||
.route("/mixnodes/skimmed", get(skimmed::deprecated_mixnodes_basic))
|
||||
.route("/by-addresses", post(nodes_by_addresses))
|
||||
.layer(CompressionLayer::new())
|
||||
}
|
||||
+4
-4
@@ -1,9 +1,9 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::node_status_api::models::{AxumErrorResponse, AxumResult};
|
||||
use crate::nym_nodes::handlers::unstable::NodesParamsWithRole;
|
||||
use crate::support::http::state::AppState;
|
||||
use crate::unstable_routes::v1::nym_nodes::helpers::NodesParamsWithRole;
|
||||
use axum::extract::{Query, State};
|
||||
use nym_api_requests::nym_nodes::{CachedNodesResponse, SemiSkimmedNode};
|
||||
use nym_http_api_common::FormattedResponse;
|
||||
@@ -15,11 +15,11 @@ use nym_http_api_common::FormattedResponse;
|
||||
path = "",
|
||||
context_path = "/v1/unstable/nym-nodes/semi-skimmed",
|
||||
responses(
|
||||
// (status = 200, body = CachedNodesResponse<SemiSkimmedNode>)
|
||||
// (status = 200, body = CachedNodesResponse<SemiSkimmedNode>)
|
||||
(status = 501)
|
||||
)
|
||||
)]
|
||||
pub(super) async fn nodes_expanded(
|
||||
pub(crate) async fn nodes_expanded(
|
||||
_state: State<AppState>,
|
||||
_query_params: Query<NodesParamsWithRole>,
|
||||
) -> AxumResult<FormattedResponse<CachedNodesResponse<SemiSkimmedNode>>> {
|
||||
@@ -0,0 +1,297 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::node_status_api::models::AxumResult;
|
||||
use crate::support::http::state::AppState;
|
||||
use crate::unstable_routes::v1::nym_nodes::helpers::{NodesParams, NodesParamsWithRole};
|
||||
use crate::unstable_routes::v1::nym_nodes::skimmed::helpers::{
|
||||
entry_gateways_basic, exit_gateways_basic, mixnodes_basic, nodes_basic,
|
||||
};
|
||||
use crate::unstable_routes::v1::nym_nodes::skimmed::{
|
||||
PaginatedCachedNodesResponseSchema, PaginatedSkimmedNodes,
|
||||
};
|
||||
use axum::extract::{Query, State};
|
||||
use nym_api_requests::nym_nodes::{CachedNodesResponse, NodeRoleQueryParam, SkimmedNode};
|
||||
use nym_http_api_common::FormattedResponse;
|
||||
|
||||
/// Deprecated query that gets ALL gateways
|
||||
#[utoipa::path(
|
||||
tag = "Unstable Nym Nodes",
|
||||
get,
|
||||
params(NodesParams),
|
||||
path = "/gateways/skimmed",
|
||||
context_path = "/v1/unstable/nym-nodes",
|
||||
responses(
|
||||
(status = 200, content(
|
||||
(CachedNodesResponse<SkimmedNode> = "application/json"),
|
||||
(CachedNodesResponse<SkimmedNode> = "application/yaml"),
|
||||
(CachedNodesResponse<SkimmedNode> = "application/bincode")
|
||||
))
|
||||
),
|
||||
)]
|
||||
#[deprecated(note = "use '/v1/unstable/nym-nodes/entry-gateways/skimmed/all' instead")]
|
||||
pub(crate) async fn deprecated_gateways_basic(
|
||||
state: State<AppState>,
|
||||
query_params: Query<NodesParams>,
|
||||
) -> AxumResult<FormattedResponse<CachedNodesResponse<SkimmedNode>>> {
|
||||
let output = query_params.output.unwrap_or_default();
|
||||
|
||||
// 1. call '/v1/unstable/skimmed/entry-gateways/all'
|
||||
let all_gateways = entry_gateways_basic_all(state, query_params)
|
||||
.await?
|
||||
.into_inner();
|
||||
|
||||
// 3. return result
|
||||
Ok(output.to_response(CachedNodesResponse {
|
||||
refreshed_at: all_gateways.metadata.refreshed_at,
|
||||
// 2. remove pagination
|
||||
nodes: all_gateways.nodes.data,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Deprecated query that gets ACTIVE-ONLY mixnodes
|
||||
#[utoipa::path(
|
||||
tag = "Unstable Nym Nodes",
|
||||
get,
|
||||
params(NodesParams),
|
||||
path = "/mixnodes/skimmed",
|
||||
context_path = "/v1/unstable/nym-nodes",
|
||||
responses(
|
||||
(status = 200, content(
|
||||
(CachedNodesResponse<SkimmedNode> = "application/json"),
|
||||
(CachedNodesResponse<SkimmedNode> = "application/yaml"),
|
||||
(CachedNodesResponse<SkimmedNode> = "application/bincode")
|
||||
))
|
||||
),
|
||||
)]
|
||||
#[deprecated(note = "use '/v1/unstable/nym-nodes/skimmed/mixnodes/active' instead")]
|
||||
pub(crate) async fn deprecated_mixnodes_basic(
|
||||
state: State<AppState>,
|
||||
query_params: Query<NodesParams>,
|
||||
) -> AxumResult<FormattedResponse<CachedNodesResponse<SkimmedNode>>> {
|
||||
let output = query_params.output.unwrap_or_default();
|
||||
|
||||
// 1. call '/v1/unstable/nym-nodes/skimmed/mixnodes/active'
|
||||
let active_mixnodes = mixnodes_basic_active(state, query_params)
|
||||
.await?
|
||||
.into_inner();
|
||||
|
||||
// 3. return result
|
||||
Ok(output.to_response(CachedNodesResponse {
|
||||
refreshed_at: active_mixnodes.metadata.refreshed_at,
|
||||
// 2. remove pagination
|
||||
nodes: active_mixnodes.nodes.data,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Return all Nym Nodes and optionally legacy mixnodes/gateways (if `no-legacy` flag is not used)
|
||||
/// that are currently bonded.
|
||||
#[utoipa::path(
|
||||
tag = "Unstable Nym Nodes",
|
||||
get,
|
||||
params(NodesParamsWithRole),
|
||||
path = "",
|
||||
context_path = "/v1/unstable/nym-nodes/skimmed",
|
||||
responses(
|
||||
(status = 200, content(
|
||||
(PaginatedCachedNodesResponseSchema = "application/json"),
|
||||
(PaginatedCachedNodesResponseSchema = "application/yaml"),
|
||||
(PaginatedCachedNodesResponseSchema = "application/bincode")
|
||||
))
|
||||
),
|
||||
)]
|
||||
pub(crate) async fn nodes_basic_all(
|
||||
state: State<AppState>,
|
||||
Query(query_params): Query<NodesParamsWithRole>,
|
||||
) -> PaginatedSkimmedNodes {
|
||||
if let Some(role) = query_params.role {
|
||||
return match role {
|
||||
NodeRoleQueryParam::ActiveMixnode => {
|
||||
mixnodes_basic_all(state, Query(query_params.into())).await
|
||||
}
|
||||
NodeRoleQueryParam::EntryGateway => {
|
||||
entry_gateways_basic_all(state, Query(query_params.into())).await
|
||||
}
|
||||
NodeRoleQueryParam::ExitGateway => {
|
||||
exit_gateways_basic_all(state, Query(query_params.into())).await
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
nodes_basic(state, Query(query_params.into()), false).await
|
||||
}
|
||||
|
||||
/// 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(
|
||||
tag = "Unstable Nym Nodes",
|
||||
get,
|
||||
params(NodesParams),
|
||||
path = "/active",
|
||||
context_path = "/v1/unstable/nym-nodes/skimmed",
|
||||
responses(
|
||||
(status = 200, content(
|
||||
(PaginatedCachedNodesResponseSchema = "application/json"),
|
||||
(PaginatedCachedNodesResponseSchema = "application/yaml"),
|
||||
(PaginatedCachedNodesResponseSchema = "application/bincode")
|
||||
))
|
||||
),
|
||||
)]
|
||||
pub(crate) async fn nodes_basic_active(
|
||||
state: State<AppState>,
|
||||
Query(query_params): Query<NodesParamsWithRole>,
|
||||
) -> PaginatedSkimmedNodes {
|
||||
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
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
nodes_basic(state, Query(query_params.into()), true).await
|
||||
}
|
||||
|
||||
/// Returns Nym Nodes and optionally legacy mixnodes (if `no-legacy` flag is not used)
|
||||
/// that are currently bonded and support mixing role.
|
||||
#[utoipa::path(
|
||||
tag = "Unstable Nym Nodes",
|
||||
get,
|
||||
params(NodesParams),
|
||||
path = "/mixnodes/all",
|
||||
context_path = "/v1/unstable/nym-nodes/skimmed",
|
||||
responses(
|
||||
(status = 200, content(
|
||||
(PaginatedCachedNodesResponseSchema = "application/json"),
|
||||
(PaginatedCachedNodesResponseSchema = "application/yaml"),
|
||||
(PaginatedCachedNodesResponseSchema = "application/bincode")
|
||||
))
|
||||
),
|
||||
)]
|
||||
pub(crate) async fn mixnodes_basic_all(
|
||||
state: State<AppState>,
|
||||
query_params: Query<NodesParams>,
|
||||
) -> PaginatedSkimmedNodes {
|
||||
mixnodes_basic(state, query_params, false).await
|
||||
}
|
||||
|
||||
/// Returns Nym Nodes and optionally legacy mixnodes (if `no-legacy` flag is not used)
|
||||
/// that are currently bonded and are in the active set with one of the mixing roles.
|
||||
#[utoipa::path(
|
||||
tag = "Unstable Nym Nodes",
|
||||
get,
|
||||
params(NodesParams),
|
||||
path = "/mixnodes/active",
|
||||
context_path = "/v1/unstable/nym-nodes/skimmed",
|
||||
responses(
|
||||
(status = 200, content(
|
||||
(PaginatedCachedNodesResponseSchema = "application/json"),
|
||||
(PaginatedCachedNodesResponseSchema = "application/yaml"),
|
||||
(PaginatedCachedNodesResponseSchema = "application/bincode")
|
||||
))
|
||||
),
|
||||
)]
|
||||
pub(crate) async fn mixnodes_basic_active(
|
||||
state: State<AppState>,
|
||||
query_params: Query<NodesParams>,
|
||||
) -> PaginatedSkimmedNodes {
|
||||
mixnodes_basic(state, query_params, true).await
|
||||
}
|
||||
|
||||
/// Returns Nym Nodes and optionally legacy gateways (if `no-legacy` flag is not used)
|
||||
/// that are currently bonded and are in the active set with the entry role.
|
||||
#[utoipa::path(
|
||||
tag = "Unstable Nym Nodes",
|
||||
get,
|
||||
params(NodesParams),
|
||||
path = "/entry-gateways/active",
|
||||
context_path = "/v1/unstable/nym-nodes/skimmed",
|
||||
responses(
|
||||
(status = 200, content(
|
||||
(PaginatedCachedNodesResponseSchema = "application/json"),
|
||||
(PaginatedCachedNodesResponseSchema = "application/yaml"),
|
||||
(PaginatedCachedNodesResponseSchema = "application/bincode")
|
||||
))
|
||||
),
|
||||
)]
|
||||
pub(crate) async fn entry_gateways_basic_active(
|
||||
state: State<AppState>,
|
||||
query_params: Query<NodesParams>,
|
||||
) -> PaginatedSkimmedNodes {
|
||||
entry_gateways_basic(state, query_params, true).await
|
||||
}
|
||||
|
||||
/// Returns Nym Nodes and optionally legacy gateways (if `no-legacy` flag is not used)
|
||||
/// that are currently bonded and support entry gateway role.
|
||||
#[utoipa::path(
|
||||
tag = "Unstable Nym Nodes",
|
||||
get,
|
||||
params(NodesParams),
|
||||
path = "/entry-gateways/all",
|
||||
context_path = "/v1/unstable/nym-nodes/skimmed",
|
||||
responses(
|
||||
(status = 200, content(
|
||||
(PaginatedCachedNodesResponseSchema = "application/json"),
|
||||
(PaginatedCachedNodesResponseSchema = "application/yaml"),
|
||||
(PaginatedCachedNodesResponseSchema = "application/bincode")
|
||||
))
|
||||
),
|
||||
)]
|
||||
pub(crate) async fn entry_gateways_basic_all(
|
||||
state: State<AppState>,
|
||||
query_params: Query<NodesParams>,
|
||||
) -> PaginatedSkimmedNodes {
|
||||
entry_gateways_basic(state, query_params, false).await
|
||||
}
|
||||
|
||||
/// Returns Nym Nodes and optionally legacy gateways (if `no-legacy` flag is not used)
|
||||
/// that are currently bonded and are in the active set with the exit role.
|
||||
#[utoipa::path(
|
||||
tag = "Unstable Nym Nodes",
|
||||
get,
|
||||
params(NodesParams),
|
||||
path = "/exit-gateways/active",
|
||||
context_path = "/v1/unstable/nym-nodes/skimmed",
|
||||
responses(
|
||||
(status = 200, content(
|
||||
(PaginatedCachedNodesResponseSchema = "application/json"),
|
||||
(PaginatedCachedNodesResponseSchema = "application/yaml"),
|
||||
(PaginatedCachedNodesResponseSchema = "application/bincode")
|
||||
))
|
||||
),
|
||||
)]
|
||||
pub(crate) async fn exit_gateways_basic_active(
|
||||
state: State<AppState>,
|
||||
query_params: Query<NodesParams>,
|
||||
) -> PaginatedSkimmedNodes {
|
||||
exit_gateways_basic(state, query_params, true).await
|
||||
}
|
||||
|
||||
/// Returns Nym Nodes and optionally legacy gateways (if `no-legacy` flag is not used)
|
||||
/// that are currently bonded and support exit gateway role.
|
||||
#[utoipa::path(
|
||||
tag = "Unstable Nym Nodes",
|
||||
get,
|
||||
params(NodesParams),
|
||||
path = "/exit-gateways/all",
|
||||
context_path = "/v1/unstable/nym-nodes/skimmed",
|
||||
responses(
|
||||
(status = 200, content(
|
||||
(PaginatedCachedNodesResponseSchema = "application/json"),
|
||||
(PaginatedCachedNodesResponseSchema = "application/yaml"),
|
||||
(PaginatedCachedNodesResponseSchema = "application/bincode")
|
||||
))
|
||||
),
|
||||
)]
|
||||
pub(crate) async fn exit_gateways_basic_all(
|
||||
state: State<AppState>,
|
||||
query_params: Query<NodesParams>,
|
||||
) -> PaginatedSkimmedNodes {
|
||||
exit_gateways_basic(state, query_params, false).await
|
||||
}
|
||||
@@ -0,0 +1,343 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::node_describe_cache::cache::DescribedNodes;
|
||||
use crate::node_status_api::models::AxumErrorResponse;
|
||||
use crate::support::caching::Cache;
|
||||
use crate::support::http::state::AppState;
|
||||
use crate::unstable_routes::v1::nym_nodes::helpers::{refreshed_at, LegacyAnnotation, NodesParams};
|
||||
use crate::unstable_routes::v1::nym_nodes::skimmed::PaginatedSkimmedNodes;
|
||||
use axum::extract::{Query, State};
|
||||
use nym_api_requests::models::{NodeAnnotation, NymNodeDescription};
|
||||
use nym_api_requests::nym_nodes::{NodeRole, PaginatedCachedNodesResponse, SkimmedNode};
|
||||
use nym_http_api_common::Output;
|
||||
use nym_mixnet_contract_common::NodeId;
|
||||
use nym_topology::CachedEpochRewardedSet;
|
||||
use std::collections::HashMap;
|
||||
use std::future::Future;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::RwLockReadGuard;
|
||||
use tracing::trace;
|
||||
|
||||
/// Given all relevant caches, build part of response for JUST Nym Nodes
|
||||
fn build_nym_nodes_response<'a, NI>(
|
||||
rewarded_set: &CachedEpochRewardedSet,
|
||||
nym_nodes_subset: NI,
|
||||
annotations: &HashMap<NodeId, NodeAnnotation>,
|
||||
current_key_rotation: u32,
|
||||
active_only: bool,
|
||||
) -> Vec<SkimmedNode>
|
||||
where
|
||||
NI: Iterator<Item = &'a NymNodeDescription> + 'a,
|
||||
{
|
||||
let mut nodes = Vec::new();
|
||||
for nym_node in nym_nodes_subset {
|
||||
let node_id = nym_node.node_id;
|
||||
|
||||
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(
|
||||
current_key_rotation,
|
||||
role,
|
||||
annotation.last_24h_performance,
|
||||
));
|
||||
}
|
||||
nodes
|
||||
}
|
||||
|
||||
/// Given all relevant caches, add appropriate legacy nodes to the part of the response
|
||||
fn add_legacy<LN>(
|
||||
nodes: &mut Vec<SkimmedNode>,
|
||||
rewarded_set: &CachedEpochRewardedSet,
|
||||
describe_cache: &DescribedNodes,
|
||||
annotated_legacy_nodes: &HashMap<NodeId, LN>,
|
||||
current_key_rotation: u32,
|
||||
active_only: bool,
|
||||
) where
|
||||
LN: LegacyAnnotation,
|
||||
{
|
||||
for (node_id, legacy) in annotated_legacy_nodes.iter() {
|
||||
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) {
|
||||
// legacy nodes don't support key rotation
|
||||
nodes.push(described.to_skimmed_node(current_key_rotation, 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
|
||||
pub(crate) async fn build_skimmed_nodes_response<'a, NI, LG, Fut, LN>(
|
||||
state: &'a AppState,
|
||||
Query(query_params): Query<NodesParams>,
|
||||
nym_nodes_subset: NI,
|
||||
annotated_legacy_nodes_getter: LG,
|
||||
active_only: bool,
|
||||
output: Output,
|
||||
) -> PaginatedSkimmedNodes
|
||||
where
|
||||
// iterator returning relevant subset of nym-nodes (like mixing nym-nodes, entries, etc.)
|
||||
NI: Iterator<Item = &'a NymNodeDescription> + 'a,
|
||||
|
||||
// async function that returns cache of appropriate legacy nodes (mixnodes or gateways)
|
||||
LG: Fn(&'a AppState) -> Fut,
|
||||
Fut:
|
||||
Future<Output = Result<RwLockReadGuard<'a, Cache<HashMap<NodeId, LN>>>, AxumErrorResponse>>,
|
||||
|
||||
// the legacy node (MixNodeBondAnnotated or GatewayBondAnnotated)
|
||||
LN: LegacyAnnotation + 'a,
|
||||
{
|
||||
// TODO: implement it
|
||||
let _ = query_params.per_page;
|
||||
let _ = query_params.page;
|
||||
|
||||
// 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?;
|
||||
|
||||
let contract_cache = state.nym_contract_cache();
|
||||
|
||||
let interval = contract_cache.current_interval().await?;
|
||||
let current_key_rotation = contract_cache.current_key_rotation_id().await?;
|
||||
|
||||
// 4.0 If the client indicates that they already know about the current topology send empty response
|
||||
if let Some(client_known_epoch) = query_params.epoch_id {
|
||||
if client_known_epoch == interval.current_epoch_id() {
|
||||
return Ok(output.to_response(PaginatedCachedNodesResponse::no_updates(
|
||||
interval.current_epoch_absolute_id(),
|
||||
current_key_rotation,
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
// 4. start building the response
|
||||
let mut nodes = build_nym_nodes_response(
|
||||
&rewarded_set,
|
||||
nym_nodes_subset,
|
||||
&annotations,
|
||||
current_key_rotation,
|
||||
active_only,
|
||||
);
|
||||
|
||||
// 5. if we allow legacy nodes, repeat the procedure for them, otherwise return just nym-nodes
|
||||
if let Some(true) = query_params.no_legacy {
|
||||
// min of all caches
|
||||
let refreshed_at = refreshed_at([
|
||||
rewarded_set.timestamp(),
|
||||
annotations.timestamp(),
|
||||
describe_cache.timestamp(),
|
||||
]);
|
||||
|
||||
return Ok(output.to_response(
|
||||
PaginatedCachedNodesResponse::new_full(
|
||||
interval.current_epoch_absolute_id(),
|
||||
current_key_rotation,
|
||||
refreshed_at,
|
||||
nodes,
|
||||
)
|
||||
.fresh(interval),
|
||||
));
|
||||
}
|
||||
|
||||
// 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,
|
||||
&rewarded_set,
|
||||
&describe_cache,
|
||||
&annotated_legacy_nodes,
|
||||
current_key_rotation,
|
||||
active_only,
|
||||
);
|
||||
|
||||
// min of all caches
|
||||
let refreshed_at = refreshed_at([
|
||||
rewarded_set.timestamp(),
|
||||
annotations.timestamp(),
|
||||
describe_cache.timestamp(),
|
||||
annotated_legacy_nodes.timestamp(),
|
||||
]);
|
||||
|
||||
let base_response = output.to_response(
|
||||
PaginatedCachedNodesResponse::new_full(
|
||||
interval.current_epoch_absolute_id(),
|
||||
current_key_rotation,
|
||||
refreshed_at,
|
||||
nodes,
|
||||
)
|
||||
.fresh(interval),
|
||||
);
|
||||
|
||||
if !active_only {
|
||||
return Ok(base_response);
|
||||
}
|
||||
|
||||
// if caller requested only active nodes, the response is valid until the epoch changes
|
||||
// (but add 2 minutes due to epoch transition not being instantaneous
|
||||
let epoch_end = interval.current_epoch_end();
|
||||
let expiration = epoch_end + Duration::from_secs(120);
|
||||
Ok(base_response.with_expires_header(expiration))
|
||||
}
|
||||
|
||||
pub(crate) async fn nodes_basic(
|
||||
state: State<AppState>,
|
||||
Query(query_params): Query<NodesParams>,
|
||||
active_only: bool,
|
||||
) -> PaginatedSkimmedNodes {
|
||||
let output = query_params.output.unwrap_or_default();
|
||||
|
||||
// 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 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 interval = state.nym_contract_cache().current_interval().await?;
|
||||
let current_key_rotation = state.nym_contract_cache().current_key_rotation_id().await?;
|
||||
|
||||
let mut nodes = build_nym_nodes_response(
|
||||
&rewarded_set,
|
||||
all_nym_nodes,
|
||||
&annotations,
|
||||
current_key_rotation,
|
||||
active_only,
|
||||
);
|
||||
|
||||
// add legacy gateways to the response
|
||||
add_legacy(
|
||||
&mut nodes,
|
||||
&rewarded_set,
|
||||
&describe_cache,
|
||||
&legacy_gateways,
|
||||
current_key_rotation,
|
||||
active_only,
|
||||
);
|
||||
|
||||
// add legacy mixnodes to the response
|
||||
add_legacy(
|
||||
&mut nodes,
|
||||
&rewarded_set,
|
||||
&describe_cache,
|
||||
&legacy_mixnodes,
|
||||
current_key_rotation,
|
||||
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(output.to_response(PaginatedCachedNodesResponse::new_full(
|
||||
current_key_rotation,
|
||||
interval.current_epoch_absolute_id(),
|
||||
refreshed_at,
|
||||
nodes,
|
||||
)))
|
||||
}
|
||||
|
||||
pub(crate) async fn mixnodes_basic(
|
||||
state: State<AppState>,
|
||||
query_params: Query<NodesParams>,
|
||||
active_only: bool,
|
||||
) -> PaginatedSkimmedNodes {
|
||||
let output = query_params.output.unwrap_or_default();
|
||||
|
||||
// 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();
|
||||
|
||||
build_skimmed_nodes_response(
|
||||
&state.0,
|
||||
query_params,
|
||||
mixing_nym_nodes,
|
||||
|state| state.legacy_mixnode_annotations(),
|
||||
active_only,
|
||||
output,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn entry_gateways_basic(
|
||||
state: State<AppState>,
|
||||
query_params: Query<NodesParams>,
|
||||
active_only: bool,
|
||||
) -> PaginatedSkimmedNodes {
|
||||
let output = query_params.output.unwrap_or_default();
|
||||
|
||||
// 1. grab all relevant described nym-nodes
|
||||
let describe_cache = state.describe_nodes_cache_data().await?;
|
||||
let mixing_nym_nodes = describe_cache.entry_capable_nym_nodes();
|
||||
|
||||
build_skimmed_nodes_response(
|
||||
&state.0,
|
||||
query_params,
|
||||
mixing_nym_nodes,
|
||||
|state| state.legacy_gateways_annotations(),
|
||||
active_only,
|
||||
output,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn exit_gateways_basic(
|
||||
state: State<AppState>,
|
||||
query_params: Query<NodesParams>,
|
||||
active_only: bool,
|
||||
) -> PaginatedSkimmedNodes {
|
||||
let output = query_params.output.unwrap_or_default();
|
||||
|
||||
// 1. grab all relevant described nym-nodes
|
||||
let describe_cache = state.describe_nodes_cache_data().await?;
|
||||
let mixing_nym_nodes = describe_cache.exit_capable_nym_nodes();
|
||||
|
||||
build_skimmed_nodes_response(
|
||||
&state.0,
|
||||
query_params,
|
||||
mixing_nym_nodes,
|
||||
|state| state.legacy_gateways_annotations(),
|
||||
active_only,
|
||||
output,
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::node_status_api::models::AxumResult;
|
||||
use nym_api_requests::models::OffsetDateTimeJsonSchemaWrapper;
|
||||
use nym_api_requests::nym_nodes::{PaginatedCachedNodesResponse, SkimmedNode};
|
||||
use nym_api_requests::pagination::PaginatedResponse;
|
||||
use nym_http_api_common::FormattedResponse;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
pub(crate) mod handlers;
|
||||
pub(crate) mod helpers;
|
||||
|
||||
pub type PaginatedSkimmedNodes =
|
||||
AxumResult<FormattedResponse<PaginatedCachedNodesResponse<SkimmedNode>>>;
|
||||
|
||||
pub(crate) use handlers::*;
|
||||
|
||||
#[allow(dead_code)] // not dead, used in OpenAPI docs
|
||||
#[derive(ToSchema)]
|
||||
#[schema(title = "PaginatedCachedNodesResponse")]
|
||||
pub struct PaginatedCachedNodesResponseSchema {
|
||||
pub refreshed_at: OffsetDateTimeJsonSchemaWrapper,
|
||||
#[schema(value_type = SkimmedNode)]
|
||||
pub nodes: PaginatedResponse<SkimmedNode>,
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::support::http::state::AppState;
|
||||
use axum::Router;
|
||||
|
||||
pub(crate) fn unstable_routes_v2() -> Router<AppState> {
|
||||
Router::new()
|
||||
}
|
||||
Reference in New Issue
Block a user