diff --git a/Cargo.lock b/Cargo.lock index ad64f4272e0..4cfa46fddb6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5718,7 +5718,7 @@ dependencies = [ "nym-types", "nym-validator-client", "nym-wireguard", - "nym-wireguard-private-metadata", + "nym-wireguard-private-metadata-server", "nym-wireguard-types", "rand 0.8.5", "serde", @@ -7312,27 +7312,65 @@ dependencies = [ ] [[package]] -name = "nym-wireguard-private-metadata" -version = "0.1.0" +name = "nym-wireguard-private-metadata-client" +version = "1.0.0" +dependencies = [ + "async-trait", + "nym-http-api-client", + "nym-wireguard-private-metadata-shared", + "tracing", +] + +[[package]] +name = "nym-wireguard-private-metadata-server" +version = "1.0.0" dependencies = [ "anyhow", "async-trait", "axum 0.7.9", - "bincode", "futures", "nym-credential-verification", "nym-credentials-interface", "nym-http-api-common", "nym-wireguard", + "nym-wireguard-private-metadata-shared", + "tokio", + "tokio-util", + "tower-http 0.5.2", + "utoipa", + "utoipa-swagger-ui", +] + +[[package]] +name = "nym-wireguard-private-metadata-shared" +version = "1.0.0" +dependencies = [ + "axum 0.7.9", + "bincode", + "nym-credentials-interface", "schemars 0.8.22", "serde", "thiserror 2.0.12", + "utoipa", +] + +[[package]] +name = "nym-wireguard-private-metadata-tests" +version = "1.0.0" +dependencies = [ + "async-trait", + "axum 0.7.9", + "nym-credential-verification", + "nym-credentials-interface", + "nym-http-api-client", + "nym-http-api-common", + "nym-wireguard", + "nym-wireguard-private-metadata-client", + "nym-wireguard-private-metadata-server", + "nym-wireguard-private-metadata-shared", "tokio", - "tokio-util", "tower-http 0.5.2", "utoipa", - "utoipa-swagger-ui", - "utoipauto", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 1889936cfe0..95b45ef28b6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -102,7 +102,10 @@ members = [ "common/wasm/storage", "common/wasm/utils", "common/wireguard", - "common/wireguard-private-metadata", + "common/wireguard-private-metadata/client", + "common/wireguard-private-metadata/server", + "common/wireguard-private-metadata/shared", + "common/wireguard-private-metadata/tests", "common/wireguard-types", "common/zulip-client", "documentation/autodoc", diff --git a/common/http-api-client/src/lib.rs b/common/http-api-client/src/lib.rs index 64359c96e79..60122efaf12 100644 --- a/common/http-api-client/src/lib.rs +++ b/common/http-api-client/src/lib.rs @@ -136,6 +136,7 @@ //! ``` #![warn(missing_docs)] +pub use reqwest::ClientBuilder as ReqwestClientBuilder; pub use reqwest::StatusCode; use crate::path::RequestPath; diff --git a/common/wireguard-private-metadata/client/Cargo.toml b/common/wireguard-private-metadata/client/Cargo.toml new file mode 100644 index 00000000000..d7d23ce4450 --- /dev/null +++ b/common/wireguard-private-metadata/client/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "nym-wireguard-private-metadata-client" +version = "1.0.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +async-trait = { workspace = true } +tracing = { workspace = true } +nym-http-api-client = { path = "../../http-api-client" } +nym-wireguard-private-metadata-shared = { path = "../shared" } + +[lints] +workspace = true diff --git a/common/wireguard-private-metadata/client/src/lib.rs b/common/wireguard-private-metadata/client/src/lib.rs new file mode 100644 index 00000000000..3f683efe5e7 --- /dev/null +++ b/common/wireguard-private-metadata/client/src/lib.rs @@ -0,0 +1,58 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use async_trait::async_trait; +use tracing::instrument; + +use nym_http_api_client::{ApiClient, Client, HttpClientError, NO_PARAMS}; + +use nym_wireguard_private_metadata_shared::{ + routes, Version, {ErrorResponse, Request, Response}, +}; + +pub type WireguardMetadataApiClientError = HttpClientError; + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +pub trait WireguardMetadataApiClient: ApiClient { + #[instrument(level = "debug", skip(self))] + async fn version(&self) -> Result { + let version: u64 = self + .get_json( + &[routes::V1_API_VERSION, routes::BANDWIDTH, routes::VERSION], + NO_PARAMS, + ) + .await?; + Ok(version.into()) + } + + #[instrument(level = "debug", skip(self))] + async fn available_bandwidth( + &self, + request_body: &Request, + ) -> Result { + self.post_json( + &[routes::V1_API_VERSION, routes::BANDWIDTH, routes::AVAILABLE], + NO_PARAMS, + request_body, + ) + .await + } + + #[instrument(level = "debug", skip(self, request_body))] + async fn topup_bandwidth( + &self, + request_body: &Request, + ) -> Result { + self.post_json( + &[routes::V1_API_VERSION, routes::BANDWIDTH, routes::TOPUP], + NO_PARAMS, + request_body, + ) + .await + } +} + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl WireguardMetadataApiClient for Client {} diff --git a/common/wireguard-private-metadata/Cargo.toml b/common/wireguard-private-metadata/server/Cargo.toml similarity index 62% rename from common/wireguard-private-metadata/Cargo.toml rename to common/wireguard-private-metadata/server/Cargo.toml index 8c636f69923..fa850181f13 100644 --- a/common/wireguard-private-metadata/Cargo.toml +++ b/common/wireguard-private-metadata/server/Cargo.toml @@ -1,6 +1,6 @@ [package] -name = "nym-wireguard-private-metadata" -version = "0.1.0" +name = "nym-wireguard-private-metadata-server" +version = "1.0.0" authors.workspace = true repository.workspace = true homepage.workspace = true @@ -11,11 +11,7 @@ license.workspace = true [dependencies] anyhow = { workspace = true } axum = { workspace = true, features = ["tokio", "macros"] } -bincode = { workspace = true } futures = { workspace = true } -schemars = { workspace = true, features = ["preserve_order"] } -serde = { workspace = true } -thiserror = { workspace = true } tokio = { workspace = true, features = ["rt-multi-thread", "net", "io-util"] } tokio-util = { workspace = true } tower-http = { workspace = true, features = [ @@ -27,17 +23,21 @@ tower-http = { workspace = true, features = [ "compression-zstd", ] } utoipa = { workspace = true, features = ["axum_extras", "time"] } -utoipauto = { workspace = true } utoipa-swagger-ui = { workspace = true, features = ["axum"] } -nym-credentials-interface = { path = "../credentials-interface" } -nym-credential-verification = { path = "../credential-verification" } -nym-http-api-common = { path = "../http-api-common", features = [ +nym-credentials-interface = { path = "../../credentials-interface" } +nym-credential-verification = { path = "../../credential-verification" } +nym-http-api-common = { path = "../../http-api-common", features = [ "middleware", "utoipa", "output", ] } -nym-wireguard = { path = "../wireguard" } +nym-wireguard = { path = "../../wireguard" } +nym-wireguard-private-metadata-shared = { path = "../shared" } [dev-dependencies] async-trait = { workspace = true } + + +[lints] +workspace = true diff --git a/common/wireguard-private-metadata/src/http/mod.rs b/common/wireguard-private-metadata/server/src/http/mod.rs similarity index 100% rename from common/wireguard-private-metadata/src/http/mod.rs rename to common/wireguard-private-metadata/server/src/http/mod.rs diff --git a/common/wireguard-private-metadata/src/http/openapi.rs b/common/wireguard-private-metadata/server/src/http/openapi.rs similarity index 65% rename from common/wireguard-private-metadata/src/http/openapi.rs rename to common/wireguard-private-metadata/server/src/http/openapi.rs index 3b6f228f925..5e912665820 100644 --- a/common/wireguard-private-metadata/src/http/openapi.rs +++ b/common/wireguard-private-metadata/server/src/http/openapi.rs @@ -3,12 +3,12 @@ use utoipa::OpenApi; -use crate::models::{AvailableBandwidthResponse, TopUpRequest}; +use nym_wireguard_private_metadata_shared::{Request, Response}; #[derive(OpenApi)] #[openapi( info(title = "Nym Wireguard Private Metadata"), tags(), - components(schemas(AvailableBandwidthResponse, TopUpRequest)) + components(schemas(Request, Response)) )] pub(crate) struct ApiDoc; diff --git a/common/wireguard-private-metadata/src/http/router.rs b/common/wireguard-private-metadata/server/src/http/router.rs similarity index 99% rename from common/wireguard-private-metadata/src/http/router.rs rename to common/wireguard-private-metadata/server/src/http/router.rs index 60e1bc0c97d..c5936f1f4cf 100644 --- a/common/wireguard-private-metadata/src/http/router.rs +++ b/common/wireguard-private-metadata/server/src/http/router.rs @@ -66,7 +66,7 @@ fn setup_cors() -> CorsLayer { } pub struct RouterWithState { - router: Router, + pub router: Router, } impl RouterWithState { diff --git a/common/wireguard-private-metadata/src/http/state.rs b/common/wireguard-private-metadata/server/src/http/state.rs similarity index 51% rename from common/wireguard-private-metadata/src/http/state.rs rename to common/wireguard-private-metadata/server/src/http/state.rs index 737605774bb..06916bb35fa 100644 --- a/common/wireguard-private-metadata/src/http/state.rs +++ b/common/wireguard-private-metadata/server/src/http/state.rs @@ -5,11 +5,8 @@ use std::net::IpAddr; use nym_credentials_interface::CredentialSpendingData; -use crate::{ - error::MetadataError, - models::{latest, AvailableBandwidthResponse}, - transceiver::PeerControllerTransceiver, -}; +use crate::transceiver::PeerControllerTransceiver; +use nym_wireguard_private_metadata_shared::error::MetadataError; #[derive(Clone, axum::extract::FromRef)] pub struct AppState { @@ -21,23 +18,18 @@ impl AppState { Self { transceiver } } - pub(crate) async fn available_bandwidth( - &self, - ip: IpAddr, - ) -> Result { - let value = self.transceiver.query_bandwidth(ip).await?; - let res = latest::InnerAvailableBandwidthResponse::new(value).try_into()?; - Ok(res) + pub async fn available_bandwidth(&self, ip: IpAddr) -> Result { + self.transceiver.query_bandwidth(ip).await } - pub(crate) async fn topup_bandwidth( + // Top up with a credential and return the afterwards available bandwidth + pub async fn topup_bandwidth( &self, ip: IpAddr, credential: CredentialSpendingData, - ) -> Result<(), MetadataError> { + ) -> Result { self.transceiver .topup_bandwidth(ip, Box::new(credential)) - .await?; - Ok(()) + .await } } diff --git a/common/wireguard-private-metadata/server/src/lib.rs b/common/wireguard-private-metadata/server/src/lib.rs new file mode 100644 index 00000000000..3d772ac1e05 --- /dev/null +++ b/common/wireguard-private-metadata/server/src/lib.rs @@ -0,0 +1,13 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +mod http; +mod network; +mod transceiver; + +pub use http::{ + router::{ApiHttpServer, RouterBuilder, RouterWithState}, + state::AppState, + ShutdownHandles, +}; +pub use transceiver::PeerControllerTransceiver; diff --git a/common/wireguard-private-metadata/server/src/network.rs b/common/wireguard-private-metadata/server/src/network.rs new file mode 100644 index 00000000000..36b22ba32b8 --- /dev/null +++ b/common/wireguard-private-metadata/server/src/network.rs @@ -0,0 +1,111 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use std::net::SocketAddr; + +use axum::{ + extract::{ConnectInfo, Query, State}, + Json, Router, +}; +use nym_http_api_common::{FormattedResponse, OutputParams}; +use nym_wireguard_private_metadata_shared::{ + interface::{RequestData, ResponseData}, + latest, AxumErrorResponse, AxumResult, Construct, Extract, Request, Response, +}; +use tower_http::compression::CompressionLayer; + +use crate::http::state::AppState; + +pub(crate) fn bandwidth_routes() -> Router { + Router::new() + .route("/version", axum::routing::get(version)) + .route("/available", axum::routing::post(available_bandwidth)) + .route("/topup", axum::routing::post(topup_bandwidth)) + .layer(CompressionLayer::new()) +} + +#[utoipa::path( + tag = "bandwidth", + get, + path = "/v1/bandwidth/version", + responses( + (status = 200, content( + (Response = "application/bincode") + )) + ), +)] +async fn version(Query(output): Query) -> AxumResult> { + let output = output.output.unwrap_or_default(); + Ok(output.to_response(latest::VERSION.into())) +} + +#[utoipa::path( + tag = "bandwidth", + post, + request_body = Request, + path = "/v1/bandwidth/available", + responses( + (status = 200, content( + (Response = "application/bincode") + )) + ), +)] +async fn available_bandwidth( + ConnectInfo(addr): ConnectInfo, + Query(output): Query, + State(state): State, + Json(request): Json, +) -> AxumResult> { + let output = output.output.unwrap_or_default(); + + let (RequestData::AvailableBandwidth(_), version) = + request.extract().map_err(AxumErrorResponse::bad_request)? + else { + return Err(AxumErrorResponse::bad_request("incorrect request type")); + }; + let available_bandwidth = state + .available_bandwidth(addr.ip()) + .await + .map_err(AxumErrorResponse::bad_request)?; + let response = Response::construct( + ResponseData::AvailableBandwidth(available_bandwidth), + version, + ) + .map_err(AxumErrorResponse::bad_request)?; + + Ok(output.to_response(response)) +} + +#[utoipa::path( + tag = "bandwidth", + post, + request_body = Request, + path = "/v1/bandwidth/topup", + responses( + (status = 200, content( + (Response = "application/bincode") + )) + ), +)] +async fn topup_bandwidth( + ConnectInfo(addr): ConnectInfo, + Query(output): Query, + State(state): State, + Json(request): Json, +) -> AxumResult> { + let output = output.output.unwrap_or_default(); + + let (RequestData::TopUpBandwidth(credential), version) = + request.extract().map_err(AxumErrorResponse::bad_request)? + else { + return Err(AxumErrorResponse::bad_request("incorrect request type")); + }; + let available_bandwidth = state + .topup_bandwidth(addr.ip(), *credential) + .await + .map_err(AxumErrorResponse::bad_request)?; + let response = Response::construct(ResponseData::TopUpBandwidth(available_bandwidth), version) + .map_err(AxumErrorResponse::bad_request)?; + + Ok(output.to_response(response)) +} diff --git a/common/wireguard-private-metadata/src/transceiver.rs b/common/wireguard-private-metadata/server/src/transceiver.rs similarity index 92% rename from common/wireguard-private-metadata/src/transceiver.rs rename to common/wireguard-private-metadata/server/src/transceiver.rs index 80145730ece..cbe77126cf7 100644 --- a/common/wireguard-private-metadata/src/transceiver.rs +++ b/common/wireguard-private-metadata/server/src/transceiver.rs @@ -1,13 +1,15 @@ -use futures::channel::oneshot; -use nym_credential_verification::ClientBandwidth; -use nym_credentials_interface::CredentialSpendingData; -use tokio::sync::mpsc; +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 use std::net::IpAddr; -use nym_wireguard::peer_controller::PeerControlRequest; +use futures::channel::oneshot; +use tokio::sync::mpsc; -use crate::error::MetadataError; +use nym_credential_verification::ClientBandwidth; +use nym_credentials_interface::CredentialSpendingData; +use nym_wireguard::peer_controller::PeerControlRequest; +use nym_wireguard_private_metadata_shared::error::MetadataError; #[derive(Clone)] pub struct PeerControllerTransceiver { @@ -39,6 +41,7 @@ impl PeerControllerTransceiver { Ok(self.get_client_bandwidth(ip).await?.available().await) } + // Top up with a credential and return the afterwards available bandwidth pub(crate) async fn topup_bandwidth( &self, ip: IpAddr, @@ -61,20 +64,26 @@ impl PeerControllerTransceiver { .map_err(|err| MetadataError::Unsuccessful { reason: err.to_string(), })?; - let available_bandwidth = verifier.verify().await?; + let available_bandwidth = + verifier + .verify() + .await + .map_err(|err| MetadataError::CredentialVerification { + message: err.to_string(), + })?; Ok(available_bandwidth) } } #[cfg(test)] -mod tests { +pub(crate) mod tests { use nym_credential_verification::TicketVerifier; use nym_wireguard::CONTROL_CHANNEL_SIZE; use super::*; - const CREDENTIAL_BYTES: [u8; 1245] = [ + pub(crate) const CREDENTIAL_BYTES: [u8; 1245] = [ 0, 0, 4, 133, 96, 179, 223, 185, 136, 23, 213, 166, 59, 203, 66, 69, 209, 181, 227, 254, 16, 102, 98, 237, 59, 119, 170, 111, 31, 194, 51, 59, 120, 17, 115, 229, 79, 91, 11, 139, 154, 2, 212, 23, 68, 70, 167, 3, 240, 54, 224, 171, 221, 1, 69, 48, 60, 118, 119, 249, 123, @@ -139,10 +148,16 @@ mod tests { 0, 0, 0, 0, 0, 1, ]; - struct MockVerifier { + pub(crate) struct MockVerifier { ret: i64, } + impl MockVerifier { + pub(crate) fn new(ret: i64) -> MockVerifier { + Self { ret } + } + } + #[async_trait::async_trait] impl TicketVerifier for MockVerifier { async fn verify(&mut self) -> nym_credential_verification::Result { @@ -162,7 +177,7 @@ mod tests { .send(Ok(ClientBandwidth::new(Default::default()))) .ok(); } - _ => unimplemented!(), + _ => panic!("Not expected"), } }); @@ -197,7 +212,7 @@ mod tests { ip: _, response_tx: _, } => {} - _ => unimplemented!(), + _ => panic!("Not expected"), } }); @@ -222,7 +237,7 @@ mod tests { ))) .ok(); } - _ => unimplemented!(), + _ => panic!("Not expected"), } }); @@ -247,10 +262,10 @@ mod tests { response_tx, } => { response_tx - .send(Ok(Box::new(MockVerifier { ret: verifier_bw }))) + .send(Ok(Box::new(MockVerifier::new(verifier_bw)))) .ok(); } - _ => unimplemented!(), + _ => panic!("Not expected"), } }); @@ -280,7 +295,7 @@ mod tests { ))) .ok(); } - _ => unimplemented!(), + _ => panic!("Not expected"), } }); diff --git a/common/wireguard-private-metadata/shared/Cargo.toml b/common/wireguard-private-metadata/shared/Cargo.toml new file mode 100644 index 00000000000..26b3cc6cd6d --- /dev/null +++ b/common/wireguard-private-metadata/shared/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "nym-wireguard-private-metadata-shared" +version = "1.0.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +axum = { workspace = true } +bincode = { workspace = true } +schemars = { workspace = true, features = ["preserve_order"] } +serde = { workspace = true } +thiserror = { workspace = true } +utoipa = { workspace = true } + +nym-credentials-interface = { path = "../../credentials-interface" } + +[features] +testing = [] + +[lints] +workspace = true diff --git a/common/wireguard-private-metadata/src/error.rs b/common/wireguard-private-metadata/shared/src/error.rs similarity index 77% rename from common/wireguard-private-metadata/src/error.rs rename to common/wireguard-private-metadata/shared/src/error.rs index 47d1faaa83a..3783462a4d7 100644 --- a/common/wireguard-private-metadata/src/error.rs +++ b/common/wireguard-private-metadata/shared/src/error.rs @@ -26,11 +26,3 @@ impl From for MetadataError { } } } - -impl From for MetadataError { - fn from(value: nym_credential_verification::Error) -> Self { - Self::CredentialVerification { - message: value.to_string(), - } - } -} diff --git a/common/wireguard-private-metadata/shared/src/lib.rs b/common/wireguard-private-metadata/shared/src/lib.rs new file mode 100644 index 00000000000..de791578746 --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/lib.rs @@ -0,0 +1,20 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod error; +mod models; +pub mod routes; + +#[cfg(feature = "testing")] +pub use models::v0; +pub use models::{ + error::Error as ModelError, interface, latest, v1, AxumErrorResponse, AxumResult, Construct, + ErrorResponse, Extract, Request, Response, Version, +}; + +fn make_bincode_serializer() -> impl bincode::Options { + use bincode::Options; + bincode::DefaultOptions::new() + .with_big_endian() + .with_varint_encoding() +} diff --git a/common/wireguard-private-metadata/shared/src/models/error.rs b/common/wireguard-private-metadata/shared/src/models/error.rs new file mode 100644 index 00000000000..45dc88617d4 --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/error.rs @@ -0,0 +1,30 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::models::Version; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error(transparent)] + Bincode(#[from] bincode::Error), + + #[error("trying to deserialize from version {source_version:?} into {target_version:?}")] + InvalidVersion { + source_version: Version, + target_version: Version, + }, + + #[error( + "trying to deserialize from query type {source_query_type} query type {target_query_type}" + )] + InvalidQueryType { + source_query_type: String, + target_query_type: String, + }, + + #[error("update not possible from {from:?} to {to:?}")] + UpdateNotPossible { from: Version, to: Version }, + + #[error("downgrade not possible from {from:?} to {to:?}")] + DowngradeNotPossible { from: Version, to: Version }, +} diff --git a/common/wireguard-private-metadata/shared/src/models/interface.rs b/common/wireguard-private-metadata/shared/src/models/interface.rs new file mode 100644 index 00000000000..3f4537f170f --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/interface.rs @@ -0,0 +1,145 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_credentials_interface::CredentialSpendingData; + +#[cfg(feature = "testing")] +use crate::models::v0; +use crate::models::{v1, Construct, Extract, Request, Response, Version}; + +pub enum RequestData { + AvailableBandwidth(()), + TopUpBandwidth(Box), +} + +impl From for RequestData { + fn from(value: super::latest::interface::RequestData) -> Self { + match value { + super::latest::interface::RequestData::AvailableBandwidth(inner) => { + Self::AvailableBandwidth(inner) + } + super::latest::interface::RequestData::TopUpBandwidth(credential_spending_data) => { + Self::TopUpBandwidth(credential_spending_data) + } + } + } +} + +impl From for super::latest::interface::RequestData { + fn from(value: RequestData) -> Self { + match value { + RequestData::AvailableBandwidth(inner) => Self::AvailableBandwidth(inner), + RequestData::TopUpBandwidth(credential_spending_data) => { + Self::TopUpBandwidth(credential_spending_data) + } + } + } +} + +impl From for ResponseData { + fn from(value: super::latest::interface::ResponseData) -> Self { + match value { + super::latest::interface::ResponseData::AvailableBandwidth(inner) => { + Self::AvailableBandwidth(inner) + } + super::latest::interface::ResponseData::TopUpBandwidth(credential_spending_data) => { + Self::TopUpBandwidth(credential_spending_data) + } + } + } +} + +impl From for super::latest::interface::ResponseData { + fn from(value: ResponseData) -> Self { + match value { + ResponseData::AvailableBandwidth(inner) => Self::AvailableBandwidth(inner), + ResponseData::TopUpBandwidth(credential_spending_data) => { + Self::TopUpBandwidth(credential_spending_data) + } + } + } +} + +impl Construct for Request { + fn construct(info: RequestData, version: Version) -> Result { + match version { + #[cfg(feature = "testing")] + Version::V0 => { + let translate_info = super::latest::interface::RequestData::from(info); + let downgrade_info = v0::interface::RequestData::try_from(translate_info)?; + let versioned_request = v0::VersionedRequest::construct(downgrade_info, version)?; + Ok(versioned_request.try_into()?) + } + Version::V1 => { + let versioned_request = v1::VersionedRequest::construct(info.into(), version)?; + Ok(versioned_request.try_into()?) + } + } + } +} + +impl Extract for Request { + fn extract(&self) -> Result<(RequestData, Version), crate::models::Error> { + match self.version { + #[cfg(feature = "testing")] + super::Version::V0 => { + let versioned_request = v0::VersionedRequest::try_from(self.clone())?; + let (request, version) = versioned_request.extract()?; + + let upgrade_request = super::latest::interface::RequestData::try_from(request)?; + + Ok((upgrade_request.into(), version)) + } + super::Version::V1 => { + let versioned_request = v1::VersionedRequest::try_from(self.clone())?; + let (extracted, version) = versioned_request.extract()?; + Ok((extracted.into(), version)) + } + } + } +} + +pub enum ResponseData { + AvailableBandwidth(i64), + TopUpBandwidth(i64), +} + +impl Construct for Response { + fn construct(info: ResponseData, version: Version) -> Result { + match version { + #[cfg(feature = "testing")] + super::Version::V0 => { + let translate_response = super::latest::interface::ResponseData::from(info); + let downgrade_response = v0::interface::ResponseData::try_from(translate_response)?; + let versioned_response = + v0::VersionedResponse::construct(downgrade_response, version)?; + Ok(versioned_response.try_into()?) + } + Version::V1 => { + let versioned_response = v1::VersionedResponse::construct(info.into(), version)?; + Ok(versioned_response.try_into()?) + } + } + } +} + +impl Extract for Response { + fn extract(&self) -> Result<(ResponseData, Version), super::error::Error> { + match self.version { + #[cfg(feature = "testing")] + super::Version::V0 => { + let versioned_response = v0::VersionedResponse::try_from(self.clone())?; + let (response, version) = versioned_response.extract()?; + + let upgrade_response = super::latest::interface::ResponseData::try_from(response)?; + + Ok((upgrade_response.into(), version)) + } + super::Version::V1 => { + let versioned_response = v1::VersionedResponse::try_from(self.clone())?; + let (extracted, version) = versioned_response.extract()?; + Ok((extracted.into(), version)) + } + } + } +} diff --git a/common/wireguard-private-metadata/shared/src/models/mod.rs b/common/wireguard-private-metadata/shared/src/models/mod.rs new file mode 100644 index 00000000000..e408d7c5dfb --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/mod.rs @@ -0,0 +1,175 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use std::fmt::{Display, Formatter}; + +use axum::http::StatusCode; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +pub(crate) mod error; +pub mod interface; +#[cfg(feature = "testing")] +pub mod v0; // dummy version, only for filling boilerplate code for update/downgrade and testing +pub mod v1; + +pub use v1 as latest; + +use crate::models::error::Error; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)] +pub enum Version { + #[cfg(feature = "testing")] + /// only used for testing purposes, don't include it in your matching arms + V0, + V1, +} + +impl From for Version { + fn from(value: u64) -> Self { + #[cfg(feature = "testing")] + let zero_version = Version::V0; + #[cfg(not(feature = "testing"))] + let zero_version = latest::VERSION; + match value { + 0 => zero_version, + 1 => Version::V1, + _ => latest::VERSION, // if unknown, it means we're behind, so we can use the latest we know about + } + } +} + +impl From for u64 { + fn from(value: Version) -> Self { + // remember to modify the above match if you're bumping the version + match value { + #[cfg(feature = "testing")] + Version::V0 => 0, + Version::V1 => 1, + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] +pub struct Request { + pub version: Version, + pub(crate) inner: Vec, +} + +#[derive(Clone, Serialize, Deserialize, ToSchema)] +pub struct Response { + pub version: Version, + pub(crate) inner: Vec, +} + +pub trait Extract { + fn extract(&self) -> Result<(T, Version), Error>; +} + +pub trait Construct: Sized { + fn construct(info: T, version: Version) -> Result; +} + +pub type AxumResult = Result; + +#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)] +pub struct ErrorResponse { + pub message: String, +} + +impl Display for ErrorResponse { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + self.message.fmt(f) + } +} + +pub struct AxumErrorResponse { + message: ErrorResponse, + status: StatusCode, +} + +impl AxumErrorResponse { + pub fn bad_request(msg: impl Display) -> Self { + Self { + message: ErrorResponse { + message: msg.to_string(), + }, + status: StatusCode::BAD_REQUEST, + } + } +} + +impl axum::response::IntoResponse for AxumErrorResponse { + fn into_response(self) -> axum::response::Response { + (self.status, self.message.message.to_string()).into_response() + } +} + +mod tests { + #[allow(dead_code)] + pub(crate) const CREDENTIAL_BYTES: [u8; 1245] = [ + 0, 0, 4, 133, 96, 179, 223, 185, 136, 23, 213, 166, 59, 203, 66, 69, 209, 181, 227, 254, + 16, 102, 98, 237, 59, 119, 170, 111, 31, 194, 51, 59, 120, 17, 115, 229, 79, 91, 11, 139, + 154, 2, 212, 23, 68, 70, 167, 3, 240, 54, 224, 171, 221, 1, 69, 48, 60, 118, 119, 249, 123, + 35, 172, 227, 131, 96, 232, 209, 187, 123, 4, 197, 102, 90, 96, 45, 125, 135, 140, 99, 1, + 151, 17, 131, 143, 157, 97, 107, 139, 232, 212, 87, 14, 115, 253, 255, 166, 167, 186, 43, + 90, 96, 173, 105, 120, 40, 10, 163, 250, 224, 214, 200, 178, 4, 160, 16, 130, 59, 76, 193, + 39, 240, 3, 101, 141, 209, 183, 226, 186, 207, 56, 210, 187, 7, 164, 240, 164, 205, 37, 81, + 184, 214, 193, 195, 90, 205, 238, 225, 195, 104, 12, 123, 203, 57, 233, 243, 215, 145, 195, + 196, 57, 38, 125, 172, 18, 47, 63, 165, 110, 219, 180, 40, 58, 116, 92, 254, 160, 98, 48, + 92, 254, 232, 107, 184, 80, 234, 60, 160, 235, 249, 76, 41, 38, 165, 28, 40, 136, 74, 48, + 166, 50, 245, 23, 201, 140, 101, 79, 93, 235, 128, 186, 146, 126, 180, 134, 43, 13, 186, + 19, 195, 48, 168, 201, 29, 216, 95, 176, 198, 132, 188, 64, 39, 212, 150, 32, 52, 53, 38, + 228, 199, 122, 226, 217, 75, 40, 191, 151, 48, 164, 242, 177, 79, 14, 122, 105, 151, 85, + 88, 199, 162, 17, 96, 103, 83, 178, 128, 9, 24, 30, 74, 108, 241, 85, 240, 166, 97, 241, + 85, 199, 11, 198, 226, 234, 70, 107, 145, 28, 208, 114, 51, 12, 234, 108, 101, 202, 112, + 48, 185, 22, 159, 67, 109, 49, 27, 149, 90, 109, 32, 226, 112, 7, 201, 208, 209, 104, 31, + 97, 134, 204, 145, 27, 181, 206, 181, 106, 32, 110, 136, 115, 249, 201, 111, 5, 245, 203, + 71, 121, 169, 126, 151, 178, 236, 59, 221, 195, 48, 135, 115, 6, 50, 227, 74, 97, 107, 107, + 213, 90, 2, 203, 154, 138, 47, 128, 52, 134, 128, 224, 51, 65, 240, 90, 8, 55, 175, 180, + 178, 204, 206, 168, 110, 51, 57, 189, 169, 48, 169, 136, 121, 99, 51, 170, 178, 214, 74, 1, + 96, 151, 167, 25, 173, 180, 171, 155, 10, 55, 142, 234, 190, 113, 90, 79, 80, 244, 71, 166, + 30, 235, 113, 150, 133, 1, 218, 17, 109, 111, 223, 24, 216, 177, 41, 2, 204, 65, 221, 212, + 207, 236, 144, 6, 65, 224, 55, 42, 1, 1, 161, 134, 118, 127, 111, 220, 110, 127, 240, 71, + 223, 129, 12, 93, 20, 220, 60, 56, 71, 146, 184, 95, 132, 69, 28, 56, 53, 192, 213, 22, + 119, 230, 152, 225, 182, 188, 163, 219, 37, 175, 247, 73, 14, 247, 38, 72, 243, 1, 48, 131, + 59, 8, 13, 96, 143, 185, 127, 241, 161, 217, 24, 149, 193, 40, 16, 30, 202, 151, 28, 119, + 240, 153, 101, 156, 61, 193, 72, 245, 199, 181, 12, 231, 65, 166, 67, 142, 121, 207, 202, + 58, 197, 113, 188, 248, 42, 124, 105, 48, 161, 241, 55, 209, 36, 194, 27, 63, 233, 144, + 189, 85, 117, 234, 9, 139, 46, 31, 206, 114, 95, 131, 29, 240, 13, 81, 142, 140, 133, 33, + 30, 41, 141, 37, 80, 217, 95, 221, 76, 115, 86, 201, 165, 51, 252, 9, 28, 209, 1, 48, 150, + 74, 248, 212, 187, 222, 66, 210, 3, 200, 19, 217, 171, 184, 42, 148, 53, 150, 57, 50, 6, + 227, 227, 62, 49, 42, 148, 148, 157, 82, 191, 58, 24, 34, 56, 98, 120, 89, 105, 176, 85, + 15, 253, 241, 41, 153, 195, 136, 1, 48, 142, 126, 213, 101, 223, 79, 133, 230, 105, 38, + 161, 149, 2, 21, 136, 150, 42, 72, 218, 85, 146, 63, 223, 58, 108, 186, 183, 248, 62, 20, + 47, 34, 113, 160, 177, 204, 181, 16, 24, 212, 224, 35, 84, 51, 168, 56, 136, 11, 1, 48, + 135, 242, 62, 149, 230, 178, 32, 224, 119, 26, 234, 163, 237, 224, 114, 95, 112, 140, 170, + 150, 96, 125, 136, 221, 180, 78, 18, 11, 12, 184, 2, 198, 217, 119, 43, 69, 4, 172, 109, + 55, 183, 40, 131, 172, 161, 88, 183, 101, 1, 48, 173, 216, 22, 73, 42, 255, 211, 93, 249, + 87, 159, 115, 61, 91, 55, 130, 17, 216, 60, 34, 122, 55, 8, 244, 244, 153, 151, 57, 5, 144, + 178, 55, 249, 64, 211, 168, 34, 148, 56, 89, 92, 203, 70, 124, 219, 152, 253, 165, 0, 32, + 203, 116, 63, 7, 240, 222, 82, 86, 11, 149, 167, 72, 224, 55, 190, 66, 201, 65, 168, 184, + 96, 47, 194, 241, 168, 124, 7, 74, 214, 250, 37, 76, 32, 218, 69, 122, 103, 215, 145, 169, + 24, 212, 229, 168, 106, 10, 144, 31, 13, 25, 178, 242, 250, 106, 159, 40, 48, 163, 165, 61, + 130, 57, 146, 4, 73, 32, 254, 233, 125, 135, 212, 29, 111, 4, 177, 114, 15, 210, 170, 82, + 108, 110, 62, 166, 81, 209, 106, 176, 156, 14, 133, 242, 60, 127, 120, 242, 28, 97, 0, 1, + 32, 103, 93, 109, 89, 240, 91, 1, 84, 150, 50, 206, 157, 203, 49, 220, 120, 234, 175, 234, + 150, 126, 225, 94, 163, 164, 199, 138, 114, 62, 99, 106, 112, 1, 32, 171, 40, 220, 82, 241, + 203, 76, 146, 111, 139, 182, 179, 237, 182, 115, 75, 128, 201, 107, 43, 214, 0, 135, 217, + 160, 68, 150, 232, 144, 114, 237, 98, 32, 30, 134, 232, 59, 93, 163, 253, 244, 13, 202, 52, + 147, 168, 83, 121, 123, 95, 21, 210, 209, 225, 223, 143, 49, 10, 205, 238, 1, 22, 83, 81, + 70, 1, 32, 26, 76, 6, 234, 160, 50, 139, 102, 161, 232, 155, 106, 130, 171, 226, 210, 233, + 178, 85, 247, 71, 123, 55, 53, 46, 67, 148, 137, 156, 207, 208, 107, 1, 32, 102, 31, 4, 98, + 110, 156, 144, 61, 229, 140, 198, 84, 196, 238, 128, 35, 131, 182, 137, 125, 241, 95, 69, + 131, 170, 27, 2, 144, 75, 72, 242, 102, 3, 32, 121, 80, 45, 173, 56, 65, 218, 27, 40, 251, + 197, 32, 169, 104, 123, 110, 90, 78, 153, 166, 38, 9, 129, 228, 99, 8, 1, 116, 142, 233, + 162, 69, 32, 216, 169, 159, 116, 95, 12, 63, 176, 195, 6, 183, 123, 135, 75, 61, 112, 106, + 83, 235, 176, 41, 27, 248, 48, 71, 165, 170, 12, 92, 103, 103, 81, 32, 58, 74, 75, 145, + 192, 94, 153, 69, 80, 128, 241, 3, 16, 117, 192, 86, 161, 103, 44, 174, 211, 196, 182, 124, + 55, 11, 107, 142, 49, 88, 6, 41, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 0, 37, 139, 240, 0, 0, + 0, 0, 0, 0, 0, 1, + ]; +} diff --git a/common/wireguard-private-metadata/shared/src/models/v0/available_bandwidth/mod.rs b/common/wireguard-private-metadata/shared/src/models/v0/available_bandwidth/mod.rs new file mode 100644 index 00000000000..10698cca2d2 --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v0/available_bandwidth/mod.rs @@ -0,0 +1,5 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod request; +pub mod response; diff --git a/common/wireguard-private-metadata/shared/src/models/v0/available_bandwidth/request.rs b/common/wireguard-private-metadata/shared/src/models/v0/available_bandwidth/request.rs new file mode 100644 index 00000000000..78dfdec7b57 --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v0/available_bandwidth/request.rs @@ -0,0 +1,86 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use bincode::Options; +use serde::{Deserialize, Serialize}; + +use crate::{make_bincode_serializer, models::Request}; + +use super::super::{Error, QueryType, VersionedRequest}; + +#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct InnerAvailableBandwidthRequest {} + +impl TryFrom for InnerAvailableBandwidthRequest { + type Error = Error; + + fn try_from(value: VersionedRequest) -> Result { + match value.query_type { + QueryType::AvailableBandwidth => { + Ok(make_bincode_serializer().deserialize(&value.inner)?) + } + QueryType::TopupBandwidth => Err(Error::InvalidQueryType { + source_query_type: value.query_type.to_string(), + target_query_type: QueryType::AvailableBandwidth.to_string(), + }), + } + } +} + +impl TryFrom for VersionedRequest { + type Error = Error; + + fn try_from(value: InnerAvailableBandwidthRequest) -> Result { + Ok(Self { + query_type: QueryType::AvailableBandwidth, + inner: make_bincode_serializer().serialize(&value)?, + }) + } +} + +impl TryFrom for InnerAvailableBandwidthRequest { + type Error = crate::error::MetadataError; + + fn try_from(value: Request) -> Result { + VersionedRequest::try_from(value)? + .try_into() + .map_err(|err: Error| crate::error::MetadataError::Models { + message: err.to_string(), + }) + } +} + +impl TryFrom for Request { + type Error = crate::error::MetadataError; + + fn try_from(value: InnerAvailableBandwidthRequest) -> Result { + VersionedRequest::try_from(value)? + .try_into() + .map_err(|err: Error| crate::error::MetadataError::Models { + message: err.to_string(), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn serde() { + let req = InnerAvailableBandwidthRequest {}; + let ser = VersionedRequest::try_from(req).unwrap(); + assert_eq!(QueryType::AvailableBandwidth, ser.query_type); + let de = InnerAvailableBandwidthRequest::try_from(ser).unwrap(); + assert_eq!(req, de); + } + + #[test] + fn empty_content() { + let future_req = VersionedRequest { + query_type: QueryType::AvailableBandwidth, + inner: vec![], + }; + assert!(InnerAvailableBandwidthRequest::try_from(future_req).is_ok()); + } +} diff --git a/common/wireguard-private-metadata/shared/src/models/v0/available_bandwidth/response.rs b/common/wireguard-private-metadata/shared/src/models/v0/available_bandwidth/response.rs new file mode 100644 index 00000000000..5243e312ee8 --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v0/available_bandwidth/response.rs @@ -0,0 +1,86 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use bincode::Options; +use serde::{Deserialize, Serialize}; + +use crate::{make_bincode_serializer, models::Response}; + +use super::super::{Error, QueryType, VersionedResponse}; + +#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct InnerAvailableBandwidthResponse {} + +impl TryFrom for InnerAvailableBandwidthResponse { + type Error = Error; + + fn try_from(value: VersionedResponse) -> Result { + match value.query_type { + QueryType::AvailableBandwidth => { + Ok(make_bincode_serializer().deserialize(&value.inner)?) + } + QueryType::TopupBandwidth => Err(Error::InvalidQueryType { + source_query_type: value.query_type.to_string(), + target_query_type: QueryType::AvailableBandwidth.to_string(), + }), + } + } +} + +impl TryFrom for VersionedResponse { + type Error = Error; + + fn try_from(value: InnerAvailableBandwidthResponse) -> Result { + Ok(Self { + query_type: QueryType::AvailableBandwidth, + inner: make_bincode_serializer().serialize(&value)?, + }) + } +} + +impl TryFrom for InnerAvailableBandwidthResponse { + type Error = crate::error::MetadataError; + + fn try_from(value: Response) -> Result { + VersionedResponse::try_from(value)? + .try_into() + .map_err(|err: Error| crate::error::MetadataError::Models { + message: err.to_string(), + }) + } +} + +impl TryFrom for Response { + type Error = crate::error::MetadataError; + + fn try_from(value: InnerAvailableBandwidthResponse) -> Result { + VersionedResponse::try_from(value)? + .try_into() + .map_err(|err: Error| crate::error::MetadataError::Models { + message: err.to_string(), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn serde() { + let resp = InnerAvailableBandwidthResponse {}; + let ser = VersionedResponse::try_from(resp).unwrap(); + assert_eq!(QueryType::AvailableBandwidth, ser.query_type); + let de = InnerAvailableBandwidthResponse::try_from(ser).unwrap(); + assert_eq!(resp, de); + } + + #[test] + fn empty_content() { + let future_resp = VersionedResponse { + query_type: QueryType::AvailableBandwidth, + inner: vec![], + }; + assert!(InnerAvailableBandwidthResponse::try_from(future_resp).is_ok()); + } +} diff --git a/common/wireguard-private-metadata/shared/src/models/v0/interface.rs b/common/wireguard-private-metadata/shared/src/models/v0/interface.rs new file mode 100644 index 00000000000..2f8fd2805cd --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v0/interface.rs @@ -0,0 +1,73 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use super::{ + available_bandwidth::{ + request::InnerAvailableBandwidthRequest, response::InnerAvailableBandwidthResponse, + }, + topup_bandwidth::{request::InnerTopUpRequest, response::InnerTopUpResponse}, + QueryType, VersionedRequest, VersionedResponse, VERSION, +}; +use crate::models::{error::Error, Construct, Extract, Version}; + +#[derive(Debug, Clone, PartialEq)] +pub enum RequestData { + AvailableBandwidth(()), + TopUpBandwidth(()), +} + +#[derive(Debug, Clone, PartialEq)] +pub enum ResponseData { + AvailableBandwidth(()), + TopUpBandwidth(()), +} + +impl Construct for VersionedRequest { + fn construct(info: RequestData, _version: Version) -> Result { + match info { + RequestData::AvailableBandwidth(_) => Ok(InnerAvailableBandwidthRequest {}.try_into()?), + RequestData::TopUpBandwidth(_) => Ok(InnerTopUpRequest {}.try_into()?), + } + } +} + +impl Extract for VersionedRequest { + fn extract(&self) -> Result<(RequestData, Version), Error> { + match self.query_type { + QueryType::AvailableBandwidth => { + let _req = InnerAvailableBandwidthRequest::try_from(self.clone())?; + Ok((RequestData::AvailableBandwidth(()), VERSION)) + } + QueryType::TopupBandwidth => { + let _req = InnerTopUpRequest::try_from(self.clone())?; + Ok((RequestData::TopUpBandwidth(()), VERSION)) + } + } + } +} + +impl Construct for VersionedResponse { + fn construct(info: ResponseData, _version: Version) -> Result { + match info { + ResponseData::AvailableBandwidth(()) => { + Ok(InnerAvailableBandwidthResponse {}.try_into()?) + } + ResponseData::TopUpBandwidth(()) => Ok(InnerTopUpResponse {}.try_into()?), + } + } +} + +impl Extract for VersionedResponse { + fn extract(&self) -> Result<(ResponseData, Version), Error> { + match self.query_type { + QueryType::AvailableBandwidth => { + let _resp = InnerAvailableBandwidthResponse::try_from(self.clone())?; + Ok((ResponseData::AvailableBandwidth(()), VERSION)) + } + QueryType::TopupBandwidth => { + let _resp = InnerTopUpResponse::try_from(self.clone())?; + Ok((ResponseData::TopUpBandwidth(()), VERSION)) + } + } + } +} diff --git a/common/wireguard-private-metadata/shared/src/models/v0/mod.rs b/common/wireguard-private-metadata/shared/src/models/v0/mod.rs new file mode 100644 index 00000000000..997fbf6f572 --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v0/mod.rs @@ -0,0 +1,175 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use std::fmt::Display; + +use bincode::Options; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +use super::error::Error; +use crate::{ + make_bincode_serializer, + models::{Request, Response, Version}, +}; + +pub(crate) mod available_bandwidth; +pub mod interface; +pub(crate) mod topup_bandwidth; + +pub const VERSION: Version = Version::V0; + +pub use available_bandwidth::{ + request::InnerAvailableBandwidthRequest as AvailableBandwidthRequest, + response::InnerAvailableBandwidthResponse as AvailableBandwidthResponse, +}; +pub use topup_bandwidth::{ + request::InnerTopUpRequest as TopUpRequest, response::InnerTopUpResponse as TopUpResponse, +}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)] +pub enum QueryType { + AvailableBandwidth, + TopupBandwidth, +} + +impl Display for QueryType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{self:?}") + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)] +pub struct VersionedRequest { + query_type: QueryType, + inner: Vec, +} + +impl TryFrom for Request { + type Error = Error; + + fn try_from(value: VersionedRequest) -> Result { + Ok(Request { + version: VERSION, + inner: make_bincode_serializer().serialize(&value)?, + }) + } +} + +impl TryFrom for VersionedRequest { + type Error = Error; + + fn try_from(value: Request) -> Result { + if value.version != VERSION { + return Err(Error::InvalidVersion { + source_version: value.version, + target_version: VERSION, + }); + } + Ok(make_bincode_serializer().deserialize(&value.inner)?) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)] +pub struct VersionedResponse { + query_type: QueryType, + inner: Vec, +} + +impl TryFrom for Response { + type Error = Error; + + fn try_from(value: VersionedResponse) -> Result { + Ok(Response { + version: VERSION, + inner: make_bincode_serializer().serialize(&value)?, + }) + } +} + +impl TryFrom for VersionedResponse { + type Error = Error; + + fn try_from(value: Response) -> Result { + if value.version != VERSION { + return Err(Error::InvalidVersion { + source_version: value.version, + target_version: VERSION, + }); + } + Ok(make_bincode_serializer().deserialize(&value.inner)?) + } +} + +#[cfg(test)] +mod tests { + use self::{ + available_bandwidth::{ + request::InnerAvailableBandwidthRequest, response::InnerAvailableBandwidthResponse, + }, + topup_bandwidth::{request::InnerTopUpRequest, response::InnerTopUpResponse}, + }; + + use super::*; + + #[test] + fn serde_request_av_bw() { + let req = VersionedRequest { + query_type: QueryType::AvailableBandwidth, + inner: make_bincode_serializer() + .serialize(&InnerAvailableBandwidthRequest {}) + .unwrap(), + }; + + let ser = Request::try_from(req.clone()).unwrap(); + assert_eq!(VERSION, ser.version); + let de = VersionedRequest::try_from(ser).unwrap(); + assert_eq!(req, de); + } + + #[test] + fn serde_response_av_bw() { + let resp = VersionedResponse { + query_type: QueryType::AvailableBandwidth, + inner: make_bincode_serializer() + .serialize(&InnerAvailableBandwidthResponse {}) + .unwrap(), + }; + + let ser = Response::try_from(resp.clone()).unwrap(); + assert_eq!(VERSION, ser.version); + let de = VersionedResponse::try_from(ser).unwrap(); + assert_eq!(resp, de); + } + + #[test] + fn serde_request_topup() { + let req = VersionedRequest { + query_type: QueryType::TopupBandwidth, + inner: make_bincode_serializer() + .serialize(&InnerTopUpRequest {}) + .unwrap(), + }; + + let ser = Request::try_from(req.clone()).unwrap(); + assert_eq!(VERSION, ser.version); + let de = VersionedRequest::try_from(ser).unwrap(); + assert_eq!(req, de); + } + + #[test] + fn serde_response_topup() { + let resp = VersionedResponse { + query_type: QueryType::TopupBandwidth, + inner: make_bincode_serializer() + .serialize(&InnerTopUpResponse {}) + .unwrap(), + }; + + let ser = Response::try_from(resp.clone()).unwrap(); + assert_eq!(VERSION, ser.version); + let de = VersionedResponse::try_from(ser).unwrap(); + assert_eq!(resp, de); + } +} diff --git a/common/wireguard-private-metadata/shared/src/models/v0/topup_bandwidth/mod.rs b/common/wireguard-private-metadata/shared/src/models/v0/topup_bandwidth/mod.rs new file mode 100644 index 00000000000..10698cca2d2 --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v0/topup_bandwidth/mod.rs @@ -0,0 +1,5 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod request; +pub mod response; diff --git a/common/wireguard-private-metadata/shared/src/models/v0/topup_bandwidth/request.rs b/common/wireguard-private-metadata/shared/src/models/v0/topup_bandwidth/request.rs new file mode 100644 index 00000000000..9c333478d2e --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v0/topup_bandwidth/request.rs @@ -0,0 +1,84 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use bincode::Options; +use serde::{Deserialize, Serialize}; + +use crate::{make_bincode_serializer, models::Request}; + +use super::super::{Error, QueryType, VersionedRequest}; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct InnerTopUpRequest {} + +impl TryFrom for InnerTopUpRequest { + type Error = Error; + + fn try_from(value: VersionedRequest) -> Result { + match value.query_type { + QueryType::TopupBandwidth => Ok(make_bincode_serializer().deserialize(&value.inner)?), + QueryType::AvailableBandwidth => Err(Error::InvalidQueryType { + source_query_type: value.query_type.to_string(), + target_query_type: QueryType::TopupBandwidth.to_string(), + }), + } + } +} + +impl TryFrom for VersionedRequest { + type Error = Error; + + fn try_from(value: InnerTopUpRequest) -> Result { + Ok(Self { + query_type: QueryType::TopupBandwidth, + inner: make_bincode_serializer().serialize(&value)?, + }) + } +} + +impl TryFrom for InnerTopUpRequest { + type Error = crate::error::MetadataError; + + fn try_from(value: Request) -> Result { + VersionedRequest::try_from(value)? + .try_into() + .map_err(|err: Error| crate::error::MetadataError::Models { + message: err.to_string(), + }) + } +} + +impl TryFrom for Request { + type Error = crate::error::MetadataError; + + fn try_from(value: InnerTopUpRequest) -> Result { + VersionedRequest::try_from(value)? + .try_into() + .map_err(|err: Error| crate::error::MetadataError::Models { + message: err.to_string(), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn serde() { + let req = InnerTopUpRequest {}; + let ser = VersionedRequest::try_from(req.clone()).unwrap(); + assert_eq!(QueryType::TopupBandwidth, ser.query_type); + let de = InnerTopUpRequest::try_from(ser).unwrap(); + assert_eq!(req, de); + } + + #[test] + fn empty_content() { + let future_req = VersionedRequest { + query_type: QueryType::TopupBandwidth, + inner: vec![], + }; + assert!(InnerTopUpRequest::try_from(future_req).is_ok()); + } +} diff --git a/common/wireguard-private-metadata/shared/src/models/v0/topup_bandwidth/response.rs b/common/wireguard-private-metadata/shared/src/models/v0/topup_bandwidth/response.rs new file mode 100644 index 00000000000..cd934b6e7e8 --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v0/topup_bandwidth/response.rs @@ -0,0 +1,84 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use bincode::Options; +use serde::{Deserialize, Serialize}; + +use crate::{make_bincode_serializer, models::Response}; + +use super::super::{Error, QueryType, VersionedResponse}; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct InnerTopUpResponse {} + +impl TryFrom for InnerTopUpResponse { + type Error = Error; + + fn try_from(value: VersionedResponse) -> Result { + match value.query_type { + QueryType::TopupBandwidth => Ok(make_bincode_serializer().deserialize(&value.inner)?), + QueryType::AvailableBandwidth => Err(Error::InvalidQueryType { + source_query_type: value.query_type.to_string(), + target_query_type: QueryType::TopupBandwidth.to_string(), + }), + } + } +} + +impl TryFrom for VersionedResponse { + type Error = Error; + + fn try_from(value: InnerTopUpResponse) -> Result { + Ok(Self { + query_type: QueryType::TopupBandwidth, + inner: make_bincode_serializer().serialize(&value)?, + }) + } +} + +impl TryFrom for InnerTopUpResponse { + type Error = crate::error::MetadataError; + + fn try_from(value: Response) -> Result { + VersionedResponse::try_from(value)? + .try_into() + .map_err(|err: Error| crate::error::MetadataError::Models { + message: err.to_string(), + }) + } +} + +impl TryFrom for Response { + type Error = crate::error::MetadataError; + + fn try_from(value: InnerTopUpResponse) -> Result { + VersionedResponse::try_from(value)? + .try_into() + .map_err(|err: Error| crate::error::MetadataError::Models { + message: err.to_string(), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn serde() { + let resp = InnerTopUpResponse {}; + let ser = VersionedResponse::try_from(resp.clone()).unwrap(); + assert_eq!(QueryType::TopupBandwidth, ser.query_type); + let de = InnerTopUpResponse::try_from(ser).unwrap(); + assert_eq!(resp, de); + } + + #[test] + fn empty_content() { + let future_resp = VersionedResponse { + query_type: QueryType::TopupBandwidth, + inner: vec![], + }; + assert!(InnerTopUpResponse::try_from(future_resp).is_ok()); + } +} diff --git a/common/wireguard-private-metadata/shared/src/models/v1/available_bandwidth/mod.rs b/common/wireguard-private-metadata/shared/src/models/v1/available_bandwidth/mod.rs new file mode 100644 index 00000000000..10698cca2d2 --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v1/available_bandwidth/mod.rs @@ -0,0 +1,5 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod request; +pub mod response; diff --git a/common/wireguard-private-metadata/shared/src/models/v1/available_bandwidth/request.rs b/common/wireguard-private-metadata/shared/src/models/v1/available_bandwidth/request.rs new file mode 100644 index 00000000000..78dfdec7b57 --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v1/available_bandwidth/request.rs @@ -0,0 +1,86 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use bincode::Options; +use serde::{Deserialize, Serialize}; + +use crate::{make_bincode_serializer, models::Request}; + +use super::super::{Error, QueryType, VersionedRequest}; + +#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct InnerAvailableBandwidthRequest {} + +impl TryFrom for InnerAvailableBandwidthRequest { + type Error = Error; + + fn try_from(value: VersionedRequest) -> Result { + match value.query_type { + QueryType::AvailableBandwidth => { + Ok(make_bincode_serializer().deserialize(&value.inner)?) + } + QueryType::TopupBandwidth => Err(Error::InvalidQueryType { + source_query_type: value.query_type.to_string(), + target_query_type: QueryType::AvailableBandwidth.to_string(), + }), + } + } +} + +impl TryFrom for VersionedRequest { + type Error = Error; + + fn try_from(value: InnerAvailableBandwidthRequest) -> Result { + Ok(Self { + query_type: QueryType::AvailableBandwidth, + inner: make_bincode_serializer().serialize(&value)?, + }) + } +} + +impl TryFrom for InnerAvailableBandwidthRequest { + type Error = crate::error::MetadataError; + + fn try_from(value: Request) -> Result { + VersionedRequest::try_from(value)? + .try_into() + .map_err(|err: Error| crate::error::MetadataError::Models { + message: err.to_string(), + }) + } +} + +impl TryFrom for Request { + type Error = crate::error::MetadataError; + + fn try_from(value: InnerAvailableBandwidthRequest) -> Result { + VersionedRequest::try_from(value)? + .try_into() + .map_err(|err: Error| crate::error::MetadataError::Models { + message: err.to_string(), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn serde() { + let req = InnerAvailableBandwidthRequest {}; + let ser = VersionedRequest::try_from(req).unwrap(); + assert_eq!(QueryType::AvailableBandwidth, ser.query_type); + let de = InnerAvailableBandwidthRequest::try_from(ser).unwrap(); + assert_eq!(req, de); + } + + #[test] + fn empty_content() { + let future_req = VersionedRequest { + query_type: QueryType::AvailableBandwidth, + inner: vec![], + }; + assert!(InnerAvailableBandwidthRequest::try_from(future_req).is_ok()); + } +} diff --git a/common/wireguard-private-metadata/shared/src/models/v1/available_bandwidth/response.rs b/common/wireguard-private-metadata/shared/src/models/v1/available_bandwidth/response.rs new file mode 100644 index 00000000000..f5addd609f3 --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v1/available_bandwidth/response.rs @@ -0,0 +1,90 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use bincode::Options; +use serde::{Deserialize, Serialize}; + +use crate::{make_bincode_serializer, models::Response}; + +use super::super::{Error, QueryType, VersionedResponse}; + +#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct InnerAvailableBandwidthResponse { + pub available_bandwidth: i64, +} + +impl TryFrom for InnerAvailableBandwidthResponse { + type Error = Error; + + fn try_from(value: VersionedResponse) -> Result { + match value.query_type { + QueryType::AvailableBandwidth => { + Ok(make_bincode_serializer().deserialize(&value.inner)?) + } + QueryType::TopupBandwidth => Err(Error::InvalidQueryType { + source_query_type: value.query_type.to_string(), + target_query_type: QueryType::AvailableBandwidth.to_string(), + }), + } + } +} + +impl TryFrom for VersionedResponse { + type Error = Error; + + fn try_from(value: InnerAvailableBandwidthResponse) -> Result { + Ok(Self { + query_type: QueryType::AvailableBandwidth, + inner: make_bincode_serializer().serialize(&value)?, + }) + } +} + +impl TryFrom for InnerAvailableBandwidthResponse { + type Error = crate::error::MetadataError; + + fn try_from(value: Response) -> Result { + VersionedResponse::try_from(value)? + .try_into() + .map_err(|err: Error| crate::error::MetadataError::Models { + message: err.to_string(), + }) + } +} + +impl TryFrom for Response { + type Error = crate::error::MetadataError; + + fn try_from(value: InnerAvailableBandwidthResponse) -> Result { + VersionedResponse::try_from(value)? + .try_into() + .map_err(|err: Error| crate::error::MetadataError::Models { + message: err.to_string(), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn serde() { + let resp = InnerAvailableBandwidthResponse { + available_bandwidth: 42, + }; + let ser = VersionedResponse::try_from(resp).unwrap(); + assert_eq!(QueryType::AvailableBandwidth, ser.query_type); + let de = InnerAvailableBandwidthResponse::try_from(ser).unwrap(); + assert_eq!(resp, de); + } + + #[test] + fn invalid_content() { + let future_resp = VersionedResponse { + query_type: QueryType::AvailableBandwidth, + inner: vec![], + }; + assert!(InnerAvailableBandwidthResponse::try_from(future_resp).is_err()); + } +} diff --git a/common/wireguard-private-metadata/shared/src/models/v1/interface.rs b/common/wireguard-private-metadata/shared/src/models/v1/interface.rs new file mode 100644 index 00000000000..46cafddc7da --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v1/interface.rs @@ -0,0 +1,224 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_credentials_interface::CredentialSpendingData; + +#[cfg(feature = "testing")] +use super::super::v0 as previous; + +use super::{ + available_bandwidth::{ + request::InnerAvailableBandwidthRequest, response::InnerAvailableBandwidthResponse, + }, + topup_bandwidth::{request::InnerTopUpRequest, response::InnerTopUpResponse}, + QueryType, VersionedRequest, VersionedResponse, VERSION, +}; +use crate::models::{error::Error, Construct, Extract, Version}; + +#[derive(Debug, Clone, PartialEq)] +pub enum RequestData { + AvailableBandwidth(()), + TopUpBandwidth(Box), +} + +#[derive(Debug, Clone, PartialEq)] +pub enum ResponseData { + AvailableBandwidth(i64), + TopUpBandwidth(i64), +} + +impl Construct for VersionedRequest { + fn construct(info: RequestData, _version: Version) -> Result { + match info { + RequestData::AvailableBandwidth(_) => Ok(InnerAvailableBandwidthRequest {}.try_into()?), + RequestData::TopUpBandwidth(credential) => Ok(InnerTopUpRequest { + credential: *credential, + } + .try_into()?), + } + } +} + +impl Extract for VersionedRequest { + fn extract(&self) -> Result<(RequestData, Version), Error> { + match self.query_type { + QueryType::AvailableBandwidth => { + let _req = InnerAvailableBandwidthRequest::try_from(self.clone())?; + Ok((RequestData::AvailableBandwidth(()), VERSION)) + } + QueryType::TopupBandwidth => { + let req = InnerTopUpRequest::try_from(self.clone())?; + Ok(( + RequestData::TopUpBandwidth(Box::new(req.credential)), + VERSION, + )) + } + } + } +} + +impl Construct for VersionedResponse { + fn construct(info: ResponseData, _version: Version) -> Result { + match info { + ResponseData::AvailableBandwidth(available_bandwidth) => { + Ok(InnerAvailableBandwidthResponse { + available_bandwidth, + } + .try_into()?) + } + ResponseData::TopUpBandwidth(available_bandwidth) => Ok(InnerTopUpResponse { + available_bandwidth, + } + .try_into()?), + } + } +} + +impl Extract for VersionedResponse { + fn extract(&self) -> Result<(ResponseData, Version), Error> { + match self.query_type { + QueryType::AvailableBandwidth => { + let resp = InnerAvailableBandwidthResponse::try_from(self.clone())?; + Ok(( + ResponseData::AvailableBandwidth(resp.available_bandwidth), + VERSION, + )) + } + QueryType::TopupBandwidth => { + let resp = InnerTopUpResponse::try_from(self.clone())?; + Ok(( + ResponseData::TopUpBandwidth(resp.available_bandwidth), + VERSION, + )) + } + } + } +} + +// this should be with #[cfg(feature = "testing")] only coming from v0, don't copy this for future versions +#[cfg(feature = "testing")] +impl TryFrom for RequestData { + type Error = super::Error; + + fn try_from(value: previous::interface::RequestData) -> Result { + match value { + previous::interface::RequestData::AvailableBandwidth(inner) => { + Ok(Self::AvailableBandwidth(inner)) + } + previous::interface::RequestData::TopUpBandwidth(_) => { + Err(super::Error::UpdateNotPossible { + from: previous::VERSION, + to: VERSION, + }) + } + } + } +} + +// this should be with #[cfg(feature = "testing")] only coming from v0, don't copy this for future versions +#[cfg(feature = "testing")] +impl TryFrom for previous::interface::RequestData { + type Error = super::Error; + + fn try_from(value: RequestData) -> Result { + match value { + RequestData::AvailableBandwidth(inner) => Ok(Self::AvailableBandwidth(inner)), + RequestData::TopUpBandwidth(_) => Ok(Self::TopUpBandwidth(())), + } + } +} + +// this should be with #[cfg(feature = "testing")] only coming from v0, don't copy this for future versions +#[cfg(feature = "testing")] +impl TryFrom for ResponseData { + type Error = super::Error; + + fn try_from(value: previous::interface::ResponseData) -> Result { + match value { + previous::interface::ResponseData::AvailableBandwidth(_) => { + Err(super::Error::UpdateNotPossible { + from: previous::VERSION, + to: VERSION, + }) + } + previous::interface::ResponseData::TopUpBandwidth(_) => { + Err(super::Error::UpdateNotPossible { + from: previous::VERSION, + to: VERSION, + }) + } + } + } +} + +// this should be with #[cfg(feature = "testing")] only coming from v0, don't copy this for future versions +#[cfg(feature = "testing")] +impl TryFrom for previous::interface::ResponseData { + type Error = super::Error; + + fn try_from(value: ResponseData) -> Result { + match value { + ResponseData::AvailableBandwidth(_) => Ok(Self::AvailableBandwidth(())), + ResponseData::TopUpBandwidth(_) => Ok(Self::TopUpBandwidth(())), + } + } +} + +#[cfg(test)] +mod test { + use crate::models::tests::CREDENTIAL_BYTES; + + use super::*; + + #[test] + fn request_upgrade() { + assert_eq!( + RequestData::try_from(previous::interface::RequestData::AvailableBandwidth(())) + .unwrap(), + RequestData::AvailableBandwidth(()) + ); + assert!( + RequestData::try_from(previous::interface::RequestData::TopUpBandwidth(())).is_err(), + ); + } + + #[test] + fn response_upgrade() { + assert!( + ResponseData::try_from(previous::interface::ResponseData::AvailableBandwidth(())) + .is_err() + ); + assert!( + ResponseData::try_from(previous::interface::ResponseData::TopUpBandwidth(())).is_err() + ); + } + + #[test] + fn request_downgrade() { + assert_eq!( + previous::interface::RequestData::try_from(RequestData::AvailableBandwidth(())) + .unwrap(), + previous::interface::RequestData::AvailableBandwidth(()) + ); + assert_eq!( + previous::interface::RequestData::try_from(RequestData::TopUpBandwidth(Box::new( + CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap() + ))) + .unwrap(), + previous::interface::RequestData::TopUpBandwidth(()) + ); + } + + #[test] + fn response_downgrade() { + assert_eq!( + previous::interface::ResponseData::try_from(ResponseData::AvailableBandwidth(42)) + .unwrap(), + previous::interface::ResponseData::AvailableBandwidth(()) + ); + assert_eq!( + previous::interface::ResponseData::try_from(ResponseData::TopUpBandwidth(42)).unwrap(), + previous::interface::ResponseData::TopUpBandwidth(()) + ); + } +} diff --git a/common/wireguard-private-metadata/shared/src/models/v1/mod.rs b/common/wireguard-private-metadata/shared/src/models/v1/mod.rs new file mode 100644 index 00000000000..787a74f671b --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v1/mod.rs @@ -0,0 +1,224 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use std::fmt::Display; + +use bincode::Options; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +use super::error::Error; +use crate::{ + make_bincode_serializer, + models::{Request, Response, Version}, +}; + +pub use available_bandwidth::{ + request::InnerAvailableBandwidthRequest as AvailableBandwidthRequest, + response::InnerAvailableBandwidthResponse as AvailableBandwidthResponse, +}; +pub use topup_bandwidth::{ + request::InnerTopUpRequest as TopUpRequest, response::InnerTopUpResponse as TopUpResponse, +}; + +pub(crate) mod available_bandwidth; +pub mod interface; +pub(crate) mod topup_bandwidth; + +pub const VERSION: Version = Version::V1; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)] +pub enum QueryType { + AvailableBandwidth, + TopupBandwidth, +} + +impl Display for QueryType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{self:?}") + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)] +pub struct VersionedRequest { + query_type: QueryType, + inner: Vec, +} + +impl TryFrom for Request { + type Error = Error; + + fn try_from(value: VersionedRequest) -> Result { + Ok(Request { + version: VERSION, + inner: make_bincode_serializer().serialize(&value)?, + }) + } +} + +impl TryFrom for VersionedRequest { + type Error = Error; + + fn try_from(value: Request) -> Result { + if value.version != VERSION { + return Err(Error::InvalidVersion { + source_version: value.version, + target_version: VERSION, + }); + } + Ok(make_bincode_serializer().deserialize(&value.inner)?) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)] +pub struct VersionedResponse { + query_type: QueryType, + inner: Vec, +} + +impl TryFrom for Response { + type Error = Error; + + fn try_from(value: VersionedResponse) -> Result { + Ok(Response { + version: VERSION, + inner: make_bincode_serializer().serialize(&value)?, + }) + } +} + +impl TryFrom for VersionedResponse { + type Error = Error; + + fn try_from(value: Response) -> Result { + if value.version != VERSION { + return Err(Error::InvalidVersion { + source_version: value.version, + target_version: VERSION, + }); + } + Ok(make_bincode_serializer().deserialize(&value.inner)?) + } +} + +#[cfg(test)] +mod tests { + + use nym_credentials_interface::CredentialSpendingData; + + use crate::models::tests::CREDENTIAL_BYTES; + + use self::{ + available_bandwidth::{ + request::InnerAvailableBandwidthRequest, response::InnerAvailableBandwidthResponse, + }, + topup_bandwidth::{request::InnerTopUpRequest, response::InnerTopUpResponse}, + }; + + use super::*; + + #[test] + fn mismatched_request_version() { + let version = Version::V0; + let future_bw = Request { + version, + inner: vec![], + }; + if let Err(Error::InvalidVersion { + source_version, + target_version, + }) = VersionedRequest::try_from(future_bw) + { + assert_eq!(source_version, version); + assert_eq!(target_version, VERSION); + } else { + panic!("failed"); + }; + } + + #[test] + fn mismatched_response_version() { + let version = Version::V0; + let future_bw = Response { + version, + inner: vec![], + }; + if let Err(Error::InvalidVersion { + source_version, + target_version, + }) = VersionedResponse::try_from(future_bw) + { + assert_eq!(source_version, version); + assert_eq!(target_version, VERSION); + } else { + panic!("failed"); + }; + } + + #[test] + fn serde_request_av_bw() { + let req = VersionedRequest { + query_type: QueryType::AvailableBandwidth, + inner: make_bincode_serializer() + .serialize(&InnerAvailableBandwidthResponse { + available_bandwidth: 42, + }) + .unwrap(), + }; + + let ser = Request::try_from(req.clone()).unwrap(); + assert_eq!(VERSION, ser.version); + let de = VersionedRequest::try_from(ser).unwrap(); + assert_eq!(req, de); + } + + #[test] + fn serde_response_av_bw() { + let resp = VersionedResponse { + query_type: QueryType::AvailableBandwidth, + inner: make_bincode_serializer() + .serialize(&InnerAvailableBandwidthRequest {}) + .unwrap(), + }; + + let ser = Response::try_from(resp.clone()).unwrap(); + assert_eq!(VERSION, ser.version); + let de = VersionedResponse::try_from(ser).unwrap(); + assert_eq!(resp, de); + } + + #[test] + fn serde_request_topup() { + let req = VersionedRequest { + query_type: QueryType::TopupBandwidth, + inner: make_bincode_serializer() + .serialize(&InnerTopUpRequest { + credential: CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap(), + }) + .unwrap(), + }; + + let ser = Request::try_from(req.clone()).unwrap(); + assert_eq!(VERSION, ser.version); + let de = VersionedRequest::try_from(ser).unwrap(); + assert_eq!(req, de); + } + + #[test] + fn serde_response_topup() { + let resp = VersionedResponse { + query_type: QueryType::TopupBandwidth, + inner: make_bincode_serializer() + .serialize(&InnerTopUpResponse { + available_bandwidth: 42, + }) + .unwrap(), + }; + + let ser = Response::try_from(resp.clone()).unwrap(); + assert_eq!(VERSION, ser.version); + let de = VersionedResponse::try_from(ser).unwrap(); + assert_eq!(resp, de); + } +} diff --git a/common/wireguard-private-metadata/shared/src/models/v1/topup_bandwidth/mod.rs b/common/wireguard-private-metadata/shared/src/models/v1/topup_bandwidth/mod.rs new file mode 100644 index 00000000000..10698cca2d2 --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v1/topup_bandwidth/mod.rs @@ -0,0 +1,5 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod request; +pub mod response; diff --git a/common/wireguard-private-metadata/shared/src/models/v1/topup_bandwidth/request.rs b/common/wireguard-private-metadata/shared/src/models/v1/topup_bandwidth/request.rs new file mode 100644 index 00000000000..871cc127ef8 --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v1/topup_bandwidth/request.rs @@ -0,0 +1,92 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use bincode::Options; +use nym_credentials_interface::CredentialSpendingData; +use serde::{Deserialize, Serialize}; + +use crate::{make_bincode_serializer, models::Request}; + +use super::super::{Error, QueryType, VersionedRequest}; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct InnerTopUpRequest { + /// Ecash credential + pub credential: CredentialSpendingData, +} + +impl TryFrom for InnerTopUpRequest { + type Error = Error; + + fn try_from(value: VersionedRequest) -> Result { + match value.query_type { + QueryType::TopupBandwidth => Ok(make_bincode_serializer().deserialize(&value.inner)?), + QueryType::AvailableBandwidth => Err(Error::InvalidQueryType { + source_query_type: value.query_type.to_string(), + target_query_type: QueryType::TopupBandwidth.to_string(), + }), + } + } +} + +impl TryFrom for VersionedRequest { + type Error = Error; + + fn try_from(value: InnerTopUpRequest) -> Result { + Ok(Self { + query_type: QueryType::TopupBandwidth, + inner: make_bincode_serializer().serialize(&value)?, + }) + } +} + +impl TryFrom for InnerTopUpRequest { + type Error = crate::error::MetadataError; + + fn try_from(value: Request) -> Result { + VersionedRequest::try_from(value)? + .try_into() + .map_err(|err: Error| crate::error::MetadataError::Models { + message: err.to_string(), + }) + } +} + +impl TryFrom for Request { + type Error = crate::error::MetadataError; + + fn try_from(value: InnerTopUpRequest) -> Result { + VersionedRequest::try_from(value)? + .try_into() + .map_err(|err: Error| crate::error::MetadataError::Models { + message: err.to_string(), + }) + } +} + +#[cfg(test)] +mod tests { + use crate::models::tests::CREDENTIAL_BYTES; + + use super::*; + + #[test] + fn serde() { + let req = InnerTopUpRequest { + credential: CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap(), + }; + let ser = VersionedRequest::try_from(req.clone()).unwrap(); + assert_eq!(QueryType::TopupBandwidth, ser.query_type); + let de = InnerTopUpRequest::try_from(ser).unwrap(); + assert_eq!(req, de); + } + + #[test] + fn invalid_content() { + let future_req = VersionedRequest { + query_type: QueryType::TopupBandwidth, + inner: vec![], + }; + assert!(InnerTopUpRequest::try_from(future_req).is_err()); + } +} diff --git a/common/wireguard-private-metadata/shared/src/models/v1/topup_bandwidth/response.rs b/common/wireguard-private-metadata/shared/src/models/v1/topup_bandwidth/response.rs new file mode 100644 index 00000000000..08e0ef111f4 --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v1/topup_bandwidth/response.rs @@ -0,0 +1,88 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use bincode::Options; +use serde::{Deserialize, Serialize}; + +use crate::{make_bincode_serializer, models::Response}; + +use super::super::{Error, QueryType, VersionedResponse}; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct InnerTopUpResponse { + pub available_bandwidth: i64, +} + +impl TryFrom for InnerTopUpResponse { + type Error = Error; + + fn try_from(value: VersionedResponse) -> Result { + match value.query_type { + QueryType::TopupBandwidth => Ok(make_bincode_serializer().deserialize(&value.inner)?), + QueryType::AvailableBandwidth => Err(Error::InvalidQueryType { + source_query_type: value.query_type.to_string(), + target_query_type: QueryType::TopupBandwidth.to_string(), + }), + } + } +} + +impl TryFrom for VersionedResponse { + type Error = Error; + + fn try_from(value: InnerTopUpResponse) -> Result { + Ok(Self { + query_type: QueryType::TopupBandwidth, + inner: make_bincode_serializer().serialize(&value)?, + }) + } +} + +impl TryFrom for InnerTopUpResponse { + type Error = crate::error::MetadataError; + + fn try_from(value: Response) -> Result { + VersionedResponse::try_from(value)? + .try_into() + .map_err(|err: Error| crate::error::MetadataError::Models { + message: err.to_string(), + }) + } +} + +impl TryFrom for Response { + type Error = crate::error::MetadataError; + + fn try_from(value: InnerTopUpResponse) -> Result { + VersionedResponse::try_from(value)? + .try_into() + .map_err(|err: Error| crate::error::MetadataError::Models { + message: err.to_string(), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn serde() { + let resp = InnerTopUpResponse { + available_bandwidth: 42, + }; + let ser = VersionedResponse::try_from(resp.clone()).unwrap(); + assert_eq!(QueryType::TopupBandwidth, ser.query_type); + let de = InnerTopUpResponse::try_from(ser).unwrap(); + assert_eq!(resp, de); + } + + #[test] + fn invalid_content() { + let future_resp = VersionedResponse { + query_type: QueryType::TopupBandwidth, + inner: vec![], + }; + assert!(InnerTopUpResponse::try_from(future_resp).is_err()); + } +} diff --git a/common/wireguard-private-metadata/shared/src/routes.rs b/common/wireguard-private-metadata/shared/src/routes.rs new file mode 100644 index 00000000000..bda615fe1c8 --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/routes.rs @@ -0,0 +1,10 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub const V1_API_VERSION: &str = "v1"; + +pub const BANDWIDTH: &str = "bandwidth"; + +pub const VERSION: &str = "version"; +pub const AVAILABLE: &str = "available"; +pub const TOPUP: &str = "topup"; diff --git a/common/wireguard-private-metadata/src/lib.rs b/common/wireguard-private-metadata/src/lib.rs deleted file mode 100644 index 197986c8366..00000000000 --- a/common/wireguard-private-metadata/src/lib.rs +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright 2025 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -#[cfg(target_os = "linux")] -mod error; -#[cfg(target_os = "linux")] -mod http; -#[cfg(target_os = "linux")] -mod models; -#[cfg(target_os = "linux")] -mod network; -#[cfg(target_os = "linux")] -mod transceiver; - -#[cfg(target_os = "linux")] -pub use http::{ - router::{ApiHttpServer, RouterBuilder, RouterWithState}, - state::AppState, - ShutdownHandles, -}; -#[cfg(target_os = "linux")] -pub use transceiver::PeerControllerTransceiver; - -#[cfg(target_os = "linux")] -fn make_bincode_serializer() -> impl bincode::Options { - use bincode::Options; - bincode::DefaultOptions::new() - .with_big_endian() - .with_varint_encoding() -} diff --git a/common/wireguard-private-metadata/src/models/error.rs b/common/wireguard-private-metadata/src/models/error.rs deleted file mode 100644 index 673650d322b..00000000000 --- a/common/wireguard-private-metadata/src/models/error.rs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright 2025 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::models::Version; - -#[derive(Debug, thiserror::Error)] -pub enum Error { - #[error(transparent)] - Bincode(#[from] bincode::Error), - - #[error("trying to deserialize from version {source_version} into {target_version}")] - InvalidVersion { - source_version: Version, - target_version: Version, - }, -} diff --git a/common/wireguard-private-metadata/src/models/mod.rs b/common/wireguard-private-metadata/src/models/mod.rs deleted file mode 100644 index 18ef3180e05..00000000000 --- a/common/wireguard-private-metadata/src/models/mod.rs +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright 2025 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use std::fmt::Display; - -use axum::http::StatusCode; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use utoipa::ToSchema; - -pub(crate) mod error; -pub(crate) mod version_1; - -pub(crate) use version_1 as latest; - -pub type Version = usize; - -#[derive(Clone, Serialize, Deserialize, ToSchema)] -pub struct AvailableBandwidthResponse { - pub(crate) version: Version, - pub(crate) inner: Vec, -} - -#[derive(Clone, Serialize, Deserialize, JsonSchema, ToSchema)] -pub struct TopUpRequest { - pub(crate) version: Version, - pub(crate) inner: Vec, -} - -pub(crate) type AxumResult = Result; - -pub(crate) struct AxumErrorResponse { - message: String, - status: StatusCode, -} - -impl AxumErrorResponse { - pub(crate) fn bad_request(msg: impl Display) -> Self { - Self { - message: msg.to_string(), - status: StatusCode::BAD_REQUEST, - } - } -} - -impl axum::response::IntoResponse for AxumErrorResponse { - fn into_response(self) -> axum::response::Response { - (self.status, self.message).into_response() - } -} diff --git a/common/wireguard-private-metadata/src/models/version_1.rs b/common/wireguard-private-metadata/src/models/version_1.rs deleted file mode 100644 index 1c7d6768f9e..00000000000 --- a/common/wireguard-private-metadata/src/models/version_1.rs +++ /dev/null @@ -1,163 +0,0 @@ -// Copyright 2025 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use bincode::Options; -use serde::{Deserialize, Serialize}; - -use super::error::Error; -use crate::{ - make_bincode_serializer, - models::{AvailableBandwidthResponse, TopUpRequest}, -}; - -use nym_credentials_interface::CredentialSpendingData; - -pub const VERSION: usize = 1; - -#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct InnerAvailableBandwidthResponse { - pub(crate) value: i64, -} - -impl InnerAvailableBandwidthResponse { - pub(crate) fn new(value: i64) -> Self { - Self { value } - } -} - -impl TryFrom for AvailableBandwidthResponse { - type Error = Error; - - fn try_from(value: InnerAvailableBandwidthResponse) -> Result { - Ok(AvailableBandwidthResponse { - version: VERSION, - inner: make_bincode_serializer().serialize(&value)?, - }) - } -} - -impl TryFrom for InnerAvailableBandwidthResponse { - type Error = Error; - - fn try_from(value: AvailableBandwidthResponse) -> Result { - if value.version != VERSION { - return Err(Error::InvalidVersion { - source_version: value.version, - target_version: VERSION, - }); - } - Ok(make_bincode_serializer().deserialize(&value.inner)?) - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] -pub struct InnerTopUpRequest { - /// Ecash credential - pub credential: CredentialSpendingData, -} - -impl TryFrom for TopUpRequest { - type Error = Error; - - fn try_from(value: InnerTopUpRequest) -> Result { - Ok(TopUpRequest { - version: VERSION, - inner: make_bincode_serializer().serialize(&value)?, - }) - } -} - -impl TryFrom for InnerTopUpRequest { - type Error = Error; - - fn try_from(value: TopUpRequest) -> Result { - if value.version != VERSION { - return Err(Error::InvalidVersion { - source_version: value.version, - target_version: VERSION, - }); - } - Ok(make_bincode_serializer().deserialize(&value.inner)?) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn serde_available_bandwidth() { - let bw = InnerAvailableBandwidthResponse::new(42); - let ser = AvailableBandwidthResponse::try_from(bw).unwrap(); - assert_eq!(VERSION, ser.version); - assert_eq!(ser.inner, vec![84]); - let de = InnerAvailableBandwidthResponse::try_from(ser).unwrap(); - assert_eq!(bw, de); - } - - #[test] - fn mismatched_version_available_bandwidth() { - let version = 4242; - let future_bw = AvailableBandwidthResponse { - version, - inner: vec![], - }; - if let Err(Error::InvalidVersion { - source_version, - target_version, - }) = InnerAvailableBandwidthResponse::try_from(future_bw) - { - assert_eq!(source_version, version); - assert_eq!(target_version, VERSION); - } else { - panic!("failed"); - }; - } - - #[test] - fn invalid_content_available_bandwidth() { - let future_bw = AvailableBandwidthResponse { - version: VERSION, - inner: vec![], - }; - assert!(InnerAvailableBandwidthResponse::try_from(future_bw).is_err()); - } - - #[test] - fn serde_topup() { - let bw = InnerAvailableBandwidthResponse::new(42); - let ser = AvailableBandwidthResponse::try_from(bw).unwrap(); - assert_eq!(VERSION, ser.version); - assert_eq!(ser.inner, vec![84]); - let de = InnerAvailableBandwidthResponse::try_from(ser).unwrap(); - assert_eq!(bw, de); - } - - #[test] - fn mismatched_version_topup() { - let version = 4242; - let future_bw = AvailableBandwidthResponse { - version, - inner: vec![], - }; - if let Err(Error::InvalidVersion { - source_version, - target_version, - }) = InnerAvailableBandwidthResponse::try_from(future_bw) - { - assert_eq!(source_version, version); - assert_eq!(target_version, VERSION); - } else { - panic!("failed"); - }; - } - - #[test] - fn invalid_content_topup() { - let future_bw = AvailableBandwidthResponse { - version: VERSION, - inner: vec![], - }; - assert!(InnerAvailableBandwidthResponse::try_from(future_bw).is_err()); - } -} diff --git a/common/wireguard-private-metadata/src/network.rs b/common/wireguard-private-metadata/src/network.rs deleted file mode 100644 index 6f751bed057..00000000000 --- a/common/wireguard-private-metadata/src/network.rs +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright 2025 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use std::net::SocketAddr; - -use axum::{ - extract::{ConnectInfo, Query, State}, - Json, Router, -}; -use nym_http_api_common::{FormattedResponse, OutputParams}; -use tower_http::compression::CompressionLayer; - -use crate::{ - http::state::AppState, - models::{ - latest::InnerTopUpRequest, AvailableBandwidthResponse, AxumErrorResponse, AxumResult, - TopUpRequest, - }, -}; - -pub(crate) fn bandwidth_routes() -> Router { - Router::new() - .route("/available", axum::routing::get(available_bandwidth)) - .route("/topup", axum::routing::post(topup_bandwidth)) - .layer(CompressionLayer::new()) -} - -#[utoipa::path( - tag = "bandwidth", - get, - path = "/v1/bandwidth/available", - responses( - (status = 200, content( - (AvailableBandwidthResponse = "application/bincode") - )) - - ), -)] -async fn available_bandwidth( - ConnectInfo(addr): ConnectInfo, - Query(output): Query, - State(state): State, -) -> AxumResult> { - let output = output.output.unwrap_or_default(); - Ok(output.to_response( - state - .available_bandwidth(addr.ip()) - .await - .map_err(AxumErrorResponse::bad_request)?, - )) -} - -#[utoipa::path( - tag = "bandwidth", - post, - request_body = TopUpRequest, - path = "/v1/bandwidth/topup", - responses( - (status = 200), - ), -)] -async fn topup_bandwidth( - ConnectInfo(addr): ConnectInfo, - Query(output): Query, - State(state): State, - Json(request): Json, -) -> AxumResult> { - let output = output.output.unwrap_or_default(); - let credential = InnerTopUpRequest::try_from(request) - .map_err(AxumErrorResponse::bad_request)? - .credential; - state - .topup_bandwidth(addr.ip(), credential) - .await - .map_err(AxumErrorResponse::bad_request)?; - Ok(output.to_response(())) -} diff --git a/common/wireguard-private-metadata/tests/Cargo.toml b/common/wireguard-private-metadata/tests/Cargo.toml new file mode 100644 index 00000000000..6827c16f602 --- /dev/null +++ b/common/wireguard-private-metadata/tests/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "nym-wireguard-private-metadata-tests" +version = "1.0.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +async-trait = { workspace = true } +axum = { workspace = true, features = ["tokio", "macros"] } +nym-credential-verification = { path = "../../credential-verification" } +nym-credentials-interface = { path = "../../credentials-interface" } +nym-http-api-client = { path = "../../http-api-client" } +nym-http-api-common = { path = "../../http-api-common", features = [ + "middleware", + "utoipa", + "output", +] } +nym-wireguard = { path = "../../wireguard" } +tokio = { workspace = true, features = ["rt-multi-thread", "net", "io-util"] } +tower-http = { workspace = true, features = [ + "cors", + "trace", + "compression-br", + "compression-deflate", + "compression-gzip", + "compression-zstd", +] } +utoipa = { workspace = true, features = ["axum_extras", "time"] } + +nym-wireguard-private-metadata-client = { path = "../client" } +nym-wireguard-private-metadata-shared = { path = "../shared", features = [ + "testing", +] } +nym-wireguard-private-metadata-server = { path = "../server" } + +[lints] +workspace = true diff --git a/common/wireguard-private-metadata/tests/src/lib.rs b/common/wireguard-private-metadata/tests/src/lib.rs new file mode 100644 index 00000000000..c1a981e484f --- /dev/null +++ b/common/wireguard-private-metadata/tests/src/lib.rs @@ -0,0 +1,217 @@ +#[cfg(test)] +mod v0; + +#[cfg(test)] +mod tests { + use std::net::SocketAddr; + + use nym_credential_verification::{ClientBandwidth, TicketVerifier}; + use nym_credentials_interface::CredentialSpendingData; + use nym_http_api_client::Client; + use nym_wireguard::{peer_controller::PeerControlRequest, CONTROL_CHANNEL_SIZE}; + use nym_wireguard_private_metadata_client::WireguardMetadataApiClient; + use nym_wireguard_private_metadata_server::{ + AppState, PeerControllerTransceiver, RouterBuilder, + }; + use nym_wireguard_private_metadata_shared::{latest, v0, v1, ErrorResponse}; + use tokio::{net::TcpListener, sync::mpsc}; + + pub(crate) const VERIFIER_AVAILABLE_BANDWIDTH: i64 = 42; + pub(crate) const CREDENTIAL_BYTES: [u8; 1245] = [ + 0, 0, 4, 133, 96, 179, 223, 185, 136, 23, 213, 166, 59, 203, 66, 69, 209, 181, 227, 254, + 16, 102, 98, 237, 59, 119, 170, 111, 31, 194, 51, 59, 120, 17, 115, 229, 79, 91, 11, 139, + 154, 2, 212, 23, 68, 70, 167, 3, 240, 54, 224, 171, 221, 1, 69, 48, 60, 118, 119, 249, 123, + 35, 172, 227, 131, 96, 232, 209, 187, 123, 4, 197, 102, 90, 96, 45, 125, 135, 140, 99, 1, + 151, 17, 131, 143, 157, 97, 107, 139, 232, 212, 87, 14, 115, 253, 255, 166, 167, 186, 43, + 90, 96, 173, 105, 120, 40, 10, 163, 250, 224, 214, 200, 178, 4, 160, 16, 130, 59, 76, 193, + 39, 240, 3, 101, 141, 209, 183, 226, 186, 207, 56, 210, 187, 7, 164, 240, 164, 205, 37, 81, + 184, 214, 193, 195, 90, 205, 238, 225, 195, 104, 12, 123, 203, 57, 233, 243, 215, 145, 195, + 196, 57, 38, 125, 172, 18, 47, 63, 165, 110, 219, 180, 40, 58, 116, 92, 254, 160, 98, 48, + 92, 254, 232, 107, 184, 80, 234, 60, 160, 235, 249, 76, 41, 38, 165, 28, 40, 136, 74, 48, + 166, 50, 245, 23, 201, 140, 101, 79, 93, 235, 128, 186, 146, 126, 180, 134, 43, 13, 186, + 19, 195, 48, 168, 201, 29, 216, 95, 176, 198, 132, 188, 64, 39, 212, 150, 32, 52, 53, 38, + 228, 199, 122, 226, 217, 75, 40, 191, 151, 48, 164, 242, 177, 79, 14, 122, 105, 151, 85, + 88, 199, 162, 17, 96, 103, 83, 178, 128, 9, 24, 30, 74, 108, 241, 85, 240, 166, 97, 241, + 85, 199, 11, 198, 226, 234, 70, 107, 145, 28, 208, 114, 51, 12, 234, 108, 101, 202, 112, + 48, 185, 22, 159, 67, 109, 49, 27, 149, 90, 109, 32, 226, 112, 7, 201, 208, 209, 104, 31, + 97, 134, 204, 145, 27, 181, 206, 181, 106, 32, 110, 136, 115, 249, 201, 111, 5, 245, 203, + 71, 121, 169, 126, 151, 178, 236, 59, 221, 195, 48, 135, 115, 6, 50, 227, 74, 97, 107, 107, + 213, 90, 2, 203, 154, 138, 47, 128, 52, 134, 128, 224, 51, 65, 240, 90, 8, 55, 175, 180, + 178, 204, 206, 168, 110, 51, 57, 189, 169, 48, 169, 136, 121, 99, 51, 170, 178, 214, 74, 1, + 96, 151, 167, 25, 173, 180, 171, 155, 10, 55, 142, 234, 190, 113, 90, 79, 80, 244, 71, 166, + 30, 235, 113, 150, 133, 1, 218, 17, 109, 111, 223, 24, 216, 177, 41, 2, 204, 65, 221, 212, + 207, 236, 144, 6, 65, 224, 55, 42, 1, 1, 161, 134, 118, 127, 111, 220, 110, 127, 240, 71, + 223, 129, 12, 93, 20, 220, 60, 56, 71, 146, 184, 95, 132, 69, 28, 56, 53, 192, 213, 22, + 119, 230, 152, 225, 182, 188, 163, 219, 37, 175, 247, 73, 14, 247, 38, 72, 243, 1, 48, 131, + 59, 8, 13, 96, 143, 185, 127, 241, 161, 217, 24, 149, 193, 40, 16, 30, 202, 151, 28, 119, + 240, 153, 101, 156, 61, 193, 72, 245, 199, 181, 12, 231, 65, 166, 67, 142, 121, 207, 202, + 58, 197, 113, 188, 248, 42, 124, 105, 48, 161, 241, 55, 209, 36, 194, 27, 63, 233, 144, + 189, 85, 117, 234, 9, 139, 46, 31, 206, 114, 95, 131, 29, 240, 13, 81, 142, 140, 133, 33, + 30, 41, 141, 37, 80, 217, 95, 221, 76, 115, 86, 201, 165, 51, 252, 9, 28, 209, 1, 48, 150, + 74, 248, 212, 187, 222, 66, 210, 3, 200, 19, 217, 171, 184, 42, 148, 53, 150, 57, 50, 6, + 227, 227, 62, 49, 42, 148, 148, 157, 82, 191, 58, 24, 34, 56, 98, 120, 89, 105, 176, 85, + 15, 253, 241, 41, 153, 195, 136, 1, 48, 142, 126, 213, 101, 223, 79, 133, 230, 105, 38, + 161, 149, 2, 21, 136, 150, 42, 72, 218, 85, 146, 63, 223, 58, 108, 186, 183, 248, 62, 20, + 47, 34, 113, 160, 177, 204, 181, 16, 24, 212, 224, 35, 84, 51, 168, 56, 136, 11, 1, 48, + 135, 242, 62, 149, 230, 178, 32, 224, 119, 26, 234, 163, 237, 224, 114, 95, 112, 140, 170, + 150, 96, 125, 136, 221, 180, 78, 18, 11, 12, 184, 2, 198, 217, 119, 43, 69, 4, 172, 109, + 55, 183, 40, 131, 172, 161, 88, 183, 101, 1, 48, 173, 216, 22, 73, 42, 255, 211, 93, 249, + 87, 159, 115, 61, 91, 55, 130, 17, 216, 60, 34, 122, 55, 8, 244, 244, 153, 151, 57, 5, 144, + 178, 55, 249, 64, 211, 168, 34, 148, 56, 89, 92, 203, 70, 124, 219, 152, 253, 165, 0, 32, + 203, 116, 63, 7, 240, 222, 82, 86, 11, 149, 167, 72, 224, 55, 190, 66, 201, 65, 168, 184, + 96, 47, 194, 241, 168, 124, 7, 74, 214, 250, 37, 76, 32, 218, 69, 122, 103, 215, 145, 169, + 24, 212, 229, 168, 106, 10, 144, 31, 13, 25, 178, 242, 250, 106, 159, 40, 48, 163, 165, 61, + 130, 57, 146, 4, 73, 32, 254, 233, 125, 135, 212, 29, 111, 4, 177, 114, 15, 210, 170, 82, + 108, 110, 62, 166, 81, 209, 106, 176, 156, 14, 133, 242, 60, 127, 120, 242, 28, 97, 0, 1, + 32, 103, 93, 109, 89, 240, 91, 1, 84, 150, 50, 206, 157, 203, 49, 220, 120, 234, 175, 234, + 150, 126, 225, 94, 163, 164, 199, 138, 114, 62, 99, 106, 112, 1, 32, 171, 40, 220, 82, 241, + 203, 76, 146, 111, 139, 182, 179, 237, 182, 115, 75, 128, 201, 107, 43, 214, 0, 135, 217, + 160, 68, 150, 232, 144, 114, 237, 98, 32, 30, 134, 232, 59, 93, 163, 253, 244, 13, 202, 52, + 147, 168, 83, 121, 123, 95, 21, 210, 209, 225, 223, 143, 49, 10, 205, 238, 1, 22, 83, 81, + 70, 1, 32, 26, 76, 6, 234, 160, 50, 139, 102, 161, 232, 155, 106, 130, 171, 226, 210, 233, + 178, 85, 247, 71, 123, 55, 53, 46, 67, 148, 137, 156, 207, 208, 107, 1, 32, 102, 31, 4, 98, + 110, 156, 144, 61, 229, 140, 198, 84, 196, 238, 128, 35, 131, 182, 137, 125, 241, 95, 69, + 131, 170, 27, 2, 144, 75, 72, 242, 102, 3, 32, 121, 80, 45, 173, 56, 65, 218, 27, 40, 251, + 197, 32, 169, 104, 123, 110, 90, 78, 153, 166, 38, 9, 129, 228, 99, 8, 1, 116, 142, 233, + 162, 69, 32, 216, 169, 159, 116, 95, 12, 63, 176, 195, 6, 183, 123, 135, 75, 61, 112, 106, + 83, 235, 176, 41, 27, 248, 48, 71, 165, 170, 12, 92, 103, 103, 81, 32, 58, 74, 75, 145, + 192, 94, 153, 69, 80, 128, 241, 3, 16, 117, 192, 86, 161, 103, 44, 174, 211, 196, 182, 124, + 55, 11, 107, 142, 49, 88, 6, 41, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 0, 37, 139, 240, 0, 0, + 0, 0, 0, 0, 0, 1, + ]; + + pub(crate) struct MockVerifier { + ret: i64, + } + + impl MockVerifier { + pub(crate) fn new(ret: i64) -> MockVerifier { + Self { ret } + } + } + + #[async_trait::async_trait] + impl TicketVerifier for MockVerifier { + async fn verify(&mut self) -> nym_credential_verification::Result { + Ok(self.ret) + } + } + + pub(crate) async fn spawn_server_and_create_client() -> Client { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap(); + let (request_tx, mut request_rx) = mpsc::channel(CONTROL_CHANNEL_SIZE); + let router = RouterBuilder::with_default_routes() + .with_state(AppState::new(PeerControllerTransceiver::new(request_tx))) + .router; + + tokio::spawn(async move { + loop { + match request_rx.recv().await { + Some(PeerControlRequest::GetClientBandwidthByIp { ip: _, response_tx }) => { + response_tx + .send(Ok(ClientBandwidth::new(Default::default()))) + .ok(); + } + Some(PeerControlRequest::GetVerifierByIp { + ip: _, + credential: _, + response_tx, + }) => { + response_tx + .send(Ok(Box::new(MockVerifier::new( + VERIFIER_AVAILABLE_BANDWIDTH, + )))) + .ok(); + } + None => break, + _ => panic!("Not expected"), + } + } + }); + + tokio::spawn(async move { + axum::serve( + listener, + router.into_make_service_with_connect_info::(), + ) + .await + .unwrap(); + }); + Client::new_url::<_, ErrorResponse>(addr.to_string(), None).unwrap() + } + + #[tokio::test] + async fn query_latest_version() { + let client = spawn_server_and_create_client().await; + let version = client.version().await.unwrap(); + assert_eq!(version, latest::VERSION); + } + + #[tokio::test] + async fn query_against_server_v0() { + let client = super::v0::network::test::spawn_server_and_create_client().await; + + // version check + let version = client.version().await.unwrap(); + assert_eq!(version, v0::VERSION); + + // v0 reqwests + let request = v0::AvailableBandwidthRequest {}.try_into().unwrap(); + let response = client.available_bandwidth(&request).await.unwrap(); + v0::AvailableBandwidthResponse::try_from(response).unwrap(); + + let request = v0::TopUpRequest {}.try_into().unwrap(); + let response = client.topup_bandwidth(&request).await.unwrap(); + v0::TopUpResponse::try_from(response).unwrap(); + + // v1 reqwests + let request = v1::AvailableBandwidthRequest {}.try_into().unwrap(); + assert!(client.available_bandwidth(&request).await.is_err()); + + let request = v1::TopUpRequest { + credential: CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap(), + } + .try_into() + .unwrap(); + assert!(client.topup_bandwidth(&request).await.is_err()); + } + + #[tokio::test] + async fn query_against_server_v1() { + let client = spawn_server_and_create_client().await; + + // version check + let version = client.version().await.unwrap(); + assert_eq!(version, v1::VERSION); + + // v0 reqwests + let request = v0::AvailableBandwidthRequest {}.try_into().unwrap(); + let response = client.available_bandwidth(&request).await.unwrap(); + v0::AvailableBandwidthResponse::try_from(response).unwrap(); + + let request = v0::TopUpRequest {}.try_into().unwrap(); + assert!(client.topup_bandwidth(&request).await.is_err()); + + // v1 reqwests + let request = v1::AvailableBandwidthRequest {}.try_into().unwrap(); + let response = client.available_bandwidth(&request).await.unwrap(); + let available_bandwidth = v1::AvailableBandwidthResponse::try_from(response) + .unwrap() + .available_bandwidth; + assert_eq!(available_bandwidth, 0); + + let request = v1::TopUpRequest { + credential: CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap(), + } + .try_into() + .unwrap(); + let response = client.topup_bandwidth(&request).await.unwrap(); + let available_bandwidth = v1::TopUpResponse::try_from(response) + .unwrap() + .available_bandwidth; + assert_eq!(available_bandwidth, VERIFIER_AVAILABLE_BANDWIDTH); + } +} diff --git a/common/wireguard-private-metadata/tests/src/v0/interface.rs b/common/wireguard-private-metadata/tests/src/v0/interface.rs new file mode 100644 index 00000000000..77cd2d12fe4 --- /dev/null +++ b/common/wireguard-private-metadata/tests/src/v0/interface.rs @@ -0,0 +1,150 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_wireguard_private_metadata_shared::{ + v0 as latest, Construct, Extract, Request, Response, Version, +}; + +pub enum RequestData { + AvailableBandwidth(()), + TopUpBandwidth(()), +} + +impl From for RequestData { + fn from(value: latest::interface::RequestData) -> Self { + match value { + latest::interface::RequestData::AvailableBandwidth(inner) => { + Self::AvailableBandwidth(inner) + } + latest::interface::RequestData::TopUpBandwidth(credential_spending_data) => { + Self::TopUpBandwidth(credential_spending_data) + } + } + } +} + +impl From for latest::interface::RequestData { + fn from(value: RequestData) -> Self { + match value { + RequestData::AvailableBandwidth(inner) => Self::AvailableBandwidth(inner), + RequestData::TopUpBandwidth(credential_spending_data) => { + Self::TopUpBandwidth(credential_spending_data) + } + } + } +} + +impl From for ResponseData { + fn from(value: latest::interface::ResponseData) -> Self { + match value { + latest::interface::ResponseData::AvailableBandwidth(inner) => { + Self::AvailableBandwidth(inner) + } + latest::interface::ResponseData::TopUpBandwidth(credential_spending_data) => { + Self::TopUpBandwidth(credential_spending_data) + } + } + } +} + +impl From for latest::interface::ResponseData { + fn from(value: ResponseData) -> Self { + match value { + ResponseData::AvailableBandwidth(inner) => Self::AvailableBandwidth(inner), + ResponseData::TopUpBandwidth(credential_spending_data) => { + Self::TopUpBandwidth(credential_spending_data) + } + } + } +} + +impl Construct for Request { + fn construct( + info: RequestData, + version: Version, + ) -> Result { + match version { + Version::V0 => { + let translate_info = latest::interface::RequestData::from(info); + let versioned_request = + latest::VersionedRequest::construct(translate_info, latest::VERSION)?; + Ok(versioned_request.try_into()?) + } + _ => Err( + nym_wireguard_private_metadata_shared::ModelError::DowngradeNotPossible { + from: version, + to: Version::V0, + }, + ), + } + } +} + +impl Extract for Request { + fn extract( + &self, + ) -> Result<(RequestData, Version), nym_wireguard_private_metadata_shared::ModelError> { + match self.version { + Version::V0 => { + let versioned_request = latest::VersionedRequest::try_from(self.clone())?; + let (request, version) = versioned_request.extract()?; + + Ok((request.into(), version)) + } + _ => Err( + nym_wireguard_private_metadata_shared::ModelError::UpdateNotPossible { + from: self.version, + to: Version::V0, + }, + ), + } + } +} + +pub enum ResponseData { + AvailableBandwidth(()), + TopUpBandwidth(()), +} + +impl Construct for Response { + fn construct( + info: ResponseData, + version: Version, + ) -> Result { + match version { + Version::V0 => { + let translate_response = latest::interface::ResponseData::from(info); + let versioned_response = + latest::VersionedResponse::construct(translate_response, version)?; + Ok(versioned_response.try_into()?) + } + _ => Err( + nym_wireguard_private_metadata_shared::ModelError::DowngradeNotPossible { + from: version, + to: Version::V0, + }, + ), + } + } +} + +impl Extract for Response { + fn extract( + &self, + ) -> Result<(ResponseData, Version), nym_wireguard_private_metadata_shared::ModelError> { + match self.version { + Version::V0 => { + let versioned_response = latest::VersionedResponse::try_from(self.clone())?; + let (response, version) = versioned_response.extract()?; + + Ok((response.into(), version)) + } + _ => Err( + nym_wireguard_private_metadata_shared::ModelError::UpdateNotPossible { + from: self.version, + to: Version::V0, + }, + ), + } + } +} diff --git a/common/wireguard-private-metadata/tests/src/v0/mod.rs b/common/wireguard-private-metadata/tests/src/v0/mod.rs new file mode 100644 index 00000000000..7338cc5ae23 --- /dev/null +++ b/common/wireguard-private-metadata/tests/src/v0/mod.rs @@ -0,0 +1,2 @@ +pub(crate) mod interface; +pub(crate) mod network; diff --git a/common/wireguard-private-metadata/tests/src/v0/network.rs b/common/wireguard-private-metadata/tests/src/v0/network.rs new file mode 100644 index 00000000000..6a847ecc368 --- /dev/null +++ b/common/wireguard-private-metadata/tests/src/v0/network.rs @@ -0,0 +1,146 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +#[cfg(test)] +pub(crate) mod test { + use std::net::SocketAddr; + + use crate::{ + tests::{MockVerifier, VERIFIER_AVAILABLE_BANDWIDTH}, + v0::interface::{RequestData, ResponseData}, + }; + use axum::{extract::Query, Json, Router}; + use nym_credential_verification::ClientBandwidth; + use nym_http_api_client::Client; + use nym_http_api_common::{FormattedResponse, OutputParams}; + use nym_wireguard::{peer_controller::PeerControlRequest, CONTROL_CHANNEL_SIZE}; + use nym_wireguard_private_metadata_server::PeerControllerTransceiver; + use nym_wireguard_private_metadata_shared::ErrorResponse; + use nym_wireguard_private_metadata_shared::{ + v0 as latest, AxumErrorResponse, AxumResult, Construct, Extract, Request, Response, + }; + use tokio::{net::TcpListener, sync::mpsc}; + use tower_http::compression::CompressionLayer; + + use nym_wireguard_private_metadata_server::AppState; + + fn bandwidth_routes() -> Router { + Router::new() + .route("/version", axum::routing::get(version)) + .route("/available", axum::routing::post(available_bandwidth)) + .route("/topup", axum::routing::post(topup_bandwidth)) + .layer(CompressionLayer::new()) + } + + #[utoipa::path( + tag = "bandwidth", + get, + path = "/v1/bandwidth/version", + responses( + (status = 200, content( + (Response = "application/bincode") + )) + ), +)] + async fn version(Query(output): Query) -> AxumResult> { + let output = output.output.unwrap_or_default(); + Ok(output.to_response(latest::VERSION.into())) + } + + #[utoipa::path( + tag = "bandwidth", + post, + request_body = Request, + path = "/v1/bandwidth/available", + responses( + (status = 200, content( + (Response = "application/bincode") + )) + ), +)] + async fn available_bandwidth( + Query(output): Query, + Json(request): Json, + ) -> AxumResult> { + let output = output.output.unwrap_or_default(); + + let (RequestData::AvailableBandwidth(_), version) = + request.extract().map_err(AxumErrorResponse::bad_request)? + else { + return Err(AxumErrorResponse::bad_request("incorrect request type")); + }; + let response = Response::construct(ResponseData::AvailableBandwidth(()), version) + .map_err(AxumErrorResponse::bad_request)?; + + Ok(output.to_response(response)) + } + + #[utoipa::path( + tag = "bandwidth", + post, + request_body = Request, + path = "/v1/bandwidth/topup", + responses( + (status = 200, content( + (Response = "application/bincode") + )) + ), +)] + async fn topup_bandwidth( + Query(output): Query, + Json(request): Json, + ) -> AxumResult> { + let output = output.output.unwrap_or_default(); + + let (RequestData::TopUpBandwidth(_), version) = + request.extract().map_err(AxumErrorResponse::bad_request)? + else { + return Err(AxumErrorResponse::bad_request("incorrect request type")); + }; + let response = Response::construct(ResponseData::TopUpBandwidth(()), version) + .map_err(AxumErrorResponse::bad_request)?; + + Ok(output.to_response(response)) + } + + pub(crate) async fn spawn_server_and_create_client() -> Client { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap(); + let (request_tx, mut request_rx) = mpsc::channel(CONTROL_CHANNEL_SIZE); + let router = Router::new() + .nest("/v1", Router::new().nest("/bandwidth", bandwidth_routes())) + .with_state(AppState::new(PeerControllerTransceiver::new(request_tx))); + + tokio::spawn(async move { + match request_rx.recv().await.unwrap() { + PeerControlRequest::GetClientBandwidthByIp { ip: _, response_tx } => { + response_tx + .send(Ok(ClientBandwidth::new(Default::default()))) + .ok(); + } + PeerControlRequest::GetVerifierByIp { + ip: _, + credential: _, + response_tx, + } => { + response_tx + .send(Ok(Box::new(MockVerifier::new( + VERIFIER_AVAILABLE_BANDWIDTH, + )))) + .ok(); + } + _ => panic!("Not expected"), + } + }); + + tokio::spawn(async move { + axum::serve( + listener, + router.into_make_service_with_connect_info::(), + ) + .await + .unwrap(); + }); + Client::new_url::<_, ErrorResponse>(addr.to_string(), None).unwrap() + } +} diff --git a/gateway/Cargo.toml b/gateway/Cargo.toml index c5db93110aa..7c93c020d74 100644 --- a/gateway/Cargo.toml +++ b/gateway/Cargo.toml @@ -72,7 +72,7 @@ nym-ip-packet-router = { path = "../service-providers/ip-packet-router" } nym-node-metrics = { path = "../nym-node/nym-node-metrics" } nym-wireguard = { path = "../common/wireguard" } -nym-wireguard-private-metadata = { path = "../common/wireguard-private-metadata" } +nym-wireguard-private-metadata-server = { path = "../common/wireguard-private-metadata/server" } nym-wireguard-types = { path = "../common/wireguard-types", default-features = false } nym-authenticator-requests = { path = "../common/authenticator-requests" } diff --git a/gateway/src/node/mod.rs b/gateway/src/node/mod.rs index 0fb36942209..a89a68cb239 100644 --- a/gateway/src/node/mod.rs +++ b/gateway/src/node/mod.rs @@ -484,7 +484,7 @@ impl GatewayTasksBuilder { pub async fn try_start_wireguard( &mut self, ) -> Result< - nym_wireguard_private_metadata::ShutdownHandles, + nym_wireguard_private_metadata_server::ShutdownHandles, Box, > { let all_peers = self.get_wireguard_peers().await?; @@ -501,9 +501,9 @@ impl GatewayTasksBuilder { ); }; - let router = nym_wireguard_private_metadata::RouterBuilder::with_default_routes(); - let router = router.with_state(nym_wireguard_private_metadata::AppState::new( - nym_wireguard_private_metadata::PeerControllerTransceiver::new( + let router = nym_wireguard_private_metadata_server::RouterBuilder::with_default_routes(); + let router = router.with_state(nym_wireguard_private_metadata_server::AppState::new( + nym_wireguard_private_metadata_server::PeerControllerTransceiver::new( wireguard_data.inner.peer_tx().clone(), ), )); @@ -512,8 +512,17 @@ impl GatewayTasksBuilder { wireguard_data.inner.config().private_ipv4.into(), wireguard_data.inner.config().announced_metadata_port, ); - let server = router.build_server(&bind_address).await?; + let wg_handle = nym_wireguard::start_wireguard( + ecash_manager, + self.metrics.clone(), + all_peers, + self.legacy_task_client.fork("wireguard"), + wireguard_data, + ) + .await?; + + let server = router.build_server(&bind_address).await?; let cancel_token: tokio_util::sync::CancellationToken = (*self.shutdown_token).clone(); let axum_shutdown_receiver = cancel_token.clone().cancelled_owned(); let server_handle = tokio::spawn(async move { @@ -523,15 +532,7 @@ impl GatewayTasksBuilder { } }); - let wg_handle = nym_wireguard::start_wireguard( - ecash_manager, - self.metrics.clone(), - all_peers, - self.legacy_task_client.fork("wireguard"), - wireguard_data, - ) - .await?; - let shutdown_handles = nym_wireguard_private_metadata::ShutdownHandles::new( + let shutdown_handles = nym_wireguard_private_metadata_server::ShutdownHandles::new( server_handle, wg_handle, cancel_token,