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")]
22pub(crate) struct NodeVersionResponse {
24 #[schema(example = "2.1.0")]
25 version: String,
26}
27
28#[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#[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 }))]
78pub(crate) struct NodePeersQueryRequest {
80 #[serde(default)]
81 #[schema(required = false, example = 0.7)]
82 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")]
92pub(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}))]
111pub(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")]
136pub(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}))]
167pub(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#[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 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")]
313pub(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 #[schema(example = 15)]
344 channel_closure_period: u64,
345}
346
347#[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}))]
397pub(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#[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}