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};
9#[cfg(feature = "telemetry")]
10use hopr_lib::PeerPacketStatsSnapshot;
11use hopr_lib::{
12 Address, Multiaddr,
13 api::{
14 graph::{EdgeLinkObservable, traits::EdgeObservableRead},
15 network::Health,
16 node::HoprNodeNetworkOperations,
17 },
18};
19use serde::{Deserialize, Serialize};
20use serde_with::{DisplayFromStr, serde_as};
21
22use crate::{ApiError, ApiErrorStatus, BASE_PATH, InternalState, checksum_address_serializer};
23
24#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
25#[schema(example = json!({
26 "version": "2.1.0",
27 }))]
28#[serde(rename_all = "camelCase")]
29pub(crate) struct NodeVersionResponse {
31 #[schema(example = "2.1.0")]
32 version: String,
33}
34
35#[utoipa::path(
37 get,
38 path = const_format::formatcp!("{BASE_PATH}/node/version"),
39 description = "Get the release version of the running node",
40 responses(
41 (status = 200, description = "Fetched node version", body = NodeVersionResponse),
42 (status = 401, description = "Invalid authorization token.", body = ApiError),
43 ),
44 security(
45 ("api_token" = []),
46 ("bearer_token" = [])
47 ),
48 tag = "Node"
49 )]
50pub(super) async fn version() -> impl IntoResponse {
51 let version = hopr_lib::constants::APP_VERSION.to_string();
52 (StatusCode::OK, Json(NodeVersionResponse { version })).into_response()
53}
54
55#[utoipa::path(
57 get,
58 path = const_format::formatcp!("{BASE_PATH}/node/configuration"),
59 description = "Get the configuration of the running node",
60 responses(
61 (status = 200, description = "Fetched node configuration", body = HashMap<String, String>, example = json!({
62 "network": "anvil-localhost",
63 "provider": "http://127.0.0.1:8545",
64 "hoprToken": "0x9a676e781a523b5d0c0e43731313a708cb607508",
65 "hoprChannels": "0x9a9f2ccfde556a7e9ff0848998aa4a0cfd8863ae",
66 "...": "..."
67 })),
68 (status = 401, description = "Invalid authorization token.", body = ApiError),
69 ),
70 security(
71 ("api_token" = []),
72 ("bearer_token" = [])
73 ),
74 tag = "Configuration"
75 )]
76pub(super) async fn configuration(State(state): State<Arc<InternalState>>) -> impl IntoResponse {
77 (StatusCode::OK, Json(state.hoprd_cfg.clone())).into_response()
78}
79
80#[derive(Debug, Clone, Deserialize, utoipa::ToSchema, utoipa::IntoParams)]
81#[into_params(parameter_in = Query)]
82#[schema(example = json!({
83 "quality": 0.7
84 }))]
85pub(crate) struct NodePeersQueryRequest {
87 #[serde(default)]
88 #[schema(required = false, example = 0.7)]
89 score: f64,
91}
92
93#[derive(Debug, Default, Clone, Serialize, utoipa::ToSchema)]
94#[schema(example = json!({
95 "sent": 10,
96 "success": 10
97}))]
98#[serde(rename_all = "camelCase")]
99pub(crate) struct HeartbeatInfo {
101 #[schema(example = 10)]
102 sent: u64,
103 #[schema(example = 10)]
104 success: u64,
105}
106
107#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
108#[schema(example = json!({
109 "packetsOut": 100,
110 "packetsIn": 50,
111 "bytesOut": 102400,
112 "bytesIn": 51200
113}))]
114#[serde(rename_all = "camelCase")]
115pub(crate) struct PeerPacketStatsResponse {
117 #[schema(example = 100)]
118 pub packets_out: u64,
119 #[schema(example = 50)]
120 pub packets_in: u64,
121 #[schema(example = 102400)]
122 pub bytes_out: u64,
123 #[schema(example = 51200)]
124 pub bytes_in: u64,
125}
126
127#[cfg(feature = "telemetry")]
128impl From<PeerPacketStatsSnapshot> for PeerPacketStatsResponse {
129 fn from(snapshot: PeerPacketStatsSnapshot) -> Self {
130 Self {
131 packets_out: snapshot.packets_out,
132 packets_in: snapshot.packets_in,
133 bytes_out: snapshot.bytes_out,
134 bytes_in: snapshot.bytes_in,
135 }
136 }
137}
138
139#[serde_as]
140#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
141#[serde(rename_all = "camelCase")]
142#[schema(example = json!({
143 "address": "0xb4ce7e6e36ac8b01a974725d5ba730af2b156fbe",
144 "multiaddr": "/ip4/178.12.1.9/tcp/19092",
145 "probeRate": 0.476,
146 "lastSeen": 1690000000,
147 "averageLatency": 100,
148 "score": 0.7,
149 "packetStats": {
150 "packetsOut": 100,
151 "packetsIn": 50,
152 "bytesOut": 102400,
153 "bytesIn": 51200
154 }
155}))]
156pub(crate) struct PeerObservations {
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 #[schema(example = 0.476)]
165 probe_rate: f64,
166 #[schema(example = 1690000000)]
167 last_update: u128,
168 #[schema(example = 100)]
169 average_latency: u128,
170 #[schema(example = 0.7)]
171 score: f64,
172 #[cfg(feature = "telemetry")]
174 #[serde(skip_serializing_if = "Option::is_none")]
175 packet_stats: Option<PeerPacketStatsResponse>,
176}
177
178#[serde_as]
179#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
180#[schema(example = json!({
181 "address": "0xb4ce7e6e36ac8b01a974725d5ba730af2b156fbe",
182 "multiaddrs": "[/ip4/178.12.1.9/tcp/19092]"
183}))]
184#[serde(rename_all = "camelCase")]
185pub(crate) struct AnnouncedPeer {
187 #[serde(serialize_with = "checksum_address_serializer")]
188 #[schema(value_type = String, example = "0xb4ce7e6e36ac8b01a974725d5ba730af2b156fbe")]
189 address: Address,
190 #[serde_as(as = "Vec<DisplayFromStr>")]
191 #[schema(value_type = Vec<String>, example = "[/ip4/178.12.1.9/tcp/19092]")]
192 multiaddrs: Vec<Multiaddr>,
193}
194
195#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
196#[serde(rename_all = "camelCase")]
197#[schema(example = json!({
198 "connected": [{
199 "address": "0xb4ce7e6e36ac8b01a974725d5ba730af2b156fbe",
200 "multiaddr": "/ip4/178.12.1.9/tcp/19092",
201 "heartbeats": {
202 "sent": 10,
203 "success": 10
204 },
205 "lastSeen": 1690000000,
206 "lastSeenLatency": 100,
207 "quality": 0.7,
208 "backoff": 0.5,
209 "isNew": true,
210 }],
211 "announced": [{
212 "address": "0xb4ce7e6e36ac8b01a974725d5ba730af2b156fbe",
213 "multiaddr": "/ip4/178.12.1.9/tcp/19092"
214 }]
215}))]
216pub(crate) struct NodePeersResponse {
218 #[schema(example = json!([{
219 "address": "0xb4ce7e6e36ac8b01a974725d5ba730af2b156fbe",
220 "multiaddr": "/ip4/178.12.1.9/tcp/19092",
221 "heartbeats": {
222 "sent": 10,
223 "success": 10
224 },
225 "lastSeen": 1690000000,
226 "lastSeenLatency": 100,
227 "quality": 0.7,
228 "backoff": 0.5,
229 "isNew": true,
230 }]))]
231 connected: Vec<PeerObservations>,
232 #[schema(example = json!([{
233 "address": "0xb4ce7e6e36ac8b01a974725d5ba730af2b156fbe",
234 "multiaddr": "/ip4/178.12.1.9/tcp/19092"
235 }]))]
236 announced: Vec<AnnouncedPeer>,
237}
238
239#[utoipa::path(
247 get,
248 path = const_format::formatcp!("{BASE_PATH}/node/peers"),
249 description = "Lists information for connected and announced peers",
250 params(NodePeersQueryRequest),
251 responses(
252 (status = 200, description = "Successfully returned observed peers", body = NodePeersResponse),
253 (status = 400, description = "Failed to extract a valid quality parameter", body = ApiError),
254 (status = 401, description = "Invalid authorization token.", body = ApiError),
255 ),
256 security(
257 ("api_token" = []),
258 ("bearer_token" = [])
259 ),
260 tag = "Node"
261 )]
262pub(super) async fn peers(
263 Query(NodePeersQueryRequest { score }): Query<NodePeersQueryRequest>,
264 State(state): State<Arc<InternalState>>,
265) -> Result<impl IntoResponse, ApiError> {
266 if !(0.0f64..=1.0f64).contains(&score) {
267 return Ok((StatusCode::BAD_REQUEST, ApiErrorStatus::InvalidQuality).into_response());
268 }
269
270 let hopr = state.hopr.clone();
271
272 let all_network_peers = futures::stream::iter(hopr.network_connected_peers().await?)
273 .filter_map(|peer| {
274 let hopr = hopr.clone();
275
276 async move {
277 let info = hopr.network_peer_info(&peer)?;
279
280 if info.score() < score {
282 return None;
283 }
284
285 let address = hopr.peerid_to_chain_key(&peer).await.ok().flatten()?;
287
288 let multiaddresses = hopr.network_observed_multiaddresses(&peer).await;
289
290 Some(PeerObservations {
291 address,
292 multiaddr: multiaddresses.first().cloned(),
293 last_update: info.last_update().as_millis(),
294 average_latency: info
295 .immediate_qos()
296 .and_then(|qos| qos.average_latency())
297 .map_or(0, |latency| latency.as_millis()),
298 probe_rate: info.immediate_qos().map_or(0.0, |qos| qos.average_probe_rate()),
299 score: info.score(),
300 #[cfg(feature = "telemetry")]
301 packet_stats: hopr
302 .network_peer_packet_stats(&peer)
303 .await
304 .ok()
305 .flatten()
306 .map(PeerPacketStatsResponse::from),
307 })
308 }
309 })
310 .collect::<Vec<_>>()
311 .await;
312
313 let announced_peers = hopr
314 .accounts_announced_on_chain()
315 .await?
316 .into_iter()
317 .map(|announced| async move {
318 AnnouncedPeer {
319 address: announced.chain_addr,
320 multiaddrs: announced.get_multiaddrs().to_vec(),
321 }
322 })
323 .collect::<FuturesUnordered<_>>()
324 .collect()
325 .await;
326
327 let body = NodePeersResponse {
328 connected: all_network_peers,
329 announced: announced_peers,
330 };
331
332 Ok((StatusCode::OK, Json(body)).into_response())
333}
334
335#[serde_as]
336#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
337#[schema(example = json!({
338 "announcedAddress": [
339 "/ip4/10.0.2.100/tcp/19092"
340 ],
341 "providerUrl": "https://staging.blokli.hoprnet.link",
342 "hoprNetworkName": "rotsee",
343 "channelClosurePeriod": 15,
344 "connectivityStatus": "Green",
345 "hoprNodeSafe": "0x42bc901b1d040f984ed626eff550718498a6798a",
346 "listeningAddress": [
347 "/ip4/10.0.2.100/tcp/19092"
348 ],
349 }))]
350#[serde(rename_all = "camelCase")]
351pub(crate) struct NodeInfoResponse {
354 #[serde_as(as = "Vec<DisplayFromStr>")]
355 #[schema(value_type = Vec<String>, example = json!(["/ip4/10.0.2.100/tcp/19092"]))]
356 announced_address: Vec<Multiaddr>,
357 #[serde_as(as = "Vec<DisplayFromStr>")]
358 #[schema(value_type = Vec<String>, example = json!(["/ip4/10.0.2.100/tcp/19092"]))]
359 listening_address: Vec<Multiaddr>,
360 #[schema(value_type = String, example = "https://staging.blokli.hoprnet.link")]
361 provider_url: String,
362 #[schema(value_type = String, example = "rotsee")]
363 hopr_network_name: String,
364 #[serde(serialize_with = "checksum_address_serializer")]
365 #[schema(value_type = String, example = "0x42bc901b1d040f984ed626eff550718498a6798a")]
366 hopr_node_safe: Address,
367 #[serde_as(as = "DisplayFromStr")]
368 #[schema(value_type = String, example = "Green")]
369 connectivity_status: Health,
370 #[schema(example = 15)]
372 channel_closure_period: u64,
373}
374
375#[utoipa::path(
377 get,
378 path = const_format::formatcp!("{BASE_PATH}/node/info"),
379 description = "Get information about this HOPR Node",
380 responses(
381 (status = 200, description = "Fetched node informations", body = NodeInfoResponse),
382 (status = 422, description = "Unknown failure", body = ApiError)
383 ),
384 security(
385 ("api_token" = []),
386 ("bearer_token" = [])
387 ),
388 tag = "Node"
389 )]
390pub(super) async fn info(State(state): State<Arc<InternalState>>) -> Result<impl IntoResponse, ApiError> {
391 let hopr = state.hopr.clone();
392
393 let safe_config = hopr.get_safe_config();
394
395 let provider_url = state
396 .hoprd_cfg
397 .as_object()
398 .and_then(|cfg| cfg.get("blokli_url"))
399 .and_then(|v| v.as_str());
400
401 match futures::try_join!(hopr.chain_info(), hopr.get_channel_closure_notice_period()) {
402 Ok((info, channel_closure_notice_period)) => {
403 let body = NodeInfoResponse {
404 announced_address: hopr.local_multiaddresses(),
405 listening_address: hopr.local_multiaddresses(),
406 provider_url: provider_url.unwrap_or("n/a").to_owned(),
407 hopr_network_name: info.hopr_network_name,
408 hopr_node_safe: safe_config.safe_address,
409 connectivity_status: hopr.network_health().await,
410 channel_closure_period: channel_closure_notice_period.as_secs(),
411 };
412
413 Ok((StatusCode::OK, Json(body)).into_response())
414 }
415 Err(error) => Ok((StatusCode::UNPROCESSABLE_ENTITY, ApiErrorStatus::from(error)).into_response()),
416 }
417}
418
419#[serde_as]
420#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
421#[serde(rename_all = "camelCase")]
422#[schema(example = json!({
423 "isEligible": true,
424 "multiaddrs": ["/ip4/10.0.2.100/tcp/19091"]
425}))]
426pub(crate) struct EntryNode {
428 #[serde_as(as = "Vec<DisplayFromStr>")]
429 #[schema(value_type = Vec<String>, example = json!(["/ip4/10.0.2.100/tcp/19091"]))]
430 multiaddrs: Vec<Multiaddr>,
431 #[schema(example = true)]
432 is_eligible: bool,
433}
434
435#[utoipa::path(
437 get,
438 path = const_format::formatcp!("{BASE_PATH}/node/entry-nodes"),
439 description = "List all known entry nodes with multiaddrs and eligibility",
440 responses(
441 (status = 200, description = "Fetched public nodes' information", body = HashMap<String, EntryNode>, example = json!({
442 "0x188c4462b75e46f0c7262d7f48d182447b93a93c": {
443 "isEligible": true,
444 "multiaddrs": ["/ip4/10.0.2.100/tcp/19091"]
445 }
446 })),
447 (status = 401, description = "Invalid authorization token.", body = ApiError),
448 (status = 422, description = "Unknown failure", body = ApiError)
449 ),
450 security(
451 ("api_token" = []),
452 ("bearer_token" = [])
453 ),
454 tag = "Node"
455 )]
456pub(super) async fn entry_nodes(State(state): State<Arc<InternalState>>) -> Result<impl IntoResponse, ApiError> {
457 let hopr = state.hopr.clone();
458
459 match hopr.get_public_nodes().await {
460 Ok(nodes) => {
461 let mut body = HashMap::new();
462 for (_, address, mas) in nodes.into_iter() {
463 body.insert(
464 address.to_string(),
465 EntryNode {
466 multiaddrs: mas,
467 is_eligible: true,
468 },
469 );
470 }
471
472 Ok((StatusCode::OK, Json(body)).into_response())
473 }
474 Err(error) => Ok((StatusCode::UNPROCESSABLE_ENTITY, ApiErrorStatus::from(error)).into_response()),
475 }
476}