From e849e608b229a37ecfe33440ef1aea89dd971937 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Fri, 9 May 2025 11:11:22 +0200 Subject: [PATCH] feat: nym-api bincode + yaml support (#5745) * introduce 'Bincode' variant for FormattedResponse * allow nym-api to return responses in bincode (and also yaml) * client parsing support * cargo fmt * missing changes to nym-api tests * fixed node status api build + adjusted NymApiClient construction * NMv2 fixes + further api changes * feature-locking http-api-common to fix wasm build --- Cargo.lock | 4 +- .../client-libs/validator-client/Cargo.toml | 2 +- .../validator-client/src/client.rs | 39 ++- .../validator-client/src/nym_api/mod.rs | 28 +- common/http-api-client/Cargo.toml | 6 +- common/http-api-client/src/lib.rs | 118 ++++++-- common/http-api-common/Cargo.toml | 41 ++- common/http-api-common/src/lib.rs | 99 +------ common/http-api-common/src/response.rs | 145 ++++++++++ nym-api/Cargo.toml | 2 +- .../src/circulating_supply_api/handlers.rs | 54 +++- nym-api/src/ecash/api_routes/aggregation.rs | 62 +++-- nym-api/src/ecash/api_routes/helpers.rs | 3 + nym-api/src/ecash/api_routes/issued.rs | 88 ++++-- .../src/ecash/api_routes/partial_signing.rs | 56 ++-- nym-api/src/ecash/api_routes/spending.rs | 60 ++-- nym-api/src/network/handlers.rs | 117 +++++--- nym-api/src/node_status_api/handlers/mod.rs | 19 +- .../handlers/network_monitor.rs | 231 ++++++++++----- .../src/node_status_api/handlers/unstable.rs | 60 ++-- .../handlers/without_monitor.rs | 120 +++++--- nym-api/src/nym_contract_cache/handlers.rs | 262 +++++++++++++----- nym-api/src/nym_nodes/handlers/legacy.rs | 39 ++- nym-api/src/nym_nodes/handlers/mod.rs | 122 +++++--- .../nym_nodes/handlers/unstable/full_fat.rs | 4 +- .../src/nym_nodes/handlers/unstable/mod.rs | 24 +- .../handlers/unstable/semi_skimmed.rs | 4 +- .../nym_nodes/handlers/unstable/skimmed.rs | 134 ++++++--- nym-api/src/status/handlers.rs | 54 +++- nym-api/src/support/http/helpers.rs | 5 +- .../nym-credential-proxy-requests/Cargo.toml | 2 +- .../nym-node-status-api/src/monitor/mod.rs | 4 +- .../src/node_scraper/mod.rs | 2 +- nym-node/src/node/mod.rs | 2 +- nym-node/src/node/shared_network.rs | 2 +- nym-wallet/Cargo.lock | 11 + 36 files changed, 1432 insertions(+), 593 deletions(-) create mode 100644 common/http-api-common/src/response.rs diff --git a/Cargo.lock b/Cargo.lock index b788cb1875..c8db184347 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5786,12 +5786,14 @@ name = "nym-http-api-client" version = "0.1.0" dependencies = [ "async-trait", + "bincode", "bytes", "encoding_rs", "hickory-resolver", "http 1.3.1", "mime", "nym-bin-common", + "nym-http-api-common", "once_cell", "reqwest 0.12.15", "serde", @@ -5809,12 +5811,12 @@ version = "0.1.0" dependencies = [ "axum 0.7.9", "axum-client-ip", + "bincode", "bytes", "colored", "futures", "mime", "serde", - "serde_json", "serde_yaml", "subtle 2.6.1", "tower 0.5.2", diff --git a/common/client-libs/validator-client/Cargo.toml b/common/client-libs/validator-client/Cargo.toml index 7ff310b039..3bab78f9ad 100644 --- a/common/client-libs/validator-client/Cargo.toml +++ b/common/client-libs/validator-client/Cargo.toml @@ -22,7 +22,7 @@ nym-group-contract-common = { path = "../../cosmwasm-smart-contracts/group-contr nym-serde-helpers = { path = "../../serde-helpers", features = ["hex", "base64"] } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } -nym-http-api-client = { path = "../../../common/http-api-client" } +nym-http-api-client = { path = "../../../common/http-api-client", features = ["bincode"] } thiserror = { workspace = true } tracing = { workspace = true } url = { workspace = true, features = ["serde"] } diff --git a/common/client-libs/validator-client/src/client.rs b/common/client-libs/validator-client/src/client.rs index 4cb7aaa3b6..7ac336266b 100644 --- a/common/client-libs/validator-client/src/client.rs +++ b/common/client-libs/validator-client/src/client.rs @@ -345,25 +345,47 @@ impl Client { #[derive(Clone)] pub struct NymApiClient { + pub use_bincode: bool, pub nym_api: nym_api::Client, // TODO: perhaps if we really need it at some (currently I don't see any reasons for it) // we could re-implement the communication with the REST API on port 1317 } +impl From for NymApiClient { + fn from(nym_api: nym_api::Client) -> Self { + NymApiClient { + use_bincode: false, + nym_api, + } + } +} + // we have to allow the use of deprecated method here as they're calling the deprecated trait methods #[allow(deprecated)] impl NymApiClient { pub fn new(api_url: Url) -> Self { let nym_api = nym_api::Client::new(api_url, None); - NymApiClient { nym_api } + NymApiClient { + use_bincode: false, + nym_api, + } } #[cfg(not(target_arch = "wasm32"))] pub fn new_with_timeout(api_url: Url, timeout: std::time::Duration) -> Self { let nym_api = nym_api::Client::new(api_url, Some(timeout)); - NymApiClient { nym_api } + NymApiClient { + use_bincode: false, + nym_api, + } + } + + #[must_use] + pub fn with_bincode(mut self, use_bincode: bool) -> Self { + self.use_bincode = use_bincode; + self } pub fn new_with_user_agent(api_url: Url, user_agent: impl Into) -> Self { @@ -373,7 +395,10 @@ impl NymApiClient { .build::() .expect("failed to build nym api client"); - NymApiClient { nym_api } + NymApiClient { + use_bincode: false, + nym_api, + } } pub fn api_url(&self) -> &Url { @@ -410,7 +435,7 @@ impl NymApiClient { loop { let mut res = self .nym_api - .get_basic_entry_assigned_nodes(false, Some(page), None) + .get_basic_entry_assigned_nodes(false, Some(page), None, self.use_bincode) .await?; nodes.append(&mut res.nodes.data); @@ -436,7 +461,7 @@ impl NymApiClient { loop { let mut res = self .nym_api - .get_basic_active_mixing_assigned_nodes(false, Some(page), None) + .get_basic_active_mixing_assigned_nodes(false, Some(page), None, self.use_bincode) .await?; nodes.append(&mut res.nodes.data); @@ -462,7 +487,7 @@ impl NymApiClient { loop { let mut res = self .nym_api - .get_basic_mixing_capable_nodes(false, Some(page), None) + .get_basic_mixing_capable_nodes(false, Some(page), None, self.use_bincode) .await?; nodes.append(&mut res.nodes.data); @@ -485,7 +510,7 @@ impl NymApiClient { loop { let mut res = self .nym_api - .get_basic_nodes(false, Some(page), None) + .get_basic_nodes(false, Some(page), None, self.use_bincode) .await?; nodes.append(&mut res.nodes.data); diff --git a/common/client-libs/validator-client/src/nym_api/mod.rs b/common/client-libs/validator-client/src/nym_api/mod.rs index fa916c9d12..6922e53dd4 100644 --- a/common/client-libs/validator-client/src/nym_api/mod.rs +++ b/common/client-libs/validator-client/src/nym_api/mod.rs @@ -318,6 +318,7 @@ pub trait NymApiClientExt: ApiClient { no_legacy: bool, page: Option, per_page: Option, + use_bincode: bool, ) -> Result, NymAPIError> { let mut params = Vec::new(); @@ -333,7 +334,11 @@ pub trait NymApiClientExt: ApiClient { params.push(("per_page", per_page.to_string())) } - self.get_json( + if use_bincode { + params.push(("output", "bincode".to_string())) + } + + self.get_response( &[ routes::API_VERSION, "unstable", @@ -355,6 +360,7 @@ pub trait NymApiClientExt: ApiClient { no_legacy: bool, page: Option, per_page: Option, + use_bincode: bool, ) -> Result, NymAPIError> { let mut params = Vec::new(); @@ -370,7 +376,11 @@ pub trait NymApiClientExt: ApiClient { params.push(("per_page", per_page.to_string())) } - self.get_json( + if use_bincode { + params.push(("output", "bincode".to_string())) + } + + self.get_response( &[ routes::API_VERSION, "unstable", @@ -392,6 +402,7 @@ pub trait NymApiClientExt: ApiClient { no_legacy: bool, page: Option, per_page: Option, + use_bincode: bool, ) -> Result, NymAPIError> { let mut params = Vec::new(); @@ -407,7 +418,11 @@ pub trait NymApiClientExt: ApiClient { params.push(("per_page", per_page.to_string())) } - self.get_json( + if use_bincode { + params.push(("output", "bincode".to_string())) + } + + self.get_response( &[ routes::API_VERSION, "unstable", @@ -427,6 +442,7 @@ pub trait NymApiClientExt: ApiClient { no_legacy: bool, page: Option, per_page: Option, + use_bincode: bool, ) -> Result, NymAPIError> { let mut params = Vec::new(); @@ -442,7 +458,11 @@ pub trait NymApiClientExt: ApiClient { params.push(("per_page", per_page.to_string())) } - self.get_json( + if use_bincode { + params.push(("output", "bincode".to_string())) + } + + self.get_response( &[ routes::API_VERSION, "unstable", diff --git a/common/http-api-client/Cargo.toml b/common/http-api-client/Cargo.toml index eea45af346..8adf106588 100644 --- a/common/http-api-client/Cargo.toml +++ b/common/http-api-client/Cargo.toml @@ -12,6 +12,7 @@ license.workspace = true [dependencies] async-trait = { workspace = true } +bincode = { workspace = true, optional = true } reqwest = { workspace = true, features = ["json", "gzip"] } http.workspace = true url = { workspace = true } @@ -26,7 +27,7 @@ bytes = { workspace = true } encoding_rs = { workspace = true } mime = { workspace = true } - +nym-http-api-common = { path = "../http-api-common", default-features = false, optional = true } nym-bin-common = { path = "../bin-common" } [target."cfg(not(target_arch = \"wasm32\"))".dependencies] @@ -39,3 +40,6 @@ features = ["tokio"] [dev-dependencies] tokio = { workspace = true, features = ["rt", "macros"] } + +[features] +bincode = ["dep:bincode", "nym-http-api-common", "nym-http-api-common/bincode"] diff --git a/common/http-api-client/src/lib.rs b/common/http-api-client/src/lib.rs index 5a360f7be1..34fba45ad1 100644 --- a/common/http-api-client/src/lib.rs +++ b/common/http-api-client/src/lib.rs @@ -144,10 +144,13 @@ use serde::{Deserialize, Serialize}; use std::fmt::Display; use std::time::Duration; use thiserror::Error; -use tracing::{instrument, warn}; +use tracing::{debug, instrument, warn}; use url::Url; +use bytes::Bytes; +use http::header::CONTENT_TYPE; use http::HeaderMap; +use mime::Mime; pub use reqwest::IntoUrl; #[cfg(not(target_arch = "wasm32"))] use std::net::SocketAddr; @@ -210,11 +213,8 @@ pub enum HttpClientError { #[error("failed to resolve request. status: '{status}', additional error message: {error}")] EndpointFailure { status: StatusCode, error: E }, - #[error("failed to decode response body: {source} from {content}")] - ResponseDecodeFailure { - source: serde_json::Error, - content: String, - }, + #[error("failed to decode response body: {message} from {content}")] + ResponseDecodeFailure { message: String, content: String }, #[cfg(target_arch = "wasm32")] #[error("the request has timed out")] @@ -698,11 +698,29 @@ pub trait ApiClient: ApiClientCore { /// defined key-value parameters, e.g. `[("since", "12345")]`. Attempt to parse the response /// into the provided type `T`. #[instrument(level = "debug", skip_all)] + // TODO: deprecate in favour of get_response that works based on mime type in the response async fn get_json( &self, path: PathSegments<'_>, params: Params<'_, K, V>, ) -> Result> + where + for<'a> T: Deserialize<'a>, + K: AsRef + Sync, + V: AsRef + Sync, + E: Display + DeserializeOwned, + { + self.get_response(path, params).await + } + + /// 'get' data from the segment-defined path, e.g. `["api", "v1", "mixnodes"]`, with tuple + /// defined key-value parameters, e.g. `[("since", "12345")]`. Attempt to parse the response + /// into the provided type `T` based on the content type header + async fn get_response( + &self, + path: PathSegments<'_>, + params: Params<'_, K, V>, + ) -> Result> where for<'a> T: Deserialize<'a>, K: AsRef + Sync, @@ -877,14 +895,10 @@ fn sanitize_url, V: AsRef>( url } -fn decode_as_text(bytes: &bytes::Bytes, headers: HeaderMap) -> String { +fn decode_as_text(bytes: &bytes::Bytes, headers: &HeaderMap) -> String { use encoding_rs::{Encoding, UTF_8}; - use mime::Mime; - let content_type = headers - .get(http::header::CONTENT_TYPE) - .and_then(|value| value.to_str().ok()) - .and_then(|value| value.parse::().ok()); + let content_type = try_get_mime_type(headers); let encoding_name = content_type .as_ref() @@ -897,7 +911,7 @@ fn decode_as_text(bytes: &bytes::Bytes, headers: HeaderMap) -> String { text.into_owned() } -/// Attempt to parse a json object from an HTTP response +/// Attempt to parse a response object from an HTTP response #[instrument(level = "debug", skip_all)] pub async fn parse_response(res: Response, allow_empty: bool) -> Result> where @@ -919,16 +933,7 @@ where // internally reqwest is first retrieving bytes and then performing parsing via serde_json // (and similarly does the same thing for text()) let full = res.bytes().await?; - match serde_json::from_slice(&full) { - Ok(data) => Ok(data), - Err(err) => { - let content = decode_as_text(&full, headers); - Err(HttpClientError::ResponseDecodeFailure { - source: err, - content, - }) - } - } + decode_raw_response(&headers, full) } else if res.status() == StatusCode::NOT_FOUND { Err(HttpClientError::NotFound) } else { @@ -947,6 +952,73 @@ where } } +fn decode_as_json(headers: &HeaderMap, content: Bytes) -> Result> +where + T: DeserializeOwned, + E: DeserializeOwned + Display, +{ + match serde_json::from_slice(&content) { + Ok(data) => Ok(data), + Err(err) => { + let content = decode_as_text(&content, headers); + Err(HttpClientError::ResponseDecodeFailure { + message: err.to_string(), + content, + }) + } + } +} + +#[cfg(feature = "bincode")] +fn decode_as_bincode(headers: &HeaderMap, content: Bytes) -> Result> +where + T: DeserializeOwned, + E: DeserializeOwned + Display, +{ + use bincode::Options; + + let opts = nym_http_api_common::make_bincode_serializer(); + match opts.deserialize(&content) { + Ok(data) => Ok(data), + Err(err) => { + let content = decode_as_text(&content, headers); + Err(HttpClientError::ResponseDecodeFailure { + message: err.to_string(), + content, + }) + } + } +} + +fn decode_raw_response(headers: &HeaderMap, content: Bytes) -> Result> +where + T: DeserializeOwned, + E: DeserializeOwned + Display, +{ + // if content type header is missing, fallback to our old default, json + let mime = try_get_mime_type(headers).unwrap_or(mime::APPLICATION_JSON); + + debug!("attempting to parse response as {mime}"); + + // unfortunately we can't use stronger typing for subtype as "bincode" is not a defined mime type + match (mime.type_(), mime.subtype().as_str()) { + (mime::APPLICATION, "json") => decode_as_json(headers, content), + #[cfg(feature = "bincode")] + (mime::APPLICATION, "bincode") => decode_as_bincode(headers, content), + (_, _) => { + debug!("unrecognised mime type {mime}. falling back to json decoding..."); + decode_as_json(headers, content) + } + } +} + +fn try_get_mime_type(headers: &HeaderMap) -> Option { + headers + .get(CONTENT_TYPE) + .and_then(|value| value.to_str().ok()) + .and_then(|value| value.parse::().ok()) +} + #[cfg(test)] mod tests { use super::*; diff --git a/common/http-api-common/Cargo.toml b/common/http-api-common/Cargo.toml index e8cf08b103..49ee4b73d5 100644 --- a/common/http-api-common/Cargo.toml +++ b/common/http-api-common/Cargo.toml @@ -11,20 +11,39 @@ license.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -axum-client-ip.workspace = true -axum.workspace = true -bytes = { workspace = true } -colored.workspace = true -futures = { workspace = true } -mime = { workspace = true } +axum = { workspace = true, optional = true } +axum-client-ip = { workspace = true, optional = true } +bincode = { workspace = true, optional = true } +bytes = { workspace = true, optional = true } +colored = { workspace = true, optional = true } +futures = { workspace = true, optional = true } +mime = { workspace = true, optional = true } serde = { workspace = true, features = ["derive"] } -serde_json.workspace = true -serde_yaml = { workspace = true } -subtle.workspace = true -tower = { workspace = true } +serde_yaml = { workspace = true, optional = true } +subtle = { workspace = true, optional = true } +tower = { workspace = true, optional = true } tracing.workspace = true utoipa = { workspace = true, optional = true } -zeroize = { workspace = true } +zeroize = { workspace = true, optional = true } [features] +default = [] +output = [ + "axum", + "bytes", + "mime", + "serde_yaml" +] + +middleware = [ + "axum", + "axum-client-ip", + "colored", + "futures", + "subtle", + "tower", + "zeroize" +] + utoipa = ["dep:utoipa"] +bincode = ["dep:bincode"] \ No newline at end of file diff --git a/common/http-api-common/src/lib.rs b/common/http-api-common/src/lib.rs index ffd258de95..32f79adb2b 100644 --- a/common/http-api-common/src/lib.rs +++ b/common/http-api-common/src/lib.rs @@ -1,92 +1,21 @@ -// Copyright 2023 - Nym Technologies SA +// Copyright 2023-2025 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use axum::http::{header, HeaderValue, StatusCode}; -use axum::response::{IntoResponse, Response}; -use axum::Json; -use bytes::{BufMut, BytesMut}; -use serde::{Deserialize, Serialize}; - +#[cfg(feature = "middleware")] pub mod middleware; -#[derive(Debug, Clone)] -pub enum FormattedResponse { - Json(Json), - Yaml(Yaml), -} +#[cfg(feature = "output")] +pub mod response; -impl IntoResponse for FormattedResponse -where - T: Serialize, -{ - fn into_response(self) -> Response { - match self { - FormattedResponse::Json(json_response) => json_response.into_response(), - FormattedResponse::Yaml(yaml_response) => yaml_response.into_response(), - } - } -} +// don't break existing imports +#[cfg(feature = "output")] +pub use response::*; -#[derive(Default, Debug, Serialize, Deserialize, Copy, Clone)] -#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] -#[serde(rename_all = "lowercase")] -pub enum Output { - #[default] - Json, - Yaml, -} - -#[derive(Default, Debug, Serialize, Deserialize, Copy, Clone)] -#[cfg_attr(feature = "utoipa", derive(utoipa::IntoParams, utoipa::ToSchema))] -#[serde(default)] -pub struct OutputParams { - pub output: Option, -} - -impl Output { - pub fn to_response(self, data: T) -> FormattedResponse { - match self { - Output::Json => FormattedResponse::Json(Json(data)), - Output::Yaml => FormattedResponse::Yaml(Yaml(data)), - } - } -} - -#[derive(Debug, Clone, Copy, Default)] -#[must_use] -pub struct Yaml(pub T); - -impl From for Yaml { - fn from(inner: T) -> Self { - Self(inner) - } -} - -impl IntoResponse for Yaml -where - T: Serialize, -{ - // replicates axum's Json - fn into_response(self) -> Response { - let mut buf = BytesMut::with_capacity(128).writer(); - match serde_yaml::to_writer(&mut buf, &self.0) { - Ok(()) => ( - [( - header::CONTENT_TYPE, - HeaderValue::from_static("application/yaml"), - )], - buf.into_inner().freeze(), - ) - .into_response(), - Err(err) => ( - StatusCode::INTERNAL_SERVER_ERROR, - [( - header::CONTENT_TYPE, - HeaderValue::from_static(mime::TEXT_PLAIN_UTF_8.as_ref()), - )], - err.to_string(), - ) - .into_response(), - } - } +// be explicit about those values because bincode uses different defaults in different places +#[cfg(feature = "bincode")] +pub fn make_bincode_serializer() -> impl bincode::Options { + use bincode::Options; + bincode::DefaultOptions::new() + .with_little_endian() + .with_varint_encoding() } diff --git a/common/http-api-common/src/response.rs b/common/http-api-common/src/response.rs new file mode 100644 index 0000000000..d5f3f736c6 --- /dev/null +++ b/common/http-api-common/src/response.rs @@ -0,0 +1,145 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use axum::http::{header, HeaderValue, StatusCode}; +use axum::response::{IntoResponse, Response}; +use axum::Json; +use bytes::{BufMut, BytesMut}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone)] +pub enum FormattedResponse { + Json(Json), + Yaml(Yaml), + #[cfg(feature = "bincode")] + Bincode(Bincode), +} +impl FormattedResponse { + pub fn into_inner(self) -> T { + match self { + FormattedResponse::Json(inner) => inner.0, + FormattedResponse::Yaml(inner) => inner.0, + #[cfg(feature = "bincode")] + FormattedResponse::Bincode(inner) => inner.0, + } + } +} + +impl IntoResponse for FormattedResponse +where + T: Serialize, +{ + fn into_response(self) -> Response { + match self { + FormattedResponse::Json(json_response) => json_response.into_response(), + FormattedResponse::Yaml(yaml_response) => yaml_response.into_response(), + #[cfg(feature = "bincode")] + FormattedResponse::Bincode(bincode_response) => bincode_response.into_response(), + } + } +} + +#[derive(Default, Debug, Serialize, Deserialize, Copy, Clone)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] +#[serde(rename_all = "lowercase")] +pub enum Output { + #[default] + Json, + Yaml, + #[cfg(feature = "bincode")] + Bincode, +} + +#[derive(Default, Debug, Serialize, Deserialize, Copy, Clone)] +#[cfg_attr(feature = "utoipa", derive(utoipa::IntoParams, utoipa::ToSchema))] +#[serde(default)] +pub struct OutputParams { + pub output: Option, +} + +impl Output { + pub fn to_response(self, data: T) -> FormattedResponse { + match self { + Output::Json => FormattedResponse::Json(Json(data)), + Output::Yaml => FormattedResponse::Yaml(Yaml(data)), + #[cfg(feature = "bincode")] + Output::Bincode => FormattedResponse::Bincode(Bincode(data)), + } + } +} + +#[derive(Debug, Clone, Copy, Default)] +#[must_use] +pub struct Yaml(pub T); + +impl From for Yaml { + fn from(inner: T) -> Self { + Self(inner) + } +} + +impl IntoResponse for Yaml +where + T: Serialize, +{ + // replicates axum's Json + fn into_response(self) -> Response { + let mut buf = BytesMut::with_capacity(128).writer(); + match serde_yaml::to_writer(&mut buf, &self.0) { + Ok(()) => ( + [( + header::CONTENT_TYPE, + HeaderValue::from_static("application/yaml"), + )], + buf.into_inner().freeze(), + ) + .into_response(), + Err(err) => ( + StatusCode::INTERNAL_SERVER_ERROR, + [( + header::CONTENT_TYPE, + HeaderValue::from_static(mime::TEXT_PLAIN_UTF_8.as_ref()), + )], + err.to_string(), + ) + .into_response(), + } + } +} + +#[derive(Debug, Clone, Copy, Default)] +#[must_use] +#[cfg(feature = "bincode")] +pub struct Bincode(pub T); + +#[cfg(feature = "bincode")] +impl IntoResponse for Bincode +where + T: Serialize, +{ + // replicates axum's Json + fn into_response(self) -> Response { + use bincode::Options; + let mut buf = BytesMut::with_capacity(128).writer(); + + match crate::make_bincode_serializer().serialize_into(&mut buf, &self.0) { + Ok(()) => ( + [( + header::CONTENT_TYPE, + HeaderValue::from_static("application/bincode"), + )], + buf.into_inner().freeze(), + ) + .into_response(), + Err(err) => ( + StatusCode::INTERNAL_SERVER_ERROR, + [( + header::CONTENT_TYPE, + HeaderValue::from_static(mime::TEXT_PLAIN_UTF_8.as_ref()), + )], + err.to_string(), + ) + .into_response(), + } + } +} diff --git a/nym-api/Cargo.toml b/nym-api/Cargo.toml index 7cc91bee56..489c36c4e3 100644 --- a/nym-api/Cargo.toml +++ b/nym-api/Cargo.toml @@ -117,7 +117,7 @@ nym-bin-common = { path = "../common/bin-common", features = ["output_format", " nym-node-tester-utils = { path = "../common/node-tester-utils" } nym-node-requests = { path = "../nym-node/nym-node-requests" } nym-types = { path = "../common/types" } -nym-http-api-common = { path = "../common/http-api-common", features = ["utoipa"] } +nym-http-api-common = { path = "../common/http-api-common", features = ["utoipa", "bincode", "output", "middleware"] } nym-serde-helpers = { path = "../common/serde-helpers", features = ["date"] } nym-ticketbooks-merkle = { path = "../common/ticketbooks-merkle" } nym-statistics-common = { path = "../common/statistics" } diff --git a/nym-api/src/circulating_supply_api/handlers.rs b/nym-api/src/circulating_supply_api/handlers.rs index 3e49659b55..357a0c46c4 100644 --- a/nym-api/src/circulating_supply_api/handlers.rs +++ b/nym-api/src/circulating_supply_api/handlers.rs @@ -3,8 +3,10 @@ use crate::node_status_api::models::{AxumErrorResponse, AxumResult}; use crate::support::http::state::AppState; +use axum::extract::Query; use axum::{extract, Router}; use nym_api_requests::models::CirculatingSupplyResponse; +use nym_http_api_common::{FormattedResponse, OutputParams}; use nym_validator_client::nyxd::Coin; pub(crate) fn circulating_supply_routes() -> Router { @@ -25,18 +27,26 @@ pub(crate) fn circulating_supply_routes() -> Router { get, path = "/v1/circulating-supply", responses( - (status = 200, body = CirculatingSupplyResponse) - ) + (status = 200, content( + (CirculatingSupplyResponse = "application/json"), + (CirculatingSupplyResponse = "application/yaml"), + (CirculatingSupplyResponse = "application/bincode") + )) + ), + params(OutputParams) )] async fn get_full_circulating_supply( + Query(output): Query, extract::State(state): extract::State, -) -> AxumResult> { +) -> AxumResult> { + let output = output.output.unwrap_or_default(); + match state .circulating_supply_cache() .get_circulating_supply() .await { - Some(value) => Ok(value.into()), + Some(value) => Ok(output.to_response(value)), None => Err(AxumErrorResponse::internal_msg("unavailable")), } } @@ -46,12 +56,19 @@ async fn get_full_circulating_supply( get, path = "/v1/circulating-supply/total-supply-value", responses( - (status = 200, body = [f64]) - ) + (status = 200, content( + (f64 = "application/json"), + (f64 = "application/yaml"), + (f64 = "application/bincode") + )) + ), + params(OutputParams) )] async fn get_total_supply( + Query(output): Query, extract::State(state): extract::State, -) -> AxumResult> { +) -> AxumResult> { + let output = output.output.unwrap_or_default(); let full_circulating_supply = match state .circulating_supply_cache() .get_circulating_supply() @@ -61,7 +78,9 @@ async fn get_total_supply( None => return Err(AxumErrorResponse::internal_msg("unavailable")), }; - Ok(unym_coin_to_float_unym(full_circulating_supply.total_supply.into()).into()) + let total_supply = unym_coin_to_float_unym(full_circulating_supply.total_supply.into()); + + Ok(output.to_response(total_supply)) } #[utoipa::path( @@ -69,12 +88,20 @@ async fn get_total_supply( get, path = "/v1/circulating-supply/circulating-supply-value", responses( - (status = 200, body = [f64]) - ) + (status = 200, content( + (f64 = "application/json"), + (f64 = "application/yaml"), + (f64 = "application/bincode") + )) + ), + params(OutputParams) )] async fn get_circulating_supply( + Query(output): Query, extract::State(state): extract::State, -) -> AxumResult> { +) -> AxumResult> { + let output = output.output.unwrap_or_default(); + let full_circulating_supply = match state .circulating_supply_cache() .get_circulating_supply() @@ -84,7 +111,10 @@ async fn get_circulating_supply( None => return Err(AxumErrorResponse::internal_msg("unavailable")), }; - Ok(unym_coin_to_float_unym(full_circulating_supply.circulating_supply.into()).into()) + let circulating_supply = + unym_coin_to_float_unym(full_circulating_supply.circulating_supply.into()); + + Ok(output.to_response(circulating_supply)) } // TODO: this is not the best place to put it, it should be more centralised, diff --git a/nym-api/src/ecash/api_routes/aggregation.rs b/nym-api/src/ecash/api_routes/aggregation.rs index 8b7baef6e6..d832022d74 100644 --- a/nym-api/src/ecash/api_routes/aggregation.rs +++ b/nym-api/src/ecash/api_routes/aggregation.rs @@ -7,12 +7,13 @@ use crate::ecash::state::EcashState; use crate::node_status_api::models::AxumResult; use crate::support::http::state::AppState; use axum::extract::{Query, State}; -use axum::{Json, Router}; +use axum::Router; use nym_api_requests::ecash::models::{ AggregatedCoinIndicesSignatureResponse, AggregatedExpirationDateSignatureResponse, }; use nym_api_requests::ecash::VerificationKeyResponse; use nym_ecash_time::{cred_exp_date, EcashTime}; +use nym_http_api_common::{FormattedResponse, Output}; use nym_validator_client::nym_api::rfc_3339_date; use serde::Deserialize; use std::sync::Arc; @@ -45,26 +46,32 @@ pub(crate) fn aggregation_routes() -> Router { ), path = "/v1/ecash/master-verification-key", responses( - (status = 200, body = VerificationKeyResponse) - ) + (status = 200, content( + (VerificationKeyResponse = "application/json"), + (VerificationKeyResponse = "application/yaml"), + (VerificationKeyResponse = "application/bincode") + )) + ), )] async fn master_verification_key( State(state): State>, - Query(EpochIdParam { epoch_id }): Query, -) -> AxumResult> { + Query(EpochIdParam { epoch_id, output }): Query, +) -> AxumResult> { trace!("aggregated_verification_key request"); + let output = output.unwrap_or_default(); // see if we're not in the middle of new dkg state.ensure_dkg_not_in_progress().await?; let key = state.master_verification_key(epoch_id).await?; - Ok(Json(VerificationKeyResponse::new(key.clone()))) + Ok(output.to_response(VerificationKeyResponse::new(key.clone()))) } #[derive(Deserialize, IntoParams)] struct ExpirationDateParam { expiration_date: Option, + output: Option, } #[utoipa::path( @@ -75,14 +82,22 @@ struct ExpirationDateParam { ), path = "/v1/ecash/aggregated-expiration-date-signatures", responses( - (status = 200, body = AggregatedExpirationDateSignatureResponse) - ) + (status = 200, content( + (AggregatedExpirationDateSignatureResponse = "application/json"), + (AggregatedExpirationDateSignatureResponse = "application/yaml"), + (AggregatedExpirationDateSignatureResponse = "application/bincode") + )) + ), )] async fn expiration_date_signatures( State(state): State>, - Query(ExpirationDateParam { expiration_date }): Query, -) -> AxumResult> { + Query(ExpirationDateParam { + expiration_date, + output, + }): Query, +) -> AxumResult> { trace!("aggregated_expiration_date_signatures request"); + let output = output.unwrap_or_default(); let expiration_date = match expiration_date { None => cred_exp_date().ecash_date(), @@ -97,11 +112,13 @@ async fn expiration_date_signatures( .master_expiration_date_signatures(expiration_date) .await?; - Ok(Json(AggregatedExpirationDateSignatureResponse { - epoch_id: expiration_date_signatures.epoch_id, - expiration_date, - signatures: expiration_date_signatures.signatures.clone(), - })) + Ok( + output.to_response(AggregatedExpirationDateSignatureResponse { + epoch_id: expiration_date_signatures.epoch_id, + expiration_date, + signatures: expiration_date_signatures.signatures.clone(), + }), + ) } #[utoipa::path( @@ -112,21 +129,26 @@ async fn expiration_date_signatures( ), path = "/v1/ecash/aggregated-coin-indices-signatures", responses( - (status = 200, body = AggregatedCoinIndicesSignatureResponse) - ) + (status = 200, content( + (AggregatedCoinIndicesSignatureResponse = "application/json"), + (AggregatedCoinIndicesSignatureResponse = "application/yaml"), + (AggregatedCoinIndicesSignatureResponse = "application/bincode") + )) + ), )] async fn coin_indices_signatures( - Query(EpochIdParam { epoch_id }): Query, + Query(EpochIdParam { epoch_id, output }): Query, State(state): State>, -) -> AxumResult> { +) -> AxumResult> { trace!("aggregated_coin_indices_signatures request"); + let output = output.unwrap_or_default(); // see if we're not in the middle of new dkg state.ensure_dkg_not_in_progress().await?; let coin_indices_signatures = state.master_coin_index_signatures(epoch_id).await?; - Ok(Json(AggregatedCoinIndicesSignatureResponse { + Ok(output.to_response(AggregatedCoinIndicesSignatureResponse { epoch_id: coin_indices_signatures.epoch_id, signatures: coin_indices_signatures.signatures.clone(), })) diff --git a/nym-api/src/ecash/api_routes/helpers.rs b/nym-api/src/ecash/api_routes/helpers.rs index 9d0270acc7..b625ba29ba 100644 --- a/nym-api/src/ecash/api_routes/helpers.rs +++ b/nym-api/src/ecash/api_routes/helpers.rs @@ -1,7 +1,10 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use nym_http_api_common::Output; + #[derive(serde::Deserialize, utoipa::IntoParams)] pub(super) struct EpochIdParam { pub(super) epoch_id: Option, + pub(super) output: Option, } diff --git a/nym-api/src/ecash/api_routes/issued.rs b/nym-api/src/ecash/api_routes/issued.rs index 717073e262..fd63ccdfe1 100644 --- a/nym-api/src/ecash/api_routes/issued.rs +++ b/nym-api/src/ecash/api_routes/issued.rs @@ -13,6 +13,7 @@ use nym_api_requests::ecash::models::{ IssuedTicketbooksForCountResponse, IssuedTicketbooksForResponse, IssuedTicketbooksOnCountResponse, SignableMessageBody, }; +use nym_http_api_common::{FormattedResponse, OutputParams}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::cmp::min; @@ -80,15 +81,20 @@ pub(crate) struct IssuanceDatePathParam { path = "/issued-ticketbooks-count", context_path = "/v1/ecash", responses( - (status = 200, body = IssuedTicketbooksCountResponse), + (status = 200, content( + (IssuedTicketbooksCountResponse = "application/json"), + (IssuedTicketbooksCountResponse = "application/yaml"), + (IssuedTicketbooksCountResponse = "application/bincode") + )), (status = 400, body = String, description = "this nym-api is not an ecash signer in the current epoch"), ) )] async fn issued_ticketbooks_count( Query(pagination): Query, State(state): State>, -) -> AxumResult> { +) -> AxumResult> { state.ensure_signer().await?; + let output = pagination.output.unwrap_or_default(); let page = pagination.page.unwrap_or_default(); let per_page = min( @@ -98,9 +104,7 @@ async fn issued_ticketbooks_count( MAX_ISSUANCE_COUNT_PAGE_SIZE, ); - Ok(Json( - state.get_issued_ticketbooks_count(page, per_page).await?, - )) + Ok(output.to_response(state.get_issued_ticketbooks_count(page, per_page).await?)) } /// Returns number of issued ticketbooks for particular expiration date. @@ -109,22 +113,28 @@ async fn issued_ticketbooks_count( tag = "Ecash", get, params( - ExpirationDatePathParam + ExpirationDatePathParam, OutputParams ), path = "/issued-ticketbooks-for-count/{expiration_date}", context_path = "/v1/ecash", responses( - (status = 200, body = IssuedTicketbooksForCountResponse), + (status = 200, content( + (IssuedTicketbooksForCountResponse = "application/json"), + (IssuedTicketbooksForCountResponse = "application/yaml"), + (IssuedTicketbooksForCountResponse = "application/bincode") + )), (status = 400, body = String, description = "this nym-api is not an ecash signer in the current epoch"), ) )] async fn issued_ticketbooks_for_count( + Query(output): Query, Path(ExpirationDatePathParam { expiration_date }): Path, State(state): State>, -) -> AxumResult> { +) -> AxumResult> { state.ensure_signer().await?; + let output = output.output.unwrap_or_default(); - Ok(Json( + Ok(output.to_response( state .get_issued_ticketbooks_for_count(expiration_date) .await?, @@ -137,46 +147,56 @@ async fn issued_ticketbooks_for_count( tag = "Ecash", get, params( - IssuanceDatePathParam + IssuanceDatePathParam, OutputParams ), path = "/issued-ticketbooks-on-count/{issuance_date}", context_path = "/v1/ecash", responses( - (status = 200, body = IssuedTicketbooksOnCountResponse), + (status = 200, content( + (IssuedTicketbooksOnCountResponse = "application/json"), + (IssuedTicketbooksOnCountResponse = "application/yaml"), + (IssuedTicketbooksOnCountResponse = "application/bincode") + )), (status = 400, body = String, description = "this nym-api is not an ecash signer in the current epoch"), ) )] async fn issued_ticketbooks_on_count( + Query(output): Query, Path(IssuanceDatePathParam { issuance_date }): Path, State(state): State>, -) -> AxumResult> { +) -> AxumResult> { state.ensure_signer().await?; + let output = output.output.unwrap_or_default(); - Ok(Json( - state.get_issued_ticketbooks_on_count(issuance_date).await?, - )) + Ok(output.to_response(state.get_issued_ticketbooks_on_count(issuance_date).await?)) } #[utoipa::path( tag = "Ecash", get, params( - ExpirationDatePathParam + ExpirationDatePathParam, OutputParams ), path = "/issued-ticketbooks-for/{expiration_date}", context_path = "/v1/ecash", responses( - (status = 200, body = IssuedTicketbooksForResponse), + (status = 200, content( + (IssuedTicketbooksForResponse = "application/json"), + (IssuedTicketbooksForResponse = "application/yaml"), + (IssuedTicketbooksForResponse = "application/bincode") + )), (status = 400, body = String, description = "this nym-api is not an ecash signer in the current epoch"), ) )] async fn issued_ticketbooks_for( + Query(output): Query, State(state): State>, Path(ExpirationDatePathParam { expiration_date }): Path, -) -> AxumResult> { +) -> AxumResult> { state.ensure_signer().await?; + let output = output.output.unwrap_or_default(); - Ok(Json( + Ok(output.to_response( state .get_issued_ticketbooks_deposits_on(expiration_date) .await? @@ -191,17 +211,24 @@ async fn issued_ticketbooks_for( path = "/issued-ticketbooks-challenge-commitment", context_path = "/v1/ecash", responses( - (status = 200, body = IssuedTicketbooksChallengeCommitmentResponse), + (status = 200, content( + (IssuedTicketbooksChallengeCommitmentResponse = "application/json"), + (IssuedTicketbooksChallengeCommitmentResponse = "application/yaml"), + (IssuedTicketbooksChallengeCommitmentResponse = "application/bincode") + )), (status = 400, body = String, description = "this nym-api is not an ecash signer in the current epoch"), - ) + ), + params(OutputParams) )] async fn issued_ticketbooks_challenge_commitment( + Query(output): Query, State(state): State>, Json(request): Json, -) -> AxumResult> { +) -> AxumResult> { state.ensure_signer().await?; + let output = output.output.unwrap_or_default(); - Ok(Json( + Ok(output.to_response( state .get_issued_ticketbooks_challenge_commitment(request) .await? @@ -216,17 +243,24 @@ async fn issued_ticketbooks_challenge_commitment( path = "/issued-ticketbooks-data", context_path = "/v1/ecash", responses( - (status = 200, body = IssuedTicketbooksDataResponse), + (status = 200, content( + (IssuedTicketbooksDataResponse = "application/json"), + (IssuedTicketbooksDataResponse = "application/yaml"), + (IssuedTicketbooksDataResponse = "application/bincode") + )), (status = 400, body = String, description = "this nym-api is not an ecash signer in the current epoch"), - ) + ), + params(OutputParams) )] async fn issued_ticketbooks_data( + Query(output): Query, State(state): State>, Json(request): Json, -) -> AxumResult> { +) -> AxumResult> { state.ensure_signer().await?; + let output = output.output.unwrap_or_default(); - Ok(Json( + Ok(output.to_response( state .get_issued_ticketbooks_data(request) .await? diff --git a/nym-api/src/ecash/api_routes/partial_signing.rs b/nym-api/src/ecash/api_routes/partial_signing.rs index b5d53a5ed0..b19e938d88 100644 --- a/nym-api/src/ecash/api_routes/partial_signing.rs +++ b/nym-api/src/ecash/api_routes/partial_signing.rs @@ -14,6 +14,7 @@ use nym_api_requests::ecash::{ PartialExpirationDateSignatureResponse, }; use nym_ecash_time::{cred_exp_date, EcashTime}; +use nym_http_api_common::{FormattedResponse, Output, OutputParams}; use nym_validator_client::nym_api::rfc_3339_date; use serde::Deserialize; use std::ops::Deref; @@ -41,16 +42,22 @@ pub(crate) fn partial_signing_routes() -> Router { request_body = BlindSignRequestBody, path = "/v1/ecash/blind-sign", responses( - (status = 200, body = BlindedSignatureResponse), + (status = 200, content( + (BlindedSignatureResponse = "application/json"), + (BlindedSignatureResponse = "application/yaml"), + (BlindedSignatureResponse = "application/bincode") + )), (status = 400, body = String, description = "this nym-api is not an ecash signer in the current epoch"), - - ) + ), + params(OutputParams) )] async fn post_blind_sign( + Query(output): Query, State(state): State>, Json(blind_sign_request_body): Json, -) -> AxumResult> { +) -> AxumResult> { state.ensure_signer().await?; + let output = output.output.unwrap_or_default(); debug!("Received blind sign request"); trace!("body: {:?}", blind_sign_request_body); @@ -73,7 +80,7 @@ async fn post_blind_sign( "checking if we have already issued credential for this deposit (deposit_id: {deposit_id})", ); if let Some(blinded_signature) = state.already_issued(deposit_id).await? { - return Ok(Json(BlindedSignatureResponse { blinded_signature })); + return Ok(output.to_response(BlindedSignatureResponse { blinded_signature })); } //check if account was blacklisted @@ -101,12 +108,13 @@ async fn post_blind_sign( .await?; // finally return the credential to the client - Ok(Json(BlindedSignatureResponse { blinded_signature })) + Ok(output.to_response(BlindedSignatureResponse { blinded_signature })) } #[derive(Deserialize, IntoParams)] struct ExpirationDateParam { expiration_date: Option, + output: Option, } #[utoipa::path( @@ -117,15 +125,23 @@ struct ExpirationDateParam { ), path = "/v1/ecash/partial-expiration-date-signatures", responses( - (status = 200, body = PartialExpirationDateSignatureResponse), + (status = 200, content( + (PartialExpirationDateSignatureResponse = "application/json"), + (PartialExpirationDateSignatureResponse = "application/yaml"), + (PartialExpirationDateSignatureResponse = "application/bincode") + )), (status = 400, body = String, description = "this nym-api is not an ecash signer in the current epoch"), ) )] async fn partial_expiration_date_signatures( State(state): State>, - Query(ExpirationDateParam { expiration_date }): Query, -) -> AxumResult> { + Query(ExpirationDateParam { + expiration_date, + output, + }): Query, +) -> AxumResult> { state.ensure_signer().await?; + let output = output.unwrap_or_default(); let expiration_date = match expiration_date { None => cred_exp_date().ecash_date(), @@ -140,7 +156,7 @@ async fn partial_expiration_date_signatures( .partial_expiration_date_signatures(expiration_date) .await?; - Ok(Json(PartialExpirationDateSignatureResponse { + Ok(output.to_response(PartialExpirationDateSignatureResponse { epoch_id: expiration_date_signatures.epoch_id, expiration_date, signatures: expiration_date_signatures.signatures.clone(), @@ -155,14 +171,18 @@ async fn partial_expiration_date_signatures( ), path = "/v1/ecash/partial-coin-indices-signatures", responses( - (status = 200, body = PartialExpirationDateSignatureResponse), + (status = 200, content( + (PartialCoinIndicesSignatureResponse = "application/json"), + (PartialCoinIndicesSignatureResponse = "application/yaml"), + (PartialCoinIndicesSignatureResponse = "application/bincode") + )), (status = 400, body = String, description = "this nym-api is not an ecash signer in the current epoch"), ) )] async fn partial_coin_indices_signatures( State(state): State>, - Query(EpochIdParam { epoch_id }): Query, -) -> AxumResult> { + Query(EpochIdParam { epoch_id, output }): Query, +) -> AxumResult> { state.ensure_signer().await?; // see if we're not in the middle of new dkg @@ -170,8 +190,10 @@ async fn partial_coin_indices_signatures( let coin_indices_signatures = state.partial_coin_index_signatures(epoch_id).await?; - Ok(Json(PartialCoinIndicesSignatureResponse { - epoch_id: coin_indices_signatures.epoch_id, - signatures: coin_indices_signatures.signatures.clone(), - })) + Ok(output + .unwrap_or_default() + .to_response(PartialCoinIndicesSignatureResponse { + epoch_id: coin_indices_signatures.epoch_id, + signatures: coin_indices_signatures.signatures.clone(), + })) } diff --git a/nym-api/src/ecash/api_routes/spending.rs b/nym-api/src/ecash/api_routes/spending.rs index c0fd546ef6..bae3342245 100644 --- a/nym-api/src/ecash/api_routes/spending.rs +++ b/nym-api/src/ecash/api_routes/spending.rs @@ -5,7 +5,7 @@ use crate::ecash::error::EcashError; use crate::ecash::state::EcashState; use crate::node_status_api::models::{AxumErrorResponse, AxumResult}; use crate::support::http::state::AppState; -use axum::extract::State; +use axum::extract::{Query, State}; use axum::http::StatusCode; use axum::{Json, Router}; use nym_api_requests::constants::MIN_BATCH_REDEMPTION_DELAY; @@ -15,6 +15,7 @@ use nym_api_requests::ecash::models::{ }; use nym_compact_ecash::identify::IdentifyResult; use nym_ecash_time::EcashTime; +use nym_http_api_common::{FormattedResponse, Output, OutputParams}; use std::collections::HashSet; use std::ops::Deref; use std::sync::Arc; @@ -39,9 +40,10 @@ pub(crate) fn spending_routes() -> Router { const ONE_AM: Time = time!(1:00); fn reject_ticket( + output: Output, reason: EcashTicketVerificationRejection, -) -> AxumResult> { - Ok(Json(EcashTicketVerificationResponse::reject(reason))) +) -> AxumResult> { + Ok(output.to_response(EcashTicketVerificationResponse::reject(reason))) } // TODO: optimise it; for now it's just dummy split of the original `verify_offline_credential` @@ -52,23 +54,29 @@ fn reject_ticket( request_body = VerifyEcashTicketBody, path = "/v1/ecash/verify-ecash-ticket", responses( - (status = 200, body = EcashTicketVerificationResponse), + (status = 200, content( + (EcashTicketVerificationResponse = "application/json"), + (EcashTicketVerificationResponse = "application/yaml"), + (EcashTicketVerificationResponse = "application/bincode") + )), (status = 400, body = String, description = "this nym-api is not an ecash signer in the current epoch"), ) )] async fn verify_ticket( + Query(output): Query, State(state): State>, // TODO in the future: make it send binary data rather than json Json(verify_ticket_body): Json, -) -> AxumResult> { +) -> AxumResult> { state.ensure_signer().await?; + let output = output.output.unwrap_or_default(); let credential_data = &verify_ticket_body.credential; let gateway_cosmos_addr = &verify_ticket_body.gateway_cosmos_addr; // easy check: is there only a single payment attached? if credential_data.payment.spend_value != 1 { - return reject_ticket(EcashTicketVerificationRejection::MultipleTickets); + return reject_ticket(output, EcashTicketVerificationRejection::MultipleTickets); } let sn = &credential_data.encoded_serial_number(); @@ -83,11 +91,14 @@ async fn verify_ticket( // only accept yesterday date if we're near the day transition, i.e. before 1AM UTC if spend_date != today_ecash && now.time() > ONE_AM && spend_date != yesterday_ecash { - return reject_ticket(EcashTicketVerificationRejection::InvalidSpentDate { - today: today_ecash, - yesterday: yesterday_ecash, - received: spend_date, - }); + return reject_ticket( + output, + EcashTicketVerificationRejection::InvalidSpentDate { + today: today_ecash, + yesterday: yesterday_ecash, + received: spend_date, + }, + ); } // actual double spend detection with storage @@ -101,7 +112,7 @@ async fn verify_ticket( IdentifyResult::NotADuplicatePayment => {} //SW NOTE This should never happen, quick message? IdentifyResult::DuplicatePayInfo(_) => { warn!("Identical payInfo"); - return reject_ticket(EcashTicketVerificationRejection::ReplayedTicket); + return reject_ticket(output, EcashTicketVerificationRejection::ReplayedTicket); } IdentifyResult::DoubleSpendingPublicKeys(pub_key) => { //Actual double spending @@ -110,7 +121,7 @@ async fn verify_ticket( pub_key.to_base58_string() ); error!("UNIMPLEMENTED: blacklisting the double spend key"); - return reject_ticket(EcashTicketVerificationRejection::DoubleSpend); + return reject_ticket(output, EcashTicketVerificationRejection::DoubleSpend); } } } @@ -119,7 +130,7 @@ async fn verify_ticket( // perform actual crypto verification if credential_data.verify(&verification_key).is_err() { - return reject_ticket(EcashTicketVerificationRejection::InvalidTicket); + return reject_ticket(output, EcashTicketVerificationRejection::InvalidTicket); } // store credential and check whether it wasn't already there (due to a parallel request) @@ -127,10 +138,10 @@ async fn verify_ticket( .store_verified_ticket(credential_data, gateway_cosmos_addr) .await?; if !was_inserted { - return reject_ticket(EcashTicketVerificationRejection::ReplayedTicket); + return reject_ticket(output, EcashTicketVerificationRejection::ReplayedTicket); } - Ok(Json(EcashTicketVerificationResponse { verified: Ok(()) })) + Ok(output.to_response(EcashTicketVerificationResponse { verified: Ok(()) })) } #[utoipa::path( @@ -139,16 +150,23 @@ async fn verify_ticket( request_body = BatchRedeemTicketsBody, path = "/v1/ecash/batch-redeem-ecash-tickets", responses( - (status = 200, body = EcashBatchTicketRedemptionResponse), + (status = 200, content( + (EcashBatchTicketRedemptionResponse = "application/json"), + (EcashBatchTicketRedemptionResponse = "application/yaml"), + (EcashBatchTicketRedemptionResponse = "application/bincode") + )), (status = 400, body = String, description = "this nym-api is not an ecash signer in the current epoch"), - ) + ), + params(OutputParams) )] async fn batch_redeem_tickets( + Query(output): Query, State(state): State>, // TODO in the future: make it send binary data rather than json Json(batch_redeem_credentials_body): Json, -) -> AxumResult> { +) -> AxumResult> { state.ensure_signer().await?; + let output = output.output.unwrap_or_default(); // 1. see if that gateway has even submitted any tickets let Some(provider_info) = state @@ -208,7 +226,7 @@ async fn batch_redeem_tickets( // 7. update the time of the last verification for this provider state.update_last_batch_verification(&provider_info).await?; - Ok(Json(EcashBatchTicketRedemptionResponse { + Ok(output.to_response(EcashBatchTicketRedemptionResponse { proposal_accepted: true, })) } @@ -224,7 +242,7 @@ async fn batch_redeem_tickets( ) )] #[deprecated] -async fn double_spending_filter_v1() -> AxumResult> { +async fn double_spending_filter_v1() -> AxumResult> { AxumResult::Err(AxumErrorResponse::new( "permanently restricted", StatusCode::GONE, diff --git a/nym-api/src/network/handlers.rs b/nym-api/src/network/handlers.rs index 380e78e736..f5a3e12736 100644 --- a/nym-api/src/network/handlers.rs +++ b/nym-api/src/network/handlers.rs @@ -4,10 +4,11 @@ use crate::network::models::{ContractInformation, NetworkDetails}; use crate::node_status_api::models::AxumResult; use crate::support::http::state::AppState; -use axum::extract::State; -use axum::{extract, Json, Router}; +use axum::extract::{Query, State}; +use axum::{extract, Router}; use nym_api_requests::models::ChainStatusResponse; use nym_contracts_common::ContractBuildInformation; +use nym_http_api_common::{FormattedResponse, OutputParams}; use std::collections::HashMap; use tower_http::compression::CompressionLayer; use utoipa::ToSchema; @@ -29,13 +30,21 @@ pub(crate) fn nym_network_routes() -> Router { get, path = "/v1/network/details", responses( - (status = 200, body = NetworkDetails) - ) + (status = 200, content( + (NetworkDetails = "application/json"), + (NetworkDetails = "application/yaml"), + (NetworkDetails = "application/bincode") + )) + ), + params(OutputParams) )] async fn network_details( + Query(output): Query, extract::State(state): extract::State, -) -> axum::Json { - state.network_details().to_owned().into() +) -> FormattedResponse { + let output = output.output.unwrap_or_default(); + + output.to_response(state.network_details().to_owned()) } #[utoipa::path( @@ -43,10 +52,20 @@ async fn network_details( get, path = "/v1/network/chain-status", responses( - (status = 200, body = ChainStatusResponse) - ) + (status = 200, content( + (ChainStatusResponse = "application/json"), + (ChainStatusResponse = "application/yaml"), + (ChainStatusResponse = "application/bincode") + )) + ), + params(OutputParams) )] -async fn chain_status(State(state): State) -> AxumResult> { +async fn chain_status( + Query(output): Query, + State(state): State, +) -> AxumResult> { + let output = output.output.unwrap_or_default(); + let chain_status = state .chain_status_cache .get_or_refresh(&state.nyxd_client) @@ -54,7 +73,7 @@ async fn chain_status(State(state): State) -> AxumResult) - ) + (status = 200, content( + (HashMap = "application/json"), + (HashMap = "application/yaml"), + (HashMap = "application/bincode") + )) + ), + params(OutputParams) )] async fn nym_contracts( + Query(output): Query, extract::State(state): extract::State, -) -> axum::Json>> { +) -> FormattedResponse>> { + let output = output.output.unwrap_or_default(); + let info = state.nym_contract_cache().contract_details().await; - info.iter() - .map(|(contract, info)| { - ( - contract.to_owned(), - ContractInformation { - address: info.address.as_ref().map(|a| a.to_string()), - details: info.base.clone(), - }, - ) - }) - .collect::>() - .into() + output.to_response( + info.iter() + .map(|(contract, info)| { + ( + contract.to_owned(), + ContractInformation { + address: info.address.as_ref().map(|a| a.to_string()), + details: info.base.clone(), + }, + ) + }) + .collect::>(), + ) } #[allow(dead_code)] // not dead, used in OpenAPI docs @@ -120,23 +148,32 @@ pub struct ContractInformationBuildInformation { get, path = "/v1/network/nym-contracts-detailed", responses( - (status = 200, body = HashMap) - ) + (status = 200, content( + (HashMap = "application/json"), + (HashMap = "application/yaml"), + (HashMap = "application/bincode") + )) + ), + params(OutputParams) )] async fn nym_contracts_detailed( + Query(output): Query, extract::State(state): extract::State, -) -> axum::Json>> { +) -> FormattedResponse>> { + let output = output.output.unwrap_or_default(); + let info = state.nym_contract_cache().contract_details().await; - info.iter() - .map(|(contract, info)| { - ( - contract.to_owned(), - ContractInformation { - address: info.address.as_ref().map(|a| a.to_string()), - details: info.detailed.clone(), - }, - ) - }) - .collect::>() - .into() + output.to_response( + info.iter() + .map(|(contract, info)| { + ( + contract.to_owned(), + ContractInformation { + address: info.address.as_ref().map(|a| a.to_string()), + details: info.detailed.clone(), + }, + ) + }) + .collect::>(), + ) } diff --git a/nym-api/src/node_status_api/handlers/mod.rs b/nym-api/src/node_status_api/handlers/mod.rs index b720294af9..31a1e306b1 100644 --- a/nym-api/src/node_status_api/handlers/mod.rs +++ b/nym-api/src/node_status_api/handlers/mod.rs @@ -4,10 +4,11 @@ use crate::node_status_api::models::AxumResult; use crate::support::caching::cache::UninitialisedCache; use crate::support::http::state::AppState; -use axum::extract::State; +use axum::extract::{Query, State}; use axum::routing::get; -use axum::{Json, Router}; +use axum::Router; use nym_api_requests::models::ConfigScoreDataResponse; +use nym_http_api_common::{FormattedResponse, OutputParams}; use nym_mixnet_contract_common::NodeId; use serde::Deserialize; use utoipa::IntoParams; @@ -43,17 +44,25 @@ struct MixIdParam { path = "/config-score-details", context_path = "/v1/status", responses( - (status = 200, body = ConfigScoreDataResponse) + (status = 200, content( + (ConfigScoreDataResponse = "application/json"), + (ConfigScoreDataResponse = "application/yaml"), + (ConfigScoreDataResponse = "application/bincode") + )) ), + params(OutputParams) )] async fn config_score_details( + Query(output): Query, State(state): State, -) -> AxumResult> { +) -> AxumResult> { + let output = output.output.unwrap_or_default(); + let data = state .nym_contract_cache() .maybe_config_score_data_owned() .await .ok_or(UninitialisedCache)?; - Ok(Json(data.into_inner().into())) + Ok(output.to_response(data.into_inner().into())) } diff --git a/nym-api/src/node_status_api/handlers/network_monitor.rs b/nym-api/src/node_status_api/handlers/network_monitor.rs index dbd3720089..559ca11182 100644 --- a/nym-api/src/node_status_api/handlers/network_monitor.rs +++ b/nym-api/src/node_status_api/handlers/network_monitor.rs @@ -22,6 +22,7 @@ use nym_api_requests::models::{ MixNodeBondAnnotated, MixnodeCoreStatusResponse, MixnodeStatusReportResponse, MixnodeUptimeHistoryResponse, RewardEstimationResponse, UptimeResponse, }; +use nym_http_api_common::{FormattedResponse, Output, OutputParams}; use serde::Deserialize; use utoipa::IntoParams; @@ -103,17 +104,23 @@ pub(super) fn network_monitor_routes() -> Router { get, path = "/v1/status/gateway/{identity}/report", responses( - (status = 200, body = GatewayStatusReportResponse) - ) + (status = 200, content( + (GatewayStatusReportResponse = "application/json"), + (GatewayStatusReportResponse = "application/yaml"), + (GatewayStatusReportResponse = "application/bincode") + )) + ), + params(OutputParams) )] #[deprecated] async fn gateway_report( Path(identity): Path, + Query(output): Query, State(state): State, -) -> AxumResult> { - Ok(Json( - _gateway_report(state.node_status_cache(), &identity).await?, - )) +) -> AxumResult> { + let output = output.output.unwrap_or_default(); + + Ok(output.to_response(_gateway_report(state.node_status_cache(), &identity).await?)) } #[utoipa::path( @@ -121,15 +128,23 @@ async fn gateway_report( get, path = "/v1/status/gateway/{identity}/history", responses( - (status = 200, body = GatewayUptimeHistoryResponse) - ) + (status = 200, content( + (GatewayUptimeHistoryResponse = "application/json"), + (GatewayUptimeHistoryResponse = "application/yaml"), + (GatewayUptimeHistoryResponse = "application/bincode") + )) + ), + params(OutputParams) )] #[deprecated] async fn gateway_uptime_history( Path(identity): Path, + Query(output): Query, State(state): State, -) -> AxumResult> { - Ok(Json( +) -> AxumResult> { + let output = output.output.unwrap_or_default(); + + Ok(output.to_response( _gateway_uptime_history(state.storage(), state.nym_contract_cache(), &identity).await?, )) } @@ -138,6 +153,7 @@ async fn gateway_uptime_history( #[into_params(parameter_in = Query)] struct SinceQueryParams { since: Option, + output: Option, } #[utoipa::path( @@ -148,18 +164,22 @@ struct SinceQueryParams { ), path = "/v1/status/gateway/{identity}/core-status-count", responses( - (status = 200, body = GatewayCoreStatusResponse) - ) + (status = 200, content( + (GatewayCoreStatusResponse = "application/json"), + (GatewayCoreStatusResponse = "application/yaml"), + (GatewayCoreStatusResponse = "application/bincode") + )) + ), )] #[deprecated] async fn gateway_core_status_count( Path(identity): Path, - Query(SinceQueryParams { since }): Query, + Query(SinceQueryParams { since, output }): Query, State(state): State, -) -> AxumResult> { - Ok(Json( - _gateway_core_status_count(state.storage(), &identity, since).await?, - )) +) -> AxumResult> { + Ok(output + .unwrap_or_default() + .to_response(_gateway_core_status_count(state.storage(), &identity, since).await?)) } #[utoipa::path( @@ -167,57 +187,71 @@ async fn gateway_core_status_count( get, path = "/v1/status/gateway/{identity}/avg_uptime", responses( - (status = 200, body = GatewayUptimeResponse) - ) + (status = 200, content( + (GatewayUptimeResponse = "application/json"), + (GatewayUptimeResponse = "application/yaml"), + (GatewayUptimeResponse = "application/bincode") + )) + ), + params(OutputParams) )] #[deprecated] async fn get_gateway_avg_uptime( Path(identity): Path, + Query(output): Query, State(state): State, -) -> AxumResult> { - Ok(Json( - _get_gateway_avg_uptime(state.node_status_cache(), &identity).await?, - )) +) -> AxumResult> { + let output = output.output.unwrap_or_default(); + + Ok(output.to_response(_get_gateway_avg_uptime(state.node_status_cache(), &identity).await?)) } #[utoipa::path( tag = "network-monitor-status", get, - params( - MixIdParam - ), path = "/v1/status/mixnode/{mix_id}/report", responses( - (status = 200, body = MixnodeStatusReportResponse) - ) + (status = 200, content( + (MixnodeStatusReportResponse = "application/json"), + (MixnodeStatusReportResponse = "application/yaml"), + (MixnodeStatusReportResponse = "application/bincode") + )) + ), + params(OutputParams, MixIdParam) )] #[deprecated] async fn mixnode_report( Path(MixIdParam { mix_id }): Path, + Query(output): Query, State(state): State, -) -> AxumResult> { - Ok(Json( - _mixnode_report(state.node_status_cache(), mix_id).await?, - )) +) -> AxumResult> { + let output = output.output.unwrap_or_default(); + + Ok(output.to_response(_mixnode_report(state.node_status_cache(), mix_id).await?)) } #[utoipa::path( tag = "network-monitor-status", get, - params( - MixIdParam - ), path = "/v1/status/mixnode/{mix_id}/history", responses( - (status = 200, body = MixnodeUptimeHistoryResponse) - ) + (status = 200, content( + (MixnodeUptimeHistoryResponse = "application/json"), + (MixnodeUptimeHistoryResponse = "application/yaml"), + (MixnodeUptimeHistoryResponse = "application/bincode") + )) + ), + params(MixIdParam, OutputParams) )] #[deprecated] async fn mixnode_uptime_history( Path(MixIdParam { mix_id }): Path, + Query(output): Query, State(state): State, -) -> AxumResult> { - Ok(Json( +) -> AxumResult> { + let output = output.output.unwrap_or_default(); + + Ok(output.to_response( _mixnode_uptime_history(state.storage(), state.nym_contract_cache(), mix_id).await?, )) } @@ -230,37 +264,48 @@ async fn mixnode_uptime_history( ), path = "/v1/status/mixnode/{mix_id}/core-status-count", responses( - (status = 200, body = MixnodeCoreStatusResponse) - ) + (status = 200, content( + (MixnodeCoreStatusResponse = "application/json"), + (MixnodeCoreStatusResponse = "application/yaml"), + (MixnodeCoreStatusResponse = "application/bincode") + )) + ), )] #[deprecated] async fn mixnode_core_status_count( Path(MixIdParam { mix_id }): Path, - Query(SinceQueryParams { since }): Query, + Query(SinceQueryParams { since, output }): Query, State(state): State, -) -> AxumResult> { - Ok(Json( - _mixnode_core_status_count(state.storage(), mix_id, since).await?, - )) +) -> AxumResult> { + let output = output.unwrap_or_default(); + + Ok(output.to_response(_mixnode_core_status_count(state.storage(), mix_id, since).await?)) } #[utoipa::path( tag = "network-monitor-status", get, params( - MixIdParam + MixIdParam, OutputParams ), path = "/v1/status/mixnode/{mix_id}/reward-estimation", responses( - (status = 200, body = RewardEstimationResponse) - ) + (status = 200, content( + (RewardEstimationResponse = "application/json"), + (RewardEstimationResponse = "application/yaml"), + (RewardEstimationResponse = "application/bincode") + )) + ), )] #[deprecated] async fn get_mixnode_reward_estimation( Path(MixIdParam { mix_id }): Path, + Query(output): Query, State(state): State, -) -> AxumResult> { - Ok(Json( +) -> AxumResult> { + let output = output.output.unwrap_or_default(); + + Ok(output.to_response( _get_mixnode_reward_estimation( state.node_status_cache(), state.nym_contract_cache(), @@ -274,21 +319,28 @@ async fn get_mixnode_reward_estimation( tag = "network-monitor-status", post, params( - ComputeRewardEstParam, MixIdParam + OutputParams, MixIdParam ), path = "/v1/status/mixnode/{mix_id}/compute-reward-estimation", request_body = ComputeRewardEstParam, responses( - (status = 200, body = RewardEstimationResponse) - ) + (status = 200, content( + (RewardEstimationResponse = "application/json"), + (RewardEstimationResponse = "application/yaml"), + (RewardEstimationResponse = "application/bincode") + )) + ), )] #[deprecated] async fn compute_mixnode_reward_estimation( Path(MixIdParam { mix_id }): Path, + Query(output): Query, State(state): State, Json(user_reward_param): Json, -) -> AxumResult> { - Ok(Json( +) -> AxumResult> { + let output = output.output.unwrap_or_default(); + + Ok(output.to_response( _compute_mixnode_reward_estimation( &user_reward_param, state.node_status_cache(), @@ -303,21 +355,26 @@ async fn compute_mixnode_reward_estimation( tag = "network-monitor-status", get, params( - MixIdParam + MixIdParam, OutputParams ), path = "/v1/status/mixnode/{mix_id}/avg_uptime", responses( - (status = 200, body = UptimeResponse) - ) + (status = 200, content( + (UptimeResponse = "application/json"), + (UptimeResponse = "application/yaml"), + (UptimeResponse = "application/bincode") + )) + ), )] #[deprecated] async fn get_mixnode_avg_uptime( Path(MixIdParam { mix_id }): Path, + Query(output): Query, State(state): State, -) -> AxumResult> { - Ok(Json( - _get_mixnode_avg_uptime(state.node_status_cache(), mix_id).await?, - )) +) -> AxumResult> { + let output = output.output.unwrap_or_default(); + + Ok(output.to_response(_get_mixnode_avg_uptime(state.node_status_cache(), mix_id).await?)) } #[utoipa::path( @@ -325,14 +382,22 @@ async fn get_mixnode_avg_uptime( get, path = "/v1/status/mixnodes/detailed-unfiltered", responses( - (status = 200, body = MixNodeBondAnnotated) - ) + (status = 200, content( + (MixNodeBondAnnotated = "application/json"), + (MixNodeBondAnnotated = "application/yaml"), + (MixNodeBondAnnotated = "application/bincode") + )) + ), + params(OutputParams) )] #[deprecated] pub async fn get_mixnodes_detailed_unfiltered( + Query(output): Query, State(state): State, -) -> Json> { - Json(_get_mixnodes_detailed_unfiltered(state.node_status_cache()).await) +) -> FormattedResponse> { + let output = output.output.unwrap_or_default(); + + output.to_response(_get_mixnodes_detailed_unfiltered(state.node_status_cache()).await) } #[utoipa::path( @@ -340,14 +405,22 @@ pub async fn get_mixnodes_detailed_unfiltered( get, path = "/v1/status/gateways/detailed", responses( - (status = 200, body = GatewayBondAnnotated) - ) + (status = 200, content( + (GatewayBondAnnotated = "application/json"), + (GatewayBondAnnotated = "application/yaml"), + (GatewayBondAnnotated = "application/bincode") + )) + ), + params(OutputParams) )] #[deprecated] pub async fn get_gateways_detailed( + Query(output): Query, State(state): State, -) -> Json> { - Json(_get_legacy_gateways_detailed(state.node_status_cache()).await) +) -> FormattedResponse> { + let output = output.output.unwrap_or_default(); + + output.to_response(_get_legacy_gateways_detailed(state.node_status_cache()).await) } #[utoipa::path( @@ -355,12 +428,20 @@ pub async fn get_gateways_detailed( get, path = "/v1/status/gateways/detailed-unfiltered", responses( - (status = 200, body = GatewayBondAnnotated) - ) + (status = 200, content( + (GatewayBondAnnotated = "application/json"), + (GatewayBondAnnotated = "application/yaml"), + (GatewayBondAnnotated = "application/bincode") + )) + ), + params(OutputParams) )] #[deprecated] pub async fn get_gateways_detailed_unfiltered( + Query(output): Query, State(state): State, -) -> Json> { - Json(_get_legacy_gateways_detailed_unfiltered(state.node_status_cache()).await) +) -> FormattedResponse> { + let output = output.output.unwrap_or_default(); + + output.to_response(_get_legacy_gateways_detailed_unfiltered(state.node_status_cache()).await) } diff --git a/nym-api/src/node_status_api/handlers/unstable.rs b/nym-api/src/node_status_api/handlers/unstable.rs index 76dcf912cd..747ec4620b 100644 --- a/nym-api/src/node_status_api/handlers/unstable.rs +++ b/nym-api/src/node_status_api/handlers/unstable.rs @@ -8,12 +8,12 @@ use crate::support::http::state::AppState; use crate::support::storage::NymApiStorage; use anyhow::bail; use axum::extract::{Path, Query, State}; -use axum::Json; use nym_api_requests::models::{ GatewayTestResultResponse, MixnodeTestResultResponse, NetworkMonitorRunDetailsResponse, PartialTestResult, TestNode, TestRoute, }; use nym_api_requests::pagination::Pagination; +use nym_http_api_common::{FormattedResponse, OutputParams}; use nym_mixnet_contract_common::NodeId; use std::cmp::min; use std::collections::{BTreeMap, HashMap}; @@ -173,14 +173,18 @@ async fn _mixnode_test_results( ), path = "/v1/status/mixnodes/unstable/{mix_id}/test-results", responses( - (status = 200, body = MixnodeTestResultResponse) - ) + (status = 200, content( + (MixnodeTestResultResponse = "application/json"), + (MixnodeTestResultResponse = "application/yaml"), + (MixnodeTestResultResponse = "application/bincode") + )) + ), )] pub async fn mixnode_test_results( Path(mix_id): Path, Query(pagination): Query, State(state): State, -) -> AxumResult> { +) -> AxumResult> { let page = pagination.page.unwrap_or_default(); let per_page = min( pagination @@ -188,6 +192,7 @@ pub async fn mixnode_test_results( .unwrap_or(DEFAULT_TEST_RESULTS_PAGE_SIZE), MAX_TEST_RESULTS_PAGE_SIZE, ); + let output = pagination.output.unwrap_or_default(); match _mixnode_test_results( mix_id, @@ -198,7 +203,7 @@ pub async fn mixnode_test_results( ) .await { - Ok(res) => Ok(Json(res)), + Ok(res) => Ok(output.to_response(res)), Err(err) => Err(AxumErrorResponse::internal_msg(format!( "failed to retrieve mixnode test results for node {mix_id}: {err}" ))), @@ -272,14 +277,18 @@ async fn _gateway_test_results( ), path = "/v1/status/gateways/unstable/{identity}/test-results", responses( - (status = 200, body = GatewayTestResultResponse) - ) + (status = 200, content( + (GatewayTestResultResponse = "application/json"), + (GatewayTestResultResponse = "application/yaml"), + (GatewayTestResultResponse = "application/bincode") + )) + ), )] pub async fn gateway_test_results( Path(gateway_identity): Path, Query(pagination): Query, State(state): State, -) -> AxumResult> { +) -> AxumResult> { let page = pagination.page.unwrap_or_default(); let per_page = min( pagination @@ -287,6 +296,7 @@ pub async fn gateway_test_results( .unwrap_or(DEFAULT_TEST_RESULTS_PAGE_SIZE), MAX_TEST_RESULTS_PAGE_SIZE, ); + let output = pagination.output.unwrap_or_default(); match _gateway_test_results( &gateway_identity, @@ -297,7 +307,7 @@ pub async fn gateway_test_results( ) .await { - Ok(res) => Ok(Json(res)), + Ok(res) => Ok(output.to_response(res)), Err(err) => Err(AxumErrorResponse::internal_msg(format!( "failed to retrieve mixnode test results for gateway {gateway_identity}: {err}" ))), @@ -349,15 +359,23 @@ async fn _latest_monitor_run_report( get, path = "/v1/status/network-monitor/unstable/run/{monitor_run_id}/details", responses( - (status = 200, body = NetworkMonitorRunDetailsResponse) - ) + (status = 200, content( + (NetworkMonitorRunDetailsResponse = "application/json"), + (NetworkMonitorRunDetailsResponse = "application/yaml"), + (NetworkMonitorRunDetailsResponse = "application/bincode") + )) + ), + params(OutputParams) )] pub async fn monitor_run_report( Path(monitor_run_id): Path, + Query(output): Query, State(state): State, -) -> AxumResult> { +) -> AxumResult> { + let output = output.output.unwrap_or_default(); + match _monitor_run_report(monitor_run_id, state.storage()).await { - Ok(res) => Ok(Json(res)), + Ok(res) => Ok(output.to_response(res)), Err(err) => Err(AxumErrorResponse::internal_msg(format!( "failed to retrieve monitor run report for run {monitor_run_id}: {err}" ))), @@ -369,14 +387,22 @@ pub async fn monitor_run_report( get, path = "/v1/status/network-monitor/unstable/run/latest/details", responses( - (status = 200, body = NetworkMonitorRunDetailsResponse) - ) + (status = 200, content( + (NetworkMonitorRunDetailsResponse = "application/json"), + (NetworkMonitorRunDetailsResponse = "application/yaml"), + (NetworkMonitorRunDetailsResponse = "application/bincode") + )) + ), + params(OutputParams) )] pub async fn latest_monitor_run_report( + Query(output): Query, State(state): State, -) -> AxumResult> { +) -> AxumResult> { + let output = output.output.unwrap_or_default(); + match _latest_monitor_run_report(state.storage()).await { - Ok(res) => Ok(Json(res)), + Ok(res) => Ok(output.to_response(res)), Err(err) => Err(AxumErrorResponse::internal_msg(format!( "failed to retrieve the latest monitor run report: {err}" ))), diff --git a/nym-api/src/node_status_api/handlers/without_monitor.rs b/nym-api/src/node_status_api/handlers/without_monitor.rs index e364e1ee31..d66addae61 100644 --- a/nym-api/src/node_status_api/handlers/without_monitor.rs +++ b/nym-api/src/node_status_api/handlers/without_monitor.rs @@ -11,13 +11,14 @@ use crate::node_status_api::helpers::{ }; use crate::node_status_api::models::{AxumErrorResponse, AxumResult}; use crate::support::http::state::AppState; -use axum::extract::{Path, State}; +use axum::extract::{Path, Query, State}; use axum::routing::{get, post}; use axum::Json; use axum::Router; use nym_api_requests::models::{ MixNodeBondAnnotated, MixnodeStatusResponse, StakeSaturationResponse, }; +use nym_http_api_common::{FormattedResponse, OutputParams}; use nym_mixnet_contract_common::NodeId; use nym_types::monitoring::MonitorMessage; use tracing::error; @@ -153,15 +154,22 @@ pub(crate) async fn submit_node_monitoring_results( ), path = "/v1/status/mixnode/{mix_id}/status", responses( - (status = 200, body = MixnodeStatusResponse) - ) + (status = 200, content( + (MixnodeStatusResponse = "application/json"), + (MixnodeStatusResponse = "application/yaml"), + (MixnodeStatusResponse = "application/bincode") + )) + ), + params(OutputParams) )] #[deprecated] async fn get_mixnode_status( Path(MixIdParam { mix_id }): Path, + Query(output): Query, State(state): State, -) -> Json { - Json(_get_mixnode_status(state.nym_contract_cache(), mix_id).await) +) -> FormattedResponse { + let output = output.output.unwrap_or_default(); + output.to_response(_get_mixnode_status(state.nym_contract_cache(), mix_id).await) } #[utoipa::path( @@ -172,15 +180,23 @@ async fn get_mixnode_status( ), path = "/v1/status/mixnode/{mix_id}/stake-saturation", responses( - (status = 200, body = StakeSaturationResponse) - ) + (status = 200, content( + (StakeSaturationResponse = "application/json"), + (StakeSaturationResponse = "application/yaml"), + (StakeSaturationResponse = "application/bincode") + )) + ), + params(OutputParams) )] #[deprecated] async fn get_mixnode_stake_saturation( Path(mix_id): Path, + Query(output): Query, State(state): State, -) -> AxumResult> { - Ok(Json( +) -> AxumResult> { + let output = output.output.unwrap_or_default(); + + Ok(output.to_response( _get_mixnode_stake_saturation( state.node_status_cache(), state.nym_contract_cache(), @@ -194,22 +210,28 @@ async fn get_mixnode_stake_saturation( tag = "status", get, params( - MixIdParam + MixIdParam, OutputParams ), path = "/v1/status/mixnode/{mix_id}/inclusion-probability", responses( - (status = 200, body = nym_api_requests::models::InclusionProbabilityResponse) - ) + (status = 200, content( + (nym_api_requests::models::InclusionProbabilityResponse = "application/json"), + (nym_api_requests::models::InclusionProbabilityResponse = "application/yaml"), + (nym_api_requests::models::InclusionProbabilityResponse = "application/bincode") + )) + ), )] #[deprecated] #[allow(deprecated)] async fn get_mixnode_inclusion_probability( Path(mix_id): Path, + Query(output): Query, State(state): State, -) -> AxumResult> { - Ok(Json( - _get_mixnode_inclusion_probability(state.node_status_cache(), mix_id).await?, - )) +) -> AxumResult> { + let output = output.output.unwrap_or_default(); + + Ok(output + .to_response(_get_mixnode_inclusion_probability(state.node_status_cache(), mix_id).await?)) } #[utoipa::path( @@ -217,17 +239,23 @@ async fn get_mixnode_inclusion_probability( get, path = "/v1/status/mixnodes/inclusion-probability", responses( - (status = 200, body = nym_api_requests::models::AllInclusionProbabilitiesResponse) - ) + (status = 200, content( + (nym_api_requests::models::AllInclusionProbabilitiesResponse = "application/json"), + (nym_api_requests::models::AllInclusionProbabilitiesResponse = "application/yaml"), + (nym_api_requests::models::AllInclusionProbabilitiesResponse = "application/bincode") + )) + ), + params(OutputParams) )] #[deprecated] #[allow(deprecated)] async fn get_mixnode_inclusion_probabilities( + Query(output): Query, State(state): State, -) -> AxumResult> { - Ok(Json( - _get_mixnode_inclusion_probabilities(state.node_status_cache()).await?, - )) +) -> AxumResult> { + let output = output.output.unwrap_or_default(); + + Ok(output.to_response(_get_mixnode_inclusion_probabilities(state.node_status_cache()).await?)) } #[utoipa::path( @@ -235,14 +263,22 @@ async fn get_mixnode_inclusion_probabilities( get, path = "/v1/status/mixnodes/detailed", responses( - (status = 200, body = MixNodeBondAnnotated) - ) + (status = 200, content( + (MixNodeBondAnnotated = "application/json"), + (MixNodeBondAnnotated = "application/yaml"), + (MixNodeBondAnnotated = "application/bincode") + )) + ), + params(OutputParams) )] #[deprecated] pub async fn get_mixnodes_detailed( + Query(output): Query, State(state): State, -) -> Json> { - Json(_get_legacy_mixnodes_detailed(state.node_status_cache()).await) +) -> FormattedResponse> { + let output = output.output.unwrap_or_default(); + + output.to_response(_get_legacy_mixnodes_detailed(state.node_status_cache()).await) } #[utoipa::path( @@ -250,14 +286,22 @@ pub async fn get_mixnodes_detailed( get, path = "/v1/status/mixnodes/rewarded/detailed", responses( - (status = 200, body = MixNodeBondAnnotated) - ) + (status = 200, content( + (MixNodeBondAnnotated = "application/json"), + (MixNodeBondAnnotated = "application/yaml"), + (MixNodeBondAnnotated = "application/bincode") + )) + ), + params(OutputParams) )] #[deprecated] pub async fn get_rewarded_set_detailed( + Query(output): Query, State(state): State, -) -> Json> { - Json( +) -> FormattedResponse> { + let output = output.output.unwrap_or_default(); + + output.to_response( _get_rewarded_set_legacy_mixnodes_detailed( state.node_status_cache(), state.nym_contract_cache(), @@ -271,14 +315,22 @@ pub async fn get_rewarded_set_detailed( get, path = "/v1/status/mixnodes/active/detailed", responses( - (status = 200, body = MixNodeBondAnnotated) - ) + (status = 200, content( + (MixNodeBondAnnotated = "application/json"), + (MixNodeBondAnnotated = "application/yaml"), + (MixNodeBondAnnotated = "application/bincode") + )) + ), + params(OutputParams) )] #[deprecated] pub async fn get_active_set_detailed( + Query(output): Query, State(state): State, -) -> Json> { - Json( +) -> FormattedResponse> { + let output = output.output.unwrap_or_default(); + + output.to_response( _get_active_set_legacy_mixnodes_detailed( state.node_status_cache(), state.nym_contract_cache(), diff --git a/nym-api/src/nym_contract_cache/handlers.rs b/nym-api/src/nym_contract_cache/handlers.rs index 00f02e1fe2..c40a64d8c4 100644 --- a/nym-api/src/nym_contract_cache/handlers.rs +++ b/nym-api/src/nym_contract_cache/handlers.rs @@ -7,10 +7,11 @@ use crate::node_status_api::helpers::{ }; use crate::support::http::state::AppState; use crate::support::legacy_helpers::{to_legacy_gateway, to_legacy_mixnode}; -use axum::extract::State; -use axum::{Json, Router}; +use axum::extract::{Query, State}; +use axum::Router; use nym_api_requests::legacy::LegacyMixNodeDetailsWithLayer; use nym_api_requests::models::MixNodeBondAnnotated; +use nym_http_api_common::{FormattedResponse, OutputParams}; use nym_mixnet_contract_common::reward_params::Performance; use nym_mixnet_contract_common::{reward_params::RewardingParams, GatewayBond, Interval, NodeId}; use std::collections::HashSet; @@ -55,23 +56,32 @@ pub(crate) fn nym_contract_cache_routes() -> Router { get, path = "/v1/mixnodes", responses( - (status = 200, body = Vec) - ) + (status = 200, content( + (Vec = "application/json"), + (Vec = "application/yaml"), + (Vec = "application/bincode") + )) + ), + params(OutputParams) )] #[deprecated] -async fn get_mixnodes(State(state): State) -> Json> { +async fn get_mixnodes( + Query(output): Query, + State(state): State, +) -> FormattedResponse> { + let output = output.output.unwrap_or_default(); let mut out = state.nym_contract_cache().legacy_mixnodes_filtered().await; let Ok(describe_cache) = state.described_nodes_cache.get().await else { - return Json(out); + return output.to_response(out); }; let Some(migrated_nymnodes) = state.nym_contract_cache().all_cached_nym_nodes().await else { - return Json(out); + return output.to_response(out); }; let Ok(annotations) = state.node_annotations().await else { - return Json(out); + return output.to_response(out); }; // safety: valid percentage value @@ -100,7 +110,7 @@ async fn get_mixnodes(State(state): State) -> Json) -> Json) - ) + (status = 200, content( + (Vec = "application/json"), + (Vec = "application/yaml"), + (Vec = "application/bincode") + )) + ), + params(OutputParams) )] #[deprecated] -async fn get_mixnodes_detailed(State(state): State) -> Json> { - _get_legacy_mixnodes_detailed(state.node_status_cache()) - .await - .into() +async fn get_mixnodes_detailed( + Query(output): Query, + State(state): State, +) -> FormattedResponse> { + let output = output.output.unwrap_or_default(); + + output.to_response(_get_legacy_mixnodes_detailed(state.node_status_cache()).await) } #[utoipa::path( @@ -130,11 +148,21 @@ async fn get_mixnodes_detailed(State(state): State) -> Json) - ) + (status = 200, content( + (Vec = "application/json"), + (Vec = "application/yaml"), + (Vec = "application/bincode") + )) + ), + params(OutputParams) )] #[deprecated] -async fn get_gateways(State(state): State) -> Json> { +async fn get_gateways( + Query(output): Query, + State(state): State, +) -> FormattedResponse> { + let output = output.output.unwrap_or_default(); + // legacy let mut out: Vec = state .nym_contract_cache() @@ -145,15 +173,15 @@ async fn get_gateways(State(state): State) -> Json> { .collect(); let Ok(describe_cache) = state.described_nodes_cache.get().await else { - return Json(out); + return output.to_response(out); }; let Some(migrated_nymnodes) = state.nym_contract_cache().all_cached_nym_nodes().await else { - return Json(out); + return output.to_response(out); }; let Ok(annotations) = state.node_annotations().await else { - return Json(out); + return output.to_response(out); }; // safety: valid percentage value @@ -182,7 +210,7 @@ async fn get_gateways(State(state): State) -> Json> { out.push(node); } - Json(out) + output.to_response(out) } #[utoipa::path( @@ -190,14 +218,22 @@ async fn get_gateways(State(state): State) -> Json> { get, path = "/v1/mixnodes/rewarded", responses( - (status = 200, body = Vec) - ) + (status = 200, content( + (Vec = "application/json"), + (Vec = "application/yaml"), + (Vec = "application/bincode") + )) + ), + params(OutputParams) )] #[deprecated] async fn get_rewarded_set( + Query(output): Query, State(state): State, -) -> Json> { - Json( +) -> FormattedResponse> { + let output = output.output.unwrap_or_default(); + + output.to_response( state .nym_contract_cache() .legacy_v1_rewarded_set_mixnodes() @@ -218,19 +254,28 @@ async fn get_rewarded_set( get, path = "/v1/mixnodes/rewarded/detailed", responses( - (status = 200, body = Vec) - ) + (status = 200, content( + (Vec = "application/json"), + (Vec = "application/yaml"), + (Vec = "application/bincode") + )) + ), + params(OutputParams) )] #[deprecated] async fn get_rewarded_set_detailed( + Query(output): Query, State(state): State, -) -> Json> { - _get_rewarded_set_legacy_mixnodes_detailed( - state.node_status_cache(), - state.nym_contract_cache(), +) -> FormattedResponse> { + let output = output.output.unwrap_or_default(); + + output.to_response( + _get_rewarded_set_legacy_mixnodes_detailed( + state.node_status_cache(), + state.nym_contract_cache(), + ) + .await, ) - .await - .into() } #[utoipa::path( @@ -238,11 +283,21 @@ async fn get_rewarded_set_detailed( get, path = "/v1/mixnodes/active", responses( - (status = 200, body = Vec) - ) + (status = 200, content( + (Vec = "application/json"), + (Vec = "application/yaml"), + (Vec = "application/bincode") + )) + ), + params(OutputParams) )] #[deprecated] -async fn get_active_set(State(state): State) -> Json> { +async fn get_active_set( + Query(output): Query, + State(state): State, +) -> FormattedResponse> { + let output = output.output.unwrap_or_default(); + let mut out = state .nym_contract_cache() .legacy_v1_active_set_mixnodes() @@ -250,19 +305,19 @@ async fn get_active_set(State(state): State) -> Json) -> Json) -> Json) - ) + (status = 200, content( + (Vec = "application/json"), + (Vec = "application/yaml"), + (Vec = "application/bincode") + )) + ), + params(OutputParams) )] #[deprecated] -async fn get_active_set_detailed(State(state): State) -> Json> { - _get_active_set_legacy_mixnodes_detailed(state.node_status_cache(), state.nym_contract_cache()) - .await - .into() +async fn get_active_set_detailed( + Query(output): Query, + State(state): State, +) -> FormattedResponse> { + let output = output.output.unwrap_or_default(); + + output.to_response( + _get_active_set_legacy_mixnodes_detailed( + state.node_status_cache(), + state.nym_contract_cache(), + ) + .await, + ) } #[utoipa::path( @@ -326,22 +395,31 @@ async fn get_active_set_detailed(State(state): State) -> Json>) - ) + (status = 200, content( + (Option> = "application/json"), + (Option> = "application/yaml"), + (Option> = "application/bincode") + )) + ), + params(OutputParams) )] #[deprecated] -async fn get_blacklisted_mixnodes(State(state): State) -> Json>> { +async fn get_blacklisted_mixnodes( + Query(output): Query, + State(state): State, +) -> FormattedResponse>> { + let output = output.output.unwrap_or_default(); + let blacklist = state .nym_contract_cache() .mixnodes_blacklist() .await .to_owned(); if blacklist.is_empty() { - None + output.to_response(None) } else { - Some(blacklist) + output.to_response(Some(blacklist)) } - .into() } #[utoipa::path( @@ -349,18 +427,28 @@ async fn get_blacklisted_mixnodes(State(state): State) -> Json>) - ) + (status = 200, content( + (Option> = "application/json"), + (Option> = "application/yaml"), + (Option> = "application/bincode") + )) + ), + params(OutputParams) )] #[deprecated] -async fn get_blacklisted_gateways(State(state): State) -> Json>> { +async fn get_blacklisted_gateways( + Query(output): Query, + State(state): State, +) -> FormattedResponse>> { + let output = output.output.unwrap_or_default(); + let cache = state.nym_contract_cache(); let blacklist = cache.gateways_blacklist().await.clone(); if blacklist.is_empty() { - Json(None) + output.to_response(None) } else { let gateways = cache.legacy_gateways_all().await; - Json(Some( + output.to_response(Some( gateways .into_iter() .filter(|g| blacklist.contains(&g.node_id)) @@ -375,18 +463,27 @@ async fn get_blacklisted_gateways(State(state): State) -> Json) - ) + (status = 200, content( + (Option = "application/json"), + (Option = "application/yaml"), + (Option = "application/bincode") + )) + ), + params(OutputParams) )] async fn get_interval_reward_params( + Query(output): Query, State(state): State, -) -> Json> { - state - .nym_contract_cache() - .interval_reward_params() - .await - .to_owned() - .into() +) -> FormattedResponse> { + let output = output.output.unwrap_or_default(); + + output.to_response( + state + .nym_contract_cache() + .interval_reward_params() + .await + .to_owned(), + ) } #[utoipa::path( @@ -394,14 +491,25 @@ async fn get_interval_reward_params( get, path = "/v1/epoch/current", responses( - (status = 200, body = Option) - ) + (status = 200, content( + (Option = "application/json"), + (Option = "application/yaml"), + (Option = "application/bincode") + )) + ), + params(OutputParams) )] -async fn get_current_epoch(State(state): State) -> Json> { - state - .nym_contract_cache() - .current_interval() - .await - .to_owned() - .into() +async fn get_current_epoch( + Query(output): Query, + State(state): State, +) -> FormattedResponse> { + let output = output.output.unwrap_or_default(); + + output.to_response( + state + .nym_contract_cache() + .current_interval() + .await + .to_owned(), + ) } diff --git a/nym-api/src/nym_nodes/handlers/legacy.rs b/nym-api/src/nym_nodes/handlers/legacy.rs index adb96f2f76..5910c758c9 100644 --- a/nym-api/src/nym_nodes/handlers/legacy.rs +++ b/nym-api/src/nym_nodes/handlers/legacy.rs @@ -3,10 +3,11 @@ use crate::support::http::state::AppState; use crate::support::legacy_helpers::{to_legacy_gateway, to_legacy_mixnode}; -use axum::extract::State; -use axum::{Json, Router}; +use axum::extract::{Query, State}; +use axum::Router; use nym_api_requests::legacy::LegacyMixNodeBondWithLayer; use nym_api_requests::models::{LegacyDescribedGateway, LegacyDescribedMixNode}; +use nym_http_api_common::{FormattedResponse, OutputParams}; use tower_http::compression::CompressionLayer; // we want to mark the routes as deprecated in swagger, but still expose them @@ -29,22 +30,29 @@ pub(crate) fn legacy_nym_node_routes() -> Router { get, path = "/v1/gateways/described", responses( - (status = 200, body = Vec) - ) + (status = 200, content( + (Vec = "application/json"), + (Vec = "application/yaml"), + (Vec = "application/bincode") + )) + ), + params(OutputParams) )] #[deprecated] async fn get_gateways_described( + Query(output): Query, State(state): State, -) -> Json> { +) -> FormattedResponse> { let contract_cache = state.nym_contract_cache(); let describe_cache = state.described_nodes_cache(); + let output = output.output.unwrap_or_default(); // legacy let legacy = contract_cache.legacy_gateways_filtered().await; // if the self describe cache is unavailable, well, don't attach describe data and only return legacy gateways let Ok(describe_cache) = describe_cache.get().await else { - return Json(legacy.into_iter().map(Into::into).collect()); + return output.to_response(legacy.into_iter().map(Into::into).collect()); }; let migrated_nymnodes = state.nym_contract_cache().nym_nodes().await; @@ -75,7 +83,7 @@ async fn get_gateways_described( }) } - Json(out) + output.to_response(out) } #[utoipa::path( @@ -83,15 +91,22 @@ async fn get_gateways_described( get, path = "/v1/mixnodes/described", responses( - (status = 200, body = Vec) - ) + (status = 200, content( + (Vec = "application/json"), + (Vec = "application/yaml"), + (Vec = "application/bincode") + )) + ), + params(OutputParams) )] #[deprecated] async fn get_mixnodes_described( + Query(output): Query, State(state): State, -) -> Json> { +) -> FormattedResponse> { let contract_cache = state.nym_contract_cache(); let describe_cache = state.described_nodes_cache(); + let output = output.output.unwrap_or_default(); let legacy: Vec = contract_cache .legacy_mixnodes_filtered() @@ -102,7 +117,7 @@ async fn get_mixnodes_described( // if the self describe cache is unavailable, well, don't attach describe data and only return legacy mixnodes let Ok(describe_cache) = describe_cache.get().await else { - return Json(legacy.into_iter().map(Into::into).collect()); + return output.to_response(legacy.into_iter().map(Into::into).collect()); }; let migrated_nymnodes = state.nym_contract_cache().nym_nodes().await; @@ -131,5 +146,5 @@ async fn get_mixnodes_described( }) } - Json(out) + output.to_response(out) } diff --git a/nym-api/src/nym_nodes/handlers/mod.rs b/nym-api/src/nym_nodes/handlers/mod.rs index da6ef82c7b..bd9dd674d6 100644 --- a/nym-api/src/nym_nodes/handlers/mod.rs +++ b/nym-api/src/nym_nodes/handlers/mod.rs @@ -15,9 +15,9 @@ use nym_api_requests::models::{ }; use nym_api_requests::pagination::{PaginatedResponse, Pagination}; use nym_contracts_common::NaiveFloat; +use nym_http_api_common::{FormattedResponse, Output, OutputParams}; use nym_mixnet_contract_common::reward_params::Performance; use nym_mixnet_contract_common::NymNodeDetails; -use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::time::Duration; use time::{Date, OffsetDateTime}; @@ -55,10 +55,20 @@ pub(crate) fn nym_node_routes() -> Router { path = "/rewarded-set", context_path = "/v1/nym-nodes", responses( - (status = 200, body = RewardedSetResponse) + (status = 200, content( + (RewardedSetResponse = "application/json"), + (RewardedSetResponse = "application/yaml"), + (RewardedSetResponse = "application/bincode") + )) ), + params(OutputParams) )] -async fn rewarded_set(State(state): State) -> AxumResult> { +async fn rewarded_set( + Query(output): Query, + State(state): State, +) -> AxumResult> { + let output = output.output.unwrap_or_default(); + let cached_rewarded_set = state .nym_contract_cache() .rewarded_set() @@ -67,7 +77,7 @@ async fn rewarded_set(State(state): State) -> AxumResult) + (status = 200, content( + (PaginatedResponse = "application/json"), + (PaginatedResponse = "application/yaml"), + (PaginatedResponse = "application/bincode") + )) ), params(PaginationRequest) )] async fn nodes_noise( State(state): State, Query(pagination): Query, -) -> AxumResult>> { +) -> AxumResult>> { // TODO: implement it let _ = pagination; + let output = pagination.output.unwrap_or_default(); let describe_cache = state.describe_nodes_cache_data().await?; @@ -167,7 +182,7 @@ async fn nodes_noise( let total = nodes.len(); - Ok(Json(PaginatedResponse { + Ok(output.to_response(PaginatedResponse { pagination: Pagination { total, page: 0, @@ -183,21 +198,26 @@ async fn nodes_noise( path = "/bonded", context_path = "/v1/nym-nodes", responses( - (status = 200, body = PaginatedResponse) + (status = 200, content( + (PaginatedResponse = "application/json"), + (PaginatedResponse = "application/yaml"), + (PaginatedResponse = "application/bincode") + )) ), params(PaginationRequest) )] async fn get_bonded_nodes( State(state): State, Query(pagination): Query, -) -> Json> { +) -> FormattedResponse> { // TODO: implement it let _ = pagination; + let output = pagination.output.unwrap_or_default(); let details = state.nym_contract_cache().nym_nodes().await; let total = details.len(); - Json(PaginatedResponse { + output.to_response(PaginatedResponse { pagination: Pagination { total, page: 0, @@ -213,21 +233,26 @@ async fn get_bonded_nodes( path = "/described", context_path = "/v1/nym-nodes", responses( - (status = 200, body = PaginatedResponse) + (status = 200, content( + (PaginatedResponse = "application/json"), + (PaginatedResponse = "application/yaml"), + (PaginatedResponse = "application/bincode") + )) ), params(PaginationRequest) )] async fn get_described_nodes( State(state): State, Query(pagination): Query, -) -> AxumResult>> { +) -> AxumResult>> { // TODO: implement it let _ = pagination; + let output = pagination.output.unwrap_or_default(); let cache = state.described_nodes_cache.get().await?; let descriptions = cache.all_nodes().cloned().collect::>(); - Ok(Json(PaginatedResponse { + Ok(output.to_response(PaginatedResponse { pagination: Pagination { total: descriptions.len(), page: 0, @@ -243,21 +268,28 @@ async fn get_described_nodes( path = "/annotation/{node_id}", context_path = "/v1/nym-nodes", responses( - (status = 200, body = AnnotationResponse) + (status = 200, content( + (AnnotationResponse = "application/json"), + (AnnotationResponse = "application/yaml"), + (AnnotationResponse = "application/bincode") + )) ), - params(NodeIdParam), + params(NodeIdParam, OutputParams), )] async fn get_node_annotation( Path(NodeIdParam { node_id }): Path, + Query(output): Query, State(state): State, -) -> AxumResult> { +) -> AxumResult> { + let output = output.output.unwrap_or_default(); + let annotations = state .node_status_cache .node_annotations() .await .ok_or_else(AxumErrorResponse::internal)?; - Ok(Json(AnnotationResponse { + Ok(output.to_response(AnnotationResponse { node_id, annotation: annotations.get(&node_id).cloned(), })) @@ -269,21 +301,28 @@ async fn get_node_annotation( path = "/performance/{node_id}", context_path = "/v1/nym-nodes", responses( - (status = 200, body = NodePerformanceResponse) + (status = 200, content( + (NodePerformanceResponse = "application/json"), + (NodePerformanceResponse = "application/yaml"), + (NodePerformanceResponse = "application/bincode") + )) ), - params(NodeIdParam), + params(NodeIdParam, OutputParams), )] async fn get_current_node_performance( Path(NodeIdParam { node_id }): Path, + Query(output): Query, State(state): State, -) -> AxumResult> { +) -> AxumResult> { + let output = output.output.unwrap_or_default(); + let annotations = state .node_status_cache .node_annotations() .await .ok_or_else(AxumErrorResponse::internal)?; - Ok(Json(NodePerformanceResponse { + Ok(output.to_response(NodePerformanceResponse { node_id, performance: annotations .get(&node_id) @@ -292,12 +331,13 @@ async fn get_current_node_performance( } // todo; probably extract it to requests crate -#[derive(Debug, Serialize, Deserialize, Copy, Clone, IntoParams, ToSchema, JsonSchema)] +#[derive(Debug, Serialize, Deserialize, Copy, Clone, IntoParams, ToSchema)] #[into_params(parameter_in = Query)] pub(crate) struct DateQuery { #[schema(value_type = String, example = "1970-01-01")] - #[schemars(with = "String")] pub(crate) date: Date, + + pub(crate) output: Option, } #[utoipa::path( @@ -306,21 +346,27 @@ pub(crate) struct DateQuery { path = "/historical-performance/{node_id}", context_path = "/v1/nym-nodes", responses( - (status = 200, body = NodeDatePerformanceResponse) + (status = 200, content( + (NodeDatePerformanceResponse = "application/json"), + (NodeDatePerformanceResponse = "application/yaml"), + (NodeDatePerformanceResponse = "application/bincode") + )) ), params(DateQuery, NodeIdParam) )] async fn get_historical_performance( Path(NodeIdParam { node_id }): Path, - Query(DateQuery { date }): Query, + Query(DateQuery { date, output }): Query, State(state): State, -) -> AxumResult> { +) -> AxumResult> { + let output = output.unwrap_or_default(); + let uptime = state .storage() .get_historical_node_uptime_on(node_id, date) .await?; - Ok(Json(NodeDatePerformanceResponse { + Ok(output.to_response(NodeDatePerformanceResponse { node_id, date, performance: uptime.and_then(|u| { @@ -337,7 +383,11 @@ async fn get_historical_performance( path = "/performance-history/{node_id}", context_path = "/v1/nym-nodes", responses( - (status = 200, body = PerformanceHistoryResponse) + (status = 200, content( + (PerformanceHistoryResponse = "application/json"), + (PerformanceHistoryResponse = "application/yaml"), + (PerformanceHistoryResponse = "application/bincode") + )) ), params(PaginationRequest, NodeIdParam) )] @@ -345,9 +395,10 @@ async fn get_node_performance_history( Path(NodeIdParam { node_id }): Path, State(state): State, Query(pagination): Query, -) -> AxumResult> { +) -> AxumResult> { // TODO: implement it let _ = pagination; + let output = pagination.output.unwrap_or_default(); let history = state .storage() @@ -358,7 +409,7 @@ async fn get_node_performance_history( .collect::>(); let total = history.len(); - Ok(Json(PerformanceHistoryResponse { + Ok(output.to_response(PerformanceHistoryResponse { node_id, history: PaginatedResponse { pagination: Pagination { @@ -377,7 +428,11 @@ async fn get_node_performance_history( path = "/uptime-history/{node_id}", context_path = "/v1/nym-nodes", responses( - (status = 200, body = PerformanceHistoryResponse) + (status = 200, content( + (PerformanceHistoryResponse = "application/json"), + (PerformanceHistoryResponse = "application/yaml"), + (PerformanceHistoryResponse = "application/bincode") + )) ), params(PaginationRequest, NodeIdParam) )] @@ -385,9 +440,10 @@ async fn get_node_uptime_history( Path(NodeIdParam { node_id }): Path, State(state): State, Query(pagination): Query, -) -> AxumResult> { +) -> AxumResult> { // TODO: implement it let _ = pagination; + let output = pagination.output.unwrap_or_default(); let history = state .storage() @@ -398,7 +454,7 @@ async fn get_node_uptime_history( .collect::>(); let total = history.len(); - Ok(Json(UptimeHistoryResponse { + Ok(output.to_response(UptimeHistoryResponse { node_id, history: PaginatedResponse { pagination: Pagination { diff --git a/nym-api/src/nym_nodes/handlers/unstable/full_fat.rs b/nym-api/src/nym_nodes/handlers/unstable/full_fat.rs index debc284208..2937b0377d 100644 --- a/nym-api/src/nym_nodes/handlers/unstable/full_fat.rs +++ b/nym-api/src/nym_nodes/handlers/unstable/full_fat.rs @@ -5,8 +5,8 @@ use crate::node_status_api::models::{AxumErrorResponse, AxumResult}; use crate::nym_nodes::handlers::unstable::NodesParamsWithRole; use crate::support::http::state::AppState; use axum::extract::{Query, State}; -use axum::Json; use nym_api_requests::nym_nodes::{CachedNodesResponse, FullFatNode}; +use nym_http_api_common::FormattedResponse; #[utoipa::path( tag = "Unstable Nym Nodes", @@ -22,6 +22,6 @@ use nym_api_requests::nym_nodes::{CachedNodesResponse, FullFatNode}; pub(super) async fn nodes_detailed( _state: State, _query_params: Query, -) -> AxumResult>> { +) -> AxumResult>> { Err(AxumErrorResponse::not_implemented()) } diff --git a/nym-api/src/nym_nodes/handlers/unstable/mod.rs b/nym-api/src/nym_nodes/handlers/unstable/mod.rs index 79a88cee29..92d695ddfc 100644 --- a/nym-api/src/nym_nodes/handlers/unstable/mod.rs +++ b/nym-api/src/nym_nodes/handlers/unstable/mod.rs @@ -30,12 +30,13 @@ use crate::nym_nodes::handlers::unstable::skimmed::{ }; use crate::support::http::helpers::PaginationRequest; use crate::support::http::state::AppState; -use axum::extract::State; +use axum::extract::{Query, State}; use axum::routing::{get, post}; use axum::{Json, Router}; use nym_api_requests::nym_nodes::{ NodeRoleQueryParam, NodesByAddressesRequestBody, NodesByAddressesResponse, }; +use nym_http_api_common::{FormattedResponse, Output, OutputParams}; use serde::Deserialize; use std::collections::HashMap; use tower_http::compression::CompressionLayer; @@ -98,6 +99,8 @@ struct NodesParamsWithRole { // the client already knows about the latest topology state, allowing a `no-updates` response // instead of wasting bandwidth serving an unchanged topology. epoch_id: Option, + + output: Option, } #[derive(Debug, Deserialize, utoipa::IntoParams)] @@ -113,6 +116,7 @@ struct NodesParams { // the client already knows about the latest topology state, allowing a `no-updates` response // instead of wasting bandwidth serving an unchanged topology. epoch_id: Option, + output: Option, } impl From for NodesParams { @@ -123,6 +127,7 @@ impl From for NodesParams { page: params.page, per_page: params.per_page, epoch_id: params.epoch_id, + output: params.output, } } } @@ -130,6 +135,7 @@ impl From for NodesParams { impl<'a> From<&'a NodesParams> for PaginationRequest { fn from(params: &'a NodesParams) -> Self { PaginationRequest { + output: params.output, page: params.page, per_page: params.per_page, } @@ -143,13 +149,19 @@ impl<'a> From<&'a NodesParams> for PaginationRequest { path = "/by-addresses", context_path = "/v1/unstable/nym-nodes", responses( - (status = 200, body = NodesByAddressesResponse) - ) + (status = 200, content( + (NodesByAddressesResponse = "application/json"), + (NodesByAddressesResponse = "application/yaml"), + (NodesByAddressesResponse = "application/bincode") + )) + ), + params(OutputParams) )] async fn nodes_by_addresses( + Query(output): Query, state: State, Json(body): Json, -) -> AxumResult> { +) -> AxumResult> { // if the request is too big, simply reject it if body.addresses.len() > 100 { return Err(AxumErrorResponse::bad_request( @@ -157,6 +169,8 @@ async fn nodes_by_addresses( )); } + let output = output.output.unwrap_or_default(); + // TODO: perhaps introduce different cache because realistically nym-api will receive // request for the same couple addresses from all nodes in quick succession let describe_cache = state.describe_nodes_cache_data().await?; @@ -166,5 +180,5 @@ async fn nodes_by_addresses( existence.insert(address, describe_cache.node_with_address(address)); } - Ok(Json(NodesByAddressesResponse { existence })) + Ok(output.to_response(NodesByAddressesResponse { existence })) } diff --git a/nym-api/src/nym_nodes/handlers/unstable/semi_skimmed.rs b/nym-api/src/nym_nodes/handlers/unstable/semi_skimmed.rs index e8b9ffb2bc..f7dff11e3e 100644 --- a/nym-api/src/nym_nodes/handlers/unstable/semi_skimmed.rs +++ b/nym-api/src/nym_nodes/handlers/unstable/semi_skimmed.rs @@ -5,8 +5,8 @@ use crate::node_status_api::models::{AxumErrorResponse, AxumResult}; use crate::nym_nodes::handlers::unstable::NodesParamsWithRole; use crate::support::http::state::AppState; use axum::extract::{Query, State}; -use axum::Json; use nym_api_requests::nym_nodes::{CachedNodesResponse, SemiSkimmedNode}; +use nym_http_api_common::FormattedResponse; #[utoipa::path( tag = "Unstable Nym Nodes", @@ -22,6 +22,6 @@ use nym_api_requests::nym_nodes::{CachedNodesResponse, SemiSkimmedNode}; pub(super) async fn nodes_expanded( _state: State, _query_params: Query, -) -> AxumResult>> { +) -> AxumResult>> { Err(AxumErrorResponse::not_implemented()) } diff --git a/nym-api/src/nym_nodes/handlers/unstable/skimmed.rs b/nym-api/src/nym_nodes/handlers/unstable/skimmed.rs index 8eb5388dea..f64e937000 100644 --- a/nym-api/src/nym_nodes/handlers/unstable/skimmed.rs +++ b/nym-api/src/nym_nodes/handlers/unstable/skimmed.rs @@ -8,7 +8,6 @@ use crate::nym_nodes::handlers::unstable::{NodesParams, NodesParamsWithRole}; use crate::support::caching::Cache; use crate::support::http::state::AppState; use axum::extract::{Query, State}; -use axum::Json; use nym_api_requests::models::{ NodeAnnotation, NymNodeDescription, OffsetDateTimeJsonSchemaWrapper, }; @@ -16,6 +15,7 @@ use nym_api_requests::nym_nodes::{ CachedNodesResponse, NodeRole, NodeRoleQueryParam, PaginatedCachedNodesResponse, SkimmedNode, }; use nym_api_requests::pagination::PaginatedResponse; +use nym_http_api_common::{FormattedResponse, Output}; use nym_mixnet_contract_common::NodeId; use nym_topology::CachedEpochRewardedSet; use std::collections::HashMap; @@ -24,7 +24,8 @@ use tokio::sync::RwLockReadGuard; use tracing::trace; use utoipa::ToSchema; -pub type PaginatedSkimmedNodes = AxumResult>>; +pub type PaginatedSkimmedNodes = + AxumResult>>; /// Given all relevant caches, build part of response for JUST Nym Nodes fn build_nym_nodes_response<'a, NI>( @@ -97,6 +98,7 @@ async fn build_skimmed_nodes_response<'a, NI, LG, Fut, LN>( nym_nodes_subset: NI, annotated_legacy_nodes_getter: LG, active_only: bool, + output: Output, ) -> PaginatedSkimmedNodes where // iterator returning relevant subset of nym-nodes (like mixing nym-nodes, entries, etc.) @@ -134,7 +136,7 @@ where if let Some(client_known_epoch) = query_params.epoch_id { if let Some(ref interval) = maybe_interval { if client_known_epoch == interval.current_epoch_id() { - return Ok(Json(PaginatedCachedNodesResponse::no_updates())); + return Ok(output.to_response(PaginatedCachedNodesResponse::no_updates())); } } } @@ -152,7 +154,7 @@ where describe_cache.timestamp(), ]); - return Ok(Json( + return Ok(output.to_response( PaginatedCachedNodesResponse::new_full(refreshed_at, nodes).fresh(maybe_interval), )); } @@ -176,7 +178,7 @@ where annotated_legacy_nodes.timestamp(), ]); - Ok(Json( + Ok(output.to_response( PaginatedCachedNodesResponse::new_full(refreshed_at, nodes).fresh(maybe_interval), )) } @@ -189,22 +191,30 @@ where path = "/gateways/skimmed", context_path = "/v1/unstable/nym-nodes", responses( - (status = 200, body = CachedNodesResponse) - ) + (status = 200, content( + (CachedNodesResponse = "application/json"), + (CachedNodesResponse = "application/yaml"), + (CachedNodesResponse = "application/bincode") + )) + ), )] #[deprecated(note = "use '/v1/unstable/nym-nodes/entry-gateways/skimmed/all' instead")] pub(super) async fn deprecated_gateways_basic( state: State, query_params: Query, -) -> AxumResult>> { +) -> AxumResult>> { + let output = query_params.output.unwrap_or_default(); + // 1. call '/v1/unstable/skimmed/entry-gateways/all' - let all_gateways = entry_gateways_basic_all(state, query_params).await?; + let all_gateways = entry_gateways_basic_all(state, query_params) + .await? + .into_inner(); // 3. return result - Ok(Json(CachedNodesResponse { + Ok(output.to_response(CachedNodesResponse { refreshed_at: all_gateways.refreshed_at, // 2. remove pagination - nodes: all_gateways.0.nodes.data, + nodes: all_gateways.nodes.data, })) } @@ -216,30 +226,40 @@ pub(super) async fn deprecated_gateways_basic( path = "/mixnodes/skimmed", context_path = "/v1/unstable/nym-nodes", responses( - (status = 200, body = CachedNodesResponse) - ) + (status = 200, content( + (CachedNodesResponse = "application/json"), + (CachedNodesResponse = "application/yaml"), + (CachedNodesResponse = "application/bincode") + )) + ), )] #[deprecated(note = "use '/v1/unstable/nym-nodes/skimmed/mixnodes/active' instead")] pub(super) async fn deprecated_mixnodes_basic( state: State, query_params: Query, -) -> AxumResult>> { +) -> AxumResult>> { + let output = query_params.output.unwrap_or_default(); + // 1. call '/v1/unstable/nym-nodes/skimmed/mixnodes/active' - let active_mixnodes = mixnodes_basic_active(state, query_params).await?; + let active_mixnodes = mixnodes_basic_active(state, query_params) + .await? + .into_inner(); // 3. return result - Ok(Json(CachedNodesResponse { + Ok(output.to_response(CachedNodesResponse { refreshed_at: active_mixnodes.refreshed_at, // 2. remove pagination - nodes: active_mixnodes.0.nodes.data, + nodes: active_mixnodes.nodes.data, })) } async fn nodes_basic( state: State, - Query(_query_params): Query, + Query(query_params): Query, active_only: bool, ) -> PaginatedSkimmedNodes { + let output = query_params.output.unwrap_or_default(); + // unfortunately we have to build the response semi-manually here as we need to add two sources of legacy nodes // 1. grab all relevant described nym-nodes @@ -281,10 +301,7 @@ async fn nodes_basic( legacy_gateways.timestamp(), ]); - Ok(Json(PaginatedCachedNodesResponse::new_full( - refreshed_at, - nodes, - ))) + Ok(output.to_response(PaginatedCachedNodesResponse::new_full(refreshed_at, nodes))) } #[allow(dead_code)] // not dead, used in OpenAPI docs @@ -305,8 +322,12 @@ pub struct PaginatedCachedNodesResponseSchema { path = "", context_path = "/v1/unstable/nym-nodes/skimmed", responses( - (status = 200, body = PaginatedCachedNodesResponseSchema) - ) + (status = 200, content( + (PaginatedCachedNodesResponseSchema = "application/json"), + (PaginatedCachedNodesResponseSchema = "application/yaml"), + (PaginatedCachedNodesResponseSchema = "application/bincode") + )) + ), )] pub(super) async fn nodes_basic_all( state: State, @@ -338,8 +359,12 @@ pub(super) async fn nodes_basic_all( path = "/active", context_path = "/v1/unstable/nym-nodes/skimmed", responses( - (status = 200, body = PaginatedCachedNodesResponseSchema) - ) + (status = 200, content( + (PaginatedCachedNodesResponseSchema = "application/json"), + (PaginatedCachedNodesResponseSchema = "application/yaml"), + (PaginatedCachedNodesResponseSchema = "application/bincode") + )) + ), )] pub(super) async fn nodes_basic_active( state: State, @@ -367,6 +392,8 @@ async fn mixnodes_basic( query_params: Query, active_only: bool, ) -> PaginatedSkimmedNodes { + let output = query_params.output.unwrap_or_default(); + // 1. grab all relevant described nym-nodes let describe_cache = state.describe_nodes_cache_data().await?; let mixing_nym_nodes = describe_cache.mixing_nym_nodes(); @@ -377,6 +404,7 @@ async fn mixnodes_basic( mixing_nym_nodes, |state| state.legacy_mixnode_annotations(), active_only, + output, ) .await } @@ -390,8 +418,12 @@ async fn mixnodes_basic( path = "/mixnodes/all", context_path = "/v1/unstable/nym-nodes/skimmed", responses( - (status = 200, body = PaginatedCachedNodesResponseSchema) - ) + (status = 200, content( + (PaginatedCachedNodesResponseSchema = "application/json"), + (PaginatedCachedNodesResponseSchema = "application/yaml"), + (PaginatedCachedNodesResponseSchema = "application/bincode") + )) + ), )] pub(super) async fn mixnodes_basic_all( state: State, @@ -409,8 +441,12 @@ pub(super) async fn mixnodes_basic_all( path = "/mixnodes/active", context_path = "/v1/unstable/nym-nodes/skimmed", responses( - (status = 200, body = PaginatedCachedNodesResponseSchema) - ) + (status = 200, content( + (PaginatedCachedNodesResponseSchema = "application/json"), + (PaginatedCachedNodesResponseSchema = "application/yaml"), + (PaginatedCachedNodesResponseSchema = "application/bincode") + )) + ), )] pub(super) async fn mixnodes_basic_active( state: State, @@ -424,6 +460,8 @@ async fn entry_gateways_basic( query_params: Query, active_only: bool, ) -> PaginatedSkimmedNodes { + let output = query_params.output.unwrap_or_default(); + // 1. grab all relevant described nym-nodes let describe_cache = state.describe_nodes_cache_data().await?; let mixing_nym_nodes = describe_cache.entry_capable_nym_nodes(); @@ -434,6 +472,7 @@ async fn entry_gateways_basic( mixing_nym_nodes, |state| state.legacy_gateways_annotations(), active_only, + output, ) .await } @@ -447,8 +486,12 @@ async fn entry_gateways_basic( path = "/entry-gateways/active", context_path = "/v1/unstable/nym-nodes/skimmed", responses( - (status = 200, body = PaginatedCachedNodesResponseSchema) - ) + (status = 200, content( + (PaginatedCachedNodesResponseSchema = "application/json"), + (PaginatedCachedNodesResponseSchema = "application/yaml"), + (PaginatedCachedNodesResponseSchema = "application/bincode") + )) + ), )] pub(super) async fn entry_gateways_basic_active( state: State, @@ -466,8 +509,12 @@ pub(super) async fn entry_gateways_basic_active( path = "/entry-gateways/all", context_path = "/v1/unstable/nym-nodes/skimmed", responses( - (status = 200, body = PaginatedCachedNodesResponseSchema) - ) + (status = 200, content( + (PaginatedCachedNodesResponseSchema = "application/json"), + (PaginatedCachedNodesResponseSchema = "application/yaml"), + (PaginatedCachedNodesResponseSchema = "application/bincode") + )) + ), )] pub(super) async fn entry_gateways_basic_all( state: State, @@ -481,6 +528,8 @@ async fn exit_gateways_basic( query_params: Query, active_only: bool, ) -> PaginatedSkimmedNodes { + let output = query_params.output.unwrap_or_default(); + // 1. grab all relevant described nym-nodes let describe_cache = state.describe_nodes_cache_data().await?; let mixing_nym_nodes = describe_cache.exit_capable_nym_nodes(); @@ -491,6 +540,7 @@ async fn exit_gateways_basic( mixing_nym_nodes, |state| state.legacy_gateways_annotations(), active_only, + output, ) .await } @@ -504,8 +554,12 @@ async fn exit_gateways_basic( path = "/exit-gateways/active", context_path = "/v1/unstable/nym-nodes/skimmed", responses( - (status = 200, body = PaginatedCachedNodesResponseSchema) - ) + (status = 200, content( + (PaginatedCachedNodesResponseSchema = "application/json"), + (PaginatedCachedNodesResponseSchema = "application/yaml"), + (PaginatedCachedNodesResponseSchema = "application/bincode") + )) + ), )] pub(super) async fn exit_gateways_basic_active( state: State, @@ -523,8 +577,12 @@ pub(super) async fn exit_gateways_basic_active( path = "/exit-gateways/all", context_path = "/v1/unstable/nym-nodes/skimmed", responses( - (status = 200, body = PaginatedCachedNodesResponseSchema) - ) + (status = 200, content( + (PaginatedCachedNodesResponseSchema = "application/json"), + (PaginatedCachedNodesResponseSchema = "application/yaml"), + (PaginatedCachedNodesResponseSchema = "application/bincode") + )) + ), )] pub(super) async fn exit_gateways_basic_all( state: State, diff --git a/nym-api/src/status/handlers.rs b/nym-api/src/status/handlers.rs index 1a280eb5df..6745a7934e 100644 --- a/nym-api/src/status/handlers.rs +++ b/nym-api/src/status/handlers.rs @@ -4,14 +4,14 @@ use crate::node_status_api::models::{AxumErrorResponse, AxumResult}; use crate::status::ApiStatusState; use crate::support::http::state::AppState; -use axum::extract::State; -use axum::Json; +use axum::extract::{Query, State}; use axum::Router; use nym_api_requests::models::{ ApiHealthResponse, ApiStatus, ChainStatus, SignerInformationResponse, }; use nym_bin_common::build_information::BinaryBuildInformationOwned; use nym_compact_ecash::Base58; +use nym_http_api_common::{FormattedResponse, OutputParams}; use std::time::Duration; use time::OffsetDateTime; @@ -30,12 +30,22 @@ pub(crate) fn api_status_routes() -> Router { get, path = "/v1/api-status/health", responses( - (status = 200, body = ApiHealthResponse) - ) + (status = 200, content( + (ApiHealthResponse = "application/json"), + (ApiHealthResponse = "application/yaml"), + (ApiHealthResponse = "application/bincode") + )) + ), + params(OutputParams) )] -async fn health(State(state): State) -> Json { +async fn health( + Query(output): Query, + State(state): State, +) -> FormattedResponse { const CHAIN_STALL_THRESHOLD: Duration = Duration::from_secs(5 * 60); + let output = output.output.unwrap_or_default(); + let uptime = state.api_status.startup_time.elapsed(); let chain_status = match state .chain_status_cache @@ -61,7 +71,7 @@ async fn health(State(state): State) -> Json { chain_status, uptime: uptime.as_secs(), }; - Json(health) + output.to_response(health) } #[utoipa::path( @@ -69,13 +79,21 @@ async fn health(State(state): State) -> Json { get, path = "/v1/api-status/build-information", responses( - (status = 200, body = BinaryBuildInformationOwned) - ) + (status = 200, content( + (BinaryBuildInformationOwned = "application/json"), + (BinaryBuildInformationOwned = "application/yaml"), + (BinaryBuildInformationOwned = "application/bincode") + )) + ), + params(OutputParams) )] async fn build_information( + Query(output): Query, State(state): State, -) -> Json { - Json(state.build_information.to_owned()) +) -> FormattedResponse { + let output = output.output.unwrap_or_default(); + + output.to_response(state.build_information.to_owned()) } #[utoipa::path( @@ -83,17 +101,25 @@ async fn build_information( get, path = "/v1/api-status/signer-information", responses( - (status = 200, body = SignerInformationResponse) - ) + (status = 200, content( + (SignerInformationResponse = "application/json"), + (SignerInformationResponse = "application/yaml"), + (SignerInformationResponse = "application/bincode") + )) + ), + params(OutputParams) )] async fn signer_information( + Query(output): Query, State(state): State, -) -> AxumResult> { +) -> AxumResult> { let signer_state = state.signer_information.as_ref().ok_or_else(|| { AxumErrorResponse::internal_msg("this api does not expose zk-nym signing functionalities") })?; - Ok(Json(SignerInformationResponse { + let output = output.output.unwrap_or_default(); + + Ok(output.to_response(SignerInformationResponse { cosmos_address: signer_state.cosmos_address.clone(), identity: signer_state.identity.clone(), announce_address: signer_state.announce_address.clone(), diff --git a/nym-api/src/support/http/helpers.rs b/nym-api/src/support/http/helpers.rs index 92f6d5585a..ed11746900 100644 --- a/nym-api/src/support/http/helpers.rs +++ b/nym-api/src/support/http/helpers.rs @@ -1,14 +1,15 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use nym_http_api_common::Output; use nym_mixnet_contract_common::NodeId; -use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use utoipa::{IntoParams, ToSchema}; -#[derive(Serialize, Deserialize, Debug, JsonSchema, ToSchema, IntoParams)] +#[derive(Serialize, Deserialize, Debug, ToSchema, IntoParams)] #[into_params(parameter_in = Query)] pub struct PaginationRequest { + pub output: Option, pub page: Option, pub per_page: Option, } diff --git a/nym-credential-proxy/nym-credential-proxy-requests/Cargo.toml b/nym-credential-proxy/nym-credential-proxy-requests/Cargo.toml index ed887405d2..4dd51fc5c8 100644 --- a/nym-credential-proxy/nym-credential-proxy-requests/Cargo.toml +++ b/nym-credential-proxy/nym-credential-proxy-requests/Cargo.toml @@ -36,6 +36,6 @@ features = ["tokio"] [features] default = ["query-types"] -query-types = ["nym-http-api-common"] +query-types = ["nym-http-api-common", "nym-http-api-common/output"] openapi = ["utoipa"] tsify = ["dep:tsify", "wasm-bindgen"] diff --git a/nym-node-status-api/nym-node-status-api/src/monitor/mod.rs b/nym-node-status-api/nym-node-status-api/src/monitor/mod.rs index 77d7199060..14b3192483 100644 --- a/nym-node-status-api/nym-node-status-api/src/monitor/mod.rs +++ b/nym-node-status-api/nym-node-status-api/src/monitor/mod.rs @@ -110,7 +110,7 @@ impl Monitor { .with_timeout(self.nym_api_client_timeout) .build::<&str>()?; - let api_client = NymApiClient { nym_api }; + let api_client = NymApiClient::from(nym_api); let described_nodes = api_client .get_all_described_nodes() @@ -195,7 +195,7 @@ impl Monitor { tracing::info!("🟣 mixnodes_described: {}", mixnodes_described.len()); let mixing_assigned_nodes = api_client .nym_api - .get_basic_active_mixing_assigned_nodes(false, None, None) + .get_basic_active_mixing_assigned_nodes(false, None, None, false) .await .log_error("get_basic_active_mixing_assigned_nodes")? .nodes diff --git a/nym-node-status-api/nym-node-status-api/src/node_scraper/mod.rs b/nym-node-status-api/nym-node-status-api/src/node_scraper/mod.rs index f294b47b0f..e9974e8df4 100644 --- a/nym-node-status-api/nym-node-status-api/src/node_scraper/mod.rs +++ b/nym-node-status-api/nym-node-status-api/src/node_scraper/mod.rs @@ -62,7 +62,7 @@ async fn run( .with_timeout(nym_api_client_timeout) .build::<&str>()?; - let api_client = NymApiClient { nym_api }; + let api_client = NymApiClient::from(nym_api); //SW TBC what nodes exactly need to be scraped, the skimmed node endpoint seems to return more nodes let bonded_nodes = api_client.get_all_bonded_nym_nodes().await?; diff --git a/nym-node/src/node/mod.rs b/nym-node/src/node/mod.rs index 5dc3e3302e..f3b3b3b1b9 100644 --- a/nym-node/src/node/mod.rs +++ b/nym-node/src/node/mod.rs @@ -842,7 +842,7 @@ impl NymNode { } }; - let client = NymApiClient { nym_api }; + let client = NymApiClient::from(nym_api); // make new request every time in case previous one takes longer and invalidates the signature let request = NodeRefreshBody::new(self.ed25519_identity_keys.private_key()); diff --git a/nym-node/src/node/shared_network.rs b/nym-node/src/node/shared_network.rs index 17e229f4e3..1b6ace3210 100644 --- a/nym-node/src/node/shared_network.rs +++ b/nym-node/src/node/shared_network.rs @@ -177,7 +177,7 @@ impl NetworkRefresher { let mut this = NetworkRefresher { querier: NodesQuerier { - client: NymApiClient { nym_api }, + client: NymApiClient::from(nym_api), nym_api_urls, currently_used_api: 0, }, diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index 3efcb8f461..2079f84062 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -4186,12 +4186,14 @@ name = "nym-http-api-client" version = "0.1.0" dependencies = [ "async-trait", + "bincode", "bytes", "encoding_rs", "hickory-resolver", "http 1.3.1", "mime", "nym-bin-common", + "nym-http-api-common", "once_cell", "reqwest 0.12.15", "serde", @@ -4202,6 +4204,15 @@ dependencies = [ "wasmtimer", ] +[[package]] +name = "nym-http-api-common" +version = "0.1.0" +dependencies = [ + "bincode", + "serde", + "tracing", +] + [[package]] name = "nym-mixnet-contract-common" version = "0.6.0"