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")]
22pub(crate) struct AccountAddressesResponse {
24 #[schema(example = "0x07eaf07d6624f741e04f4092a755a9027aaab7f6")]
25 native: String,
26}
27
28#[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")]
61pub(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#[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")]
140pub(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")]
154pub(crate) struct WithdrawResponse {
156 #[schema(example = "0xb4ce7e6e36ac8b01a974725d5ba730af2b156fbe")]
157 receipt: String,
158}
159
160#[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}