Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7a448e87d5 | |||
| aa58c80db9 | |||
| 7a9c41ad73 | |||
| ea0d2f8c66 | |||
| e849e608b2 | |||
| 3f6acbfd66 | |||
| 899a2bfc8a | |||
| 3049abf4f1 | |||
| 1dc42df59c | |||
| e2b85c91df | |||
| 796a7fba0a | |||
| fbcf44eeb9 | |||
| f4785099c2 | |||
| b04d3ba376 | |||
| b2dfdda210 | |||
| 41ef3a26f5 | |||
| bae1b488de | |||
| 40cf2c441a | |||
| 34871b14b3 | |||
| c14b010f9e |
@@ -59,3 +59,6 @@ nym-api/redocly/formatted-openapi.json
|
||||
|
||||
*.sqlite
|
||||
.build
|
||||
|
||||
**/settings.sql
|
||||
**/enter_db.sh
|
||||
|
||||
@@ -4,6 +4,20 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [2025.9-appenzeller] (2025-05-13)
|
||||
|
||||
- build(deps): bump clap from 4.5.36 to 4.5.37 in the patch-updates group ([#5722])
|
||||
- build(deps): bump golang.org/x/net from 0.36.0 to 0.38.0 in /wasm/mix-fetch/go-mix-conn ([#5720])
|
||||
- build(deps-dev): bump http-proxy-middleware from 2.0.6 to 2.0.9 in /wasm/client/internal-dev ([#5719])
|
||||
- Add /account/{address} ([#5673])
|
||||
- Add contains ticketbook data db query ([#5670])
|
||||
|
||||
[#5722]: https://github.com/nymtech/nym/pull/5722
|
||||
[#5720]: https://github.com/nymtech/nym/pull/5720
|
||||
[#5719]: https://github.com/nymtech/nym/pull/5719
|
||||
[#5673]: https://github.com/nymtech/nym/pull/5673
|
||||
[#5670]: https://github.com/nymtech/nym/pull/5670
|
||||
|
||||
## [2025.8-tourist] (2025-04-29)
|
||||
|
||||
- add reserved byte to reply surb serialisation ([#5731])
|
||||
|
||||
Generated
+19
-13
@@ -1211,9 +1211,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.36"
|
||||
version = "4.5.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2df961d8c8a0d08aa9945718ccf584145eee3f3aa06cddbeac12933781102e04"
|
||||
checksum = "eccb054f56cbd38340b380d4a8e69ef1f02f1af43db2f0cc817a4774d80ae071"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
@@ -1221,9 +1221,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.36"
|
||||
version = "4.5.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "132dbda40fb6753878316a489d5a1242a8ef2f0d9e47ba01c951ea8aa7d013a5"
|
||||
checksum = "efd9466fac8543255d3b1fcad4762c5e116ffe808c8a3043d4263cd4fd4862a2"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@@ -4733,7 +4733,7 @@ checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
|
||||
|
||||
[[package]]
|
||||
name = "nym-api"
|
||||
version = "1.1.56"
|
||||
version = "1.1.57"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -4760,6 +4760,7 @@ dependencies = [
|
||||
"humantime-serde",
|
||||
"itertools 0.14.0",
|
||||
"k256",
|
||||
"moka",
|
||||
"nym-api-requests",
|
||||
"nym-bandwidth-controller",
|
||||
"nym-bin-common",
|
||||
@@ -4987,7 +4988,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-cli"
|
||||
version = "1.1.54"
|
||||
version = "1.1.55"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64 0.22.1",
|
||||
@@ -5070,7 +5071,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-client"
|
||||
version = "1.1.54"
|
||||
version = "1.1.55"
|
||||
dependencies = [
|
||||
"bs58",
|
||||
"clap",
|
||||
@@ -5785,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",
|
||||
@@ -5808,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",
|
||||
@@ -6068,7 +6072,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-network-requester"
|
||||
version = "1.1.55"
|
||||
version = "1.1.56"
|
||||
dependencies = [
|
||||
"addr",
|
||||
"anyhow",
|
||||
@@ -6119,7 +6123,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-node"
|
||||
version = "1.10.0"
|
||||
version = "1.11.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
@@ -6252,7 +6256,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-node-status-api"
|
||||
version = "2.1.0"
|
||||
version = "2.3.0"
|
||||
dependencies = [
|
||||
"ammonia",
|
||||
"anyhow",
|
||||
@@ -6269,6 +6273,8 @@ dependencies = [
|
||||
"nym-contracts-common",
|
||||
"nym-crypto",
|
||||
"nym-http-api-client",
|
||||
"nym-http-api-common",
|
||||
"nym-mixnet-contract-common",
|
||||
"nym-network-defaults",
|
||||
"nym-node-metrics",
|
||||
"nym-node-requests",
|
||||
@@ -6513,7 +6519,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-socks5-client"
|
||||
version = "1.1.54"
|
||||
version = "1.1.55"
|
||||
dependencies = [
|
||||
"bs58",
|
||||
"clap",
|
||||
@@ -7117,7 +7123,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nymvisor"
|
||||
version = "0.1.19"
|
||||
version = "0.1.20"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
|
||||
+1
-1
@@ -211,7 +211,7 @@ chacha20 = "0.9.0"
|
||||
chacha20poly1305 = "0.10.1"
|
||||
chrono = "0.4.40"
|
||||
cipher = "0.4.3"
|
||||
clap = "4.5.36"
|
||||
clap = "4.5.37"
|
||||
clap_complete = "4.5"
|
||||
clap_complete_fig = "4.5"
|
||||
colored = "2.2"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nym-client"
|
||||
version = "1.1.54"
|
||||
version = "1.1.55"
|
||||
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
|
||||
description = "Implementation of the Nym Client"
|
||||
edition = "2021"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nym-socks5-client"
|
||||
version = "1.1.54"
|
||||
version = "1.1.55"
|
||||
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
|
||||
description = "A SOCKS5 localhost proxy that converts incoming messages to Sphinx and sends them to a Nym address"
|
||||
edition = "2021"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -9,13 +9,35 @@ use axum::response::IntoResponse;
|
||||
use axum_client_ip::InsecureClientIp;
|
||||
use colored::Colorize;
|
||||
use std::time::Instant;
|
||||
use tracing::info;
|
||||
use tracing::{debug, info};
|
||||
|
||||
enum LogLevel {
|
||||
Debug,
|
||||
Info,
|
||||
}
|
||||
|
||||
pub async fn log_request_info(
|
||||
insecure_client_ip: InsecureClientIp,
|
||||
request: Request,
|
||||
next: Next,
|
||||
) -> impl IntoResponse {
|
||||
log_request(insecure_client_ip, request, next, LogLevel::Info).await
|
||||
}
|
||||
|
||||
pub async fn log_request_debug(
|
||||
insecure_client_ip: InsecureClientIp,
|
||||
request: Request,
|
||||
next: Next,
|
||||
) -> impl IntoResponse {
|
||||
log_request(insecure_client_ip, request, next, LogLevel::Debug).await
|
||||
}
|
||||
|
||||
/// Simple logger for requests
|
||||
pub async fn logger(
|
||||
async fn log_request(
|
||||
InsecureClientIp(addr): InsecureClientIp,
|
||||
request: Request,
|
||||
next: Next,
|
||||
level: LogLevel,
|
||||
) -> impl IntoResponse {
|
||||
// TODO dz use `OriginalUri` extractor to get full URI even for nested
|
||||
// routers if routes aren't logged correctly in handlers
|
||||
@@ -58,7 +80,14 @@ pub async fn logger(
|
||||
|
||||
let agent_str = "agent".bold();
|
||||
|
||||
info!("[{addr} -> {host}] {method} '{uri}': {print_status} {time_taken} {agent_str}: {agent}");
|
||||
match level {
|
||||
LogLevel::Debug => debug!(
|
||||
"[{addr} -> {host}] {method} '{uri}': {print_status} {time_taken} {agent_str}: {agent}"
|
||||
),
|
||||
LogLevel::Info => info!(
|
||||
"[{addr} -> {host}] {method} '{uri}': {print_status} {time_taken} {agent_str}: {agent}"
|
||||
),
|
||||
}
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import StakingTarget from 'components/outputs/api-scraping-outputs/nyx-outputs/staking-target.md';
|
||||
import StakingScaleFactor from 'components/outputs/api-scraping-outputs/nyx-outputs/staking-scale-factor.md';
|
||||
import StakeSaturation from 'components/outputs/api-scraping-outputs/nyx-outputs/stake-saturation.md';
|
||||
import CirculatingSupply from 'components/outputs/api-scraping-outputs/nyx-outputs/circulating-supply.md';
|
||||
import { Callout } from 'nextra/components';
|
||||
|
||||
Stake saturation is a node reputation done in a form of self bond or stakers delegation. Optimal stake saturation level is calculated as:
|
||||
|
||||
<Callout type="info" borderColor="#008080" backgroundColor="#20b2aa" emoji="📌">
|
||||
> **stake_saturation_level = staking_target / rewarded_set_size**
|
||||
>
|
||||
> **rewarded_set_size = active_set_size + standby_set_size**
|
||||
</Callout>
|
||||
|
||||
With current circulating supply of <span style={{display: 'inline-block'}}><CirculatingSupply /></span> NYM, staking target of <span style={{display: 'inline-block'}}><StakingTarget /></span> NYM, divided by the sum of nodes in the [rewarded set](https://validator.nymtech.net/api/v1/epoch/reward_params), <b>the stake saturation level is <span style={{display: 'inline-block'}}><StakeSaturation /></span> NYM per node.</b>
|
||||
|
||||
Node stake saturation is a value between `0` and `1` following this logic.
|
||||
|
||||
**Node stake saturation formula:**
|
||||
|
||||
<Callout type="info" borderColor="#008080" backgroundColor="#20b2aa" emoji="📌">
|
||||
> **node_stake_saturation = node_total_stake / stake_saturation_level**
|
||||
</Callout>
|
||||
|
||||
There is a caveat that the maximum value can be `1`. In practice it means that:
|
||||
|
||||
1. If `node_total_stake < stake_saturation_level` then `node_stake_saturation` will be a float between `0` and `1`
|
||||
|
||||
2. If `node_total_stake = stake_saturation_level` then `node_stake_saturation` will be `1`
|
||||
|
||||
3. If `node_total_stake > stake_saturation_level` then `node_stake_saturation` will be `1` due the capping function working as anti-whale prevention.
|
||||
- This results in smaller % APY per every staked (self bond or delegation) NYM token on that node, as the maximum rewards is capped and in this case distributed in between more staked tokens.
|
||||
- For example if `node_total_stake = 2 * stake_saturation_level` then the reward per staked token will be 50% in comparison to a case where `node_total_stake = stake_saturation_level`, in other words with 100% *over-saturation*, APY is half the maximum.
|
||||
+1
@@ -0,0 +1 @@
|
||||
5_278
|
||||
@@ -1 +1 @@
|
||||
Wednesday, April 16th 2025, 12:32:08 UTC
|
||||
Wednesday, April 30th 2025, 12:26:39 UTC
|
||||
|
||||
@@ -47,9 +47,72 @@ This page displays a full list of all the changes during our release cycle from
|
||||
|
||||
<VarInfo />
|
||||
|
||||
## `v2025.8-tourist`
|
||||
|
||||
- [Release Binaries](https://github.com/nymtech/nym/releases/tag/nym-binaries-v2025.8-tourist)
|
||||
- [`nym-node`](nodes/nym-node.mdx) version `1.10.0`
|
||||
|
||||
```shell
|
||||
nym-node
|
||||
Binary Name: nym-node
|
||||
Build Timestamp: 2025-04-29T11:36:50.614557168Z
|
||||
Build Version: 1.10.0
|
||||
Commit SHA: e594630314d4676cbe9bba9ab07bd405a9cf679a
|
||||
Commit Date: 2025-04-29T12:19:54.000000000+02:00
|
||||
Commit Branch: HEAD
|
||||
rustc Version: 1.86.0
|
||||
rustc Channel: stable
|
||||
cargo Profile: release
|
||||
```
|
||||
### Operators Updates & Tools
|
||||
|
||||
- [New Nym wallet](https://github.com/nymtech/nym/releases/tag/nym-wallet-v1.2.18) is out: Release version `1.2.18`
|
||||
|
||||
**Documentation Updates**
|
||||
|
||||
- [Tokenomics page](tokenomics.mdx) updated
|
||||
- [Node operators rewards page](tokenomics/mixnet-rewards.mdx) updated with new parts added:
|
||||
- [Node performance calculation](tokenomics/mixnet-rewards#node-performance-calculation)
|
||||
- [Stake saturation calculation](tokenomics/mixnet-rewards#stake-saturation)
|
||||
- [Rewards calculation](tokenomics/mixnet-rewards#rewards-calculation)
|
||||
- [Rewards distribution](tokenomics/mixnet-rewards#rewards-distribution)
|
||||
|
||||
### Features
|
||||
|
||||
- [Adding fresh nym-api tests and workflow](https://github.com/nymtech/nym/pull/5659)
|
||||
|
||||
- [Replay protection](https://github.com/nymtech/nym/pull/5682): Introduces replay detection into a `nym-node`. Currently it uses a bloomfilter that's reset every 25h. In the future this will be controlled by the key rotation. The bloomfilter is also periodically flushed to disk in order to be able to recover from a crash/shutdown.
|
||||
|
||||
- [Tauri V2 - Wallet Migration](https://github.com/nymtech/nym/pull/5687):
|
||||
- The core of the lifting was done via the migrate command
|
||||
- A lot of API's changed
|
||||
- Improved styling
|
||||
- Pipelines are fixed for all platforms
|
||||
|
||||
- [Make mix hops optional for Mixnet Client](https://github.com/nymtech/nym/pull/5696): As is the route selection for packets, reply SURBs, and acknowledgements are selected deep withing the `MixnetClient` machinery. This makes custom route selection (i.e. for authenticating / registering wiregaurd mode vpn clients) is not supported. This PR adds configuration toggle that allows `MixnetClient` to be built where packets are sent direct through entry to Exit Gateway nodes -- no mix hops.
|
||||
|
||||
- [Bump the `nym-vpn deb` metapackage to `1.0`](https://github.com/nymtech/nym/pull/5697)
|
||||
|
||||
- [Updated sphinx payload keys](https://github.com/nymtech/nym/pull/5698): This PR uses the new version of the `sphinx-packet` that allows us to derive payload encryption keys from the seed rather than having to attach the keys themselves. In practice each reply surb is now ~60% smaller. However, currently all of those functionalities are **DISABLED** by default. It is because it required nodes to actually understand the new scheme. it shouldn't be enabled until sufficient number of nodes, particularly mixnodes, had upgraded.
|
||||
|
||||
- [Allow copy and paste on logins fields for the wallet](https://github.com/nymtech/nym/pull/5699): Allow shell open for url links (some platforms it's not working as expected)
|
||||
|
||||
- [Removed old explorer-api](https://github.com/nymtech/nym/pull/5701)
|
||||
|
||||
- [Peer handle should die more gracefully](https://github.com/nymtech/nym/pull/5704)
|
||||
|
||||
- [Update `hickory` DNS `0.24.4` to `0.25`](https://github.com/nymtech/nym/pull/5709): Update the dependency on `hickory` DNS to the latest minor version
|
||||
|
||||
- [Remove inactive peers](https://github.com/nymtech/nym/pull/5721)
|
||||
|
||||
- [Add reserved byte to reply surb serialisation](https://github.com/nymtech/nym/pull/5731)
|
||||
|
||||
### Bugfix
|
||||
|
||||
|
||||
## `v2025.7-tex`
|
||||
|
||||
- [Release Binaries](https://github.com/nymtech/nym/releases/tag/nym-binaries-v2025.9-tex)
|
||||
- [Release Binaries](https://github.com/nymtech/nym/releases/tag/nym-binaries-v2025.7-tex)
|
||||
- [`nym-node`](nodes/nym-node.mdx) version `1.9.0`
|
||||
|
||||
```shell
|
||||
|
||||
@@ -20,10 +20,10 @@ This documentation page provides a guide on how to set up and run a [NYM NODE](.
|
||||
```sh
|
||||
nym-node
|
||||
Binary Name: nym-node
|
||||
Build Timestamp: 2025-04-15T14:36:52.729991996Z
|
||||
Build Version: 1.9.0
|
||||
Commit SHA: 08b6be93c49e8c225e74ffabb5529493bd4b13b6
|
||||
Commit Date: 2025-04-15T15:29:46.000000000+02:00
|
||||
Build Timestamp: 2025-04-29T11:36:50.614557168Z
|
||||
Build Version: 1.10.0
|
||||
Commit SHA: e594630314d4676cbe9bba9ab07bd405a9cf679a
|
||||
Commit Date: 2025-04-29T12:19:54.000000000+02:00
|
||||
Commit Branch: HEAD
|
||||
rustc Version: 1.86.0
|
||||
rustc Channel: stable
|
||||
|
||||
@@ -11,6 +11,8 @@ import TokenTable from 'components/outputs/api-scraping-outputs/nyx-outputs/toke
|
||||
import StakingTarget from 'components/outputs/api-scraping-outputs/nyx-outputs/staking-target.md';
|
||||
import StakingScaleFactor from 'components/outputs/api-scraping-outputs/nyx-outputs/staking-scale-factor.md';
|
||||
import StakeSaturation from 'components/outputs/api-scraping-outputs/nyx-outputs/stake-saturation.md';
|
||||
import StakeSaturationSnippet from 'components/operators/snippets/stake-saturation.mdx';
|
||||
import EpochRewardBudget from 'components/outputs/api-scraping-outputs/nyx-outputs/epoch-reward-budget.md';
|
||||
import { TimeNow } from 'components/time-now.tsx';
|
||||
import { AccordionTemplate } from 'components/accordion-template.tsx';
|
||||
|
||||
@@ -83,7 +85,7 @@ Below is a table with token supply distribution.
|
||||
|
||||
<TokenTable />
|
||||
|
||||
To get live data, visit [Nym token page](https://nym.com.net/about/token) or see how to [query API endpoints](#query-tokenomics-api).
|
||||
To get live data, visit [SpectreDAO token dashboard](https://explorer.nym.spectredao.net/token) or see how to [query API endpoints](#query-tokenomics-api).
|
||||
|
||||
### Calculation & Explanation
|
||||
|
||||
@@ -95,7 +97,7 @@ To get a full comprehension of [node operators rewards](tokenomics/mixnet-reward
|
||||
│ │ supply │ │ nym nodes │ │
|
||||
│circulating│ scale │ staking │ in rewarded │ stake │
|
||||
│ supply │ factor │ target │ set │saturation │
|
||||
│ ├────────────►│ ├──────────────►│ │
|
||||
│ ├────────────►│ ├──────────────►│ level │
|
||||
└───────────┘ └───────────┘ └───────────┘
|
||||
|
||||
```
|
||||
@@ -121,20 +123,11 @@ The value of this variable can be changed to optimize the metrics of the network
|
||||
|
||||
#### Stake saturation
|
||||
|
||||
Node reputation in a form of self bond or stakers delegation. Stake saturation is calculated as:
|
||||
<StakeSaturationSnippet />
|
||||
|
||||
<Callout type="info" borderColor="#008080" backgroundColor="#20b2aa" emoji="📌">
|
||||
> **stake_saturation = staking_target / rewarded_set_size**
|
||||
>
|
||||
> **rewarded_set_size = active_set_size + standby_set_size**
|
||||
</Callout>
|
||||
#### Rewarded Set
|
||||
|
||||
|
||||
With current circulating supply of <span style={{display: 'inline-block'}}><CirculatingSupply /></span> NYM, staking target of <span style={{display: 'inline-block'}}><StakingTarget /></span> NYM, divided by the sum of nodes in the [rewarded set](https://validator.nymtech.net/api/v1/epoch/reward_params), <b>the stake saturation level is <span style={{display: 'inline-block'}}><StakeSaturation /></span> NYM per node.</b>
|
||||
|
||||
|
||||
|
||||
#### Active set
|
||||
> To read more about rewards calculation, please see next page [*Nym Operators Rewards*](tokenomics/mixnet-rewards.mdx) or you can go directly into details about [Rewarded set selection logic](tokenomics/mixnet-rewards#rewarded-set-selection).
|
||||
|
||||
Nym Network needs an optimised number of nodes to route and mix the packets. This healthy balance lies in between being too congested - which would detriment speed and user experience - on one side, and having too little traffic per node - which would could weaken anonymity - on the other.
|
||||
|
||||
@@ -146,22 +139,19 @@ The way how we approach this challenge is different for Mixnet (5-hop) and dVPN
|
||||
<strong>dVPN mode</strong>,
|
||||
]} defaultIndex="0">
|
||||
<MyTab>
|
||||
Nym Mixnet is using an active set of chosen nodes. Currently <b>the [active set size](https://validator.nymtech.net/api/v1/epoch/reward_params) is 240 nodes</b>, 120 with Gateway functionality: 50 entry (1st layer) and 70 exit (5th layer) and 120 as Mixnode (2nd, 3rd and 4th mixing layer). The active set is chosen in the beginning of each epoch (60min). The best performing and reputated (optimal stake saturation) nodes are chosen. Performance is much more ample as you can see in the formula below:
|
||||
Nym Mixnet is using an active set of chosen nodes. Currently <b>the [active set size](https://validator.nymtech.net/api/v1/epoch/reward_params) is 240 nodes</b>, 120 with Gateway functionality: 50 entry (1st layer) and 70 exit (5th layer) and 120 as Mixnode (2nd, 3rd and 4th mixing layer). The active set is chosen in the beginning of each epoch (60min).
|
||||
|
||||
<Callout type ="info" borderColor="#008080" backgroundColor="#20b2aa" emoji="📌">
|
||||
> **active_set_selection_probability = ( node_performance ^ 20 ) * stake_saturation**
|
||||
</Callout>
|
||||
|
||||
The alorithm for selecting the nodes into the Rewarded set is in detail explained in the [Rewarded set selection logic part](tokenomics/mixnet-rewards#rewarded-set-selection).
|
||||
</MyTab>
|
||||
<MyTab>
|
||||
In dVPN (2-hop) mode every node which meets the performance criteria, including wireguard and IPv6 routing tests, becomes eligible to take part in the network. Whether the node is working on not then depends on the end users choise of the location or exact nodes selection.
|
||||
In dVPN (2-hop) mode every node which meets the [performance criteria](tokenomics/mixnet-rewards#node-performance-calculation), including wireguard and IPv6 routing tests, becomes eligible to take part in the network. Whether the node is working or not then depends on the NymVPN end users choise of the location or exact nodes selection.
|
||||
</MyTab>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
In both cases, the selection algorithm also looks whether the node runs with [Terms & Conditions](nodes/nym-node/setup.mdx#terms--conditions) accepted **AND** if it's not a legacy binary version. In case either of these criterias are not met, the node will have be excluded from the rewarded set selection.
|
||||
In both cases, the selection algorithm also looks whether the node runs with [Terms & Conditions](nodes/nym-node/setup.mdx#terms--conditions) accepted **AND** if it's not a legacy binary version. In case either of these criterias are not met, the node will have be excluded from the [rewarded set selection](tokenomics/mixnet-rewards#rewarded-set-selection).
|
||||
|
||||
To read more about rewards calculation, please see next page [*Nym Operators Rewards*](tokenomics/mixnet-rewards.md).
|
||||
|
||||
## Query Validator API
|
||||
|
||||
@@ -170,20 +160,20 @@ We have available API endpoints which can be accessed via [Swagger UI page](http
|
||||
```sh
|
||||
curl -X 'GET' \
|
||||
'https://validator.nymtech.net/api/v1/circulating-supply' \
|
||||
-H 'accept: application/json'sh
|
||||
-H 'accept: application/json'
|
||||
|
||||
curl -X 'GET' \
|
||||
'https://validator.nymtech.net/api/v1/circulating-supply/total-supply-value' \
|
||||
-H 'accept: application/json'sh
|
||||
-H 'accept: application/json'
|
||||
|
||||
|
||||
curl -X 'GET' \
|
||||
'https://validator.nymtech.net/api/v1/circulating-supply-value' \
|
||||
-H 'accept: application/json'sh
|
||||
-H 'accept: application/json'
|
||||
|
||||
curl -X 'GET' \
|
||||
'https://validator.nymtech.net/api/v1/epoch/reward_params' \
|
||||
-H 'accept: application/json'sh
|
||||
-H 'accept: application/json'
|
||||
```
|
||||
|
||||
> The unit of value is measured in `uNYM`.
|
||||
|
||||
@@ -6,6 +6,8 @@ import { VarInfo } from 'components/variable-info.tsx';
|
||||
import { MigrateTabs } from 'components/operators/nodes/node-migrate-command-tabs';
|
||||
import NyxPercentStake from 'components/outputs/nyx-outputs/nyx-percent-stake.md';
|
||||
import NyxTotalStake from 'components/outputs/nyx-outputs/nyx-total-stake.md';
|
||||
import EpochRewardBudget from 'components/outputs/api-scraping-outputs/nyx-outputs/epoch-reward-budget.md';
|
||||
import StakeSaturationSnippet from 'components/operators/snippets/stake-saturation.mdx';
|
||||
import { TimeNow } from 'components/time-now.tsx';
|
||||
import { AccordionTemplate } from 'components/accordion-template.tsx';
|
||||
import { Clt } from 'components/callout-custom/CalloutCustom.jsx';
|
||||
@@ -19,9 +21,9 @@ import { Clt } from 'components/callout-custom/CalloutCustom.jsx';
|
||||
<TimeNow />
|
||||
|
||||
* Nym tokenomics are based on the research paper [*Reward Sharing for Mixnets*](https://nymtech.net/nym-cryptoecon-paper.pdf)
|
||||
* For a more comprehensive overview, live data and supply graphs, visit [*nymtech.net/about/token*](https://nymtech.net/about/token)
|
||||
* For a more comprehensive overview, live data and supply graphs, visit [*explorer.nym.spectredao.net/token*](https://explorer.nym.spectredao.net/token)
|
||||
|
||||
We are working on the final architecture of [*Fair Mixnet*](#fair-mixnet) tokenomics implementation and its detailed documentation. **The current design is called [*Naive rewarding*](#naive-rewarding).** It is an intermediate step, allowing operators to migrate to `nym-node` in Mixnet smart contract and for the first time recieve delegations and earn rewards for any `nym-node` functionality, in opposite to the past system, where only Mixnodes were able to recieve delegations and rewards.
|
||||
We are working on the final architecture of [*Fair Mixnet*](#fair-mixnet) tokenomics implementation and its detailed documentation. **The current design is called [*Naive rewarding*](#naive-rewarding).** It is an intermediate step, allowing operators to migrate to `nym-node` in Mixnet smart contract and for the first time recieve delegations and earn rewards for any `nym-node` [functionality](../nodes/nym-node/setup#functionality-mode), in opposite to the past system, where only Mixnodes were able to recieve delegations and rewards.
|
||||
|
||||
**Please read the [roadmap section below](#roadmap) to see the planned development.**
|
||||
|
||||
@@ -51,8 +53,8 @@ Nodes bonded with vesting tokens are [not allowed to join rewarded set](https://
|
||||
|
||||
This is a quick summary, to understand the full picture, please see detailed [*Rewards Logic & Calculation*](#rewards-logic--calculation) chapter below.
|
||||
|
||||
* The operators of `nym-node` get rewarded from Mixmining pool, which emits around 6000 NYM per hour.
|
||||
* A [rewarded set](../tokenomics.mdx#active-set) of `nym-nodes` selected for Nym network routing and mixing can be is currently 240 nodes in total and it's selected for each new epoch (60 min). The number can be adjusted - look here for the current value: [validator.nymtech.net/api/v1/epoch/reward_params](https://validator.nymtech.net/api/v1/epoch/reward_params)
|
||||
* The operators of `nym-node` get rewarded from [Mixmining pool](https://validator.nymtech.net/api/v1/epoch/reward_params), which emits <span style={{display: 'inline-block'}}><EpochRewardBudget /></span> NYM per hour.
|
||||
* The [rewarded set](../tokenomics.mdx#active-set) of `nym-nodes` selected for Nym network routing and mixing is currently 240 nodes in total and it's selected for each new epoch (60 min). The number can be adjusted - look here for the current value: [validator.nymtech.net/api/v1/epoch/reward_params](https://validator.nymtech.net/api/v1/epoch/reward_params)
|
||||
* `nym-nodes` can run in mode `entry-gateway`, `exit-gateway` and `mixnode`, which are positioned into layers
|
||||
* NymVPN users can chose to route through Nym Network in two ways:
|
||||
- Mixnet: 5 layers routing and mixing - full privacy
|
||||
@@ -61,14 +63,14 @@ This is a quick summary, to understand the full picture, please see detailed [*R
|
||||
* In the final model, nodes will get rewarded based on their layer position and the work they do (collected user tickets), where and the reward distribution per layer will be according to a [decision made by the operators](https://forum.nymtech.net/t/poll-what-should-be-the-split-of-mixmining-rewards-among-the-layers-of-the-nym-mixnet/407) as follows:
|
||||
- 5-hop: 16%-16%-16%-16%-36%
|
||||
- 2-hop: 33%-67%
|
||||
* Currently Gateways earn rewards only from taking a part in the rewarded set. The operators can sign up to a grant program as a substitution for 2-hop routing.
|
||||
* Currently Gateways earn rewards only from taking a part in the rewarded set. The operators can sign up to a grant program as a substitution for 2-hop (Wireguad) routing.
|
||||
* To read more about the final design and future implementation, see [*Roadmap*](#roadmap) chapter for more details.
|
||||
|
||||
## Rewards Logic & Calculation
|
||||
|
||||
**Note that in the current intermediate model we use one active set to reward all nodes and they are asign same (naive) work factor of 1 / 240, whether they work as Mixnode or Gateway of any kind, in both 2-hop and 5-hop mode. In reality it means that all nodes are rewarded within 5-hop reward scheme only.**
|
||||
**Note that in the current intermediate model we use one active set to reward all nodes and they are asigned same work factor of 1 / 240, whether they work as Mixnode or Gateway of any kind, in both 2-hop and 5-hop mode (hence *naive rewarding*). In reality it means that all nodes are rewarded within 5-hop reward scheme only.**
|
||||
|
||||
**However NymVPN client can chose any `nym-node --mode entry-gateway` and `--mode exit-gateway` in the network to route through the mixnet and as well as any of those which passed [wireguard probing test](https://harbourmaster.nymtech.net) to route as dVPN nodes.**
|
||||
**However NymVPN client can choose any `nym-node` with `--wireguard-enabled true` flag (which passed [wireguard probing test](https://harbourmaster.nymtech.net)) to route as dVPN Gateway, both entry and exit.**
|
||||
|
||||
### Nym Network rewarded set distribution
|
||||
|
||||
@@ -121,9 +123,8 @@ This is a quick summary, to understand the full picture, please see detailed [*R
|
||||
| **Network layer** | **1** | **2** |
|
||||
| :-- | :---: | :---: |
|
||||
| Node functionality in layer | Entry Gateway | Exit Gateway |
|
||||
| Naive rewarding: Nodes in [active set](tokenomics.mdx#active-set) | 50 | 70 |
|
||||
| Naive rewarding: Rewards distribution per node | 1 / 240 | 1 / 240 | 1 / 240 | 1 / 240 | 1 / 240 |
|
||||
| Final model: Active nodes | All following criteria for eligibility | All following criteria for eligibility |
|
||||
| Naive rewarding: Nodes in [active set](tokenomics.mdx#active-set) | only Mixnet mode | only Mixnet mode |
|
||||
| Naive rewarding: Rewards distribution per node | only Mixnet mode | only Mixnet mode |
|
||||
| Final model: Rewards distribution per node | 0.33 \* collected_user_tickets | 0.67 \* collected_user_tickets |
|
||||
|
||||
</MyTab>
|
||||
@@ -135,52 +136,171 @@ This is a quick summary, to understand the full picture, please see detailed [*R
|
||||
|
||||
For a node to be rewarded, the node must be part of a [Rewarded set](https://validator.nymtech.net/api/v1/epoch/reward_params) (which currently = active set) in the first place. The Rewarded set is freshly selected at the start of each epoch (every 60 min), and it consists of 240 Nym nodes that are probabilistically chosen from all the available nodes. These 240 nodes include 120 gateways and 120 mixnodes (40 for each of 3 mixnet layers).
|
||||
|
||||
Rewarded set nodes are randomly selected, and their selection chances increase with a node score that includes three parameters:
|
||||
Nodes selected into the rewarded set are chosen probabilisticaly are randomly selected, and their selection chances increase the larger nodes weight is. Weight value is always between `0` and `1` and it's calculated from these parameters:
|
||||
|
||||
1. [Config score](#config-score-calculation): highest (`1`) when the node is running the latest version of the software with [T&C's accepted](../nodes/nym-node/setup.mdx#terms--conditions)
|
||||
2. [Performance](#performance-calculation): highest (`1`) when the node is consistently online and correctly processes all the received traffic
|
||||
3. [Stake saturation](../tokenomics.mdx#stake-saturation): including bond and delegated stake
|
||||
1. [Performance](#node-performance-calculation): This value consists of:
|
||||
- [Config score](#config-score-calculation): highest (`1`) when the node is running the latest version of the software, has [T&C's accepted](../nodes/nym-node/setup.mdx#terms--conditions) and self described API endpoint available
|
||||
- [Routing ](#routing-score-calculation): highest (`1`) when the node is consistently online and correctly processes all the received traffic
|
||||
2. [Stake saturation](#stake-saturation): including bond and delegated stake
|
||||
|
||||
Besides these values, the API is also looking whether the node is bonded in Mixnet smart contract as a Nym Node or legacy node (Mixnode or Gateway). **Only nodes bonded as Nym Node in Mixnet smart contract can be selected to the Rewrded set, if you haven't migrated your node yet, please [follow these steps](../nodes/nym-node/bonding#migrate-to-nym-node-in-mixnet-smart-contract)!**
|
||||
|
||||
**The node score is calculated with this formula:**
|
||||
**Node weight is calculated with this formula:**
|
||||
|
||||
<Callout type="info" emoji="📌">
|
||||
> **active_set_selection_probability = total_stake \* (( config_score \* node_performance ) ^ 20 )**
|
||||
> **active_set_selection_weight = stake_saturation \* ( node_performance ^ 20 )**
|
||||
</Callout>
|
||||
|
||||
Note that the score helps prioritize some nodes over others. If all available nodes have the same score, then the selection is done uniformly at random. By raising the config and performance components to 20, values of these parameters that are below one incur a heavy penalization for the node’s selection chances.
|
||||
For the rewarded set selection weight, good [performance](#node-performance-calculation) is much more essential than [stake saturation](#stake-saturation), because it's lifted to 20th power in the selection algorhitm.
|
||||
|
||||
Besides these values, the API is also checks whether the node is bonded in Mixnet smart contract as a Nym Node or legacy node (Mixnode or Gateway). **Only nodes bonded as Nym Node in Mixnet smart contract can be selected to the Rewrded set. Thus, if you haven't migrated your node yet, please [follow these steps](../nodes/nym-node/bonding#migrate-to-nym-node-in-mixnet-smart-contract)!**
|
||||
For a comparison we made an example with 5 nodes, where first number is node performance and second stake saturation (assuming all of them [`config_score`](#config-score-calculation)) = 1 for simplification):
|
||||
|
||||
<br />
|
||||
<AccordionTemplate name="✏️ Calculation example: performance ^ 20 * node_stake_saturation">
|
||||
> node_1 = 1.00 ^ 20 \* 1.0 = **1** <br />
|
||||
> node_2 = 1.00 ^ 20 \* 0.5 = **0.5** <br />
|
||||
> node_3 = 0.99 ^ 20 \* 1.0 = **0.818** <br />
|
||||
> node_4 = 0.95 ^ 20 \* 1.0 = **0.358** <br />
|
||||
> node_5 = 0.90 ^ 20 \* 1.0 = **0.122** <br />
|
||||
</AccordionTemplate>
|
||||
|
||||
As you can see the performance is much more important during the Rewarded set selection. A node with 100% performance but only 50% stake saturation has much bigger chance to be chosen than a node with 95% performance and 100% stake saturation and incomparably bigger chance than 90% performing node with 100% stake saturation.
|
||||
|
||||
The nodes are chosen probababilistically in each epoch (60 min), so even nodes with lower performance will eventually be chosen, just much less often, as their chances decrease.
|
||||
Note that the score helps prioritize some nodes over others. If all available nodes have the same score, then the selection is done uniformly at random. By raising the node performance to 20, values of these parameters that are below one incur a heavy penalization for the node’s selection chances.
|
||||
|
||||
<br />
|
||||
<AccordionTemplate name="✏️ Explanation & Example: Rewarded set probabilistic selection">
|
||||
**Explanation**
|
||||
|
||||
The nodes are selected probabilistically, that means that even nodes with lower weight have a small chace to get slected. The probabilistic alorithm follows this logic:
|
||||
|
||||
1. Summarize all nodes weight together
|
||||
2. Make a random selection roll for the first slot in the active set
|
||||
3. If a node is selected, take all its weight away from the draft queue
|
||||
4. Repeat points 1. - 3. for each slot in the active set
|
||||
|
||||
**Example**
|
||||
|
||||
We know that nodes weight is a float between 0 and 1. For simplification we will use integers in this example and much smaller set.
|
||||
|
||||
- Total nodes: 8
|
||||
- Rewarded set: 4
|
||||
- Nodes weight:
|
||||
- node1 = 5
|
||||
- node2 = 5
|
||||
- node3 = 10
|
||||
- node4 = 10
|
||||
- node5 = 20
|
||||
- node6 = 40
|
||||
- node7 = 50
|
||||
- node8 = 60
|
||||
|
||||
1. Summarize all nodes weight together:
|
||||
```
|
||||
weight_total = 5 + 5 + 10 + 10 + 20 + 40 + 50 + 60
|
||||
weight_total = 200
|
||||
```
|
||||
2. Roll a dice from 1 to 200:
|
||||
- Imagine the nodes are in line each representing the weight like index:
|
||||
- node1 = 1-5
|
||||
- node2 = 6-10
|
||||
- node3 = 11-20
|
||||
- node4 = 21-30
|
||||
- node5 = 31-50
|
||||
- node6 = 51-90
|
||||
- node7 = 91-150
|
||||
- node8 = 151-200
|
||||
- Say the function resulted in number 170
|
||||
3. Add node8 to the rewarded set and take it out of the lottery, summarize all the weights again:
|
||||
```
|
||||
weight_total = 5 + 5 + 10 + 10 + 20 + 40 + 50
|
||||
weight_total = 140
|
||||
```
|
||||
4. Roll a dice from 1 to 140:
|
||||
- Say the function resulted in number 4
|
||||
5. Add node1 to the rewarded set and take it out of the lottery, summarize all the weights again:
|
||||
```
|
||||
weight_total = 5 + 10 + 10 + 20 + 40 + 50
|
||||
weight_total = 135
|
||||
```
|
||||
6. Roll a dice from 1 to 135:
|
||||
- Say the function resulted in number 72
|
||||
7. Add node6 to the rewarded set and take it out of the lottery, summarize all the weights again:
|
||||
```
|
||||
weight_total = 5 + 10 + 10 + 20 + 50
|
||||
weight_total = 95
|
||||
```
|
||||
8. Roll a dice from 1 to 95:
|
||||
- Say the function resulted in number 21
|
||||
9. Add node4 to the rewarded set
|
||||
10. Rewarded set of 4 nodes is selected with these nodes to be chosen:
|
||||
1. node8
|
||||
2. node1
|
||||
3. node6
|
||||
4. node4
|
||||
11. After an epoch - 60 minutes - pull all bonded nodes and repeat the exact same process with their current weights
|
||||
|
||||
In reality we have mixing nodes selected into 3 layers. To increase security, there is an additional function in place where a node cannot be assigned to the same layer in two following epochs.
|
||||
</AccordionTemplate>
|
||||
|
||||
Below we break down [performance calculation](#node-performance-calculation) and show examples.
|
||||
|
||||
|
||||
### Node Performance Calculation
|
||||
|
||||
Performance is a value between `0` and `1`. The final performance number is a result of multiplying [config score](#config-score-calculation) and [routing score](#routing-score-calculation).
|
||||
|
||||
<Callout type="info" emoji="📌">
|
||||
> **node_performance = config_score \* routing_score**
|
||||
</Callout>
|
||||
|
||||
Performance value is an average of last 24h.
|
||||
|
||||
<Callout>
|
||||
All parameters regarding performance score can be browsed or pull live from:
|
||||
|
||||
`https://validator.nymtech.net/api/v1/nym-nodes/annotation/<NODE_ID>`
|
||||
|
||||
In case you don't know your nodes `NODE_ID`, it's easy to find as long as your node is bonded. Visit [validator.nymtech.net/api/v1/nym-nodes/bonded](https://validator.nymtech.net/api/v1/nym-nodes/bonded) and search your node using `identity_key` or bonding Nyx account address (denoted as `owner`).
|
||||
</Callout>
|
||||
|
||||
#### Config Score Calculation
|
||||
|
||||
The nodes selection to the active set has a new parameter - `config_score`. Config score currently looks into three paramteres:
|
||||
Config score is in place to ensure that the node configuration is done properly so the node is eligible for taking part in Nym network. The API looks into these paramteres:
|
||||
|
||||
1. If the node binary is `nym-node` (not legacy `nym-mixnode` or `nym-gateway`): `1` if `True`, `0` if `False`
|
||||
2. If [Terms & Conditions](../nodes/nym-node/setup.mdx#terms--conditions) are accepted: `1` if `True`, `0` if `False`
|
||||
3. Version of `nym-node` binary: decreasing weight for outdated versions, as explained below
|
||||
3. If the nodes self described endpoint is available: `1` if `True`, `0` if `False`
|
||||
4. Version of `nym-node` binary: decreasing weight for outdated versions, as [explained below](#versions-behind-calculation)
|
||||
|
||||
**The `config_score` parameter calculation formula:**
|
||||
**The `config_score` calculation formula:**
|
||||
|
||||
<Callout type="info" emoji="📌">
|
||||
> **config_score = is_tc_accepted \* is_nym-node_binary \* ( 0.995 ^ ( ( X * versions_behind) ^ 1.65 ) )**
|
||||
> **config_score = is_tc_accepted \* is_nym-node_binary \* self_described_api_available \* ( 0.995 ^ ( ( X * versions_behind) ^ 1.65 ) )**
|
||||
</Callout>
|
||||
|
||||
First two points have binary values of either 0 or 1, with a following logic:
|
||||
First three points have binary values of either `0` or `1`, with a following logic:
|
||||
|
||||
| **Run `nym-node` binary** | **T&C's accepted** | **Value** |
|
||||
| :-- | :-- | ---: |
|
||||
| True | True | 1 |
|
||||
| True | False | 0 |
|
||||
| False | True | 0 |
|
||||
| False | False | 0 |
|
||||
| **Run `nym-node` binary** | **T&C's accepted** | **Self described available** | **Value** |
|
||||
| :-- | :-- | :-- | ---: |
|
||||
| **True** | **True** | **True** | **1** |
|
||||
| True | False | False | 0 |
|
||||
| True | True | False | 0 |
|
||||
| False | True | True | 0 |
|
||||
| False | False | True | 0 |
|
||||
| True | False | True | 0 |
|
||||
| False | False | False | 0 |
|
||||
| False | True | False | 0 |
|
||||
|
||||
Only if both conditions above are `True` the node can have any chance to be selected, as otherwise the probability will always be 0.
|
||||
**Only if ALL conditions above are `True` the node can have any chance to be selected, as otherwise the probability will always be 0.**
|
||||
|
||||
**The `versions_behind` parameter in `config_score` calculation**
|
||||
<Callout type="info" emoji="ℹ️">
|
||||
Besides these values, the API also checks whether the node is bonded in Mixnet smart contract as a Nym Node or legacy node (Mixnode or Gateway). **Only nodes bonded as Nym Node in Mixnet smart contract can be selected to the Rewrded set. Thus, if you haven't migrated your node yet, please [follow these steps](../nodes/nym-node/bonding#migrate-to-nym-node-in-mixnet-smart-contract)!**
|
||||
</ Callout>
|
||||
|
||||
From release `2024.14-crunch` (`nym-node v1.2.0`), the `config_score` parameter takes into account also nodes version. The "current version" is the one marked as `Latest` in our repository. The parameter `versions_behind` indicates the number of versions between the `Latest` version and the version run by the node, and it is factored into the config score with the formula:
|
||||
##### Versions Behind Calculation
|
||||
|
||||
From release `2024.14-crunch` (`nym-node v1.2.0`), the `config_score` parameter takes into account also nodes version (denoted as `versions_behind`). The "current version" is the one marked as `Latest` in our repository. The parameter `versions_behind` indicates the number of versions between the `Latest` version and the version run by the node, and it is factored into the config score with this formula:
|
||||
|
||||
<Callout type="info" emoji="📌">
|
||||
> **0.995 ^ ( ( X * versions_behind ) ^ 1.65 )**
|
||||
@@ -213,30 +333,86 @@ Note that the `X` multiplier heavily lowers the `config_score` when nodes are ou
|
||||
|
||||
As you can see above, the algorithm is designed to give maximum selection score (`1`) to the latest version, while non-upgraded nodes receive a lower score. The score decreases faster when the node has failed to make a major version upgrade, and slower when the node is behind only with minor updates. This scoring de-prioritizes the selection of outdated nodes, even if their saturation and performance are high. Nodes are selected probabilistically in each epoch (60 min), according to their scores, to be part of the Rewarded set. This scoring mechanism gives priority to the operators running up-to-date nodes, ensuring that the network is as updated as possible.
|
||||
|
||||
#### Performance Calculation
|
||||
#### Routing Score Calculation
|
||||
|
||||
Performance is measured by Nym Network Monitor which sends thousands of packages through different routes every 15 minutes and measures how many were dropped on the way. Test result represents percentage of packets succesfully returned (can be anything between 0 and 1). Performance value is nodes average of these tests in last 24h.
|
||||
Routing score is measured by Nym Network Monitor which sends thousands of packages through different routes every 15 minutes and measures how many were dropped on the way. Test result represents percentage of packets succesfully returned which are then converted into floats bettween `0` and `1`.
|
||||
|
||||
Good performance is much more essential than [total stake](../tokenomics.mdx#stake-saturation), because it's lifted to 20th power in the selection formula.
|
||||
### Stake Saturation
|
||||
|
||||
For a comparison we made an example with 5 nodes, where first number is node performance and second stake saturation (assuming all of them `config_score` = 1 for simplification):
|
||||
> If you want to understand more about NYM supply, read [tokenomics page](../tokenomics#tokenomics) first.
|
||||
|
||||
<br />
|
||||
<AccordionTemplate name="✏️ Example: Performance ^ 20 * total_stake calculation">
|
||||
> node_1 = 1.00 ^ 20 \* 1.0 = 1 <br />
|
||||
> node_2 = 1.00 ^ 20 \* 0.5 = 0.5 <br />
|
||||
> node_3 = 0.99 ^ 20 \* 1.0 = 0.818 <br />
|
||||
> node_4 = 0.95 ^ 20 \* 1.0 = 0.358 <br />
|
||||
> node_5 = 0.90 ^ 20 \* 1.0 = 0.122 <br />
|
||||
</AccordionTemplate>
|
||||
<StakeSaturationSnippet />
|
||||
|
||||
As you can see the performance (also known as *Routing score*) is much more important during the Rewarded set selection. A node with 100% performance but only 50% stake saturation has much bigger chance to be chosen than a node with 95% performance but full stake saturation and incomparably bigger chance than 90% performing node with 100% stake saturation.
|
||||
|
||||
The nodes are chosen probababilistically in each epoch (60 min), so even nodes with lower performance will eventually be chosen, just much less often, as their chances decrease.
|
||||
### Rewards Calculation
|
||||
|
||||
### Layer Distribution
|
||||
Once the rewarded set (currently 120 Mixnodes and 120 Gateways) is selected, the nodes can start to route and mix packets in the Nym Network. Each hour a total of <span style={{display: 'inline-block'}}><EpochRewardBudget /></span> NYM is distributed between the layers from Mixmining pool. Currently in our *Naive rewarding* intermediate design, all layers get a same portion, therefore each node is *naively* assigned same working factor and therefore earns 1/240 of the rewards per epoch.
|
||||
|
||||
Once the rewarded set (currently 120 Mixnodes and 120 Gateways) is selected, the nodes can start to route and mix packets in the Nym Network. Each hour a total of 6000 NYM is distributed between the layers from Mixmining pool. Currently in our *Naive rewarding* intermediate design, all layers get a same portion, therefore each node is *naively* assigned same working factor and therefore earns 1/240 of the rewards per epoch.
|
||||
If a node is active in the rewarded set, it will receive rewards in the end of the epoch, the size is dependant on [stake saturation](../tokenomics.mdx#stake-saturation) and [performance](#performance-calculation). This is how rewards get distributed between nodes in the rewarded set.
|
||||
|
||||
**Rewards distribution formula:**
|
||||
|
||||
<Callout type="info" emoji="📌">
|
||||
> **node_epoch_rewards = reward_budget * 1 / active_set_size * (stake_saturation * performance)**
|
||||
>
|
||||
> In the current state:
|
||||
>
|
||||
> **node_epoch_rewards = <span style={{display: 'inline-block'}}><EpochRewardBudget /></span> * 1 / 240 * (stake_saturation * performance)**
|
||||
</Callout>
|
||||
|
||||
Performance and stake saturation play an equally decisive role in the size of rewards earned after the epoch. The closer a node is to maximum value (`1`) of each of these parameters, the more rewards it will get.
|
||||
|
||||
Given that there is a highly unlikely chance of all nodes having maximum stake saturation and performance, in majority of cases there will be some part of the reward budget left undistributed. This "change" is then kept in the [Mixmining reserve](../tokenomics#tokenomics).
|
||||
|
||||
<Callout>
|
||||
All parameters regarding performance score can be browsed or pull live from:
|
||||
|
||||
`https://validator.nymtech.net/api/v1/nym-nodes/annotation/<NODE_ID>`
|
||||
|
||||
In case you don't know your nodes `NODE_ID`, it's easy to find as long as your node is bonded. Visit [validator.nymtech.net/api/v1/nym-nodes/bonded](https://validator.nymtech.net/api/v1/nym-nodes/bonded) and search your node using `identity_key` or bonding Nyx account address (denoted as `owner`).
|
||||
</Callout>
|
||||
|
||||
**Rewards are send to the Nyx account used for bonding the node and each delegator automatically by the end of an epoch in which the node was part of the rewarded set,** following the logic [described below](#rewards-distribution).
|
||||
|
||||
#### Rewards Distribution
|
||||
|
||||
Once the [rewards are asigned per each node](#rewards-calculation) they need to be distributed between the operator of the node and delegators (people who staked their NYM on that node). The distribution is pretty straightforward and it happens in the following order:
|
||||
|
||||
|
||||
1. **Operators Cost (O.C.)**: How many NYM the operator requests to cover their costs per month, [divided by `720`](#nyx-epoch-vs-interval) (this value is set by the operator in the bonding wallet node settings)
|
||||
2. **Profit Margin (P.M.)**: What's the extra % cut the operator requests (this value is set by the operator in the bonding wallet node settings)
|
||||
3. **Bond & Stake proportionaly**: The remaining rewards are distributed proportionaly to the weight of every stake (including self bond, self delegation and each delegation).
|
||||
|
||||
##### Nyx Epoch vs Interval
|
||||
|
||||
<Callout type="info" emoji="📌">
|
||||
> **1 epoch = 60 min** <br />
|
||||
> **1 interval = 720 epochs**
|
||||
>
|
||||
> The logic is that interval is 30 days: <br />
|
||||
> 24 epochs * 30 days = 720
|
||||
</ Callout>
|
||||
|
||||
The Operators Cost (O.C). is a value denominated in NYM, that a node operator requires to get paid before the rewards get distributed. The cost is estimated per one month. However, it's paid only in epochs when the node is active. To calculate how O.C. works, we use a value called `interval` which represents 30 days (approximate month), or more precisely 720 epochs. To get covered a full O.C, the node would have to be active for the entire month.
|
||||
|
||||
**O.C. real revenue formula**
|
||||
|
||||
Therefore every epoch a node is active, the operator gets:
|
||||
<Callout type="info" emoji="📌">
|
||||
> **epoch_operator_cost_revenue = operators_cost / epochs_per_interval**
|
||||
>
|
||||
> that is:
|
||||
>
|
||||
> **epoch_operator_cost_revenue = operators_cost / 720**
|
||||
</ Callout>
|
||||
|
||||
To calculate O.C. per month, multiply it by number of active epochs:
|
||||
<Callout type="info" emoji="📌">
|
||||
> **monthly_operator_cost_revenue = ( operator_cost / 720 ) * active_epochs**
|
||||
</Callout>
|
||||
|
||||
{/*
|
||||
#### Final Layer Distribution (under development)
|
||||
|
||||
We are working on the final design with the ratio implementing a [decision made by the operators](https://forum.nymtech.net/t/poll-what-should-be-the-split-of-mixmining-rewards-among-the-layers-of-the-nym-mixnet/407) as follows:
|
||||
|
||||
@@ -248,7 +424,6 @@ We are working on the final design with the ratio implementing a [decision made
|
||||
> 33%; 67%
|
||||
</Callout>
|
||||
|
||||
{/*
|
||||
In real numbers: If hourly revenue to all 240 nodes is 6000 NYM, the layer compartmentalisation is 960 NYM for Entry Gateway layer and each Mixnode layer and 2160 NYM for Exit Gateway layer. The calculation is in the example below:
|
||||
|
||||
<Callout borderColor="#966fd6" backgroundColor="#b19cd9" emoji="">
|
||||
@@ -271,11 +446,9 @@ $33\% - 67\%$
|
||||
|
||||
## Roadmap
|
||||
|
||||
We are working on the final architecture of [*Fair Mixnet*](#fair-mixnet) tokenomics implementation. The current design is called [*Naive rewarding*](#naive-rewarding). This is an intermediate step, expecting operators to migrate to `nym-node` in Mixnet smart contract and be able to recieve delegations and earn rewards for any `nym-node` functionality, in opposite to the past system, where only Mixnodes were able to recieve delegations and rewards.
|
||||
We are working on the final architecture of [*Fair Mixnet*](#fair-mixnet) tokenomics implementation, following the [decision made by the node operators](https://forum.nymtech.net/t/poll-what-should-be-the-split-of-mixmining-rewards-among-the-layers-of-the-nym-mixnet/407). The current design is called [*Naive rewarding*](#naive-rewarding). This is an intermediate step, expecting operators to migrate to `nym-node` in Mixnet smart contract and be able to recieve delegations and earn rewards for any `nym-node` functionality, in opposite to the past system, where only Mixnodes were able to recieve delegations and rewards.
|
||||
|
||||
On November 5th, we presented a release roadmap in live [Operators Townhall](https://www.youtube.com/watch?v=3G1pJqvO2VM) where we explained in detail the steps of Nym node and tokenomics development and the effect it will have on node operators and put it into a rough timeline.
|
||||
|
||||

|
||||
On November 5th 2024, we presented a release roadmap in live [Operators Townhall](https://www.youtube.com/watch?v=3G1pJqvO2VM) where we explained in detail the steps of Nym node and tokenomics development and the effect it will have on node operators and put it into a rough timeline.
|
||||
|
||||
### Naive Rewarding
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ const config: DocsThemeConfig = {
|
||||
bookDescriptions[topLevel] ||
|
||||
defaultDescription;
|
||||
|
||||
const title = config.title + (route === "/" ? "" : " - Nym docs");
|
||||
const title = (route === "/" ? "Nym docs" : config.title + " - Nym docs");
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -13,17 +13,19 @@ cd cmdrun &&
|
||||
./nyx-total-stake.sh > ../../docs/components/outputs/api-scraping-outputs/nyx-outputs/nyx-total-stake.md &&
|
||||
|
||||
cd ../api-scraping &&
|
||||
python api_targets.py v --api mainnet --endpoint circulating-supply --value circulating_supply amount --separator _ > ../../docs/components/outputs/api-scraping-outputs/nyx-outputs/circulating-supply.md &&
|
||||
python api_targets.py validator --api mainnet --endpoint circulating-supply --value circulating_supply amount --separator _ > ../../docs/components/outputs/api-scraping-outputs/nyx-outputs/circulating-supply.md &&
|
||||
|
||||
python api_targets.py v --api mainnet --endpoint circulating-supply --format markdown --separator _ > ../../docs/components/outputs/api-scraping-outputs/nyx-outputs/token-table.md &&
|
||||
python api_targets.py validator --api mainnet --endpoint circulating-supply --format markdown --separator _ > ../../docs/components/outputs/api-scraping-outputs/nyx-outputs/token-table.md &&
|
||||
|
||||
python api_targets.py v --api mainnet --endpoint epoch/reward_params --value interval staking_supply_scale_factor --format percent > ../../docs/components/outputs/api-scraping-outputs/nyx-outputs/staking-scale-factor.md &&
|
||||
python api_targets.py validator --api mainnet --endpoint epoch/reward_params --value interval staking_supply_scale_factor --format percent > ../../docs/components/outputs/api-scraping-outputs/nyx-outputs/staking-scale-factor.md &&
|
||||
|
||||
python api_targets.py v --api mainnet --endpoint epoch/reward_params --value interval stake_saturation_point --separator _ > ../../docs/components/outputs/api-scraping-outputs/nyx-outputs/stake-saturation.md &&
|
||||
python api_targets.py validator --api mainnet --endpoint epoch/reward_params --value interval stake_saturation_point --separator _ > ../../docs/components/outputs/api-scraping-outputs/nyx-outputs/stake-saturation.md &&
|
||||
|
||||
python api_targets.py validator --api mainnet --endpoint epoch/reward_params --value interval epoch_reward_budget --format markdown --separator _ > ../../docs/components/outputs/api-scraping-outputs/nyx-outputs/epoch-reward-budget.md &&
|
||||
|
||||
python api_targets.py time_now > ../../docs/components/outputs/api-scraping-outputs/time-now.md &&
|
||||
|
||||
python api_targets.py c --staking_target --separator _ > ../../docs/components/outputs/api-scraping-outputs/nyx-outputs/staking-target.md &&
|
||||
python api_targets.py calculate --staking_target --separator _ > ../../docs/components/outputs/api-scraping-outputs/nyx-outputs/staking-target.md &&
|
||||
|
||||
cd ../../../scripts &&
|
||||
echo '```python' > ../documentation/docs/components/outputs/command-outputs/node-api-check-query-help.md &&
|
||||
|
||||
+4
-2
@@ -4,7 +4,7 @@
|
||||
[package]
|
||||
name = "nym-api"
|
||||
license = "GPL-3.0"
|
||||
version = "1.1.56"
|
||||
version = "1.1.58"
|
||||
authors.workspace = true
|
||||
edition = "2021"
|
||||
rust-version.workspace = true
|
||||
@@ -27,6 +27,7 @@ humantime-serde = { workspace = true }
|
||||
k256 = { workspace = true, features = [
|
||||
"ecdsa-core",
|
||||
] } # needed for the Verifier trait; pull whatever version is used by other dependencies
|
||||
moka = { workspace = true }
|
||||
pin-project = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
rand_chacha = { workspace = true }
|
||||
@@ -116,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" }
|
||||
@@ -128,6 +129,7 @@ v2-performance = []
|
||||
generate-ts = ["ts-rs"]
|
||||
|
||||
[build-dependencies]
|
||||
anyhow = { workspace = true }
|
||||
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
||||
sqlx = { workspace = true, features = [
|
||||
"runtime-tokio-rustls",
|
||||
|
||||
+37
-1
@@ -1,13 +1,20 @@
|
||||
use sqlx::{Connection, FromRow, SqliteConnection};
|
||||
use std::env;
|
||||
|
||||
const SQLITE_DB_FILENAME: &str = "nym-api-example.sqlite";
|
||||
|
||||
// it's fine if compilation fails
|
||||
#[allow(clippy::unwrap_used)]
|
||||
#[allow(clippy::expect_used)]
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let out_dir = env::var("OUT_DIR").unwrap();
|
||||
let database_path = format!("{}/nym-api-example.sqlite", out_dir);
|
||||
let database_path = format!("{}/{}", out_dir, SQLITE_DB_FILENAME);
|
||||
|
||||
#[cfg(target_family = "unix")]
|
||||
write_db_path_to_file(&out_dir, SQLITE_DB_FILENAME)
|
||||
.await
|
||||
.ok();
|
||||
|
||||
let mut conn = SqliteConnection::connect(&format!("sqlite://{}?mode=rwc", database_path))
|
||||
.await
|
||||
@@ -62,3 +69,32 @@ async fn main() {
|
||||
// not a valid windows path... but hey, it works...
|
||||
println!("cargo:rustc-env=DATABASE_URL=sqlite:///{}", &database_path);
|
||||
}
|
||||
|
||||
/// use `./enter_db.sh` to inspect DB
|
||||
#[cfg(target_family = "unix")]
|
||||
async fn write_db_path_to_file(out_dir: &str, db_filename: &str) -> anyhow::Result<()> {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use tokio::{fs::File, io::AsyncWriteExt};
|
||||
|
||||
if env::var("CI").is_ok() {
|
||||
return Ok(());
|
||||
}
|
||||
let mut file = File::create("settings.sql").await?;
|
||||
let settings = ".mode columns
|
||||
.headers on";
|
||||
file.write_all(settings.as_bytes()).await?;
|
||||
|
||||
let mut file = File::create("enter_db.sh").await?;
|
||||
let contents = format!(
|
||||
"#!/bin/sh\n\
|
||||
sqlite3 -init settings.sql {}/{}",
|
||||
out_dir, db_filename,
|
||||
);
|
||||
file.write_all(contents.as_bytes()).await?;
|
||||
|
||||
file.set_permissions(std::fs::Permissions::from_mode(0o755))
|
||||
.await
|
||||
.map_err(anyhow::Error::from)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
}))
|
||||
|
||||
@@ -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>,
|
||||
}
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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(),
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -17,6 +17,7 @@ use crate::support::config;
|
||||
use crate::support::http::state::{AppState, ChainStatusCache, ForcedRefresh};
|
||||
use crate::support::nyxd::Client;
|
||||
use crate::support::storage::NymApiStorage;
|
||||
use crate::unstable_routes::account::cache::AddressInfoCache;
|
||||
use async_trait::async_trait;
|
||||
use axum::Router;
|
||||
use axum_test::http::StatusCode;
|
||||
@@ -1274,6 +1275,7 @@ impl TestFixture {
|
||||
AppState {
|
||||
nyxd_client,
|
||||
chain_status_cache: ChainStatusCache::new(Duration::from_secs(42)),
|
||||
address_info_cache: AddressInfoCache::new(),
|
||||
forced_refresh: ForcedRefresh::new(true),
|
||||
nym_contract_cache: NymContractCache::new(),
|
||||
node_status_cache: NodeStatusCache::new(),
|
||||
|
||||
@@ -6,7 +6,9 @@ use crate::epoch_operations::helpers::stake_to_f64;
|
||||
use crate::EpochAdvancer;
|
||||
use cosmwasm_std::Decimal;
|
||||
use nym_mixnet_contract_common::reward_params::{Performance, RewardedSetParams};
|
||||
use nym_mixnet_contract_common::{EpochState, NodeId, NymNodeDetails, RewardedSet};
|
||||
use nym_mixnet_contract_common::{
|
||||
EpochState, NodeId, NymNodeDetails, RewardedSet, RewardingParams,
|
||||
};
|
||||
use rand::prelude::SliceRandom;
|
||||
use rand::rngs::OsRng;
|
||||
use std::collections::HashSet;
|
||||
@@ -25,14 +27,14 @@ enum AvailableRole {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct NodeWithStakeAndPerformance {
|
||||
struct NodeWithSaturationAndPerformance {
|
||||
node_id: NodeId,
|
||||
available_roles: Vec<AvailableRole>,
|
||||
total_stake: Decimal,
|
||||
saturation: Decimal,
|
||||
performance: Performance,
|
||||
}
|
||||
|
||||
impl NodeWithStakeAndPerformance {
|
||||
impl NodeWithSaturationAndPerformance {
|
||||
fn to_selection_weight(&self) -> f64 {
|
||||
let scaled_performance = match self.performance.checked_pow(20) {
|
||||
Ok(perf) => perf,
|
||||
@@ -42,7 +44,7 @@ impl NodeWithStakeAndPerformance {
|
||||
}
|
||||
};
|
||||
|
||||
let scaled_stake = self.total_stake * scaled_performance;
|
||||
let scaled_stake = self.saturation * scaled_performance;
|
||||
stake_to_f64(scaled_stake)
|
||||
}
|
||||
|
||||
@@ -62,7 +64,7 @@ impl NodeWithStakeAndPerformance {
|
||||
impl EpochAdvancer {
|
||||
fn determine_rewarded_set(
|
||||
&self,
|
||||
nodes: Vec<NodeWithStakeAndPerformance>,
|
||||
nodes: Vec<NodeWithSaturationAndPerformance>,
|
||||
spec: RewardedSetParams,
|
||||
) -> Result<RewardedSet, RewardingError> {
|
||||
if nodes.is_empty() {
|
||||
@@ -204,7 +206,8 @@ impl EpochAdvancer {
|
||||
async fn attach_performance_to_eligible_nodes(
|
||||
&self,
|
||||
nym_nodes: &[NymNodeDetails],
|
||||
) -> Vec<NodeWithStakeAndPerformance> {
|
||||
reward_params: &RewardingParams,
|
||||
) -> Vec<NodeWithSaturationAndPerformance> {
|
||||
let mut with_performance = Vec::new();
|
||||
|
||||
// SAFETY: the cache MUST HAVE been initialised before now
|
||||
@@ -218,7 +221,7 @@ impl EpochAdvancer {
|
||||
|
||||
for nym_node in nym_nodes {
|
||||
let node_id = nym_node.node_id();
|
||||
let total_stake = nym_node.total_stake();
|
||||
let saturation = nym_node.rewarding_details.bond_saturation(reward_params);
|
||||
|
||||
let Some(self_described) = described_cache.get_description(&node_id) else {
|
||||
continue;
|
||||
@@ -230,7 +233,7 @@ impl EpochAdvancer {
|
||||
};
|
||||
|
||||
let performance = annotation.detailed_performance.to_rewarding_performance();
|
||||
debug!("nym-node {node_id}: stake: {total_stake}, performance: {performance}");
|
||||
debug!("nym-node {node_id}: saturation: {saturation}, performance: {performance}");
|
||||
|
||||
let mut available_roles = Vec::new();
|
||||
if self_described.declared_role.mixnode {
|
||||
@@ -248,10 +251,10 @@ impl EpochAdvancer {
|
||||
continue;
|
||||
}
|
||||
|
||||
with_performance.push(NodeWithStakeAndPerformance {
|
||||
with_performance.push(NodeWithSaturationAndPerformance {
|
||||
node_id: nym_node.node_id(),
|
||||
available_roles,
|
||||
total_stake,
|
||||
saturation,
|
||||
performance,
|
||||
})
|
||||
}
|
||||
@@ -273,13 +276,8 @@ impl EpochAdvancer {
|
||||
}
|
||||
|
||||
info!("attempting to assign the rewarded set for the upcoming epoch...");
|
||||
let nodes_with_performance =
|
||||
self.attach_performance_to_eligible_nodes(nym_nodes).await;
|
||||
|
||||
if let Err(err) = self
|
||||
._update_rewarded_set_and_advance_epoch(nodes_with_performance)
|
||||
.await
|
||||
{
|
||||
if let Err(err) = self._update_rewarded_set_and_advance_epoch(nym_nodes).await {
|
||||
error!("FAILED to assign the rewarded set... - {err}");
|
||||
Err(err)
|
||||
} else {
|
||||
@@ -300,15 +298,19 @@ impl EpochAdvancer {
|
||||
|
||||
async fn _update_rewarded_set_and_advance_epoch(
|
||||
&self,
|
||||
all_nodes: Vec<NodeWithStakeAndPerformance>,
|
||||
nym_nodes: &[NymNodeDetails],
|
||||
) -> Result<(), RewardingError> {
|
||||
// we grab rewarding parameters here as they might have gotten updated when performing epoch actions
|
||||
let rewarding_parameters = self.nyxd_client.get_current_rewarding_parameters().await?;
|
||||
|
||||
debug!("Rewarding parameters: {rewarding_parameters:?}");
|
||||
|
||||
let nodes_with_performance = self
|
||||
.attach_performance_to_eligible_nodes(nym_nodes, &rewarding_parameters)
|
||||
.await;
|
||||
|
||||
let new_rewarded_set =
|
||||
self.determine_rewarded_set(all_nodes, rewarding_parameters.rewarded_set)?;
|
||||
self.determine_rewarded_set(nodes_with_performance, rewarding_parameters.rewarded_set)?;
|
||||
|
||||
debug!("New rewarded set: {:?}", new_rewarded_set);
|
||||
|
||||
|
||||
+1
-1
@@ -26,8 +26,8 @@ pub(crate) mod nym_contract_cache;
|
||||
pub(crate) mod nym_nodes;
|
||||
mod status;
|
||||
pub(crate) mod support;
|
||||
mod unstable_routes;
|
||||
|
||||
// TODO rocket: remove all such Todos once rocket is phased out completely
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), anyhow::Error> {
|
||||
cfg_if::cfg_if! {if #[cfg(feature = "console-subscriber")] {
|
||||
|
||||
@@ -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<_, _>>(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -327,7 +327,6 @@ pub(crate) type AxumResult<T> = Result<T, AxumErrorResponse>;
|
||||
// #[schema(title = "ErrorResponse")]
|
||||
pub(crate) struct AxumErrorResponse {
|
||||
message: RequestError,
|
||||
// #[schema(value_type = u16)]
|
||||
status: StatusCode,
|
||||
}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -46,7 +47,7 @@ pub(crate) mod semi_skimmed;
|
||||
pub(crate) mod skimmed;
|
||||
|
||||
#[allow(deprecated)]
|
||||
pub(crate) fn nym_node_routes_unstable() -> Router<AppState> {
|
||||
pub(crate) fn routes() -> Router<AppState> {
|
||||
Router::new()
|
||||
.nest(
|
||||
"/skimmed",
|
||||
@@ -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>,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -27,6 +27,7 @@ use crate::support::http::RouterBuilder;
|
||||
use crate::support::nyxd;
|
||||
use crate::support::storage::runtime_migrations::m001_directory_services_v2_1::migrate_to_directory_services_v2_1;
|
||||
use crate::support::storage::NymApiStorage;
|
||||
use crate::unstable_routes::account::cache::AddressInfoCache;
|
||||
use crate::{
|
||||
circulating_supply_api, ecash, epoch_operations, network_monitor, node_describe_cache,
|
||||
node_status_api, nym_contract_cache,
|
||||
@@ -193,6 +194,7 @@ async fn start_nym_api_tasks_axum(config: &Config) -> anyhow::Result<ShutdownHan
|
||||
let router = router.with_state(AppState {
|
||||
nyxd_client: nyxd_client.clone(),
|
||||
chain_status_cache: ChainStatusCache::new(DEFAULT_CHAIN_STATUS_CACHE_TTL),
|
||||
address_info_cache: AddressInfoCache::new(),
|
||||
forced_refresh: ForcedRefresh::new(
|
||||
config.topology_cacher.debug.node_describe_allow_illegal_ips,
|
||||
),
|
||||
|
||||
@@ -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>,
|
||||
}
|
||||
|
||||
@@ -5,116 +5,6 @@ pub(crate) mod helpers;
|
||||
pub(crate) mod openapi;
|
||||
pub(crate) mod router;
|
||||
pub(crate) mod state;
|
||||
mod unstable_routes;
|
||||
|
||||
pub(crate) use router::RouterBuilder;
|
||||
|
||||
// pub(crate) async fn setup_rocket(
|
||||
// config: &Config,
|
||||
// network_details: NetworkDetails,
|
||||
// nyxd_client: nyxd::Client,
|
||||
// identity_keypair: identity::KeyPair,
|
||||
// coconut_keypair: ecash::keys::KeyPair,
|
||||
// storage: NymApiStorage,
|
||||
// ) -> anyhow::Result<Rocket<Ignite>> {
|
||||
// let openapi_settings = rocket_okapi::settings::OpenApiSettings::default();
|
||||
// let mut rocket = rocket::build();
|
||||
//
|
||||
// let mix_denom = network_details.network.chain_details.mix_denom.base.clone();
|
||||
//
|
||||
// mount_endpoints_and_merged_docs! {
|
||||
// rocket,
|
||||
// "/v1".to_owned(),
|
||||
// openapi_settings,
|
||||
// "/" => (vec![], openapi::custom_openapi_spec()),
|
||||
// "" => circulating_supply_api::circulating_supply_routes(&openapi_settings),
|
||||
// "" => nym_contract_cache::nym_contract_cache_routes(&openapi_settings),
|
||||
// "/status" => node_status_api::node_status_routes(&openapi_settings, config.network_monitor.enabled),
|
||||
// "/network" => network_routes(&openapi_settings),
|
||||
// "/api-status" => api_status_routes(&openapi_settings),
|
||||
// "/ecash" => ecash::routes_open_api(&openapi_settings, config.ecash_signer.enabled),
|
||||
// "" => nym_node_routes_deprecated(&openapi_settings),
|
||||
//
|
||||
// // => when we move those routes, we'll need to add a redirection for backwards compatibility
|
||||
// "/unstable/nym-nodes" => nym_node_routes_next(&openapi_settings)
|
||||
// }
|
||||
//
|
||||
// let rocket = rocket
|
||||
// .manage(network_details)
|
||||
// .manage(SharedCache::<DescribedNodes>::new())
|
||||
// .mount("/swagger", make_swagger_ui(&openapi::get_docs()))
|
||||
// .attach(setup_rocket_cors()?)
|
||||
// .attach(NymContractCache::stage())
|
||||
// .attach(NodeStatusCache::stage())
|
||||
// .attach(CirculatingSupplyCache::stage(mix_denom.clone()))
|
||||
// .manage(unstable::NodeInfoCache::default())
|
||||
// .manage(storage.clone());
|
||||
//
|
||||
// let mut status_state = ApiStatusState::new();
|
||||
//
|
||||
// let rocket = if config.ecash_signer.enabled {
|
||||
// // make sure we have some tokens to cover multisig fees
|
||||
// let balance = nyxd_client.balance(&mix_denom).await?;
|
||||
// if balance.amount < ecash::MINIMUM_BALANCE {
|
||||
// let address = nyxd_client.address().await;
|
||||
// let min = Coin::new(ecash::MINIMUM_BALANCE, mix_denom);
|
||||
// bail!("the account ({address}) doesn't have enough funds to cover verification fees. it has {balance} while it needs at least {min}")
|
||||
// }
|
||||
//
|
||||
// let cosmos_address = nyxd_client.address().await.to_string();
|
||||
// let announce_address = config
|
||||
// .ecash_signer
|
||||
// .announce_address
|
||||
// .clone()
|
||||
// .map(|u| u.to_string())
|
||||
// .unwrap_or_default();
|
||||
// status_state.add_zk_nym_signer(SignerState {
|
||||
// cosmos_address,
|
||||
// identity: identity_keypair.public_key().to_base58_string(),
|
||||
// announce_address,
|
||||
// ecash_keypair: coconut_keypair.clone(),
|
||||
// });
|
||||
//
|
||||
// let ecash_contract = nyxd_client
|
||||
// .get_ecash_contract_address()
|
||||
// .await
|
||||
// .context("e-cash contract address is required to setup the zk-nym signer")?;
|
||||
//
|
||||
// let comm_channel = QueryCommunicationChannel::new(nyxd_client.clone());
|
||||
//
|
||||
// let ecash_state = EcashState::new(
|
||||
// ecash_contract,
|
||||
// nyxd_client.clone(),
|
||||
// identity_keypair,
|
||||
// coconut_keypair,
|
||||
// comm_channel,
|
||||
// storage.clone(),
|
||||
// )
|
||||
// .await?;
|
||||
//
|
||||
// rocket.manage(ecash_state)
|
||||
// } else {
|
||||
// rocket
|
||||
// };
|
||||
//
|
||||
// Ok(rocket.manage(status_state).ignite().await?)
|
||||
// }
|
||||
//
|
||||
// fn setup_rocket_cors() -> Result<Cors> {
|
||||
// let allowed_origins = AllowedOrigins::all();
|
||||
//
|
||||
// // You can also deserialize this
|
||||
// let cors = rocket_cors::CorsOptions {
|
||||
// allowed_origins,
|
||||
// allowed_methods: vec![rocket::http::Method::Post, rocket::http::Method::Get]
|
||||
// .into_iter()
|
||||
// .map(From::from)
|
||||
// .collect(),
|
||||
// allowed_headers: AllowedHeaders::all(),
|
||||
// allow_credentials: true,
|
||||
// ..Default::default()
|
||||
// }
|
||||
// .to_cors()?;
|
||||
//
|
||||
// Ok(cors)
|
||||
// }
|
||||
use crate::unstable_routes;
|
||||
|
||||
@@ -17,7 +17,7 @@ use axum::response::Redirect;
|
||||
use axum::routing::get;
|
||||
use axum::Router;
|
||||
use core::net::SocketAddr;
|
||||
use nym_http_api_common::middleware::logging::logger;
|
||||
use nym_http_api_common::middleware::logging::log_request_info;
|
||||
use tokio::net::TcpListener;
|
||||
use tokio_util::sync::WaitForCancellationFutureOwned;
|
||||
use tower_http::cors::CorsLayer;
|
||||
@@ -91,7 +91,7 @@ impl RouterBuilder {
|
||||
fn finalize_routes(self) -> Router<AppState> {
|
||||
self.unfinished_router
|
||||
.layer(setup_cors())
|
||||
.layer(axum::middleware::from_fn(logger))
|
||||
.layer(axum::middleware::from_fn(log_request_info))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ use crate::support::caching::cache::SharedCache;
|
||||
use crate::support::caching::Cache;
|
||||
use crate::support::nyxd::Client;
|
||||
use crate::support::storage;
|
||||
use crate::unstable_routes::account::cache::AddressInfoCache;
|
||||
use crate::unstable_routes::models::NyxAccountDetails;
|
||||
use axum::extract::FromRef;
|
||||
use nym_api_requests::models::{
|
||||
DetailedChainStatus, GatewayBondAnnotated, MixNodeBondAnnotated, NodeAnnotation,
|
||||
@@ -85,6 +87,7 @@ pub(crate) struct AppState {
|
||||
pub(crate) nyxd_client: Client,
|
||||
pub(crate) chain_status_cache: ChainStatusCache,
|
||||
|
||||
pub(crate) address_info_cache: AddressInfoCache,
|
||||
pub(crate) forced_refresh: ForcedRefresh,
|
||||
pub(crate) nym_contract_cache: NymContractCache,
|
||||
pub(crate) node_status_cache: NodeStatusCache,
|
||||
@@ -292,4 +295,43 @@ impl AppState {
|
||||
.await
|
||||
.ok_or_else(AxumErrorResponse::internal)
|
||||
}
|
||||
|
||||
pub(crate) async fn get_address_info(
|
||||
self,
|
||||
account_id: nym_validator_client::nyxd::AccountId,
|
||||
) -> Result<NyxAccountDetails, AxumErrorResponse> {
|
||||
let address = account_id.to_string();
|
||||
match self.address_info_cache.get(&address).await {
|
||||
Some(guard) => {
|
||||
tracing::trace!("Fetching from cache...");
|
||||
let read_lock = guard.read().await;
|
||||
Ok(read_lock.clone())
|
||||
}
|
||||
None => {
|
||||
tracing::trace!("No cache for {}, refreshing data...", &address);
|
||||
|
||||
let address_info = self
|
||||
.address_info_cache
|
||||
.collect_balances(
|
||||
self.nyxd_client.clone(),
|
||||
self.nym_contract_cache.clone(),
|
||||
self.network_details()
|
||||
.network
|
||||
.chain_details
|
||||
.mix_denom
|
||||
.base
|
||||
.to_owned(),
|
||||
&address,
|
||||
account_id,
|
||||
)
|
||||
.await?;
|
||||
|
||||
self.address_info_cache
|
||||
.upsert_address_info(&address, address_info.clone())
|
||||
.await;
|
||||
|
||||
Ok(address_info)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::nym_nodes::handlers::unstable::nym_node_routes_unstable;
|
||||
use crate::support::http::state::AppState;
|
||||
use axum::Router;
|
||||
|
||||
// as those get stabilised, they should get deprecated and use a redirection instead
|
||||
pub(crate) fn unstable_routes() -> Router<AppState> {
|
||||
Router::new().nest("/nym-nodes", nym_node_routes_unstable())
|
||||
}
|
||||
@@ -29,14 +29,15 @@ use nym_mixnet_contract_common::mixnode::MixNodeDetails;
|
||||
use nym_mixnet_contract_common::nym_node::Role;
|
||||
use nym_mixnet_contract_common::reward_params::RewardingParams;
|
||||
use nym_mixnet_contract_common::{
|
||||
ConfigScoreParams, CurrentIntervalResponse, EpochRewardedSet, EpochStatus, ExecuteMsg,
|
||||
GatewayBond, HistoricalNymNodeVersionEntry, IdentityKey, NymNodeDetails, RewardedSet,
|
||||
RoleAssignment,
|
||||
ConfigScoreParams, CurrentIntervalResponse, Delegation, EpochRewardedSet, EpochStatus,
|
||||
ExecuteMsg, GatewayBond, HistoricalNymNodeVersionEntry, IdentityKey, NymNodeDetails,
|
||||
RewardedSet, RoleAssignment,
|
||||
};
|
||||
use nym_validator_client::coconut::EcashApiError;
|
||||
use nym_validator_client::nyxd::contract_traits::mixnet_query_client::MixnetQueryClientExt;
|
||||
use nym_validator_client::nyxd::contract_traits::PagedDkgQueryClient;
|
||||
use nym_validator_client::nyxd::error::NyxdError;
|
||||
use nym_validator_client::nyxd::Coin;
|
||||
use nym_validator_client::nyxd::{
|
||||
contract_traits::{
|
||||
DkgQueryClient, DkgSigningClient, EcashQueryClient, GroupQueryClient, MixnetQueryClient,
|
||||
@@ -48,7 +49,7 @@ use nym_validator_client::nyxd::{
|
||||
};
|
||||
use nym_validator_client::nyxd::{
|
||||
hash::{Hash, SHA256_HASH_SIZE},
|
||||
AccountId, Coin, TendermintTime,
|
||||
AccountId, TendermintTime,
|
||||
};
|
||||
use nym_validator_client::{
|
||||
nyxd, DirectSigningHttpRpcNyxdClient, EcashApiClient, QueryHttpRpcNyxdClient,
|
||||
@@ -403,6 +404,21 @@ impl Client {
|
||||
nyxd_signing!(self, reconcile_epoch_events(limit, None).await?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn get_all_delegator_delegations(
|
||||
&self,
|
||||
delegation_owner: &AccountId,
|
||||
) -> Result<Vec<Delegation>, NyxdError> {
|
||||
nyxd_query!(self, get_all_delegator_delegations(delegation_owner).await)
|
||||
}
|
||||
|
||||
pub(crate) async fn get_address_balance(
|
||||
&self,
|
||||
address: &AccountId,
|
||||
denom: impl Into<String>,
|
||||
) -> Result<Option<Coin>, NyxdError> {
|
||||
nyxd_query!(self, get_balance(&address, denom.into()).await)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
use crate::{
|
||||
node_status_api::models::AxumResult,
|
||||
nym_contract_cache::cache::NymContractCache,
|
||||
unstable_routes::{
|
||||
account::data_collector::AddressDataCollector,
|
||||
models::{NyxAccountDelegationDetails, NyxAccountDetails},
|
||||
},
|
||||
};
|
||||
use moka::{future::Cache, Entry};
|
||||
use nym_validator_client::nyxd::AccountId;
|
||||
use std::{sync::Arc, time::Duration};
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct AddressInfoCache {
|
||||
inner: Cache<String, Arc<RwLock<NyxAccountDetails>>>,
|
||||
}
|
||||
|
||||
impl AddressInfoCache {
|
||||
pub(crate) fn new() -> Self {
|
||||
// epoch duration = 1 hour
|
||||
// cache TTL is slightly lower than that to avoid too stale data in case
|
||||
// cache was refreshed JUST BEFORE epoch transition
|
||||
let cache_ttl = Duration::from_secs(60 * 30);
|
||||
let max_capacity = 1000;
|
||||
|
||||
AddressInfoCache {
|
||||
inner: Cache::builder()
|
||||
.time_to_live(cache_ttl)
|
||||
.max_capacity(max_capacity)
|
||||
.build(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn get(&self, key: &str) -> Option<Arc<RwLock<NyxAccountDetails>>> {
|
||||
self.inner.get(key).await
|
||||
}
|
||||
|
||||
pub(crate) async fn upsert_address_info(
|
||||
&self,
|
||||
address: &str,
|
||||
address_info: NyxAccountDetails,
|
||||
) -> Entry<String, Arc<RwLock<NyxAccountDetails>>> {
|
||||
self.inner
|
||||
.entry_by_ref(address)
|
||||
.and_upsert_with(|maybe_entry| async {
|
||||
if let Some(entry) = maybe_entry {
|
||||
let v = entry.into_value();
|
||||
let mut guard = v.write().await;
|
||||
*guard = address_info;
|
||||
v.clone()
|
||||
} else {
|
||||
Arc::new(RwLock::new(address_info))
|
||||
}
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn collect_balances(
|
||||
&self,
|
||||
nyxd_client: crate::nyxd::Client,
|
||||
nym_contract_cache: NymContractCache,
|
||||
base_denom: String,
|
||||
address: &str,
|
||||
account_id: AccountId,
|
||||
) -> AxumResult<NyxAccountDetails> {
|
||||
let mut collector = AddressDataCollector::new(
|
||||
nyxd_client,
|
||||
nym_contract_cache,
|
||||
base_denom,
|
||||
account_id.clone(),
|
||||
);
|
||||
|
||||
// ==> get balances of chain tokens <==
|
||||
let balance = collector.get_address_balance().await?;
|
||||
|
||||
// it's very difficult to lower existing balance to exactly 0
|
||||
// so assume this is an unused address and return early
|
||||
if balance.amount == 0 {
|
||||
let address_info = NyxAccountDetails {
|
||||
address: address.to_string(),
|
||||
balance: balance.clone().into(),
|
||||
total_value: balance.clone().into(),
|
||||
delegations: Vec::new(),
|
||||
accumulated_rewards: Vec::new(),
|
||||
total_delegations: balance.clone().into(),
|
||||
claimable_rewards: balance.clone().into(),
|
||||
operator_rewards: None,
|
||||
};
|
||||
|
||||
return Ok(address_info);
|
||||
}
|
||||
|
||||
// ==> get list of delegations (history) <==
|
||||
let delegation_data = collector.get_delegations().await?;
|
||||
|
||||
// ==> get the current reward for each active delegation <==
|
||||
// calculate rewards from nodes this delegator delegated to
|
||||
let accumulated_rewards = collector.calculate_rewards(&delegation_data).await?;
|
||||
|
||||
// ==> convert totals <==
|
||||
let claimable_rewards = collector.claimable_rewards();
|
||||
let total_value = collector.total_value();
|
||||
let total_delegations = collector.total_delegations();
|
||||
let operator_rewards = collector.operator_rewards();
|
||||
|
||||
let address_info = NyxAccountDetails {
|
||||
address: account_id.to_string(),
|
||||
balance: balance.into(),
|
||||
delegations: delegation_data
|
||||
.delegations()
|
||||
.into_iter()
|
||||
.map(|d| NyxAccountDelegationDetails {
|
||||
delegated: d.amount,
|
||||
height: d.height,
|
||||
node_id: d.node_id,
|
||||
proxy: d.proxy,
|
||||
})
|
||||
.collect(),
|
||||
accumulated_rewards,
|
||||
total_delegations,
|
||||
claimable_rewards,
|
||||
total_value,
|
||||
operator_rewards,
|
||||
};
|
||||
|
||||
Ok(address_info)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::{
|
||||
node_status_api::models::{AxumErrorResponse, AxumResult},
|
||||
nym_contract_cache::cache::NymContractCache,
|
||||
unstable_routes::models::NyxAccountDelegationRewardDetails,
|
||||
};
|
||||
use cosmwasm_std::{Coin, Decimal};
|
||||
use nym_mixnet_contract_common::NodeRewarding;
|
||||
use nym_topology::NodeId;
|
||||
use nym_validator_client::nyxd::AccountId;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use tracing::warn;
|
||||
|
||||
pub(crate) struct AddressDataCollector {
|
||||
nyxd_client: crate::nyxd::Client,
|
||||
nym_contract_cache: NymContractCache,
|
||||
account_id: AccountId,
|
||||
total_value: u128,
|
||||
operator_rewards: u128,
|
||||
claimable_rewards: u128,
|
||||
total_delegations: u128,
|
||||
base_denom: String,
|
||||
}
|
||||
|
||||
impl AddressDataCollector {
|
||||
pub(crate) fn new(
|
||||
nyxd_client: crate::nyxd::Client,
|
||||
nym_contract_cache: NymContractCache,
|
||||
base_denom: String,
|
||||
account_id: AccountId,
|
||||
) -> Self {
|
||||
Self {
|
||||
nyxd_client,
|
||||
nym_contract_cache,
|
||||
base_denom,
|
||||
account_id,
|
||||
total_value: 0,
|
||||
operator_rewards: 0,
|
||||
claimable_rewards: 0,
|
||||
total_delegations: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn get_address_balance(
|
||||
&mut self,
|
||||
) -> AxumResult<nym_validator_client::nyxd::Coin> {
|
||||
let balance = self
|
||||
.nyxd_client
|
||||
.get_address_balance(&self.account_id, &self.base_denom)
|
||||
.await?
|
||||
.unwrap_or_else(|| nym_validator_client::nyxd::Coin::new(0u128, &self.base_denom));
|
||||
self.total_value += balance.amount;
|
||||
|
||||
Ok(balance)
|
||||
}
|
||||
|
||||
pub(crate) async fn get_delegations(&mut self) -> AxumResult<AddressDelegationInfo> {
|
||||
let og_delegations = self
|
||||
.nyxd_client
|
||||
.get_all_delegator_delegations(&self.account_id)
|
||||
.await?;
|
||||
|
||||
let delegated_to_nodes = og_delegations
|
||||
.iter()
|
||||
.map(|d| d.node_id)
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
let nym_nodes = self
|
||||
.nym_contract_cache
|
||||
.all_cached_nym_nodes()
|
||||
.await
|
||||
.ok_or_else(AxumErrorResponse::service_unavailable)?
|
||||
.iter()
|
||||
.filter_map(|node_details| {
|
||||
// is this an operator of this node?
|
||||
if self.account_id.to_string() == node_details.bond_information.owner.as_str() {
|
||||
let pending_operator_reward =
|
||||
node_details.pending_operator_reward().amount.u128();
|
||||
|
||||
// add operator rewards
|
||||
self.operator_rewards += pending_operator_reward;
|
||||
|
||||
// add to totals
|
||||
self.total_value += pending_operator_reward;
|
||||
}
|
||||
if delegated_to_nodes.contains(&node_details.node_id()) {
|
||||
Some((
|
||||
node_details.node_id(),
|
||||
// avoid cloning node data which we don't need
|
||||
(
|
||||
node_details.rewarding_details.clone(),
|
||||
node_details.is_unbonding(),
|
||||
),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
Ok(AddressDelegationInfo {
|
||||
delegations: og_delegations,
|
||||
delegated_to_nodes: nym_nodes,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) async fn calculate_rewards(
|
||||
&mut self,
|
||||
delegation_data: &AddressDelegationInfo,
|
||||
) -> AxumResult<Vec<NyxAccountDelegationRewardDetails>> {
|
||||
let mut accumulated_rewards = Vec::new();
|
||||
for delegation in delegation_data.delegations.iter() {
|
||||
let node_id = &delegation.node_id;
|
||||
|
||||
if let Some((rewarding_details, is_unbonding)) =
|
||||
delegation_data.delegated_to_nodes.get(node_id)
|
||||
{
|
||||
match rewarding_details.determine_delegation_reward(delegation) {
|
||||
Ok(delegation_reward) => {
|
||||
let reward = NyxAccountDelegationRewardDetails {
|
||||
node_id: delegation.node_id,
|
||||
rewards: decimal_to_coin(delegation_reward, &self.base_denom),
|
||||
amount_staked: delegation.amount.clone(),
|
||||
node_still_fully_bonded: !is_unbonding,
|
||||
};
|
||||
// 4. sum the rewards and delegations
|
||||
self.total_delegations += delegation.amount.amount.u128();
|
||||
self.total_value += delegation.amount.amount.u128();
|
||||
self.total_value += reward.rewards.amount.u128();
|
||||
self.claimable_rewards += reward.rewards.amount.u128();
|
||||
|
||||
accumulated_rewards.push(reward);
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(
|
||||
"Couldn't determine delegations for {} on node {}: {}",
|
||||
&self.account_id, node_id, err
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(accumulated_rewards)
|
||||
}
|
||||
|
||||
pub(crate) fn claimable_rewards(&self) -> Coin {
|
||||
Coin::new(self.claimable_rewards, self.base_denom.to_string())
|
||||
}
|
||||
|
||||
pub(crate) fn total_value(&self) -> Coin {
|
||||
Coin::new(self.total_value, self.base_denom.to_string())
|
||||
}
|
||||
|
||||
pub(crate) fn total_delegations(&self) -> Coin {
|
||||
Coin::new(self.total_delegations, self.base_denom.to_string())
|
||||
}
|
||||
|
||||
pub(crate) fn operator_rewards(&self) -> Option<Coin> {
|
||||
if self.operator_rewards > 0 {
|
||||
Some(Coin::new(
|
||||
self.operator_rewards,
|
||||
self.base_denom.to_string(),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct AddressDelegationInfo {
|
||||
delegations: Vec<nym_mixnet_contract_common::Delegation>,
|
||||
delegated_to_nodes: HashMap<NodeId, RewardAndBondInfo>,
|
||||
}
|
||||
|
||||
impl AddressDelegationInfo {
|
||||
pub(crate) fn delegations(self) -> Vec<nym_mixnet_contract_common::Delegation> {
|
||||
self.delegations
|
||||
}
|
||||
}
|
||||
|
||||
type RewardAndBondInfo = (NodeRewarding, bool);
|
||||
|
||||
fn decimal_to_coin(decimal: Decimal, denom: impl Into<String>) -> Coin {
|
||||
Coin::new(decimal.to_uint_floor(), denom)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn decimal_to_coin_test() {
|
||||
let test_values = [
|
||||
(1234, 0, 1234),
|
||||
(1234, 2, 12),
|
||||
(1_234_000_000_000_000u128, 6, 1_234_000_000u128),
|
||||
];
|
||||
|
||||
for (amount, decimal_places, coin_amount) in test_values {
|
||||
let decimal =
|
||||
Decimal::from_atomics(cosmwasm_std::Uint128::new(amount), decimal_places).unwrap();
|
||||
let coin_from_decimal = decimal_to_coin(decimal, "unym");
|
||||
assert_eq!(coin_from_decimal, Coin::new(coin_amount, "unym"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::{
|
||||
node_status_api::models::{AxumErrorResponse, AxumResult},
|
||||
support::http::state::AppState,
|
||||
unstable_routes::models::NyxAccountDetails,
|
||||
};
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
routing::get,
|
||||
Json, Router,
|
||||
};
|
||||
use nym_validator_client::nyxd::AccountId;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::str::FromStr;
|
||||
use tracing::{error, instrument};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
pub(crate) mod cache;
|
||||
pub(crate) mod data_collector;
|
||||
|
||||
pub(crate) fn routes() -> Router<AppState> {
|
||||
Router::new().route("/:address", get(address))
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, utoipa::IntoParams)]
|
||||
pub struct AddressQueryParam {
|
||||
#[serde(default)]
|
||||
pub address: String,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
tag = "Unstable",
|
||||
get,
|
||||
path = "/{address}",
|
||||
context_path = "/v1/unstable/account",
|
||||
responses(
|
||||
(status = 200, body = NyxAccountDetails)
|
||||
),
|
||||
params(AddressQueryParam)
|
||||
)]
|
||||
#[instrument(level = "info", skip_all, fields(address=address))]
|
||||
async fn address(
|
||||
Path(AddressQueryParam { address }): Path<AddressQueryParam>,
|
||||
State(state): State<AppState>,
|
||||
) -> AxumResult<Json<NyxAccountDetails>> {
|
||||
let account_id = AccountId::from_str(&address).map_err(|err| {
|
||||
error!("{err}");
|
||||
AxumErrorResponse::not_found(&address)
|
||||
})?;
|
||||
|
||||
state.get_address_info(account_id).await.map(Json)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
pub(crate) mod account;
|
||||
pub(crate) mod models;
|
||||
|
||||
use crate::support::http::state::AppState;
|
||||
use axum::Router;
|
||||
|
||||
// as those get stabilised, they should get deprecated and use a redirection instead
|
||||
pub(crate) fn unstable_routes() -> Router<AppState> {
|
||||
Router::new()
|
||||
.nest("/nym-nodes", crate::nym_nodes::handlers::unstable::routes())
|
||||
.nest("/account", account::routes())
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use cosmwasm_std::{Addr, Coin};
|
||||
use nym_topology::NodeId;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::schema;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, utoipa::ToSchema, utoipa::ToResponse)]
|
||||
#[schema(title = "Coin")]
|
||||
pub struct CoinSchema {
|
||||
pub denom: String,
|
||||
pub amount: u128,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, utoipa::ToSchema, utoipa::ToResponse)]
|
||||
pub struct NyxAccountDelegationDetails {
|
||||
pub node_id: NodeId,
|
||||
#[schema(value_type = CoinSchema)]
|
||||
pub delegated: Coin,
|
||||
pub height: u64,
|
||||
#[schema(value_type = Option<String>)]
|
||||
pub proxy: Option<Addr>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, utoipa::ToSchema, utoipa::ToResponse)]
|
||||
pub struct NyxAccountDelegationRewardDetails {
|
||||
pub node_id: NodeId,
|
||||
#[schema(value_type = CoinSchema)]
|
||||
pub rewards: Coin,
|
||||
#[schema(value_type = String)]
|
||||
pub amount_staked: Coin,
|
||||
pub node_still_fully_bonded: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, utoipa::ToSchema, utoipa::ToResponse)]
|
||||
pub struct NyxAccountDetails {
|
||||
pub address: String,
|
||||
#[schema(value_type = CoinSchema)]
|
||||
pub balance: Coin,
|
||||
#[schema(value_type = CoinSchema)]
|
||||
pub total_value: Coin,
|
||||
pub delegations: Vec<NyxAccountDelegationDetails>,
|
||||
pub accumulated_rewards: Vec<NyxAccountDelegationRewardDetails>,
|
||||
#[schema(value_type = String)]
|
||||
pub total_delegations: Coin,
|
||||
#[schema(value_type = CoinSchema)]
|
||||
pub claimable_rewards: Coin,
|
||||
#[schema(value_type = Option<CoinSchema>)]
|
||||
pub operator_rewards: Option<Coin>,
|
||||
}
|
||||
@@ -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"]
|
||||
|
||||
@@ -31,7 +31,7 @@ pub fn build_router(state: ApiState, auth_token: String) -> Router {
|
||||
.nest(routes::API, api::routes(auth_middleware))
|
||||
// we don't have to be using middleware, but we already had that code
|
||||
// we might want something like: https://github.com/tokio-rs/axum/blob/main/examples/tracing-aka-logging/src/main.rs#L44 instead
|
||||
.layer(axum::middleware::from_fn(logging::logger))
|
||||
.layer(axum::middleware::from_fn(logging::log_request_info))
|
||||
.with_state(state);
|
||||
|
||||
cfg_if::cfg_if! {
|
||||
|
||||
@@ -20,7 +20,7 @@ nym-bin-common = { path = "../../common/bin-common", features = ["models"]}
|
||||
nym-node-status-client = { path = "../nym-node-status-client" }
|
||||
nym-crypto = { path = "../../common/crypto", features = ["asymmetric", "rand"] }
|
||||
rand = { workspace = true }
|
||||
tokio = { workspace = true, features = ["macros", "rt-multi-thread", "process"] }
|
||||
tokio = { workspace = true, features = ["macros", "rt-multi-thread", "process", "fs"] }
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true, features = ["env-filter"] }
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
[package]
|
||||
name = "nym-node-status-api"
|
||||
version = "2.1.0"
|
||||
version = "2.3.0"
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
@@ -25,10 +25,12 @@ futures-util = { workspace = true }
|
||||
itertools = { workspace = true }
|
||||
moka = { workspace = true, features = ["future"] }
|
||||
nym-contracts-common = { path = "../../common/cosmwasm-smart-contracts/contracts-common" }
|
||||
nym-mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-contract", features = ["utoipa"] }
|
||||
nym-bin-common = { path = "../../common/bin-common", features = ["models"] }
|
||||
nym-node-status-client = { path = "../nym-node-status-client" }
|
||||
nym-crypto = { path = "../../common/crypto", features = ["asymmetric", "serde"] }
|
||||
nym-http-api-client = { path = "../../common/http-api-client" }
|
||||
nym-http-api-common = { path = "../../common/http-api-common" }
|
||||
nym-network-defaults = { path = "../../common/network-defaults" }
|
||||
nym-serde-helpers = { path = "../../common/serde-helpers" }
|
||||
nym-statistics-common = { path = "../../common/statistics" }
|
||||
|
||||
@@ -6,6 +6,7 @@ use crate::{
|
||||
DbPool,
|
||||
},
|
||||
http::models::Gateway,
|
||||
mixnet_scraper::helpers::NodeDescriptionResponse,
|
||||
};
|
||||
use futures_util::TryStreamExt;
|
||||
use sqlx::{pool::PoolConnection, Sqlite};
|
||||
@@ -131,3 +132,39 @@ pub(crate) async fn get_bonded_gateway_id_keys(pool: &DbPool) -> anyhow::Result<
|
||||
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
pub(crate) async fn insert_gateway_description(
|
||||
conn: &mut PoolConnection<Sqlite>,
|
||||
identity_key: &str,
|
||||
description: &NodeDescriptionResponse,
|
||||
timestamp: i64,
|
||||
) -> anyhow::Result<()> {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO gateway_description (
|
||||
gateway_identity_key,
|
||||
moniker,
|
||||
website,
|
||||
security_contact,
|
||||
details,
|
||||
last_updated_utc
|
||||
) VALUES (?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT (gateway_identity_key) DO UPDATE SET
|
||||
moniker = excluded.moniker,
|
||||
website = excluded.website,
|
||||
security_contact = excluded.security_contact,
|
||||
details = excluded.details,
|
||||
last_updated_utc = excluded.last_updated_utc
|
||||
"#,
|
||||
identity_key,
|
||||
description.moniker,
|
||||
description.website,
|
||||
description.security_contact,
|
||||
description.details,
|
||||
timestamp,
|
||||
)
|
||||
.execute(conn.as_mut())
|
||||
.await
|
||||
.map(drop)
|
||||
.map_err(From::from)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use futures_util::TryStreamExt;
|
||||
use sqlx::{pool::PoolConnection, Sqlite};
|
||||
use tracing::error;
|
||||
|
||||
use crate::{
|
||||
@@ -9,6 +10,7 @@ use crate::{
|
||||
DbPool,
|
||||
},
|
||||
http::models::{DailyStats, Mixnode},
|
||||
mixnet_scraper::helpers::NodeDescriptionResponse,
|
||||
};
|
||||
|
||||
pub(crate) async fn update_mixnodes(
|
||||
@@ -157,3 +159,34 @@ pub(crate) async fn get_bonded_mix_ids(pool: &DbPool) -> anyhow::Result<HashSet<
|
||||
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
pub(crate) async fn insert_mixnode_description(
|
||||
conn: &mut PoolConnection<Sqlite>,
|
||||
mix_id: &i64,
|
||||
description: &NodeDescriptionResponse,
|
||||
timestamp: i64,
|
||||
) -> anyhow::Result<()> {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO mixnode_description (
|
||||
mix_id, moniker, website, security_contact, details, last_updated_utc
|
||||
) VALUES (?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT (mix_id) DO UPDATE SET
|
||||
moniker = excluded.moniker,
|
||||
website = excluded.website,
|
||||
security_contact = excluded.security_contact,
|
||||
details = excluded.details,
|
||||
last_updated_utc = excluded.last_updated_utc
|
||||
"#,
|
||||
mix_id,
|
||||
description.moniker,
|
||||
description.website,
|
||||
description.security_contact,
|
||||
description.details,
|
||||
timestamp,
|
||||
)
|
||||
.execute(conn.as_mut())
|
||||
.await
|
||||
.map(drop)
|
||||
.map_err(From::from)
|
||||
}
|
||||
|
||||
@@ -4,12 +4,16 @@ use nym_validator_client::{
|
||||
client::{NodeId, NymNodeDetails},
|
||||
models::NymNodeDescription,
|
||||
};
|
||||
use sqlx::{pool::PoolConnection, Sqlite};
|
||||
use std::collections::HashMap;
|
||||
use tracing::instrument;
|
||||
|
||||
use crate::db::{
|
||||
models::{NymNodeDto, NymNodeInsertRecord},
|
||||
DbPool,
|
||||
use crate::{
|
||||
db::{
|
||||
models::{NymNodeDto, NymNodeInsertRecord},
|
||||
DbPool,
|
||||
},
|
||||
mixnet_scraper::helpers::NodeDescriptionResponse,
|
||||
};
|
||||
|
||||
pub(crate) async fn get_all_nym_nodes(pool: &DbPool) -> anyhow::Result<Vec<NymNodeDto>> {
|
||||
@@ -194,6 +198,8 @@ pub(crate) async fn get_node_self_description(
|
||||
nym_nodes
|
||||
WHERE
|
||||
self_described IS NOT NULL
|
||||
ORDER BY
|
||||
node_id
|
||||
"#,
|
||||
)
|
||||
.fetch_all(&mut *conn)
|
||||
@@ -256,3 +262,34 @@ pub(crate) async fn get_bonded_node_description(
|
||||
})
|
||||
.map_err(From::from)
|
||||
}
|
||||
|
||||
pub(crate) async fn insert_nym_node_description(
|
||||
conn: &mut PoolConnection<Sqlite>,
|
||||
node_id: &i64,
|
||||
description: &NodeDescriptionResponse,
|
||||
timestamp: i64,
|
||||
) -> anyhow::Result<()> {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO nym_node_descriptions (
|
||||
node_id, moniker, website, security_contact, details, last_updated_utc
|
||||
) VALUES (?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT (node_id) DO UPDATE SET
|
||||
moniker = excluded.moniker,
|
||||
website = excluded.website,
|
||||
security_contact = excluded.security_contact,
|
||||
details = excluded.details,
|
||||
last_updated_utc = excluded.last_updated_utc
|
||||
"#,
|
||||
node_id,
|
||||
description.moniker,
|
||||
description.website,
|
||||
description.security_contact,
|
||||
description.details,
|
||||
timestamp,
|
||||
)
|
||||
.execute(conn.as_mut())
|
||||
.await
|
||||
.map(drop)
|
||||
.map_err(From::from)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
use crate::{
|
||||
db::{
|
||||
models::{ScrapeNodeKind, ScraperNodeInfo},
|
||||
queries, DbPool,
|
||||
queries::{
|
||||
self, gateways::insert_gateway_description, mixnodes::insert_mixnode_description,
|
||||
nym_nodes::insert_nym_node_description,
|
||||
},
|
||||
DbPool,
|
||||
},
|
||||
mixnet_scraper::helpers::NodeDescriptionResponse,
|
||||
};
|
||||
@@ -125,78 +129,18 @@ pub(crate) async fn insert_scraped_node_description(
|
||||
|
||||
match node_kind {
|
||||
ScrapeNodeKind::LegacyMixnode { mix_id } => {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO mixnode_description (
|
||||
mix_id, moniker, website, security_contact, details, last_updated_utc
|
||||
) VALUES (?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT (mix_id) DO UPDATE SET
|
||||
moniker = excluded.moniker,
|
||||
website = excluded.website,
|
||||
security_contact = excluded.security_contact,
|
||||
details = excluded.details,
|
||||
last_updated_utc = excluded.last_updated_utc
|
||||
"#,
|
||||
mix_id,
|
||||
description.moniker,
|
||||
description.website,
|
||||
description.security_contact,
|
||||
description.details,
|
||||
timestamp,
|
||||
)
|
||||
.execute(&mut *conn)
|
||||
.await?;
|
||||
insert_mixnode_description(&mut conn, mix_id, description, timestamp).await?;
|
||||
}
|
||||
ScrapeNodeKind::MixingNymNode { node_id } => {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO nym_node_descriptions (
|
||||
node_id, moniker, website, security_contact, details, last_updated_utc
|
||||
) VALUES (?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT (node_id) DO UPDATE SET
|
||||
moniker = excluded.moniker,
|
||||
website = excluded.website,
|
||||
security_contact = excluded.security_contact,
|
||||
details = excluded.details,
|
||||
last_updated_utc = excluded.last_updated_utc
|
||||
"#,
|
||||
node_id,
|
||||
description.moniker,
|
||||
description.website,
|
||||
description.security_contact,
|
||||
description.details,
|
||||
timestamp,
|
||||
)
|
||||
.execute(&mut *conn)
|
||||
.await?;
|
||||
insert_nym_node_description(&mut conn, node_id, description, timestamp).await?;
|
||||
}
|
||||
ScrapeNodeKind::EntryExitNymNode { identity_key, .. } => {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO gateway_description (
|
||||
gateway_identity_key,
|
||||
moniker,
|
||||
website,
|
||||
security_contact,
|
||||
details,
|
||||
last_updated_utc
|
||||
) VALUES (?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT (gateway_identity_key) DO UPDATE SET
|
||||
moniker = excluded.moniker,
|
||||
website = excluded.website,
|
||||
security_contact = excluded.security_contact,
|
||||
details = excluded.details,
|
||||
last_updated_utc = excluded.last_updated_utc
|
||||
"#,
|
||||
identity_key,
|
||||
description.moniker,
|
||||
description.website,
|
||||
description.security_contact,
|
||||
description.details,
|
||||
timestamp,
|
||||
)
|
||||
.execute(&mut *conn)
|
||||
.await?;
|
||||
ScrapeNodeKind::EntryExitNymNode {
|
||||
node_id,
|
||||
identity_key,
|
||||
} => {
|
||||
insert_nym_node_description(&mut conn, node_id, description, timestamp).await?;
|
||||
// for historic reasons (/gateways API), store this info into gateways table as well
|
||||
insert_gateway_description(&mut conn, identity_key, description, timestamp).await?;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ async fn get_all_sessions(
|
||||
};
|
||||
|
||||
Ok(Json(PagedResult::paginate(
|
||||
Pagination { size, page },
|
||||
Pagination::new(size, page),
|
||||
day_and_node_filtered,
|
||||
)))
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use anyhow::anyhow;
|
||||
use axum::{response::Redirect, Router};
|
||||
use nym_http_api_common::middleware::logging::log_request_debug;
|
||||
use tokio::net::ToSocketAddrs;
|
||||
use tower_http::{cors::CorsLayer, trace::TraceLayer};
|
||||
use tower_http::cors::CorsLayer;
|
||||
use utoipa::OpenApi;
|
||||
use utoipa_swagger_ui::SwaggerUi;
|
||||
|
||||
@@ -39,7 +40,10 @@ impl RouterBuilder {
|
||||
.nest("/summary", summary::routes())
|
||||
.nest("/metrics", metrics::routes()),
|
||||
)
|
||||
.nest("/v3", Router::new().nest("/nym-nodes", nym_nodes::routes()))
|
||||
.nest(
|
||||
"/explorer/v3",
|
||||
Router::new().nest("/nym-nodes", nym_nodes::routes()),
|
||||
)
|
||||
.nest(
|
||||
"/internal",
|
||||
Router::new().nest("/testruns", testruns::routes()),
|
||||
@@ -62,7 +66,7 @@ impl RouterBuilder {
|
||||
// CORS layer needs to wrap other API layers
|
||||
.layer(setup_cors())
|
||||
// logger should be outermost layer
|
||||
.layer(TraceLayer::new_for_http())
|
||||
.layer(axum::middleware::from_fn(log_request_debug))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,32 +1,41 @@
|
||||
use axum::{
|
||||
extract::{Query, State},
|
||||
extract::{Path, Query, State},
|
||||
Json, Router,
|
||||
};
|
||||
use nym_validator_client::client::NodeId;
|
||||
use serde::Deserialize;
|
||||
use tracing::instrument;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::http::{
|
||||
error::{HttpError, HttpResult},
|
||||
models::ExtendedNymNode,
|
||||
models::{ExtendedNymNode, NodeDelegation},
|
||||
state::AppState,
|
||||
PagedResult, Pagination,
|
||||
};
|
||||
|
||||
pub(crate) fn routes() -> Router<AppState> {
|
||||
Router::new().route("/", axum::routing::get(nym_nodes))
|
||||
Router::new()
|
||||
.route("/", axum::routing::get(nym_nodes))
|
||||
.route(
|
||||
"/:node_id/delegations",
|
||||
axum::routing::get(node_delegations),
|
||||
)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
tag = "Nym Nodes",
|
||||
tag = "Nym Explorer",
|
||||
get,
|
||||
params(
|
||||
Pagination
|
||||
),
|
||||
path = "/v3/nym-nodes",
|
||||
path = "/nym-nodes",
|
||||
context_path = "/explorer/v3",
|
||||
responses(
|
||||
(status = 200, body = PagedResult<ExtendedNymNode>)
|
||||
)
|
||||
)]
|
||||
#[instrument(level = tracing::Level::DEBUG, skip_all, fields(page=pagination.page, size=pagination.size))]
|
||||
#[instrument(level = tracing::Level::INFO, skip_all, fields(page=pagination.page, size=pagination.size))]
|
||||
async fn nym_nodes(
|
||||
Query(pagination): Query<Pagination>,
|
||||
State(state): State<AppState>,
|
||||
@@ -45,3 +54,35 @@ async fn nym_nodes(
|
||||
|
||||
Ok(Json(PagedResult::paginate(pagination, nodes)))
|
||||
}
|
||||
|
||||
#[allow(dead_code)] // clippy doesn't detect usage in utoipa macros
|
||||
#[derive(Deserialize, IntoParams)]
|
||||
#[into_params(parameter_in = Path)]
|
||||
struct NodeIdParam {
|
||||
#[param(minimum = 0)]
|
||||
node_id: NodeId,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
tag = "Nym Explorer",
|
||||
get,
|
||||
params(
|
||||
NodeIdParam
|
||||
),
|
||||
path = "/{node_id}/delegations",
|
||||
context_path = "/explorer/v3/nym-nodes",
|
||||
responses(
|
||||
(status = 200, body = NodeDelegation)
|
||||
)
|
||||
)]
|
||||
#[instrument(level = tracing::Level::INFO, skip(state))]
|
||||
async fn node_delegations(
|
||||
Path(node_id): Path<NodeId>,
|
||||
State(state): State<AppState>,
|
||||
) -> HttpResult<Json<Vec<NodeDelegation>>> {
|
||||
state
|
||||
.node_delegations(node_id)
|
||||
.await
|
||||
.ok_or_else(|| HttpError::no_delegations_for_node(node_id))
|
||||
.map(Json)
|
||||
}
|
||||
|
||||
@@ -104,7 +104,10 @@ async fn mixnodes(
|
||||
.map(|(s, _)| s)
|
||||
.collect();
|
||||
|
||||
Ok(Json(PagedResult::paginate(Pagination { size, page }, res)))
|
||||
Ok(Json(PagedResult::paginate(
|
||||
Pagination::new(size, page),
|
||||
res,
|
||||
)))
|
||||
}
|
||||
|
||||
struct ServiceFilter {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use nym_mixnet_contract_common::NodeId;
|
||||
use std::fmt::Display;
|
||||
|
||||
pub(crate) type HttpResult<T> = Result<T, HttpError>;
|
||||
@@ -40,6 +41,14 @@ impl HttpError {
|
||||
status: axum::http::StatusCode::SERVICE_UNAVAILABLE,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn no_delegations_for_node(node_id: NodeId) -> Self {
|
||||
Self {
|
||||
message: serde_json::json!({"message": format!("No delegation data for node_id={}", node_id)})
|
||||
.to_string(),
|
||||
status: axum::http::StatusCode::NOT_FOUND,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl axum::response::IntoResponse for HttpError {
|
||||
|
||||
@@ -18,7 +18,7 @@ pub struct PagedResult<T: ToSchema> {
|
||||
impl<T: Clone + ToSchema> PagedResult<T> {
|
||||
pub fn paginate(pagination: Pagination, res: Vec<T>) -> Self {
|
||||
let total = res.len();
|
||||
let (size, mut page) = pagination.intoto_inner_values();
|
||||
let (size, mut page) = pagination.into_inner_values();
|
||||
|
||||
if page * size > total {
|
||||
page = total / size;
|
||||
@@ -42,14 +42,25 @@ pub(crate) struct Pagination {
|
||||
page: Option<usize>,
|
||||
}
|
||||
|
||||
const SIZE_DEFAULT: usize = 10;
|
||||
const SIZE_MAX: usize = 200;
|
||||
const PAGE_DEFAULT: usize = 0;
|
||||
|
||||
impl Default for Pagination {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
size: Some(SIZE_DEFAULT),
|
||||
page: Some(PAGE_DEFAULT),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Pagination {
|
||||
// unwrap stored values or use predefined defaults
|
||||
pub(crate) fn intoto_inner_values(self) -> (usize, usize) {
|
||||
const SIZE_DEFAULT: usize = 10;
|
||||
const SIZE_MAX: usize = 200;
|
||||
|
||||
const PAGE_DEFAULT: usize = 0;
|
||||
pub(crate) fn new(size: Option<usize>, page: Option<usize>) -> Self {
|
||||
Self { size, page }
|
||||
}
|
||||
|
||||
pub(crate) fn into_inner_values(self) -> (usize, usize) {
|
||||
(
|
||||
self.size.unwrap_or(SIZE_DEFAULT).min(SIZE_MAX),
|
||||
self.page.unwrap_or(PAGE_DEFAULT),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use cosmwasm_std::Decimal;
|
||||
use cosmwasm_std::{Addr, Coin, Decimal};
|
||||
use nym_mixnet_contract_common::CoinSchema;
|
||||
use nym_node_requests::api::v1::node::models::NodeDescription;
|
||||
use nym_validator_client::client::NodeId;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -55,13 +56,13 @@ pub(crate) struct ExtendedNymNode {
|
||||
#[schema(value_type = String)]
|
||||
pub(crate) total_stake: Decimal,
|
||||
pub(crate) original_pledge: u128,
|
||||
pub(crate) bonding_address: String,
|
||||
pub(crate) bonding_address: Option<String>,
|
||||
pub(crate) bonded: bool,
|
||||
pub(crate) node_type: String,
|
||||
pub(crate) node_type: nym_validator_client::models::DescribedNodeType,
|
||||
pub(crate) ip_address: String,
|
||||
pub(crate) accepted_tnc: bool,
|
||||
pub(crate) self_description: serde_json::Value,
|
||||
pub(crate) rewarding_details: serde_json::Value,
|
||||
pub(crate) self_description: nym_validator_client::models::NymNodeData,
|
||||
pub(crate) rewarding_details: Option<nym_mixnet_contract_common::NodeRewarding>,
|
||||
pub(crate) description: NodeDescription,
|
||||
pub(crate) geoip: Option<NodeGeoData>,
|
||||
}
|
||||
@@ -120,3 +121,27 @@ pub struct SessionStats {
|
||||
pub mixnet_sessions: Option<serde_json::Value>,
|
||||
pub unknown_sessions: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
|
||||
pub struct NodeDelegation {
|
||||
#[schema(value_type = CoinSchema)]
|
||||
pub amount: Coin,
|
||||
pub cumulative_reward_ratio: String,
|
||||
pub block_height: u64,
|
||||
#[schema(value_type = String)]
|
||||
pub owner: Addr,
|
||||
#[schema(value_type = Option<String>)]
|
||||
pub proxy: Option<Addr>,
|
||||
}
|
||||
|
||||
impl From<nym_mixnet_contract_common::Delegation> for NodeDelegation {
|
||||
fn from(value: nym_mixnet_contract_common::Delegation) -> Self {
|
||||
Self {
|
||||
amount: value.amount,
|
||||
cumulative_reward_ratio: value.cumulative_reward_ratio.to_string(),
|
||||
block_height: value.height,
|
||||
owner: value.owner,
|
||||
proxy: value.proxy,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
use axum::Router;
|
||||
use core::net::SocketAddr;
|
||||
use nym_crypto::asymmetric::ed25519::PublicKey;
|
||||
use tokio::{net::TcpListener, task::JoinHandle};
|
||||
use std::sync::Arc;
|
||||
use tokio::{net::TcpListener, sync::RwLock, task::JoinHandle};
|
||||
use tokio_util::sync::{CancellationToken, WaitForCancellationFutureOwned};
|
||||
|
||||
use crate::{
|
||||
db::DbPool,
|
||||
http::{api::RouterBuilder, state::AppState},
|
||||
monitor::NodeGeoCache,
|
||||
monitor::{DelegationsCache, NodeGeoCache},
|
||||
};
|
||||
|
||||
/// Return handles that allow for graceful shutdown of server + awaiting its
|
||||
@@ -19,6 +20,7 @@ pub(crate) async fn start_http_api(
|
||||
agent_key_list: Vec<PublicKey>,
|
||||
agent_max_count: i64,
|
||||
node_geocache: NodeGeoCache,
|
||||
node_delegations: Arc<RwLock<DelegationsCache>>,
|
||||
) -> anyhow::Result<ShutdownHandles> {
|
||||
let router_builder = RouterBuilder::with_default_routes();
|
||||
|
||||
@@ -28,6 +30,7 @@ pub(crate) async fn start_http_api(
|
||||
agent_key_list,
|
||||
agent_max_count,
|
||||
node_geocache,
|
||||
node_delegations,
|
||||
)
|
||||
.await;
|
||||
let router = router_builder.with_state(state);
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
use std::{collections::HashMap, sync::Arc, time::Duration};
|
||||
|
||||
use cosmwasm_std::Decimal;
|
||||
use moka::{future::Cache, Entry};
|
||||
use nym_contracts_common::NaiveFloat;
|
||||
use nym_crypto::asymmetric::ed25519::PublicKey;
|
||||
use nym_validator_client::{models::DescribedNodeType, nym_api::SkimmedNode};
|
||||
use nym_mixnet_contract_common::NodeId;
|
||||
use nym_validator_client::nym_api::SkimmedNode;
|
||||
use std::{collections::HashMap, sync::Arc, time::Duration};
|
||||
use tokio::sync::RwLock;
|
||||
use tracing::instrument;
|
||||
|
||||
use crate::{
|
||||
db::{queries, DbPool},
|
||||
http::models::{DailyStats, ExtendedNymNode, Gateway, Mixnode, NodeGeoData, SummaryHistory},
|
||||
monitor::NodeGeoCache,
|
||||
monitor::{DelegationsCache, NodeGeoCache},
|
||||
};
|
||||
|
||||
use super::models::SessionStats;
|
||||
@@ -23,6 +23,7 @@ pub(crate) struct AppState {
|
||||
agent_key_list: Vec<PublicKey>,
|
||||
agent_max_count: i64,
|
||||
node_geocache: NodeGeoCache,
|
||||
node_delegations: Arc<RwLock<DelegationsCache>>,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
@@ -32,6 +33,7 @@ impl AppState {
|
||||
agent_key_list: Vec<PublicKey>,
|
||||
agent_max_count: i64,
|
||||
node_geocache: NodeGeoCache,
|
||||
node_delegations: Arc<RwLock<DelegationsCache>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
db_pool,
|
||||
@@ -39,6 +41,7 @@ impl AppState {
|
||||
agent_key_list,
|
||||
agent_max_count,
|
||||
node_geocache,
|
||||
node_delegations,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,6 +64,16 @@ impl AppState {
|
||||
pub(crate) fn node_geocache(&self) -> NodeGeoCache {
|
||||
self.node_geocache.clone()
|
||||
}
|
||||
|
||||
pub(crate) async fn node_delegations(
|
||||
&self,
|
||||
node_id: NodeId,
|
||||
) -> Option<Vec<super::models::NodeDelegation>> {
|
||||
self.node_delegations
|
||||
.read()
|
||||
.await
|
||||
.delegations_owned(node_id)
|
||||
}
|
||||
}
|
||||
|
||||
static GATEWAYS_LIST_KEY: &str = "gateways";
|
||||
@@ -401,11 +414,7 @@ async fn aggregate_node_info_from_db(
|
||||
.get(&node_id)
|
||||
.map(|node| node.performance.naive_to_f64())
|
||||
.unwrap_or(0.0);
|
||||
let node_type = match described_node.contract_node_type {
|
||||
DescribedNodeType::NymNode => "nym_node".to_string(),
|
||||
DescribedNodeType::LegacyMixnode => "legacy_mixnode".to_string(),
|
||||
DescribedNodeType::LegacyGateway => "legacy_gateway".to_string(),
|
||||
};
|
||||
let node_type = described_node.contract_node_type;
|
||||
let ip_address = described_node
|
||||
.description
|
||||
.host_information
|
||||
@@ -417,11 +426,10 @@ async fn aggregate_node_info_from_db(
|
||||
.description
|
||||
.auxiliary_details
|
||||
.accepted_operator_terms_and_conditions;
|
||||
let description = described_node.description;
|
||||
let self_described = described_node.description;
|
||||
|
||||
let bonding_address = bond_details
|
||||
.map(|details| details.bond_information.owner.to_string())
|
||||
.unwrap_or_default();
|
||||
let bonding_address =
|
||||
bond_details.map(|details| details.bond_information.owner.to_string());
|
||||
|
||||
let node_description = node_descriptions.get(&node_id).cloned().unwrap_or_default();
|
||||
let geoip = {
|
||||
@@ -449,8 +457,8 @@ async fn aggregate_node_info_from_db(
|
||||
bonded,
|
||||
node_type,
|
||||
accepted_tnc,
|
||||
self_description: serde_json::to_value(description).unwrap_or_default(),
|
||||
rewarding_details: serde_json::to_value(rewarding_details).unwrap_or_default(),
|
||||
self_description: self_described,
|
||||
rewarding_details: rewarding_details.to_owned(),
|
||||
description: node_description,
|
||||
geoip,
|
||||
});
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
use crate::monitor::DelegationsCache;
|
||||
use clap::Parser;
|
||||
use nym_crypto::asymmetric::ed25519::PublicKey;
|
||||
use nym_task::signal::wait_for_signal;
|
||||
use nym_validator_client::nyxd::NyxdClient;
|
||||
use std::sync::Arc;
|
||||
|
||||
mod cli;
|
||||
mod db;
|
||||
@@ -41,19 +44,27 @@ async fn main() -> anyhow::Result<()> {
|
||||
let geocache = moka::future::Cache::builder()
|
||||
.time_to_live(args.geodata_ttl)
|
||||
.build();
|
||||
let delegations_cache = DelegationsCache::new();
|
||||
|
||||
// Start the monitor
|
||||
let args_clone = args.clone();
|
||||
let geocache_clone = geocache.clone();
|
||||
let delegations_cache_clone = Arc::clone(&delegations_cache);
|
||||
let config = nym_validator_client::nyxd::Config::try_from_nym_network_details(
|
||||
&nym_network_defaults::NymNetworkDetails::new_from_env(),
|
||||
)?;
|
||||
let nyxd_client = NyxdClient::connect(config, args.nyxd_addr.as_str())
|
||||
.map_err(|err| anyhow::anyhow!("Couldn't connect: {}", err))?;
|
||||
|
||||
tokio::spawn(async move {
|
||||
monitor::spawn_in_background(
|
||||
db_pool,
|
||||
args_clone.nym_api_client_timeout,
|
||||
args_clone.nyxd_addr,
|
||||
nyxd_client,
|
||||
args_clone.monitor_refresh_interval,
|
||||
args_clone.ipinfo_api_token,
|
||||
geocache_clone,
|
||||
delegations_cache_clone,
|
||||
)
|
||||
.await;
|
||||
tracing::info!("Started monitor task");
|
||||
@@ -74,6 +85,7 @@ async fn main() -> anyhow::Result<()> {
|
||||
agent_key_list.to_owned(),
|
||||
args.max_agent_count,
|
||||
geocache,
|
||||
delegations_cache,
|
||||
)
|
||||
.await
|
||||
.expect("Failed to start server");
|
||||
|
||||
@@ -12,25 +12,30 @@ use crate::utils::{decimal_to_i64, LogError, NumericalCheckedCast};
|
||||
use anyhow::anyhow;
|
||||
use moka::future::Cache;
|
||||
use nym_network_defaults::NymNetworkDetails;
|
||||
use nym_validator_client::client::{NodeId, NymApiClientExt, NymNodeDetails};
|
||||
use nym_validator_client::models::{
|
||||
LegacyDescribedMixNode, MixNodeBondAnnotated, NymNodeDescription,
|
||||
use nym_validator_client::{
|
||||
client::{NodeId, NymApiClientExt, NymNodeDetails},
|
||||
models::{LegacyDescribedMixNode, MixNodeBondAnnotated, NymNodeDescription},
|
||||
};
|
||||
use nym_validator_client::nym_nodes::{NodeRole, SkimmedNode};
|
||||
use nym_validator_client::nyxd::contract_traits::PagedMixnetQueryClient;
|
||||
use nym_validator_client::nyxd::{AccountId, NyxdClient};
|
||||
use nym_validator_client::NymApiClient;
|
||||
use reqwest::Url;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::str::FromStr;
|
||||
use tokio::time::Duration;
|
||||
use nym_validator_client::{
|
||||
nym_nodes::{NodeRole, SkimmedNode},
|
||||
nyxd::{contract_traits::PagedMixnetQueryClient, AccountId},
|
||||
NymApiClient, QueryHttpRpcNyxdClient,
|
||||
};
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
str::FromStr,
|
||||
sync::Arc,
|
||||
};
|
||||
use tokio::{sync::RwLock, time::Duration};
|
||||
use tracing::instrument;
|
||||
|
||||
pub(crate) use geodata::IpInfoClient;
|
||||
pub(crate) use node_delegations::DelegationsCache;
|
||||
|
||||
mod geodata;
|
||||
mod node_delegations;
|
||||
|
||||
const FAILURE_RETRY_DELAY: Duration = Duration::from_secs(60);
|
||||
const MONITOR_FAILURE_RETRY_DELAY: Duration = Duration::from_secs(60);
|
||||
static DELEGATION_PROGRAM_WALLET: &str = "n1rnxpdpx3kldygsklfft0gech7fhfcux4zst5lw";
|
||||
pub(crate) type NodeGeoCache = Cache<NodeId, Location>;
|
||||
|
||||
@@ -38,9 +43,10 @@ struct Monitor {
|
||||
db_pool: DbPool,
|
||||
network_details: NymNetworkDetails,
|
||||
nym_api_client_timeout: Duration,
|
||||
nyxd_addr: Url,
|
||||
nyxd_client: QueryHttpRpcNyxdClient,
|
||||
ipinfo: IpInfoClient,
|
||||
geocache: NodeGeoCache,
|
||||
node_delegations: Arc<RwLock<DelegationsCache>>,
|
||||
}
|
||||
|
||||
// TODO dz: query many NYM APIs:
|
||||
@@ -49,19 +55,22 @@ struct Monitor {
|
||||
pub(crate) async fn spawn_in_background(
|
||||
db_pool: DbPool,
|
||||
nym_api_client_timeout: Duration,
|
||||
nyxd_addr: Url,
|
||||
nyxd_client: nym_validator_client::QueryHttpRpcNyxdClient,
|
||||
refresh_interval: Duration,
|
||||
ipinfo_api_token: String,
|
||||
geocache: NodeGeoCache,
|
||||
node_delegations: Arc<RwLock<DelegationsCache>>,
|
||||
) {
|
||||
let ipinfo = IpInfoClient::new(ipinfo_api_token.clone());
|
||||
|
||||
let mut monitor = Monitor {
|
||||
db_pool,
|
||||
network_details: nym_network_defaults::NymNetworkDetails::new_from_env(),
|
||||
nym_api_client_timeout,
|
||||
nyxd_addr,
|
||||
nyxd_client,
|
||||
ipinfo,
|
||||
geocache,
|
||||
node_delegations,
|
||||
};
|
||||
|
||||
loop {
|
||||
@@ -70,10 +79,9 @@ pub(crate) async fn spawn_in_background(
|
||||
if let Err(e) = monitor.run().await {
|
||||
tracing::error!(
|
||||
"Monitor run failed: {e}, retrying in {}s...",
|
||||
FAILURE_RETRY_DELAY.as_secs()
|
||||
MONITOR_FAILURE_RETRY_DELAY.as_secs()
|
||||
);
|
||||
// TODO dz implement some sort of backoff
|
||||
tokio::time::sleep(FAILURE_RETRY_DELAY).await;
|
||||
tokio::time::sleep(MONITOR_FAILURE_RETRY_DELAY).await;
|
||||
} else {
|
||||
tracing::info!(
|
||||
"Info successfully collected, sleeping for {}s...",
|
||||
@@ -102,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()
|
||||
@@ -151,7 +159,7 @@ impl Monitor {
|
||||
})?;
|
||||
|
||||
// refresh geodata for all nodes
|
||||
for (_, node_description) in described_nodes.iter() {
|
||||
for node_description in described_nodes.values() {
|
||||
self.location_cached(node_description).await;
|
||||
}
|
||||
|
||||
@@ -187,14 +195,13 @@ 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
|
||||
.data;
|
||||
|
||||
let delegation_program_members =
|
||||
get_delegation_program_details(&self.network_details, &self.nyxd_addr).await?;
|
||||
let delegation_program_members = self.get_delegation_program_details().await?;
|
||||
|
||||
// keep stats for later
|
||||
let assigned_entry_count = nym_nodes
|
||||
@@ -233,7 +240,9 @@ impl Monitor {
|
||||
tracing::debug!("{} mixnode info written to DB!", mixnodes_count);
|
||||
})?;
|
||||
|
||||
let (all_historical_gateways, all_historical_mixnodes) = calculate_stats(&pool).await?;
|
||||
self.refresh_node_delegations(&bonded_nym_nodes).await;
|
||||
|
||||
let (all_historical_gateways, all_historical_mixnodes) = historical_count(&pool).await?;
|
||||
|
||||
//
|
||||
// write summary keys and values to table
|
||||
@@ -295,7 +304,7 @@ impl Monitor {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip_all)]
|
||||
#[instrument(level = "info", skip_all)]
|
||||
async fn location_cached(&mut self, node: &NymNodeDescription) -> Location {
|
||||
let node_id = node.node_id;
|
||||
|
||||
@@ -457,9 +466,34 @@ impl Monitor {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(level = "info", skip_all)]
|
||||
async fn refresh_node_delegations(&mut self, bonded_nodes: &HashMap<NodeId, NymNodeDetails>) {
|
||||
let delegations_per_node = node_delegations::refresh(&self.nyxd_client, bonded_nodes).await;
|
||||
|
||||
// update after refreshing all to avoid holding write lock for too long
|
||||
*self.node_delegations.write().await = delegations_per_node;
|
||||
}
|
||||
|
||||
async fn get_delegation_program_details(&self) -> anyhow::Result<Vec<NodeId>> {
|
||||
let account_id = AccountId::from_str(DELEGATION_PROGRAM_WALLET)
|
||||
.map_err(|e| anyhow!("Invalid bech32 address: {}", e))?;
|
||||
|
||||
let delegations = self
|
||||
.nyxd_client
|
||||
.get_all_delegator_delegations(&account_id)
|
||||
.await?;
|
||||
|
||||
let mix_ids: Vec<NodeId> = delegations
|
||||
.iter()
|
||||
.map(|delegation| delegation.node_id)
|
||||
.collect();
|
||||
|
||||
Ok(mix_ids)
|
||||
}
|
||||
}
|
||||
|
||||
async fn calculate_stats(pool: &DbPool) -> anyhow::Result<(usize, usize)> {
|
||||
async fn historical_count(pool: &DbPool) -> anyhow::Result<(usize, usize)> {
|
||||
let mut conn = pool.acquire().await?;
|
||||
|
||||
let all_historical_gateways = sqlx::query_scalar!(r#"SELECT count(id) FROM gateways"#)
|
||||
@@ -474,25 +508,3 @@ async fn calculate_stats(pool: &DbPool) -> anyhow::Result<(usize, usize)> {
|
||||
|
||||
Ok((all_historical_gateways, all_historical_mixnodes))
|
||||
}
|
||||
|
||||
async fn get_delegation_program_details(
|
||||
network_details: &NymNetworkDetails,
|
||||
nyxd_addr: &Url,
|
||||
) -> anyhow::Result<Vec<u32>> {
|
||||
let config = nym_validator_client::nyxd::Config::try_from_nym_network_details(network_details)?;
|
||||
|
||||
let client = NyxdClient::connect(config, nyxd_addr.as_str())
|
||||
.map_err(|err| anyhow::anyhow!("Couldn't connect: {}", err))?;
|
||||
|
||||
let account_id = AccountId::from_str(DELEGATION_PROGRAM_WALLET)
|
||||
.map_err(|e| anyhow!("Invalid bech32 address: {}", e))?;
|
||||
|
||||
let delegations = client.get_all_delegator_delegations(&account_id).await?;
|
||||
|
||||
let mix_ids: Vec<u32> = delegations
|
||||
.iter()
|
||||
.map(|delegation| delegation.node_id)
|
||||
.collect();
|
||||
|
||||
Ok(mix_ids)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
use nym_mixnet_contract_common::{NodeId, NymNodeDetails};
|
||||
use nym_validator_client::{nyxd::contract_traits::PagedMixnetQueryClient, QueryHttpRpcNyxdClient};
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
use tokio::{sync::RwLock, time::Instant};
|
||||
use tracing::{info, warn};
|
||||
|
||||
// abstracts away data structure that holds delegations
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct DelegationsCache {
|
||||
pub inner: HashMap<NodeId, Vec<crate::http::models::NodeDelegation>>,
|
||||
}
|
||||
|
||||
impl DelegationsCache {
|
||||
pub(crate) fn new() -> Arc<RwLock<Self>> {
|
||||
let a = Self {
|
||||
inner: HashMap::new(),
|
||||
};
|
||||
Arc::new(RwLock::new(a))
|
||||
}
|
||||
|
||||
pub(crate) fn delegations_owned(
|
||||
&self,
|
||||
node_id: NodeId,
|
||||
) -> Option<Vec<crate::http::models::NodeDelegation>> {
|
||||
self.inner.get(&node_id).cloned()
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) async fn refresh(
|
||||
client: &QueryHttpRpcNyxdClient,
|
||||
bonded_nodes: &HashMap<NodeId, NymNodeDetails>,
|
||||
) -> DelegationsCache {
|
||||
info!("👥 Refreshing {} node delegations...", bonded_nodes.len());
|
||||
let now = Instant::now();
|
||||
|
||||
let mut delegations_per_node = HashMap::new();
|
||||
for node_id in bonded_nodes.keys() {
|
||||
if let Ok(delegations) = client
|
||||
.get_all_single_mixnode_delegations(*node_id)
|
||||
.await
|
||||
.inspect_err(|err| warn!("Failed to get delegations for {}: {}", node_id, err))
|
||||
{
|
||||
delegations_per_node
|
||||
.insert(*node_id, delegations.into_iter().map(From::from).collect());
|
||||
}
|
||||
}
|
||||
let time_taken = Instant::now() - now;
|
||||
info!("👥 Node delegations refreshed in {}s", time_taken.as_secs(),);
|
||||
|
||||
DelegationsCache {
|
||||
inner: delegations_per_node,
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -3,7 +3,7 @@
|
||||
|
||||
[package]
|
||||
name = "nym-node"
|
||||
version = "1.10.0"
|
||||
version = "1.11.0"
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
|
||||
@@ -159,7 +159,7 @@ impl NymNodeRouter {
|
||||
)
|
||||
.nest(routes::LANDING_PAGE, landing_page::routes(config.landing))
|
||||
.nest(routes::API, api::routes(config.api))
|
||||
.layer(axum::middleware::from_fn(logging::logger))
|
||||
.layer(axum::middleware::from_fn(logging::log_request_info))
|
||||
.with_state(state),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
Generated
+270
-46
@@ -1326,6 +1326,12 @@ dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "critical-section"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b"
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-channel"
|
||||
version = "0.5.15"
|
||||
@@ -2458,6 +2464,19 @@ dependencies = [
|
||||
"x11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generator"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc6bd114ceda131d3b1d665eba35788690ad37f5916457286b32ab6fd3c438dd"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"log",
|
||||
"rustversion",
|
||||
"windows 0.58.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.7"
|
||||
@@ -2849,57 +2868,59 @@ checksum = "7ebdb29d2ea9ed0083cd8cece49bbd968021bd99b0849edb4a9a7ee0fdf6a4e0"
|
||||
|
||||
[[package]]
|
||||
name = "hickory-proto"
|
||||
version = "0.24.4"
|
||||
version = "0.25.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92652067c9ce6f66ce53cc38d1169daa36e6e7eb7dd3b63b5103bd9d97117248"
|
||||
checksum = "6d844af74f7b799e41c78221be863bade11c430d46042c3b49ca8ae0c6d27287"
|
||||
dependencies = [
|
||||
"async-recursion",
|
||||
"async-trait",
|
||||
"bytes",
|
||||
"cfg-if",
|
||||
"critical-section",
|
||||
"data-encoding",
|
||||
"enum-as-inner",
|
||||
"futures-channel",
|
||||
"futures-io",
|
||||
"futures-util",
|
||||
"h2 0.3.26",
|
||||
"http 0.2.12",
|
||||
"h2 0.4.8",
|
||||
"http 1.3.1",
|
||||
"idna",
|
||||
"ipnet",
|
||||
"once_cell",
|
||||
"rand 0.8.5",
|
||||
"rustls 0.21.12",
|
||||
"rustls-pemfile 1.0.4",
|
||||
"thiserror 1.0.69",
|
||||
"rand 0.9.0",
|
||||
"ring",
|
||||
"rustls 0.23.25",
|
||||
"thiserror 2.0.12",
|
||||
"tinyvec",
|
||||
"tokio",
|
||||
"tokio-rustls 0.24.1",
|
||||
"tokio-rustls 0.26.2",
|
||||
"tracing",
|
||||
"url",
|
||||
"webpki-roots 0.25.4",
|
||||
"webpki-roots",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hickory-resolver"
|
||||
version = "0.24.4"
|
||||
version = "0.25.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cbb117a1ca520e111743ab2f6688eddee69db4e0ea242545a604dce8a66fd22e"
|
||||
checksum = "a128410b38d6f931fcc6ca5c107a3b02cabd6c05967841269a4ad65d23c44331"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"futures-util",
|
||||
"hickory-proto",
|
||||
"ipconfig",
|
||||
"lru-cache",
|
||||
"moka",
|
||||
"once_cell",
|
||||
"parking_lot",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.0",
|
||||
"resolv-conf",
|
||||
"rustls 0.21.12",
|
||||
"rustls 0.23.25",
|
||||
"smallvec",
|
||||
"thiserror 1.0.69",
|
||||
"thiserror 2.0.12",
|
||||
"tokio",
|
||||
"tokio-rustls 0.24.1",
|
||||
"tokio-rustls 0.26.2",
|
||||
"tracing",
|
||||
"webpki-roots 0.25.4",
|
||||
"webpki-roots",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3102,7 +3123,7 @@ dependencies = [
|
||||
"tokio",
|
||||
"tokio-rustls 0.26.2",
|
||||
"tower-service",
|
||||
"webpki-roots 0.26.8",
|
||||
"webpki-roots",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3641,12 +3662,6 @@ dependencies = [
|
||||
"redox_syscall",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linked-hash-map"
|
||||
version = "0.5.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.4.15"
|
||||
@@ -3685,12 +3700,16 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lru-cache"
|
||||
version = "0.1.2"
|
||||
name = "loom"
|
||||
version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c"
|
||||
checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca"
|
||||
dependencies = [
|
||||
"linked-hash-map",
|
||||
"cfg-if",
|
||||
"generator",
|
||||
"scoped-tls",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3713,6 +3732,15 @@ dependencies = [
|
||||
"tendril",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "matchers"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
|
||||
dependencies = [
|
||||
"regex-automata 0.1.10",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "matches"
|
||||
version = "0.1.10"
|
||||
@@ -3773,6 +3801,25 @@ dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "moka"
|
||||
version = "0.12.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a9321642ca94a4282428e6ea4af8cc2ca4eac48ac7a6a4ea8f33f76d0ce70926"
|
||||
dependencies = [
|
||||
"crossbeam-channel",
|
||||
"crossbeam-epoch",
|
||||
"crossbeam-utils",
|
||||
"loom",
|
||||
"parking_lot",
|
||||
"portable-atomic",
|
||||
"rustc_version",
|
||||
"smallvec",
|
||||
"tagptr",
|
||||
"thiserror 1.0.69",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "muda"
|
||||
version = "0.16.1"
|
||||
@@ -3876,6 +3923,16 @@ dependencies = [
|
||||
"minimal-lexical",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nu-ansi-term"
|
||||
version = "0.46.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
|
||||
dependencies = [
|
||||
"overload",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-bigint"
|
||||
version = "0.4.6"
|
||||
@@ -3979,7 +4036,6 @@ version = "0.6.0"
|
||||
dependencies = [
|
||||
"const-str",
|
||||
"log",
|
||||
"pretty_env_logger",
|
||||
"schemars",
|
||||
"serde",
|
||||
"utoipa",
|
||||
@@ -4129,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",
|
||||
@@ -4145,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"
|
||||
@@ -4647,6 +4714,10 @@ name = "once_cell"
|
||||
version = "1.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
dependencies = [
|
||||
"critical-section",
|
||||
"portable-atomic",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opaque-debug"
|
||||
@@ -4750,6 +4821,12 @@ dependencies = [
|
||||
"thiserror 2.0.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "overload"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
|
||||
|
||||
[[package]]
|
||||
name = "p256"
|
||||
version = "0.13.2"
|
||||
@@ -5193,6 +5270,12 @@ dependencies = [
|
||||
"universal-hash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e"
|
||||
|
||||
[[package]]
|
||||
name = "powerfmt"
|
||||
version = "0.2.0"
|
||||
@@ -5620,8 +5703,17 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
"regex-automata 0.4.9",
|
||||
"regex-syntax 0.8.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
|
||||
dependencies = [
|
||||
"regex-syntax 0.6.29",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5632,9 +5724,15 @@ checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
"regex-syntax 0.8.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.6.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.5"
|
||||
@@ -5730,7 +5828,7 @@ dependencies = [
|
||||
"wasm-bindgen-futures",
|
||||
"wasm-streams",
|
||||
"web-sys",
|
||||
"webpki-roots 0.26.8",
|
||||
"webpki-roots",
|
||||
"windows-registry",
|
||||
]
|
||||
|
||||
@@ -5872,6 +5970,7 @@ version = "0.23.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "822ee9188ac4ec04a2f0531e55d035fb2de73f18b41a63c70c2712503b6fb13c"
|
||||
dependencies = [
|
||||
"log",
|
||||
"once_cell",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
@@ -5997,6 +6096,12 @@ dependencies = [
|
||||
"syn 2.0.100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scoped-tls"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.2.0"
|
||||
@@ -6308,6 +6413,15 @@ dependencies = [
|
||||
"digest 0.10.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sharded-slab"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shared_child"
|
||||
version = "1.0.1"
|
||||
@@ -6666,6 +6780,12 @@ dependencies = [
|
||||
"version-compare",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tagptr"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417"
|
||||
|
||||
[[package]]
|
||||
name = "tao"
|
||||
version = "0.32.8"
|
||||
@@ -7235,6 +7355,16 @@ dependencies = [
|
||||
"syn 2.0.100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thread_local"
|
||||
version = "1.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiff"
|
||||
version = "0.9.1"
|
||||
@@ -7497,6 +7627,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"valuable",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-log"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
|
||||
dependencies = [
|
||||
"log",
|
||||
"once_cell",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-subscriber"
|
||||
version = "0.3.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
|
||||
dependencies = [
|
||||
"matchers",
|
||||
"nu-ansi-term",
|
||||
"once_cell",
|
||||
"regex",
|
||||
"sharded-slab",
|
||||
"smallvec",
|
||||
"thread_local",
|
||||
"tracing",
|
||||
"tracing-core",
|
||||
"tracing-log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7757,6 +7917,12 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "valuable"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
|
||||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
version = "0.2.15"
|
||||
@@ -8082,12 +8248,6 @@ dependencies = [
|
||||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "0.25.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1"
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "0.26.8"
|
||||
@@ -8108,7 +8268,7 @@ dependencies = [
|
||||
"windows 0.60.0",
|
||||
"windows-core 0.60.1",
|
||||
"windows-implement 0.59.0",
|
||||
"windows-interface",
|
||||
"windows-interface 0.59.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8201,6 +8361,16 @@ dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.58.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6"
|
||||
dependencies = [
|
||||
"windows-core 0.58.0",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.60.0"
|
||||
@@ -8232,6 +8402,19 @@ dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.58.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99"
|
||||
dependencies = [
|
||||
"windows-implement 0.58.0",
|
||||
"windows-interface 0.58.0",
|
||||
"windows-result 0.2.0",
|
||||
"windows-strings 0.1.0",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.60.1"
|
||||
@@ -8239,9 +8422,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca21a92a9cae9bf4ccae5cf8368dce0837100ddf6e6d57936749e85f152f6247"
|
||||
dependencies = [
|
||||
"windows-implement 0.59.0",
|
||||
"windows-interface",
|
||||
"windows-interface 0.59.1",
|
||||
"windows-link",
|
||||
"windows-result",
|
||||
"windows-result 0.3.2",
|
||||
"windows-strings 0.3.1",
|
||||
]
|
||||
|
||||
@@ -8252,9 +8435,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980"
|
||||
dependencies = [
|
||||
"windows-implement 0.60.0",
|
||||
"windows-interface",
|
||||
"windows-interface 0.59.1",
|
||||
"windows-link",
|
||||
"windows-result",
|
||||
"windows-result 0.3.2",
|
||||
"windows-strings 0.4.0",
|
||||
]
|
||||
|
||||
@@ -8268,6 +8451,17 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.58.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.59.0"
|
||||
@@ -8290,6 +8484,17 @@ dependencies = [
|
||||
"syn 2.0.100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-interface"
|
||||
version = "0.58.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-interface"
|
||||
version = "0.59.1"
|
||||
@@ -8323,11 +8528,20 @@ version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3"
|
||||
dependencies = [
|
||||
"windows-result",
|
||||
"windows-result 0.3.2",
|
||||
"windows-strings 0.3.1",
|
||||
"windows-targets 0.53.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.3.2"
|
||||
@@ -8337,6 +8551,16 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10"
|
||||
dependencies = [
|
||||
"windows-result 0.2.0",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.3.1"
|
||||
|
||||
@@ -10,3 +10,4 @@ pub mod error;
|
||||
pub mod mixnet_client;
|
||||
pub mod mixnet_listener;
|
||||
mod peer_manager;
|
||||
mod seen_credential_cache;
|
||||
|
||||
@@ -7,7 +7,7 @@ use std::{
|
||||
time::{Duration, SystemTime},
|
||||
};
|
||||
|
||||
use crate::{config::Config, error::*};
|
||||
use crate::{config::Config, error::*, seen_credential_cache::SeenCredentialCache};
|
||||
use crate::{error::AuthenticatorError, peer_manager::PeerManager};
|
||||
use defguard_wireguard_rs::net::IpAddrMask;
|
||||
use defguard_wireguard_rs::{host::Peer, key::Key};
|
||||
@@ -77,6 +77,8 @@ pub(crate) struct MixnetListener {
|
||||
pub(crate) ecash_verifier: Option<Arc<EcashManager>>,
|
||||
|
||||
pub(crate) timeout_check_interval: IntervalStream,
|
||||
|
||||
pub(crate) seen_credential_cache: SeenCredentialCache,
|
||||
}
|
||||
|
||||
impl MixnetListener {
|
||||
@@ -98,6 +100,7 @@ impl MixnetListener {
|
||||
peer_manager: PeerManager::new(wireguard_gateway_data),
|
||||
ecash_verifier,
|
||||
timeout_check_interval,
|
||||
seen_credential_cache: SeenCredentialCache::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -728,19 +731,28 @@ impl MixnetListener {
|
||||
"bandwidth entry should have just been created".to_string(),
|
||||
))?;
|
||||
|
||||
let client_bandwidth = ClientBandwidth::new(bandwidth.into());
|
||||
let mut verifier = CredentialVerifier::new(
|
||||
CredentialSpendingRequest::new(msg.credential()),
|
||||
ecash_verifier.clone(),
|
||||
BandwidthStorageManager::new(
|
||||
ecash_verifier.storage().clone(),
|
||||
client_bandwidth,
|
||||
client_id,
|
||||
BandwidthFlushingBehaviourConfig::default(),
|
||||
true,
|
||||
),
|
||||
);
|
||||
let available_bandwidth = verifier.verify().await?;
|
||||
let available_bandwidth = if self.received_retry(&msg) {
|
||||
// don't process the credential and just return the current bandwidth
|
||||
bandwidth.available
|
||||
} else {
|
||||
let client_bandwidth = ClientBandwidth::new(bandwidth.into());
|
||||
let credential = msg.credential();
|
||||
let mut verifier = CredentialVerifier::new(
|
||||
CredentialSpendingRequest::new(credential.clone()),
|
||||
ecash_verifier.clone(),
|
||||
BandwidthStorageManager::new(
|
||||
ecash_verifier.storage().clone(),
|
||||
client_bandwidth,
|
||||
client_id,
|
||||
BandwidthFlushingBehaviourConfig::default(),
|
||||
true,
|
||||
),
|
||||
);
|
||||
let available_bandwidth = verifier.verify().await?;
|
||||
self.seen_credential_cache
|
||||
.insert_credential(credential, msg.pub_key());
|
||||
available_bandwidth
|
||||
};
|
||||
|
||||
let bytes = match AuthenticatorVersion::from(protocol) {
|
||||
AuthenticatorVersion::V5 => v5::response::AuthenticatorResponse::new_topup_bandwidth(
|
||||
@@ -777,6 +789,18 @@ impl MixnetListener {
|
||||
Ok((bytes, reply_to))
|
||||
}
|
||||
|
||||
fn received_retry(&self, msg: &Box<dyn TopUpMessage + Send + Sync + 'static>) -> bool {
|
||||
if let Some(peer_pub_key) = self
|
||||
.seen_credential_cache
|
||||
.get_peer_pub_key(&msg.credential())
|
||||
{
|
||||
// check if the same peer sent the same credential twice, probably because of a retry
|
||||
peer_pub_key == msg.pub_key()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
async fn on_reconstructed_message(
|
||||
&mut self,
|
||||
reconstructed: ReconstructedMessage,
|
||||
@@ -855,6 +879,7 @@ impl MixnetListener {
|
||||
if let Err(e) = self.remove_stale_registrations().await {
|
||||
log::error!("Could not clear stale registrations. The registration process might get jammed soon - {:?}", e);
|
||||
}
|
||||
self.seen_credential_cache.remove_stale();
|
||||
}
|
||||
msg = self.mixnet_client.next() => {
|
||||
if let Some(msg) = msg {
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
time::{Duration, SystemTime},
|
||||
};
|
||||
|
||||
use nym_credentials_interface::CredentialSpendingData;
|
||||
use nym_wireguard_types::PeerPublicKey;
|
||||
|
||||
const SEEN_CREDENTIAL_CACHE_TIME: Duration = Duration::from_secs(60 * 60); // 1 hour
|
||||
|
||||
#[derive(Eq, Hash, PartialEq)]
|
||||
struct TimestampedPeerPubKey {
|
||||
peer_pub_key: PeerPublicKey,
|
||||
timestamp: SystemTime,
|
||||
}
|
||||
|
||||
pub(crate) struct SeenCredentialCache {
|
||||
cached_credentials: HashMap<String, TimestampedPeerPubKey>,
|
||||
}
|
||||
|
||||
impl SeenCredentialCache {
|
||||
pub(crate) fn new() -> Self {
|
||||
SeenCredentialCache {
|
||||
cached_credentials: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn insert_credential(
|
||||
&mut self,
|
||||
credential: CredentialSpendingData,
|
||||
peer_pub_key: PeerPublicKey,
|
||||
) {
|
||||
let value = TimestampedPeerPubKey {
|
||||
peer_pub_key,
|
||||
timestamp: SystemTime::now(),
|
||||
};
|
||||
self.cached_credentials
|
||||
.insert(credential.serial_number_b58(), value);
|
||||
}
|
||||
|
||||
pub(crate) fn get_peer_pub_key(
|
||||
&self,
|
||||
credential: &CredentialSpendingData,
|
||||
) -> Option<PeerPublicKey> {
|
||||
self.cached_credentials
|
||||
.get(&credential.serial_number_b58())
|
||||
.map(|value| value.peer_pub_key)
|
||||
}
|
||||
|
||||
pub(crate) fn remove_stale(&mut self) {
|
||||
let now = SystemTime::now();
|
||||
self.cached_credentials.retain(|_, value| {
|
||||
let Ok(cache_time) = now.duration_since(value.timestamp) else {
|
||||
return false;
|
||||
};
|
||||
cache_time < SEEN_CREDENTIAL_CACHE_TIME
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
[package]
|
||||
name = "nym-network-requester"
|
||||
license = "GPL-3.0"
|
||||
version = "1.1.55"
|
||||
version = "1.1.56"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version = "1.70"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nym-cli"
|
||||
version = "1.1.54"
|
||||
version = "1.1.55"
|
||||
authors.workspace = true
|
||||
edition = "2021"
|
||||
license.workspace = true
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nymvisor"
|
||||
version = "0.1.19"
|
||||
version = "0.1.20"
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
|
||||
+3
-3
@@ -1943,9 +1943,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/http-proxy-middleware": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz",
|
||||
"integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==",
|
||||
"version": "2.0.9",
|
||||
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz",
|
||||
"integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1162,9 +1162,9 @@ http-parser-js@>=0.5.1:
|
||||
integrity sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==
|
||||
|
||||
http-proxy-middleware@^2.0.3:
|
||||
version "2.0.6"
|
||||
resolved "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz"
|
||||
integrity sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==
|
||||
version "2.0.9"
|
||||
resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz#e9e63d68afaa4eee3d147f39149ab84c0c2815ef"
|
||||
integrity sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==
|
||||
dependencies:
|
||||
"@types/http-proxy" "^1.17.8"
|
||||
http-proxy "^1.18.1"
|
||||
|
||||
@@ -4,6 +4,6 @@ go 1.23.0
|
||||
|
||||
toolchain go1.23.3
|
||||
|
||||
require golang.org/x/net v0.36.0
|
||||
require golang.org/x/net v0.38.0
|
||||
|
||||
require golang.org/x/text v0.22.0 // indirect
|
||||
require golang.org/x/text v0.23.0 // indirect
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA=
|
||||
golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
|
||||
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||
|
||||
Reference in New Issue
Block a user