diff --git a/Cargo.lock b/Cargo.lock index 76725316e2..7d5c5c56f7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4147,6 +4147,7 @@ dependencies = [ "nym-node-requests", "nym-node-tester-utils", "nym-pemstore", + "nym-serde-helpers", "nym-sphinx", "nym-task", "nym-topology", diff --git a/common/cosmwasm-smart-contracts/contracts-common/src/types.rs b/common/cosmwasm-smart-contracts/contracts-common/src/types.rs index df333ee7b9..30a01c17a9 100644 --- a/common/cosmwasm-smart-contracts/contracts-common/src/types.rs +++ b/common/cosmwasm-smart-contracts/contracts-common/src/types.rs @@ -23,7 +23,7 @@ pub fn truncate_decimal(amount: Decimal) -> Uint128 { #[derive(Error, Debug)] pub enum ContractsCommonError { #[error("Provided percent value ({0}) is greater than 100%")] - InvalidPercent(Decimal), + InvalidPercent(String), #[error("{source}")] StdErr { @@ -41,7 +41,7 @@ pub struct Percent(#[serde(deserialize_with = "de_decimal_percent")] Decimal); impl Percent { pub fn new(value: Decimal) -> Result { if value > Decimal::one() { - Err(ContractsCommonError::InvalidPercent(value)) + Err(ContractsCommonError::InvalidPercent(value.to_string())) } else { Ok(Percent(value)) } @@ -129,6 +129,62 @@ impl Deref for Percent { } } +// this is not implemented via From traits due to its naive nature and loss of precision +#[cfg(not(target_arch = "wasm32"))] +pub trait NaiveFloat { + fn naive_to_f64(&self) -> f64; + + fn naive_try_from_f64(val: f64) -> Result + where + Self: Sized; +} + +#[cfg(not(target_arch = "wasm32"))] +impl NaiveFloat for Percent { + fn naive_to_f64(&self) -> f64 { + use cosmwasm_std::Fraction; + + // note: this conversion loses precision with too many decimal places, + // but for the purposes of displaying basic performance, that's not an issue + self.numerator().u128() as f64 / self.denominator().u128() as f64 + } + + fn naive_try_from_f64(val: f64) -> Result + where + Self: Sized, + { + // we are only interested in positive values between 0 and 1 + if !(0. ..=1.).contains(&val) { + return Err(ContractsCommonError::InvalidPercent(val.to_string())); + } + + fn gcd(mut x: u64, mut y: u64) -> u64 { + while y > 0 { + let rem = x % y; + x = y; + y = rem; + } + + x + } + + fn to_rational(x: f64) -> (u64, u64) { + let log = x.log2().floor(); + if log >= 0.0 { + (x as u64, 1) + } else { + let num: u64 = (x / f64::EPSILON) as _; + let den: u64 = (1.0 / f64::EPSILON) as _; + let gcd = gcd(num, den); + (num / gcd, den / gcd) + } + } + + let (n, d) = to_rational(val); + Percent::new(Decimal::from_ratio(n, d)) + } +} + // implement custom Deserialize because we want to validate Percent has the correct range fn de_decimal_percent<'de, D>(deserializer: D) -> Result where @@ -216,6 +272,7 @@ impl ContractBuildInformation { #[cfg(test)] mod tests { use super::*; + use cosmwasm_std::Fraction; #[test] fn percent_serde() { @@ -255,4 +312,19 @@ mod tests { let p = serde_json::from_str::<'_, Percent>("\"1.00\"").unwrap(); assert_eq!(p.round_to_integer(), 100); } + + #[test] + fn naive_float_conversion() { + // around 15 decimal places is the maximum precision we can handle + // which is still way more than enough for what we use it for + let float: f64 = "0.546295475423853".parse().unwrap(); + let percent: Percent = "0.546295475423853".parse().unwrap(); + + assert_eq!(float, percent.naive_to_f64()); + + let epsilon = Decimal::from_ratio(1u64, 1000000000000000u64); + let converted = Percent::naive_try_from_f64(float).unwrap(); + + assert!(converted.0 - converted.0 < epsilon); + } } diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs index 5e0f69596b..17f86abba6 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs @@ -372,7 +372,7 @@ impl ExecuteMsg { ExecuteMsg::MigrateGateway { .. } => "migrating legacy gateway".into(), ExecuteMsg::BondNymNode { .. } => "bonding nym-node".into(), ExecuteMsg::UnbondNymNode { .. } => "unbonding nym-node".into(), - ExecuteMsg::UpdateNodeConfig { update } => "updating node config".into(), + ExecuteMsg::UpdateNodeConfig { .. } => "updating node config".into(), #[cfg(feature = "contract-testing")] ExecuteMsg::TestingResolveAllPendingEvents { .. } => { diff --git a/contracts/Cargo.lock b/contracts/Cargo.lock index 764c405a8b..befc886831 100644 --- a/contracts/Cargo.lock +++ b/contracts/Cargo.lock @@ -1638,9 +1638,9 @@ checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" [[package]] name = "schemars" -version = "0.8.16" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45a28f4c49489add4ce10783f7911893516f15afe45d015608d41faca6bc4d29" +checksum = "09c024468a378b7e36765cd36702b7a90cc3cba11654f6685c8f233408e89e92" dependencies = [ "dyn-clone", "schemars_derive", @@ -1650,14 +1650,14 @@ dependencies = [ [[package]] name = "schemars_derive" -version = "0.8.16" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c767fd6fa65d9ccf9cf026122c1b555f2ef9a4f0cea69da4d7dbc3e258d30967" +checksum = "b1eee588578aff73f856ab961cd2f79e36bc45d7ded33a7562adba4667aecc0e" dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 1.0.109", + "syn 2.0.59", ] [[package]] @@ -1699,9 +1699,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.209" +version = "1.0.210" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99fce0ffe7310761ca6bf9faf5115afbc19688edd00171d81b1bb1b116c63e09" +checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" dependencies = [ "serde_derive", ] @@ -1726,9 +1726,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.209" +version = "1.0.210" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170" +checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" dependencies = [ "proc-macro2", "quote", @@ -1737,13 +1737,13 @@ dependencies = [ [[package]] name = "serde_derive_internals" -version = "0.26.0" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85bf8229e7920a9f636479437026331ce11aa132b4dde37d121944a44d6e5f3c" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.59", ] [[package]] diff --git a/nym-api/Cargo.toml b/nym-api/Cargo.toml index 0c147339c7..5c0a24822a 100644 --- a/nym-api/Cargo.toml +++ b/nym-api/Cargo.toml @@ -117,6 +117,7 @@ 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-serde-helpers = { path = "../common/serde-helpers", features = ["date"] } [features] no-reward = [] diff --git a/nym-api/migrations/20240726120000_v3_changes.sql b/nym-api/migrations/20240726120000_v3_changes.sql index 4feb17a881..652729d215 100644 --- a/nym-api/migrations/20240726120000_v3_changes.sql +++ b/nym-api/migrations/20240726120000_v3_changes.sql @@ -15,3 +15,10 @@ CREATE TABLE v3_migration_info ( id INTEGER PRIMARY KEY CHECK (id = 0) ); +--CREATE TABLE node_historical_performance ( +-- contract_node_id INTEGER NOT NULL, +-- date DATE NOT NULL, +-- performance FLOAT NOT NULL, +-- +-- UNIQUE(contract_node_id, date); +--) \ No newline at end of file diff --git a/nym-api/nym-api-requests/Cargo.toml b/nym-api/nym-api-requests/Cargo.toml index e0e23c9319..41889310b2 100644 --- a/nym-api/nym-api-requests/Cargo.toml +++ b/nym-api/nym-api-requests/Cargo.toml @@ -13,6 +13,7 @@ cosmwasm-std = { workspace = true } getset = { workspace = true } schemars = { workspace = true, features = ["preserve_order"] } serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } ts-rs = { workspace = true, optional = true } tendermint = { workspace = true } time = { workspace = true, features = ["serde", "parsing", "formatting"] } @@ -35,9 +36,6 @@ nym-node-requests = { path = "../../nym-node/nym-node-requests", default-feature nym-network-defaults = { path = "../../common/network-defaults" } -[dev-dependencies] -serde_json.workspace = true - [features] default = [] generate-ts = ["ts-rs", "nym-mixnet-contract-common/generate-ts"] diff --git a/nym-api/nym-api-requests/src/models.rs b/nym-api/nym-api-requests/src/models.rs index e4d5fb47c7..826fd90f02 100644 --- a/nym-api/nym-api-requests/src/models.rs +++ b/nym-api/nym-api-requests/src/models.rs @@ -10,7 +10,7 @@ use crate::pagination::PaginatedResponse; use cosmwasm_std::{Addr, Coin, Decimal, Uint128}; use nym_mixnet_contract_common::reward_params::{Performance, RewardingParams}; use nym_mixnet_contract_common::rewarding::RewardEstimate; -use nym_mixnet_contract_common::{IdentityKey, Interval, MixNode, NodeId, Percent}; +use nym_mixnet_contract_common::{IdentityKey, Interval, MixNode, NodeId, NymNodeDetails, Percent}; use nym_network_defaults::{DEFAULT_MIX_LISTENING_PORT, DEFAULT_VERLOC_LISTENING_PORT}; use nym_node_requests::api::v1::authenticator::models::Authenticator; use nym_node_requests::api::v1::gateway::models::Wireguard; @@ -26,7 +26,7 @@ use std::fmt::{Debug, Display, Formatter}; use std::net::IpAddr; use std::ops::{Deref, DerefMut}; use std::{fmt, time::Duration}; -use time::OffsetDateTime; +use time::{Date, OffsetDateTime}; use utoipa::{IntoParams, ToResponse, ToSchema}; #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] @@ -127,12 +127,27 @@ pub struct NodeAnnotation { pub last_24h_performance: Performance, } -#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)] +#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] pub struct AnnotationResponse { - pub node_id: Option, + pub node_id: NodeId, pub annotation: Option, } +#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] +pub struct NodePerformanceResponse { + pub node_id: NodeId, + pub performance: Option, +} + +#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] +pub struct NodeDatePerformanceResponse { + pub node_id: NodeId, + #[schema(value_type = String, example = "1970-01-01")] + #[schemars(with = "String")] + pub date: Date, + pub performance: Option, +} + #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] #[schema(title = "LegacyMixNodeDetailsWithLayer")] pub struct LegacyMixNodeDetailsWithLayerSchema { @@ -392,8 +407,38 @@ pub struct GatewayStatusReportResponse { pub last_day: Uptime, } +#[derive(Serialize, Deserialize, schemars::JsonSchema, ToSchema)] +pub struct PerformanceHistoryResponse { + pub node_id: NodeId, + pub history: PaginatedResponse, +} + +#[derive(Serialize, Deserialize, schemars::JsonSchema, ToSchema)] +pub struct UptimeHistoryResponse { + pub node_id: NodeId, + pub history: PaginatedResponse, +} + #[derive(Clone, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] pub struct HistoricalUptimeResponse { + #[schema(value_type = String, example = "1970-01-01")] + #[schemars(with = "String")] + pub date: Date, + + pub uptime: Uptime, +} + +#[derive(Clone, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] +pub struct HistoricalPerformanceResponse { + #[schema(value_type = String, example = "1970-01-01")] + #[schemars(with = "String")] + pub date: Date, + + pub performance: f64, +} + +#[derive(Clone, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] +pub struct OldHistoricalUptimeResponse { pub date: String, #[schema(value_type = u8)] pub uptime: Uptime, @@ -404,14 +449,14 @@ pub struct MixnodeUptimeHistoryResponse { pub mix_id: NodeId, pub identity: String, pub owner: String, - pub history: Vec, + pub history: Vec, } #[derive(Clone, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] pub struct GatewayUptimeHistoryResponse { pub identity: String, pub owner: String, - pub history: Vec, + pub history: Vec, } #[derive(ToSchema)] diff --git a/nym-api/src/node_describe_cache/query_helpers.rs b/nym-api/src/node_describe_cache/query_helpers.rs index b4d11e1190..cdb9e04d4e 100644 --- a/nym-api/src/node_describe_cache/query_helpers.rs +++ b/nym-api/src/node_describe_cache/query_helpers.rs @@ -158,6 +158,8 @@ where F7: Future, F8: Future, { + // okay. the fact I have to bypass clippy here means it wasn't a good idea to create this abomination after all + #[allow(clippy::too_many_arguments)] fn new( build_info: F1, roles: F2, diff --git a/nym-api/src/node_status_api/handlers/mod.rs b/nym-api/src/node_status_api/handlers/mod.rs index a83cd3fcd6..f42cd59d84 100644 --- a/nym-api/src/node_status_api/handlers/mod.rs +++ b/nym-api/src/node_status_api/handlers/mod.rs @@ -30,9 +30,3 @@ pub(crate) fn node_status_routes(network_monitor: bool) -> Router { struct MixIdParam { mix_id: NodeId, } - -#[derive(Deserialize, IntoParams)] -#[into_params(parameter_in = Path)] -struct NodeIdParam { - node_id: NodeId, -} diff --git a/nym-api/src/node_status_api/models.rs b/nym-api/src/node_status_api/models.rs index 4fe2cf1b6c..ce4c0eb1b1 100644 --- a/nym-api/src/node_status_api/models.rs +++ b/nym-api/src/node_status_api/models.rs @@ -5,15 +5,20 @@ use crate::ecash::error::{EcashError, RedemptionError}; use crate::node_status_api::utils::NodeUptimes; use crate::storage::models::NodeStatus; use crate::support::caching::cache::UninitialisedCache; -use nym_api_requests::models::{HistoricalUptimeResponse, NodePerformance, RequestError}; +use nym_api_requests::models::{ + HistoricalPerformanceResponse, HistoricalUptimeResponse, NodePerformance, + OldHistoricalUptimeResponse, RequestError, +}; +use nym_contracts_common::NaiveFloat; use nym_mixnet_contract_common::reward_params::Performance; use nym_mixnet_contract_common::{IdentityKey, NodeId}; +use nym_serde_helpers::date::DATE_FORMAT; use reqwest::StatusCode; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::fmt::Display; use thiserror::Error; -use time::OffsetDateTime; +use time::{Date, OffsetDateTime}; use tracing::error; #[derive(Error, Debug)] @@ -264,9 +269,42 @@ pub struct HistoricalUptime { pub(crate) uptime: Uptime, } -impl From for HistoricalUptimeResponse { +#[derive(Error, Debug)] +pub enum InvalidHistoricalPerformance { + #[error("the provided date could not be parsed")] + UnparsableDate, + + #[error("the provided uptime could not be parsed")] + MalformedPerformance, +} + +impl TryFrom for HistoricalPerformanceResponse { + type Error = InvalidHistoricalPerformance; + fn try_from(value: HistoricalUptime) -> Result { + Ok(HistoricalPerformanceResponse { + date: Date::parse(&value.date, DATE_FORMAT) + .map_err(|_| InvalidHistoricalPerformance::UnparsableDate)?, + performance: Performance::from_percentage_value(value.uptime.u8() as u64) + .map_err(|_| InvalidHistoricalPerformance::MalformedPerformance)? + .naive_to_f64(), + }) + } +} + +impl TryFrom for HistoricalUptimeResponse { + type Error = InvalidHistoricalPerformance; + fn try_from(value: HistoricalUptime) -> Result { + Ok(HistoricalUptimeResponse { + date: Date::parse(&value.date, DATE_FORMAT) + .map_err(|_| InvalidHistoricalPerformance::UnparsableDate)?, + uptime: value.uptime.u8(), + }) + } +} + +impl From for OldHistoricalUptimeResponse { fn from(uptime: HistoricalUptime) -> Self { - HistoricalUptimeResponse { + OldHistoricalUptimeResponse { date: uptime.date, uptime: uptime.uptime.0, } @@ -418,3 +456,11 @@ impl NymApiStorageError { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn uptime_response_conversion() {} +} diff --git a/nym-api/src/nym_nodes/handlers.rs b/nym-api/src/nym_nodes/handlers/legacy.rs similarity index 76% rename from nym-api/src/nym_nodes/handlers.rs rename to nym-api/src/nym_nodes/handlers/legacy.rs index 5692cb95fd..1ebd9260c7 100644 --- a/nym-api/src/nym_nodes/handlers.rs +++ b/nym-api/src/nym_nodes/handlers/legacy.rs @@ -1,15 +1,14 @@ -// Copyright 2023 - Nym Technologies SA +// Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only use crate::support::http::state::AppState; -use axum::{extract::State, Json, Router}; +use axum::extract::State; +use axum::{Json, Router}; use nym_api_requests::models::{LegacyDescribedGateway, LegacyDescribedMixNode}; -use std::ops::Deref; -// obviously this should get refactored later on because gateways will go away. -// unless maybe this will be filtering based on which nodes got assigned gateway role? TBD - -pub(crate) fn nym_node_routes() -> axum::Router { +// we want to mark the routes as deprecated in swagger, but still expose them +#[allow(deprecated)] +pub(crate) fn legacy_nym_node_routes() -> Router { Router::new() .route( "/gateways/described", @@ -22,13 +21,14 @@ pub(crate) fn nym_node_routes() -> axum::Router { } #[utoipa::path( - tag = "Nym Nodes", + tag = "Legacy gateways", get, path = "/v1/gateways/described", responses( (status = 200, body = Vec) ) )] +#[deprecated] async fn get_gateways_described( State(state): State, ) -> Json> { @@ -48,10 +48,7 @@ async fn get_gateways_described( gateways .into_iter() .map(|bond| LegacyDescribedGateway { - self_described: self_descriptions - .deref() - .get_description(&bond.node_id) - .cloned(), + self_described: self_descriptions.get_description(&bond.node_id).cloned(), bond, }) .collect(), @@ -59,13 +56,14 @@ async fn get_gateways_described( } #[utoipa::path( - tag = "Nym Nodes", + tag = "Legacy Mixnodes", get, path = "/v1/mixnodes/described", responses( (status = 200, body = Vec) ) )] +#[deprecated] async fn get_mixnodes_described( State(state): State, ) -> Json> { @@ -91,10 +89,7 @@ async fn get_mixnodes_described( mixnodes .into_iter() .map(|bond| LegacyDescribedMixNode { - self_described: self_descriptions - .deref() - .get_description(&bond.mix_id) - .cloned(), + self_described: self_descriptions.get_description(&bond.mix_id).cloned(), bond, }) .collect(), diff --git a/nym-api/src/nym_nodes/handlers/mod.rs b/nym-api/src/nym_nodes/handlers/mod.rs new file mode 100644 index 0000000000..4b92036e18 --- /dev/null +++ b/nym-api/src/nym_nodes/handlers/mod.rs @@ -0,0 +1,210 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::node_status_api::models::{AxumErrorResponse, AxumResult}; +use crate::support::http::helpers::{NodeIdParam, PaginationRequest}; +use crate::support::http::state::AppState; +use axum::extract::{Path, Query, State}; +use axum::routing::get; +use axum::{Json, Router}; +use nym_api_requests::models::{ + AnnotationResponse, NodeDatePerformanceResponse, NodePerformanceResponse, NymNodeData, + PerformanceHistoryResponse, +}; +use nym_api_requests::pagination::{PaginatedResponse, Pagination}; +use nym_contracts_common::NaiveFloat; +use nym_mixnet_contract_common::reward_params::Performance; +use nym_mixnet_contract_common::NymNodeDetails; +use serde::{Deserialize, Serialize}; +use time::Date; +use utoipa::{IntoParams, ToSchema}; + +pub(crate) mod legacy; +pub(crate) mod unstable; + +pub(crate) fn nym_node_routes() -> Router { + Router::new() + .route("/bonded", get(get_bonded_nodes)) + .route("/described", get(get_described_nodes)) + .route("/annotation:node_id", get(get_node_annotation)) + .route("/performance:node_id", get(get_current_node_performance)) + .route( + "/historical-performance:node_id", + get(get_historical_performance), + ) + .route( + "/performance-history:node_id", + get(get_node_performance_history), + ) + // to make it compatible with all the explorers that were used to using 0-100 values + .route("/uptime-history:node_id", get(get_node_uptime_history)) +} + +async fn get_bonded_nodes( + State(state): State, + Query(pagination): Query, +) -> Json> { + // TODO: implement it + let _ = pagination; + + let details = state.nym_contract_cache().nym_nodes().await; + let total = details.len(); + + Json(PaginatedResponse { + pagination: Pagination { + total, + page: 0, + size: total, + }, + data: details, + }) +} + +async fn get_described_nodes( + State(state): State, + Query(pagination): Query, +) -> AxumResult>> { + // TODO: implement it + let _ = pagination; + + let cache = state.described_nodes_cache.get().await?; + let descriptions = cache.all_nodes(); + + let data = descriptions + .map(|n| &n.description) + .cloned() + .collect::>(); + + Ok(Json(PaginatedResponse { + pagination: Pagination { + total: data.len(), + page: 0, + size: data.len(), + }, + data, + })) +} + +async fn get_node_annotation( + Path(NodeIdParam { node_id }): Path, + State(state): State, +) -> AxumResult> { + let annotations = state + .node_status_cache + .node_annotations() + .await + .ok_or_else(AxumErrorResponse::internal)?; + + Ok(Json(AnnotationResponse { + node_id, + annotation: annotations.get(&node_id).cloned(), + })) +} + +async fn get_current_node_performance( + Path(NodeIdParam { node_id }): Path, + State(state): State, +) -> AxumResult> { + let annotations = state + .node_status_cache + .node_annotations() + .await + .ok_or_else(AxumErrorResponse::internal)?; + + Ok(Json(NodePerformanceResponse { + node_id, + performance: annotations + .get(&node_id) + .map(|n| n.last_24h_performance.naive_to_f64()), + })) +} + +// todo; probably extract it to requests crate +#[derive(Debug, Serialize, Deserialize, Copy, Clone, IntoParams, ToSchema)] +pub(crate) struct DateQuery { + #[schema(example = "1970-01-01")] + pub(crate) date: Date, +} + +async fn get_historical_performance( + Path(NodeIdParam { node_id }): Path, + Query(DateQuery { date }): Query, + State(state): State, +) -> AxumResult> { + let uptime = state + .storage() + .get_historical_node_uptime_on(node_id, date) + .await?; + + Ok(Json(NodeDatePerformanceResponse { + node_id, + date, + performance: uptime + .map(|u| { + Performance::from_percentage_value(u.uptime as u64) + .map(|p| p.naive_to_f64()) + .ok() + }) + .flatten(), + })) +} + +async fn get_node_performance_history( + Path(NodeIdParam { node_id }): Path, + State(state): State, + Query(pagination): Query, +) -> AxumResult> { + // TODO: implement it + let _ = pagination; + + let history = state + .storage() + .get_node_uptime_history(node_id) + .await? + .into_iter() + .filter_map(|u| u.try_into().ok()) + .collect::>(); + let total = history.len(); + + Ok(Json(PerformanceHistoryResponse { + node_id, + history: PaginatedResponse { + pagination: Pagination { + total, + page: 0, + size: total, + }, + data: history, + }, + })) +} + +async fn get_node_uptime_history( + Path(NodeIdParam { node_id }): Path, + State(state): State, + Query(pagination): Query, +) -> AxumResult> { + // TODO: implement it + let _ = pagination; + + let history = state + .storage() + .get_node_uptime_history(node_id) + .await? + .into_iter() + .filter_map(|u| u.try_into().ok()) + .collect::>(); + let total = history.len(); + + Ok(Json(PerformanceHistoryResponse { + node_id, + history: PaginatedResponse { + pagination: Pagination { + total, + page: 0, + size: total, + }, + data: history, + }, + })) +} diff --git a/nym-api/src/nym_nodes/handlers_unstable.rs b/nym-api/src/nym_nodes/handlers/unstable.rs similarity index 100% rename from nym-api/src/nym_nodes/handlers_unstable.rs rename to nym-api/src/nym_nodes/handlers/unstable.rs diff --git a/nym-api/src/nym_nodes/mod.rs b/nym-api/src/nym_nodes/mod.rs index cca0d579db..33a7ff2fd8 100644 --- a/nym-api/src/nym_nodes/mod.rs +++ b/nym-api/src/nym_nodes/mod.rs @@ -2,7 +2,6 @@ // SPDX-License-Identifier: GPL-3.0-only pub(crate) mod handlers; -pub(crate) mod handlers_unstable; // pub(crate) mod routes; // mod unstable_routes; diff --git a/nym-api/src/support/http/helpers.rs b/nym-api/src/support/http/helpers.rs index 57a4837bc2..c7cd13cedd 100644 --- a/nym-api/src/support/http/helpers.rs +++ b/nym-api/src/support/http/helpers.rs @@ -1,11 +1,19 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use nym_mixnet_contract_common::NodeId; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use utoipa::IntoParams; #[derive(Serialize, Deserialize, Debug, JsonSchema)] pub struct PaginationRequest { pub page: Option, pub per_page: Option, } + +#[derive(Deserialize, IntoParams)] +#[into_params(parameter_in = Path)] +pub(crate) struct NodeIdParam { + pub(crate) node_id: NodeId, +} diff --git a/nym-api/src/support/http/mod.rs b/nym-api/src/support/http/mod.rs index a54194fdd8..9446891443 100644 --- a/nym-api/src/support/http/mod.rs +++ b/nym-api/src/support/http/mod.rs @@ -5,6 +5,7 @@ pub(crate) mod helpers; pub(crate) mod openapi; pub(crate) mod router; pub(crate) mod state; +mod unstable_routes; pub(crate) use router::RouterBuilder; diff --git a/nym-api/src/support/http/openapi.rs b/nym-api/src/support/http/openapi.rs index 89f67b26c2..f96fce29ff 100644 --- a/nym-api/src/support/http/openapi.rs +++ b/nym-api/src/support/http/openapi.rs @@ -22,7 +22,6 @@ use utoipauto::utoipauto; nym_mixnet_contract_common::Interval, nym_api_requests::models::GatewayStatusReportResponse, nym_api_requests::models::GatewayUptimeHistoryResponse, - nym_api_requests::models::HistoricalUptimeResponse, nym_api_requests::models::GatewayCoreStatusResponse, nym_api_requests::models::GatewayUptimeResponse, nym_api_requests::models::RewardEstimationResponse, @@ -76,9 +75,11 @@ use utoipauto::utoipauto; nym_api_requests::ecash::models::SpentCredentialsResponse, nym_api_requests::ecash::models::IssuedCredentialsResponse, nym_api_requests::nym_nodes::SkimmedNode, - nym_api_requests::nym_nodes::BasicEntryInformation, nym_api_requests::nym_nodes::SemiSkimmedNode, + nym_api_requests::nym_nodes::FullFatNode, + nym_api_requests::nym_nodes::BasicEntryInformation, nym_api_requests::nym_nodes::NodeRoleQueryParam, + nym_api_requests::nym_nodes::CachedNodesResponse, )) )] pub(crate) struct ApiDoc; diff --git a/nym-api/src/support/http/router.rs b/nym-api/src/support/http/router.rs index 1bddcdc494..38eec8d76e 100644 --- a/nym-api/src/support/http/router.rs +++ b/nym-api/src/support/http/router.rs @@ -5,11 +5,12 @@ use crate::circulating_supply_api::handlers::circulating_supply_routes; use crate::network::handlers::nym_network_routes; use crate::node_status_api::handlers::node_status_routes; use crate::nym_contract_cache::handlers::nym_contract_cache_routes; +use crate::nym_nodes::handlers::legacy::legacy_nym_node_routes; use crate::nym_nodes::handlers::nym_node_routes; -use crate::nym_nodes::handlers_unstable::nym_node_routes_unstable; use crate::status; use crate::support::http::openapi::ApiDoc; use crate::support::http::state::AppState; +use crate::support::http::unstable_routes::unstable_routes; use anyhow::anyhow; use axum::response::Redirect; use axum::routing::get; @@ -53,13 +54,15 @@ impl RouterBuilder { .nest( "/v1", Router::new() - .nest("/circulating-supply", circulating_supply_routes()) + // unfortunately some routes didn't use correct prefix and were attached to the root .merge(nym_contract_cache_routes()) + .merge(legacy_nym_node_routes()) + .nest("/circulating-supply", circulating_supply_routes()) .nest("/status", node_status_routes(network_monitor)) .nest("/network", nym_network_routes()) .nest("/api-status", status::handlers::api_status_routes()) - .merge(nym_node_routes()) - .nest("/unstable/nym-nodes", nym_node_routes_unstable()), // CORS layer needs to be "outside" of routes + .nest("/nym-nodes", nym_node_routes()) + .nest("/unstable", unstable_routes()), // CORS layer needs to be "outside" of routes ); Self { diff --git a/nym-api/src/support/http/unstable_routes.rs b/nym-api/src/support/http/unstable_routes.rs new file mode 100644 index 0000000000..7bc1a9644c --- /dev/null +++ b/nym-api/src/support/http/unstable_routes.rs @@ -0,0 +1,11 @@ +// Copyright 2024 - Nym Technologies SA +// 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 { + Router::new().nest("/nym-nodes", nym_node_routes_unstable()) +} diff --git a/nym-api/src/support/storage/manager.rs b/nym-api/src/support/storage/manager.rs index c7d1023f36..c5539838f9 100644 --- a/nym-api/src/support/storage/manager.rs +++ b/nym-api/src/support/storage/manager.rs @@ -1,15 +1,16 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::node_status_api::models::{HistoricalUptime, Uptime}; +use crate::node_status_api::models::{HistoricalUptime as ApiHistoricalUptime, Uptime}; use crate::node_status_api::utils::{ActiveGatewayStatuses, ActiveMixnodeStatuses}; use crate::support::storage::models::{ - ActiveGateway, ActiveMixnode, GatewayDetails, MixnodeDetails, NodeStatus, RewardingReport, - TestedGatewayStatus, TestedMixnodeStatus, TestingRoute, + ActiveGateway, ActiveMixnode, GatewayDetails, HistoricalUptime, MixnodeDetails, NodeStatus, + RewardingReport, TestedGatewayStatus, TestedMixnodeStatus, TestingRoute, }; use nym_mixnet_contract_common::{EpochId, IdentityKey, NodeId}; use nym_types::monitoring::NodeResult; use sqlx::FromRow; +use time::Date; use tracing::info; #[derive(Clone)] @@ -301,15 +302,15 @@ impl StorageManager { pub(crate) async fn get_mixnode_historical_uptimes( &self, mix_id: NodeId, - ) -> Result, sqlx::Error> { + ) -> Result, sqlx::Error> { let uptimes = sqlx::query!( r#" SELECT date, uptime - FROM mixnode_historical_uptime - JOIN mixnode_details - ON mixnode_historical_uptime.mixnode_details_id = mixnode_details.id - WHERE mixnode_details.mix_id = ? - ORDER BY date ASC + FROM mixnode_historical_uptime + JOIN mixnode_details + ON mixnode_historical_uptime.mixnode_details_id = mixnode_details.id + WHERE mixnode_details.mix_id = ? + ORDER BY date ASC "#, mix_id ) @@ -320,7 +321,7 @@ impl StorageManager { // better safe than sorry and not use an unwrap) .filter_map(|row| { Uptime::try_from(row.uptime.unwrap_or_default()) - .map(|uptime| HistoricalUptime { + .map(|uptime| ApiHistoricalUptime { date: row.date.unwrap_or_default(), uptime, }) @@ -339,15 +340,15 @@ impl StorageManager { pub(crate) async fn get_gateway_historical_uptimes( &self, node_id: NodeId, - ) -> Result, sqlx::Error> { + ) -> Result, sqlx::Error> { let uptimes = sqlx::query!( r#" SELECT date, uptime - FROM gateway_historical_uptime - JOIN gateway_details - ON gateway_historical_uptime.gateway_details_id = gateway_details.id - WHERE gateway_details.node_id = ? - ORDER BY date ASC + FROM gateway_historical_uptime + JOIN gateway_details + ON gateway_historical_uptime.gateway_details_id = gateway_details.id + WHERE gateway_details.node_id = ? + ORDER BY date ASC "#, node_id ) @@ -358,7 +359,7 @@ impl StorageManager { // better safe than sorry and not use an unwrap) .filter_map(|row| { Uptime::try_from(row.uptime.unwrap_or_default()) - .map(|uptime| HistoricalUptime { + .map(|uptime| ApiHistoricalUptime { date: row.date.unwrap_or_default(), uptime, }) @@ -369,6 +370,54 @@ impl StorageManager { Ok(uptimes) } + pub(crate) async fn get_historical_mix_uptime_on( + &self, + contract_node_id: i64, + date: Date, + ) -> Result, sqlx::Error> { + sqlx::query_as!( + HistoricalUptime, + r#" + SELECT date as "date!: Date", uptime as "uptime!" + FROM mixnode_historical_uptime + JOIN mixnode_details + ON mixnode_historical_uptime.mixnode_details_id = mixnode_details.id + WHERE + mixnode_details.mix_id = ? + AND + mixnode_historical_uptime.date = ? + "#, + contract_node_id, + date + ) + .fetch_optional(&self.connection_pool) + .await + } + + pub(crate) async fn get_historical_gateway_uptime_on( + &self, + contract_node_id: i64, + date: Date, + ) -> Result, sqlx::Error> { + sqlx::query_as!( + HistoricalUptime, + r#" + SELECT date as "date!: Date", uptime as "uptime!" + FROM gateway_historical_uptime + JOIN gateway_details + ON gateway_historical_uptime.gateway_details_id = gateway_details.id + WHERE + gateway_details.node_id = ? + AND + gateway_historical_uptime.date = ? + "#, + contract_node_id, + date + ) + .fetch_optional(&self.connection_pool) + .await + } + /// Gets all reliability statuses for mixnode with particular id that were inserted /// into the database within the specified time interval. /// diff --git a/nym-api/src/support/storage/mod.rs b/nym-api/src/support/storage/mod.rs index 5abf6ae166..e647adf7d0 100644 --- a/nym-api/src/support/storage/mod.rs +++ b/nym-api/src/support/storage/mod.rs @@ -3,20 +3,20 @@ use crate::network_monitor::test_route::TestRoute; use crate::node_status_api::models::{ - GatewayStatusReport, GatewayUptimeHistory, MixnodeStatusReport, MixnodeUptimeHistory, - NymApiStorageError, Uptime, + GatewayStatusReport, GatewayUptimeHistory, HistoricalUptime as ApiHistoricalUptime, + MixnodeStatusReport, MixnodeUptimeHistory, NymApiStorageError, Uptime, }; use crate::node_status_api::{ONE_DAY, ONE_HOUR}; use crate::storage::manager::StorageManager; use crate::storage::models::{NodeStatus, TestingRoute}; use crate::support::storage::models::{ - GatewayDetails, MixnodeDetails, TestedGatewayStatus, TestedMixnodeStatus, + GatewayDetails, HistoricalUptime, MixnodeDetails, TestedGatewayStatus, TestedMixnodeStatus, }; use nym_mixnet_contract_common::NodeId; use nym_types::monitoring::NodeResult; use sqlx::ConnectOptions; use std::path::Path; -use time::OffsetDateTime; +use time::{Date, OffsetDateTime}; use tracing::{error, info, warn}; use self::manager::{AvgGatewayReliability, AvgMixnodeReliability}; @@ -278,14 +278,29 @@ impl NymApiStorage { )) } + pub(crate) async fn get_node_uptime_history( + &self, + node_id: NodeId, + ) -> Result, NymApiStorageError> { + let history = self.manager.get_mixnode_historical_uptimes(node_id).await?; + + if !history.is_empty() { + return Ok(history); + } + + Ok(self.manager.get_gateway_historical_uptimes(node_id).await?) + } + pub(crate) async fn get_average_mixnode_uptime_in_the_last_24hrs( &self, node_id: NodeId, end_ts_secs: i64, ) -> Result { let start = end_ts_secs - 86400; - self.get_average_mixnode_uptime_in_time_interval(node_id, start, end_ts_secs) - .await + let reliability = self + .get_average_mixnode_reliability_in_time_interval(node_id, start, end_ts_secs) + .await?; + Ok(Uptime::new(reliability)) } pub(crate) async fn get_average_gateway_uptime_in_the_last_24hrs( @@ -294,8 +309,10 @@ impl NymApiStorage { end_ts_secs: i64, ) -> Result { let start = end_ts_secs - 86400; - self.get_average_gateway_uptime_in_time_interval(node_id, start, end_ts_secs) - .await + let reliability = self + .get_average_gateway_reliability_in_time_interval(node_id, start, end_ts_secs) + .await?; + Ok(Uptime::new(reliability)) } pub(crate) async fn get_average_node_uptime_in_the_last_24hrs( @@ -304,8 +321,45 @@ impl NymApiStorage { end_ts_secs: i64, ) -> Result { let start = end_ts_secs - 86400; - self.get_average_node_uptime_in_time_interval(node_id, start, end_ts_secs) + self.get_average_node_reliability_in_time_interval(node_id, start, end_ts_secs) .await + .map(Uptime::new) + } + + pub(crate) async fn get_historical_mix_uptime_on( + &self, + node_id: NodeId, + date: Date, + ) -> Result, NymApiStorageError> { + Ok(self + .manager + .get_historical_mix_uptime_on(node_id as i64, date) + .await?) + } + + pub(crate) async fn get_historical_gateway_uptime_on( + &self, + node_id: NodeId, + date: Date, + ) -> Result, NymApiStorageError> { + Ok(self + .manager + .get_historical_gateway_uptime_on(node_id as i64, date) + .await?) + } + + pub(crate) async fn get_historical_node_uptime_on( + &self, + node_id: NodeId, + date: Date, + ) -> Result, NymApiStorageError> { + if let Ok(result_as_mix) = self.get_historical_mix_uptime_on(node_id, date).await { + if result_as_mix.is_some() { + return Ok(result_as_mix); + } + } + + self.get_historical_gateway_uptime_on(node_id, date).await } /// Based on the data available in the validator API, determines the average uptime of particular @@ -316,16 +370,16 @@ impl NymApiStorage { /// * `mix_id`: mix-id (as assigned by the smart contract) of the mixnode. /// * `since`: unix timestamp indicating the lower bound interval of the selection. /// * `end`: unix timestamp indicating the upper bound interval of the selection. - pub(crate) async fn get_average_mixnode_uptime_in_time_interval( + pub(crate) async fn get_average_mixnode_reliability_in_time_interval( &self, mix_id: NodeId, start: i64, end: i64, - ) -> Result { + ) -> Result { // those two should have been a single sql query /shrug let mixnode_database_id = match self.manager.get_mixnode_database_id(mix_id).await? { Some(id) => id, - None => return Ok(Uptime::zero()), + None => return Ok(0.), }; let reliability = self @@ -333,11 +387,7 @@ impl NymApiStorage { .get_mixnode_average_reliability_in_interval(mixnode_database_id, start, end) .await?; - if let Some(reliability) = reliability { - Ok(Uptime::new(reliability)) - } else { - Ok(Uptime::zero()) - } + Ok(reliability.unwrap_or_default()) } /// Based on the data available in the validator API, determines the average uptime of particular @@ -348,16 +398,16 @@ impl NymApiStorage { /// * `identity`: base58-encoded identity of the gateway. /// * `since`: unix timestamp indicating the lower bound interval of the selection. /// * `end`: unix timestamp indicating the upper bound interval of the selection. - pub(crate) async fn get_average_gateway_uptime_in_time_interval( + pub(crate) async fn get_average_gateway_reliability_in_time_interval( &self, node_id: NodeId, start: i64, end: i64, - ) -> Result { + ) -> Result { // those two should have been a single sql query /shrug let gateway_database_id = match self.manager.get_gateway_database_id(node_id).await? { Some(id) => id, - None => return Ok(Uptime::zero()), + None => return Ok(0.), }; let reliability = self @@ -365,29 +415,25 @@ impl NymApiStorage { .get_gateway_average_reliability_in_interval(gateway_database_id, start, end) .await?; - if let Some(reliability) = reliability { - Ok(Uptime::new(reliability)) - } else { - Ok(Uptime::zero()) - } + Ok(reliability.unwrap_or_default()) } - pub(crate) async fn get_average_node_uptime_in_time_interval( + pub(crate) async fn get_average_node_reliability_in_time_interval( &self, node_id: NodeId, start: i64, end: i64, - ) -> Result { + ) -> Result { if let Ok(result_as_mix) = self - .get_average_mixnode_uptime_in_time_interval(node_id, start, end) + .get_average_mixnode_reliability_in_time_interval(node_id, start, end) .await { - if !result_as_mix.is_zero() { + if result_as_mix != 0. { return Ok(result_as_mix); } } - self.get_average_gateway_uptime_in_time_interval(node_id, start, end) + self.get_average_gateway_reliability_in_time_interval(node_id, start, end) .await } diff --git a/nym-api/src/support/storage/models.rs b/nym-api/src/support/storage/models.rs index b727567f22..6a7b120e66 100644 --- a/nym-api/src/support/storage/models.rs +++ b/nym-api/src/support/storage/models.rs @@ -4,6 +4,7 @@ use nym_api_requests::models::TestNode; use nym_mixnet_contract_common::NodeId; use sqlx::FromRow; +use time::Date; // Internally used struct to catch results from the database to calculate uptimes for given mixnode/gateway pub(crate) struct NodeStatus { @@ -121,3 +122,10 @@ pub struct TestedGatewayStatus { pub layer3_mix_id: i64, pub monitor_run_id: i64, } + +#[derive(FromRow)] +pub struct HistoricalUptime { + #[allow(dead_code)] + pub date: Date, + pub uptime: i64, +} diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index e905f79d5f..141dc304ce 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -3090,10 +3090,12 @@ dependencies = [ "nym-crypto", "nym-ecash-time", "nym-mixnet-contract-common", + "nym-network-defaults", "nym-node-requests", "nym-serde-helpers", "schemars", "serde", + "serde_json", "sha2 0.10.8", "tendermint 0.37.0", "thiserror", @@ -3282,6 +3284,7 @@ dependencies = [ "bs58", "cosmwasm-schema", "cosmwasm-std", + "cw-storage-plus", "humantime-serde", "log", "nym-contracts-common", @@ -4664,9 +4667,9 @@ dependencies = [ [[package]] name = "schemars" -version = "0.8.16" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45a28f4c49489add4ce10783f7911893516f15afe45d015608d41faca6bc4d29" +checksum = "09c024468a378b7e36765cd36702b7a90cc3cba11654f6685c8f233408e89e92" dependencies = [ "dyn-clone", "indexmap 1.9.3", @@ -4677,14 +4680,14 @@ dependencies = [ [[package]] name = "schemars_derive" -version = "0.8.16" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c767fd6fa65d9ccf9cf026122c1b555f2ef9a4f0cea69da4d7dbc3e258d30967" +checksum = "b1eee588578aff73f856ab961cd2f79e36bc45d7ded33a7562adba4667aecc0e" dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 1.0.109", + "syn 2.0.55", ] [[package]] @@ -4834,13 +4837,13 @@ dependencies = [ [[package]] name = "serde_derive_internals" -version = "0.26.0" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85bf8229e7920a9f636479437026331ce11aa132b4dde37d121944a44d6e5f3c" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.55", ] [[package]]