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