1use std::{collections::HashMap, sync::Arc};
2
3use axum::{
4 extract::{Json, Query, State},
5 http::status::StatusCode,
6 response::IntoResponse,
7};
8use futures::{StreamExt, stream::FuturesUnordered};
9use hopr_lib::{Address, AsUnixTimestamp, GraphExportConfig, Health, Multiaddr, prelude::Hash};
10use serde::{Deserialize, Serialize};
11use serde_with::{DisplayFromStr, serde_as};
12
13use crate::{
14 ApiError, ApiErrorStatus, BASE_PATH, InternalState, checksum_address_serializer, option_checksum_address_serializer,
15};
16
17#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
18#[schema(example = json!({
19 "version": "2.1.0",
20 }))]
21#[serde(rename_all = "camelCase")]
22pub(crate) struct NodeVersionResponse {
24 #[schema(example = "2.1.0")]
25 version: String,
26}
27
28#[utoipa::path(
30 get,
31 path = const_format::formatcp!("{BASE_PATH}/node/version"),
32 description = "Get the release version of the running node",
33 responses(
34 (status = 200, description = "Fetched node version", body = NodeVersionResponse),
35 (status = 401, description = "Invalid authorization token.", body = ApiError),
36 ),
37 security(
38 ("api_token" = []),
39 ("bearer_token" = [])
40 ),
41 tag = "Node"
42 )]
43pub(super) async fn version() -> impl IntoResponse {
44 let version = hopr_lib::constants::APP_VERSION.to_string();
45 (StatusCode::OK, Json(NodeVersionResponse { version })).into_response()
46}
47
48#[utoipa::path(
50 get,
51 path = const_format::formatcp!("{BASE_PATH}/node/configuration"),
52 description = "Get the configuration of the running node",
53 responses(
54 (status = 200, description = "Fetched node configuration", body = HashMap<String, String>, example = json!({
55 "network": "anvil-localhost",
56 "provider": "http://127.0.0.1:8545",
57 "hoprToken": "0x9a676e781a523b5d0c0e43731313a708cb607508",
58 "hoprChannels": "0x9a9f2ccfde556a7e9ff0848998aa4a0cfd8863ae",
59 "...": "..."
60 })),
61 (status = 401, description = "Invalid authorization token.", body = ApiError),
62 ),
63 security(
64 ("api_token" = []),
65 ("bearer_token" = [])
66 ),
67 tag = "Configuration"
68 )]
69pub(super) async fn configuration(State(state): State<Arc<InternalState>>) -> impl IntoResponse {
70 (StatusCode::OK, Json(state.hoprd_cfg.clone())).into_response()
71}
72
73#[derive(Debug, Clone, Deserialize, utoipa::ToSchema, utoipa::IntoParams)]
74#[into_params(parameter_in = Query)]
75#[schema(example = json!({
76 "quality": 0.7
77 }))]
78pub(crate) struct NodePeersQueryRequest {
80 #[serde(default)]
81 #[schema(required = false, example = 0.7)]
82 quality: f64,
84}
85
86#[derive(Debug, Default, Clone, Serialize, utoipa::ToSchema)]
87#[schema(example = json!({
88 "sent": 10,
89 "success": 10
90}))]
91#[serde(rename_all = "camelCase")]
92pub(crate) struct HeartbeatInfo {
94 #[schema(example = 10)]
95 sent: u64,
96 #[schema(example = 10)]
97 success: u64,
98}
99
100#[serde_as]
101#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
102#[serde(rename_all = "camelCase")]
103#[schema(example = json!({
104 "address": "0xb4ce7e6e36ac8b01a974725d5ba730af2b156fbe",
105 "multiaddr": "/ip4/178.12.1.9/tcp/19092",
106 "heartbeats": {
107 "sent": 10,
108 "success": 10
109 },
110 "lastSeen": 1690000000,
111 "lastSeenLatency": 100,
112 "quality": 0.7,
113 "backoff": 0.5,
114 "isNew": true,
115 "reportedVersion": "2.1.0"
116}))]
117pub(crate) struct PeerInfo {
119 #[serde(serialize_with = "option_checksum_address_serializer")]
120 #[schema(value_type = Option<String>, example = "0xb4ce7e6e36ac8b01a974725d5ba730af2b156fbe")]
121 address: Option<Address>,
122 #[serde_as(as = "Option<DisplayFromStr>")]
123 #[schema(value_type = Option<String>, example = "/ip4/178.12.1.9/tcp/19092")]
124 multiaddr: Option<Multiaddr>,
125 #[schema(example = json!({
126 "sent": 10,
127 "success": 10
128 }))]
129 heartbeats: HeartbeatInfo,
130 #[schema(example = 1690000000)]
131 last_seen: u128,
132 #[schema(example = 100)]
133 last_seen_latency: u128,
134 #[schema(example = 0.7)]
135 quality: f64,
136 #[schema(example = 0.5)]
137 backoff: f64,
138 #[schema(example = true)]
139 is_new: bool,
140}
141
142#[serde_as]
143#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
144#[schema(example = json!({
145 "address": "0xb4ce7e6e36ac8b01a974725d5ba730af2b156fbe",
146 "multiaddr": "/ip4/178.12.1.9/tcp/19092"
147}))]
148#[serde(rename_all = "camelCase")]
149pub(crate) struct AnnouncedPeer {
151 #[serde(serialize_with = "checksum_address_serializer")]
152 #[schema(value_type = String, example = "0xb4ce7e6e36ac8b01a974725d5ba730af2b156fbe")]
153 address: Address,
154 #[serde_as(as = "Option<DisplayFromStr>")]
155 #[schema(value_type = Option<String>, example = "/ip4/178.12.1.9/tcp/19092")]
156 multiaddr: Option<Multiaddr>,
157}
158
159#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
160#[serde(rename_all = "camelCase")]
161#[schema(example = json!({
162 "connected": [{
163 "address": "0xb4ce7e6e36ac8b01a974725d5ba730af2b156fbe",
164 "multiaddr": "/ip4/178.12.1.9/tcp/19092",
165 "heartbeats": {
166 "sent": 10,
167 "success": 10
168 },
169 "lastSeen": 1690000000,
170 "lastSeenLatency": 100,
171 "quality": 0.7,
172 "backoff": 0.5,
173 "isNew": true,
174 "reportedVersion": "2.1.0"
175 }],
176 "announced": [{
177 "address": "0xb4ce7e6e36ac8b01a974725d5ba730af2b156fbe",
178 "multiaddr": "/ip4/178.12.1.9/tcp/19092"
179 }]
180}))]
181pub(crate) struct NodePeersResponse {
183 #[schema(example = json!([{
184 "address": "0xb4ce7e6e36ac8b01a974725d5ba730af2b156fbe",
185 "multiaddr": "/ip4/178.12.1.9/tcp/19092",
186 "heartbeats": {
187 "sent": 10,
188 "success": 10
189 },
190 "lastSeen": 1690000000,
191 "lastSeenLatency": 100,
192 "quality": 0.7,
193 "backoff": 0.5,
194 "isNew": true,
195 "reportedVersion": "2.1.0"
196 }]))]
197 connected: Vec<PeerInfo>,
198 #[schema(example = json!([{
199 "address": "0xb4ce7e6e36ac8b01a974725d5ba730af2b156fbe",
200 "multiaddr": "/ip4/178.12.1.9/tcp/19092"
201 }]))]
202 announced: Vec<AnnouncedPeer>,
203}
204
205#[utoipa::path(
213 get,
214 path = const_format::formatcp!("{BASE_PATH}/node/peers"),
215 description = "Lists information for connected and announced peers",
216 params(NodePeersQueryRequest),
217 responses(
218 (status = 200, description = "Successfully returned observed peers", body = NodePeersResponse),
219 (status = 400, description = "Failed to extract a valid quality parameter", body = ApiError),
220 (status = 401, description = "Invalid authorization token.", body = ApiError),
221 ),
222 security(
223 ("api_token" = []),
224 ("bearer_token" = [])
225 ),
226 tag = "Node"
227 )]
228pub(super) async fn peers(
229 Query(NodePeersQueryRequest { quality }): Query<NodePeersQueryRequest>,
230 State(state): State<Arc<InternalState>>,
231) -> Result<impl IntoResponse, ApiError> {
232 if !(0.0f64..=1.0f64).contains(&quality) {
233 return Ok((StatusCode::BAD_REQUEST, ApiErrorStatus::InvalidQuality).into_response());
234 }
235
236 let hopr = state.hopr.clone();
237
238 let all_network_peers = futures::stream::iter(hopr.network_connected_peers().await?)
239 .filter_map(|peer| {
240 let hopr = hopr.clone();
241
242 async move {
243 if let Ok(Some(info)) = hopr.network_peer_info(&peer).await {
244 let avg_quality = info.get_average_quality();
245 if avg_quality >= quality {
246 Some((peer, info))
247 } else {
248 None
249 }
250 } else {
251 None
252 }
253 }
254 })
255 .filter_map(|(peer_id, info)| {
256 let hopr = hopr.clone();
257
258 async move {
259 let address = hopr.peerid_to_chain_key(&peer_id).await.ok().flatten();
260
261 let multiaddresses = hopr.network_observed_multiaddresses(&peer_id).await;
263
264 Some((address, multiaddresses, info))
265 }
266 })
267 .map(|(address, mas, info)| PeerInfo {
268 address,
269 multiaddr: mas.first().cloned(),
270 heartbeats: HeartbeatInfo {
271 sent: info.heartbeats_sent,
272 success: info.heartbeats_succeeded,
273 },
274 last_seen: info.last_seen.as_unix_timestamp().as_millis(),
275 last_seen_latency: info.last_seen_latency.as_millis() / 2,
276 quality: info.get_average_quality(),
277 backoff: info.backoff,
278 is_new: info.heartbeats_sent == 0u64,
279 })
280 .collect::<Vec<_>>()
281 .await;
282
283 let announced_peers = hopr
284 .accounts_announced_on_chain()
285 .await?
286 .into_iter()
287 .map(|announced| async move {
288 AnnouncedPeer {
289 address: announced.chain_addr,
290 multiaddr: announced.get_multiaddr(),
291 }
292 })
293 .collect::<FuturesUnordered<_>>()
294 .collect()
295 .await;
296
297 let body = NodePeersResponse {
298 connected: all_network_peers,
299 announced: announced_peers,
300 };
301
302 Ok((StatusCode::OK, Json(body)).into_response())
303}
304
305#[derive(Debug, Clone, Deserialize, Default, utoipa::IntoParams, utoipa::ToSchema)]
306#[into_params(parameter_in = Query)]
307#[serde(default, rename_all = "camelCase")]
308#[schema(example = json!({
309 "ignoreDisconnectedComponents": true,
310 "ignoreNonOpenedChannels": true,
311 "only3HopPaths": true,
312 "rawGraph": true
313 }))]
314pub(crate) struct GraphExportQuery {
316 #[schema(required = false)]
319 #[serde(default)]
320 pub ignore_disconnected_components: bool,
321 #[schema(required = false)]
323 #[serde(default)]
324 pub ignore_non_opened_channels: bool,
325 #[schema(required = false)]
327 #[serde(default)]
328 pub only_3_hop_paths: bool,
329 #[schema(required = false)]
334 #[serde(default)]
335 pub raw_graph: bool,
336}
337
338impl From<GraphExportQuery> for GraphExportConfig {
339 fn from(value: GraphExportQuery) -> Self {
340 Self {
341 ignore_disconnected_components: value.ignore_disconnected_components,
342 ignore_non_opened_channels: value.ignore_non_opened_channels,
343 only_3_hop_accessible_nodes: value.only_3_hop_paths,
344 }
345 }
346}
347
348#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
349#[schema(example = json!({
350 "graph": "
351 ...
352 242 -> 381 [ label = 'Open channel 0x82a72e271cdedd56c29e970ced3517ba93b679869c729112b5a56fa08698df8f; stake=100000000000000000 HOPR; score=None; status=open;' ]
353 ...",
354 }))]
355#[serde(rename_all = "camelCase")]
356pub(crate) struct NodeGraphResponse {
358 graph: String,
359}
360
361#[utoipa::path(
363 get,
364 path = const_format::formatcp!("{BASE_PATH}/node/graph"),
365 description = "Retrieve node's channel graph in DOT or JSON format",
366 params(GraphExportQuery),
367 responses(
368 (status = 200, description = "Fetched channel graph", body = NodeGraphResponse),
369 (status = 401, description = "Invalid authorization token.", body = ApiError),
370 ),
371 security(
372 ("api_token" = []),
373 ("bearer_token" = [])
374 ),
375 tag = "Node"
376)]
377pub(super) async fn channel_graph(
378 State(state): State<Arc<InternalState>>,
379 Query(args): Query<GraphExportQuery>,
380) -> impl IntoResponse {
381 if args.raw_graph {
382 match state.hopr.export_raw_channel_graph().await {
383 Ok(raw_graph) => (StatusCode::OK, Json(NodeGraphResponse { graph: raw_graph })).into_response(),
384 Err(error) => (
385 StatusCode::UNPROCESSABLE_ENTITY,
386 ApiErrorStatus::UnknownFailure(error.to_string()),
387 )
388 .into_response(),
389 }
390 } else {
391 (StatusCode::OK, state.hopr.export_channel_graph(args.into()).await).into_response()
392 }
393}
394
395#[serde_as]
396#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
397#[schema(example = json!({
398 "announcedAddress": [
399 "/ip4/10.0.2.100/tcp/19092"
400 ],
401 "chain": "anvil-localhost",
402 "provider": "http://127.0.0.1:8545",
403 "channelClosurePeriod": 15,
404 "connectivityStatus": "Green",
405 "hoprChannels": "0x9a9f2ccfde556a7e9ff0848998aa4a0cfd8863ae",
406 "hoprManagementModule": "0xa51c1fc2f0d1a1b8494ed1fe312d7c3a78ed91c0",
407 "hoprNetworkRegistry": "0x3aa5ebb10dc797cac828524e59a333d0a371443c",
408 "hoprNodeSafe": "0x42bc901b1d040f984ed626eff550718498a6798a",
409 "hoprNodeSafeRegistry": "0x0dcd1bf9a1b36ce34237eeafef220932846bcd82",
410 "hoprToken": "0x9a676e781a523b5d0c0e43731313a708cb607508",
411 "isEligible": true,
412 "listeningAddress": [
413 "/ip4/10.0.2.100/tcp/19092"
414 ],
415 "network": "anvil-localhost",
416 "indexerBlock": 123456,
417 "indexerChecksum": "0000000000000000000000000000000000000000000000000000000000000000",
418 "indexBlockPrevChecksum": 0,
419 "indexerLastLogBlock": 123450,
420 "indexerLastLogChecksum": "cfde556a7e9ff0848998aa4a9a9f2ccfde556a7e9ff0848998aa4a0cfd8863ae",
421 "isIndexerCorrupted": false,
422 }))]
423#[serde(rename_all = "camelCase")]
424pub(crate) struct NodeInfoResponse {
427 #[schema(value_type = String, example = "anvil-localhost")]
428 network: String,
429 #[serde_as(as = "Vec<DisplayFromStr>")]
430 #[schema(value_type = Vec<String>, example = json!(["/ip4/10.0.2.100/tcp/19092"]))]
431 announced_address: Vec<Multiaddr>,
432 #[serde_as(as = "Vec<DisplayFromStr>")]
433 #[schema(value_type = Vec<String>, example = json!(["/ip4/10.0.2.100/tcp/19092"]))]
434 listening_address: Vec<Multiaddr>,
435 #[schema(example = "anvil-localhost")]
436 chain: String,
437 #[schema(example = "http://127.0.0.1:8545")]
438 provider: String,
439 #[serde(serialize_with = "checksum_address_serializer")]
440 #[schema(value_type = String, example = "0x9a676e781a523b5d0c0e43731313a708cb607508")]
441 hopr_token: Address,
442 #[serde(serialize_with = "checksum_address_serializer")]
443 #[schema(value_type = String, example = "0x9a9f2ccfde556a7e9ff0848998aa4a0cfd8863ae")]
444 hopr_channels: Address,
445 #[serde(serialize_with = "checksum_address_serializer")]
446 #[schema(value_type = String, example = "0x3aa5ebb10dc797cac828524e59a333d0a371443c")]
447 hopr_network_registry: Address,
448 #[serde(serialize_with = "checksum_address_serializer")]
449 #[schema(value_type = String, example = "0x0dcd1bf9a1b36ce34237eeafef220932846bcd82")]
450 hopr_node_safe_registry: Address,
451 #[serde(serialize_with = "checksum_address_serializer")]
452 #[schema(value_type = String, example = "0xa51c1fc2f0d1a1b8494ed1fe312d7c3a78ed91c0")]
453 hopr_management_module: Address,
454 #[serde(serialize_with = "checksum_address_serializer")]
455 #[schema(value_type = String, example = "0x42bc901b1d040f984ed626eff550718498a6798a")]
456 hopr_node_safe: Address,
457 #[schema(example = true)]
458 is_eligible: bool,
459 #[serde_as(as = "DisplayFromStr")]
460 #[schema(value_type = String, example = "Green")]
461 connectivity_status: Health,
462 #[schema(example = 15)]
464 channel_closure_period: u64,
465 #[schema(example = 123456)]
466 indexer_block: u32,
467 #[schema(example = 123450)]
468 indexer_last_log_block: u32,
469 #[serde_as(as = "DisplayFromStr")]
470 #[schema(value_type = String, example = "cfde556a7e9ff0848998aa4a9a9f2ccfde556a7e9ff0848998aa4a0cfd8863ae")]
471 indexer_last_log_checksum: Hash,
472 #[schema(example = true)]
473 is_indexer_corrupted: bool,
474}
475
476#[utoipa::path(
478 get,
479 path = const_format::formatcp!("{BASE_PATH}/node/info"),
480 description = "Get information about this HOPR Node",
481 responses(
482 (status = 200, description = "Fetched node version", body = NodeInfoResponse),
483 (status = 422, description = "Unknown failure", body = ApiError)
484 ),
485 security(
486 ("api_token" = []),
487 ("bearer_token" = [])
488 ),
489 tag = "Node"
490 )]
491pub(super) async fn info(State(state): State<Arc<InternalState>>) -> Result<impl IntoResponse, ApiError> {
492 let hopr = state.hopr.clone();
493
494 let chain_config = hopr.chain_config();
495 let safe_config = hopr.get_safe_config();
496 let network = hopr.network();
497
498 let indexer_state_info = match hopr.get_indexer_state().await {
499 Ok(info) => info,
500 Err(error) => return Ok((StatusCode::UNPROCESSABLE_ENTITY, ApiErrorStatus::from(error)).into_response()),
501 };
502
503 let is_indexer_corrupted = hopr
505 .corrupted_channels()
506 .await
507 .map(|channels| !channels.is_empty())
508 .unwrap_or_default();
509
510 match hopr.get_channel_closure_notice_period().await {
511 Ok(channel_closure_notice_period) => {
512 let body = NodeInfoResponse {
513 network,
514 announced_address: hopr.local_multiaddresses(),
515 listening_address: hopr.local_multiaddresses(),
516 chain: chain_config.id,
517 provider: hopr.get_provider(),
518 hopr_token: chain_config.token,
519 hopr_channels: chain_config.channels,
520 hopr_network_registry: chain_config.network_registry,
521 hopr_node_safe_registry: chain_config.node_safe_registry,
522 hopr_management_module: chain_config.module_implementation,
523 hopr_node_safe: safe_config.safe_address,
524 is_eligible: true,
525 connectivity_status: hopr.network_health().await,
526 channel_closure_period: channel_closure_notice_period.as_secs(),
527 indexer_block: indexer_state_info.latest_block_number,
528 indexer_last_log_block: indexer_state_info.latest_log_block_number,
529 indexer_last_log_checksum: indexer_state_info.latest_log_checksum,
530 is_indexer_corrupted,
531 };
532
533 Ok((StatusCode::OK, Json(body)).into_response())
534 }
535 Err(error) => Ok((StatusCode::UNPROCESSABLE_ENTITY, ApiErrorStatus::from(error)).into_response()),
536 }
537}
538
539#[serde_as]
540#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
541#[serde(rename_all = "camelCase")]
542#[schema(example = json!({
543 "isEligible": true,
544 "multiaddrs": ["/ip4/10.0.2.100/tcp/19091"]
545}))]
546pub(crate) struct EntryNode {
548 #[serde_as(as = "Vec<DisplayFromStr>")]
549 #[schema(value_type = Vec<String>, example = json!(["/ip4/10.0.2.100/tcp/19091"]))]
550 multiaddrs: Vec<Multiaddr>,
551 #[schema(example = true)]
552 is_eligible: bool,
553}
554
555#[utoipa::path(
557 get,
558 path = const_format::formatcp!("{BASE_PATH}/node/entry-nodes"),
559 description = "List all known entry nodes with multiaddrs and eligibility",
560 responses(
561 (status = 200, description = "Fetched public nodes' information", body = HashMap<String, EntryNode>, example = json!({
562 "0x188c4462b75e46f0c7262d7f48d182447b93a93c": {
563 "isEligible": true,
564 "multiaddrs": ["/ip4/10.0.2.100/tcp/19091"]
565 }
566 })),
567 (status = 401, description = "Invalid authorization token.", body = ApiError),
568 (status = 422, description = "Unknown failure", body = ApiError)
569 ),
570 security(
571 ("api_token" = []),
572 ("bearer_token" = [])
573 ),
574 tag = "Node"
575 )]
576pub(super) async fn entry_nodes(State(state): State<Arc<InternalState>>) -> Result<impl IntoResponse, ApiError> {
577 let hopr = state.hopr.clone();
578
579 match hopr.get_public_nodes().await {
580 Ok(nodes) => {
581 let mut body = HashMap::new();
582 for (_, address, mas) in nodes.into_iter() {
583 body.insert(
584 address.to_string(),
585 EntryNode {
586 multiaddrs: mas,
587 is_eligible: true,
588 },
589 );
590 }
591
592 Ok((StatusCode::OK, Json(body)).into_response())
593 }
594 Err(error) => Ok((StatusCode::UNPROCESSABLE_ENTITY, ApiErrorStatus::from(error)).into_response()),
595 }
596}