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::{Address, Health, Multiaddr, api::network::Observable};
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")]
22/// Running node version.
23pub(crate) struct NodeVersionResponse {
24    #[schema(example = "2.1.0")]
25    version: String,
26}
27
28/// Get the release version of the running node.
29#[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/// Get the configuration of the running node.
49#[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    }))]
78/// Quality information for a peer.
79pub(crate) struct NodePeersQueryRequest {
80    #[serde(default)]
81    #[schema(required = false, example = 0.7)]
82    /// Minimum peer quality to be included in the response.
83    score: 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")]
92/// Heartbeat information for a peer.
93pub(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    "probeRate": 0.476,
107    "lastSeen": 1690000000,
108    "averageLatency": 100,
109    "score": 0.7,
110}))]
111/// All information about a known peer.
112pub(crate) struct PeerObservations {
113    #[serde(serialize_with = "option_checksum_address_serializer")]
114    #[schema(value_type = Option<String>, example = "0xb4ce7e6e36ac8b01a974725d5ba730af2b156fbe")]
115    address: Option<Address>,
116    #[serde_as(as = "Option<DisplayFromStr>")]
117    #[schema(value_type = Option<String>, example = "/ip4/178.12.1.9/tcp/19092")]
118    multiaddr: Option<Multiaddr>,
119    #[schema(example = 0.476)]
120    probe_rate: f64,
121    #[schema(example = 1690000000)]
122    last_update: u128,
123    #[schema(example = 100)]
124    average_latency: u128,
125    #[schema(example = 0.7)]
126    score: f64,
127}
128
129#[serde_as]
130#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
131#[schema(example = json!({
132    "address": "0xb4ce7e6e36ac8b01a974725d5ba730af2b156fbe",
133    "multiaddrs": "[/ip4/178.12.1.9/tcp/19092]"
134}))]
135#[serde(rename_all = "camelCase")]
136/// Represents a peer that has been announced on-chain.
137pub(crate) struct AnnouncedPeer {
138    #[serde(serialize_with = "checksum_address_serializer")]
139    #[schema(value_type = String, example = "0xb4ce7e6e36ac8b01a974725d5ba730af2b156fbe")]
140    address: Address,
141    #[serde_as(as = "Vec<DisplayFromStr>")]
142    #[schema(value_type = Vec<String>, example = "[/ip4/178.12.1.9/tcp/19092]")]
143    multiaddrs: Vec<Multiaddr>,
144}
145
146#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
147#[serde(rename_all = "camelCase")]
148#[schema(example = json!({
149    "connected": [{
150        "address": "0xb4ce7e6e36ac8b01a974725d5ba730af2b156fbe",
151        "multiaddr": "/ip4/178.12.1.9/tcp/19092",
152        "heartbeats": {
153            "sent": 10,
154            "success": 10
155        },
156        "lastSeen": 1690000000,
157        "lastSeenLatency": 100,
158        "quality": 0.7,
159        "backoff": 0.5,
160        "isNew": true,
161    }],
162    "announced": [{
163        "address": "0xb4ce7e6e36ac8b01a974725d5ba730af2b156fbe",
164        "multiaddr": "/ip4/178.12.1.9/tcp/19092"
165    }]
166}))]
167/// All connected and announced peers.
168pub(crate) struct NodePeersResponse {
169    #[schema(example = json!([{
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    }]))]
182    connected: Vec<PeerObservations>,
183    #[schema(example = json!([{
184        "address": "0xb4ce7e6e36ac8b01a974725d5ba730af2b156fbe",
185        "multiaddr": "/ip4/178.12.1.9/tcp/19092"
186    }]))]
187    announced: Vec<AnnouncedPeer>,
188}
189
190/// Lists information for `connected peers` and `announced peers`.
191///
192/// Connected peers are nodes which are connected to the node while announced peers are
193/// nodes which have announced to the network.
194///
195/// Optionally pass `quality` parameter to get only peers with higher or equal quality
196/// to the specified value.
197#[utoipa::path(
198        get,
199        path = const_format::formatcp!("{BASE_PATH}/node/peers"),
200        description = "Lists information for connected and announced peers",
201        params(NodePeersQueryRequest),
202        responses(
203            (status = 200, description = "Successfully returned observed peers", body = NodePeersResponse),
204            (status = 400, description = "Failed to extract a valid quality parameter", body = ApiError),
205            (status = 401, description = "Invalid authorization token.", body = ApiError),
206        ),
207        security(
208            ("api_token" = []),
209            ("bearer_token" = [])
210        ),
211        tag = "Node"
212    )]
213pub(super) async fn peers(
214    Query(NodePeersQueryRequest { score }): Query<NodePeersQueryRequest>,
215    State(state): State<Arc<InternalState>>,
216) -> Result<impl IntoResponse, ApiError> {
217    if !(0.0f64..=1.0f64).contains(&score) {
218        return Ok((StatusCode::BAD_REQUEST, ApiErrorStatus::InvalidQuality).into_response());
219    }
220
221    let hopr = state.hopr.clone();
222
223    let all_network_peers = futures::stream::iter(hopr.network_connected_peers().await?)
224        .filter_map(|peer| {
225            let hopr = hopr.clone();
226
227            async move {
228                if let Ok(Some(info)) = hopr.network_peer_info(&peer).await {
229                    if info.score() >= score {
230                        Some((peer, info))
231                    } else {
232                        None
233                    }
234                } else {
235                    None
236                }
237            }
238        })
239        .filter_map(|(peer_id, info)| {
240            let hopr = hopr.clone();
241
242            async move {
243                let address = hopr.peerid_to_chain_key(&peer_id).await.ok().flatten();
244
245                // WARNING: Only in Providence and Saint-Louis are all peers public
246                let multiaddresses = hopr.network_observed_multiaddresses(&peer_id).await;
247
248                Some((address, multiaddresses, info))
249            }
250        })
251        .map(|(address, mas, info)| PeerObservations {
252            address,
253            multiaddr: mas.first().cloned(),
254            last_update: info.last_update().as_millis(),
255            average_latency: info.average_latency().map_or(0, |d| d.as_millis()),
256            probe_rate: info.average_probe_rate(),
257            score: info.score(),
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        "chain": "anvil-localhost",
291        "provider": "http://127.0.0.1:8545",
292        "channelClosurePeriod": 15,
293        "connectivityStatus": "Green",
294        "hoprChannels": "0x9a9f2ccfde556a7e9ff0848998aa4a0cfd8863ae",
295        "hoprManagementModule": "0xa51c1fc2f0d1a1b8494ed1fe312d7c3a78ed91c0",
296        "hoprNetworkRegistry": "0x3aa5ebb10dc797cac828524e59a333d0a371443c",
297        "hoprNodeSafe": "0x42bc901b1d040f984ed626eff550718498a6798a",
298        "hoprNodeSafeRegistry": "0x0dcd1bf9a1b36ce34237eeafef220932846bcd82",
299        "hoprToken": "0x9a676e781a523b5d0c0e43731313a708cb607508",
300        "isEligible": true,
301        "listeningAddress": [
302            "/ip4/10.0.2.100/tcp/19092"
303        ],
304        "network": "anvil-localhost",
305        "indexerBlock": 123456,
306        "indexerChecksum": "0000000000000000000000000000000000000000000000000000000000000000",
307        "indexBlockPrevChecksum": 0,
308        "indexerLastLogBlock": 123450,
309        "indexerLastLogChecksum": "cfde556a7e9ff0848998aa4a9a9f2ccfde556a7e9ff0848998aa4a0cfd8863ae",
310        "isIndexerCorrupted": false,
311    }))]
312#[serde(rename_all = "camelCase")]
313/// Information about the current node. Covers network, addresses, eligibility, connectivity status, contracts addresses
314/// and indexer state.
315pub(crate) struct NodeInfoResponse {
316    #[serde_as(as = "Vec<DisplayFromStr>")]
317    #[schema(value_type = Vec<String>, example = json!(["/ip4/10.0.2.100/tcp/19092"]))]
318    announced_address: Vec<Multiaddr>,
319    #[serde_as(as = "Vec<DisplayFromStr>")]
320    #[schema(value_type = Vec<String>, example = json!(["/ip4/10.0.2.100/tcp/19092"]))]
321    listening_address: Vec<Multiaddr>,
322    #[schema(example = "anvil-localhost")]
323    chain: String,
324    #[serde(serialize_with = "checksum_address_serializer")]
325    #[schema(value_type = String, example = "0x9a676e781a523b5d0c0e43731313a708cb607508")]
326    hopr_token: Address,
327    #[serde(serialize_with = "checksum_address_serializer")]
328    #[schema(value_type = String, example = "0x9a9f2ccfde556a7e9ff0848998aa4a0cfd8863ae")]
329    hopr_channels: Address,
330    #[serde(serialize_with = "checksum_address_serializer")]
331    #[schema(value_type = String, example = "0x0dcd1bf9a1b36ce34237eeafef220932846bcd82")]
332    hopr_node_safe_registry: Address,
333    #[serde(serialize_with = "checksum_address_serializer")]
334    #[schema(value_type = String, example = "0xa51c1fc2f0d1a1b8494ed1fe312d7c3a78ed91c0")]
335    hopr_management_module: Address,
336    #[serde(serialize_with = "checksum_address_serializer")]
337    #[schema(value_type = String, example = "0x42bc901b1d040f984ed626eff550718498a6798a")]
338    hopr_node_safe: Address,
339    #[serde_as(as = "DisplayFromStr")]
340    #[schema(value_type = String, example = "Green")]
341    connectivity_status: Health,
342    /// Channel closure period in seconds
343    #[schema(example = 15)]
344    channel_closure_period: u64,
345}
346
347/// Get information about this HOPR Node.
348#[utoipa::path(
349        get,
350        path = const_format::formatcp!("{BASE_PATH}/node/info"),
351        description = "Get information about this HOPR Node",
352        responses(
353            (status = 200, description = "Fetched node informations", body = NodeInfoResponse),
354            (status = 422, description = "Unknown failure", body = ApiError)
355        ),
356        security(
357            ("api_token" = []),
358            ("bearer_token" = [])
359        ),
360        tag = "Node"
361    )]
362pub(super) async fn info(State(state): State<Arc<InternalState>>) -> Result<impl IntoResponse, ApiError> {
363    let hopr = state.hopr.clone();
364
365    let safe_config = hopr.get_safe_config();
366
367    let chain_data = futures::try_join!(hopr.get_channel_closure_notice_period(), hopr.chain_info());
368
369    match chain_data {
370        Ok((channel_closure_notice_period, chain_info)) => {
371            let body = NodeInfoResponse {
372                announced_address: hopr.local_multiaddresses(),
373                listening_address: hopr.local_multiaddresses(),
374                chain: chain_info.chain_id.to_string(),
375                hopr_token: Address::new(&chain_info.contract_addresses.token.0.0),
376                hopr_channels: Address::new(&chain_info.contract_addresses.channels.0.0),
377                hopr_node_safe_registry: Address::new(&chain_info.contract_addresses.node_safe_registry.0.0),
378                hopr_management_module: Address::new(&chain_info.contract_addresses.module_implementation.0.0),
379                hopr_node_safe: safe_config.safe_address,
380                connectivity_status: hopr.network_health().await,
381                channel_closure_period: channel_closure_notice_period.as_secs(),
382            };
383
384            Ok((StatusCode::OK, Json(body)).into_response())
385        }
386        Err(error) => Ok((StatusCode::UNPROCESSABLE_ENTITY, ApiErrorStatus::from(error)).into_response()),
387    }
388}
389
390#[serde_as]
391#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
392#[serde(rename_all = "camelCase")]
393#[schema(example = json!({
394        "isEligible": true,
395        "multiaddrs": ["/ip4/10.0.2.100/tcp/19091"]
396}))]
397/// Reachable entry node information
398pub(crate) struct EntryNode {
399    #[serde_as(as = "Vec<DisplayFromStr>")]
400    #[schema(value_type = Vec<String>, example = json!(["/ip4/10.0.2.100/tcp/19091"]))]
401    multiaddrs: Vec<Multiaddr>,
402    #[schema(example = true)]
403    is_eligible: bool,
404}
405
406/// List all known entry nodes with multiaddrs and eligibility.
407#[utoipa::path(
408        get,
409        path = const_format::formatcp!("{BASE_PATH}/node/entry-nodes"),
410        description = "List all known entry nodes with multiaddrs and eligibility",
411        responses(
412            (status = 200, description = "Fetched public nodes' information", body = HashMap<String, EntryNode>, example = json!({
413                "0x188c4462b75e46f0c7262d7f48d182447b93a93c": {
414                    "isEligible": true,
415                    "multiaddrs": ["/ip4/10.0.2.100/tcp/19091"]
416                }
417            })),
418            (status = 401, description = "Invalid authorization token.", body = ApiError),
419            (status = 422, description = "Unknown failure", body = ApiError)
420        ),
421        security(
422            ("api_token" = []),
423            ("bearer_token" = [])
424        ),
425        tag = "Node"
426    )]
427pub(super) async fn entry_nodes(State(state): State<Arc<InternalState>>) -> Result<impl IntoResponse, ApiError> {
428    let hopr = state.hopr.clone();
429
430    match hopr.get_public_nodes().await {
431        Ok(nodes) => {
432            let mut body = HashMap::new();
433            for (_, address, mas) in nodes.into_iter() {
434                body.insert(
435                    address.to_string(),
436                    EntryNode {
437                        multiaddrs: mas,
438                        is_eligible: true,
439                    },
440                );
441            }
442
443            Ok((StatusCode::OK, Json(body)).into_response())
444        }
445        Err(error) => Ok((StatusCode::UNPROCESSABLE_ENTITY, ApiErrorStatus::from(error)).into_response()),
446    }
447}