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