From 931ec03b28644ade2cd07d83fa0f52005506aad8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Fri, 12 Jun 2026 10:36:27 +0100 Subject: [PATCH] feat: expose node's chain address on self-described API (#6815) * feat: expose nym-nodes' on-chain address on v2 auxiliary endpoint * moved swagger page outside the v1 route * fixed swagger endpoint for nym-api * post rebasing fixes * remove redundant impl OfflineSigner for Arc --- .../src/signing/direct_wallet.rs | 2 + .../src/models/described/type_translation.rs | 4 +- nym-api/src/support/http/openapi.rs | 2 +- nym-gateway-probe/src/common/nodes.rs | 2 +- nym-node/nym-node-requests/src/api/client.rs | 4 +- nym-node/nym-node-requests/src/api/mod.rs | 1 + .../src/api/v1/node/models.rs | 13 +++- nym-node/nym-node-requests/src/api/v2/mod.rs | 4 ++ .../nym-node-requests/src/api/v2/node/mod.rs | 4 ++ .../src/api/v2/node/models.rs | 30 +++++++++ nym-node/nym-node-requests/src/lib.rs | 14 ++++ nym-node/src/node/http/router/api/mod.rs | 7 +- .../node/http/router/api/{v1 => }/openapi.rs | 22 ++++--- .../http/router/api/v1/authenticator/root.rs | 2 +- .../api/v1/gateway/client_interfaces/mod.rs | 6 +- .../node/http/router/api/v1/gateway/root.rs | 2 +- .../src/node/http/router/api/v1/health.rs | 2 +- .../router/api/v1/ip_packet_router/root.rs | 2 +- .../http/router/api/v1/lewes_protocol/mod.rs | 18 ++--- .../http/router/api/v1/lewes_protocol/root.rs | 9 +-- nym-node/src/node/http/router/api/v1/load.rs | 2 +- .../router/api/v1/metrics/legacy_mixing.rs | 2 +- .../router/api/v1/metrics/packets_stats.rs | 2 +- .../http/router/api/v1/metrics/prometheus.rs | 2 +- .../http/router/api/v1/metrics/sessions.rs | 2 +- .../node/http/router/api/v1/metrics/verloc.rs | 2 +- .../http/router/api/v1/metrics/wireguard.rs | 2 +- .../node/http/router/api/v1/mixnode/root.rs | 2 +- nym-node/src/node/http/router/api/v1/mod.rs | 9 ++- .../router/api/v1/network/upgrade_mode.rs | 2 +- .../api/v1/network_requester/exit_policy.rs | 2 +- .../router/api/v1/network_requester/root.rs | 2 +- .../node/http/router/api/v1/node/auxiliary.rs | 17 ++--- .../router/api/v1/node/build_information.rs | 9 +-- .../http/router/api/v1/node/description.rs | 9 +-- .../node/http/router/api/v1/node/hardware.rs | 9 +-- .../router/api/v1/node/host_information.rs | 2 +- .../src/node/http/router/api/v1/node/mod.rs | 53 +++------------ .../src/node/http/router/api/v1/node/roles.rs | 9 +-- nym-node/src/node/http/router/api/v2/mod.rs | 16 +++++ .../node/http/router/api/v2/node/auxiliary.rs | 35 ++++++++++ .../src/node/http/router/api/v2/node/mod.rs | 17 +++++ nym-node/src/node/http/router/landing_page.rs | 2 +- nym-node/src/node/http/router/mod.rs | 55 ++++------------ nym-node/src/node/http/state/mod.rs | 33 +++++++++- nym-node/src/node/mod.rs | 66 ++++++++++++------- 46 files changed, 320 insertions(+), 193 deletions(-) create mode 100644 nym-node/nym-node-requests/src/api/v2/mod.rs create mode 100644 nym-node/nym-node-requests/src/api/v2/node/mod.rs create mode 100644 nym-node/nym-node-requests/src/api/v2/node/models.rs rename nym-node/src/node/http/router/api/{v1 => }/openapi.rs (85%) create mode 100644 nym-node/src/node/http/router/api/v2/mod.rs create mode 100644 nym-node/src/node/http/router/api/v2/node/auxiliary.rs create mode 100644 nym-node/src/node/http/router/api/v2/node/mod.rs diff --git a/common/client-libs/validator-client/src/signing/direct_wallet.rs b/common/client-libs/validator-client/src/signing/direct_wallet.rs index 2cc64f8bfa..b7cd47c429 100644 --- a/common/client-libs/validator-client/src/signing/direct_wallet.rs +++ b/common/client-libs/validator-client/src/signing/direct_wallet.rs @@ -11,6 +11,8 @@ use cosmrs::tx; use cosmrs::tx::SignDoc; use nym_config::defaults; use std::borrow::Cow; +use std::ops::Deref; +use std::sync::Arc; use thiserror::Error; use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing}; diff --git a/nym-api/nym-api-requests/src/models/described/type_translation.rs b/nym-api/nym-api-requests/src/models/described/type_translation.rs index b1a7df95b1..e2a415f58e 100644 --- a/nym-api/nym-api-requests/src/models/described/type_translation.rs +++ b/nym-api/nym-api-requests/src/models/described/type_translation.rs @@ -395,8 +395,8 @@ impl From for AnnounceP } } -impl From for AuxiliaryDetailsV1 { - fn from(value: nym_node_requests::api::v1::node::models::AuxiliaryDetails) -> Self { +impl From for AuxiliaryDetailsV1 { + fn from(value: nym_node_requests::api::v1::node::models::AuxiliaryDetailsV1) -> Self { AuxiliaryDetailsV1 { location: value.location, announce_ports: value.announce_ports.into(), diff --git a/nym-api/src/support/http/openapi.rs b/nym-api/src/support/http/openapi.rs index 9d2f49af93..3ab80285f7 100644 --- a/nym-api/src/support/http/openapi.rs +++ b/nym-api/src/support/http/openapi.rs @@ -41,7 +41,7 @@ use utoipauto::utoipauto; nym_config::defaults::NymContracts, ContractVersionSchemaResponse, nym_bin_common::build_information::BinaryBuildInformationOwned, - nym_node_requests::api::v1::node::models::AuxiliaryDetails, + nym_node_requests::api::v1::node::models::AuxiliaryDetailsV1, nym_contracts_common::ContractBuildInformation )) )] diff --git a/nym-gateway-probe/src/common/nodes.rs b/nym-gateway-probe/src/common/nodes.rs index 85c87614d0..0c734d2e74 100644 --- a/nym-gateway-probe/src/common/nodes.rs +++ b/nym-gateway-probe/src/common/nodes.rs @@ -16,7 +16,7 @@ use nym_lp::peer::{DHPublicKey, LpRemotePeer}; use nym_lp_data::packet::version; use nym_network_defaults::DEFAULT_NYM_NODE_HTTP_PORT; use nym_node_requests::api::client::NymNodeApiClientExt; -use nym_node_requests::api::v1::node::models::AuxiliaryDetails as NodeAuxiliaryDetails; +use nym_node_requests::api::v1::node::models::AuxiliaryDetailsV1 as NodeAuxiliaryDetails; use nym_sdk::mixnet::NodeIdentity; use nym_sdk::mixnet::Recipient; use nym_validator_client::client::NymApiClientExt; diff --git a/nym-node/nym-node-requests/src/api/client.rs b/nym-node/nym-node-requests/src/api/client.rs index 6e3b93eeca..9b1c06f1c2 100644 --- a/nym-node/nym-node-requests/src/api/client.rs +++ b/nym-node/nym-node-requests/src/api/client.rs @@ -11,7 +11,7 @@ use crate::api::v1::ip_packet_router::models::IpPacketRouter; use crate::api::v1::network_requester::exit_policy::models::UsedExitPolicy; use crate::api::v1::network_requester::models::NetworkRequester; use crate::api::v1::node::models::{ - AuxiliaryDetails, NodeDescription, NodeRoles, SignedHostInformation, + AuxiliaryDetailsV1, NodeDescription, NodeRoles, SignedHostInformation, }; use crate::api::v1::node_load::models::NodeLoad; use crate::routes; @@ -55,7 +55,7 @@ pub trait NymNodeApiClientExt: ApiClient { self.get_json_from(routes::api::v1::roles_absolute()).await } - async fn get_auxiliary_details(&self) -> Result { + async fn get_auxiliary_details(&self) -> Result { self.get_json_from(routes::api::v1::auxiliary_absolute()) .await } diff --git a/nym-node/nym-node-requests/src/api/mod.rs b/nym-node/nym-node-requests/src/api/mod.rs index ea539a82f7..61c1da5676 100644 --- a/nym-node/nym-node-requests/src/api/mod.rs +++ b/nym-node/nym-node-requests/src/api/mod.rs @@ -15,6 +15,7 @@ use std::ops::Deref; pub mod client; pub mod helpers; pub mod v1; +pub mod v2; #[cfg(feature = "client")] pub use client::Client; diff --git a/nym-node/nym-node-requests/src/api/v1/node/models.rs b/nym-node/nym-node-requests/src/api/v1/node/models.rs index 4a6a750688..5e6c642988 100644 --- a/nym-node/nym-node-requests/src/api/v1/node/models.rs +++ b/nym-node/nym-node-requests/src/api/v1/node/models.rs @@ -12,6 +12,7 @@ use serde::{Deserialize, Serialize}; use std::net::IpAddr; pub use crate::api::SignedHostInformation; +use crate::api::v2::node::models::AuxiliaryDetailsV2; pub use nym_bin_common::build_information::BinaryBuildInformationOwned; #[derive(Clone, Default, Debug, Copy, Serialize, Deserialize, JsonSchema)] @@ -366,7 +367,7 @@ pub struct NodeDescription { /// Auxiliary details of the associated Nym Node. #[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)] #[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] -pub struct AuxiliaryDetails { +pub struct AuxiliaryDetailsV1 { /// Optional ISO 3166 alpha-2 two-letter country code of the node's **physical** location #[cfg_attr(feature = "openapi", schema(example = "PL", value_type = Option))] #[schemars(with = "Option")] @@ -383,6 +384,16 @@ pub struct AuxiliaryDetails { pub accepted_operator_terms_and_conditions: bool, } +impl From for AuxiliaryDetailsV1 { + fn from(v2: AuxiliaryDetailsV2) -> Self { + Self { + location: v2.location, + announce_ports: v2.announce_ports, + accepted_operator_terms_and_conditions: v2.accepted_operator_terms_and_conditions, + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/nym-node/nym-node-requests/src/api/v2/mod.rs b/nym-node/nym-node-requests/src/api/v2/mod.rs new file mode 100644 index 0000000000..71d9c3e9ec --- /dev/null +++ b/nym-node/nym-node-requests/src/api/v2/mod.rs @@ -0,0 +1,4 @@ +// Copyright 2026 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +pub mod node; diff --git a/nym-node/nym-node-requests/src/api/v2/node/mod.rs b/nym-node/nym-node-requests/src/api/v2/node/mod.rs new file mode 100644 index 0000000000..4ef39f2897 --- /dev/null +++ b/nym-node/nym-node-requests/src/api/v2/node/mod.rs @@ -0,0 +1,4 @@ +// Copyright 2026 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +pub mod models; diff --git a/nym-node/nym-node-requests/src/api/v2/node/models.rs b/nym-node/nym-node-requests/src/api/v2/node/models.rs new file mode 100644 index 0000000000..7b8cab88ca --- /dev/null +++ b/nym-node/nym-node-requests/src/api/v2/node/models.rs @@ -0,0 +1,30 @@ +// Copyright 2026 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::api::v1::node::models::AnnouncePorts; +use celes::Country; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +/// Auxiliary details of the associated Nym Node. +#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +pub struct AuxiliaryDetailsV2 { + /// Optional ISO 3166 alpha-2 two-letter country code of the node's **physical** location + #[cfg_attr(feature = "openapi", schema(example = "PL", value_type = Option))] + #[schemars(with = "Option")] + #[schemars(length(equal = 2))] + pub location: Option, + + /// On-chain address of this node + pub address: String, + + #[serde(default)] + pub announce_ports: AnnouncePorts, + + /// Specifies whether this node operator has agreed to the terms and conditions + /// as defined at + // make sure to include the default deserialisation as this field hasn't existed when the struct was first created + #[serde(default)] + pub accepted_operator_terms_and_conditions: bool, +} diff --git a/nym-node/nym-node-requests/src/lib.rs b/nym-node/nym-node-requests/src/lib.rs index f6b72055c7..759abf2445 100644 --- a/nym-node/nym-node-requests/src/lib.rs +++ b/nym-node/nym-node-requests/src/lib.rs @@ -23,8 +23,14 @@ pub mod routes { pub mod api { pub const V1: &str = "/v1"; + pub const V2: &str = "/v2"; + + // canonical, version-neutral Swagger UI mount + pub const SWAGGER: &str = "/swagger"; absolute_route!(v1_absolute, super::API, V1); + absolute_route!(v2_absolute, super::API, V2); + absolute_route!(swagger_absolute, super::API, SWAGGER); pub mod v1 { use super::*; @@ -152,6 +158,14 @@ pub mod routes { // use super::*; } } + + pub mod v2 { + use super::*; + + pub const AUXILIARY: &str = "/auxiliary-details"; + + absolute_route!(auxiliary_absolute, v2_absolute(), AUXILIARY); + } } } diff --git a/nym-node/src/node/http/router/api/mod.rs b/nym-node/src/node/http/router/api/mod.rs index ea3c5e4c05..46b2450e45 100644 --- a/nym-node/src/node/http/router/api/mod.rs +++ b/nym-node/src/node/http/router/api/mod.rs @@ -5,15 +5,20 @@ use crate::node::http::state::AppState; use axum::Router; use nym_node_requests::routes; +pub mod openapi; pub mod v1; +pub mod v2; pub use nym_node_requests::api as api_requests; #[derive(Debug, Clone)] pub struct Config { pub v1_config: v1::Config, + pub v2_config: v2::Config, } pub(super) fn routes(config: Config) -> Router { - Router::new().nest(routes::api::V1, v1::routes(config.v1_config)) + Router::new() + .nest(routes::api::V1, v1::routes(config.v1_config)) + .nest(routes::api::V2, v2::routes(config.v2_config)) } diff --git a/nym-node/src/node/http/router/api/v1/openapi.rs b/nym-node/src/node/http/router/api/openapi.rs similarity index 85% rename from nym-node/src/node/http/router/api/v1/openapi.rs rename to nym-node/src/node/http/router/api/openapi.rs index 721b68b5b0..c1ca7b2d7d 100644 --- a/nym-node/src/node/http/router/api/v1/openapi.rs +++ b/nym-node/src/node/http/router/api/openapi.rs @@ -3,7 +3,7 @@ use axum::Router; use nym_node_requests::api as api_requests; -use nym_node_requests::routes::api::{v1, v1_absolute}; +use nym_node_requests::routes; use utoipa::openapi::security::{Http, HttpAuthScheme}; use utoipa::{Modify, OpenApi, openapi::security::SecurityScheme}; use utoipa_swagger_ui::SwaggerUi; @@ -37,12 +37,14 @@ use utoipa_swagger_ui::SwaggerUi; crate::node::http::router::api::v1::gateway::client_interfaces::wireguard_details, crate::node::http::router::api::v1::gateway::root::root_gateway, crate::node::http::router::api::v1::lewes_protocol::root::root_lewes_protocol, - + crate::node::http::router::api::v2::node::auxiliary::auxiliary, ), components( schemas( nym_http_api_common::Output, nym_http_api_common::OutputParams, + nym_http_api_common::OutputV2, + nym_http_api_common::OutputParamsV2, api_requests::v1::health::models::NodeHealth, api_requests::v1::health::models::NodeStatus, api_requests::v1::node_load::models::NodeLoad, @@ -56,7 +58,7 @@ use utoipa_swagger_ui::SwaggerUi; api_requests::v1::node::models::Cpu, api_requests::v1::node::models::CryptoHardware, api_requests::v1::node::models::NodeDescription, - api_requests::v1::node::models::AuxiliaryDetails, + api_requests::v1::node::models::AuxiliaryDetailsV1, api_requests::v1::metrics::models::LegacyMixingStats, api_requests::v1::metrics::models::VerlocStats, api_requests::v1::metrics::models::VerlocResult, @@ -77,6 +79,7 @@ use utoipa_swagger_ui::SwaggerUi; api_requests::v1::network_requester::exit_policy::models::UsedExitPolicy, api_requests::v1::ip_packet_router::models::IpPacketRouter, api_requests::v1::lewes_protocol::models::LewesProtocol, + api_requests::v2::node::models::AuxiliaryDetailsV2, ), ), modifiers(&SecurityAddon), @@ -97,11 +100,14 @@ impl Modify for SecurityAddon { } pub(crate) fn route() -> Router { - // provide absolute path to the openapi.json - let config = - utoipa_swagger_ui::Config::from(format!("{}/api-docs/openapi.json", v1_absolute())); - SwaggerUi::new(v1::SWAGGER) - .url("/api-docs/openapi.json", ApiDoc::openapi()) + // SwaggerUi must be mounted with its absolute path: it emits internal redirects + // (e.g. `/swagger` → `/swagger/`) whose `Location` header uses this string + // literally and is not aware of any `.nest()` prefix above it. For the same + // reason, this router must be merged at the outer router level — not nested. + let openapi_json = format!("{}/api-docs/openapi.json", routes::API); + let config = utoipa_swagger_ui::Config::from(openapi_json.clone()); + SwaggerUi::new(routes::api::swagger_absolute()) + .url(openapi_json, ApiDoc::openapi()) .config(config) .into() } diff --git a/nym-node/src/node/http/router/api/v1/authenticator/root.rs b/nym-node/src/node/http/router/api/v1/authenticator/root.rs index 6c6ed9c835..470f76a8a3 100644 --- a/nym-node/src/node/http/router/api/v1/authenticator/root.rs +++ b/nym-node/src/node/http/router/api/v1/authenticator/root.rs @@ -11,7 +11,7 @@ use nym_node_requests::api::v1::authenticator::models::Authenticator; get, path = "", context_path = "/api/v1/authenticator", - tag = "Authenticator", + tag = "v1 / Authenticator", responses( (status = 501, description = "the endpoint hasn't been implemented yet"), (status = 200, content( diff --git a/nym-node/src/node/http/router/api/v1/gateway/client_interfaces/mod.rs b/nym-node/src/node/http/router/api/v1/gateway/client_interfaces/mod.rs index 703df8e7ff..b0e04b480e 100644 --- a/nym-node/src/node/http/router/api/v1/gateway/client_interfaces/mod.rs +++ b/nym-node/src/node/http/router/api/v1/gateway/client_interfaces/mod.rs @@ -41,7 +41,7 @@ pub(crate) fn routes( get, path = "/client-interfaces", context_path = "/api/v1/gateway", - tag = "Gateway", + tag = "v1 / Gateway", responses( (status = 501, description = "the endpoint hasn't been implemented yet"), (status = 200, content( @@ -67,7 +67,7 @@ pub type ClientInterfacesResponse = FormattedResponse; get, path = "/mixnet-websockets", context_path = "/api/v1/gateway/client-interfaces", - tag = "Gateway", + tag = "v1 / Gateway", responses( (status = 501, description = "the endpoint hasn't been implemented yet"), (status = 200, content( @@ -93,7 +93,7 @@ pub type MixnetWebSocketsResponse = FormattedResponse; get, path = "/wireguard", context_path = "/api/v1/gateway/client-interfaces", - tag = "Gateway", + tag = "v1 / Gateway", responses( (status = 501, description = "the endpoint hasn't been implemented yet"), (status = 200, content( diff --git a/nym-node/src/node/http/router/api/v1/gateway/root.rs b/nym-node/src/node/http/router/api/v1/gateway/root.rs index 2d96374b2e..11b27f4578 100644 --- a/nym-node/src/node/http/router/api/v1/gateway/root.rs +++ b/nym-node/src/node/http/router/api/v1/gateway/root.rs @@ -11,7 +11,7 @@ use nym_node_requests::api::v1::gateway::models::Gateway; get, path = "", context_path = "/api/v1/gateway", - tag = "Gateway", + tag = "v1 / Gateway", responses( (status = 501, description = "the endpoint hasn't been implemented yet"), (status = 200, content( diff --git a/nym-node/src/node/http/router/api/v1/health.rs b/nym-node/src/node/http/router/api/v1/health.rs index 7aed916fe9..1ff2ca3c5c 100644 --- a/nym-node/src/node/http/router/api/v1/health.rs +++ b/nym-node/src/node/http/router/api/v1/health.rs @@ -11,7 +11,7 @@ use nym_node_requests::api::v1::health::models::NodeHealth; get, path = "/health", context_path = "/api/v1", - tag = "Health", + tag = "v1 / Health", responses( (status = 200, content( (NodeHealth = "application/json"), diff --git a/nym-node/src/node/http/router/api/v1/ip_packet_router/root.rs b/nym-node/src/node/http/router/api/v1/ip_packet_router/root.rs index e85cee897e..6dff10720c 100644 --- a/nym-node/src/node/http/router/api/v1/ip_packet_router/root.rs +++ b/nym-node/src/node/http/router/api/v1/ip_packet_router/root.rs @@ -11,7 +11,7 @@ use nym_node_requests::api::v1::ip_packet_router::models::IpPacketRouter; get, path = "", context_path = "/api/v1/ip-packet-router", - tag = "IP Packet Router", + tag = "v1 / IP Packet Router", responses( (status = 501, description = "the endpoint hasn't been implemented yet"), (status = 200, content( diff --git a/nym-node/src/node/http/router/api/v1/lewes_protocol/mod.rs b/nym-node/src/node/http/router/api/v1/lewes_protocol/mod.rs index 6e0aa38e58..caa17ba54b 100644 --- a/nym-node/src/node/http/router/api/v1/lewes_protocol/mod.rs +++ b/nym-node/src/node/http/router/api/v1/lewes_protocol/mod.rs @@ -1,23 +1,15 @@ // Copyright 2026 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use crate::node::http::state::AppState; use axum::Router; use axum::routing::get; -use nym_node_requests::api::SignedLewesProtocol; pub mod root; -#[derive(Debug, Clone)] -pub struct Config { - pub details: SignedLewesProtocol, -} +#[derive(Debug, Clone, Default)] +pub struct Config {} -pub(crate) fn routes(config: Config) -> Router { - Router::new().route( - "/", - get({ - let lp_config = config.details; - move |query| root::root_lewes_protocol(lp_config, query) - }), - ) +pub(crate) fn routes(_config: Config) -> Router { + Router::new().route("/", get(root::root_lewes_protocol)) } diff --git a/nym-node/src/node/http/router/api/v1/lewes_protocol/root.rs b/nym-node/src/node/http/router/api/v1/lewes_protocol/root.rs index 06b2961717..b0065ad884 100644 --- a/nym-node/src/node/http/router/api/v1/lewes_protocol/root.rs +++ b/nym-node/src/node/http/router/api/v1/lewes_protocol/root.rs @@ -1,7 +1,8 @@ // Copyright 2026 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use axum::extract::Query; +use crate::node::http::state::AppState; +use axum::extract::{Query, State}; use axum::http::StatusCode; use nym_http_api_common::{FormattedResponse, OutputParams}; use nym_node_requests::api::{SignedLewesProtocol, SignedLewesProtocolInfo}; @@ -11,7 +12,7 @@ use nym_node_requests::api::{SignedLewesProtocol, SignedLewesProtocolInfo}; get, path = "/lewes-protocol", context_path = "/api/v1", - tag = "Lewes Protocol", + tag = "v1 / Lewes Protocol", responses( (status = 501, description = "the endpoint hasn't been implemented yet"), (status = 200, content( @@ -23,10 +24,10 @@ use nym_node_requests::api::{SignedLewesProtocol, SignedLewesProtocolInfo}; params(OutputParams) )] pub(crate) async fn root_lewes_protocol( - config: SignedLewesProtocol, Query(output): Query, + State(state): State, ) -> Result { - Ok(output.to_response(config)) + Ok(output.to_response(state.static_information.lewes_protocol.clone())) } pub type LewesProtocolResponse = FormattedResponse; diff --git a/nym-node/src/node/http/router/api/v1/load.rs b/nym-node/src/node/http/router/api/v1/load.rs index de7d1874dd..8cf82be099 100644 --- a/nym-node/src/node/http/router/api/v1/load.rs +++ b/nym-node/src/node/http/router/api/v1/load.rs @@ -11,7 +11,7 @@ use nym_node_requests::api::v1::node_load::models::NodeLoad; get, path = "/load", context_path = "/api/v1", - tag = "Node", + tag = "v1 / Node", responses( (status = 200, content( (NodeLoad = "application/json"), diff --git a/nym-node/src/node/http/router/api/v1/metrics/legacy_mixing.rs b/nym-node/src/node/http/router/api/v1/metrics/legacy_mixing.rs index de9126aead..a0f3cea16f 100644 --- a/nym-node/src/node/http/router/api/v1/metrics/legacy_mixing.rs +++ b/nym-node/src/node/http/router/api/v1/metrics/legacy_mixing.rs @@ -13,7 +13,7 @@ use nym_node_requests::api::v1::metrics::models::LegacyMixingStats; get, path = "/mixing", context_path = "/api/v1/metrics", - tag = "Metrics", + tag = "v1 / Metrics", responses( (status = 200, content( (LegacyMixingStats = "application/json"), diff --git a/nym-node/src/node/http/router/api/v1/metrics/packets_stats.rs b/nym-node/src/node/http/router/api/v1/metrics/packets_stats.rs index 347a767789..189bce7b02 100644 --- a/nym-node/src/node/http/router/api/v1/metrics/packets_stats.rs +++ b/nym-node/src/node/http/router/api/v1/metrics/packets_stats.rs @@ -15,7 +15,7 @@ use nym_node_requests::api::v1::metrics::models::packets::{ get, path = "/packets-stats", context_path = "/api/v1/metrics", - tag = "Metrics", + tag = "v1 / Metrics", responses( (status = 200, content( (PacketsStats = "application/json"), diff --git a/nym-node/src/node/http/router/api/v1/metrics/prometheus.rs b/nym-node/src/node/http/router/api/v1/metrics/prometheus.rs index 2bd8a3356b..71bd5f6423 100644 --- a/nym-node/src/node/http/router/api/v1/metrics/prometheus.rs +++ b/nym-node/src/node/http/router/api/v1/metrics/prometheus.rs @@ -8,7 +8,7 @@ use nym_metrics::metrics; get, path = "/prometheus", context_path = "/api/v1/metrics", - tag = "Metrics", + tag = "v1 / Metrics", responses( (status = 200, body = String), (status = 400, description = "`Authorization` header was missing"), diff --git a/nym-node/src/node/http/router/api/v1/metrics/sessions.rs b/nym-node/src/node/http/router/api/v1/metrics/sessions.rs index e2fd969e17..e84f73e61c 100644 --- a/nym-node/src/node/http/router/api/v1/metrics/sessions.rs +++ b/nym-node/src/node/http/router/api/v1/metrics/sessions.rs @@ -14,7 +14,7 @@ use time::macros::time; get, path = "/sessions", context_path = "/api/v1/metrics", - tag = "Metrics", + tag = "v1 / Metrics", responses( (status = 200, content( (SessionStats = "application/json"), diff --git a/nym-node/src/node/http/router/api/v1/metrics/verloc.rs b/nym-node/src/node/http/router/api/v1/metrics/verloc.rs index df8fbb5083..511c442005 100644 --- a/nym-node/src/node/http/router/api/v1/metrics/verloc.rs +++ b/nym-node/src/node/http/router/api/v1/metrics/verloc.rs @@ -15,7 +15,7 @@ use crate::node::http::state::metrics::MetricsAppState; get, path = "/verloc", context_path = "/api/v1/metrics", - tag = "Metrics", + tag = "v1 / Metrics", responses( (status = 200, content( (VerlocStats = "application/json"), diff --git a/nym-node/src/node/http/router/api/v1/metrics/wireguard.rs b/nym-node/src/node/http/router/api/v1/metrics/wireguard.rs index 1e4668a805..3831203d23 100644 --- a/nym-node/src/node/http/router/api/v1/metrics/wireguard.rs +++ b/nym-node/src/node/http/router/api/v1/metrics/wireguard.rs @@ -13,7 +13,7 @@ use nym_node_requests::api::v1::metrics::models::WireguardStats; get, path = "/wireguard-stats", context_path = "/api/v1/metrics", - tag = "Metrics", + tag = "v1 / Metrics", responses( (status = 200, content( (WireguardStats = "application/json"), diff --git a/nym-node/src/node/http/router/api/v1/mixnode/root.rs b/nym-node/src/node/http/router/api/v1/mixnode/root.rs index dff028e6c2..a92e2403f2 100644 --- a/nym-node/src/node/http/router/api/v1/mixnode/root.rs +++ b/nym-node/src/node/http/router/api/v1/mixnode/root.rs @@ -11,7 +11,7 @@ use nym_node_requests::api::v1::mixnode::models::Mixnode; get, path = "", context_path = "/api/v1/mixnode", - tag = "Mixnode", + tag = "v1 / Mixnode", responses( (status = 501, description = "the endpoint hasn't been implemented yet"), (status = 200, content( diff --git a/nym-node/src/node/http/router/api/v1/mod.rs b/nym-node/src/node/http/router/api/v1/mod.rs index 0b1bf14b4a..78a70796f2 100644 --- a/nym-node/src/node/http/router/api/v1/mod.rs +++ b/nym-node/src/node/http/router/api/v1/mod.rs @@ -3,7 +3,9 @@ use crate::node::http::state::AppState; use axum::Router; +use axum::response::Redirect; use axum::routing::get; +use nym_node_requests::routes; use nym_node_requests::routes::api::v1; pub mod authenticator; @@ -18,7 +20,6 @@ pub mod mixnode; pub mod network; pub mod network_requester; pub mod node; -pub mod openapi; #[derive(Debug, Clone)] pub struct Config { @@ -34,7 +35,12 @@ pub struct Config { } pub(super) fn routes(config: Config) -> Router { + // legacy redirects: the Swagger UI moved to a version-neutral /api/swagger + let swagger_redirect = get(|| async { Redirect::temporary(&routes::api::swagger_absolute()) }); + Router::new() + .route(v1::SWAGGER, swagger_redirect.clone()) + .route(&format!("{}/", v1::SWAGGER), swagger_redirect) .route(v1::HEALTH, get(health::root_health)) .route(v1::LOAD, get(load::root_load)) .nest(v1::NETWORK, network::routes()) @@ -59,5 +65,4 @@ pub(super) fn routes(config: Config) -> Router { lewes_protocol::routes(config.lewes_protocol), ) .merge(node::routes(config.node)) - .merge(openapi::route()) } diff --git a/nym-node/src/node/http/router/api/v1/network/upgrade_mode.rs b/nym-node/src/node/http/router/api/v1/network/upgrade_mode.rs index f487c9a4fe..131013d042 100644 --- a/nym-node/src/node/http/router/api/v1/network/upgrade_mode.rs +++ b/nym-node/src/node/http/router/api/v1/network/upgrade_mode.rs @@ -11,7 +11,7 @@ use nym_node_requests::api::v1::network::models::UpgradeModeStatus; get, path = "/upgrade-mode-status", context_path = "/api/v1/network", - tag = "Network", + tag = "v1 / Network", responses( (status = 200, content( (UpgradeModeStatus = "application/json"), diff --git a/nym-node/src/node/http/router/api/v1/network_requester/exit_policy.rs b/nym-node/src/node/http/router/api/v1/network_requester/exit_policy.rs index 1640fb0d66..00e473d7b4 100644 --- a/nym-node/src/node/http/router/api/v1/network_requester/exit_policy.rs +++ b/nym-node/src/node/http/router/api/v1/network_requester/exit_policy.rs @@ -10,7 +10,7 @@ use nym_node_requests::api::v1::network_requester::exit_policy::models::UsedExit get, path = "/exit-policy", context_path = "/api/v1/network-requester", - tag = "Network Requester", + tag = "v1 / Network Requester", responses( (status = 200, content( (UsedExitPolicy = "application/json"), diff --git a/nym-node/src/node/http/router/api/v1/network_requester/root.rs b/nym-node/src/node/http/router/api/v1/network_requester/root.rs index fe9f2c07a2..212ea9708b 100644 --- a/nym-node/src/node/http/router/api/v1/network_requester/root.rs +++ b/nym-node/src/node/http/router/api/v1/network_requester/root.rs @@ -11,7 +11,7 @@ use nym_node_requests::api::v1::network_requester::models::NetworkRequester; get, path = "", context_path = "/api/v1/network-requester", - tag = "Network Requester", + tag = "v1 / Network Requester", responses( (status = 501, description = "the endpoint hasn't been implemented yet"), (status = 200, content( diff --git a/nym-node/src/node/http/router/api/v1/node/auxiliary.rs b/nym-node/src/node/http/router/api/v1/node/auxiliary.rs index b163371ef2..8fd0d95d75 100644 --- a/nym-node/src/node/http/router/api/v1/node/auxiliary.rs +++ b/nym-node/src/node/http/router/api/v1/node/auxiliary.rs @@ -2,30 +2,31 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::node::http::router::types::RequestError; -use axum::extract::Query; +use crate::node::http::state::AppState; +use axum::extract::{Query, State}; use nym_http_api_common::{FormattedResponse, OutputParams}; -use nym_node_requests::api::v1::node::models::AuxiliaryDetails; +use nym_node_requests::api::v1::node::models::AuxiliaryDetailsV1; /// Returns auxiliary details of this node. #[utoipa::path( get, path = "/auxiliary-details", context_path = "/api/v1", - tag = "Node", + tag = "v1 / Node", responses( (status = 200, content( - (AuxiliaryDetails = "application/json"), - (AuxiliaryDetails = "application/yaml") + (AuxiliaryDetailsV1 = "application/json"), + (AuxiliaryDetailsV1 = "application/yaml") )), ), params(OutputParams) )] pub(crate) async fn auxiliary( - description: AuxiliaryDetails, Query(output): Query, + State(state): State, ) -> Result { let output = output.output.unwrap_or_default(); - Ok(output.to_response(description)) + Ok(output.to_response(state.static_information.auxiliary_data.clone().into())) } -pub type AuxiliaryDetailsResponse = FormattedResponse; +pub type AuxiliaryDetailsResponse = FormattedResponse; diff --git a/nym-node/src/node/http/router/api/v1/node/build_information.rs b/nym-node/src/node/http/router/api/v1/node/build_information.rs index 7761a99f4d..f8ce3e620c 100644 --- a/nym-node/src/node/http/router/api/v1/node/build_information.rs +++ b/nym-node/src/node/http/router/api/v1/node/build_information.rs @@ -1,7 +1,8 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use axum::extract::Query; +use crate::node::http::state::AppState; +use axum::extract::{Query, State}; use nym_http_api_common::{FormattedResponse, OutputParams}; use nym_node_requests::api::v1::node::models::BinaryBuildInformationOwned; @@ -10,7 +11,7 @@ use nym_node_requests::api::v1::node::models::BinaryBuildInformationOwned; get, path = "/build-information", context_path = "/api/v1", - tag = "Node", + tag = "v1 / Node", responses( (status = 200, content( (BinaryBuildInformationOwned = "application/json"), @@ -20,11 +21,11 @@ use nym_node_requests::api::v1::node::models::BinaryBuildInformationOwned; params(OutputParams) )] pub(crate) async fn build_information( - build_information: BinaryBuildInformationOwned, Query(output): Query, + State(state): State, ) -> BuildInformationResponse { let output = output.output.unwrap_or_default(); - output.to_response(build_information) + output.to_response(state.static_information.build_information.clone()) } pub type BuildInformationResponse = FormattedResponse; diff --git a/nym-node/src/node/http/router/api/v1/node/description.rs b/nym-node/src/node/http/router/api/v1/node/description.rs index bd6e4ee8b5..84a0c56208 100644 --- a/nym-node/src/node/http/router/api/v1/node/description.rs +++ b/nym-node/src/node/http/router/api/v1/node/description.rs @@ -2,7 +2,8 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::node::http::router::types::RequestError; -use axum::extract::Query; +use crate::node::http::state::AppState; +use axum::extract::{Query, State}; use nym_http_api_common::{FormattedResponse, OutputParams}; use nym_node_requests::api::v1::node::models::NodeDescription; @@ -11,7 +12,7 @@ use nym_node_requests::api::v1::node::models::NodeDescription; get, path = "/description", context_path = "/api/v1", - tag = "Node", + tag = "v1 / Node", responses( (status = 200, content( (NodeDescription = "application/json"), @@ -21,11 +22,11 @@ use nym_node_requests::api::v1::node::models::NodeDescription; params(OutputParams) )] pub(crate) async fn description( - description: NodeDescription, Query(output): Query, + State(state): State, ) -> Result { let output = output.output.unwrap_or_default(); - Ok(output.to_response(description)) + Ok(output.to_response(state.static_information.description.clone())) } pub type NodeDescriptionResponse = FormattedResponse; diff --git a/nym-node/src/node/http/router/api/v1/node/hardware.rs b/nym-node/src/node/http/router/api/v1/node/hardware.rs index b0f5e124d3..56dd91c41b 100644 --- a/nym-node/src/node/http/router/api/v1/node/hardware.rs +++ b/nym-node/src/node/http/router/api/v1/node/hardware.rs @@ -2,7 +2,8 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::node::http::router::types::RequestError; -use axum::extract::Query; +use crate::node::http::state::AppState; +use axum::extract::{Query, State}; use axum::http::StatusCode; use nym_http_api_common::{FormattedResponse, OutputParams}; use nym_node_requests::api::v1::node::models::HostSystem; @@ -12,7 +13,7 @@ use nym_node_requests::api::v1::node::models::HostSystem; get, path = "/system-info", context_path = "/api/v1", - tag = "Node", + tag = "v1 / Node", responses( (status = 200, content( (HostSystem = "application/json"), @@ -23,12 +24,12 @@ use nym_node_requests::api::v1::node::models::HostSystem; params(OutputParams) )] pub(crate) async fn host_system( - system_info: Option, Query(output): Query, + State(state): State, ) -> Result { let output = output.output.unwrap_or_default(); - let Some(system_info) = system_info else { + let Some(system_info) = state.static_information.system_info.clone() else { return Err(RequestError::new( "this nym-node does not wish to expose the system information", StatusCode::FORBIDDEN, diff --git a/nym-node/src/node/http/router/api/v1/node/host_information.rs b/nym-node/src/node/http/router/api/v1/node/host_information.rs index 8e828f8590..c1add6c39e 100644 --- a/nym-node/src/node/http/router/api/v1/node/host_information.rs +++ b/nym-node/src/node/http/router/api/v1/node/host_information.rs @@ -12,7 +12,7 @@ use nym_node_requests::api::{SignedDataHostInfo, v1::node::models::SignedHostInf get, path = "/host-information", context_path = "/api/v1", - tag = "Node", + tag = "v1 / Node", responses( (status = 200, content( (SignedDataHostInfo = "application/json"), diff --git a/nym-node/src/node/http/router/api/v1/node/mod.rs b/nym-node/src/node/http/router/api/v1/node/mod.rs index a877722d3a..b812d63a6d 100644 --- a/nym-node/src/node/http/router/api/v1/node/mod.rs +++ b/nym-node/src/node/http/router/api/v1/node/mod.rs @@ -10,7 +10,6 @@ use crate::node::http::api::v1::node::roles::roles; use crate::node::http::state::AppState; use axum::Router; use axum::routing::get; -use nym_node_requests::api::v1::node::models; use nym_node_requests::routes::api::v1; pub mod auxiliary; @@ -20,51 +19,15 @@ pub mod hardware; pub mod host_information; pub mod roles; -#[derive(Debug, Clone)] -pub struct Config { - pub build_information: models::BinaryBuildInformationOwned, - pub system_info: Option, - pub roles: models::NodeRoles, - pub description: models::NodeDescription, - pub auxiliary_details: models::AuxiliaryDetails, -} +#[derive(Debug, Clone, Copy)] +pub struct Config {} -pub(super) fn routes(config: Config) -> Router { +pub(super) fn routes(_config: Config) -> Router { Router::new() - .route( - v1::BUILD_INFO, - get({ - let build_info = config.build_information; - move |query| build_information(build_info, query) - }), - ) - .route( - v1::ROLES, - get({ - let node_roles = config.roles; - move |query| roles(node_roles, query) - }), - ) + .route(v1::BUILD_INFO, get(build_information)) + .route(v1::ROLES, get(roles)) .route(v1::HOST_INFO, get(host_information)) - .route( - v1::SYSTEM_INFO, - get({ - let system_info = config.system_info; - move |query| host_system(system_info, query) - }), - ) - .route( - v1::NODE_DESCRIPTION, - get({ - let node_description = config.description; - move |query| description(node_description, query) - }), - ) - .route( - v1::AUXILIARY, - get({ - let auxiliary_details = config.auxiliary_details; - move |query| auxiliary(auxiliary_details, query) - }), - ) + .route(v1::SYSTEM_INFO, get(host_system)) + .route(v1::NODE_DESCRIPTION, get(description)) + .route(v1::AUXILIARY, get(auxiliary)) } diff --git a/nym-node/src/node/http/router/api/v1/node/roles.rs b/nym-node/src/node/http/router/api/v1/node/roles.rs index c1228a5188..2a51410323 100644 --- a/nym-node/src/node/http/router/api/v1/node/roles.rs +++ b/nym-node/src/node/http/router/api/v1/node/roles.rs @@ -1,7 +1,8 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use axum::extract::Query; +use crate::node::http::state::AppState; +use axum::extract::{Query, State}; use nym_http_api_common::{FormattedResponse, OutputParams}; use nym_node_requests::api::v1::node::models::NodeRoles; @@ -10,7 +11,7 @@ use nym_node_requests::api::v1::node::models::NodeRoles; get, path = "/roles", context_path = "/api/v1", - tag = "Node", + tag = "v1 / Node", responses( (status = 200, content( (NodeRoles = "application/json"), @@ -20,11 +21,11 @@ use nym_node_requests::api::v1::node::models::NodeRoles; params(OutputParams) )] pub(crate) async fn roles( - node_roles: NodeRoles, Query(output): Query, + State(state): State, ) -> RolesResponse { let output = output.output.unwrap_or_default(); - output.to_response(node_roles) + output.to_response(state.static_information.roles) } pub type RolesResponse = FormattedResponse; diff --git a/nym-node/src/node/http/router/api/v2/mod.rs b/nym-node/src/node/http/router/api/v2/mod.rs new file mode 100644 index 0000000000..76cc068a60 --- /dev/null +++ b/nym-node/src/node/http/router/api/v2/mod.rs @@ -0,0 +1,16 @@ +// Copyright 2026 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::node::http::state::AppState; +use axum::Router; + +pub mod node; + +#[derive(Debug, Clone)] +pub struct Config { + pub node: node::Config, +} + +pub(super) fn routes(config: Config) -> Router { + Router::new().merge(node::routes(config.node)) +} diff --git a/nym-node/src/node/http/router/api/v2/node/auxiliary.rs b/nym-node/src/node/http/router/api/v2/node/auxiliary.rs new file mode 100644 index 0000000000..306d7afefa --- /dev/null +++ b/nym-node/src/node/http/router/api/v2/node/auxiliary.rs @@ -0,0 +1,35 @@ +// Copyright 2026 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::node::http::router::types::RequestError; +use crate::node::http::state::AppState; +use axum::extract::{Query, State}; +use nym_http_api_common::{FormattedResponse, OutputParamsV2}; +use nym_node_requests::api::v2::node::models::AuxiliaryDetailsV2; + +/// Returns auxiliary details of this node. +#[utoipa::path( + get, + path = "/auxiliary-details", + context_path = "/api/v2", + tag = "v2 / Node", + // distinct from v1's `auxiliary`: OpenAPI requires operationId to be unique + // across the whole document, and Swagger UI routes "Try it out" by operationId + operation_id = "v2_auxiliary", + responses( + (status = 200, content( + (AuxiliaryDetailsV2 = "application/json"), + (AuxiliaryDetailsV2 = "application/yaml") + )), + ), + params(OutputParamsV2) +)] +pub(crate) async fn auxiliary( + Query(output): Query, + State(state): State, +) -> Result { + let output = output.output.unwrap_or_default(); + Ok(output.to_response(state.static_information.auxiliary_data.clone())) +} + +pub type AuxiliaryDetailsResponse = FormattedResponse; diff --git a/nym-node/src/node/http/router/api/v2/node/mod.rs b/nym-node/src/node/http/router/api/v2/node/mod.rs new file mode 100644 index 0000000000..5152e49cb8 --- /dev/null +++ b/nym-node/src/node/http/router/api/v2/node/mod.rs @@ -0,0 +1,17 @@ +// Copyright 2026 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::node::http::api::v2::node::auxiliary::auxiliary; +use crate::node::http::state::AppState; +use axum::Router; +use axum::routing::get; +use nym_node_requests::routes::api::v2; + +pub mod auxiliary; + +#[derive(Debug, Clone, Copy)] +pub struct Config {} + +pub(super) fn routes(_config: Config) -> Router { + Router::new().route(v2::AUXILIARY, get(auxiliary)) +} diff --git a/nym-node/src/node/http/router/landing_page.rs b/nym-node/src/node/http/router/landing_page.rs index 65d3427dd0..490fc5261d 100644 --- a/nym-node/src/node/http/router/landing_page.rs +++ b/nym-node/src/node/http/router/landing_page.rs @@ -27,7 +27,7 @@ pub(super) async fn default() -> Html<&'static str> {

default page of the nym node - you can customize it by setting the 'assets' path under '[http]' section of your config.

- You can explore the REST API at /api/v1/swagger/ + You can explore the REST API at /api/swagger/
"#, ) diff --git a/nym-node/src/node/http/router/mod.rs b/nym-node/src/node/http/router/mod.rs index dfb0c84b08..57ad119b47 100644 --- a/nym-node/src/node/http/router/mod.rs +++ b/nym-node/src/node/http/router/mod.rs @@ -2,22 +2,18 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::node::http::NymNodeHttpServer; -use crate::node::http::api::v1::lewes_protocol; use crate::node::http::error::NymNodeHttpError; use crate::node::http::state::AppState; use axum::Router; use axum::response::Redirect; use axum::routing::get; -use nym_bin_common::bin_info_owned; use nym_http_api_common::middleware::logging; -use nym_node_requests::api::SignedLewesProtocol; use nym_node_requests::api::v1::authenticator::models::Authenticator; use nym_node_requests::api::v1::gateway::models::{Bridges, Gateway}; use nym_node_requests::api::v1::ip_packet_router::models::IpPacketRouter; use nym_node_requests::api::v1::mixnode::models::Mixnode; use nym_node_requests::api::v1::network_requester::exit_policy::models::UsedExitPolicy; use nym_node_requests::api::v1::network_requester::models::NetworkRequester; -use nym_node_requests::api::v1::node::models::{AuxiliaryDetails, HostSystem, NodeDescription}; use nym_node_requests::routes; use std::net::SocketAddr; use std::path::Path; @@ -36,18 +32,12 @@ pub struct HttpServerConfig { } impl HttpServerConfig { - pub fn new(signed_lewes_protocol: SignedLewesProtocol) -> Self { + pub fn new() -> Self { HttpServerConfig { landing: Default::default(), api: api::Config { v1_config: api::v1::Config { - node: api::v1::node::Config { - build_information: bin_info_owned!(), - system_info: None, - roles: Default::default(), - description: Default::default(), - auxiliary_details: Default::default(), - }, + node: api::v1::node::Config {}, metrics: Default::default(), gateway: Default::default(), mixnode: Default::default(), @@ -55,9 +45,10 @@ impl HttpServerConfig { network_requester: Default::default(), ip_packet_router: Default::default(), authenticator: Default::default(), - lewes_protocol: lewes_protocol::Config { - details: signed_lewes_protocol, - }, + lewes_protocol: Default::default(), + }, + v2_config: api::v2::Config { + node: api::v2::node::Config {}, }, }, } @@ -69,24 +60,6 @@ impl HttpServerConfig { self } - #[must_use] - pub fn with_system_info(mut self, info: HostSystem) -> Self { - self.api.v1_config.node.system_info = Some(info); - self - } - - #[must_use] - pub fn with_description(mut self, description: NodeDescription) -> Self { - self.api.v1_config.node.description = description; - self - } - - #[must_use] - pub fn with_auxiliary_details(mut self, auxiliary_details: AuxiliaryDetails) -> Self { - self.api.v1_config.node.auxiliary_details = auxiliary_details; - self - } - #[must_use] pub fn with_gateway_details(mut self, gateway: Gateway) -> Self { self.api.v1_config.gateway.details = Some(gateway); @@ -179,6 +152,10 @@ impl NymNodeRouter { ) .merge(landing_page::routes(config.landing)) .nest(routes::API, api::routes(config.api)) + // openapi must be merged at the outer router level (not nested) — + // SwaggerUi emits internal redirects that use absolute paths + // unaware of any `.nest()` prefix + .merge(api::openapi::route()) .layer(axum::middleware::from_fn(logging::log_request_info)) .with_state(state), } @@ -208,20 +185,10 @@ impl NymNodeRouter { #[cfg(test)] mod tests { use super::*; - use nym_crypto::asymmetric::{ed25519, x25519}; - use nym_node_requests::api::SignedData; - use nym_node_requests::api::v1::lewes_protocol::models::LewesProtocol; - use nym_test_utils::helpers::deterministic_rng; - use std::collections::BTreeMap; #[test] fn router_constructs_without_panic() { - let mut rng = deterministic_rng(); - let signing = ed25519::KeyPair::new(&mut rng); - let x25519_pub: x25519::DHPublicKey = x25519::PrivateKey::new(&mut rng).public_key().into(); - let lp = LewesProtocol::new(false, 0, 0, x25519_pub, BTreeMap::new()); - let signed = SignedData::new(lp, signing.private_key()).unwrap(); - let config = HttpServerConfig::new(signed); + let config = HttpServerConfig::new(); let _ = NymNodeRouter::new(config, AppState::dummy()); } } diff --git a/nym-node/src/node/http/state/mod.rs b/nym-node/src/node/http/state/mod.rs index cc72217439..6157e7e759 100644 --- a/nym-node/src/node/http/state/mod.rs +++ b/nym-node/src/node/http/state/mod.rs @@ -4,9 +4,13 @@ use crate::node::http::state::load::CachedNodeLoad; use crate::node::http::state::metrics::MetricsAppState; use crate::node::key_rotation::active_keys::ActiveSphinxKeys; +use nym_bin_common::build_information::BinaryBuildInformationOwned; use nym_credential_verification::UpgradeModeState; use nym_crypto::asymmetric::ed25519; use nym_node_metrics::NymNodeMetrics; +use nym_node_requests::api::SignedLewesProtocol; +use nym_node_requests::api::v1::node::models::{HostSystem, NodeDescription, NodeRoles}; +use nym_node_requests::api::v2::node::models::AuxiliaryDetailsV2; use nym_noise_keys::VersionedNoiseKeyV1; use nym_verloc::measurements::SharedVerlocStats; use std::net::IpAddr; @@ -23,6 +27,14 @@ pub(crate) struct StaticNodeInformation { pub(crate) x25519_versioned_noise_key: Option, pub(crate) ip_addresses: Vec, pub(crate) hostname: Option, + + // TODO: move other fields here too + pub(crate) build_information: BinaryBuildInformationOwned, + pub(crate) system_info: Option, + pub(crate) roles: NodeRoles, + pub(crate) description: NodeDescription, + pub(crate) auxiliary_data: AuxiliaryDetailsV2, + pub(crate) lewes_protocol: SignedLewesProtocol, } #[derive(Clone)] @@ -77,15 +89,34 @@ impl AppState { #[cfg(test)] pub(crate) fn dummy() -> Self { use crate::node::key_rotation::key::SphinxPrivateKey; + use nym_crypto::asymmetric::x25519; use rand::rngs::OsRng; - let ed25519_keys = ed25519::KeyPair::new(&mut OsRng); + let mut rng = nym_test_utils::helpers::deterministic_rng(); + let ed25519_keys = ed25519::KeyPair::new(&mut rng); + let x25519_pub: x25519::DHPublicKey = x25519::PrivateKey::new(&mut rng).public_key().into(); + let lp = nym_node_requests::api::v1::lewes_protocol::models::LewesProtocol::new( + false, + 0, + 0, + x25519_pub, + std::collections::BTreeMap::new(), + ); + let signed = + nym_node_requests::api::SignedData::new(lp, ed25519_keys.private_key()).unwrap(); + let attester_pk = *ed25519_keys.public_key(); let static_information = StaticNodeInformation { ed25519_identity_keys: Arc::new(ed25519_keys), x25519_versioned_noise_key: None, ip_addresses: vec![], hostname: None, + build_information: nym_bin_common::bin_info_owned!(), + system_info: None, + roles: Default::default(), + description: Default::default(), + auxiliary_data: Default::default(), + lewes_protocol: signed, }; let active_sphinx = ActiveSphinxKeys::new_fresh(SphinxPrivateKey::new(&mut OsRng, 0)); diff --git a/nym-node/src/node/mod.rs b/nym-node/src/node/mod.rs index 77d319a5f9..67786a1dc4 100644 --- a/nym-node/src/node/mod.rs +++ b/nym-node/src/node/mod.rs @@ -45,12 +45,10 @@ use crate::node::routing_filter::{OpenFilter, RoutingFilter}; use crate::node::shared_network::CachedNetwork; use crate::node::shared_network::refresher::{NetworkRefresher, NetworkRefresherConfig}; use crate::node::shared_network::topology_provider::{CachedTopologyProvider, LocalGatewayNode}; -use nym_bin_common::bin_info; +use nym_bin_common::{bin_info, bin_info_owned}; use nym_config::defaults::NymNetworkDetails; use nym_credential_verification::UpgradeModeState; use nym_crypto::asymmetric::{ed25519, x25519}; -pub use nym_gateway::node::ActiveClientsStore; -pub use nym_gateway::node::GatewayStorage; use nym_gateway::node::wireguard::PeerRegistrator; use nym_gateway::node::{GatewayTasksBuilder, UpgradeModeCheckRequestSender}; use nym_kkt::key_utils::{ @@ -69,15 +67,18 @@ use nym_node_metrics::NymNodeMetrics; use nym_node_metrics::events::MetricEventsSender; use nym_node_requests::api::SignedData; use nym_node_requests::api::v1::lewes_protocol::models::{LPHashFunction, LPKEM, LewesProtocol}; -use nym_node_requests::api::v1::node::models::{AnnouncePorts, NodeDescription}; +use nym_node_requests::api::v1::node::models::{AnnouncePorts, NodeDescription, NodeRoles}; use nym_noise::config::{NetworkMonitorAgentNode, NoiseConfig, NoiseNetworkView}; use nym_noise_keys::VersionedNoiseKeyV1; use nym_sphinx_acknowledgements::AckKey; use nym_sphinx_addressing::Recipient; use nym_task::{ShutdownManager, ShutdownToken, ShutdownTracker}; +use nym_validator_client::nyxd::AccountId; use nym_validator_client::nyxd::contract_traits::PagedNetworkMonitorsQueryClient; +use nym_validator_client::nyxd::error::NyxdError; use nym_validator_client::nyxd::nym_network_monitors_contract_common::AuthorisedNetworkMonitor; -use nym_validator_client::{QueryHttpRpcNyxdClient, UserAgent}; +use nym_validator_client::signing::signer::OfflineSigner; +use nym_validator_client::{DirectSecp256k1HdWallet, QueryHttpRpcNyxdClient, UserAgent}; use nym_verloc::measurements::SharedVerlocStats; use nym_verloc::{self, measurements::VerlocMeasurer}; use nym_wireguard::{WireguardGatewayData, peer_controller::PeerControlRequest}; @@ -95,6 +96,9 @@ use tokio_util::sync::WaitForCancellationFutureOwned; use tracing::{debug, error, info, trace}; use zeroize::Zeroizing; +pub use nym_gateway::node::ActiveClientsStore; +pub use nym_gateway::node::GatewayStorage; + pub mod bonding_information; pub mod description; pub mod helpers; @@ -892,12 +896,27 @@ impl NymNode { .collect() } + fn node_chain_address(&self) -> Result { + let network_details = NymNetworkDetails::new_from_env(); + + // derive the address (annoyingly, this will derive our private keys that we will rederive + // when starting the gateway, but changing this behaviour requires too much refactoring) + let wallet = DirectSecp256k1HdWallet::checked_from_mnemonic( + &network_details.chain_details.bech32_account_prefix, + (**self.entry_gateway.mnemonic).clone(), + ) + .map_err(NyxdError::from)?; + + Ok(wallet.get_accounts()[0].address.clone()) + } + pub(crate) async fn build_http_server( &self, shutdown: WaitForCancellationFutureOwned, ) -> Result { - let auxiliary_details = api_requests::v1::node::models::AuxiliaryDetails { + let auxiliary_data = api_requests::v2::node::models::AuxiliaryDetailsV2 { location: self.config.host.location, + address: self.node_chain_address()?.to_string(), announce_ports: AnnouncePorts { verloc_port: self.config.verloc.announce_port, mix_port: self.config.mixnet.announce_port, @@ -982,7 +1001,7 @@ impl NymNode { let signed_lewes_protocol = SignedData::new(lewes_protocol, self.ed25519_identity_keys.private_key()).unwrap(); - let mut config = HttpServerConfig::new(signed_lewes_protocol) + let mut config = HttpServerConfig::new() .with_landing_page_assets(self.config.http.landing_page_assets_path.as_ref()) .with_mixnode_details(mixnode_details) .with_gateway_details(gateway_details) @@ -990,28 +1009,16 @@ impl NymNode { .with_ip_packet_router_details(ipr_details) .with_authenticator_details(auth_details) .with_used_exit_policy(exit_policy_details) - .with_description(self.description.clone()) - .with_auxiliary_details(auxiliary_details) .with_prometheus_bearer_token(self.config.http.access_token.clone()); - if self.config.http.expose_system_info { - config = config.with_system_info(get_system_info( + let system_info = if self.config.http.expose_system_info { + Some(get_system_info( self.config.http.expose_system_hardware, self.config.http.expose_crypto_hardware, )) - } - if self.config.modes.mixnode { - config.api.v1_config.node.roles.mixnode_enabled = true; - } - - if self.config.modes.entry { - config.api.v1_config.node.roles.gateway_enabled = true - } - - if self.config.modes.exit { - config.api.v1_config.node.roles.network_requester_enabled = true; - config.api.v1_config.node.roles.ip_packet_router_enabled = true; - } + } else { + None + }; if let Some(path) = &self.config.gateway_tasks.storage_paths.bridge_client_params { config = config.with_bridge_client_params_file(path); @@ -1032,6 +1039,17 @@ impl NymNode { x25519_versioned_noise_key, ip_addresses: self.config.host.public_ips.clone(), hostname: self.config.host.hostname.clone(), + build_information: bin_info_owned!(), + system_info, + roles: NodeRoles { + mixnode_enabled: self.config.modes.mixnode, + gateway_enabled: self.config.modes.entry, + network_requester_enabled: self.config.modes.exit, + ip_packet_router_enabled: self.config.modes.exit, + }, + description: self.description.clone(), + auxiliary_data, + lewes_protocol: signed_lewes_protocol, }, self.active_sphinx_keys()?.clone(), self.metrics.clone(),