Skip to content

Commit f478509

Browse files
authored
Add nodes/delegations endpoint (#5733)
* WIP * Add /delegations endpoint * Bump package version * Remove node_id field
1 parent b04d3ba commit f478509

File tree

10 files changed

+225
-58
lines changed

10 files changed

+225
-58
lines changed

Cargo.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

nym-node-status-api/nym-node-status-api/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
[package]
55
name = "nym-node-status-api"
6-
version = "2.2.0"
6+
version = "2.3.0"
77
authors.workspace = true
88
repository.workspace = true
99
homepage.workspace = true

nym-node-status-api/nym-node-status-api/src/http/api/nym_nodes.rs

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,26 @@
11
use axum::{
2-
extract::{Query, State},
2+
extract::{Path, Query, State},
33
Json, Router,
44
};
5+
use nym_validator_client::client::NodeId;
6+
use serde::Deserialize;
57
use tracing::instrument;
8+
use utoipa::IntoParams;
69

710
use crate::http::{
811
error::{HttpError, HttpResult},
9-
models::ExtendedNymNode,
12+
models::{ExtendedNymNode, NodeDelegation},
1013
state::AppState,
1114
PagedResult, Pagination,
1215
};
1316

1417
pub(crate) fn routes() -> Router<AppState> {
15-
Router::new().route("/", axum::routing::get(nym_nodes))
18+
Router::new()
19+
.route("/", axum::routing::get(nym_nodes))
20+
.route(
21+
"/:node_id/delegations",
22+
axum::routing::get(node_delegations),
23+
)
1624
}
1725

1826
#[utoipa::path(
@@ -27,7 +35,7 @@ pub(crate) fn routes() -> Router<AppState> {
2735
(status = 200, body = PagedResult<ExtendedNymNode>)
2836
)
2937
)]
30-
#[instrument(level = tracing::Level::DEBUG, skip_all, fields(page=pagination.page, size=pagination.size))]
38+
#[instrument(level = tracing::Level::INFO, skip_all, fields(page=pagination.page, size=pagination.size))]
3139
async fn nym_nodes(
3240
Query(pagination): Query<Pagination>,
3341
State(state): State<AppState>,
@@ -46,3 +54,35 @@ async fn nym_nodes(
4654

4755
Ok(Json(PagedResult::paginate(pagination, nodes)))
4856
}
57+
58+
#[allow(dead_code)] // clippy doesn't detect usage in utoipa macros
59+
#[derive(Deserialize, IntoParams)]
60+
#[into_params(parameter_in = Path)]
61+
struct NodeIdParam {
62+
#[param(minimum = 0)]
63+
node_id: NodeId,
64+
}
65+
66+
#[utoipa::path(
67+
tag = "Nym Explorer",
68+
get,
69+
params(
70+
NodeIdParam
71+
),
72+
path = "/{node_id}/delegations",
73+
context_path = "/explorer/v3/nym-nodes",
74+
responses(
75+
(status = 200, body = NodeDelegation)
76+
)
77+
)]
78+
#[instrument(level = tracing::Level::INFO, skip(state))]
79+
async fn node_delegations(
80+
Path(node_id): Path<NodeId>,
81+
State(state): State<AppState>,
82+
) -> HttpResult<Json<Vec<NodeDelegation>>> {
83+
state
84+
.node_delegations(node_id)
85+
.await
86+
.ok_or_else(|| HttpError::no_delegations_for_node(node_id))
87+
.map(Json)
88+
}

nym-node-status-api/nym-node-status-api/src/http/error.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use nym_mixnet_contract_common::NodeId;
12
use std::fmt::Display;
23

34
pub(crate) type HttpResult<T> = Result<T, HttpError>;
@@ -40,6 +41,14 @@ impl HttpError {
4041
status: axum::http::StatusCode::SERVICE_UNAVAILABLE,
4142
}
4243
}
44+
45+
pub(crate) fn no_delegations_for_node(node_id: NodeId) -> Self {
46+
Self {
47+
message: serde_json::json!({"message": format!("No delegation data for node_id={}", node_id)})
48+
.to_string(),
49+
status: axum::http::StatusCode::NOT_FOUND,
50+
}
51+
}
4352
}
4453

4554
impl axum::response::IntoResponse for HttpError {

nym-node-status-api/nym-node-status-api/src/http/models.rs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
use cosmwasm_std::Decimal;
1+
use cosmwasm_std::{Addr, Coin, Decimal};
2+
use nym_mixnet_contract_common::CoinSchema;
23
use nym_node_requests::api::v1::node::models::NodeDescription;
34
use nym_validator_client::client::NodeId;
45
use serde::{Deserialize, Serialize};
@@ -120,3 +121,27 @@ pub struct SessionStats {
120121
pub mixnet_sessions: Option<serde_json::Value>,
121122
pub unknown_sessions: Option<serde_json::Value>,
122123
}
124+
125+
#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
126+
pub struct NodeDelegation {
127+
#[schema(value_type = CoinSchema)]
128+
pub amount: Coin,
129+
pub cumulative_reward_ratio: String,
130+
pub block_height: u64,
131+
#[schema(value_type = String)]
132+
pub owner: Addr,
133+
#[schema(value_type = Option<String>)]
134+
pub proxy: Option<Addr>,
135+
}
136+
137+
impl From<nym_mixnet_contract_common::Delegation> for NodeDelegation {
138+
fn from(value: nym_mixnet_contract_common::Delegation) -> Self {
139+
Self {
140+
amount: value.amount,
141+
cumulative_reward_ratio: value.cumulative_reward_ratio.to_string(),
142+
block_height: value.height,
143+
owner: value.owner,
144+
proxy: value.proxy,
145+
}
146+
}
147+
}

nym-node-status-api/nym-node-status-api/src/http/server.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
use axum::Router;
22
use core::net::SocketAddr;
33
use nym_crypto::asymmetric::ed25519::PublicKey;
4-
use tokio::{net::TcpListener, task::JoinHandle};
4+
use std::sync::Arc;
5+
use tokio::{net::TcpListener, sync::RwLock, task::JoinHandle};
56
use tokio_util::sync::{CancellationToken, WaitForCancellationFutureOwned};
67

78
use crate::{
89
db::DbPool,
910
http::{api::RouterBuilder, state::AppState},
10-
monitor::NodeGeoCache,
11+
monitor::{DelegationsCache, NodeGeoCache},
1112
};
1213

1314
/// Return handles that allow for graceful shutdown of server + awaiting its
@@ -19,6 +20,7 @@ pub(crate) async fn start_http_api(
1920
agent_key_list: Vec<PublicKey>,
2021
agent_max_count: i64,
2122
node_geocache: NodeGeoCache,
23+
node_delegations: Arc<RwLock<DelegationsCache>>,
2224
) -> anyhow::Result<ShutdownHandles> {
2325
let router_builder = RouterBuilder::with_default_routes();
2426

@@ -28,6 +30,7 @@ pub(crate) async fn start_http_api(
2830
agent_key_list,
2931
agent_max_count,
3032
node_geocache,
33+
node_delegations,
3134
)
3235
.await;
3336
let router = router_builder.with_state(state);

nym-node-status-api/nym-node-status-api/src/http/state.rs

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
1-
use std::{collections::HashMap, sync::Arc, time::Duration};
2-
31
use cosmwasm_std::Decimal;
42
use moka::{future::Cache, Entry};
53
use nym_contracts_common::NaiveFloat;
64
use nym_crypto::asymmetric::ed25519::PublicKey;
5+
use nym_mixnet_contract_common::NodeId;
76
use nym_validator_client::nym_api::SkimmedNode;
7+
use std::{collections::HashMap, sync::Arc, time::Duration};
88
use tokio::sync::RwLock;
99
use tracing::instrument;
1010

1111
use crate::{
1212
db::{queries, DbPool},
1313
http::models::{DailyStats, ExtendedNymNode, Gateway, Mixnode, NodeGeoData, SummaryHistory},
14-
monitor::NodeGeoCache,
14+
monitor::{DelegationsCache, NodeGeoCache},
1515
};
1616

1717
use super::models::SessionStats;
@@ -23,6 +23,7 @@ pub(crate) struct AppState {
2323
agent_key_list: Vec<PublicKey>,
2424
agent_max_count: i64,
2525
node_geocache: NodeGeoCache,
26+
node_delegations: Arc<RwLock<DelegationsCache>>,
2627
}
2728

2829
impl AppState {
@@ -32,13 +33,15 @@ impl AppState {
3233
agent_key_list: Vec<PublicKey>,
3334
agent_max_count: i64,
3435
node_geocache: NodeGeoCache,
36+
node_delegations: Arc<RwLock<DelegationsCache>>,
3537
) -> Self {
3638
Self {
3739
db_pool,
3840
cache: HttpCache::new(cache_ttl).await,
3941
agent_key_list,
4042
agent_max_count,
4143
node_geocache,
44+
node_delegations,
4245
}
4346
}
4447

@@ -61,6 +64,16 @@ impl AppState {
6164
pub(crate) fn node_geocache(&self) -> NodeGeoCache {
6265
self.node_geocache.clone()
6366
}
67+
68+
pub(crate) async fn node_delegations(
69+
&self,
70+
node_id: NodeId,
71+
) -> Option<Vec<super::models::NodeDelegation>> {
72+
self.node_delegations
73+
.read()
74+
.await
75+
.delegations_owned(node_id)
76+
}
6477
}
6578

6679
static GATEWAYS_LIST_KEY: &str = "gateways";

nym-node-status-api/nym-node-status-api/src/main.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
use crate::monitor::DelegationsCache;
12
use clap::Parser;
23
use nym_crypto::asymmetric::ed25519::PublicKey;
34
use nym_task::signal::wait_for_signal;
5+
use nym_validator_client::nyxd::NyxdClient;
6+
use std::sync::Arc;
47

58
mod cli;
69
mod db;
@@ -41,19 +44,27 @@ async fn main() -> anyhow::Result<()> {
4144
let geocache = moka::future::Cache::builder()
4245
.time_to_live(args.geodata_ttl)
4346
.build();
47+
let delegations_cache = DelegationsCache::new();
4448

4549
// Start the monitor
4650
let args_clone = args.clone();
4751
let geocache_clone = geocache.clone();
52+
let delegations_cache_clone = Arc::clone(&delegations_cache);
53+
let config = nym_validator_client::nyxd::Config::try_from_nym_network_details(
54+
&nym_network_defaults::NymNetworkDetails::new_from_env(),
55+
)?;
56+
let nyxd_client = NyxdClient::connect(config, args.nyxd_addr.as_str())
57+
.map_err(|err| anyhow::anyhow!("Couldn't connect: {}", err))?;
4858

4959
tokio::spawn(async move {
5060
monitor::spawn_in_background(
5161
db_pool,
5262
args_clone.nym_api_client_timeout,
53-
args_clone.nyxd_addr,
63+
nyxd_client,
5464
args_clone.monitor_refresh_interval,
5565
args_clone.ipinfo_api_token,
5666
geocache_clone,
67+
delegations_cache_clone,
5768
)
5869
.await;
5970
tracing::info!("Started monitor task");
@@ -74,6 +85,7 @@ async fn main() -> anyhow::Result<()> {
7485
agent_key_list.to_owned(),
7586
args.max_agent_count,
7687
geocache,
88+
delegations_cache,
7789
)
7890
.await
7991
.expect("Failed to start server");

0 commit comments

Comments
 (0)