1use std::{collections::HashMap, sync::Arc};
2
3use axum::{
4 extract::{Json, State},
5 http::status::StatusCode,
6 response::IntoResponse,
7};
8use hopr_lib::{
9 Address, Multiaddr,
10 api::{network::Health, node::HoprNodeNetworkOperations},
11};
12use serde::{Deserialize, Serialize};
13use serde_with::{DisplayFromStr, serde_as};
14
15use crate::{ApiError, ApiErrorStatus, BASE_PATH, InternalState, checksum_address_serializer};
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#[serde_as]
74#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
75#[schema(example = json!({
76 "announcedAddress": [
77 "/ip4/10.0.2.100/tcp/19092"
78 ],
79 "providerUrl": "https://staging.blokli.hoprnet.link",
80 "hoprNetworkName": "rotsee",
81 "channelClosurePeriod": 15,
82 "connectivityStatus": "Green",
83 "hoprNodeSafe": "0x42bc901b1d040f984ed626eff550718498a6798a",
84 "listeningAddress": [
85 "/ip4/10.0.2.100/tcp/19092"
86 ],
87 }))]
88#[serde(rename_all = "camelCase")]
89pub(crate) struct NodeInfoResponse {
92 #[serde_as(as = "Vec<DisplayFromStr>")]
93 #[schema(value_type = Vec<String>, example = json!(["/ip4/10.0.2.100/tcp/19092"]))]
94 announced_address: Vec<Multiaddr>,
95 #[serde_as(as = "Vec<DisplayFromStr>")]
96 #[schema(value_type = Vec<String>, example = json!(["/ip4/10.0.2.100/tcp/19092"]))]
97 listening_address: Vec<Multiaddr>,
98 #[schema(value_type = String, example = "https://staging.blokli.hoprnet.link")]
99 provider_url: String,
100 #[schema(value_type = String, example = "rotsee")]
101 hopr_network_name: String,
102 #[serde(serialize_with = "checksum_address_serializer")]
103 #[schema(value_type = String, example = "0x42bc901b1d040f984ed626eff550718498a6798a")]
104 hopr_node_safe: Address,
105 #[serde_as(as = "DisplayFromStr")]
106 #[schema(value_type = String, example = "Green")]
107 connectivity_status: Health,
108 #[schema(example = 15)]
110 channel_closure_period: u64,
111}
112
113#[utoipa::path(
115 get,
116 path = const_format::formatcp!("{BASE_PATH}/node/info"),
117 description = "Get information about this HOPR Node",
118 responses(
119 (status = 200, description = "Fetched node informations", body = NodeInfoResponse),
120 (status = 422, description = "Unknown failure", body = ApiError)
121 ),
122 security(
123 ("api_token" = []),
124 ("bearer_token" = [])
125 ),
126 tag = "Node"
127 )]
128pub(super) async fn info(State(state): State<Arc<InternalState>>) -> Result<impl IntoResponse, ApiError> {
129 let hopr = state.hopr.clone();
130
131 let safe_config = hopr.get_safe_config();
132
133 let provider_url = state
134 .hoprd_cfg
135 .as_object()
136 .and_then(|cfg| cfg.get("blokli_url"))
137 .and_then(|v| v.as_str());
138
139 match futures::try_join!(hopr.chain_info(), hopr.get_channel_closure_notice_period()) {
140 Ok((info, channel_closure_notice_period)) => {
141 let body = NodeInfoResponse {
142 announced_address: hopr.local_multiaddresses(),
143 listening_address: hopr.local_multiaddresses(),
144 provider_url: provider_url.unwrap_or("n/a").to_owned(),
145 hopr_network_name: info.hopr_network_name,
146 hopr_node_safe: safe_config.safe_address,
147 connectivity_status: hopr.network_health().await,
148 channel_closure_period: channel_closure_notice_period.as_secs(),
149 };
150
151 Ok((StatusCode::OK, Json(body)).into_response())
152 }
153 Err(error) => Ok((StatusCode::UNPROCESSABLE_ENTITY, ApiErrorStatus::from(error)).into_response()),
154 }
155}
156
157#[serde_as]
158#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
159#[serde(rename_all = "camelCase")]
160#[schema(example = json!({
161 "isEligible": true,
162 "multiaddrs": ["/ip4/10.0.2.100/tcp/19091"]
163}))]
164pub(crate) struct EntryNode {
166 #[serde_as(as = "Vec<DisplayFromStr>")]
167 #[schema(value_type = Vec<String>, example = json!(["/ip4/10.0.2.100/tcp/19091"]))]
168 multiaddrs: Vec<Multiaddr>,
169 #[schema(example = true)]
170 is_eligible: bool,
171}
172
173#[utoipa::path(
175 get,
176 path = const_format::formatcp!("{BASE_PATH}/node/entry-nodes"),
177 description = "List all known entry nodes with multiaddrs and eligibility",
178 responses(
179 (status = 200, description = "Fetched public nodes' information", body = HashMap<String, EntryNode>, example = json!({
180 "0x188c4462b75e46f0c7262d7f48d182447b93a93c": {
181 "isEligible": true,
182 "multiaddrs": ["/ip4/10.0.2.100/tcp/19091"]
183 }
184 })),
185 (status = 401, description = "Invalid authorization token.", body = ApiError),
186 (status = 422, description = "Unknown failure", body = ApiError)
187 ),
188 security(
189 ("api_token" = []),
190 ("bearer_token" = [])
191 ),
192 tag = "Node"
193 )]
194pub(super) async fn entry_nodes(State(state): State<Arc<InternalState>>) -> Result<impl IntoResponse, ApiError> {
195 let hopr = state.hopr.clone();
196
197 match hopr.get_public_nodes().await {
198 Ok(nodes) => {
199 let mut body = HashMap::new();
200 for (_, address, mas) in nodes.into_iter() {
201 body.insert(
202 address.to_string(),
203 EntryNode {
204 multiaddrs: mas,
205 is_eligible: true,
206 },
207 );
208 }
209
210 Ok((StatusCode::OK, Json(body)).into_response())
211 }
212 Err(error) => Ok((StatusCode::UNPROCESSABLE_ENTITY, ApiErrorStatus::from(error)).into_response()),
213 }
214}