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<DirectSecp256k1HdWallet>
This commit is contained in:
Jędrzej Stuczyński
2026-06-12 10:36:27 +01:00
committed by GitHub
parent 8a93bce32f
commit 931ec03b28
46 changed files with 320 additions and 193 deletions
@@ -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};
@@ -395,8 +395,8 @@ impl From<nym_node_requests::api::v1::node::models::AnnouncePorts> for AnnounceP
}
}
impl From<nym_node_requests::api::v1::node::models::AuxiliaryDetails> for AuxiliaryDetailsV1 {
fn from(value: nym_node_requests::api::v1::node::models::AuxiliaryDetails) -> Self {
impl From<nym_node_requests::api::v1::node::models::AuxiliaryDetailsV1> for AuxiliaryDetailsV1 {
fn from(value: nym_node_requests::api::v1::node::models::AuxiliaryDetailsV1) -> Self {
AuxiliaryDetailsV1 {
location: value.location,
announce_ports: value.announce_ports.into(),
+1 -1
View File
@@ -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
))
)]
+1 -1
View File
@@ -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;
+2 -2
View File
@@ -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<AuxiliaryDetails, NymNodeApiClientError> {
async fn get_auxiliary_details(&self) -> Result<AuxiliaryDetailsV1, NymNodeApiClientError> {
self.get_json_from(routes::api::v1::auxiliary_absolute())
.await
}
@@ -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;
@@ -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<String>))]
#[schemars(with = "Option<String>")]
@@ -383,6 +384,16 @@ pub struct AuxiliaryDetails {
pub accepted_operator_terms_and_conditions: bool,
}
impl From<AuxiliaryDetailsV2> 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::*;
@@ -0,0 +1,4 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
pub mod node;
@@ -0,0 +1,4 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
pub mod models;
@@ -0,0 +1,30 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// 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<String>))]
#[schemars(with = "Option<String>")]
#[schemars(length(equal = 2))]
pub location: Option<Country>,
/// 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 <https://nymtech.net/terms-and-conditions/operators/v1.0.0>
// 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,
}
+14
View File
@@ -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);
}
}
}
+6 -1
View File
@@ -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<AppState> {
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))
}
@@ -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<S: Send + Sync + 'static + Clone>() -> Router<S> {
// 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()
}
@@ -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(
@@ -41,7 +41,7 @@ pub(crate) fn routes<S: Send + Sync + 'static + Clone>(
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<ClientInterfaces>;
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<WebSockets>;
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(
@@ -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(
@@ -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"),
@@ -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(
@@ -1,23 +1,15 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// 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<S: Send + Sync + 'static + Clone>(config: Config) -> Router<S> {
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<AppState> {
Router::new().route("/", get(root::root_lewes_protocol))
}
@@ -1,7 +1,8 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// 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<OutputParams>,
State(state): State<AppState>,
) -> Result<LewesProtocolResponse, StatusCode> {
Ok(output.to_response(config))
Ok(output.to_response(state.static_information.lewes_protocol.clone()))
}
pub type LewesProtocolResponse = FormattedResponse<SignedLewesProtocol>;
+1 -1
View File
@@ -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"),
@@ -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"),
@@ -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"),
@@ -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"),
@@ -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"),
@@ -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"),
@@ -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"),
@@ -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(
+7 -2
View File
@@ -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<AppState> {
// 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<AppState> {
lewes_protocol::routes(config.lewes_protocol),
)
.merge(node::routes(config.node))
.merge(openapi::route())
}
@@ -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"),
@@ -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"),
@@ -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(
@@ -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<OutputParams>,
State(state): State<AppState>,
) -> Result<AuxiliaryDetailsResponse, RequestError> {
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<AuxiliaryDetails>;
pub type AuxiliaryDetailsResponse = FormattedResponse<AuxiliaryDetailsV1>;
@@ -1,7 +1,8 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// 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<OutputParams>,
State(state): State<AppState>,
) -> 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<BinaryBuildInformationOwned>;
@@ -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<OutputParams>,
State(state): State<AppState>,
) -> Result<NodeDescriptionResponse, RequestError> {
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<NodeDescription>;
@@ -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<HostSystem>,
Query(output): Query<OutputParams>,
State(state): State<AppState>,
) -> Result<HostSystemResponse, RequestError> {
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,
@@ -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"),
@@ -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<models::HostSystem>,
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<AppState> {
pub(super) fn routes(_config: Config) -> Router<AppState> {
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))
}
@@ -1,7 +1,8 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// 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<OutputParams>,
State(state): State<AppState>,
) -> RolesResponse {
let output = output.output.unwrap_or_default();
output.to_response(node_roles)
output.to_response(state.static_information.roles)
}
pub type RolesResponse = FormattedResponse<NodeRoles>;
@@ -0,0 +1,16 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// 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<AppState> {
Router::new().merge(node::routes(config.node))
}
@@ -0,0 +1,35 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// 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<OutputParamsV2>,
State(state): State<AppState>,
) -> Result<AuxiliaryDetailsResponse, RequestError> {
let output = output.output.unwrap_or_default();
Ok(output.to_response(state.static_information.auxiliary_data.clone()))
}
pub type AuxiliaryDetailsResponse = FormattedResponse<AuxiliaryDetailsV2>;
@@ -0,0 +1,17 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// 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<AppState> {
Router::new().route(v2::AUXILIARY, get(auxiliary))
}
@@ -27,7 +27,7 @@ pub(super) async fn default() -> Html<&'static str> {
<div>
<p> default page of the nym node - you can customize it by setting the 'assets' path under '[http]' section of your config. </p>
You can explore the REST API at <a href = "/api/v1/swagger/">/api/v1/swagger/</a>
You can explore the REST API at <a href = "/api/swagger/">/api/swagger/</a>
</div>
"#,
)
+11 -44
View File
@@ -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());
}
}
+32 -1
View File
@@ -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<VersionedNoiseKeyV1>,
pub(crate) ip_addresses: Vec<IpAddr>,
pub(crate) hostname: Option<String>,
// TODO: move other fields here too
pub(crate) build_information: BinaryBuildInformationOwned,
pub(crate) system_info: Option<HostSystem>,
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));
+42 -24
View File
@@ -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<AccountId, NymNodeError> {
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<NymNodeHttpServer, NymNodeError> {
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(),