Compare commits

...

3 Commits

Author SHA1 Message Date
benedetta davico 7a9c41ad73 bump nym-api version 2025-05-14 10:04:28 +02:00
Jędrzej Stuczyński ea0d2f8c66 feat: expires header for /active nym-api responses (#5755)
* refactor FormattedResponse to allow attaching additional headers

* helper method for including expiration headers

* add expires header for /active nodes responses

* added additional 'with_expires_header_delta' builder to FormattedResponse to allow setting expiration header with time delta
2025-05-13 16:03:44 +01:00
Jędrzej Stuczyński e849e608b2 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
2025-05-13 16:03:22 +01:00
39 changed files with 1659 additions and 603 deletions
Generated
+4 -1
View File
@@ -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,14 +5811,15 @@ 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",
"time",
"tower 0.5.2",
"tracing",
"utoipa",
@@ -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"] }
@@ -345,25 +345,47 @@ impl<C, S> Client<C, S> {
#[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<nym_api::Client> 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<UserAgent>) -> Self {
@@ -373,7 +395,10 @@ impl NymApiClient {
.build::<ValidatorClientError>()
.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);
@@ -318,6 +318,7 @@ pub trait NymApiClientExt: ApiClient {
no_legacy: bool,
page: Option<u32>,
per_page: Option<u32>,
use_bincode: bool,
) -> Result<PaginatedCachedNodesResponse<SkimmedNode>, 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<u32>,
per_page: Option<u32>,
use_bincode: bool,
) -> Result<PaginatedCachedNodesResponse<SkimmedNode>, 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<u32>,
per_page: Option<u32>,
use_bincode: bool,
) -> Result<PaginatedCachedNodesResponse<SkimmedNode>, 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<u32>,
per_page: Option<u32>,
use_bincode: bool,
) -> Result<PaginatedCachedNodesResponse<SkimmedNode>, 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",
+5 -1
View File
@@ -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"]
+95 -23
View File
@@ -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<E: Display = String> {
#[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<T, K, V, E>(
&self,
path: PathSegments<'_>,
params: Params<'_, K, V>,
) -> Result<T, HttpClientError<E>>
where
for<'a> T: Deserialize<'a>,
K: AsRef<str> + Sync,
V: AsRef<str> + 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<T, K, V, E>(
&self,
path: PathSegments<'_>,
params: Params<'_, K, V>,
) -> Result<T, HttpClientError<E>>
where
for<'a> T: Deserialize<'a>,
K: AsRef<str> + Sync,
@@ -877,14 +895,10 @@ fn sanitize_url<K: AsRef<str>, V: AsRef<str>>(
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::<Mime>().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<T, E>(res: Response, allow_empty: bool) -> Result<T, HttpClientError<E>>
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<T, E>(headers: &HeaderMap, content: Bytes) -> Result<T, HttpClientError<E>>
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<T, E>(headers: &HeaderMap, content: Bytes) -> Result<T, HttpClientError<E>>
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<T, E>(headers: &HeaderMap, content: Bytes) -> Result<T, HttpClientError<E>>
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<Mime> {
headers
.get(CONTENT_TYPE)
.and_then(|value| value.to_str().ok())
.and_then(|value| value.parse::<Mime>().ok())
}
#[cfg(test)]
mod tests {
use super::*;
+36 -11
View File
@@ -11,20 +11,45 @@ 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 }
time = { workspace = true, optional = true, features = ["macros"] }
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",
"time",
"time/formatting"
]
middleware = [
"axum",
"axum-client-ip",
"colored",
"futures",
"subtle",
"tower",
"zeroize"
]
utoipa = ["dep:utoipa"]
bincode = ["dep:bincode"]
[lints]
workspace = true
+14 -85
View File
@@ -1,92 +1,21 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// Copyright 2023-2025 - Nym Technologies SA <contact@nymtech.net>
// 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<T> {
Json(Json<T>),
Yaml(Yaml<T>),
}
#[cfg(feature = "output")]
pub mod response;
impl<T> IntoResponse for FormattedResponse<T>
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<Output>,
}
impl Output {
pub fn to_response<T: Serialize>(self, data: T) -> FormattedResponse<T> {
match self {
Output::Json => FormattedResponse::Json(Json(data)),
Output::Yaml => FormattedResponse::Yaml(Yaml(data)),
}
}
}
#[derive(Debug, Clone, Copy, Default)]
#[must_use]
pub struct Yaml<T>(pub T);
impl<T> From<T> for Yaml<T> {
fn from(inner: T) -> Self {
Self(inner)
}
}
impl<T> IntoResponse for Yaml<T>
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()
}
@@ -0,0 +1,49 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::response::{error_response, ResponseWrapper};
use axum::http::header::IntoHeaderName;
use axum::http::{header, HeaderValue};
use axum::response::{IntoResponse, Response};
use bytes::{BufMut, BytesMut};
use serde::Serialize;
#[derive(Debug, Clone, Default)]
#[must_use]
pub struct Bincode<T>(pub(crate) ResponseWrapper<T>);
impl<T> From<T> for Bincode<T> {
fn from(response: T) -> Self {
Bincode(ResponseWrapper::new(response).with_header(
header::CONTENT_TYPE,
HeaderValue::from_static("application/bincode"),
))
}
}
impl<T> Bincode<T> {
pub(crate) fn with_header(
mut self,
name: impl IntoHeaderName,
value: impl Into<HeaderValue>,
) -> Self {
self.0.headers.insert(name, value.into());
self
}
}
impl<T> IntoResponse for Bincode<T>
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.data) {
Ok(()) => (self.0.headers, buf.into_inner().freeze()).into_response(),
Err(err) => error_response(err),
}
}
}
@@ -0,0 +1,49 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::response::{error_response, ResponseWrapper};
use axum::http::header::IntoHeaderName;
use axum::http::{header, HeaderValue};
use axum::response::{IntoResponse, Response};
use bytes::{BufMut, BytesMut};
use serde::Serialize;
use utoipa::gen::serde_json;
// don't use axum's Json directly as we need to be able to define custom headers
#[derive(Debug, Clone, Default)]
#[must_use]
pub struct Json<T>(pub(crate) ResponseWrapper<T>);
impl<T> From<T> for Json<T> {
fn from(response: T) -> Self {
Json(ResponseWrapper::new(response).with_header(
header::CONTENT_TYPE,
HeaderValue::from_static(mime::APPLICATION_JSON.as_ref()),
))
}
}
impl<T> Json<T> {
pub(crate) fn with_header(
mut self,
name: impl IntoHeaderName,
value: impl Into<HeaderValue>,
) -> Self {
self.0.headers.insert(name, value.into());
self
}
}
impl<T> IntoResponse for Json<T>
where
T: Serialize,
{
fn into_response(self) -> Response {
let mut buf = BytesMut::with_capacity(128).writer();
match serde_json::to_writer(&mut buf, &self.0.data) {
Ok(()) => (self.0.headers, buf.into_inner().freeze()).into_response(),
Err(err) => error_response(err),
}
}
}
+198
View File
@@ -0,0 +1,198 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use axum::http::header::IntoHeaderName;
use axum::http::{header, HeaderMap, HeaderValue, StatusCode};
use axum::response::{IntoResponse, Response};
use serde::{Deserialize, Serialize};
use std::time::Duration;
use time::format_description::BorrowedFormatItem;
use time::macros::{format_description, offset};
use time::OffsetDateTime;
#[cfg(feature = "bincode")]
pub mod bincode;
pub mod json;
pub mod yaml;
pub use json::Json;
pub use yaml::Yaml;
#[cfg(feature = "bincode")]
pub use bincode::Bincode;
#[derive(Debug, Clone, Default)]
pub(crate) struct ResponseWrapper<T> {
data: T,
headers: HeaderMap,
}
impl<T> ResponseWrapper<T> {
pub(crate) fn new(response: T) -> ResponseWrapper<T> {
ResponseWrapper {
data: response,
headers: Default::default(),
}
}
#[must_use]
pub(crate) fn with_header(
mut self,
name: impl IntoHeaderName,
value: impl Into<HeaderValue>,
) -> Self {
self.headers.insert(name, value.into());
self
}
}
#[derive(Debug, Clone)]
pub enum FormattedResponse<T> {
Json(Json<T>),
Yaml(Yaml<T>),
#[cfg(feature = "bincode")]
Bincode(Bincode<T>),
}
impl<T> FormattedResponse<T> {
pub fn into_inner(self) -> T {
match self {
FormattedResponse::Json(inner) => inner.0.data,
FormattedResponse::Yaml(inner) => inner.0.data,
#[cfg(feature = "bincode")]
FormattedResponse::Bincode(inner) => inner.0.data,
}
}
#[must_use]
pub fn with_header(
self,
name: impl IntoHeaderName,
value: impl Into<HeaderValue>,
) -> FormattedResponse<T> {
match self {
FormattedResponse::Json(inner) => {
FormattedResponse::Json(inner.with_header(name, value))
}
FormattedResponse::Yaml(inner) => {
FormattedResponse::Yaml(inner.with_header(name, value))
}
#[cfg(feature = "bincode")]
FormattedResponse::Bincode(inner) => {
FormattedResponse::Bincode(inner.with_header(name, value))
}
}
}
/// Set the `expires` header on the response to the provided expiration.
/// Internally it will perform conversions to make sure the value is set in GMT offset,
/// e.g. `Expires: Wed, 21 Oct 2015 07:28:00 GMT`
#[must_use]
pub fn with_expires_header(self, expiration: OffsetDateTime) -> FormattedResponse<T> {
// as per RFC-7234 (section 5.3) EXPIRES header has to use value formatted
// as defined in RFC-7231 (section 7.1.1.1)
// (preferred format (IMF-fixdate) uses RFC-5322 (section 3.3)
let formatted = format_rfc5352(expiration);
// SAFETY: our formatted datetime doesn't contain forbidden characters
#[allow(clippy::unwrap_used)]
self.with_header(header::EXPIRES, HeaderValue::try_from(formatted).unwrap())
}
/// Work similarly to `with_expires_header`, but rather than setting explicit expiration value,
/// it adds the provided time delta to the current time instead.
#[must_use]
pub fn with_expires_header_delta(self, expires_in: Duration) -> FormattedResponse<T> {
self.with_expires_header(OffsetDateTime::now_utc() + expires_in)
}
}
impl<T> IntoResponse for FormattedResponse<T>
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<Output>,
}
impl Output {
pub fn to_response<T: Serialize>(self, data: T) -> FormattedResponse<T> {
match self {
Output::Json => FormattedResponse::Json(Json::from(data)),
Output::Yaml => FormattedResponse::Yaml(Yaml::from(data)),
#[cfg(feature = "bincode")]
Output::Bincode => FormattedResponse::Bincode(Bincode::from(data)),
}
}
}
pub(crate) fn error_response<E: ToString>(err: E) -> Response {
(
StatusCode::INTERNAL_SERVER_ERROR,
[(
header::CONTENT_TYPE,
HeaderValue::from_static(mime::TEXT_PLAIN_UTF_8.as_ref()),
)],
err.to_string(),
)
.into_response()
}
// SAFETY: this hardcoded datetime formatter is valid
#[allow(clippy::unwrap_used)]
fn format_rfc5352(datetime: OffsetDateTime) -> String {
// the time must be using GMT (UTC) offset
let normalised = datetime.to_offset(offset!(UTC));
normalised.format(&rfc5322()).unwrap()
}
// NOTE: this function is purposely not made public as it cannot guarantee caller
// has correctly ensured their date is using correct GMT offset
fn rfc5322() -> &'static [BorrowedFormatItem<'static>] {
// D, d M Y H:i:s T
format_description!(
"[weekday repr:short], [day] [month repr:short] [year] [hour]:[minute]:[second] GMT"
)
}
#[cfg(test)]
mod tests {
use crate::response::format_rfc5352;
use time::macros::datetime;
#[test]
fn rfc5322_formatting() {
let utc_date = datetime!(2021-08-23 12:13:14 UTC);
let non_utc_date = datetime!(2021-08-23 12:13:14 -1);
assert_eq!("Mon, 23 Aug 2021 12:13:14 GMT", format_rfc5352(utc_date));
assert_eq!(
"Mon, 23 Aug 2021 13:13:14 GMT",
format_rfc5352(non_utc_date)
);
}
}
@@ -0,0 +1,47 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::response::{error_response, ResponseWrapper};
use axum::http::header::IntoHeaderName;
use axum::http::{header, HeaderValue};
use axum::response::{IntoResponse, Response};
use bytes::{BufMut, BytesMut};
use serde::Serialize;
#[derive(Debug, Clone, Default)]
#[must_use]
pub struct Yaml<T>(pub(crate) ResponseWrapper<T>);
impl<T> From<T> for Yaml<T> {
fn from(response: T) -> Self {
Yaml(ResponseWrapper::new(response).with_header(
header::CONTENT_TYPE,
HeaderValue::from_static("application/yaml"),
))
}
}
impl<T> Yaml<T> {
pub(crate) fn with_header(
mut self,
name: impl IntoHeaderName,
value: impl Into<HeaderValue>,
) -> Self {
self.0.headers.insert(name, value.into());
self
}
}
impl<T> IntoResponse for Yaml<T>
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.data) {
Ok(()) => (self.0.headers, buf.into_inner().freeze()).into_response(),
Err(err) => error_response(err),
}
}
}
+2 -2
View File
@@ -4,7 +4,7 @@
[package]
name = "nym-api"
license = "GPL-3.0"
version = "1.1.57"
version = "1.1.58"
authors.workspace = true
edition = "2021"
rust-version.workspace = true
@@ -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" }
+42 -12
View File
@@ -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<AppState> {
@@ -25,18 +27,26 @@ pub(crate) fn circulating_supply_routes() -> Router<AppState> {
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<OutputParams>,
extract::State(state): extract::State<AppState>,
) -> AxumResult<axum::Json<CirculatingSupplyResponse>> {
) -> AxumResult<FormattedResponse<CirculatingSupplyResponse>> {
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<OutputParams>,
extract::State(state): extract::State<AppState>,
) -> AxumResult<axum::Json<f64>> {
) -> AxumResult<FormattedResponse<f64>> {
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<OutputParams>,
extract::State(state): extract::State<AppState>,
) -> AxumResult<axum::Json<f64>> {
) -> AxumResult<FormattedResponse<f64>> {
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,
+42 -20
View File
@@ -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<AppState> {
),
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<Arc<EcashState>>,
Query(EpochIdParam { epoch_id }): Query<EpochIdParam>,
) -> AxumResult<Json<VerificationKeyResponse>> {
Query(EpochIdParam { epoch_id, output }): Query<EpochIdParam>,
) -> AxumResult<FormattedResponse<VerificationKeyResponse>> {
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<String>,
output: Option<Output>,
}
#[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<Arc<EcashState>>,
Query(ExpirationDateParam { expiration_date }): Query<ExpirationDateParam>,
) -> AxumResult<Json<AggregatedExpirationDateSignatureResponse>> {
Query(ExpirationDateParam {
expiration_date,
output,
}): Query<ExpirationDateParam>,
) -> AxumResult<FormattedResponse<AggregatedExpirationDateSignatureResponse>> {
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<EpochIdParam>,
Query(EpochIdParam { epoch_id, output }): Query<EpochIdParam>,
State(state): State<Arc<EcashState>>,
) -> AxumResult<Json<AggregatedCoinIndicesSignatureResponse>> {
) -> AxumResult<FormattedResponse<AggregatedCoinIndicesSignatureResponse>> {
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(),
}))
+3
View File
@@ -1,7 +1,10 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// 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<u64>,
pub(super) output: Option<Output>,
}
+61 -27
View File
@@ -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<PaginationRequest>,
State(state): State<Arc<EcashState>>,
) -> AxumResult<Json<IssuedTicketbooksCountResponse>> {
) -> AxumResult<FormattedResponse<IssuedTicketbooksCountResponse>> {
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<OutputParams>,
Path(ExpirationDatePathParam { expiration_date }): Path<ExpirationDatePathParam>,
State(state): State<Arc<EcashState>>,
) -> AxumResult<Json<IssuedTicketbooksForCountResponse>> {
) -> AxumResult<FormattedResponse<IssuedTicketbooksForCountResponse>> {
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<OutputParams>,
Path(IssuanceDatePathParam { issuance_date }): Path<IssuanceDatePathParam>,
State(state): State<Arc<EcashState>>,
) -> AxumResult<Json<IssuedTicketbooksOnCountResponse>> {
) -> AxumResult<FormattedResponse<IssuedTicketbooksOnCountResponse>> {
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<OutputParams>,
State(state): State<Arc<EcashState>>,
Path(ExpirationDatePathParam { expiration_date }): Path<ExpirationDatePathParam>,
) -> AxumResult<Json<IssuedTicketbooksForResponse>> {
) -> AxumResult<FormattedResponse<IssuedTicketbooksForResponse>> {
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<OutputParams>,
State(state): State<Arc<EcashState>>,
Json(request): Json<IssuedTicketbooksChallengeCommitmentRequest>,
) -> AxumResult<Json<IssuedTicketbooksChallengeCommitmentResponse>> {
) -> AxumResult<FormattedResponse<IssuedTicketbooksChallengeCommitmentResponse>> {
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<OutputParams>,
State(state): State<Arc<EcashState>>,
Json(request): Json<IssuedTicketbooksDataRequest>,
) -> AxumResult<Json<IssuedTicketbooksDataResponse>> {
) -> AxumResult<FormattedResponse<IssuedTicketbooksDataResponse>> {
state.ensure_signer().await?;
let output = output.output.unwrap_or_default();
Ok(Json(
Ok(output.to_response(
state
.get_issued_ticketbooks_data(request)
.await?
+39 -17
View File
@@ -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<AppState> {
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<OutputParams>,
State(state): State<Arc<EcashState>>,
Json(blind_sign_request_body): Json<BlindSignRequestBody>,
) -> AxumResult<Json<BlindedSignatureResponse>> {
) -> AxumResult<FormattedResponse<BlindedSignatureResponse>> {
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<String>,
output: Option<Output>,
}
#[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<Arc<EcashState>>,
Query(ExpirationDateParam { expiration_date }): Query<ExpirationDateParam>,
) -> AxumResult<Json<PartialExpirationDateSignatureResponse>> {
Query(ExpirationDateParam {
expiration_date,
output,
}): Query<ExpirationDateParam>,
) -> AxumResult<FormattedResponse<PartialExpirationDateSignatureResponse>> {
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<Arc<EcashState>>,
Query(EpochIdParam { epoch_id }): Query<EpochIdParam>,
) -> AxumResult<Json<PartialCoinIndicesSignatureResponse>> {
Query(EpochIdParam { epoch_id, output }): Query<EpochIdParam>,
) -> AxumResult<FormattedResponse<PartialCoinIndicesSignatureResponse>> {
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(),
}))
}
+39 -21
View File
@@ -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<AppState> {
const ONE_AM: Time = time!(1:00);
fn reject_ticket(
output: Output,
reason: EcashTicketVerificationRejection,
) -> AxumResult<Json<EcashTicketVerificationResponse>> {
Ok(Json(EcashTicketVerificationResponse::reject(reason)))
) -> AxumResult<FormattedResponse<EcashTicketVerificationResponse>> {
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<OutputParams>,
State(state): State<Arc<EcashState>>,
// TODO in the future: make it send binary data rather than json
Json(verify_ticket_body): Json<VerifyEcashTicketBody>,
) -> AxumResult<Json<EcashTicketVerificationResponse>> {
) -> AxumResult<FormattedResponse<EcashTicketVerificationResponse>> {
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<OutputParams>,
State(state): State<Arc<EcashState>>,
// TODO in the future: make it send binary data rather than json
Json(batch_redeem_credentials_body): Json<BatchRedeemTicketsBody>,
) -> AxumResult<Json<EcashBatchTicketRedemptionResponse>> {
) -> AxumResult<FormattedResponse<EcashBatchTicketRedemptionResponse>> {
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<Json<SpentCredentialsResponse>> {
async fn double_spending_filter_v1() -> AxumResult<FormattedResponse<SpentCredentialsResponse>> {
AxumResult::Err(AxumErrorResponse::new(
"permanently restricted",
StatusCode::GONE,
+77 -40
View File
@@ -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<AppState> {
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<OutputParams>,
extract::State(state): extract::State<AppState>,
) -> axum::Json<NetworkDetails> {
state.network_details().to_owned().into()
) -> FormattedResponse<NetworkDetails> {
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<AppState>) -> AxumResult<Json<ChainStatusResponse>> {
async fn chain_status(
Query(output): Query<OutputParams>,
State(state): State<AppState>,
) -> AxumResult<FormattedResponse<ChainStatusResponse>> {
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<AppState>) -> AxumResult<Json<ChainSta
let connected_nyxd = state.network_details.connected_nyxd;
Ok(Json(ChainStatusResponse {
Ok(output.to_response(ChainStatusResponse {
connected_nyxd,
status: chain_status,
}))
@@ -87,25 +106,34 @@ pub struct ContractInformationContractVersion {
get,
path = "/v1/network/nym-contracts",
responses(
(status = 200, body = HashMap<String, ContractInformationContractVersion>)
)
(status = 200, content(
(HashMap<String, ContractInformationContractVersion> = "application/json"),
(HashMap<String, ContractInformationContractVersion> = "application/yaml"),
(HashMap<String, ContractInformationContractVersion> = "application/bincode")
))
),
params(OutputParams)
)]
async fn nym_contracts(
Query(output): Query<OutputParams>,
extract::State(state): extract::State<AppState>,
) -> axum::Json<HashMap<String, ContractInformation<cw2::ContractVersion>>> {
) -> FormattedResponse<HashMap<String, ContractInformation<cw2::ContractVersion>>> {
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::<HashMap<_, _>>()
.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::<HashMap<_, _>>(),
)
}
#[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<String, ContractInformationBuildInformation>)
)
(status = 200, content(
(HashMap<String, ContractInformationBuildInformation> = "application/json"),
(HashMap<String, ContractInformationBuildInformation> = "application/yaml"),
(HashMap<String, ContractInformationBuildInformation> = "application/bincode")
))
),
params(OutputParams)
)]
async fn nym_contracts_detailed(
Query(output): Query<OutputParams>,
extract::State(state): extract::State<AppState>,
) -> axum::Json<HashMap<String, ContractInformation<ContractBuildInformation>>> {
) -> FormattedResponse<HashMap<String, ContractInformation<ContractBuildInformation>>> {
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::<HashMap<_, _>>()
.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::<HashMap<_, _>>(),
)
}
+14 -5
View File
@@ -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<OutputParams>,
State(state): State<AppState>,
) -> AxumResult<Json<ConfigScoreDataResponse>> {
) -> AxumResult<FormattedResponse<ConfigScoreDataResponse>> {
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()))
}
@@ -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<AppState> {
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<String>,
Query(output): Query<OutputParams>,
State(state): State<AppState>,
) -> AxumResult<Json<GatewayStatusReportResponse>> {
Ok(Json(
_gateway_report(state.node_status_cache(), &identity).await?,
))
) -> AxumResult<FormattedResponse<GatewayStatusReportResponse>> {
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<String>,
Query(output): Query<OutputParams>,
State(state): State<AppState>,
) -> AxumResult<Json<GatewayUptimeHistoryResponse>> {
Ok(Json(
) -> AxumResult<FormattedResponse<GatewayUptimeHistoryResponse>> {
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<i64>,
output: Option<Output>,
}
#[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<String>,
Query(SinceQueryParams { since }): Query<SinceQueryParams>,
Query(SinceQueryParams { since, output }): Query<SinceQueryParams>,
State(state): State<AppState>,
) -> AxumResult<Json<GatewayCoreStatusResponse>> {
Ok(Json(
_gateway_core_status_count(state.storage(), &identity, since).await?,
))
) -> AxumResult<FormattedResponse<GatewayCoreStatusResponse>> {
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<String>,
Query(output): Query<OutputParams>,
State(state): State<AppState>,
) -> AxumResult<Json<GatewayUptimeResponse>> {
Ok(Json(
_get_gateway_avg_uptime(state.node_status_cache(), &identity).await?,
))
) -> AxumResult<FormattedResponse<GatewayUptimeResponse>> {
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<MixIdParam>,
Query(output): Query<OutputParams>,
State(state): State<AppState>,
) -> AxumResult<Json<MixnodeStatusReportResponse>> {
Ok(Json(
_mixnode_report(state.node_status_cache(), mix_id).await?,
))
) -> AxumResult<FormattedResponse<MixnodeStatusReportResponse>> {
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<MixIdParam>,
Query(output): Query<OutputParams>,
State(state): State<AppState>,
) -> AxumResult<Json<MixnodeUptimeHistoryResponse>> {
Ok(Json(
) -> AxumResult<FormattedResponse<MixnodeUptimeHistoryResponse>> {
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<MixIdParam>,
Query(SinceQueryParams { since }): Query<SinceQueryParams>,
Query(SinceQueryParams { since, output }): Query<SinceQueryParams>,
State(state): State<AppState>,
) -> AxumResult<Json<MixnodeCoreStatusResponse>> {
Ok(Json(
_mixnode_core_status_count(state.storage(), mix_id, since).await?,
))
) -> AxumResult<FormattedResponse<MixnodeCoreStatusResponse>> {
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<MixIdParam>,
Query(output): Query<OutputParams>,
State(state): State<AppState>,
) -> AxumResult<Json<RewardEstimationResponse>> {
Ok(Json(
) -> AxumResult<FormattedResponse<RewardEstimationResponse>> {
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<MixIdParam>,
Query(output): Query<OutputParams>,
State(state): State<AppState>,
Json(user_reward_param): Json<ComputeRewardEstParam>,
) -> AxumResult<Json<RewardEstimationResponse>> {
Ok(Json(
) -> AxumResult<FormattedResponse<RewardEstimationResponse>> {
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<MixIdParam>,
Query(output): Query<OutputParams>,
State(state): State<AppState>,
) -> AxumResult<Json<UptimeResponse>> {
Ok(Json(
_get_mixnode_avg_uptime(state.node_status_cache(), mix_id).await?,
))
) -> AxumResult<FormattedResponse<UptimeResponse>> {
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<OutputParams>,
State(state): State<AppState>,
) -> Json<Vec<MixNodeBondAnnotated>> {
Json(_get_mixnodes_detailed_unfiltered(state.node_status_cache()).await)
) -> FormattedResponse<Vec<MixNodeBondAnnotated>> {
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<OutputParams>,
State(state): State<AppState>,
) -> Json<Vec<GatewayBondAnnotated>> {
Json(_get_legacy_gateways_detailed(state.node_status_cache()).await)
) -> FormattedResponse<Vec<GatewayBondAnnotated>> {
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<OutputParams>,
State(state): State<AppState>,
) -> Json<Vec<GatewayBondAnnotated>> {
Json(_get_legacy_gateways_detailed_unfiltered(state.node_status_cache()).await)
) -> FormattedResponse<Vec<GatewayBondAnnotated>> {
let output = output.output.unwrap_or_default();
output.to_response(_get_legacy_gateways_detailed_unfiltered(state.node_status_cache()).await)
}
@@ -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<NodeId>,
Query(pagination): Query<PaginationRequest>,
State(state): State<AppState>,
) -> AxumResult<Json<MixnodeTestResultResponse>> {
) -> AxumResult<FormattedResponse<MixnodeTestResultResponse>> {
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<String>,
Query(pagination): Query<PaginationRequest>,
State(state): State<AppState>,
) -> AxumResult<Json<GatewayTestResultResponse>> {
) -> AxumResult<FormattedResponse<GatewayTestResultResponse>> {
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<i64>,
Query(output): Query<OutputParams>,
State(state): State<AppState>,
) -> AxumResult<Json<NetworkMonitorRunDetailsResponse>> {
) -> AxumResult<FormattedResponse<NetworkMonitorRunDetailsResponse>> {
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<OutputParams>,
State(state): State<AppState>,
) -> AxumResult<Json<NetworkMonitorRunDetailsResponse>> {
) -> AxumResult<FormattedResponse<NetworkMonitorRunDetailsResponse>> {
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}"
))),
@@ -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<MixIdParam>,
Query(output): Query<OutputParams>,
State(state): State<AppState>,
) -> Json<MixnodeStatusResponse> {
Json(_get_mixnode_status(state.nym_contract_cache(), mix_id).await)
) -> FormattedResponse<MixnodeStatusResponse> {
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<NodeId>,
Query(output): Query<OutputParams>,
State(state): State<AppState>,
) -> AxumResult<Json<StakeSaturationResponse>> {
Ok(Json(
) -> AxumResult<FormattedResponse<StakeSaturationResponse>> {
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<NodeId>,
Query(output): Query<OutputParams>,
State(state): State<AppState>,
) -> AxumResult<Json<nym_api_requests::models::InclusionProbabilityResponse>> {
Ok(Json(
_get_mixnode_inclusion_probability(state.node_status_cache(), mix_id).await?,
))
) -> AxumResult<FormattedResponse<nym_api_requests::models::InclusionProbabilityResponse>> {
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<OutputParams>,
State(state): State<AppState>,
) -> AxumResult<Json<nym_api_requests::models::AllInclusionProbabilitiesResponse>> {
Ok(Json(
_get_mixnode_inclusion_probabilities(state.node_status_cache()).await?,
))
) -> AxumResult<FormattedResponse<nym_api_requests::models::AllInclusionProbabilitiesResponse>> {
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<OutputParams>,
State(state): State<AppState>,
) -> Json<Vec<MixNodeBondAnnotated>> {
Json(_get_legacy_mixnodes_detailed(state.node_status_cache()).await)
) -> FormattedResponse<Vec<MixNodeBondAnnotated>> {
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<OutputParams>,
State(state): State<AppState>,
) -> Json<Vec<MixNodeBondAnnotated>> {
Json(
) -> FormattedResponse<Vec<MixNodeBondAnnotated>> {
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<OutputParams>,
State(state): State<AppState>,
) -> Json<Vec<MixNodeBondAnnotated>> {
Json(
) -> FormattedResponse<Vec<MixNodeBondAnnotated>> {
let output = output.output.unwrap_or_default();
output.to_response(
_get_active_set_legacy_mixnodes_detailed(
state.node_status_cache(),
state.nym_contract_cache(),
+185 -77
View File
@@ -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<AppState> {
get,
path = "/v1/mixnodes",
responses(
(status = 200, body = Vec<LegacyMixNodeDetailsWithLayer>)
)
(status = 200, content(
(Vec<LegacyMixNodeDetailsWithLayer> = "application/json"),
(Vec<LegacyMixNodeDetailsWithLayer> = "application/yaml"),
(Vec<LegacyMixNodeDetailsWithLayer> = "application/bincode")
))
),
params(OutputParams)
)]
#[deprecated]
async fn get_mixnodes(State(state): State<AppState>) -> Json<Vec<LegacyMixNodeDetailsWithLayer>> {
async fn get_mixnodes(
Query(output): Query<OutputParams>,
State(state): State<AppState>,
) -> FormattedResponse<Vec<LegacyMixNodeDetailsWithLayer>> {
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<AppState>) -> Json<Vec<LegacyMixNodeDe
out.push(node);
}
Json(out)
output.to_response(out)
}
// DEPRECATED: this endpoint now lives in `node_status_api`. Once all consumers are updated,
@@ -115,14 +125,22 @@ async fn get_mixnodes(State(state): State<AppState>) -> Json<Vec<LegacyMixNodeDe
get,
path = "/v1/mixnodes/detailed",
responses(
(status = 200, body = Vec<MixNodeBondAnnotated>)
)
(status = 200, content(
(Vec<MixNodeBondAnnotated> = "application/json"),
(Vec<MixNodeBondAnnotated> = "application/yaml"),
(Vec<MixNodeBondAnnotated> = "application/bincode")
))
),
params(OutputParams)
)]
#[deprecated]
async fn get_mixnodes_detailed(State(state): State<AppState>) -> Json<Vec<MixNodeBondAnnotated>> {
_get_legacy_mixnodes_detailed(state.node_status_cache())
.await
.into()
async fn get_mixnodes_detailed(
Query(output): Query<OutputParams>,
State(state): State<AppState>,
) -> FormattedResponse<Vec<MixNodeBondAnnotated>> {
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<AppState>) -> Json<Vec<MixNod
get,
path = "/v1/gateways",
responses(
(status = 200, body = Vec<GatewayBond>)
)
(status = 200, content(
(Vec<GatewayBond> = "application/json"),
(Vec<GatewayBond> = "application/yaml"),
(Vec<GatewayBond> = "application/bincode")
))
),
params(OutputParams)
)]
#[deprecated]
async fn get_gateways(State(state): State<AppState>) -> Json<Vec<GatewayBond>> {
async fn get_gateways(
Query(output): Query<OutputParams>,
State(state): State<AppState>,
) -> FormattedResponse<Vec<GatewayBond>> {
let output = output.output.unwrap_or_default();
// legacy
let mut out: Vec<GatewayBond> = state
.nym_contract_cache()
@@ -145,15 +173,15 @@ async fn get_gateways(State(state): State<AppState>) -> Json<Vec<GatewayBond>> {
.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<AppState>) -> Json<Vec<GatewayBond>> {
out.push(node);
}
Json(out)
output.to_response(out)
}
#[utoipa::path(
@@ -190,14 +218,22 @@ async fn get_gateways(State(state): State<AppState>) -> Json<Vec<GatewayBond>> {
get,
path = "/v1/mixnodes/rewarded",
responses(
(status = 200, body = Vec<LegacyMixNodeDetailsWithLayer>)
)
(status = 200, content(
(Vec<LegacyMixNodeDetailsWithLayer> = "application/json"),
(Vec<LegacyMixNodeDetailsWithLayer> = "application/yaml"),
(Vec<LegacyMixNodeDetailsWithLayer> = "application/bincode")
))
),
params(OutputParams)
)]
#[deprecated]
async fn get_rewarded_set(
Query(output): Query<OutputParams>,
State(state): State<AppState>,
) -> Json<Vec<LegacyMixNodeDetailsWithLayer>> {
Json(
) -> FormattedResponse<Vec<LegacyMixNodeDetailsWithLayer>> {
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<MixNodeBondAnnotated>)
)
(status = 200, content(
(Vec<MixNodeBondAnnotated> = "application/json"),
(Vec<MixNodeBondAnnotated> = "application/yaml"),
(Vec<MixNodeBondAnnotated> = "application/bincode")
))
),
params(OutputParams)
)]
#[deprecated]
async fn get_rewarded_set_detailed(
Query(output): Query<OutputParams>,
State(state): State<AppState>,
) -> Json<Vec<MixNodeBondAnnotated>> {
_get_rewarded_set_legacy_mixnodes_detailed(
state.node_status_cache(),
state.nym_contract_cache(),
) -> FormattedResponse<Vec<MixNodeBondAnnotated>> {
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<LegacyMixNodeDetailsWithLayer>)
)
(status = 200, content(
(Vec<LegacyMixNodeDetailsWithLayer> = "application/json"),
(Vec<LegacyMixNodeDetailsWithLayer> = "application/yaml"),
(Vec<LegacyMixNodeDetailsWithLayer> = "application/bincode")
))
),
params(OutputParams)
)]
#[deprecated]
async fn get_active_set(State(state): State<AppState>) -> Json<Vec<LegacyMixNodeDetailsWithLayer>> {
async fn get_active_set(
Query(output): Query<OutputParams>,
State(state): State<AppState>,
) -> FormattedResponse<Vec<LegacyMixNodeDetailsWithLayer>> {
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<AppState>) -> Json<Vec<LegacyMixNode
.clone();
let Some(rewarded_set) = state.nym_contract_cache().rewarded_set().await else {
return Json(out);
return output.to_response(out);
};
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
@@ -295,7 +350,7 @@ async fn get_active_set(State(state): State<AppState>) -> Json<Vec<LegacyMixNode
out.push(node);
}
Json(out)
output.to_response(out)
}
// DEPRECATED: this endpoint now lives in `node_status_api`. Once all consumers are updated,
@@ -311,14 +366,28 @@ async fn get_active_set(State(state): State<AppState>) -> Json<Vec<LegacyMixNode
get,
path = "/v1/mixnodes/active/detailed",
responses(
(status = 200, body = Vec<MixNodeBondAnnotated>)
)
(status = 200, content(
(Vec<MixNodeBondAnnotated> = "application/json"),
(Vec<MixNodeBondAnnotated> = "application/yaml"),
(Vec<MixNodeBondAnnotated> = "application/bincode")
))
),
params(OutputParams)
)]
#[deprecated]
async fn get_active_set_detailed(State(state): State<AppState>) -> Json<Vec<MixNodeBondAnnotated>> {
_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<OutputParams>,
State(state): State<AppState>,
) -> FormattedResponse<Vec<MixNodeBondAnnotated>> {
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<AppState>) -> Json<Vec<MixN
get,
path = "/v1/mixnodes/blacklisted",
responses(
(status = 200, body = Option<HashSet<NodeId>>)
)
(status = 200, content(
(Option<HashSet<NodeId>> = "application/json"),
(Option<HashSet<NodeId>> = "application/yaml"),
(Option<HashSet<NodeId>> = "application/bincode")
))
),
params(OutputParams)
)]
#[deprecated]
async fn get_blacklisted_mixnodes(State(state): State<AppState>) -> Json<Option<HashSet<NodeId>>> {
async fn get_blacklisted_mixnodes(
Query(output): Query<OutputParams>,
State(state): State<AppState>,
) -> FormattedResponse<Option<HashSet<NodeId>>> {
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<AppState>) -> Json<Option<
get,
path = "/v1/gateways/blacklisted",
responses(
(status = 200, body = Option<HashSet<String>>)
)
(status = 200, content(
(Option<HashSet<NodeId>> = "application/json"),
(Option<HashSet<NodeId>> = "application/yaml"),
(Option<HashSet<NodeId>> = "application/bincode")
))
),
params(OutputParams)
)]
#[deprecated]
async fn get_blacklisted_gateways(State(state): State<AppState>) -> Json<Option<HashSet<String>>> {
async fn get_blacklisted_gateways(
Query(output): Query<OutputParams>,
State(state): State<AppState>,
) -> FormattedResponse<Option<HashSet<String>>> {
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<AppState>) -> Json<Option<
get,
path = "/v1/epoch/reward_params",
responses(
(status = 200, body = Option<RewardingParams>)
)
(status = 200, content(
(Option<RewardingParams> = "application/json"),
(Option<RewardingParams> = "application/yaml"),
(Option<RewardingParams> = "application/bincode")
))
),
params(OutputParams)
)]
async fn get_interval_reward_params(
Query(output): Query<OutputParams>,
State(state): State<AppState>,
) -> Json<Option<RewardingParams>> {
state
.nym_contract_cache()
.interval_reward_params()
.await
.to_owned()
.into()
) -> FormattedResponse<Option<RewardingParams>> {
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<Interval>)
)
(status = 200, content(
(Option<Interval> = "application/json"),
(Option<Interval> = "application/yaml"),
(Option<Interval> = "application/bincode")
))
),
params(OutputParams)
)]
async fn get_current_epoch(State(state): State<AppState>) -> Json<Option<Interval>> {
state
.nym_contract_cache()
.current_interval()
.await
.to_owned()
.into()
async fn get_current_epoch(
Query(output): Query<OutputParams>,
State(state): State<AppState>,
) -> FormattedResponse<Option<Interval>> {
let output = output.output.unwrap_or_default();
output.to_response(
state
.nym_contract_cache()
.current_interval()
.await
.to_owned(),
)
}
+27 -12
View File
@@ -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<AppState> {
get,
path = "/v1/gateways/described",
responses(
(status = 200, body = Vec<LegacyDescribedGateway>)
)
(status = 200, content(
(Vec<LegacyDescribedGateway> = "application/json"),
(Vec<LegacyDescribedGateway> = "application/yaml"),
(Vec<LegacyDescribedGateway> = "application/bincode")
))
),
params(OutputParams)
)]
#[deprecated]
async fn get_gateways_described(
Query(output): Query<OutputParams>,
State(state): State<AppState>,
) -> Json<Vec<LegacyDescribedGateway>> {
) -> FormattedResponse<Vec<LegacyDescribedGateway>> {
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<LegacyDescribedMixNode>)
)
(status = 200, content(
(Vec<LegacyDescribedMixNode> = "application/json"),
(Vec<LegacyDescribedMixNode> = "application/yaml"),
(Vec<LegacyDescribedMixNode> = "application/bincode")
))
),
params(OutputParams)
)]
#[deprecated]
async fn get_mixnodes_described(
Query(output): Query<OutputParams>,
State(state): State<AppState>,
) -> Json<Vec<LegacyDescribedMixNode>> {
) -> FormattedResponse<Vec<LegacyDescribedMixNode>> {
let contract_cache = state.nym_contract_cache();
let describe_cache = state.described_nodes_cache();
let output = output.output.unwrap_or_default();
let legacy: Vec<LegacyMixNodeBondWithLayer> = 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)
}
+89 -33
View File
@@ -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<AppState> {
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<AppState>) -> AxumResult<Json<RewardedSetResponse>> {
async fn rewarded_set(
Query(output): Query<OutputParams>,
State(state): State<AppState>,
) -> AxumResult<FormattedResponse<RewardedSetResponse>> {
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<AppState>) -> AxumResult<Json<Rewarded
.ok_or(UninitialisedCache)?
.into_inner();
Ok(Json(
Ok(output.to_response(
nym_mixnet_contract_common::EpochRewardedSet::from(cached_rewarded_set).into(),
))
}
@@ -136,16 +146,21 @@ async fn refresh_described(
path = "/noise",
context_path = "/v1/nym-nodes",
responses(
(status = 200, body = PaginatedResponse<NoiseDetails>)
(status = 200, content(
(PaginatedResponse<NoiseDetails> = "application/json"),
(PaginatedResponse<NoiseDetails> = "application/yaml"),
(PaginatedResponse<NoiseDetails> = "application/bincode")
))
),
params(PaginationRequest)
)]
async fn nodes_noise(
State(state): State<AppState>,
Query(pagination): Query<PaginationRequest>,
) -> AxumResult<Json<PaginatedResponse<NoiseDetails>>> {
) -> AxumResult<FormattedResponse<PaginatedResponse<NoiseDetails>>> {
// 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<NymNodeDetails>)
(status = 200, content(
(PaginatedResponse<NymNodeDetails> = "application/json"),
(PaginatedResponse<NymNodeDetails> = "application/yaml"),
(PaginatedResponse<NymNodeDetails> = "application/bincode")
))
),
params(PaginationRequest)
)]
async fn get_bonded_nodes(
State(state): State<AppState>,
Query(pagination): Query<PaginationRequest>,
) -> Json<PaginatedResponse<NymNodeDetails>> {
) -> FormattedResponse<PaginatedResponse<NymNodeDetails>> {
// 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<NymNodeDescription>)
(status = 200, content(
(PaginatedResponse<NymNodeDescription> = "application/json"),
(PaginatedResponse<NymNodeDescription> = "application/yaml"),
(PaginatedResponse<NymNodeDescription> = "application/bincode")
))
),
params(PaginationRequest)
)]
async fn get_described_nodes(
State(state): State<AppState>,
Query(pagination): Query<PaginationRequest>,
) -> AxumResult<Json<PaginatedResponse<NymNodeDescription>>> {
) -> AxumResult<FormattedResponse<PaginatedResponse<NymNodeDescription>>> {
// 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::<Vec<_>>();
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<NodeIdParam>,
Query(output): Query<OutputParams>,
State(state): State<AppState>,
) -> AxumResult<Json<AnnotationResponse>> {
) -> AxumResult<FormattedResponse<AnnotationResponse>> {
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<NodeIdParam>,
Query(output): Query<OutputParams>,
State(state): State<AppState>,
) -> AxumResult<Json<NodePerformanceResponse>> {
) -> AxumResult<FormattedResponse<NodePerformanceResponse>> {
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<Output>,
}
#[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<NodeIdParam>,
Query(DateQuery { date }): Query<DateQuery>,
Query(DateQuery { date, output }): Query<DateQuery>,
State(state): State<AppState>,
) -> AxumResult<Json<NodeDatePerformanceResponse>> {
) -> AxumResult<FormattedResponse<NodeDatePerformanceResponse>> {
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<NodeIdParam>,
State(state): State<AppState>,
Query(pagination): Query<PaginationRequest>,
) -> AxumResult<Json<PerformanceHistoryResponse>> {
) -> AxumResult<FormattedResponse<PerformanceHistoryResponse>> {
// 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::<Vec<_>>();
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<NodeIdParam>,
State(state): State<AppState>,
Query(pagination): Query<PaginationRequest>,
) -> AxumResult<Json<UptimeHistoryResponse>> {
) -> AxumResult<FormattedResponse<UptimeHistoryResponse>> {
// 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::<Vec<_>>();
let total = history.len();
Ok(Json(UptimeHistoryResponse {
Ok(output.to_response(UptimeHistoryResponse {
node_id,
history: PaginatedResponse {
pagination: Pagination {
@@ -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<AppState>,
_query_params: Query<NodesParamsWithRole>,
) -> AxumResult<Json<CachedNodesResponse<FullFatNode>>> {
) -> AxumResult<FormattedResponse<CachedNodesResponse<FullFatNode>>> {
Err(AxumErrorResponse::not_implemented())
}
+19 -5
View File
@@ -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<u32>,
output: Option<Output>,
}
#[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<u32>,
output: Option<Output>,
}
impl From<NodesParamsWithRole> for NodesParams {
@@ -123,6 +127,7 @@ impl From<NodesParamsWithRole> for NodesParams {
page: params.page,
per_page: params.per_page,
epoch_id: params.epoch_id,
output: params.output,
}
}
}
@@ -130,6 +135,7 @@ impl From<NodesParamsWithRole> 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<OutputParams>,
state: State<AppState>,
Json(body): Json<NodesByAddressesRequestBody>,
) -> AxumResult<Json<NodesByAddressesResponse>> {
) -> AxumResult<FormattedResponse<NodesByAddressesResponse>> {
// 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 }))
}
@@ -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<AppState>,
_query_params: Query<NodesParamsWithRole>,
) -> AxumResult<Json<CachedNodesResponse<SemiSkimmedNode>>> {
) -> AxumResult<FormattedResponse<CachedNodesResponse<SemiSkimmedNode>>> {
Err(AxumErrorResponse::not_implemented())
}
@@ -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,15 +15,18 @@ use nym_api_requests::nym_nodes::{
CachedNodesResponse, NodeRole, NodeRoleQueryParam, PaginatedCachedNodesResponse, SkimmedNode,
};
use nym_api_requests::pagination::PaginatedResponse;
use nym_http_api_common::{FormattedResponse, Output};
use nym_mixnet_contract_common::NodeId;
use nym_topology::CachedEpochRewardedSet;
use std::collections::HashMap;
use std::future::Future;
use std::time::Duration;
use tokio::sync::RwLockReadGuard;
use tracing::trace;
use utoipa::ToSchema;
pub type PaginatedSkimmedNodes = AxumResult<Json<PaginatedCachedNodesResponse<SkimmedNode>>>;
pub type PaginatedSkimmedNodes =
AxumResult<FormattedResponse<PaginatedCachedNodesResponse<SkimmedNode>>>;
/// Given all relevant caches, build part of response for JUST Nym Nodes
fn build_nym_nodes_response<'a, NI>(
@@ -97,6 +99,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.)
@@ -124,18 +127,20 @@ where
// (ideally it'd be tied directly to the NI iterator, but I couldn't defeat the compiler)
let describe_cache = state.describe_nodes_cache_data().await?;
let maybe_interval = state
let Some(interval) = state
.nym_contract_cache()
.current_interval()
.await
.to_owned();
.to_owned()
else {
// if we can't obtain interval information, it means caches are not valid
return Err(AxumErrorResponse::service_unavailable());
};
// 4.0 If the client indicates that they already know about the current topology send empty response
if let Some(client_known_epoch) = query_params.epoch_id {
if let Some(ref interval) = maybe_interval {
if client_known_epoch == interval.current_epoch_id() {
return Ok(Json(PaginatedCachedNodesResponse::no_updates()));
}
if client_known_epoch == interval.current_epoch_id() {
return Ok(output.to_response(PaginatedCachedNodesResponse::no_updates()));
}
}
@@ -152,8 +157,8 @@ where
describe_cache.timestamp(),
]);
return Ok(Json(
PaginatedCachedNodesResponse::new_full(refreshed_at, nodes).fresh(maybe_interval),
return Ok(output.to_response(
PaginatedCachedNodesResponse::new_full(refreshed_at, nodes).fresh(Some(interval)),
));
}
@@ -176,9 +181,19 @@ where
annotated_legacy_nodes.timestamp(),
]);
Ok(Json(
PaginatedCachedNodesResponse::new_full(refreshed_at, nodes).fresh(maybe_interval),
))
let base_response = output.to_response(
PaginatedCachedNodesResponse::new_full(refreshed_at, nodes).fresh(Some(interval)),
);
if !active_only {
return Ok(base_response);
}
// if caller requested only active nodes, the response is valid until the epoch changes
// (but add 2 minutes due to epoch transition not being instantaneous
let epoch_end = interval.current_epoch_end();
let expiration = epoch_end + Duration::from_secs(120);
Ok(base_response.with_expires_header(expiration))
}
/// Deprecated query that gets ALL gateways
@@ -189,22 +204,30 @@ where
path = "/gateways/skimmed",
context_path = "/v1/unstable/nym-nodes",
responses(
(status = 200, body = CachedNodesResponse<SkimmedNode>)
)
(status = 200, content(
(CachedNodesResponse<SkimmedNode> = "application/json"),
(CachedNodesResponse<SkimmedNode> = "application/yaml"),
(CachedNodesResponse<SkimmedNode> = "application/bincode")
))
),
)]
#[deprecated(note = "use '/v1/unstable/nym-nodes/entry-gateways/skimmed/all' instead")]
pub(super) async fn deprecated_gateways_basic(
state: State<AppState>,
query_params: Query<NodesParams>,
) -> AxumResult<Json<CachedNodesResponse<SkimmedNode>>> {
) -> AxumResult<FormattedResponse<CachedNodesResponse<SkimmedNode>>> {
let output = query_params.output.unwrap_or_default();
// 1. call '/v1/unstable/skimmed/entry-gateways/all'
let all_gateways = entry_gateways_basic_all(state, query_params).await?;
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 +239,40 @@ pub(super) async fn deprecated_gateways_basic(
path = "/mixnodes/skimmed",
context_path = "/v1/unstable/nym-nodes",
responses(
(status = 200, body = CachedNodesResponse<SkimmedNode>)
)
(status = 200, content(
(CachedNodesResponse<SkimmedNode> = "application/json"),
(CachedNodesResponse<SkimmedNode> = "application/yaml"),
(CachedNodesResponse<SkimmedNode> = "application/bincode")
))
),
)]
#[deprecated(note = "use '/v1/unstable/nym-nodes/skimmed/mixnodes/active' instead")]
pub(super) async fn deprecated_mixnodes_basic(
state: State<AppState>,
query_params: Query<NodesParams>,
) -> AxumResult<Json<CachedNodesResponse<SkimmedNode>>> {
) -> AxumResult<FormattedResponse<CachedNodesResponse<SkimmedNode>>> {
let output = query_params.output.unwrap_or_default();
// 1. call '/v1/unstable/nym-nodes/skimmed/mixnodes/active'
let active_mixnodes = mixnodes_basic_active(state, query_params).await?;
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<AppState>,
Query(_query_params): Query<NodesParams>,
Query(query_params): Query<NodesParams>,
active_only: bool,
) -> PaginatedSkimmedNodes {
let output = query_params.output.unwrap_or_default();
// unfortunately we have to build the response semi-manually here as we need to add two sources of legacy nodes
// 1. grab all relevant described nym-nodes
@@ -281,10 +314,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 +335,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<AppState>,
@@ -338,8 +372,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<AppState>,
@@ -367,6 +405,8 @@ async fn mixnodes_basic(
query_params: Query<NodesParams>,
active_only: bool,
) -> PaginatedSkimmedNodes {
let output = query_params.output.unwrap_or_default();
// 1. grab all relevant described nym-nodes
let describe_cache = state.describe_nodes_cache_data().await?;
let mixing_nym_nodes = describe_cache.mixing_nym_nodes();
@@ -377,6 +417,7 @@ async fn mixnodes_basic(
mixing_nym_nodes,
|state| state.legacy_mixnode_annotations(),
active_only,
output,
)
.await
}
@@ -390,8 +431,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<AppState>,
@@ -409,8 +454,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<AppState>,
@@ -424,6 +473,8 @@ async fn entry_gateways_basic(
query_params: Query<NodesParams>,
active_only: bool,
) -> PaginatedSkimmedNodes {
let output = query_params.output.unwrap_or_default();
// 1. grab all relevant described nym-nodes
let describe_cache = state.describe_nodes_cache_data().await?;
let mixing_nym_nodes = describe_cache.entry_capable_nym_nodes();
@@ -434,6 +485,7 @@ async fn entry_gateways_basic(
mixing_nym_nodes,
|state| state.legacy_gateways_annotations(),
active_only,
output,
)
.await
}
@@ -447,8 +499,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<AppState>,
@@ -466,8 +522,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<AppState>,
@@ -481,6 +541,8 @@ async fn exit_gateways_basic(
query_params: Query<NodesParams>,
active_only: bool,
) -> PaginatedSkimmedNodes {
let output = query_params.output.unwrap_or_default();
// 1. grab all relevant described nym-nodes
let describe_cache = state.describe_nodes_cache_data().await?;
let mixing_nym_nodes = describe_cache.exit_capable_nym_nodes();
@@ -491,6 +553,7 @@ async fn exit_gateways_basic(
mixing_nym_nodes,
|state| state.legacy_gateways_annotations(),
active_only,
output,
)
.await
}
@@ -504,8 +567,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<AppState>,
@@ -523,8 +590,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<AppState>,
+40 -14
View File
@@ -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<AppState> {
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<AppState>) -> Json<ApiHealthResponse> {
async fn health(
Query(output): Query<OutputParams>,
State(state): State<AppState>,
) -> FormattedResponse<ApiHealthResponse> {
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<AppState>) -> Json<ApiHealthResponse> {
chain_status,
uptime: uptime.as_secs(),
};
Json(health)
output.to_response(health)
}
#[utoipa::path(
@@ -69,13 +79,21 @@ async fn health(State(state): State<AppState>) -> Json<ApiHealthResponse> {
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<OutputParams>,
State(state): State<ApiStatusState>,
) -> Json<BinaryBuildInformationOwned> {
Json(state.build_information.to_owned())
) -> FormattedResponse<BinaryBuildInformationOwned> {
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<OutputParams>,
State(state): State<ApiStatusState>,
) -> AxumResult<Json<SignerInformationResponse>> {
) -> AxumResult<FormattedResponse<SignerInformationResponse>> {
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(),
+3 -2
View File
@@ -1,14 +1,15 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// 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<Output>,
pub page: Option<u32>,
pub per_page: Option<u32>,
}
@@ -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"]
@@ -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
@@ -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?;
+1 -1
View File
@@ -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());
+1 -1
View File
@@ -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,
},
+11 -1
View File
@@ -4036,7 +4036,6 @@ version = "0.6.0"
dependencies = [
"const-str",
"log",
"pretty_env_logger",
"schemars",
"serde",
"utoipa",
@@ -4186,12 +4185,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 +4203,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"