hoprd_api/
alias.rs

1use axum::{
2    extract::{Json, Path, State},
3    http::status::StatusCode,
4    response::IntoResponse,
5};
6use hopr_lib::Address;
7use serde::{Deserialize, Serialize};
8use serde_with::{serde_as, DisplayFromStr};
9use std::{collections::HashMap, str::FromStr, sync::Arc};
10
11use hoprd_db_api::aliases::HoprdDbAliasesOperations;
12use hoprd_db_api::errors::DbError;
13
14use crate::{
15    types::{HoprIdentifier, PeerOrAddress},
16    ApiError, ApiErrorStatus, InternalState, BASE_PATH,
17};
18
19#[serde_as]
20#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
21#[schema(example = json!({
22        "peerId": "12D3KooWRWeTozREYHzWTbuCYskdYhED1MXpDwTrmccwzFrd2mEA"
23    }))]
24#[serde(rename_all = "camelCase")]
25pub(crate) struct PeerIdResponse {
26    #[serde_as(as = "DisplayFromStr")]
27    #[schema(value_type = String)]
28    pub peer_id: String,
29}
30
31#[serde_as]
32#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
33#[schema(example = json!({
34        "address": "0x07eaf07d6624f741e04f4092a755a9027aaab7f6"
35    }))]
36#[serde(rename_all = "camelCase")]
37pub(crate) struct AddressResponse {
38    #[serde_as(as = "DisplayFromStr")]
39    #[schema(value_type = String)]
40    pub address: Address,
41}
42
43#[serde_as]
44#[derive(Debug, Clone, Deserialize, utoipa::ToSchema)]
45#[schema(example = json!({
46        "alias": "Alice",
47        "destination": "12D3KooWRWeTozREYHzWTbuCYskdYhED1MXpDwTrmccwzFrd2mEA"
48    }))]
49#[serde(rename_all = "camelCase")]
50pub(crate) struct AliasDestinationBodyRequest {
51    pub alias: String,
52    #[serde_as(as = "DisplayFromStr")]
53    #[schema(value_type = String)]
54    pub destination: PeerOrAddress,
55}
56
57/// (deprecated, will be removed in v3.0) Get each previously set alias and its corresponding PeerId as a hashmap.
58#[utoipa::path(
59        get,
60        path = const_format::formatcp!("{BASE_PATH}/aliases"),
61        responses(
62            (status = 200, description = "Each alias with its corresponding PeerId", body = HashMap<String, String>, example = json!({
63                    "alice": "12D3KooWPWD5P5ZzMRDckgfVaicY5JNoo7JywGotoAv17d7iKx1z",
64                    "me": "12D3KooWJmLm8FnBfvYQ5BAZ5qcYBxQFFBzAAEYUBUNJNE8cRsYS"
65            })),
66            (status = 401, description = "Invalid authorization token.", body = ApiError),
67            (status = 404, description = "No aliases found", body = ApiError),
68        ),
69        security(
70            ("api_token" = []),
71            ("bearer_token" = [])
72        ),
73        tag = "Alias",
74    )]
75pub(super) async fn aliases(State(state): State<Arc<InternalState>>) -> impl IntoResponse {
76    let aliases = state.hoprd_db.get_aliases().await;
77
78    match aliases {
79        Ok(aliases) => {
80            let aliases = aliases
81                .iter()
82                .map(|alias| (alias.alias.clone(), alias.peer_id.clone()))
83                .collect::<HashMap<_, _>>();
84            (StatusCode::OK, Json(aliases)).into_response()
85        }
86        Err(DbError::BackendError(_)) => (StatusCode::NOT_FOUND, ApiErrorStatus::AliasNotFound).into_response(),
87        Err(_) => (StatusCode::INTERNAL_SERVER_ERROR, ApiErrorStatus::DatabaseError).into_response(),
88    }
89}
90
91// TODO(deprecated, will be removed in v3.0) Get each previously set alias and its corresponding ETH address as a hashmap.
92#[utoipa::path(
93        get,
94        path = const_format::formatcp!("{BASE_PATH}/aliases_addresses"),
95        responses(
96            (status = 200, description = "Each alias with its corresponding Address", body = HashMap<String, String>, example = json!({
97                    "alice": "0xb4ce7e6e36ac8b01a974725d5ba730af2b156fbe",
98                    "me": "0x07eaf07d6624f741e04f4092a755a9027aaab7f6"
99            })),
100            (status = 401, description = "Invalid authorization token.", body = ApiError),
101            (status = 404, description = "No aliases found", body = ApiError),
102        ),
103        security(
104            ("api_token" = []),
105            ("bearer_token" = [])
106        ),
107        tag = "Alias",
108    )]
109pub(super) async fn aliases_addresses(State(state): State<Arc<InternalState>>) -> impl IntoResponse {
110    let aliases = state.hoprd_db.get_aliases().await;
111
112    match aliases {
113        Ok(aliases) => {
114            let aliases = aliases
115                .into_iter()
116                .map(|alias| (alias.alias.clone(), alias.peer_id.clone()))
117                .collect::<HashMap<_, _>>();
118
119            let aliases_addresses_futures = aliases.into_iter().map(|(alias, peer_id)| {
120                let hopr = state.hopr.clone();
121                async move {
122                    let peer_or_address = match PeerOrAddress::from_str(peer_id.as_str()) {
123                        Ok(destination) => destination,
124                        Err(_) => return (alias, ApiErrorStatus::PeerNotFound.to_string()),
125                    };
126
127                    match HoprIdentifier::new_with(peer_or_address, hopr.peer_resolver()).await {
128                        Ok(destination) => (alias, destination.address.to_string()),
129                        Err(_) => (alias, ApiErrorStatus::PeerNotFound.to_string()),
130                    }
131                }
132            });
133
134            let aliases_addresses = futures::future::join_all(aliases_addresses_futures)
135                .await
136                .into_iter()
137                .collect::<HashMap<_, _>>();
138
139            (StatusCode::OK, Json(aliases_addresses)).into_response()
140        }
141        Err(DbError::BackendError(_)) => (StatusCode::NOT_FOUND, ApiErrorStatus::AliasNotFound).into_response(),
142        Err(_) => (StatusCode::INTERNAL_SERVER_ERROR, ApiErrorStatus::DatabaseError).into_response(),
143    }
144}
145
146/// (deprecated, will be removed in v3.0) Set alias for a peer with a specific PeerId.
147#[utoipa::path(
148        post,
149        path = const_format::formatcp!("{BASE_PATH}/aliases"),
150        request_body(
151            content = AliasDestinationBodyRequest,
152            description = "Alias name along with the PeerId to be aliased",
153            content_type = "application/json"),
154        responses(
155            (status = 201, description = "Alias set successfully.", body = PeerIdResponse),
156            (status = 400, description = "Invalid PeerId: The format or length of the peerId is incorrect.", body = ApiError),
157            (status = 401, description = "Invalid authorization token.", body = ApiError),
158            (status = 409, description = "Either alias exists or the peer_id is already aliased.", body = ApiError),
159            (status = 422, description = "Unknown failure", body = ApiError)
160        ),
161        security(
162            ("api_token" = []),
163            ("bearer_token" = [])
164        ),
165        tag = "Alias",
166    )]
167pub(super) async fn set_alias(
168    State(state): State<Arc<InternalState>>,
169    Json(args): Json<AliasDestinationBodyRequest>,
170) -> impl IntoResponse {
171    let hopr = state.hopr.clone();
172
173    match HoprIdentifier::new_with(args.destination, hopr.peer_resolver()).await {
174        Ok(destination) => match state
175            .hoprd_db
176            .set_alias(destination.peer_id.to_string(), args.alias)
177            .await
178        {
179            Ok(()) => (
180                StatusCode::CREATED,
181                Json(PeerIdResponse {
182                    peer_id: destination.peer_id.to_string(),
183                }),
184            )
185                .into_response(),
186            Err(DbError::LogicalError(_)) => (StatusCode::BAD_REQUEST, ApiErrorStatus::PeerNotFound).into_response(),
187            Err(DbError::AliasOrPeerIdAlreadyExists) => {
188                (StatusCode::CONFLICT, ApiErrorStatus::AliasOrPeerIdAliasAlreadyExists).into_response()
189            }
190            Err(e) => (StatusCode::UNPROCESSABLE_ENTITY, ApiErrorStatus::from(e)).into_response(),
191        },
192        Err(e) => (StatusCode::NOT_FOUND, e).into_response(),
193    }
194}
195
196#[serde_as]
197#[derive(Deserialize, utoipa::ToSchema)]
198#[schema(example = json!({
199    "alias": "Alice",
200}))]
201#[serde(rename_all = "camelCase")]
202pub(crate) struct GetAliasRequest {
203    alias: String,
204}
205
206/// (deprecated, will be removed in v3.0) Get alias for the PeerId (Hopr address) that have this alias assigned to it.
207#[utoipa::path(
208        get,
209        path = const_format::formatcp!("{BASE_PATH}/aliases/{{alias}}"),
210        params(
211            ("alias" = String, Path, description = "Alias to be shown"),
212        ),
213        responses(
214            (status = 200, description = "Get PeerId for an alias", body = PeerIdResponse),
215            (status = 401, description = "Invalid authorization token.", body = ApiError),
216            (status = 404, description = "PeerId not found", body = ApiError),
217        ),
218        security(
219            ("api_token" = []),
220            ("bearer_token" = [])
221        ),
222        tag = "Alias",
223    )]
224pub(super) async fn get_alias(
225    Path(GetAliasRequest { alias }): Path<GetAliasRequest>,
226    State(state): State<Arc<InternalState>>,
227) -> impl IntoResponse {
228    let alias = urlencoding::decode(&alias);
229
230    if alias.is_err() {
231        return (StatusCode::BAD_REQUEST, ApiErrorStatus::InvalidInput).into_response();
232    }
233
234    let alias = alias.unwrap().into_owned();
235
236    match state.hoprd_db.resolve_alias(alias.clone()).await {
237        Ok(Some(entry)) => (StatusCode::OK, Json(PeerIdResponse { peer_id: entry })).into_response(),
238        Ok(None) => (StatusCode::NOT_FOUND, ApiErrorStatus::AliasNotFound).into_response(),
239        Err(e) => (StatusCode::UNPROCESSABLE_ENTITY, ApiErrorStatus::from(e)).into_response(),
240    }
241}
242
243// TODO(deprecated, will be removed in v3.0) Get alias for the address (ETH address) that have this alias assigned to it.
244#[utoipa::path(
245        get,
246        path = const_format::formatcp!("{BASE_PATH}/aliases_addresses/{{alias}}"),
247        params(
248            ("alias" = String, Path, description = "Alias to be shown"),
249        ),
250        responses(
251            (status = 200, description = "Get ETH address for an alias", body = AddressResponse),
252            (status = 401, description = "Invalid authorization token.", body = ApiError),
253            (status = 404, description = "Address not found", body = ApiError),
254        ),
255        security(
256            ("api_token" = []),
257            ("bearer_token" = [])
258        ),
259        tag = "Alias",
260    )]
261pub(super) async fn get_alias_address(
262    Path(GetAliasRequest { alias }): Path<GetAliasRequest>,
263    State(state): State<Arc<InternalState>>,
264) -> impl IntoResponse {
265    let hopr = state.hopr.clone();
266
267    let alias = urlencoding::decode(&alias);
268
269    if alias.is_err() {
270        return (StatusCode::BAD_REQUEST, ApiErrorStatus::InvalidInput).into_response();
271    }
272
273    let alias = alias.unwrap().into_owned();
274
275    match state.hoprd_db.resolve_alias(alias.clone()).await {
276        Ok(Some(entry)) => {
277            let peer_or_address = match PeerOrAddress::from_str(entry.as_str()) {
278                Ok(destination) => destination,
279                Err(_) => return (StatusCode::NOT_FOUND, ApiErrorStatus::AliasNotFound).into_response(),
280            };
281
282            match HoprIdentifier::new_with(peer_or_address, hopr.peer_resolver()).await {
283                Ok(destination) => (
284                    StatusCode::OK,
285                    Json(AddressResponse {
286                        address: destination.address,
287                    }),
288                )
289                    .into_response(),
290                Err(_) => (StatusCode::BAD_REQUEST, ApiErrorStatus::PeerNotFound).into_response(),
291            }
292        }
293        Ok(None) => (StatusCode::NOT_FOUND, ApiErrorStatus::AliasNotFound).into_response(),
294        Err(e) => (StatusCode::UNPROCESSABLE_ENTITY, ApiErrorStatus::from(e)).into_response(),
295    }
296}
297
298#[serde_as]
299#[derive(Deserialize, utoipa::ToSchema)]
300#[schema(example = json!({
301    "alias": "Alice",
302}))]
303pub(crate) struct DeleteAliasRequest {
304    alias: String,
305}
306
307/// (deprecated, will be removed in v3.0) Delete an alias.
308#[utoipa::path(
309        delete,
310        path = const_format::formatcp!("{BASE_PATH}/aliases/{{alias}}"),
311        params(
312            ("alias" = String, Path, description = "Alias to be shown"),
313        ),
314        responses(
315            (status = 204, description = "Alias removed successfully"),
316            (status = 401, description = "Invalid authorization token.", body = ApiError),
317            (status = 422, description = "Unknown failure", body = ApiError)   // This can never happen
318        ),
319        security(
320            ("api_token" = []),
321            ("bearer_token" = [])
322        ),
323        tag = "Alias",
324    )]
325pub(super) async fn delete_alias(
326    Path(DeleteAliasRequest { alias }): Path<DeleteAliasRequest>,
327    State(state): State<Arc<InternalState>>,
328) -> impl IntoResponse {
329    let alias = urlencoding::decode(&alias);
330
331    if alias.is_err() {
332        return (StatusCode::BAD_REQUEST, ApiErrorStatus::InvalidInput).into_response();
333    }
334
335    let alias = alias.unwrap().into_owned();
336
337    match state.hoprd_db.delete_alias(alias.clone()).await {
338        Ok(_) => (StatusCode::NO_CONTENT,).into_response(),
339        Err(e) => (StatusCode::UNPROCESSABLE_ENTITY, ApiErrorStatus::from(e)).into_response(),
340    }
341}
342
343/// (deprecated, will be removed in v3.0) Clear all aliases.
344#[utoipa::path(
345        delete,
346        path = const_format::formatcp!("{BASE_PATH}/aliases"),
347        responses(
348            (status = 204, description = "All aliases removed successfully"),
349            (status = 401, description = "Invalid authorization token.", body = ApiError),
350            (status = 422, description = "Unknown failure", body = ApiError)   // This can never happen
351        ),
352        security(
353            ("api_token" = []),
354            ("bearer_token" = [])
355        ),
356        tag = "Alias",
357    )]
358pub(super) async fn clear_aliases(State(state): State<Arc<InternalState>>) -> impl IntoResponse {
359    match state.hoprd_db.clear_aliases().await {
360        Ok(_) => (StatusCode::NO_CONTENT,).into_response(),
361        Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, ApiErrorStatus::from(e)).into_response(),
362    }
363}