hoprd_api/
account.rs

1use std::{str::FromStr, sync::Arc};
2
3use axum::{
4    extract::{Json, State},
5    http::status::StatusCode,
6    response::IntoResponse,
7};
8use hopr_lib::{
9    Address, HoprBalance, WxHOPR, XDai, XDaiBalance,
10    errors::{HoprLibError, HoprStatusError},
11};
12use serde::{Deserialize, Serialize};
13use serde_with::{DisplayFromStr, serde_as};
14
15use crate::{ApiError, ApiErrorStatus, BASE_PATH, InternalState};
16
17#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
18#[schema(example = json!({
19    "native": "0x07eaf07d6624f741e04f4092a755a9027aaab7f6"
20}))]
21#[serde(rename_all = "camelCase")]
22/// Contains the node's native addresses.
23pub(crate) struct AccountAddressesResponse {
24    #[schema(example = "0x07eaf07d6624f741e04f4092a755a9027aaab7f6")]
25    native: String,
26}
27
28/// Get node's native addresses.
29#[utoipa::path(
30        get,
31        path = const_format::formatcp!("{BASE_PATH}/account/addresses"),
32        responses(
33            (status = 200, description = "The node's public addresses", body = AccountAddressesResponse),
34            (status = 401, description = "Invalid authorization token.", body = ApiError),
35            (status = 422, description = "Unknown failure", body = ApiError)
36        ),
37        security(
38            ("api_token" = []),
39            ("bearer_token" = [])
40        ),
41        tag = "Account",
42    )]
43pub(super) async fn addresses(State(state): State<Arc<InternalState>>) -> impl IntoResponse {
44    let addresses = AccountAddressesResponse {
45        native: state.hopr.me_onchain().to_checksum(),
46    };
47
48    (StatusCode::OK, Json(addresses)).into_response()
49}
50
51#[serde_as]
52#[derive(Debug, Default, Clone, Serialize, utoipa::ToSchema)]
53#[schema(example = json!({
54        "hopr": "1000 wxHOPR",
55        "native": "0.1 xDai",
56        "safeHopr": "1000 wxHOPR",
57        "safeHoprAllowance": "10000 wxHOPR",
58        "safeNative": "0.1 xDai"
59    }))]
60#[serde(rename_all = "camelCase")]
61/// Contains all node's and safe's related balances.
62pub(crate) struct AccountBalancesResponse {
63    #[serde_as(as = "DisplayFromStr")]
64    #[schema(value_type = String, example = "0.1 xDai")]
65    safe_native: XDaiBalance,
66    #[serde_as(as = "DisplayFromStr")]
67    #[schema(value_type = String, example = "0.1 xDai")]
68    native: XDaiBalance,
69    #[serde_as(as = "DisplayFromStr")]
70    #[schema(value_type = String, example = "2000 wxHOPR")]
71    safe_hopr: HoprBalance,
72    #[serde_as(as = "DisplayFromStr")]
73    #[schema(value_type = String, example = "2000 wxHOPR")]
74    hopr: HoprBalance,
75    #[serde_as(as = "DisplayFromStr")]
76    #[schema(value_type = String, example = "10000 wxHOPR")]
77    safe_hopr_allowance: HoprBalance,
78}
79
80/// Get node's and associated Safe's HOPR and native balances as the allowance for HOPR
81/// tokens to be drawn by HoprChannels from Safe.
82///
83/// HOPR tokens from the Safe balance are used to fund the payment channels between this
84/// node and other nodes on the network.
85/// NATIVE balance of the Node is used to pay for the gas fees for the blockchain.
86#[utoipa::path(
87        get,
88        path = const_format::formatcp!("{BASE_PATH}/account/balances"),
89        responses(
90            (status = 200, description = "The node's HOPR and Safe balances", body = AccountBalancesResponse),
91            (status = 401, description = "Invalid authorization token.", body = ApiError),
92            (status = 422, description = "Unknown failure", body = ApiError)
93        ),
94        security(
95            ("api_token" = []),
96            ("bearer_token" = [])
97        ),
98        tag = "Account",
99    )]
100pub(super) async fn balances(State(state): State<Arc<InternalState>>) -> impl IntoResponse {
101    let hopr = state.hopr.clone();
102
103    let mut account_balances = AccountBalancesResponse::default();
104
105    match hopr.get_balance::<XDai>().await {
106        Ok(v) => account_balances.native = v,
107        Err(e) => return (StatusCode::UNPROCESSABLE_ENTITY, ApiErrorStatus::from(e)).into_response(),
108    }
109
110    match hopr.get_balance::<WxHOPR>().await {
111        Ok(v) => account_balances.hopr = v,
112        Err(e) => return (StatusCode::UNPROCESSABLE_ENTITY, ApiErrorStatus::from(e)).into_response(),
113    }
114
115    match hopr.get_safe_balance::<XDai>().await {
116        Ok(v) => account_balances.safe_native = v,
117        Err(e) => return (StatusCode::UNPROCESSABLE_ENTITY, ApiErrorStatus::from(e)).into_response(),
118    }
119
120    match hopr.get_safe_balance::<WxHOPR>().await {
121        Ok(v) => account_balances.safe_hopr = v,
122        Err(e) => return (StatusCode::UNPROCESSABLE_ENTITY, ApiErrorStatus::from(e)).into_response(),
123    }
124
125    match hopr.safe_allowance().await {
126        Ok(v) => account_balances.safe_hopr_allowance = v,
127        Err(e) => return (StatusCode::UNPROCESSABLE_ENTITY, ApiErrorStatus::from(e)).into_response(),
128    }
129
130    (StatusCode::OK, Json(account_balances)).into_response()
131}
132
133#[serde_as]
134#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, utoipa::ToSchema)]
135#[schema(example = json!({
136        "address": "0xb4ce7e6e36ac8b01a974725d5ba730af2b156fbe",
137        "amount": "20000 wxHOPR",
138    }))]
139#[serde(rename_all = "camelCase")]
140/// Request body for the withdrawal endpoint.
141pub(crate) struct WithdrawBodyRequest {
142    #[schema(value_type = String, example= "20000 wxHOPR")]
143    amount: String,
144    #[serde_as(as = "DisplayFromStr")]
145    #[schema(value_type = String, example= "0xb4ce7e6e36ac8b01a974725d5ba730af2b156fbe")]
146    address: Address,
147}
148
149#[derive(Debug, Default, Clone, Serialize, utoipa::ToSchema)]
150#[schema(example = json!({
151        "receipt": "0xb4ce7e6e36ac8b01a974725d5ba730af2b156fbe",
152    }))]
153#[serde(rename_all = "camelCase")]
154/// Response body for the withdrawal endpoint.
155pub(crate) struct WithdrawResponse {
156    #[schema(example = "0xb4ce7e6e36ac8b01a974725d5ba730af2b156fbe")]
157    receipt: String,
158}
159
160/// Withdraw funds from this node to the ethereum wallet address.
161///
162/// Both Native or HOPR can be withdrawn using this method.
163#[utoipa::path(
164        post,
165        path = const_format::formatcp!("{BASE_PATH}/account/withdraw"),
166        description = "Withdraw funds from this node to the ethereum wallet address",
167        request_body(
168            content = WithdrawBodyRequest,
169            content_type = "application/json",
170            description = "Request body for the withdraw endpoint",
171        ),
172        responses(
173            (status = 200, description = "The node's funds have been withdrawn", body = WithdrawResponse),
174            (status = 401, description = "Invalid authorization token.", body = ApiError),
175            (status = 412, description = "The node is not ready."),
176            (status = 422, description = "Unknown failure", body = ApiError)
177        ),
178        security(
179            ("api_token" = []),
180            ("bearer_token" = [])
181        ),
182        tag = "Account",
183    )]
184pub(super) async fn withdraw(
185    State(state): State<Arc<InternalState>>,
186    Json(req_data): Json<WithdrawBodyRequest>,
187) -> impl IntoResponse {
188    let res = if let Ok(native) = XDaiBalance::from_str(&req_data.amount) {
189        state.hopr.withdraw_native(req_data.address, native).await
190    } else if let Ok(hopr) = HoprBalance::from_str(&req_data.amount) {
191        state.hopr.withdraw_tokens(req_data.address, hopr).await
192    } else {
193        Err(HoprLibError::GeneralError("invalid currency".into()))
194    };
195
196    match res {
197        Ok(receipt) => (
198            StatusCode::OK,
199            Json(WithdrawResponse {
200                receipt: receipt.to_string(),
201            }),
202        )
203            .into_response(),
204        Err(HoprLibError::StatusError(HoprStatusError::NotThereYet(..))) => {
205            (StatusCode::PRECONDITION_FAILED, ApiErrorStatus::NotReady).into_response()
206        }
207
208        Err(e) => (StatusCode::UNPROCESSABLE_ENTITY, ApiErrorStatus::from(e)).into_response(),
209    }
210}