Skip to main content

hoprd_api/
peers.rs

1use std::sync::Arc;
2
3use axum::{
4    extract::{Json, Path, State},
5    http::status::StatusCode,
6    response::IntoResponse,
7};
8use futures::FutureExt;
9use hopr_lib::{
10    Address, Multiaddr,
11    api::node::HoprNodeNetworkOperations,
12    errors::{HoprLibError, HoprStatusError, HoprTransportError},
13};
14use serde::{Deserialize, Serialize};
15use serde_with::{DisplayFromStr, DurationMilliSeconds, serde_as};
16use tracing::debug;
17
18use crate::{ApiError, ApiErrorStatus, BASE_PATH, InternalState, network::AnnouncementOriginResponse};
19
20/// A multiaddress paired with its discovery origin.
21#[serde_as]
22#[derive(Debug, Clone, serde::Serialize, utoipa::ToSchema)]
23#[schema(example = json!({
24    "multiaddress": "/ip4/10.0.2.100/tcp/19093",
25    "origin": "chain"
26}))]
27#[serde(rename_all = "camelCase")]
28pub(crate) struct MultiaddressSource {
29    #[serde_as(as = "DisplayFromStr")]
30    #[schema(value_type = String, example = "/ip4/10.0.2.100/tcp/19093")]
31    multiaddress: Multiaddr,
32    #[schema(example = "chain")]
33    origin: AnnouncementOriginResponse,
34}
35
36#[serde_as]
37#[derive(Debug, Clone, serde::Serialize, utoipa::ToSchema)]
38#[schema(example = json!({
39    "announced": ["/ip4/10.0.2.100/tcp/19093"],
40    "announcedSources": [
41        { "multiaddress": "/ip4/10.0.2.100/tcp/19093", "origin": "chain" }
42    ],
43    "observed": ["/ip4/10.0.2.100/tcp/19093"]
44}))]
45#[serde(rename_all = "camelCase")]
46/// Contains the multiaddresses of peers that are `announced` on-chain and `observed` by the node.
47pub(crate) struct NodePeerInfoResponse {
48    /// Flat list of announced multiaddresses (legacy, for backward compatibility).
49    #[serde_as(as = "Vec<DisplayFromStr>")]
50    #[schema(value_type = Vec<String>, example = json!(["/ip4/10.0.2.100/tcp/19093"]))]
51    announced: Vec<Multiaddr>,
52    /// Announced multiaddresses grouped by discovery origin.
53    announced_sources: Vec<MultiaddressSource>,
54    #[serde_as(as = "Vec<DisplayFromStr>")]
55    #[schema(value_type = Vec<String>, example = json!(["/ip4/10.0.2.100/tcp/19093"]))]
56    observed: Vec<Multiaddr>,
57}
58
59#[serde_as]
60#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
61#[serde(rename_all = "camelCase")]
62pub(crate) struct DestinationParams {
63    #[serde_as(as = "DisplayFromStr")]
64    #[schema(value_type = String)]
65    destination: Address,
66}
67
68/// Returns transport-related information about the given peer.
69///
70/// This includes the peer ids that the given peer has `announced` on-chain
71/// and peer ids that are actually `observed` by the transport layer.
72#[utoipa::path(
73    get,
74    path = const_format::formatcp!("{BASE_PATH}/peers/{{destination}}"),
75    params(
76        ("destination" = String, Path, description = "Address of the requested peer", example = "0x07eaf07d6624f741e04f4092a755a9027aaab7f6"),
77    ),
78    responses(
79        (status = 200, description = "Peer information fetched successfully.", body = NodePeerInfoResponse),
80        (status = 400, description = "Invalid destination", body = ApiError),
81        (status = 401, description = "Invalid authorization token.", body = ApiError),
82        (status = 422, description = "Unknown failure", body = ApiError)
83    ),
84    security(
85        ("api_token" = []),
86        ("bearer_token" = [])
87    ),
88    tag = "Peers",
89)]
90pub(super) async fn show_peer_info(
91    Path(DestinationParams { destination }): Path<DestinationParams>,
92    State(state): State<Arc<InternalState>>,
93) -> impl IntoResponse {
94    let hopr = state.hopr.clone();
95
96    match hopr.chain_key_to_peerid(&destination).await {
97        Ok(Some(peer)) => {
98            let res = futures::try_join!(
99                hopr.multiaddresses_announced_on_chain(&peer),
100                hopr.network_observed_multiaddresses(&peer).map(Ok)
101            );
102            match res {
103                Ok((announced, observed)) => {
104                    let announced_sources: Vec<MultiaddressSource> = announced
105                        .iter()
106                        .map(|ma| MultiaddressSource {
107                            multiaddress: ma.clone(),
108                            origin: AnnouncementOriginResponse::Chain,
109                        })
110                        .collect();
111                    Ok((
112                        StatusCode::OK,
113                        Json(NodePeerInfoResponse {
114                            announced,
115                            announced_sources,
116                            observed,
117                        }),
118                    ))
119                }
120                Err(error) => Err(ApiErrorStatus::UnknownFailure(error.to_string())),
121            }
122        }
123        Ok(None) => Err(ApiErrorStatus::PeerNotFound),
124        Err(_) => Err(ApiErrorStatus::PeerNotFound),
125    }
126}
127
128#[serde_as]
129#[derive(Debug, Clone, serde::Serialize, utoipa::ToSchema)]
130#[schema(example = json!({
131    "latency": 200,
132}))]
133#[serde(rename_all = "camelCase")]
134/// Contains the latency and the reported version of a peer that has been pinged.
135pub(crate) struct PingResponse {
136    #[serde_as(as = "DurationMilliSeconds<u64>")]
137    #[schema(value_type = u64, example = 200)]
138    latency: std::time::Duration,
139}
140
141/// Directly pings the given peer.
142#[utoipa::path(
143    post,
144    path = const_format::formatcp!("{BASE_PATH}/peers/{{destination}}/ping"),
145    description = "Directly ping the given peer",
146    params(
147        ("destination" = String, Path, description = "Address of the requested peer", example = "0x07eaf07d6624f741e04f4092a755a9027aaab7f6"),
148    ),
149    responses(
150        (status = 200, description = "Ping successful", body = PingResponse),
151        (status = 400, description = "Invalid peer id", body = ApiError),
152        (status = 401, description = "Invalid authorization token.", body = ApiError),
153        (status = 404, description = "Peer id not found in the network.", body = ApiError),
154        (status = 408, description = "Peer timed out.", body = ApiError),
155        (status = 412, description = "The node is not ready."),
156        (status = 422, description = "Unknown failure", body = ApiError)
157    ),
158    security(
159        ("api_token" = []),
160        ("bearer_token" = [])
161    ),
162    tag = "Peers",
163)]
164pub(super) async fn ping_peer(
165    Path(DestinationParams { destination }): Path<DestinationParams>,
166    State(state): State<Arc<InternalState>>,
167) -> Result<impl IntoResponse, ApiError> {
168    debug!(%destination, "Manually ping peer");
169
170    let hopr = state.hopr.clone();
171
172    match hopr.chain_key_to_peerid(&destination).await {
173        Ok(Some(peer)) => match hopr.ping(&peer).await {
174            Ok((latency, _status)) => {
175                let resp = Json(PingResponse { latency: latency / 2 });
176                Ok((StatusCode::OK, resp).into_response())
177            }
178            Err(HoprLibError::TransportError(HoprTransportError::Protocol(
179                hopr_lib::errors::ProtocolError::Timeout,
180            ))) => Ok((StatusCode::REQUEST_TIMEOUT, ApiErrorStatus::Timeout).into_response()),
181            Err(HoprLibError::TransportError(HoprTransportError::Probe(hopr_lib::ProbeError::TrafficError(_)))) => {
182                Ok((StatusCode::REQUEST_TIMEOUT, ApiErrorStatus::Timeout).into_response())
183            }
184            Err(HoprLibError::TransportError(HoprTransportError::Probe(hopr_lib::ProbeError::PingerError(e)))) => {
185                Ok((StatusCode::UNPROCESSABLE_ENTITY, ApiErrorStatus::PingError(e)).into_response())
186            }
187            Err(HoprLibError::TransportError(HoprTransportError::Probe(hopr_lib::ProbeError::NonExistingPeer))) => {
188                Ok((StatusCode::NOT_FOUND, ApiErrorStatus::PeerNotFound).into_response())
189            }
190            Err(HoprLibError::StatusError(HoprStatusError::NotThereYet(..))) => {
191                Ok((StatusCode::PRECONDITION_FAILED, ApiErrorStatus::NotReady).into_response())
192            }
193            Err(e) => Ok((StatusCode::UNPROCESSABLE_ENTITY, ApiErrorStatus::from(e)).into_response()),
194        },
195        Ok(None) => Ok((StatusCode::NOT_FOUND, ApiErrorStatus::PeerNotFound).into_response()),
196        Err(_) => Ok((StatusCode::UNPROCESSABLE_ENTITY, ApiErrorStatus::PeerNotFound).into_response()),
197    }
198}
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203
204    #[test]
205    fn multiaddress_source_should_serialize_with_single_multiaddress() -> anyhow::Result<()> {
206        let source = MultiaddressSource {
207            multiaddress: "/ip4/1.2.3.4/tcp/9091".parse()?,
208            origin: AnnouncementOriginResponse::Chain,
209        };
210
211        let json = serde_json::to_value(&source)?;
212        assert_eq!(json["multiaddress"], "/ip4/1.2.3.4/tcp/9091");
213        assert_eq!(json["origin"], "chain");
214        Ok(())
215    }
216
217    #[test]
218    fn node_peer_info_response_should_include_both_announced_fields() -> anyhow::Result<()> {
219        let ma: Multiaddr = "/ip4/10.0.2.100/tcp/19093".parse()?;
220        let response = NodePeerInfoResponse {
221            announced: vec![ma.clone()],
222            announced_sources: vec![MultiaddressSource {
223                multiaddress: ma,
224                origin: AnnouncementOriginResponse::Chain,
225            }],
226            observed: vec!["/ip4/10.0.2.100/tcp/19094".parse()?],
227        };
228
229        let json = serde_json::to_value(&response)?;
230        assert!(json["announced"].is_array());
231        assert_eq!(json["announced"][0], "/ip4/10.0.2.100/tcp/19093");
232        assert!(json["announcedSources"].is_array());
233        assert_eq!(json["announcedSources"][0]["multiaddress"], "/ip4/10.0.2.100/tcp/19093");
234        assert_eq!(json["announcedSources"][0]["origin"], "chain");
235        assert!(json["observed"].is_array());
236        Ok(())
237    }
238
239    #[test]
240    fn node_peer_info_response_should_serialize_empty_sources_when_no_announcements() -> anyhow::Result<()> {
241        let response = NodePeerInfoResponse {
242            announced: vec![],
243            announced_sources: vec![],
244            observed: vec!["/ip4/10.0.2.100/tcp/19094".parse()?],
245        };
246
247        let json = serde_json::to_value(&response)?;
248        assert_eq!(json["announced"].as_array().unwrap().len(), 0);
249        assert_eq!(json["announcedSources"].as_array().unwrap().len(), 0);
250        Ok(())
251    }
252}