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#[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")]
46pub(crate) struct NodePeerInfoResponse {
48 #[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_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#[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")]
134pub(crate) struct PingResponse {
136 #[serde_as(as = "DurationMilliSeconds<u64>")]
137 #[schema(value_type = u64, example = 200)]
138 latency: std::time::Duration,
139}
140
141#[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}