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