Add axum server to nym-api (#4803)

* Migrate nym-api HTTP server from rocket to axum (#4698)

Migrate endpoints to Axum

* Squashed after PR review

Initial WIP
- bootstrap axum server with same data as rocket
- start axum server alongside rocket
- add routes for circulating-supply, contract-cache, network
- write simple bash validation that migrated APIs return 200
- mark rocket parts of code as deprecated
- start more complicated routes: WIP

Init storage always

Add coconut routes

Add api-status routes

Expand tests

WIP

Migrate unstable APIs with query params

Update bash tests

Add node-status routes

Redirect / to /swagger

Update API tests

Implement graceful shutdown

rustfmt

Fix clippy

* Add ecash routes after rebase

* PR feedback
- add CORS layer
- move logger to common crate
- remove global log filters for nym-api and axum

* Serve OpenAPI for all endpoints (#4761)

* Playing around with swagger

* Generate OpenAPI for /status routes

* Phase out static_routes as strings
- also nest routers in a clearer way

* Generate OpenAPI for /network routes

* Generate OpenAPI for /api-status routes

* Generate OpenAPI for "nym nodes" routes

* Fix some network-monitor routes

* Generate OpenAPI for /ecash routes

* Add utoipa feature to /common mods

* Add OpenAPI for unstable routes

* Fix MixNodeDetails field in models

* Introduce axum feature flag (#4775)

* Add Axum bind_address to config

* Introduce axum feature flag

* Add comment to template.rs

* Add Github action to build wtih `axum` feature

* Refactor server start & shutdown (#4777)

* Clippy: don't forget axum feature

* Refactor router so it's safer

* Implement graceful shutdown

* Nicer pattern matching

* Better Result syntax
This commit is contained in:
Dinko Zdravac
2024-08-29 15:31:01 +02:00
committed by GitHub
parent afc1b90b57
commit a0fea6edb4
68 changed files with 4653 additions and 428 deletions
+2 -1
View File
@@ -41,12 +41,13 @@ pub struct NodeStatusCache {
impl NodeStatusCache {
/// Creates a new cache with no data.
fn new() -> NodeStatusCache {
pub(crate) fn new() -> NodeStatusCache {
NodeStatusCache {
inner: Arc::new(RwLock::new(NodeStatusCacheData::new())),
}
}
#[deprecated(note = "TODO rocket: obsolete because it's used for Rocket")]
pub fn stage() -> AdHoc {
AdHoc::on_ignite("Node Status Cache", |rocket| async {
rocket.manage(Self::new())
+16 -26
View File
@@ -52,12 +52,11 @@ pub(super) fn split_into_active_and_rewarded_set(
}
pub(super) async fn get_mixnode_performance_from_storage(
storage: &Option<NymApiStorage>,
storage: &NymApiStorage,
mix_id: MixId,
epoch: Interval,
) -> Option<Performance> {
storage
.as_ref()?
.get_average_mixnode_uptime_in_the_last_24hrs(
mix_id,
epoch.current_epoch_end_unix_timestamp(),
@@ -68,12 +67,11 @@ pub(super) async fn get_mixnode_performance_from_storage(
}
pub(super) async fn get_gateway_performance_from_storage(
storage: &Option<NymApiStorage>,
storage: &NymApiStorage,
gateway_id: &str,
epoch: Interval,
) -> Option<Performance> {
storage
.as_ref()?
.get_average_gateway_uptime_in_the_last_24hrs(
gateway_id,
epoch.current_epoch_end_unix_timestamp(),
@@ -84,7 +82,7 @@ pub(super) async fn get_gateway_performance_from_storage(
}
pub(super) async fn annotate_nodes_with_details(
storage: &Option<NymApiStorage>,
storage: &NymApiStorage,
mixnodes: Vec<MixNodeDetails>,
interval_reward_params: RewardingParams,
current_interval: Interval,
@@ -123,16 +121,12 @@ pub(super) async fn annotate_nodes_with_details(
current_interval,
);
let node_performance = if let Some(storage) = storage {
storage
.construct_mixnode_report(mixnode.mix_id())
.await
.map(NodePerformance::from)
.ok()
} else {
None
}
.unwrap_or_default();
let node_performance = storage
.construct_mixnode_report(mixnode.mix_id())
.await
.map(NodePerformance::from)
.ok()
.unwrap_or_default();
// safety: this conversion is infallible
let ip_addresses =
@@ -177,7 +171,7 @@ pub(super) async fn annotate_nodes_with_details(
}
pub(crate) async fn annotate_gateways_with_details(
storage: &Option<NymApiStorage>,
storage: &NymApiStorage,
gateway_bonds: Vec<GatewayBond>,
current_interval: Interval,
blacklist: &HashSet<IdentityKey>,
@@ -192,16 +186,12 @@ pub(crate) async fn annotate_gateways_with_details(
.await
.unwrap_or_default();
let node_performance = if let Some(storage) = storage {
storage
.construct_gateway_report(gateway_bond.identity())
.await
.map(NodePerformance::from)
.ok()
} else {
None
}
.unwrap_or_default();
let node_performance = storage
.construct_gateway_report(gateway_bond.identity())
.await
.map(NodePerformance::from)
.ok()
.unwrap_or_default();
// safety: this conversion is infallible
let ip_addresses = match NetworkAddress::from_str(&gateway_bond.gateway.host).unwrap() {
+2 -2
View File
@@ -29,7 +29,7 @@ pub struct NodeStatusCacheRefresher {
// Sources for when refreshing data
contract_cache: NymContractCache,
contract_cache_listener: watch::Receiver<CacheNotification>,
storage: Option<NymApiStorage>,
storage: NymApiStorage,
}
impl NodeStatusCacheRefresher {
@@ -38,7 +38,7 @@ impl NodeStatusCacheRefresher {
fallback_caching_interval: Duration,
contract_cache: NymContractCache,
contract_cache_listener: watch::Receiver<CacheNotification>,
storage: Option<NymApiStorage>,
storage: NymApiStorage,
) -> Self {
Self {
cache,
@@ -0,0 +1,32 @@
// Copyright 2021-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::v2::AxumAppState;
use axum::Router;
use nym_mixnet_contract_common::MixId;
use serde::Deserialize;
use utoipa::IntoParams;
pub(crate) mod network_monitor;
pub(crate) mod unstable;
pub(crate) mod without_monitor;
pub(crate) fn node_status_routes(network_monitor: bool) -> Router<AxumAppState> {
// in the minimal variant we would not have access to endpoints relying on existence
// of the network monitor and the associated storage
let without_network_monitor = without_monitor::mandatory_routes();
if network_monitor {
let with_network_monitor = network_monitor::network_monitor_routes();
with_network_monitor.merge(without_network_monitor)
} else {
without_network_monitor
}
}
#[derive(Deserialize, IntoParams)]
#[into_params(parameter_in = Path)]
struct MixIdParam {
mix_id: MixId,
}
@@ -0,0 +1,339 @@
// Copyright 2021-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::node_status_api::handlers::MixIdParam;
use crate::node_status_api::helpers::{
_compute_mixnode_reward_estimation, _gateway_core_status_count, _gateway_report,
_gateway_uptime_history, _get_gateway_avg_uptime, _get_gateways_detailed,
_get_gateways_detailed_unfiltered, _get_mixnode_avg_uptime, _get_mixnode_reward_estimation,
_get_mixnodes_detailed_unfiltered, _mixnode_core_status_count, _mixnode_report,
_mixnode_uptime_history,
};
use crate::node_status_api::models::AxumResult;
use crate::v2::AxumAppState;
use axum::extract::{Path, Query, State};
use axum::Json;
use axum::Router;
use nym_api_requests::models::{
ComputeRewardEstParam, GatewayBondAnnotated, GatewayCoreStatusResponse,
GatewayStatusReportResponse, GatewayUptimeHistoryResponse, GatewayUptimeResponse,
MixNodeBondAnnotated, MixnodeCoreStatusResponse, MixnodeStatusReportResponse,
MixnodeUptimeHistoryResponse, RewardEstimationResponse, UptimeResponse,
};
use serde::Deserialize;
use utoipa::IntoParams;
use super::unstable;
pub(super) fn network_monitor_routes() -> Router<AxumAppState> {
Router::new()
.nest(
"/gateway/:identity",
Router::new()
.route("/report", axum::routing::get(gateway_report))
.route("/history", axum::routing::get(gateway_uptime_history))
.route(
"/core-status-count",
axum::routing::get(gateway_core_status_count),
)
.route("/avg_uptime", axum::routing::get(get_gateway_avg_uptime)),
)
.nest(
"/mixnode/:mix_id",
Router::new()
.route("/report", axum::routing::get(mixnode_report))
.route("/history", axum::routing::get(mixnode_uptime_history))
.route(
"/core-status-count",
axum::routing::get(mixnode_core_status_count),
)
.route(
"/reward-estimation",
axum::routing::get(get_mixnode_reward_estimation),
)
.route(
"/compute-reward-estimation",
axum::routing::post(compute_mixnode_reward_estimation),
)
.route("/avg_uptime", axum::routing::get(get_mixnode_avg_uptime)),
)
.nest(
"/mixnodes",
Router::new()
.route(
"/detailed-unfiltered",
axum::routing::get(get_mixnodes_detailed_unfiltered),
)
.route(
"/unstable/:mix_id/test-results",
axum::routing::get(unstable::mixnode_test_results),
),
)
.nest(
"/gateways",
Router::new()
.route("/detailed", axum::routing::get(get_gateways_detailed))
.route(
"/detailed-unfiltered",
axum::routing::get(get_gateways_detailed_unfiltered),
)
.route(
"/unstable/:gateway_identity/test-results",
axum::routing::get(unstable::gateway_test_results),
),
)
}
#[utoipa::path(
tag = "network-monitor-status",
get,
path = "/v1/status/gateway/{identity}/report",
responses(
(status = 200, body = GatewayStatusReportResponse)
)
)]
async fn gateway_report(
Path(identity): Path<String>,
State(state): State<AxumAppState>,
) -> AxumResult<Json<GatewayStatusReportResponse>> {
Ok(Json(
_gateway_report(state.node_status_cache(), &identity).await?,
))
}
#[utoipa::path(
tag = "network-monitor-status",
get,
path = "/v1/status/gateway/{identity}/history",
responses(
(status = 200, body = GatewayUptimeHistoryResponse)
)
)]
async fn gateway_uptime_history(
Path(identity): Path<String>,
State(state): State<AxumAppState>,
) -> AxumResult<Json<GatewayUptimeHistoryResponse>> {
Ok(Json(
_gateway_uptime_history(state.storage(), &identity).await?,
))
}
#[derive(Deserialize, IntoParams)]
#[into_params(parameter_in = Query)]
struct SinceQueryParams {
since: Option<i64>,
}
#[utoipa::path(
tag = "network-monitor-status",
get,
params(
SinceQueryParams
),
path = "/v1/status/gateway/{identity}/core-status-count",
responses(
(status = 200, body = GatewayCoreStatusResponse)
)
)]
async fn gateway_core_status_count(
Path(identity): Path<String>,
Query(SinceQueryParams { since }): Query<SinceQueryParams>,
State(state): State<AxumAppState>,
) -> AxumResult<Json<GatewayCoreStatusResponse>> {
Ok(Json(
_gateway_core_status_count(state.storage(), &identity, since).await?,
))
}
#[utoipa::path(
tag = "network-monitor-status",
get,
path = "/v1/status/gateway/{identity}/avg_uptime",
responses(
(status = 200, body = GatewayUptimeResponse)
)
)]
async fn get_gateway_avg_uptime(
Path(identity): Path<String>,
State(state): State<AxumAppState>,
) -> AxumResult<Json<GatewayUptimeResponse>> {
Ok(Json(
_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)
)
)]
async fn mixnode_report(
Path(MixIdParam { mix_id }): Path<MixIdParam>,
State(state): State<AxumAppState>,
) -> AxumResult<Json<MixnodeStatusReportResponse>> {
Ok(Json(
_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)
)
)]
async fn mixnode_uptime_history(
Path(MixIdParam { mix_id }): Path<MixIdParam>,
State(state): State<AxumAppState>,
) -> AxumResult<Json<MixnodeUptimeHistoryResponse>> {
Ok(Json(
_mixnode_uptime_history(state.storage(), mix_id).await?,
))
}
#[utoipa::path(
tag = "network-monitor-status",
get,
params(
MixIdParam, SinceQueryParams
),
path = "/v1/status/mixnode/{mix_id}/core-status-count",
responses(
(status = 200, body = MixnodeCoreStatusResponse)
)
)]
async fn mixnode_core_status_count(
Path(MixIdParam { mix_id }): Path<MixIdParam>,
Query(SinceQueryParams { since }): Query<SinceQueryParams>,
State(state): State<AxumAppState>,
) -> AxumResult<Json<MixnodeCoreStatusResponse>> {
Ok(Json(
_mixnode_core_status_count(state.storage(), mix_id, since).await?,
))
}
#[utoipa::path(
tag = "network-monitor-status",
get,
params(
MixIdParam
),
path = "/v1/status/mixnode/{mix_id}/reward-estimation",
responses(
(status = 200, body = RewardEstimationResponse)
)
)]
async fn get_mixnode_reward_estimation(
Path(MixIdParam { mix_id }): Path<MixIdParam>,
State(state): State<AxumAppState>,
) -> AxumResult<Json<RewardEstimationResponse>> {
Ok(Json(
_get_mixnode_reward_estimation(
state.node_status_cache(),
state.nym_contract_cache(),
mix_id,
)
.await?,
))
}
#[utoipa::path(
tag = "network-monitor-status",
post,
params(
ComputeRewardEstParam, MixIdParam
),
path = "/v1/status/mixnode/{mix_id}/compute-reward-estimation",
request_body = ComputeRewardEstParam,
responses(
(status = 200, body = RewardEstimationResponse)
)
)]
async fn compute_mixnode_reward_estimation(
Path(MixIdParam { mix_id }): Path<MixIdParam>,
State(state): State<AxumAppState>,
Json(user_reward_param): Json<ComputeRewardEstParam>,
) -> AxumResult<Json<RewardEstimationResponse>> {
Ok(Json(
_compute_mixnode_reward_estimation(
&user_reward_param,
state.node_status_cache(),
state.nym_contract_cache(),
mix_id,
)
.await?,
))
}
#[utoipa::path(
tag = "network-monitor-status",
get,
params(
MixIdParam
),
path = "/v1/status/mixnode/{mix_id}/avg_uptime",
responses(
(status = 200, body = UptimeResponse)
)
)]
async fn get_mixnode_avg_uptime(
Path(MixIdParam { mix_id }): Path<MixIdParam>,
State(state): State<AxumAppState>,
) -> AxumResult<Json<UptimeResponse>> {
Ok(Json(
_get_mixnode_avg_uptime(state.node_status_cache(), mix_id).await?,
))
}
#[utoipa::path(
tag = "network-monitor-status",
get,
path = "/v1/status/mixnodes/detailed-unfiltered",
responses(
(status = 200, body = MixNodeBondAnnotated)
)
)]
pub async fn get_mixnodes_detailed_unfiltered(
State(state): State<AxumAppState>,
) -> Json<Vec<MixNodeBondAnnotated>> {
Json(_get_mixnodes_detailed_unfiltered(state.node_status_cache()).await)
}
#[utoipa::path(
tag = "network-monitor-status",
get,
path = "/v1/status/gateways/detailed",
responses(
(status = 200, body = GatewayBondAnnotated)
)
)]
pub async fn get_gateways_detailed(
State(state): State<AxumAppState>,
) -> Json<Vec<GatewayBondAnnotated>> {
Json(_get_gateways_detailed(state.node_status_cache()).await)
}
#[utoipa::path(
tag = "network-monitor-status",
get,
path = "/v1/status/gateways/detailed-unfiltered",
responses(
(status = 200, body = GatewayBondAnnotated)
)
)]
pub async fn get_gateways_detailed_unfiltered(
State(state): State<AxumAppState>,
) -> Json<Vec<GatewayBondAnnotated>> {
Json(_get_gateways_detailed_unfiltered(state.node_status_cache()).await)
}
@@ -0,0 +1,279 @@
// Copyright 2021-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::node_status_api::models::{AxumErrorResponse, AxumResult};
use crate::support::http::helpers::PaginationRequest;
use crate::support::storage::NymApiStorage;
use crate::v2::AxumAppState;
use axum::extract::{Path, Query, State};
use axum::Json;
use nym_api_requests::models::{
GatewayTestResultResponse, MixnodeTestResultResponse, PartialTestResult, TestNode, TestRoute,
};
use nym_api_requests::pagination::Pagination;
use nym_mixnet_contract_common::MixId;
use std::cmp::min;
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
pub type DbId = i64;
// a simply in-memory cache of node details
#[derive(Debug, Clone, Default)]
pub struct NodeInfoCache {
inner: Arc<RwLock<NodeInfoCacheInner>>,
}
impl NodeInfoCache {
async fn get_mix_node_details(&self, db_id: DbId, storage: &NymApiStorage) -> TestNode {
{
let read_guard = self.inner.read().await;
if let Some(cached) = read_guard.mixnodes.get(&db_id) {
trace!("cache hit for mixnode {db_id}");
return cached.clone();
}
}
trace!("cache miss for mixnode {db_id}");
let mut write_guard = self.inner.write().await;
// double-check the cache in case somebody already updated it while we were waiting for the lock
if let Some(cached) = write_guard.mixnodes.get(&db_id) {
return cached.clone();
}
let details = match storage.get_mixnode_details_by_db_id(db_id).await {
Ok(Some(details)) => details.into(),
Ok(None) => {
error!("somebody has been messing with the database! details for mixnode with database id {db_id} have been removed!");
TestNode::default()
}
Err(err) => {
// don't insert into the cache in case another request is successful
error!("failed to retrieve details for mixnode {db_id}: {err}");
return TestNode::default();
}
};
write_guard.mixnodes.insert(db_id, details.clone());
details
}
async fn get_gateway_details(&self, db_id: DbId, storage: &NymApiStorage) -> TestNode {
{
let read_guard = self.inner.read().await;
if let Some(cached) = read_guard.gateways.get(&db_id) {
trace!("cache hit for gateway {db_id}");
return cached.clone();
}
}
trace!("cache miss for gateway {db_id}");
let mut write_guard = self.inner.write().await;
// double-check the cache in case somebody already updated it while we were waiting for the lock
if let Some(cached) = write_guard.gateways.get(&db_id) {
return cached.clone();
}
let details = match storage.get_gateway_details_by_db_id(db_id).await {
Ok(Some(details)) => details.into(),
Ok(None) => {
error!("somebody has been messing with the database! details for gateway with database id {db_id} have been removed!");
TestNode::default()
}
Err(err) => {
// don't insert into the cache in case another request is successful
error!("failed to retrieve details for gateway {db_id}: {err}");
return TestNode::default();
}
};
write_guard.gateways.insert(db_id, details.clone());
details
}
}
#[derive(Debug, Clone, Default)]
struct NodeInfoCacheInner {
mixnodes: HashMap<DbId, TestNode>,
gateways: HashMap<DbId, TestNode>,
}
const MAX_TEST_RESULTS_PAGE_SIZE: u32 = 100;
const DEFAULT_TEST_RESULTS_PAGE_SIZE: u32 = 50;
async fn _mixnode_test_results(
mix_id: MixId,
page: u32,
per_page: u32,
info_cache: &NodeInfoCache,
storage: &NymApiStorage,
) -> anyhow::Result<MixnodeTestResultResponse> {
// convert to db offset
// we're paging from page 0 like civilised people,
// so we have to skip (page * per_page) results
let offset = page * per_page;
let limit = per_page;
let raw_results = storage
.get_mixnode_detailed_statuses(mix_id, limit, offset)
.await?;
let total = match raw_results.first() {
None => 0,
Some(r) => storage.get_mixnode_detailed_statuses_count(r.db_id).await?,
};
let mut partial_results = Vec::new();
for result in raw_results {
let gateway = info_cache
.get_gateway_details(result.gateway_id, storage)
.await;
let layer1 = info_cache
.get_mix_node_details(result.layer1_mix_id, storage)
.await;
let layer2 = info_cache
.get_mix_node_details(result.layer2_mix_id, storage)
.await;
let layer3 = info_cache
.get_mix_node_details(result.layer3_mix_id, storage)
.await;
partial_results.push(PartialTestResult {
monitor_run_id: result.monitor_run_id,
timestamp: result.timestamp,
overall_reliability_for_all_routes_in_monitor_run: result.reliability,
test_routes: TestRoute {
gateway,
layer1,
layer2,
layer3,
},
})
}
Ok(MixnodeTestResultResponse {
pagination: Pagination {
total,
page,
size: partial_results.len(),
},
data: partial_results,
})
}
pub async fn mixnode_test_results(
Path(mix_id): Path<MixId>,
Query(pagination): Query<PaginationRequest>,
State(state): State<AxumAppState>,
) -> AxumResult<Json<MixnodeTestResultResponse>> {
let page = pagination.page.unwrap_or_default();
let per_page = min(
pagination
.per_page
.unwrap_or(DEFAULT_TEST_RESULTS_PAGE_SIZE),
MAX_TEST_RESULTS_PAGE_SIZE,
);
match _mixnode_test_results(
mix_id,
page,
per_page,
state.node_info_cache(),
state.storage(),
)
.await
{
Ok(res) => Ok(Json(res)),
Err(err) => Err(AxumErrorResponse::internal_msg(format!(
"failed to retrieve mixnode test results for node {mix_id}: {err}"
))),
}
}
async fn _gateway_test_results(
gateway_identity: &str,
page: u32,
per_page: u32,
info_cache: &NodeInfoCache,
storage: &NymApiStorage,
) -> anyhow::Result<GatewayTestResultResponse> {
// convert to db offset
// we're paging from page 0 like civilised people,
// so we have to skip (page * per_page) results
let offset = page * per_page;
let limit = per_page;
let raw_results = storage
.get_gateway_detailed_statuses(gateway_identity, limit, offset)
.await?;
let total = match raw_results.first() {
None => 0,
Some(r) => storage.get_gateway_detailed_statuses_count(r.db_id).await?,
};
let mut partial_results = Vec::new();
for result in raw_results {
let gateway = info_cache
.get_gateway_details(result.gateway_id, storage)
.await;
let layer1 = info_cache
.get_mix_node_details(result.layer1_mix_id, storage)
.await;
let layer2 = info_cache
.get_mix_node_details(result.layer2_mix_id, storage)
.await;
let layer3 = info_cache
.get_mix_node_details(result.layer3_mix_id, storage)
.await;
partial_results.push(PartialTestResult {
monitor_run_id: result.monitor_run_id,
timestamp: result.timestamp,
overall_reliability_for_all_routes_in_monitor_run: result.reliability,
test_routes: TestRoute {
gateway,
layer1,
layer2,
layer3,
},
})
}
Ok(GatewayTestResultResponse {
pagination: Pagination {
total,
page,
size: partial_results.len(),
},
data: partial_results,
})
}
pub async fn gateway_test_results(
Path(gateway_identity): Path<String>,
Query(pagination): Query<PaginationRequest>,
State(state): State<AxumAppState>,
) -> AxumResult<Json<GatewayTestResultResponse>> {
let page = pagination.page.unwrap_or_default();
let per_page = min(
pagination
.per_page
.unwrap_or(DEFAULT_TEST_RESULTS_PAGE_SIZE),
MAX_TEST_RESULTS_PAGE_SIZE,
);
match _gateway_test_results(
&gateway_identity,
page,
per_page,
state.node_info_cache(),
state.storage(),
)
.await
{
Ok(res) => Ok(Json(res)),
Err(err) => Err(AxumErrorResponse::internal_msg(format!(
"failed to retrieve mixnode test results for gateway {gateway_identity}: {err}"
))),
}
}
@@ -0,0 +1,176 @@
// Copyright 2021-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::node_status_api::handlers::MixIdParam;
use crate::node_status_api::helpers::{
_get_active_set_detailed, _get_mixnode_inclusion_probabilities,
_get_mixnode_inclusion_probability, _get_mixnode_stake_saturation, _get_mixnode_status,
_get_mixnodes_detailed, _get_rewarded_set_detailed,
};
use crate::node_status_api::models::AxumResult;
use crate::v2::AxumAppState;
use axum::extract::{Path, State};
use axum::Json;
use axum::Router;
use nym_api_requests::models::{
AllInclusionProbabilitiesResponse, InclusionProbabilityResponse, MixNodeBondAnnotated,
MixnodeStatusResponse, StakeSaturationResponse,
};
use nym_mixnet_contract_common::MixId;
pub(super) fn mandatory_routes() -> Router<AxumAppState> {
Router::new()
.nest(
"/mixnode/:mix_id",
Router::new()
.route("/status", axum::routing::get(get_mixnode_status))
.route(
"/stake-saturation",
axum::routing::get(get_mixnode_stake_saturation),
)
.route(
"/inclusion-probability",
axum::routing::get(get_mixnode_inclusion_probability),
),
)
.merge(
Router::new().nest(
"/mixnodes",
Router::new()
.route(
"/inclusion-probability",
axum::routing::get(get_mixnode_inclusion_probabilities),
)
.route("/detailed", axum::routing::get(get_mixnodes_detailed))
.route(
"/rewarded/detailed",
axum::routing::get(get_rewarded_set_detailed),
)
.route(
"/active/detailed",
axum::routing::get(get_active_set_detailed),
),
),
)
}
#[utoipa::path(
tag = "status",
get,
params(
MixIdParam
),
path = "/v1/status/mixnode/{mix_id}/status",
responses(
(status = 200, body = MixnodeStatusResponse)
)
)]
async fn get_mixnode_status(
Path(MixIdParam { mix_id }): Path<MixIdParam>,
State(state): State<AxumAppState>,
) -> Json<MixnodeStatusResponse> {
Json(_get_mixnode_status(state.nym_contract_cache(), mix_id).await)
}
#[utoipa::path(
tag = "status",
get,
params(
MixIdParam
),
path = "/v1/status/mixnode/{mix_id}/stake-saturation",
responses(
(status = 200, body = StakeSaturationResponse)
)
)]
async fn get_mixnode_stake_saturation(
Path(mix_id): Path<MixId>,
State(state): State<AxumAppState>,
) -> AxumResult<Json<StakeSaturationResponse>> {
Ok(Json(
_get_mixnode_stake_saturation(
state.node_status_cache(),
state.nym_contract_cache(),
mix_id,
)
.await?,
))
}
#[utoipa::path(
tag = "status",
get,
params(
MixIdParam
),
path = "/v1/status/mixnode/{mix_id}/inclusion-probability",
responses(
(status = 200, body = InclusionProbabilityResponse)
)
)]
async fn get_mixnode_inclusion_probability(
Path(mix_id): Path<MixId>,
State(state): State<AxumAppState>,
) -> AxumResult<Json<InclusionProbabilityResponse>> {
Ok(Json(
_get_mixnode_inclusion_probability(state.node_status_cache(), mix_id).await?,
))
}
#[utoipa::path(
tag = "status",
get,
path = "/v1/status/mixnodes/inclusion-probability",
responses(
(status = 200, body = AllInclusionProbabilitiesResponse)
)
)]
async fn get_mixnode_inclusion_probabilities(
State(state): State<AxumAppState>,
) -> AxumResult<Json<AllInclusionProbabilitiesResponse>> {
Ok(Json(
_get_mixnode_inclusion_probabilities(state.node_status_cache()).await?,
))
}
#[utoipa::path(
tag = "status",
get,
path = "/v1/status/mixnodes/detailed",
responses(
(status = 200, body = MixNodeBondAnnotated)
)
)]
pub async fn get_mixnodes_detailed(
State(state): State<AxumAppState>,
) -> Json<Vec<MixNodeBondAnnotated>> {
Json(_get_mixnodes_detailed(state.node_status_cache()).await)
}
#[utoipa::path(
tag = "status",
get,
path = "/v1/status/mixnodes/rewarded/detailed",
responses(
(status = 200, body = MixNodeBondAnnotated)
)
)]
pub async fn get_rewarded_set_detailed(
State(state): State<AxumAppState>,
) -> Json<Vec<MixNodeBondAnnotated>> {
Json(_get_rewarded_set_detailed(state.node_status_cache()).await)
}
#[utoipa::path(
tag = "status",
get,
path = "/v1/status/mixnodes/active/detailed",
responses(
(status = 200, body = MixNodeBondAnnotated)
)
)]
pub async fn get_active_set_detailed(
State(state): State<AxumAppState>,
) -> Json<Vec<MixNodeBondAnnotated>> {
Json(_get_active_set_detailed(state.node_status_cache()).await)
}
+41 -63
View File
@@ -1,7 +1,8 @@
// Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::node_status_api::models::ErrorResponse;
use super::reward_estimate::compute_reward_estimate;
use crate::node_status_api::models::{AxumErrorResponse, AxumResult};
use crate::storage::NymApiStorage;
use crate::support::caching::Cache;
use crate::{NodeStatusCache, NymContractCache};
@@ -15,41 +16,31 @@ use nym_api_requests::models::{
UptimeResponse,
};
use nym_mixnet_contract_common::{MixId, RewardedSetNodeStatus};
use rocket::http::Status;
use rocket::State;
use super::reward_estimate::compute_reward_estimate;
async fn get_gateway_bond_annotated(
cache: &NodeStatusCache,
identity: &str,
) -> Result<GatewayBondAnnotated, ErrorResponse> {
) -> AxumResult<GatewayBondAnnotated> {
cache
.gateway_annotated(identity)
.await
.ok_or(ErrorResponse::new(
"gateway bond not found",
Status::NotFound,
))
.ok_or(AxumErrorResponse::not_found("gateway bond not found"))
}
async fn get_mixnode_bond_annotated(
cache: &NodeStatusCache,
mix_id: MixId,
) -> Result<MixNodeBondAnnotated, ErrorResponse> {
) -> AxumResult<MixNodeBondAnnotated> {
cache
.mixnode_annotated(mix_id)
.await
.ok_or(ErrorResponse::new(
"mixnode bond not found",
Status::NotFound,
))
.ok_or(AxumErrorResponse::not_found("mixnode bond not found"))
}
pub(crate) async fn _gateway_report(
cache: &NodeStatusCache,
identity: &str,
) -> Result<GatewayStatusReportResponse, ErrorResponse> {
) -> AxumResult<GatewayStatusReportResponse> {
let gateway = get_gateway_bond_annotated(cache, identity).await?;
Ok(GatewayStatusReportResponse {
@@ -64,23 +55,23 @@ pub(crate) async fn _gateway_report(
pub(crate) async fn _gateway_uptime_history(
storage: &NymApiStorage,
identity: &str,
) -> Result<GatewayUptimeHistoryResponse, ErrorResponse> {
) -> AxumResult<GatewayUptimeHistoryResponse> {
storage
.get_gateway_uptime_history(identity)
.await
.map(GatewayUptimeHistoryResponse::from)
.map_err(|err| ErrorResponse::new(err.to_string(), Status::NotFound))
.map_err(AxumErrorResponse::not_found)
}
pub(crate) async fn _gateway_core_status_count(
storage: &State<NymApiStorage>,
storage: &NymApiStorage,
identity: &str,
since: Option<i64>,
) -> Result<GatewayCoreStatusResponse, ErrorResponse> {
) -> AxumResult<GatewayCoreStatusResponse> {
let count = storage
.get_core_gateway_status_count(identity, since)
.await
.map_err(|err| ErrorResponse::new(err.to_string(), Status::NotFound))?;
.map_err(AxumErrorResponse::not_found)?;
Ok(GatewayCoreStatusResponse {
identity: identity.to_string(),
@@ -91,7 +82,7 @@ pub(crate) async fn _gateway_core_status_count(
pub(crate) async fn _mixnode_report(
cache: &NodeStatusCache,
mix_id: MixId,
) -> Result<MixnodeStatusReportResponse, ErrorResponse> {
) -> AxumResult<MixnodeStatusReportResponse> {
let mixnode = get_mixnode_bond_annotated(cache, mix_id).await?;
Ok(MixnodeStatusReportResponse {
@@ -107,23 +98,23 @@ pub(crate) async fn _mixnode_report(
pub(crate) async fn _mixnode_uptime_history(
storage: &NymApiStorage,
mix_id: MixId,
) -> Result<MixnodeUptimeHistoryResponse, ErrorResponse> {
) -> AxumResult<MixnodeUptimeHistoryResponse> {
storage
.get_mixnode_uptime_history(mix_id)
.await
.map(MixnodeUptimeHistoryResponse::from)
.map_err(|err| ErrorResponse::new(err.to_string(), Status::NotFound))
.map_err(AxumErrorResponse::not_found)
}
pub(crate) async fn _mixnode_core_status_count(
storage: &State<NymApiStorage>,
storage: &NymApiStorage,
mix_id: MixId,
since: Option<i64>,
) -> Result<MixnodeCoreStatusResponse, ErrorResponse> {
) -> AxumResult<MixnodeCoreStatusResponse> {
let count = storage
.get_core_mixnode_status_count(mix_id, since)
.await
.map_err(|err| ErrorResponse::new(err.to_string(), Status::NotFound))?;
.map_err(AxumErrorResponse::not_found)?;
Ok(MixnodeCoreStatusResponse { mix_id, count })
}
@@ -138,22 +129,22 @@ pub(crate) async fn _get_mixnode_status(
}
pub(crate) async fn _get_mixnode_reward_estimation(
cache: &State<NodeStatusCache>,
validator_cache: &State<NymContractCache>,
cache: &NodeStatusCache,
validator_cache: &NymContractCache,
mix_id: MixId,
) -> Result<RewardEstimationResponse, ErrorResponse> {
) -> AxumResult<RewardEstimationResponse> {
let (mixnode, status) = cache.mixnode_details(mix_id).await;
if let Some(mixnode) = mixnode {
let reward_params = validator_cache.interval_reward_params().await;
let as_at = reward_params.timestamp();
let reward_params = reward_params
.into_inner()
.ok_or_else(|| ErrorResponse::new("server error", Status::InternalServerError))?;
.ok_or_else(AxumErrorResponse::internal)?;
let current_interval = validator_cache
.current_interval()
.await
.into_inner()
.ok_or_else(|| ErrorResponse::new("server error", Status::InternalServerError))?;
.ok_or_else(AxumErrorResponse::internal)?;
let reward_estimation = compute_reward_estimate(
&mixnode.mixnode_details,
@@ -170,31 +161,28 @@ pub(crate) async fn _get_mixnode_reward_estimation(
as_at: as_at.unix_timestamp(),
})
} else {
Err(ErrorResponse::new(
"mixnode bond not found",
Status::NotFound,
))
Err(AxumErrorResponse::not_found("mixnode bond not found"))
}
}
pub(crate) async fn _compute_mixnode_reward_estimation(
user_reward_param: ComputeRewardEstParam,
user_reward_param: &ComputeRewardEstParam,
cache: &NodeStatusCache,
validator_cache: &NymContractCache,
mix_id: MixId,
) -> Result<RewardEstimationResponse, ErrorResponse> {
) -> AxumResult<RewardEstimationResponse> {
let (mixnode, actual_status) = cache.mixnode_details(mix_id).await;
if let Some(mut mixnode) = mixnode {
let reward_params = validator_cache.interval_reward_params().await;
let as_at = reward_params.timestamp();
let reward_params = reward_params
.into_inner()
.ok_or_else(|| ErrorResponse::new("server error", Status::InternalServerError))?;
.ok_or_else(AxumErrorResponse::internal)?;
let current_interval = validator_cache
.current_interval()
.await
.into_inner()
.ok_or_else(|| ErrorResponse::new("server error", Status::InternalServerError))?;
.ok_or_else(AxumErrorResponse::internal)?;
// For these parameters we either use the provided ones, or fall back to the system ones
let performance = user_reward_param.performance.unwrap_or(mixnode.performance);
@@ -222,21 +210,20 @@ pub(crate) async fn _compute_mixnode_reward_estimation(
.profit_margin_percent = profit_margin_percent;
}
if let Some(interval_operating_cost) = user_reward_param.interval_operating_cost {
if let Some(interval_operating_cost) = &user_reward_param.interval_operating_cost {
mixnode
.mixnode_details
.rewarding_details
.cost_params
.interval_operating_cost = interval_operating_cost;
.interval_operating_cost = interval_operating_cost.clone();
}
if mixnode.mixnode_details.rewarding_details.operator
+ mixnode.mixnode_details.rewarding_details.delegates
> reward_params.interval.staking_supply
{
return Err(ErrorResponse::new(
return Err(AxumErrorResponse::unprocessable_entity(
"Pledge plus delegation too large",
Status::UnprocessableEntity,
));
}
@@ -255,10 +242,7 @@ pub(crate) async fn _compute_mixnode_reward_estimation(
as_at: as_at.unix_timestamp(),
})
} else {
Err(ErrorResponse::new(
"mixnode bond not found",
Status::NotFound,
))
Err(AxumErrorResponse::not_found("mixnode bond not found"))
}
}
@@ -266,7 +250,7 @@ pub(crate) async fn _get_mixnode_stake_saturation(
cache: &NodeStatusCache,
validator_cache: &NymContractCache,
mix_id: MixId,
) -> Result<StakeSaturationResponse, ErrorResponse> {
) -> AxumResult<StakeSaturationResponse> {
let (mixnode, _) = cache.mixnode_details(mix_id).await;
if let Some(mixnode) = mixnode {
// Recompute the stake saturation just so that we can confidently state that the `as_at`
@@ -275,7 +259,7 @@ pub(crate) async fn _get_mixnode_stake_saturation(
let as_at = reward_params.timestamp();
let rewarding_params = reward_params
.into_inner()
.ok_or_else(|| ErrorResponse::new("server error", Status::InternalServerError))?;
.ok_or_else(AxumErrorResponse::internal)?;
Ok(StakeSaturationResponse {
saturation: mixnode
@@ -289,17 +273,14 @@ pub(crate) async fn _get_mixnode_stake_saturation(
as_at: as_at.unix_timestamp(),
})
} else {
Err(ErrorResponse::new(
"mixnode bond not found",
Status::NotFound,
))
Err(AxumErrorResponse::not_found("mixnode bond not found"))
}
}
pub(crate) async fn _get_mixnode_inclusion_probability(
cache: &NodeStatusCache,
mix_id: MixId,
) -> Result<InclusionProbabilityResponse, ErrorResponse> {
) -> AxumResult<InclusionProbabilityResponse> {
cache
.inclusion_probabilities()
.await
@@ -309,13 +290,13 @@ pub(crate) async fn _get_mixnode_inclusion_probability(
in_active: p.in_active.into(),
in_reserve: p.in_reserve.into(),
})
.ok_or_else(|| ErrorResponse::new("mixnode bond not found", Status::NotFound))
.ok_or_else(|| AxumErrorResponse::not_found("mixnode bond not found"))
}
pub(crate) async fn _get_mixnode_avg_uptime(
cache: &NodeStatusCache,
mix_id: MixId,
) -> Result<UptimeResponse, ErrorResponse> {
) -> AxumResult<UptimeResponse> {
let mixnode = get_mixnode_bond_annotated(cache, mix_id).await?;
Ok(UptimeResponse {
@@ -328,7 +309,7 @@ pub(crate) async fn _get_mixnode_avg_uptime(
pub(crate) async fn _get_gateway_avg_uptime(
cache: &NodeStatusCache,
identity: &str,
) -> Result<GatewayUptimeResponse, ErrorResponse> {
) -> AxumResult<GatewayUptimeResponse> {
let gateway = get_gateway_bond_annotated(cache, identity).await?;
Ok(GatewayUptimeResponse {
@@ -340,7 +321,7 @@ pub(crate) async fn _get_gateway_avg_uptime(
pub(crate) async fn _get_mixnode_inclusion_probabilities(
cache: &NodeStatusCache,
) -> Result<AllInclusionProbabilitiesResponse, ErrorResponse> {
) -> AxumResult<AllInclusionProbabilitiesResponse> {
if let Some(prob) = cache.inclusion_probabilities().await {
let as_at = prob.timestamp();
let prob = prob.into_inner();
@@ -353,10 +334,7 @@ pub(crate) async fn _get_mixnode_inclusion_probabilities(
as_at: as_at.unix_timestamp(),
})
} else {
Err(ErrorResponse::new(
"No data available",
Status::ServiceUnavailable,
))
Err(AxumErrorResponse::service_unavailable())
}
}
@@ -0,0 +1,405 @@
// Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::node_status_api::models::RocketErrorResponse;
use crate::storage::NymApiStorage;
use crate::support::caching::Cache;
use crate::{NodeStatusCache, NymContractCache};
use cosmwasm_std::Decimal;
use nym_api_requests::models::{
AllInclusionProbabilitiesResponse, ComputeRewardEstParam, GatewayBondAnnotated,
GatewayCoreStatusResponse, GatewayStatusReportResponse, GatewayUptimeHistoryResponse,
GatewayUptimeResponse, InclusionProbabilityResponse, MixNodeBondAnnotated,
MixnodeCoreStatusResponse, MixnodeStatusReportResponse, MixnodeStatusResponse,
MixnodeUptimeHistoryResponse, RewardEstimationResponse, StakeSaturationResponse,
UptimeResponse,
};
use nym_mixnet_contract_common::{MixId, RewardedSetNodeStatus};
use rocket::http::Status;
use rocket::State;
use super::reward_estimate::compute_reward_estimate;
async fn get_gateway_bond_annotated(
cache: &NodeStatusCache,
identity: &str,
) -> Result<GatewayBondAnnotated, RocketErrorResponse> {
cache
.gateway_annotated(identity)
.await
.ok_or(RocketErrorResponse::new(
"gateway bond not found",
Status::NotFound,
))
}
async fn get_mixnode_bond_annotated(
cache: &NodeStatusCache,
mix_id: MixId,
) -> Result<MixNodeBondAnnotated, RocketErrorResponse> {
cache
.mixnode_annotated(mix_id)
.await
.ok_or(RocketErrorResponse::new(
"mixnode bond not found",
Status::NotFound,
))
}
pub(crate) async fn _gateway_report(
cache: &NodeStatusCache,
identity: &str,
) -> Result<GatewayStatusReportResponse, RocketErrorResponse> {
let gateway = get_gateway_bond_annotated(cache, identity).await?;
Ok(GatewayStatusReportResponse {
identity: gateway.identity().to_owned(),
owner: gateway.owner().to_string(),
most_recent: gateway.node_performance.most_recent.round_to_integer(),
last_hour: gateway.node_performance.last_hour.round_to_integer(),
last_day: gateway.node_performance.last_24h.round_to_integer(),
})
}
pub(crate) async fn _gateway_uptime_history(
storage: &NymApiStorage,
identity: &str,
) -> Result<GatewayUptimeHistoryResponse, RocketErrorResponse> {
storage
.get_gateway_uptime_history(identity)
.await
.map(GatewayUptimeHistoryResponse::from)
.map_err(|err| RocketErrorResponse::new(err.to_string(), Status::NotFound))
}
pub(crate) async fn _gateway_core_status_count(
storage: &State<NymApiStorage>,
identity: &str,
since: Option<i64>,
) -> Result<GatewayCoreStatusResponse, RocketErrorResponse> {
let count = storage
.get_core_gateway_status_count(identity, since)
.await
.map_err(|err| RocketErrorResponse::new(err.to_string(), Status::NotFound))?;
Ok(GatewayCoreStatusResponse {
identity: identity.to_string(),
count,
})
}
pub(crate) async fn _mixnode_report(
cache: &NodeStatusCache,
mix_id: MixId,
) -> Result<MixnodeStatusReportResponse, RocketErrorResponse> {
let mixnode = get_mixnode_bond_annotated(cache, mix_id).await?;
Ok(MixnodeStatusReportResponse {
mix_id,
identity: mixnode.identity_key().to_owned(),
owner: mixnode.owner().to_string(),
most_recent: mixnode.node_performance.most_recent.round_to_integer(),
last_hour: mixnode.node_performance.last_hour.round_to_integer(),
last_day: mixnode.node_performance.last_24h.round_to_integer(),
})
}
pub(crate) async fn _mixnode_uptime_history(
storage: &NymApiStorage,
mix_id: MixId,
) -> Result<MixnodeUptimeHistoryResponse, RocketErrorResponse> {
storage
.get_mixnode_uptime_history(mix_id)
.await
.map(MixnodeUptimeHistoryResponse::from)
.map_err(|err| RocketErrorResponse::new(err.to_string(), Status::NotFound))
}
pub(crate) async fn _mixnode_core_status_count(
storage: &State<NymApiStorage>,
mix_id: MixId,
since: Option<i64>,
) -> Result<MixnodeCoreStatusResponse, RocketErrorResponse> {
let count = storage
.get_core_mixnode_status_count(mix_id, since)
.await
.map_err(|err| RocketErrorResponse::new(err.to_string(), Status::NotFound))?;
Ok(MixnodeCoreStatusResponse { mix_id, count })
}
pub(crate) async fn _get_mixnode_status(
cache: &NymContractCache,
mix_id: MixId,
) -> MixnodeStatusResponse {
MixnodeStatusResponse {
status: cache.mixnode_status(mix_id).await,
}
}
pub(crate) async fn _get_mixnode_reward_estimation(
cache: &State<NodeStatusCache>,
validator_cache: &State<NymContractCache>,
mix_id: MixId,
) -> Result<RewardEstimationResponse, RocketErrorResponse> {
let (mixnode, status) = cache.mixnode_details(mix_id).await;
if let Some(mixnode) = mixnode {
let reward_params = validator_cache.interval_reward_params().await;
let as_at = reward_params.timestamp();
let reward_params = reward_params
.into_inner()
.ok_or_else(|| RocketErrorResponse::new("server error", Status::InternalServerError))?;
let current_interval = validator_cache
.current_interval()
.await
.into_inner()
.ok_or_else(|| RocketErrorResponse::new("server error", Status::InternalServerError))?;
let reward_estimation = compute_reward_estimate(
&mixnode.mixnode_details,
mixnode.performance,
status.into(),
reward_params,
current_interval,
);
Ok(RewardEstimationResponse {
estimation: reward_estimation,
reward_params,
epoch: current_interval,
as_at: as_at.unix_timestamp(),
})
} else {
Err(RocketErrorResponse::new(
"mixnode bond not found",
Status::NotFound,
))
}
}
pub(crate) async fn _compute_mixnode_reward_estimation(
user_reward_param: ComputeRewardEstParam,
cache: &NodeStatusCache,
validator_cache: &NymContractCache,
mix_id: MixId,
) -> Result<RewardEstimationResponse, RocketErrorResponse> {
let (mixnode, actual_status) = cache.mixnode_details(mix_id).await;
if let Some(mut mixnode) = mixnode {
let reward_params = validator_cache.interval_reward_params().await;
let as_at = reward_params.timestamp();
let reward_params = reward_params
.into_inner()
.ok_or_else(|| RocketErrorResponse::new("server error", Status::InternalServerError))?;
let current_interval = validator_cache
.current_interval()
.await
.into_inner()
.ok_or_else(|| RocketErrorResponse::new("server error", Status::InternalServerError))?;
// For these parameters we either use the provided ones, or fall back to the system ones
let performance = user_reward_param.performance.unwrap_or(mixnode.performance);
let status = match user_reward_param.active_in_rewarded_set {
Some(true) => Some(RewardedSetNodeStatus::Active),
Some(false) => Some(RewardedSetNodeStatus::Standby),
None => actual_status.into(),
};
if let Some(pledge_amount) = user_reward_param.pledge_amount {
mixnode.mixnode_details.rewarding_details.operator =
Decimal::from_ratio(pledge_amount, 1u64);
}
if let Some(total_delegation) = user_reward_param.total_delegation {
mixnode.mixnode_details.rewarding_details.delegates =
Decimal::from_ratio(total_delegation, 1u64);
}
if let Some(profit_margin_percent) = user_reward_param.profit_margin_percent {
mixnode
.mixnode_details
.rewarding_details
.cost_params
.profit_margin_percent = profit_margin_percent;
}
if let Some(interval_operating_cost) = user_reward_param.interval_operating_cost {
mixnode
.mixnode_details
.rewarding_details
.cost_params
.interval_operating_cost = interval_operating_cost;
}
if mixnode.mixnode_details.rewarding_details.operator
+ mixnode.mixnode_details.rewarding_details.delegates
> reward_params.interval.staking_supply
{
return Err(RocketErrorResponse::new(
"Pledge plus delegation too large",
Status::UnprocessableEntity,
));
}
let reward_estimation = compute_reward_estimate(
&mixnode.mixnode_details,
performance,
status,
reward_params,
current_interval,
);
Ok(RewardEstimationResponse {
estimation: reward_estimation,
reward_params,
epoch: current_interval,
as_at: as_at.unix_timestamp(),
})
} else {
Err(RocketErrorResponse::new(
"mixnode bond not found",
Status::NotFound,
))
}
}
pub(crate) async fn _get_mixnode_stake_saturation(
cache: &NodeStatusCache,
validator_cache: &NymContractCache,
mix_id: MixId,
) -> Result<StakeSaturationResponse, RocketErrorResponse> {
let (mixnode, _) = cache.mixnode_details(mix_id).await;
if let Some(mixnode) = mixnode {
// Recompute the stake saturation just so that we can confidently state that the `as_at`
// field is consistent and correct. Luckily this is very cheap.
let reward_params = validator_cache.interval_reward_params().await;
let as_at = reward_params.timestamp();
let rewarding_params = reward_params
.into_inner()
.ok_or_else(|| RocketErrorResponse::new("server error", Status::InternalServerError))?;
Ok(StakeSaturationResponse {
saturation: mixnode
.mixnode_details
.rewarding_details
.bond_saturation(&rewarding_params),
uncapped_saturation: mixnode
.mixnode_details
.rewarding_details
.uncapped_bond_saturation(&rewarding_params),
as_at: as_at.unix_timestamp(),
})
} else {
Err(RocketErrorResponse::new(
"mixnode bond not found",
Status::NotFound,
))
}
}
pub(crate) async fn _get_mixnode_inclusion_probability(
cache: &NodeStatusCache,
mix_id: MixId,
) -> Result<InclusionProbabilityResponse, RocketErrorResponse> {
cache
.inclusion_probabilities()
.await
.map(Cache::into_inner)
.and_then(|p| p.node(mix_id).cloned())
.map(|p| InclusionProbabilityResponse {
in_active: p.in_active.into(),
in_reserve: p.in_reserve.into(),
})
.ok_or_else(|| RocketErrorResponse::new("mixnode bond not found", Status::NotFound))
}
pub(crate) async fn _get_mixnode_avg_uptime(
cache: &NodeStatusCache,
mix_id: MixId,
) -> Result<UptimeResponse, RocketErrorResponse> {
let mixnode = get_mixnode_bond_annotated(cache, mix_id).await?;
Ok(UptimeResponse {
mix_id,
avg_uptime: mixnode.node_performance.last_24h.round_to_integer(),
node_performance: mixnode.node_performance,
})
}
pub(crate) async fn _get_gateway_avg_uptime(
cache: &NodeStatusCache,
identity: &str,
) -> Result<GatewayUptimeResponse, RocketErrorResponse> {
let gateway = get_gateway_bond_annotated(cache, identity).await?;
Ok(GatewayUptimeResponse {
identity: identity.to_string(),
avg_uptime: gateway.node_performance.last_24h.round_to_integer(),
node_performance: gateway.node_performance,
})
}
pub(crate) async fn _get_mixnode_inclusion_probabilities(
cache: &NodeStatusCache,
) -> Result<AllInclusionProbabilitiesResponse, RocketErrorResponse> {
if let Some(prob) = cache.inclusion_probabilities().await {
let as_at = prob.timestamp();
let prob = prob.into_inner();
Ok(AllInclusionProbabilitiesResponse {
inclusion_probabilities: prob.inclusion_probabilities,
samples: prob.samples,
elapsed: prob.elapsed,
delta_max: prob.delta_max,
delta_l2: prob.delta_l2,
as_at: as_at.unix_timestamp(),
})
} else {
Err(RocketErrorResponse::new(
"No data available",
Status::ServiceUnavailable,
))
}
}
pub(crate) async fn _get_mixnodes_detailed(cache: &NodeStatusCache) -> Vec<MixNodeBondAnnotated> {
cache
.mixnodes_annotated_filtered()
.await
.unwrap_or_default()
}
pub(crate) async fn _get_mixnodes_detailed_unfiltered(
cache: &NodeStatusCache,
) -> Vec<MixNodeBondAnnotated> {
cache.mixnodes_annotated_full().await.unwrap_or_default()
}
pub(crate) async fn _get_rewarded_set_detailed(
cache: &NodeStatusCache,
) -> Vec<MixNodeBondAnnotated> {
cache
.rewarded_set_annotated()
.await
.unwrap_or_default()
.into_inner()
}
pub(crate) async fn _get_active_set_detailed(cache: &NodeStatusCache) -> Vec<MixNodeBondAnnotated> {
cache
.active_set_annotated()
.await
.unwrap_or_default()
.into_inner()
}
pub(crate) async fn _get_gateways_detailed(cache: &NodeStatusCache) -> Vec<GatewayBondAnnotated> {
cache
.gateways_annotated_filtered()
.await
.unwrap_or_default()
}
pub(crate) async fn _get_gateways_detailed_unfiltered(
cache: &NodeStatusCache,
) -> Vec<GatewayBondAnnotated> {
cache.gateways_annotated_full().await.unwrap_or_default()
}
+38 -34
View File
@@ -15,10 +15,14 @@ use rocket_okapi::{openapi_get_routes_spec, settings::OpenApiSettings};
use std::time::Duration;
pub(crate) mod cache;
#[cfg(feature = "axum")]
pub(crate) mod handlers;
#[cfg(feature = "axum")]
pub(crate) mod helpers;
pub(crate) mod helpers_deprecated;
pub(crate) mod models;
pub(crate) mod reward_estimate;
pub(crate) mod routes;
pub(crate) mod routes_deprecated;
pub(crate) mod uptime_updater;
pub(crate) mod utils;
@@ -32,42 +36,42 @@ pub(crate) fn node_status_routes(
) -> (Vec<Route>, OpenApi) {
if enabled {
openapi_get_routes_spec![
settings: routes::gateway_report,
routes::gateway_uptime_history,
routes::gateway_core_status_count,
routes::mixnode_report,
routes::mixnode_uptime_history,
routes::mixnode_core_status_count,
routes::get_mixnode_status,
routes::get_mixnode_reward_estimation,
routes::compute_mixnode_reward_estimation,
routes::get_mixnode_stake_saturation,
routes::get_mixnode_inclusion_probability,
routes::get_mixnode_avg_uptime,
routes::get_gateway_avg_uptime,
routes::get_mixnode_inclusion_probabilities,
routes::get_mixnodes_detailed,
routes::get_mixnodes_detailed_unfiltered,
routes::get_rewarded_set_detailed,
routes::get_active_set_detailed,
routes::get_gateways_detailed,
routes::get_gateways_detailed_unfiltered,
routes::unstable::mixnode_test_results,
routes::unstable::gateway_test_results,
routes::submit_gateway_monitoring_results,
routes::submit_node_monitoring_results,
settings: routes_deprecated::gateway_report,
routes_deprecated::gateway_uptime_history,
routes_deprecated::gateway_core_status_count,
routes_deprecated::mixnode_report,
routes_deprecated::mixnode_uptime_history,
routes_deprecated::mixnode_core_status_count,
routes_deprecated::get_mixnode_status,
routes_deprecated::get_mixnode_reward_estimation,
routes_deprecated::compute_mixnode_reward_estimation,
routes_deprecated::get_mixnode_stake_saturation,
routes_deprecated::get_mixnode_inclusion_probability,
routes_deprecated::get_mixnode_avg_uptime,
routes_deprecated::get_gateway_avg_uptime,
routes_deprecated::get_mixnode_inclusion_probabilities,
routes_deprecated::get_mixnodes_detailed,
routes_deprecated::get_mixnodes_detailed_unfiltered,
routes_deprecated::get_rewarded_set_detailed,
routes_deprecated::get_active_set_detailed,
routes_deprecated::get_gateways_detailed,
routes_deprecated::get_gateways_detailed_unfiltered,
routes_deprecated::unstable::mixnode_test_results,
routes_deprecated::unstable::gateway_test_results,
routes_deprecated::submit_gateway_monitoring_results,
routes_deprecated::submit_node_monitoring_results,
]
} else {
// in the minimal variant we would not have access to endpoints relying on existence
// of the network monitor and the associated storage
openapi_get_routes_spec![
settings: routes::get_mixnode_status,
routes::get_mixnode_stake_saturation,
routes::get_mixnode_inclusion_probability,
routes::get_mixnode_inclusion_probabilities,
routes::get_mixnodes_detailed,
routes::get_rewarded_set_detailed,
routes::get_active_set_detailed,
settings: routes_deprecated::get_mixnode_status,
routes_deprecated::get_mixnode_stake_saturation,
routes_deprecated::get_mixnode_inclusion_probability,
routes_deprecated::get_mixnode_inclusion_probabilities,
routes_deprecated::get_mixnodes_detailed,
routes_deprecated::get_rewarded_set_detailed,
routes_deprecated::get_active_set_detailed,
]
}
}
@@ -80,7 +84,7 @@ pub(crate) fn start_cache_refresh(
config: &config::NodeStatusAPI,
nym_contract_cache_state: &NymContractCache,
node_status_cache_state: &NodeStatusCache,
storage: Option<&storage::NymApiStorage>,
storage: storage::NymApiStorage,
nym_contract_cache_listener: tokio::sync::watch::Receiver<support::caching::CacheNotification>,
shutdown: &TaskManager,
) {
@@ -89,7 +93,7 @@ pub(crate) fn start_cache_refresh(
config.debug.caching_interval,
nym_contract_cache_state.to_owned(),
nym_contract_cache_listener,
storage.cloned(),
storage,
);
let shutdown_listener = shutdown.subscribe();
tokio::spawn(async move { nym_api_cache_refresher.run(shutdown_listener).await });
+104 -6
View File
@@ -330,21 +330,22 @@ impl From<HistoricalUptime> for HistoricalUptimeResponse {
}
}
pub(crate) struct ErrorResponse {
#[deprecated(note = "TODO rocket remove once Rocket is phased out")]
pub(crate) struct RocketErrorResponse {
error_message: RequestError,
status: Status,
}
impl ErrorResponse {
impl RocketErrorResponse {
pub(crate) fn new(error_message: impl Into<String>, status: Status) -> Self {
ErrorResponse {
RocketErrorResponse {
error_message: RequestError::new(error_message),
status,
}
}
}
impl<'r, 'o: 'r> Responder<'r, 'o> for ErrorResponse {
impl<'r, 'o: 'r> Responder<'r, 'o> for RocketErrorResponse {
fn respond_to(self, req: &'r Request<'_>) -> response::Result<'o> {
// piggyback on the existing implementation
// also prefer json over plain for ease of use in frontend
@@ -355,7 +356,7 @@ impl<'r, 'o: 'r> Responder<'r, 'o> for ErrorResponse {
}
}
impl JsonSchema for ErrorResponse {
impl JsonSchema for RocketErrorResponse {
fn schema_name() -> String {
"ErrorResponse".to_owned()
}
@@ -384,7 +385,7 @@ impl JsonSchema for ErrorResponse {
}
}
impl OpenApiResponderInner for ErrorResponse {
impl OpenApiResponderInner for RocketErrorResponse {
fn responses(_gen: &mut OpenApiGenerator) -> rocket_okapi::Result<Responses> {
let mut responses = Responses::default();
ensure_status_code_exists(&mut responses, 404);
@@ -392,6 +393,103 @@ impl OpenApiResponderInner for ErrorResponse {
}
}
#[cfg(feature = "axum")]
pub(crate) use axum_error::{AxumErrorResponse, AxumResult};
#[cfg(feature = "axum")]
/// TODO rocket: extract types from this module when axum becomes the only server in Nym API
mod axum_error {
pub use super::*;
use crate::ecash::error::{EcashError, RedemptionError};
use std::fmt::Display;
// TODO rocket remove smurf name after eliminating `rocket`
pub(crate) type AxumResult<T> = Result<T, AxumErrorResponse>;
pub(crate) struct AxumErrorResponse {
message: RequestError,
status: axum::http::StatusCode,
}
impl AxumErrorResponse {
pub(crate) fn internal_msg(msg: impl Display) -> Self {
Self {
message: RequestError::new(msg.to_string()),
status: axum::http::StatusCode::INTERNAL_SERVER_ERROR,
}
}
pub(crate) fn internal() -> Self {
Self {
message: RequestError::new("Internal server error"),
status: axum::http::StatusCode::INTERNAL_SERVER_ERROR,
}
}
pub(crate) fn not_implemented() -> Self {
Self {
message: RequestError::empty(),
status: axum::http::StatusCode::NOT_IMPLEMENTED,
}
}
pub(crate) fn not_found(msg: impl Display) -> Self {
Self {
message: RequestError::new(msg.to_string()),
status: axum::http::StatusCode::NOT_FOUND,
}
}
pub(crate) fn service_unavailable() -> Self {
Self {
message: RequestError::empty(),
status: axum::http::StatusCode::SERVICE_UNAVAILABLE,
}
}
pub(crate) fn unprocessable_entity(msg: impl Display) -> Self {
Self {
message: RequestError::new(msg.to_string()),
status: axum::http::StatusCode::UNPROCESSABLE_ENTITY,
}
}
}
impl axum::response::IntoResponse for AxumErrorResponse {
fn into_response(self) -> axum::response::Response {
(self.status, self.message.message().to_string()).into_response()
}
}
impl From<NymApiStorageError> for AxumErrorResponse {
fn from(value: NymApiStorageError) -> Self {
error!("{value}");
Self {
message: RequestError::empty(),
status: axum::http::StatusCode::INTERNAL_SERVER_ERROR,
}
}
}
impl From<EcashError> for AxumErrorResponse {
fn from(value: EcashError) -> Self {
Self {
message: RequestError::new(value.to_string()),
status: axum::http::StatusCode::BAD_REQUEST,
}
}
}
#[cfg(feature = "axum")]
impl From<RedemptionError> for AxumErrorResponse {
fn from(value: RedemptionError) -> Self {
Self {
message: RequestError::new(value.to_string()),
status: axum::http::StatusCode::BAD_REQUEST,
}
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum NymApiStorageError {
#[error("could not find status report associated with mixnode {mix_id}")]
@@ -15,9 +15,9 @@ use rocket::serde::json::Json;
use rocket::State;
use rocket_okapi::openapi;
use super::helpers::_get_gateways_detailed;
use super::helpers_deprecated::_get_gateways_detailed;
use super::NodeStatusCache;
use crate::node_status_api::helpers::{
use crate::node_status_api::helpers_deprecated::{
_compute_mixnode_reward_estimation, _gateway_core_status_count, _gateway_report,
_gateway_uptime_history, _get_active_set_detailed, _get_gateway_avg_uptime,
_get_gateways_detailed_unfiltered, _get_mixnode_avg_uptime,
@@ -26,7 +26,7 @@ use crate::node_status_api::helpers::{
_get_mixnodes_detailed, _get_mixnodes_detailed_unfiltered, _get_rewarded_set_detailed,
_mixnode_core_status_count, _mixnode_report, _mixnode_uptime_history,
};
use crate::node_status_api::models::ErrorResponse;
use crate::node_status_api::models::RocketErrorResponse;
use crate::storage::NymApiStorage;
use crate::NymContractCache;
@@ -35,23 +35,23 @@ use crate::NymContractCache;
pub(crate) async fn submit_gateway_monitoring_results(
message: Json<MonitorMessage>,
storage: &State<NymApiStorage>,
) -> Result<(), ErrorResponse> {
) -> Result<(), RocketErrorResponse> {
if !message.from_allowed() {
return Err(ErrorResponse::new(
return Err(RocketErrorResponse::new(
"Monitor not registered to submit results".to_string(),
rocket::http::Status::Forbidden,
));
}
if !message.timely() {
return Err(ErrorResponse::new(
return Err(RocketErrorResponse::new(
"Message is too old".to_string(),
rocket::http::Status::BadRequest,
));
}
if !message.verify() {
return Err(ErrorResponse::new(
return Err(RocketErrorResponse::new(
"Invalid signature".to_string(),
rocket::http::Status::BadRequest,
));
@@ -65,7 +65,7 @@ pub(crate) async fn submit_gateway_monitoring_results(
Ok(_) => Ok(()),
Err(err) => {
error!("failed to submit gateway monitoring results: {}", err);
Err(ErrorResponse::new(
Err(RocketErrorResponse::new(
"failed to submit gateway monitoring results".to_string(),
rocket::http::Status::InternalServerError,
))
@@ -78,23 +78,23 @@ pub(crate) async fn submit_gateway_monitoring_results(
pub(crate) async fn submit_node_monitoring_results(
message: Json<MonitorMessage>,
storage: &State<NymApiStorage>,
) -> Result<(), ErrorResponse> {
) -> Result<(), RocketErrorResponse> {
if !message.from_allowed() {
return Err(ErrorResponse::new(
return Err(RocketErrorResponse::new(
"Monitor not registered to submit results".to_string(),
rocket::http::Status::Forbidden,
));
}
if !message.timely() {
return Err(ErrorResponse::new(
return Err(RocketErrorResponse::new(
"Message is too old".to_string(),
rocket::http::Status::BadRequest,
));
}
if !message.verify() {
return Err(ErrorResponse::new(
return Err(RocketErrorResponse::new(
"Invalid signature".to_string(),
rocket::http::Status::BadRequest,
));
@@ -108,7 +108,7 @@ pub(crate) async fn submit_node_monitoring_results(
Ok(_) => Ok(()),
Err(err) => {
error!("failed to submit node monitoring results: {}", err);
Err(ErrorResponse::new(
Err(RocketErrorResponse::new(
"failed to submit node monitoring results".to_string(),
rocket::http::Status::InternalServerError,
))
@@ -121,7 +121,7 @@ pub(crate) async fn submit_node_monitoring_results(
pub(crate) async fn gateway_report(
cache: &State<NodeStatusCache>,
identity: &str,
) -> Result<Json<GatewayStatusReportResponse>, ErrorResponse> {
) -> Result<Json<GatewayStatusReportResponse>, RocketErrorResponse> {
Ok(Json(_gateway_report(cache, identity).await?))
}
@@ -130,7 +130,7 @@ pub(crate) async fn gateway_report(
pub(crate) async fn gateway_uptime_history(
storage: &State<NymApiStorage>,
identity: &str,
) -> Result<Json<GatewayUptimeHistoryResponse>, ErrorResponse> {
) -> Result<Json<GatewayUptimeHistoryResponse>, RocketErrorResponse> {
Ok(Json(_gateway_uptime_history(storage, identity).await?))
}
@@ -140,7 +140,7 @@ pub(crate) async fn gateway_core_status_count(
storage: &State<NymApiStorage>,
identity: &str,
since: Option<i64>,
) -> Result<Json<GatewayCoreStatusResponse>, ErrorResponse> {
) -> Result<Json<GatewayCoreStatusResponse>, RocketErrorResponse> {
Ok(Json(
_gateway_core_status_count(storage, identity, since).await?,
))
@@ -151,7 +151,7 @@ pub(crate) async fn gateway_core_status_count(
pub(crate) async fn mixnode_report(
cache: &State<NodeStatusCache>,
mix_id: MixId,
) -> Result<Json<MixnodeStatusReportResponse>, ErrorResponse> {
) -> Result<Json<MixnodeStatusReportResponse>, RocketErrorResponse> {
Ok(Json(_mixnode_report(cache, mix_id).await?))
}
@@ -160,7 +160,7 @@ pub(crate) async fn mixnode_report(
pub(crate) async fn mixnode_uptime_history(
storage: &State<NymApiStorage>,
mix_id: MixId,
) -> Result<Json<MixnodeUptimeHistoryResponse>, ErrorResponse> {
) -> Result<Json<MixnodeUptimeHistoryResponse>, RocketErrorResponse> {
Ok(Json(_mixnode_uptime_history(storage, mix_id).await?))
}
@@ -170,7 +170,7 @@ pub(crate) async fn mixnode_core_status_count(
storage: &State<NymApiStorage>,
mix_id: MixId,
since: Option<i64>,
) -> Result<Json<MixnodeCoreStatusResponse>, ErrorResponse> {
) -> Result<Json<MixnodeCoreStatusResponse>, RocketErrorResponse> {
Ok(Json(
_mixnode_core_status_count(storage, mix_id, since).await?,
))
@@ -191,7 +191,7 @@ pub(crate) async fn get_mixnode_reward_estimation(
cache: &State<NodeStatusCache>,
validator_cache: &State<NymContractCache>,
mix_id: MixId,
) -> Result<Json<RewardEstimationResponse>, ErrorResponse> {
) -> Result<Json<RewardEstimationResponse>, RocketErrorResponse> {
Ok(Json(
_get_mixnode_reward_estimation(cache, validator_cache, mix_id).await?,
))
@@ -207,7 +207,7 @@ pub(crate) async fn compute_mixnode_reward_estimation(
cache: &State<NodeStatusCache>,
validator_cache: &State<NymContractCache>,
mix_id: MixId,
) -> Result<Json<RewardEstimationResponse>, ErrorResponse> {
) -> Result<Json<RewardEstimationResponse>, RocketErrorResponse> {
Ok(Json(
_compute_mixnode_reward_estimation(
user_reward_param.into_inner(),
@@ -225,7 +225,7 @@ pub(crate) async fn get_mixnode_stake_saturation(
cache: &State<NodeStatusCache>,
validator_cache: &State<NymContractCache>,
mix_id: MixId,
) -> Result<Json<StakeSaturationResponse>, ErrorResponse> {
) -> Result<Json<StakeSaturationResponse>, RocketErrorResponse> {
Ok(Json(
_get_mixnode_stake_saturation(cache, validator_cache, mix_id).await?,
))
@@ -236,7 +236,7 @@ pub(crate) async fn get_mixnode_stake_saturation(
pub(crate) async fn get_mixnode_inclusion_probability(
cache: &State<NodeStatusCache>,
mix_id: MixId,
) -> Result<Json<InclusionProbabilityResponse>, ErrorResponse> {
) -> Result<Json<InclusionProbabilityResponse>, RocketErrorResponse> {
Ok(Json(
_get_mixnode_inclusion_probability(cache, mix_id).await?,
))
@@ -247,7 +247,7 @@ pub(crate) async fn get_mixnode_inclusion_probability(
pub(crate) async fn get_mixnode_avg_uptime(
cache: &State<NodeStatusCache>,
mix_id: MixId,
) -> Result<Json<UptimeResponse>, ErrorResponse> {
) -> Result<Json<UptimeResponse>, RocketErrorResponse> {
Ok(Json(_get_mixnode_avg_uptime(cache, mix_id).await?))
}
@@ -256,7 +256,7 @@ pub(crate) async fn get_mixnode_avg_uptime(
pub(crate) async fn get_gateway_avg_uptime(
cache: &State<NodeStatusCache>,
identity: &str,
) -> Result<Json<GatewayUptimeResponse>, ErrorResponse> {
) -> Result<Json<GatewayUptimeResponse>, RocketErrorResponse> {
Ok(Json(_get_gateway_avg_uptime(cache, identity).await?))
}
@@ -264,7 +264,7 @@ pub(crate) async fn get_gateway_avg_uptime(
#[get("/mixnodes/inclusion_probability")]
pub(crate) async fn get_mixnode_inclusion_probabilities(
cache: &State<NodeStatusCache>,
) -> Result<Json<AllInclusionProbabilitiesResponse>, ErrorResponse> {
) -> Result<Json<AllInclusionProbabilitiesResponse>, RocketErrorResponse> {
Ok(Json(_get_mixnode_inclusion_probabilities(cache).await?))
}
@@ -317,7 +317,7 @@ pub async fn get_gateways_detailed_unfiltered(
}
pub mod unstable {
use crate::node_status_api::models::ErrorResponse;
use crate::node_status_api::models::RocketErrorResponse;
use crate::support::http::helpers::PaginationRequest;
use crate::support::storage::NymApiStorage;
use nym_api_requests::models::{
@@ -486,7 +486,7 @@ pub mod unstable {
pagination: PaginationRequest,
info_cache: &State<NodeInfoCache>,
storage: &State<NymApiStorage>,
) -> Result<Json<MixnodeTestResultResponse>, ErrorResponse> {
) -> Result<Json<MixnodeTestResultResponse>, RocketErrorResponse> {
let page = pagination.page.unwrap_or_default();
let per_page = min(
pagination
@@ -497,7 +497,7 @@ pub mod unstable {
match _mixnode_test_results(mix_id, page, per_page, info_cache, storage).await {
Ok(res) => Ok(Json(res)),
Err(err) => Err(ErrorResponse::new(
Err(err) => Err(RocketErrorResponse::new(
format!("failed to retrieve mixnode test results for node {mix_id}: {err}"),
Status::InternalServerError,
)),
@@ -570,7 +570,7 @@ pub mod unstable {
pagination: PaginationRequest,
info_cache: &State<NodeInfoCache>,
storage: &State<NymApiStorage>,
) -> Result<Json<GatewayTestResultResponse>, ErrorResponse> {
) -> Result<Json<GatewayTestResultResponse>, RocketErrorResponse> {
let page = pagination.page.unwrap_or_default();
let per_page = min(
pagination
@@ -581,7 +581,7 @@ pub mod unstable {
match _gateway_test_results(gateway_identity, page, per_page, info_cache, storage).await {
Ok(res) => Ok(Json(res)),
Err(err) => Err(ErrorResponse::new(
Err(err) => Err(RocketErrorResponse::new(
format!(
"failed to retrieve mixnode test results for gateway {gateway_identity}: {err}"
),
@@ -118,8 +118,8 @@ impl HistoricalUptimeUpdater {
}
}
pub(crate) fn start(storage: &NymApiStorage, shutdown: &TaskManager) {
let uptime_updater = HistoricalUptimeUpdater::new(storage.to_owned());
pub(crate) fn start(storage: NymApiStorage, shutdown: &TaskManager) {
let uptime_updater = HistoricalUptimeUpdater::new(storage);
let shutdown_listener = shutdown.subscribe();
tokio::spawn(async move { uptime_updater.run(shutdown_listener).await });
}