1use axum::{
2 extract::{Json, Query, State},
3 http::status::StatusCode,
4 response::IntoResponse,
5};
6use futures::stream::FuturesUnordered;
7use futures::StreamExt;
8use libp2p_identity::PeerId;
9use serde::{Deserialize, Serialize};
10use serde_with::{serde_as, DisplayFromStr};
11use std::{collections::HashMap, sync::Arc};
12
13use hopr_crypto_types::prelude::Hash;
14use hopr_lib::{Address, AsUnixTimestamp, GraphExportConfig, Health, Multiaddr};
15
16use crate::{
17 checksum_address_serializer, option_checksum_address_serializer, ApiError, ApiErrorStatus, InternalState, BASE_PATH,
18};
19
20#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
21#[schema(example = json!({
22 "version": "2.1.0",
23 "apiVersion": "3.10.0"
24 }))]
25#[serde(rename_all = "camelCase")]
26pub(crate) struct NodeVersionResponse {
27 version: String,
28 api_version: String,
29}
30
31#[utoipa::path(
33 get,
34 path = const_format::formatcp!("{BASE_PATH}/node/version"),
35 responses(
36 (status = 200, description = "Fetched node version", body = NodeVersionResponse),
37 (status = 401, description = "Invalid authorization token.", body = ApiError),
38 ),
39 security(
40 ("api_token" = []),
41 ("bearer_token" = [])
42 ),
43 tag = "Node"
44 )]
45pub(super) async fn version(State(state): State<Arc<InternalState>>) -> impl IntoResponse {
46 let version = state.hopr.version();
47 let api_version = env!("CARGO_PKG_VERSION").to_owned();
48 (StatusCode::OK, Json(NodeVersionResponse { version, api_version })).into_response()
49}
50
51#[utoipa::path(
53 get,
54 path = const_format::formatcp!("{BASE_PATH}/node/configuration"),
55 responses(
56 (status = 200, description = "Fetched node configuration", body = String),
57 (status = 401, description = "Invalid authorization token.", body = ApiError),
58 ),
59 security(
60 ("api_token" = []),
61 ("bearer_token" = [])
62 ),
63 tag = "Configuration"
64 )]
65pub(super) async fn configuration(State(state): State<Arc<InternalState>>) -> impl IntoResponse {
66 (StatusCode::OK, state.hoprd_cfg.clone()).into_response()
67}
68
69#[derive(Debug, Clone, Deserialize, utoipa::ToSchema, utoipa::IntoParams)]
70#[schema(example = json!({
71 "quality": 0.7
72 }))]
73#[into_params(parameter_in = Query)]
74pub(crate) struct NodePeersQueryRequest {
75 #[schema(required = false)]
76 #[serde(default)]
77 quality: f64,
78}
79
80#[derive(Debug, Default, Clone, Serialize, utoipa::ToSchema)]
81#[schema(example = json!({
82 "sent": 10,
83 "success": 10
84}))]
85#[serde(rename_all = "camelCase")]
86pub(crate) struct HeartbeatInfo {
87 sent: u64,
88 success: u64,
89}
90
91#[serde_as]
92#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
93#[serde(rename_all = "camelCase")]
94pub(crate) struct PeerInfo {
95 #[serde_as(as = "DisplayFromStr")]
96 #[schema(value_type = String)]
97 peer_id: PeerId,
98 #[serde(serialize_with = "option_checksum_address_serializer")]
99 #[schema(value_type = Option<String>)]
100 peer_address: Option<Address>,
101 #[serde_as(as = "Option<DisplayFromStr>")]
102 #[schema(value_type = Option<String>)]
103 multiaddr: Option<Multiaddr>,
104 heartbeats: HeartbeatInfo,
105 last_seen: u128,
106 last_seen_latency: u128,
107 quality: f64,
108 backoff: f64,
109 is_new: bool,
110 reported_version: String,
111}
112
113#[serde_as]
114#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
115#[schema(example = json!({
116 "peerId": "12D3KooWRWeaTozREYHzWTbuCYskdYhED1MXpDwTrmccwzFrd2mEA",
117 "peerAddress": "0xb4ce7e6e36ac8b01a974725d5ba730af2b156fbe",
118 "multiaddr": "/ip4/178.12.1.9/tcp/19092"
119}))]
120#[serde(rename_all = "camelCase")]
121pub(crate) struct AnnouncedPeer {
122 #[serde_as(as = "DisplayFromStr")]
123 #[schema(value_type = String)]
124 peer_id: PeerId,
125 #[serde(serialize_with = "checksum_address_serializer")]
126 #[schema(value_type = String)]
127 peer_address: Address,
128 #[serde_as(as = "Option<DisplayFromStr>")]
129 #[schema(value_type = Option<String>)]
130 multiaddr: Option<Multiaddr>,
131}
132
133#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
134#[serde(rename_all = "camelCase")]
135pub(crate) struct NodePeersResponse {
136 connected: Vec<PeerInfo>,
137 announced: Vec<AnnouncedPeer>,
138}
139
140#[utoipa::path(
148 get,
149 path = const_format::formatcp!("{BASE_PATH}/node/peers"),
150 params(NodePeersQueryRequest),
151 responses(
152 (status = 200, description = "Successfully returned observed peers", body = NodePeersResponse),
153 (status = 400, description = "Failed to extract a valid quality parameter", body = ApiError),
154 (status = 401, description = "Invalid authorization token.", body = ApiError),
155 ),
156 security(
157 ("api_token" = []),
158 ("bearer_token" = [])
159 ),
160 tag = "Node"
161 )]
162pub(super) async fn peers(
163 Query(NodePeersQueryRequest { quality }): Query<NodePeersQueryRequest>,
164 State(state): State<Arc<InternalState>>,
165) -> Result<impl IntoResponse, ApiError> {
166 if !(0.0f64..=1.0f64).contains(&quality) {
167 return Ok((StatusCode::BAD_REQUEST, ApiErrorStatus::InvalidQuality).into_response());
168 }
169
170 let hopr = state.hopr.clone();
171
172 let all_network_peers = futures::stream::iter(hopr.network_connected_peers().await?)
173 .filter_map(|peer| {
174 let hopr = hopr.clone();
175
176 async move {
177 if let Ok(Some(info)) = hopr.network_peer_info(&peer).await {
178 let avg_quality = info.get_average_quality();
179 if avg_quality >= quality {
180 Some((peer, info))
181 } else {
182 None
183 }
184 } else {
185 None
186 }
187 }
188 })
189 .filter_map(|(peer_id, info)| {
190 let hopr = hopr.clone();
191
192 async move {
193 let address = hopr.peerid_to_chain_key(&peer_id).await.ok().flatten();
194
195 let multiaddresses = hopr.network_observed_multiaddresses(&peer_id).await;
197
198 Some((address, peer_id, multiaddresses, info))
199 }
200 })
201 .map(|(address, peer_id, mas, info)| PeerInfo {
202 peer_id,
203 peer_address: address,
204 multiaddr: mas.first().cloned(),
205 heartbeats: HeartbeatInfo {
206 sent: info.heartbeats_sent,
207 success: info.heartbeats_succeeded,
208 },
209 last_seen: info.last_seen.as_unix_timestamp().as_millis(),
210 last_seen_latency: info.last_seen_latency.as_millis() / 2,
211 quality: info.get_average_quality(),
212 backoff: info.backoff,
213 is_new: info.heartbeats_sent == 0u64,
214 reported_version: info.peer_version.unwrap_or("UNKNOWN".to_string()),
215 })
216 .collect::<Vec<_>>()
217 .await;
218
219 let announced_peers = hopr
220 .accounts_announced_on_chain()
221 .await?
222 .into_iter()
223 .map(|announced| async move {
224 AnnouncedPeer {
225 peer_id: announced.public_key.into(),
226 peer_address: announced.chain_addr,
227 multiaddr: announced.get_multiaddr(),
228 }
229 })
230 .collect::<FuturesUnordered<_>>()
231 .collect()
232 .await;
233
234 let body = NodePeersResponse {
235 connected: all_network_peers,
236 announced: announced_peers,
237 };
238
239 Ok((StatusCode::OK, Json(body)).into_response())
240}
241
242#[cfg(all(feature = "prometheus", not(test)))]
243use hopr_metrics::metrics::gather_all_metrics as collect_hopr_metrics;
244
245#[cfg(any(not(feature = "prometheus"), test))]
246fn collect_hopr_metrics() -> Result<String, ApiErrorStatus> {
247 Err(ApiErrorStatus::UnknownFailure("BUILT WITHOUT METRICS SUPPORT".into()))
248}
249
250#[utoipa::path(
252 get,
253 path = const_format::formatcp!("{BASE_PATH}/node/metrics"),
254 responses(
255 (status = 200, description = "Fetched node metrics", body = String),
256 (status = 401, description = "Invalid authorization token.", body = ApiError),
257 (status = 422, description = "Unknown failure", body = ApiError)
258 ),
259 security(
260 ("api_token" = []),
261 ("bearer_token" = [])
262 ),
263 tag = "Node"
264 )]
265pub(super) async fn metrics() -> impl IntoResponse {
266 match collect_hopr_metrics() {
267 Ok(metrics) => (StatusCode::OK, metrics).into_response(),
268 Err(error) => (StatusCode::UNPROCESSABLE_ENTITY, ApiErrorStatus::from(error)).into_response(),
269 }
270}
271
272#[derive(Debug, Clone, Deserialize, Default, utoipa::IntoParams, utoipa::ToSchema)]
273#[into_params(parameter_in = Query)]
274#[serde(default, rename_all = "camelCase")]
275pub(crate) struct GraphExportQuery {
276 #[schema(required = false)]
279 #[serde(default)]
280 pub ignore_disconnected_components: bool,
281 #[schema(required = false)]
283 #[serde(default)]
284 pub ignore_non_opened_channels: bool,
285 #[schema(required = false)]
287 #[serde(default)]
288 pub only_3_hop_paths: bool,
289 #[schema(required = false)]
294 #[serde(default)]
295 pub raw_graph: bool,
296}
297
298impl From<GraphExportQuery> for GraphExportConfig {
299 fn from(value: GraphExportQuery) -> Self {
300 Self {
301 ignore_disconnected_components: value.ignore_disconnected_components,
302 ignore_non_opened_channels: value.ignore_non_opened_channels,
303 only_3_hop_accessible_nodes: value.only_3_hop_paths,
304 }
305 }
306}
307
308#[utoipa::path(
310 get,
311 path = const_format::formatcp!("{BASE_PATH}/node/graph"),
312 params(GraphExportQuery),
313 responses(
314 (status = 200, description = "Fetched channel graph", body = String),
315 (status = 401, description = "Invalid authorization token.", body = ApiError),
316 ),
317 security(
318 ("api_token" = []),
319 ("bearer_token" = [])
320 ),
321 tag = "Node"
322)]
323pub(super) async fn channel_graph(
324 State(state): State<Arc<InternalState>>,
325 Query(args): Query<GraphExportQuery>,
326) -> impl IntoResponse {
327 if args.raw_graph {
328 match state.hopr.export_raw_channel_graph().await {
329 Ok(raw_graph) => (StatusCode::OK, raw_graph).into_response(),
330 Err(error) => (
331 StatusCode::UNPROCESSABLE_ENTITY,
332 ApiErrorStatus::UnknownFailure(error.to_string()),
333 )
334 .into_response(),
335 }
336 } else {
337 (StatusCode::OK, state.hopr.export_channel_graph(args.into()).await).into_response()
338 }
339}
340
341#[serde_as]
342#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
343#[schema(example = json!({
344 "announcedAddress": [
345 "/ip4/10.0.2.100/tcp/19092"
346 ],
347 "chain": "anvil-localhost",
348 "provider": "http://127.0.0.1:8545",
349 "channelClosurePeriod": 15,
350 "connectivityStatus": "Green",
351 "hoprChannels": "0x9a9f2ccfde556a7e9ff0848998aa4a0cfd8863ae",
352 "hoprManagementModule": "0xa51c1fc2f0d1a1b8494ed1fe312d7c3a78ed91c0",
353 "hoprNetworkRegistry": "0x3aa5ebb10dc797cac828524e59a333d0a371443c",
354 "hoprNodeSafe": "0x42bc901b1d040f984ed626eff550718498a6798a",
355 "hoprNodeSageRegistry": "0x0dcd1bf9a1b36ce34237eeafef220932846bcd82",
356 "hoprToken": "0x9a676e781a523b5d0c0e43731313a708cb607508",
357 "isEligible": true,
358 "listeningAddress": [
359 "/ip4/10.0.2.100/tcp/19092"
360 ],
361 "network": "anvil-localhost",
362 "indexerBlock": 123456,
363 "indexerChecksum": "0000000000000000000000000000000000000000000000000000000000000000",
364 "indexBlockPrevChecksum": 0,
365 "indexerLastLogBlock": 123450,
366 "indexerLastLogChecksum": "cfde556a7e9ff0848998aa4a9a9f2ccfde556a7e9ff0848998aa4a0cfd8863ae",
367 }))]
368#[serde(rename_all = "camelCase")]
369pub(crate) struct NodeInfoResponse {
370 network: String,
371 #[serde_as(as = "Vec<DisplayFromStr>")]
372 #[schema(value_type = Vec<String>)]
373 announced_address: Vec<Multiaddr>,
374 #[serde_as(as = "Vec<DisplayFromStr>")]
375 #[schema(value_type = Vec<String>)]
376 listening_address: Vec<Multiaddr>,
377 chain: String,
378 provider: String,
379 #[serde(serialize_with = "checksum_address_serializer")]
380 #[schema(value_type = String)]
381 hopr_token: Address,
382 #[serde(serialize_with = "checksum_address_serializer")]
383 #[schema(value_type = String)]
384 hopr_channels: Address,
385 #[serde(serialize_with = "checksum_address_serializer")]
386 #[schema(value_type = String)]
387 hopr_network_registry: Address,
388 #[serde(serialize_with = "checksum_address_serializer")]
389 #[schema(value_type = String)]
390 hopr_node_safe_registry: Address,
391 #[serde(serialize_with = "checksum_address_serializer")]
392 #[schema(value_type = String)]
393 hopr_management_module: Address,
394 #[serde(serialize_with = "checksum_address_serializer")]
395 #[schema(value_type = String)]
396 hopr_node_safe: Address,
397 is_eligible: bool,
398 #[serde_as(as = "DisplayFromStr")]
399 #[schema(value_type = String)]
400 connectivity_status: Health,
401 channel_closure_period: u64,
403 indexer_block: u32,
404 indexer_last_log_block: u32,
405 #[serde_as(as = "DisplayFromStr")]
406 #[schema(value_type = String)]
407 indexer_last_log_checksum: Hash,
408}
409
410#[utoipa::path(
412 get,
413 path = const_format::formatcp!("{BASE_PATH}/node/info"),
414 responses(
415 (status = 200, description = "Fetched node version", body = NodeInfoResponse),
416 (status = 422, description = "Unknown failure", body = ApiError)
417 ),
418 security(
419 ("api_token" = []),
420 ("bearer_token" = [])
421 ),
422 tag = "Node"
423 )]
424pub(super) async fn info(State(state): State<Arc<InternalState>>) -> Result<impl IntoResponse, ApiError> {
425 let hopr = state.hopr.clone();
426
427 let chain_config = hopr.chain_config();
428 let safe_config = hopr.get_safe_config();
429 let network = hopr.network();
430 let me_address = hopr.me_onchain();
431
432 let indexer_state_info = match hopr.get_indexer_state().await {
433 Ok(info) => info,
434 Err(error) => return Ok((StatusCode::UNPROCESSABLE_ENTITY, ApiErrorStatus::from(error)).into_response()),
435 };
436
437 let is_eligible = hopr.is_allowed_to_access_network(either::Right(me_address)).await?;
438
439 match hopr.get_channel_closure_notice_period().await {
440 Ok(channel_closure_notice_period) => {
441 let body = NodeInfoResponse {
442 network,
443 announced_address: hopr.local_multiaddresses(),
444 listening_address: hopr.local_multiaddresses(),
445 chain: chain_config.id,
446 provider: hopr.get_provider(),
447 hopr_token: chain_config.token,
448 hopr_channels: chain_config.channels,
449 hopr_network_registry: chain_config.network_registry,
450 hopr_node_safe_registry: chain_config.node_safe_registry,
451 hopr_management_module: chain_config.module_implementation,
452 hopr_node_safe: safe_config.safe_address,
453 is_eligible,
454 connectivity_status: hopr.network_health().await,
455 channel_closure_period: channel_closure_notice_period.as_secs(),
456 indexer_block: indexer_state_info.latest_block_number,
457 indexer_last_log_block: indexer_state_info.latest_log_block_number,
458 indexer_last_log_checksum: indexer_state_info.latest_log_checksum,
459 };
460
461 Ok((StatusCode::OK, Json(body)).into_response())
462 }
463 Err(error) => Ok((StatusCode::UNPROCESSABLE_ENTITY, ApiErrorStatus::from(error)).into_response()),
464 }
465}
466
467#[serde_as]
468#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
469#[serde(rename_all = "camelCase")]
470pub(crate) struct EntryNode {
471 #[serde_as(as = "Vec<DisplayFromStr>")]
472 #[schema(value_type = Vec<String>)]
473 multiaddrs: Vec<Multiaddr>,
474 is_eligible: bool,
475}
476
477#[utoipa::path(
479 get,
480 path = const_format::formatcp!("{BASE_PATH}/node/entryNodes"),
481 responses(
482 (status = 200, description = "Fetched public nodes' information", body = HashMap<String, EntryNode>, example = json!({
483 "0x188c4462b75e46f0c7262d7f48d182447b93a93c": {
484 "isElligible": true,
485 "multiaddrs": ["/ip4/10.0.2.100/tcp/19091"]
486 }
487 })),
488 (status = 401, description = "Invalid authorization token.", body = ApiError),
489 (status = 422, description = "Unknown failure", body = ApiError)
490 ),
491 security(
492 ("api_token" = []),
493 ("bearer_token" = [])
494 ),
495 tag = "Node"
496 )]
497pub(super) async fn entry_nodes(State(state): State<Arc<InternalState>>) -> Result<impl IntoResponse, ApiError> {
498 let hopr = state.hopr.clone();
499
500 match hopr.get_public_nodes().await {
501 Ok(nodes) => {
502 let mut body = HashMap::new();
503 for (peer_id, address, mas) in nodes.into_iter() {
504 body.insert(
505 address.to_string(),
506 EntryNode {
507 multiaddrs: mas,
508 is_eligible: hopr.is_allowed_to_access_network(either::Left(&peer_id)).await?,
509 },
510 );
511 }
512
513 Ok((StatusCode::OK, Json(body)).into_response())
514 }
515 Err(error) => Ok((StatusCode::UNPROCESSABLE_ENTITY, ApiErrorStatus::from(error)).into_response()),
516 }
517}