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};
9use hopr_lib::{
10    Address, Multiaddr,
11    api::{
12        graph::{EdgeLinkObservable, traits::EdgeObservableRead},
13        network::Health,
14        node::HoprNodeNetworkOperations,
15    },
16};
17use serde::{Deserialize, Serialize};
18use serde_with::{DisplayFromStr, serde_as};
19
20use crate::{ApiError, ApiErrorStatus, BASE_PATH, InternalState, checksum_address_serializer};
21
22#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
23#[schema(example = json!({
24        "version": "2.1.0",
25    }))]
26#[serde(rename_all = "camelCase")]
27/// Running node version.
28pub(crate) struct NodeVersionResponse {
29    #[schema(example = "2.1.0")]
30    version: String,
31}
32
33/// Get the release version of the running node.
34#[utoipa::path(
35        get,
36        path = const_format::formatcp!("{BASE_PATH}/node/version"),
37        description = "Get the release version of the running node",
38        responses(
39            (status = 200, description = "Fetched node version", body = NodeVersionResponse),
40            (status = 401, description = "Invalid authorization token.", body = ApiError),
41        ),
42        security(
43            ("api_token" = []),
44            ("bearer_token" = [])
45        ),
46        tag = "Node"
47    )]
48pub(super) async fn version() -> impl IntoResponse {
49    let version = hopr_lib::constants::APP_VERSION.to_string();
50    (StatusCode::OK, Json(NodeVersionResponse { version })).into_response()
51}
52
53/// Get the configuration of the running node.
54#[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    }))]
83/// Quality information for a peer.
84pub(crate) struct NodePeersQueryRequest {
85    #[serde(default)]
86    #[schema(required = false, example = 0.7)]
87    /// Minimum peer quality to be included in the response.
88    score: 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")]
97/// Heartbeat information for a peer.
98pub(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    "probeRate": 0.476,
112    "lastSeen": 1690000000,
113    "averageLatency": 100,
114    "score": 0.7
115}))]
116/// All information about a known peer.
117pub(crate) struct PeerObservations {
118    #[serde(serialize_with = "checksum_address_serializer")]
119    #[schema(value_type = String, example = "0xb4ce7e6e36ac8b01a974725d5ba730af2b156fbe")]
120    address: 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 = 0.476)]
125    probe_rate: f64,
126    #[schema(example = 1690000000)]
127    last_update: u128,
128    #[schema(example = 100)]
129    average_latency: u128,
130    #[schema(example = 0.7)]
131    score: f64,
132}
133
134#[serde_as]
135#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
136#[schema(example = json!({
137    "address": "0xb4ce7e6e36ac8b01a974725d5ba730af2b156fbe",
138    "multiaddrs": "[/ip4/178.12.1.9/tcp/19092]"
139}))]
140#[serde(rename_all = "camelCase")]
141/// Represents a peer that has been announced on-chain.
142pub(crate) struct AnnouncedPeer {
143    #[serde(serialize_with = "checksum_address_serializer")]
144    #[schema(value_type = String, example = "0xb4ce7e6e36ac8b01a974725d5ba730af2b156fbe")]
145    address: Address,
146    #[serde_as(as = "Vec<DisplayFromStr>")]
147    #[schema(value_type = Vec<String>, example = "[/ip4/178.12.1.9/tcp/19092]")]
148    multiaddrs: Vec<Multiaddr>,
149}
150
151#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
152#[serde(rename_all = "camelCase")]
153#[schema(example = json!({
154    "connected": [{
155        "address": "0xb4ce7e6e36ac8b01a974725d5ba730af2b156fbe",
156        "multiaddr": "/ip4/178.12.1.9/tcp/19092",
157        "heartbeats": {
158            "sent": 10,
159            "success": 10
160        },
161        "lastSeen": 1690000000,
162        "lastSeenLatency": 100,
163        "quality": 0.7,
164        "backoff": 0.5,
165        "isNew": true,
166    }],
167    "announced": [{
168        "address": "0xb4ce7e6e36ac8b01a974725d5ba730af2b156fbe",
169        "multiaddr": "/ip4/178.12.1.9/tcp/19092"
170    }]
171}))]
172/// All connected and announced peers.
173pub(crate) struct NodePeersResponse {
174    #[schema(example = json!([{
175        "address": "0xb4ce7e6e36ac8b01a974725d5ba730af2b156fbe",
176        "multiaddr": "/ip4/178.12.1.9/tcp/19092",
177        "heartbeats": {
178            "sent": 10,
179            "success": 10
180        },
181        "lastSeen": 1690000000,
182        "lastSeenLatency": 100,
183        "quality": 0.7,
184        "backoff": 0.5,
185        "isNew": true,
186    }]))]
187    connected: Vec<PeerObservations>,
188    #[schema(example = json!([{
189        "address": "0xb4ce7e6e36ac8b01a974725d5ba730af2b156fbe",
190        "multiaddr": "/ip4/178.12.1.9/tcp/19092"
191    }]))]
192    announced: Vec<AnnouncedPeer>,
193}
194
195/// Lists information for `connected peers` and `announced peers`.
196///
197/// Connected peers are nodes which are connected to the node while announced peers are
198/// nodes which have announced to the network.
199///
200/// Optionally pass `quality` parameter to get only peers with higher or equal quality
201/// to the specified value.
202#[utoipa::path(
203        get,
204        path = const_format::formatcp!("{BASE_PATH}/node/peers"),
205        description = "Lists information for connected and announced peers",
206        params(NodePeersQueryRequest),
207        responses(
208            (status = 200, description = "Successfully returned observed peers", body = NodePeersResponse),
209            (status = 400, description = "Failed to extract a valid quality parameter", body = ApiError),
210            (status = 401, description = "Invalid authorization token.", body = ApiError),
211        ),
212        security(
213            ("api_token" = []),
214            ("bearer_token" = [])
215        ),
216        tag = "Node"
217    )]
218pub(super) async fn peers(
219    Query(NodePeersQueryRequest { score }): Query<NodePeersQueryRequest>,
220    State(state): State<Arc<InternalState>>,
221) -> Result<impl IntoResponse, ApiError> {
222    if !(0.0f64..=1.0f64).contains(&score) {
223        return Ok((StatusCode::BAD_REQUEST, ApiErrorStatus::InvalidQuality).into_response());
224    }
225
226    let hopr = state.hopr.clone();
227
228    let all_network_peers = futures::stream::iter(hopr.network_connected_peers().await?)
229        .filter_map(|peer| {
230            let hopr = hopr.clone();
231
232            async move {
233                // no observations recorded yet
234                let info = hopr.network_peer_info(&peer)?;
235
236                // peer score is low enough not to be considered
237                if info.score() < score {
238                    return None;
239                }
240
241                // no known chain address for the current peer can be found
242                let address = hopr.peerid_to_chain_key(&peer).await.ok().flatten()?;
243
244                let multiaddresses = hopr.network_observed_multiaddresses(&peer).await;
245
246                Some(PeerObservations {
247                    address,
248                    multiaddr: multiaddresses.first().cloned(),
249                    last_update: info.last_update().as_millis(),
250                    average_latency: info
251                        .immediate_qos()
252                        .and_then(|qos| qos.average_latency())
253                        .map_or(0, |latency| latency.as_millis()),
254                    probe_rate: info.immediate_qos().map_or(0.0, |qos| qos.average_probe_rate()),
255                    score: info.score(),
256                })
257            }
258        })
259        .collect::<Vec<_>>()
260        .await;
261
262    let announced_peers = hopr
263        .accounts_announced_on_chain()
264        .await?
265        .into_iter()
266        .map(|announced| async move {
267            AnnouncedPeer {
268                address: announced.chain_addr,
269                multiaddrs: announced.get_multiaddrs().to_vec(),
270            }
271        })
272        .collect::<FuturesUnordered<_>>()
273        .collect()
274        .await;
275
276    let body = NodePeersResponse {
277        connected: all_network_peers,
278        announced: announced_peers,
279    };
280
281    Ok((StatusCode::OK, Json(body)).into_response())
282}
283
284#[serde_as]
285#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
286#[schema(example = json!({
287        "announcedAddress": [
288            "/ip4/10.0.2.100/tcp/19092"
289        ],
290        "providerUrl": "https://staging.blokli.hoprnet.link",
291        "hoprNetworkName": "rotsee",
292        "channelClosurePeriod": 15,
293        "connectivityStatus": "Green",
294        "hoprNodeSafe": "0x42bc901b1d040f984ed626eff550718498a6798a",
295        "listeningAddress": [
296            "/ip4/10.0.2.100/tcp/19092"
297        ],
298    }))]
299#[serde(rename_all = "camelCase")]
300/// Information about the current node. Covers network, addresses, eligibility, connectivity status, contracts addresses
301/// and indexer state.
302pub(crate) struct NodeInfoResponse {
303    #[serde_as(as = "Vec<DisplayFromStr>")]
304    #[schema(value_type = Vec<String>, example = json!(["/ip4/10.0.2.100/tcp/19092"]))]
305    announced_address: Vec<Multiaddr>,
306    #[serde_as(as = "Vec<DisplayFromStr>")]
307    #[schema(value_type = Vec<String>, example = json!(["/ip4/10.0.2.100/tcp/19092"]))]
308    listening_address: Vec<Multiaddr>,
309    #[schema(value_type = String, example = "https://staging.blokli.hoprnet.link")]
310    provider_url: String,
311    #[schema(value_type = String, example = "rotsee")]
312    hopr_network_name: String,
313    #[serde(serialize_with = "checksum_address_serializer")]
314    #[schema(value_type = String, example = "0x42bc901b1d040f984ed626eff550718498a6798a")]
315    hopr_node_safe: Address,
316    #[serde_as(as = "DisplayFromStr")]
317    #[schema(value_type = String, example = "Green")]
318    connectivity_status: Health,
319    /// Channel closure period in seconds
320    #[schema(example = 15)]
321    channel_closure_period: u64,
322}
323
324/// Get information about this HOPR Node.
325#[utoipa::path(
326        get,
327        path = const_format::formatcp!("{BASE_PATH}/node/info"),
328        description = "Get information about this HOPR Node",
329        responses(
330            (status = 200, description = "Fetched node informations", body = NodeInfoResponse),
331            (status = 422, description = "Unknown failure", body = ApiError)
332        ),
333        security(
334            ("api_token" = []),
335            ("bearer_token" = [])
336        ),
337        tag = "Node"
338    )]
339pub(super) async fn info(State(state): State<Arc<InternalState>>) -> Result<impl IntoResponse, ApiError> {
340    let hopr = state.hopr.clone();
341
342    let safe_config = hopr.get_safe_config();
343
344    let provider_url = state
345        .hoprd_cfg
346        .as_object()
347        .and_then(|cfg| cfg.get("blokli_url"))
348        .and_then(|v| v.as_str());
349
350    match futures::try_join!(hopr.chain_info(), hopr.get_channel_closure_notice_period()) {
351        Ok((info, channel_closure_notice_period)) => {
352            let body = NodeInfoResponse {
353                announced_address: hopr.local_multiaddresses(),
354                listening_address: hopr.local_multiaddresses(),
355                provider_url: provider_url.unwrap_or("n/a").to_owned(),
356                hopr_network_name: info.hopr_network_name,
357                hopr_node_safe: safe_config.safe_address,
358                connectivity_status: hopr.network_health().await,
359                channel_closure_period: channel_closure_notice_period.as_secs(),
360            };
361
362            Ok((StatusCode::OK, Json(body)).into_response())
363        }
364        Err(error) => Ok((StatusCode::UNPROCESSABLE_ENTITY, ApiErrorStatus::from(error)).into_response()),
365    }
366}
367
368#[serde_as]
369#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
370#[serde(rename_all = "camelCase")]
371#[schema(example = json!({
372        "isEligible": true,
373        "multiaddrs": ["/ip4/10.0.2.100/tcp/19091"]
374}))]
375/// Reachable entry node information
376pub(crate) struct EntryNode {
377    #[serde_as(as = "Vec<DisplayFromStr>")]
378    #[schema(value_type = Vec<String>, example = json!(["/ip4/10.0.2.100/tcp/19091"]))]
379    multiaddrs: Vec<Multiaddr>,
380    #[schema(example = true)]
381    is_eligible: bool,
382}
383
384/// List all known entry nodes with multiaddrs and eligibility.
385#[utoipa::path(
386        get,
387        path = const_format::formatcp!("{BASE_PATH}/node/entry-nodes"),
388        description = "List all known entry nodes with multiaddrs and eligibility",
389        responses(
390            (status = 200, description = "Fetched public nodes' information", body = HashMap<String, EntryNode>, example = json!({
391                "0x188c4462b75e46f0c7262d7f48d182447b93a93c": {
392                    "isEligible": true,
393                    "multiaddrs": ["/ip4/10.0.2.100/tcp/19091"]
394                }
395            })),
396            (status = 401, description = "Invalid authorization token.", body = ApiError),
397            (status = 422, description = "Unknown failure", body = ApiError)
398        ),
399        security(
400            ("api_token" = []),
401            ("bearer_token" = [])
402        ),
403        tag = "Node"
404    )]
405pub(super) async fn entry_nodes(State(state): State<Arc<InternalState>>) -> Result<impl IntoResponse, ApiError> {
406    let hopr = state.hopr.clone();
407
408    match hopr.get_public_nodes().await {
409        Ok(nodes) => {
410            let mut body = HashMap::new();
411            for (_, address, mas) in nodes.into_iter() {
412                body.insert(
413                    address.to_string(),
414                    EntryNode {
415                        multiaddrs: mas,
416                        is_eligible: true,
417                    },
418                );
419            }
420
421            Ok((StatusCode::OK, Json(body)).into_response())
422        }
423        Err(error) => Ok((StatusCode::UNPROCESSABLE_ENTITY, ApiErrorStatus::from(error)).into_response()),
424    }
425}