Skip to main content

hoprd_api/
node.rs

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")]
22/// Running node version.
23pub(crate) struct NodeVersionResponse {
24    #[schema(example = "2.1.0")]
25    version: String,
26}
27
28/// Get the release version of the running node.
29#[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/// Get the configuration of the running node.
49#[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")]
89/// Information about the current node. Covers network, addresses, eligibility, connectivity status, contracts addresses
90/// and indexer state.
91pub(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    /// Channel closure period in seconds
109    #[schema(example = 15)]
110    channel_closure_period: u64,
111}
112
113/// Get information about this HOPR Node.
114#[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}))]
164/// Reachable entry node information
165pub(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/// List all known entry nodes with multiaddrs and eligibility.
174#[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}