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")]
27pub(crate) struct NodeVersionResponse {
29 #[schema(example = "2.1.0")]
30 version: String,
31}
32
33#[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#[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 }))]
83pub(crate) struct NodePeersQueryRequest {
85 #[serde(default)]
86 #[schema(required = false, example = 0.7)]
87 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")]
97pub(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}))]
116pub(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")]
141pub(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}))]
172pub(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#[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 let info = hopr.network_peer_info(&peer)?;
235
236 if info.score() < score {
238 return None;
239 }
240
241 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")]
300pub(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 #[schema(example = 15)]
321 channel_closure_period: u64,
322}
323
324#[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}))]
375pub(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#[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}