diff --git a/nym-node-status-api/Cargo.toml b/nym-node-status-api/Cargo.toml index 2b26a6dfe6..3b8d315ccb 100644 --- a/nym-node-status-api/Cargo.toml +++ b/nym-node-status-api/Cargo.toml @@ -18,6 +18,7 @@ axum = { workspace = true, features = ["tokio"] } chrono = { workspace = true } clap = { workspace = true, features = ["cargo", "derive"] } cosmwasm-std = { workspace = true } +envy = { workspace = true } futures-util = { workspace = true } moka = { workspace = true, features = ["future"] } nym-bin-common = { path = "../common/bin-common" } @@ -26,21 +27,17 @@ nym-network-defaults = { path = "../common/network-defaults" } nym-validator-client = { path = "../common/client-libs/validator-client" } nym-task = { path = "../common/task" } nym-node-requests = { path = "../nym-node/nym-node-requests", features = ["openapi"] } +reqwest = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } serde_json_path = { workspace = true } sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite"] } thiserror = { workspace = true } -# TODO dz recosider features before merge -tokio = { workspace = true, features = [ - "rt-multi-thread", - "macros", - "signal", - "time", -] } +tokio = { workspace = true, features = ["rt-multi-thread"] } tokio-util = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true, features = ["env-filter"] } +tracing-log = { workspace = true } tower-http = { workspace = true, features = ["cors", "trace"] } utoipa = { workspace = true, features = ["axum_extras", "time"] } utoipa-swagger-ui = { workspace = true, features = ["axum"] } diff --git a/nym-node-status-api/launch_node_status_api.sh b/nym-node-status-api/launch_node_status_api.sh index 4aa1bebd8b..e5059c5011 100755 --- a/nym-node-status-api/launch_node_status_api.sh +++ b/nym-node-status-api/launch_node_status_api.sh @@ -2,63 +2,9 @@ set -e -function usage() { - echo "Usage: $0 [-ci]" - echo " -c Clear DB and re-initialize it before launching the binary." - echo " -i Only initialize and prepare database, env vars then exit without" - echo " launching" - exit 0 -} - -function init_db() { - rm -rf data/* - # https://github.com/launchbadge/sqlx/blob/main/sqlx-cli/README.md - cargo sqlx database drop -y - - cargo sqlx database create - cargo sqlx migrate run - cargo sqlx prepare - - echo "Fresh database ready!" -} - -# export DATABASE_URL as absolute path due to this -# https://github.com/launchbadge/sqlx/issues/3099 -db_filename="nym-node-status-api.sqlite" -script_abs_path=$(realpath "$0") -package_dir=$(dirname "$script_abs_path") -db_abs_path="$package_dir/data/$db_filename" -dotenv_file="$package_dir/.env" -# echo "DATABASE_URL=sqlite://$db_abs_path" > "$dotenv_file" - export RUST_LOG=${RUST_LOG:-debug} -# export DATABASE_URL from .env file -set -a && source "$dotenv_file" && set +a - -clear_db=false -init_only=false - -while getopts "ci" opt; do - case ${opt} in - c) - clear_db=true - ;; - i) - init_only=true - ;; - \?) - usage - ;; - esac -done - -if [ "$clear_db" = true ] || [ "$init_only" = true ]; then - init_db -fi - -if [ "$init_only" = true ]; then - exit 0 -fi +export NYM_API_CLIENT_TIMEOUT=60; +export EXPLORER_CLIENT_TIMEOUT=60; -cargo run --package nym-node-status-api +cargo run --package nym-node-status-api --release -- --config-env-file ../envs/mainnet.env diff --git a/nym-node-status-api/src/config.rs b/nym-node-status-api/src/config.rs index 3ee787e6d7..44b38cd74b 100644 --- a/nym-node-status-api/src/config.rs +++ b/nym-node-status-api/src/config.rs @@ -1,26 +1,42 @@ use anyhow::anyhow; +use reqwest::Url; +use serde::Deserialize; +use std::time::Duration; -#[derive(Debug)] +#[derive(Debug, Clone, Deserialize)] pub(crate) struct Config { + #[serde(default = "Config::default_http_cache_seconds")] nym_http_cache_ttl: u64, + #[serde(default = "Config::default_http_port")] http_port: u16, + #[serde(rename = "nyxd")] + nyxd_addr: Url, + #[serde(default = "Config::default_client_timeout")] + #[serde(deserialize_with = "parse_duration")] + nym_api_client_timeout: Duration, + #[serde(default = "Config::default_client_timeout")] + #[serde(deserialize_with = "parse_duration")] + explorer_client_timeout: Duration, } -const NYM_HTTP_CACHE_SECONDS_DEFAULT: u64 = 30; -const HTTP_PORT_DEFAULT: u16 = 8000; - impl Config { - pub(crate) fn from_env() -> Self { - Self { - nym_http_cache_ttl: read_env_var("NYM_HTTP_CACHE_SECONDS") - .unwrap_or(NYM_HTTP_CACHE_SECONDS_DEFAULT.to_string()) - .parse() - .unwrap_or(NYM_HTTP_CACHE_SECONDS_DEFAULT), - http_port: read_env_var("HTTP_PORT") - .unwrap_or(HTTP_PORT_DEFAULT.to_string()) - .parse() - .unwrap_or(HTTP_PORT_DEFAULT), - } + pub(crate) fn from_env() -> anyhow::Result { + envy::from_env::().map_err(|e| { + tracing::error!("Failed to load config from env: {e}"); + anyhow::Error::from(e) + }) + } + + fn default_client_timeout() -> Duration { + Duration::from_secs(15) + } + + fn default_http_port() -> u16 { + 8000 + } + + fn default_http_cache_seconds() -> u64 { + 30 } pub(crate) fn nym_http_cache_ttl(&self) -> u64 { @@ -30,6 +46,28 @@ impl Config { pub(crate) fn http_port(&self) -> u16 { self.http_port } + + pub(crate) fn nyxd_addr(&self) -> &Url { + &self.nyxd_addr + } + + pub(crate) fn nym_api_client_timeout(&self) -> Duration { + self.nym_api_client_timeout.to_owned() + } + + pub(crate) fn nym_explorer_client_timeout(&self) -> Duration { + self.explorer_client_timeout.to_owned() + } +} + + +fn parse_duration<'de, D>(deserializer: D) -> Result +where + D: serde::Deserializer<'de>, +{ + let s: String = Deserialize::deserialize(deserializer)?; + let secs: u64 = s.parse().map_err(serde::de::Error::custom)?; + Ok(Duration::from_secs(secs)) } pub(super) fn read_env_var(env_var: &str) -> anyhow::Result { diff --git a/nym-node-status-api/src/db/models.rs b/nym-node-status-api/src/db/models.rs index 0d4b5fdef9..54164b34e3 100644 --- a/nym-node-status-api/src/db/models.rs +++ b/nym-node-status-api/src/db/models.rs @@ -1,4 +1,7 @@ -use crate::http::{self, models::SummaryHistory}; +use crate::{ + http::{self, models::SummaryHistory}, + monitor::NumericalCheckedCast, +}; use nym_node_requests::api::v1::node::models::NodeDescription; use serde::{Deserialize, Serialize}; use utoipa::ToSchema; @@ -31,16 +34,20 @@ pub(crate) struct GatewayDto { pub(crate) website: String, } -impl From for http::models::Gateway { - fn from(value: GatewayDto) -> Self { +impl TryFrom for http::models::Gateway { + type Error = anyhow::Error; + + fn try_from(value: GatewayDto) -> Result { // Instead of using routing_score_successes / routing_score_samples, we use the // number of successful testruns in the last 24h. let routing_score = 0f32; let config_score = 0u32; - let last_updated_utc = timestamp_as_utc(value.last_updated_utc as u64).to_rfc3339(); + let last_updated_utc = + timestamp_as_utc(value.last_updated_utc.cast_checked()?).to_rfc3339(); let last_testrun_utc = value .last_testrun_utc - .map(|t| timestamp_as_utc(t as u64).to_rfc3339()); + .and_then(|i| i.cast_checked().ok()) + .map(|t| timestamp_as_utc(t).to_rfc3339()); let self_described = value.self_described.clone().unwrap_or("null".to_string()); let explorer_pretty_bond = value @@ -68,7 +75,7 @@ impl From for http::models::Gateway { details: value.details.clone(), }; - http::models::Gateway { + Ok(http::models::Gateway { gateway_identity_key: value.gateway_identity_key.clone(), bonded, blacklisted, @@ -82,7 +89,7 @@ impl From for http::models::Gateway { config_score, last_testrun_utc, last_updated_utc, - } + }) } } @@ -121,10 +128,11 @@ pub(crate) struct MixnodeDto { pub(crate) details: String, } -impl From for http::models::Mixnode { - fn from(value: MixnodeDto) -> Self { - // TODO dz avoid as? - let mix_id = value.mix_id as u32; +impl TryFrom for http::models::Mixnode { + type Error = anyhow::Error; + + fn try_from(value: MixnodeDto) -> Result { + let mix_id = value.mix_id.cast_checked()?; let full_details = value.full_details.clone(); let full_details = serde_json::from_str(&full_details).unwrap_or(None); @@ -133,7 +141,8 @@ impl From for http::models::Mixnode { .clone() .map(|v| serde_json::from_str(&v).unwrap_or(serde_json::Value::Null)); - let last_updated_utc = timestamp_as_utc(value.last_updated_utc as u64).to_rfc3339(); + let last_updated_utc = + timestamp_as_utc(value.last_updated_utc.cast_checked()?).to_rfc3339(); let blacklisted = value.blacklisted; let is_dp_delegatee = value.is_dp_delegatee; let moniker = value.moniker.clone(); @@ -141,7 +150,7 @@ impl From for http::models::Mixnode { let security_contact = value.security_contact.clone(); let details = value.details.clone(); - http::models::Mixnode { + Ok(http::models::Mixnode { mix_id, bonded: value.bonded, blacklisted, @@ -156,7 +165,7 @@ impl From for http::models::Mixnode { }, self_described, last_updated_utc, - } + }) } } @@ -185,15 +194,16 @@ pub(crate) struct SummaryHistoryDto { pub timestamp_utc: i64, } -impl From for SummaryHistory { - fn from(value: SummaryHistoryDto) -> Self { +impl TryFrom for SummaryHistory { + type Error = anyhow::Error; + + fn try_from(value: SummaryHistoryDto) -> Result { let value_json = serde_json::from_str(&value.value_json).unwrap_or_default(); - SummaryHistory { + Ok(SummaryHistory { value_json, date: value.date.clone(), - // TODO dz avoid as? - timestamp_utc: timestamp_as_utc(value.timestamp_utc as u64).to_rfc3339(), - } + timestamp_utc: timestamp_as_utc(value.timestamp_utc.cast_checked()?).to_rfc3339(), + }) } } diff --git a/nym-node-status-api/src/db/queries/gateways.rs b/nym-node-status-api/src/db/queries/gateways.rs index b7315a5120..92a599154f 100644 --- a/nym-node-status-api/src/db/queries/gateways.rs +++ b/nym-node-status-api/src/db/queries/gateways.rs @@ -7,6 +7,7 @@ use crate::{ }; use futures_util::TryStreamExt; use nym_validator_client::models::DescribedGateway; +use tracing::error; pub(crate) async fn insert_gateways( pool: &DbPool, @@ -146,7 +147,14 @@ pub(crate) async fn get_all_gateways(pool: &DbPool) -> anyhow::Result>() .await?; - let items: Vec = items.into_iter().map(|item| item.into()).collect(); + let items: Vec = items + .into_iter() + .map(|item| item.try_into()) + .collect::>>() + .map_err(|e| { + error!("Conversion from DTO failed: {e}. Invalidly stored data?"); + e + })?; tracing::trace!("Fetched {} gateways from DB", items.len()); Ok(items) } diff --git a/nym-node-status-api/src/db/queries/mixnodes.rs b/nym-node-status-api/src/db/queries/mixnodes.rs index ccb9c0b3ed..8bc8020ef9 100644 --- a/nym-node-status-api/src/db/queries/mixnodes.rs +++ b/nym-node-status-api/src/db/queries/mixnodes.rs @@ -1,5 +1,6 @@ use futures_util::TryStreamExt; use nym_validator_client::models::MixNodeBondAnnotated; +use tracing::error; use crate::{ db::{ @@ -74,7 +75,14 @@ pub(crate) async fn get_all_mixnodes(pool: &DbPool) -> anyhow::Result>() .await?; - let items = items.into_iter().map(|item| item.into()).collect(); + let items = items + .into_iter() + .map(|item| item.try_into()) + .collect::>>() + .map_err(|e| { + error!("Conversion from DTO failed: {e}. Invalidly stored data?"); + e + })?; Ok(items) } diff --git a/nym-node-status-api/src/db/queries/summary.rs b/nym-node-status-api/src/db/queries/summary.rs index 6eddb52758..d3855639f6 100644 --- a/nym-node-status-api/src/db/queries/summary.rs +++ b/nym-node-status-api/src/db/queries/summary.rs @@ -1,6 +1,7 @@ use chrono::{DateTime, Utc}; use futures_util::TryStreamExt; use std::collections::HashMap; +use tracing::error; use crate::{ db::{ @@ -40,7 +41,14 @@ pub(crate) async fn get_summary_history(pool: &DbPool) -> anyhow::Result>() .await?; - let items = items.into_iter().map(|item| item.into()).collect(); + let items = items + .into_iter() + .map(|item| item.try_into()) + .collect::>>() + .map_err(|e| { + error!("Conversion from DTO failed: {e}. Invalidly stored data?"); + e + })?; Ok(items) } diff --git a/nym-node-status-api/src/http/api/mod.rs b/nym-node-status-api/src/http/api/mod.rs index 7f011e4810..4d385904b5 100644 --- a/nym-node-status-api/src/http/api/mod.rs +++ b/nym-node-status-api/src/http/api/mod.rs @@ -7,15 +7,10 @@ use utoipa_swagger_ui::SwaggerUi; use crate::http::{server::HttpServer, state::AppState}; -// TODO dz src/http/gateways.rs pub(crate) mod gateways; -// TODO dz src/http/mixnodes.rs pub(crate) mod mixnodes; -// TODO dz src/http/services.rs pub(crate) mod services; -// TODO dz src/http/summary.rs pub(crate) mod summary; -// TODO dz src/http/testruns.rs pub(crate) mod testruns; pub(crate) struct RouterBuilder { @@ -40,7 +35,7 @@ impl RouterBuilder { .nest("/mixnodes", mixnodes::routes()) .nest("/services", services::routes()) .nest("/summary", summary::routes()), - // .merge(testruns::routes()), + // .nest("/testruns", testruns::_routes()), ); Self { diff --git a/nym-node-status-api/src/http/error.rs b/nym-node-status-api/src/http/error.rs index 8c8dd8cd20..8bbd59e095 100644 --- a/nym-node-status-api/src/http/error.rs +++ b/nym-node-status-api/src/http/error.rs @@ -15,7 +15,7 @@ impl HttpError { pub(crate) fn internal() -> Self { Self { - message: String::from("Internal"), + message: serde_json::json!({"message": "Internal server error"}).to_string(), status: axum::http::StatusCode::INTERNAL_SERVER_ERROR, } } diff --git a/nym-node-status-api/src/http/server.rs b/nym-node-status-api/src/http/server.rs index d2740edf39..694f5fa79a 100644 --- a/nym-node-status-api/src/http/server.rs +++ b/nym-node-status-api/src/http/server.rs @@ -17,11 +17,10 @@ pub(crate) async fn start_http_api( ) -> anyhow::Result { let router_builder = RouterBuilder::with_default_routes(); - // TODO dz init routes - let state = AppState::new(db_pool, nym_http_cache_ttl); let router = router_builder.with_state(state); + // TODO dz do we need this to be configurable? let bind_addr = format!("0.0.0.0:{}", http_port); let server = router.build_server(bind_addr).await?; diff --git a/nym-node-status-api/src/logging.rs b/nym-node-status-api/src/logging.rs index 74479e3c7d..01dd31562e 100644 --- a/nym-node-status-api/src/logging.rs +++ b/nym-node-status-api/src/logging.rs @@ -2,8 +2,11 @@ use tracing::level_filters::LevelFilter; use tracing_subscriber::{filter::Directive, EnvFilter}; pub(crate) fn setup_tracing_logger() { - fn directive_checked(directive: String) -> Directive { - directive.parse().expect("Failed to parse log directive") + fn directive_checked(directive: impl Into) -> Directive { + directive + .into() + .parse() + .expect("Failed to parse log directive") } let log_builder = tracing_subscriber::fmt() @@ -13,6 +16,7 @@ pub(crate) fn setup_tracing_logger() { .with_file(true) // Display source code line numbers .with_line_number(true) + .with_thread_ids(true) // Don't display the event's target (module path) .with_target(false); @@ -22,10 +26,6 @@ pub(crate) fn setup_tracing_logger() { .from_env_lossy(); // these crates are more granularly filtered let filter_crates = [ - "nym_bin_common", - "nym_explorer_client", - "nym_network_defaults", - "nym_validator_client", "reqwest", "rustls", "hyper", @@ -39,5 +39,10 @@ pub(crate) fn setup_tracing_logger() { filter = filter.add_directive(directive_checked(format!("{}=warn", crate_name))); } + filter = filter.add_directive(directive_checked("nym_bin_common=debug")); + filter = filter.add_directive(directive_checked("nym_explorer_client=debug")); + filter = filter.add_directive(directive_checked("nym_network_defaults=debug")); + filter = filter.add_directive(directive_checked("nym_validator_client=debug")); + log_builder.with_env_filter(filter).init(); } diff --git a/nym-node-status-api/src/main.rs b/nym-node-status-api/src/main.rs index d7d4034af5..0fb1d0e1d2 100644 --- a/nym-node-status-api/src/main.rs +++ b/nym-node-status-api/src/main.rs @@ -23,13 +23,14 @@ async fn main() -> anyhow::Result<()> { tracing::debug!("{:?}", read_env_var("EXPLORER_API")); tracing::debug!("{:?}", read_env_var("NYM_API")); - let conf = config::Config::from_env(); - tracing::debug!("Using config:\n{:?}", conf); + let conf = config::Config::from_env()?; + tracing::debug!("Using config:\n{:#?}", conf); let storage = db::Storage::init().await?; let db_pool = storage.pool_owned().await; + let conf_clone = conf.clone(); tokio::spawn(async move { - monitor::spawn_in_background(db_pool).await; + monitor::spawn_in_background(db_pool, conf_clone).await; }); tracing::info!("Started monitor task"); @@ -40,8 +41,7 @@ async fn main() -> anyhow::Result<()> { ) .await .expect("Failed to start server"); - // TODO dz load bind address from config - // TODO dz log bind address + tracing::info!("Started HTTP server on port {}", conf.http_port()); wait_for_signal().await; diff --git a/nym-node-status-api/src/monitor/mod.rs b/nym-node-status-api/src/monitor/mod.rs index 70da2ab024..4215e297cc 100644 --- a/nym-node-status-api/src/monitor/mod.rs +++ b/nym-node-status-api/src/monitor/mod.rs @@ -1,3 +1,4 @@ +use crate::config::Config; use crate::db::models::{ gateway, mixnode, GatewayRecord, MixnodeRecord, NetworkSummary, GATEWAYS_BLACKLISTED_COUNT, GATEWAYS_BONDED_COUNT, GATEWAYS_EXPLORER_COUNT, GATEWAYS_HISTORICAL_COUNT, @@ -15,24 +16,26 @@ use nym_validator_client::nym_nodes::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::HashSet; use std::str::FromStr; use tokio::task::JoinHandle; use tokio::time::Duration; const REFRESH_DELAY: Duration = Duration::from_secs(60 * 5); -const FAILURE_RETRY_DELAY: Duration = Duration::from_secs(60); +const FAILURE_RETRY_DELAY: Duration = Duration::from_secs(15); + static DELEGATION_PROGRAM_WALLET: &str = "n1rnxpdpx3kldygsklfft0gech7fhfcux4zst5lw"; // TODO dz: query many NYM APIs: // multiple instances running directory cache, ask sachin -pub(crate) async fn spawn_in_background(db_pool: DbPool) -> JoinHandle<()> { +pub(crate) async fn spawn_in_background(db_pool: DbPool, config: Config) -> JoinHandle<()> { let network_defaults = nym_network_defaults::NymNetworkDetails::new_from_env(); loop { tracing::info!("Refreshing node info..."); - if let Err(e) = run(&db_pool, &network_defaults).await { + if let Err(e) = run(&db_pool, &network_defaults, &config).await { tracing::error!( "Monitor run failed: {e}, retrying in {}s...", FAILURE_RETRY_DELAY.as_secs() @@ -48,7 +51,11 @@ pub(crate) async fn spawn_in_background(db_pool: DbPool) -> JoinHandle<()> { } } -async fn run(pool: &DbPool, network_details: &NymNetworkDetails) -> anyhow::Result<()> { +async fn run( + pool: &DbPool, + network_details: &NymNetworkDetails, + config: &Config, +) -> anyhow::Result<()> { let default_api_url = network_details .endpoints .first() @@ -63,15 +70,32 @@ async fn run(pool: &DbPool, network_details: &NymNetworkDetails) -> anyhow::Resu let default_explorer_url = default_explorer_url.expect("explorer url missing in network config"); - let explorer_client = ExplorerClient::new(default_explorer_url)?; - let explorer_gateways = explorer_client.get_gateways().await?; + let explorer_client = ExplorerClient::new_with_timeout( + default_explorer_url, + config.nym_explorer_client_timeout(), + )?; + let explorer_gateways = explorer_client + .get_gateways() + .await + .log_error("get_gateways")?; + tracing::debug!("6"); - let api_client = NymApiClient::new(default_api_url); - let gateways = api_client.get_cached_described_gateways().await?; + let api_client = + NymApiClient::new_with_timeout(default_api_url, config.nym_api_client_timeout()); + let gateways = api_client + .get_cached_described_gateways() + .await + .log_error("get_described_gateways")?; tracing::debug!("Fetched {} gateways", gateways.len()); - let skimmed_gateways = api_client.get_basic_gateways(None).await?; + let skimmed_gateways = api_client + .get_basic_gateways(None) + .await + .log_error("get_basic_gateways")?; - let mixnodes = api_client.get_cached_mixnodes().await?; + let mixnodes = api_client + .get_cached_mixnodes() + .await + .log_error("get_cached_mixnodes")?; tracing::debug!("Fetched {} mixnodes", mixnodes.len()); // TODO dz can we calculate blacklisted GWs from their performance? @@ -80,17 +104,28 @@ async fn run(pool: &DbPool, network_details: &NymNetworkDetails) -> anyhow::Resu .nym_api .get_gateways_blacklisted() .await - .map(|vec| vec.into_iter().collect::>())?; + .map(|vec| vec.into_iter().collect::>()) + .log_error("get_gateways_blacklisted")?; // Cached mixnodes don't include blacklisted nodes // We need that to calculate the total locked tokens later let mixnodes = api_client .nym_api .get_mixnodes_detailed_unfiltered() - .await?; - let mixnodes_described = api_client.nym_api.get_mixnodes_described().await?; - let mixnodes_active = api_client.nym_api.get_active_mixnodes().await?; - let delegation_program_members = get_delegation_program_details(network_details).await?; + .await + .log_error("get_mixnodes_detailed_unfiltered")?; + let mixnodes_described = api_client + .nym_api + .get_mixnodes_described() + .await + .log_error("get_mixnodes_described")?; + let mixnodes_active = api_client + .nym_api + .get_active_mixnodes() + .await + .log_error("get_active_mixnodes")?; + let delegation_program_members = + get_delegation_program_details(network_details, config.nyxd_addr()).await?; // keep stats for later let count_bonded_mixnodes = mixnodes.len(); @@ -165,42 +200,41 @@ async fn run(pool: &DbPool, network_details: &NymNetworkDetails) -> anyhow::Resu (GATEWAYS_BLACKLISTED_COUNT, &count_gateways_blacklisted), ]; - // TODO dz do we need signed int in type definition? maybe because of API? let last_updated = chrono::offset::Utc::now(); let last_updated_utc = last_updated.timestamp().to_string(); let network_summary = NetworkSummary { mixnodes: mixnode::MixnodeSummary { bonded: mixnode::MixnodeSummaryBonded { - count: count_bonded_mixnodes as i32, - active: count_bonded_mixnodes_active as i32, - inactive: count_bonded_mixnodes_inactive as i32, - reserve: count_bonded_mixnodes_reserve as i32, + count: count_bonded_mixnodes.cast_checked()?, + active: count_bonded_mixnodes_active.cast_checked()?, + inactive: count_bonded_mixnodes_inactive.cast_checked()?, + reserve: count_bonded_mixnodes_reserve.cast_checked()?, last_updated_utc: last_updated_utc.to_owned(), }, blacklisted: mixnode::MixnodeSummaryBlacklisted { - count: count_mixnodes_blacklisted as i32, + count: count_mixnodes_blacklisted.cast_checked()?, last_updated_utc: last_updated_utc.to_owned(), }, historical: mixnode::MixnodeSummaryHistorical { - count: all_historical_mixnodes as i32, + count: all_historical_mixnodes.cast_checked()?, last_updated_utc: last_updated_utc.to_owned(), }, }, gateways: gateway::GatewaySummary { bonded: gateway::GatewaySummaryBonded { - count: count_bonded_gateways as i32, + count: count_bonded_gateways.cast_checked()?, last_updated_utc: last_updated_utc.to_owned(), }, blacklisted: gateway::GatewaySummaryBlacklisted { - count: count_gateways_blacklisted as i32, + count: count_gateways_blacklisted.cast_checked()?, last_updated_utc: last_updated_utc.to_owned(), }, historical: gateway::GatewaySummaryHistorical { - count: all_historical_gateways as i32, + count: all_historical_gateways.cast_checked()?, last_updated_utc: last_updated_utc.to_owned(), }, explorer: gateway::GatewaySummaryExplorer { - count: count_explorer_gateways as i32, + count: count_explorer_gateways.cast_checked()?, last_updated_utc: last_updated_utc.to_owned(), }, }, @@ -314,27 +348,56 @@ fn prepare_mixnode_data( Ok(mixnode_records) } +// TODO dz is there a common monorepo place this can be put? +pub trait NumericalCheckedCast +where + T: TryFrom, + >::Error: std::error::Error, + Self: std::fmt::Display + Copy, +{ + fn cast_checked(self) -> anyhow::Result { + T::try_from(self).map_err(|e| { + anyhow::anyhow!( + "Couldn't cast {} to {}: {}", + self, + std::any::type_name::(), + e + ) + }) + } +} + +impl NumericalCheckedCast for T +where + U: TryFrom, + >::Error: std::error::Error, + T: std::fmt::Display + Copy, +{ +} + async fn calculate_stats(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"#) .fetch_one(&mut *conn) - .await? as usize; + .await? + .cast_checked()?; let all_historical_mixnodes = sqlx::query_scalar!(r#"SELECT count(id) FROM mixnodes"#) .fetch_one(&mut *conn) - .await? as usize; + .await? + .cast_checked()?; Ok((all_historical_gateways, all_historical_mixnodes)) } async fn get_delegation_program_details( network_details: &NymNetworkDetails, + nyxd_addr: &Url, ) -> anyhow::Result> { let config = nym_validator_client::nyxd::Config::try_from_nym_network_details(network_details)?; - // TODO dz should this be configurable? - let client = NyxdClient::connect(config, "https://rpc.nymtech.net") + 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) @@ -369,3 +432,19 @@ fn decimal_to_i64(decimal: Decimal) -> i64 { rounded_value as i64 } + +trait LogError { + fn log_error(self, msg: &str) -> Result; +} + +impl LogError for anyhow::Result +where + E: std::error::Error, +{ + fn log_error(self, msg: &str) -> Result { + if let Err(e) = &self { + tracing::error!("[{msg}]:\t{e}"); + } + self + } +}