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