Skip to main content

hoprd_api/
node.rs

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")]
29/// Running node version.
30pub(crate) struct NodeVersionResponse {
31    #[schema(example = "2.1.0")]
32    version: String,
33}
34
35/// Get the release version of the running node.
36#[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/// Get the configuration of the running node.
56#[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    }))]
85/// Quality information for a peer.
86pub(crate) struct NodePeersQueryRequest {
87    #[serde(default)]
88    #[schema(required = false, example = 0.7)]
89    /// Minimum peer quality to be included in the response.
90    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")]
99/// Heartbeat information for a peer.
100pub(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")]
115/// Packet statistics for a peer.
116pub(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}))]
156/// All information about a known peer.
157pub(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    /// Packet statistics for this peer (if available).
173    #[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")]
185/// Represents a peer that has been announced on-chain.
186pub(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}))]
216/// All connected and announced peers.
217pub(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/// Lists information for `connected peers` and `announced peers`.
240///
241/// Connected peers are nodes which are connected to the node while announced peers are
242/// nodes which have announced to the network.
243///
244/// Optionally pass `quality` parameter to get only peers with higher or equal quality
245/// to the specified value.
246#[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                // no observations recorded yet
278                let info = hopr.network_peer_info(&peer)?;
279
280                // peer score is low enough not to be considered
281                if info.score() < score {
282                    return None;
283                }
284
285                // no known chain address for the current peer can be found
286                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")]
351/// Information about the current node. Covers network, addresses, eligibility, connectivity status, contracts addresses
352/// and indexer state.
353pub(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    /// Channel closure period in seconds
371    #[schema(example = 15)]
372    channel_closure_period: u64,
373}
374
375/// Get information about this HOPR Node.
376#[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}))]
426/// Reachable entry node information
427pub(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/// List all known entry nodes with multiaddrs and eligibility.
436#[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}