1use std::sync::Arc;
2
3use axum::{
4 extract::{Json, Path, State},
5 http::status::StatusCode,
6 response::IntoResponse,
7};
8use hopr_lib::{
9 ChannelTicketStatistics, HoprBalance, ToHex,
10 errors::{HoprLibError, HoprStatusError},
11 prelude::Hash,
12};
13use serde::Deserialize;
14use serde_with::{DisplayFromStr, serde_as};
15
16use crate::{ApiError, ApiErrorStatus, BASE_PATH, InternalState};
17
18#[serde_as]
19#[derive(Debug, Clone, serde::Serialize, utoipa::ToSchema)]
20#[schema(example = json!({
21 "amount": "100",
22 "channelEpoch": 1,
23 "channelId": "0x04efc1481d3f106b88527b3844ba40042b823218a9cd29d1aa11c2c2ef8f538f",
24 "index": 0,
25 "indexOffset": 1,
26 "signature": "0xe445fcf4e90d25fe3c9199ccfaff85e23ecce8773304d85e7120f1f38787f2329822470487a37f1b5408c8c0b73e874ee9f7594a632713b6096e616857999891",
27 "winProb": "1"
28 }))]
29#[serde(rename_all = "camelCase")]
30pub(crate) struct ChannelTicket {
32 #[serde_as(as = "DisplayFromStr")]
33 #[schema(value_type = String, example = "0x04efc1481d3f106b88527b3844ba40042b823218a9cd29d1aa11c2c2ef8f538f")]
34 channel_id: Hash,
35 #[serde_as(as = "DisplayFromStr")]
36 #[schema(value_type = String, example = "1.0 wxHOPR")]
37 amount: HoprBalance,
38 #[schema(example = 0)]
39 index: u64,
40 #[schema(example = "1")]
41 win_prob: String,
42 #[schema(example = 1)]
43 channel_epoch: u32,
44 #[schema(
45 example = "0xe445fcf4e90d25fe3c9199ccfaff85e23ecce8773304d85e7120f1f38787f2329822470487a37f1b5408c8c0b73e874ee9f7594a632713b6096e616857999891"
46 )]
47 signature: String,
48}
49
50#[derive(Deserialize)]
51#[serde(rename_all = "camelCase")]
52pub(crate) struct ChannelIdParams {
53 channel_id: String,
54}
55
56#[utoipa::path(
58 get,
59 path = const_format::formatcp!("{BASE_PATH}/channels/{{channelId}}/tickets"),
60 description = "Lists all tickets for the given channel ID.",
61 params(
62 ("channelId" = String, Path, description = "ID of the channel.", example = "0x04efc1481d3f106b88527b3844ba40042b823218a9cd29d1aa11c2c2ef8f538f")
63 ),
64 responses(
65 (status = 200, description = "Fetched all tickets for the given channel ID", body = [ChannelTicket], example = json!([
66 {
67 "amount": "10 wxHOPR",
68 "channelEpoch": 1,
69 "channelId": "0x04efc1481d3f106b88527b3844ba40042b823218a9cd29d1aa11c2c2ef8f538f",
70 "index": 0,
71 "indexOffset": 1,
72 "signature": "0xe445fcf4e90d25fe3c9199ccfaff85e23ecce8773304d85e7120f1f38787f2329822470487a37f1b5408c8c0b73e874ee9f7594a632713b6096e616857999891",
73 "winProb": "1"
74 }
75 ])),
76 (status = 400, description = "Invalid channel id.", body = ApiError),
77 (status = 401, description = "Invalid authorization token.", body = ApiError),
78 (status = 404, description = "Channel not found.", body = ApiError),
79 (status = 422, description = "Unknown failure", body = ApiError)
80 ),
81 security(
82 ("api_token" = []),
83 ("bearer_token" = [])
84 ),
85 tag = "Channels"
86 )]
87pub(super) async fn show_channel_tickets(
88 Path(ChannelIdParams { channel_id }): Path<ChannelIdParams>,
89 State(state): State<Arc<InternalState>>,
90) -> impl IntoResponse {
91 let hopr = state.hopr.clone();
92
93 match Hash::from_hex(channel_id.as_str()) {
94 Ok(channel_id) => match hopr.tickets_in_channel(&channel_id).await {
95 Ok(Some(_tickets)) => {
96 let tickets: Vec<ChannelTicket> = vec![];
97 (StatusCode::OK, Json(tickets)).into_response()
98 }
99 Ok(None) => (StatusCode::NOT_FOUND, ApiErrorStatus::TicketsNotFound).into_response(),
100 Err(e) => (StatusCode::UNPROCESSABLE_ENTITY, ApiErrorStatus::from(e)).into_response(),
101 },
102 Err(_) => (StatusCode::BAD_REQUEST, ApiErrorStatus::InvalidChannelId).into_response(),
103 }
104}
105
106#[utoipa::path(
108 get,
109 path = const_format::formatcp!("{BASE_PATH}/tickets"),
110 description = "(deprecated) Returns an empty array.",
111 responses(
112 (status = 200, description = "Fetched all tickets in all the channels", body = [ChannelTicket], example = json!([
113 {
114 "amount": "10 wxHOPR",
115 "channelEpoch": 1,
116 "channelId": "0x04efc1481d3f106b88527b3844ba40042b823218a9cd29d1aa11c2c2ef8f538f",
117 "index": 0,
118 "indexOffset": 1,
119 "signature": "0xe445fcf4e90d25fe3c9199ccfaff85e23ecce8773304d85e7120f1f38787f2329822470487a37f1b5408c8c0b73e874ee9f7594a632713b6096e616857999891",
120 "winProb": "1"
121 }
122 ])),
123 (status = 401, description = "Invalid authorization token.", body = ApiError),
124 (status = 422, description = "Unknown failure", body = ApiError)
125 ),
126 security(
127 ("api_token" = []),
128 ("bearer_token" = [])
129 ),
130 tag = "Tickets"
131 )]
132pub(super) async fn show_all_tickets() -> impl IntoResponse {
133 let tickets: Vec<ChannelTicket> = vec![];
134 (StatusCode::OK, Json(tickets)).into_response()
135}
136
137#[serde_as]
138#[derive(Debug, Clone, serde::Serialize, utoipa::ToSchema)]
139#[schema(example = json!({
140 "winningCount": 0,
141 "neglectedValue": "0 wxHOPR",
142 "redeemedValue": "1000 wxHOPR",
143 "rejectedValue": "0 wxHOPR",
144 "unredeemedValue": "2000 wxHOPR",
145 }))]
146#[serde(rename_all = "camelCase")]
147pub(crate) struct NodeTicketStatisticsResponse {
149 #[schema(example = 0)]
150 winning_count: u64,
151 #[serde_as(as = "DisplayFromStr")]
152 #[schema(value_type = String, example = "20 wxHOPR")]
153 unredeemed_value: HoprBalance,
154 #[serde_as(as = "DisplayFromStr")]
155 #[schema(value_type = String,example = "100 wxHOPR")]
156 redeemed_value: HoprBalance,
157 #[serde_as(as = "DisplayFromStr")]
158 #[schema(value_type = String,example = "0 wxHOPR")]
159 neglected_value: HoprBalance,
160 #[serde_as(as = "DisplayFromStr")]
161 #[schema(value_type = String, example = "0 wHOPR")]
162 rejected_value: HoprBalance,
163}
164
165impl From<ChannelTicketStatistics> for NodeTicketStatisticsResponse {
166 fn from(value: ChannelTicketStatistics) -> Self {
167 Self {
168 winning_count: value.winning_tickets as u64,
169 unredeemed_value: value.unredeemed_value,
170 redeemed_value: value.redeemed_value(),
171 neglected_value: value.neglected_value(),
172 rejected_value: value.rejected_value(),
173 }
174 }
175}
176
177#[utoipa::path(
179 get,
180 path = const_format::formatcp!("{BASE_PATH}/tickets/statistics"),
181 description = "Returns current complete statistics on tickets.",
182 responses(
183 (status = 200, description = "Tickets statistics fetched successfully. Check schema for description of every field in the statistics.", body = NodeTicketStatisticsResponse),
184 (status = 401, description = "Invalid authorization token.", body = ApiError),
185 (status = 422, description = "Unknown failure", body = ApiError)
186 ),
187 security(
188 ("api_token" = []),
189 ("bearer_token" = [])
190 ),
191 tag = "Tickets"
192 )]
193pub(super) async fn show_ticket_statistics(State(state): State<Arc<InternalState>>) -> impl IntoResponse {
194 let hopr = state.hopr.clone();
195 match hopr.ticket_statistics().await.map(NodeTicketStatisticsResponse::from) {
196 Ok(stats) => (StatusCode::OK, Json(stats)).into_response(),
197 Err(e) => (StatusCode::UNPROCESSABLE_ENTITY, ApiErrorStatus::from(e)).into_response(),
198 }
199}
200
201#[utoipa::path(
203 delete,
204 path = const_format::formatcp!("{BASE_PATH}/tickets/statistics"),
205 description = "Resets the ticket metrics.",
206 responses(
207 (status = 204, description = "Ticket statistics reset successfully."),
208 (status = 401, description = "Invalid authorization token.", body = ApiError),
209 (status = 422, description = "Unknown failure", body = ApiError)
210 ),
211 security(
212 ("api_token" = []),
213 ("bearer_token" = [])
214 ),
215 tag = "Tickets"
216 )]
217pub(super) async fn reset_ticket_statistics(State(state): State<Arc<InternalState>>) -> impl IntoResponse {
218 let hopr = state.hopr.clone();
219 match hopr.reset_ticket_statistics().await {
220 Ok(()) => (StatusCode::NO_CONTENT, "").into_response(),
221 Err(e) => (StatusCode::UNPROCESSABLE_ENTITY, ApiErrorStatus::from(e)).into_response(),
222 }
223}
224
225#[utoipa::path(
230 post,
231 path = const_format::formatcp!("{BASE_PATH}/tickets/redeem"),
232 description = "Starts redeeming of all tickets in all channels.",
233 responses(
234 (status = 204, description = "Tickets redeemed successfully."),
235 (status = 401, description = "Invalid authorization token.", body = ApiError),
236 (status = 412, description = "The node is not ready."),
237 (status = 422, description = "Unknown failure", body = ApiError)
238 ),
239 security(
240 ("api_token" = []),
241 ("bearer_token" = [])
242 ),
243 tag = "Tickets"
244 )]
245pub(super) async fn redeem_all_tickets(State(state): State<Arc<InternalState>>) -> impl IntoResponse {
246 let hopr = state.hopr.clone();
247 match hopr.redeem_all_tickets(0).await {
248 Ok(()) => (StatusCode::NO_CONTENT, "").into_response(),
249 Err(HoprLibError::StatusError(HoprStatusError::NotThereYet(..))) => {
250 (StatusCode::PRECONDITION_FAILED, ApiErrorStatus::NotReady).into_response()
251 }
252 Err(e) => (StatusCode::UNPROCESSABLE_ENTITY, ApiErrorStatus::from(e)).into_response(),
253 }
254}
255
256#[utoipa::path(
261 post,
262 path = const_format::formatcp!("{BASE_PATH}/channels/{{channelId}}/tickets/redeem"),
263 description = "Starts redeeming all tickets in the given channel.",
264 params(
265 ("channelId" = String, Path, description = "ID of the channel.", example = "0x04efc1481d3f106b88527b3844ba40042b823218a9cd29d1aa11c2c2ef8f538f")
266 ),
267 responses(
268 (status = 204, description = "Tickets redeemed successfully."),
269 (status = 400, description = "Invalid channel id.", body = ApiError),
270 (status = 401, description = "Invalid authorization token.", body = ApiError),
271 (status = 404, description = "Tickets were not found for that channel. That means that no messages were sent inside this channel yet.", body = ApiError),
272 (status = 412, description = "The node is not ready."),
273 (status = 422, description = "Unknown failure", body = ApiError)
274 ),
275 security(
276 ("api_token" = []),
277 ("bearer_token" = [])
278 ),
279 tag = "Channels"
280 )]
281pub(super) async fn redeem_tickets_in_channel(
282 Path(ChannelIdParams { channel_id }): Path<ChannelIdParams>,
283 State(state): State<Arc<InternalState>>,
284) -> impl IntoResponse {
285 let hopr = state.hopr.clone();
286
287 match Hash::from_hex(channel_id.as_str()) {
288 Ok(channel_id) => match hopr.redeem_tickets_in_channel(&channel_id, 0).await {
289 Ok(_) => (StatusCode::NO_CONTENT, "").into_response(),
290 Err(HoprLibError::StatusError(HoprStatusError::NotThereYet(..))) => {
292 (StatusCode::PRECONDITION_FAILED, ApiErrorStatus::NotReady).into_response()
293 }
294 Err(e) => (StatusCode::UNPROCESSABLE_ENTITY, ApiErrorStatus::from(e)).into_response(),
295 },
296 Err(_) => (StatusCode::BAD_REQUEST, ApiErrorStatus::InvalidChannelId).into_response(),
297 }
298}