1use std::{
2 collections::{HashMap, HashSet},
3 fmt::{Display, Formatter},
4 ops::{Bound, RangeBounds},
5};
6
7use futures::stream::BoxStream;
8use hopr_internal_types::prelude::*;
9use hopr_primitive_types::prelude::*;
10
11#[derive(Debug, Clone, PartialEq, Eq, Default)]
13pub enum TicketIndexSelector {
14 #[default]
17 None,
18 Single(u64),
20 Multiple(HashSet<u64>),
22 Range((Bound<u64>, Bound<u64>)),
24}
25
26impl Display for TicketIndexSelector {
27 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
28 match &self {
29 TicketIndexSelector::None => write!(f, ""),
30 TicketIndexSelector::Single(idx) => write!(f, "with index {idx}"),
31 TicketIndexSelector::Multiple(indices) => write!(f, "with indices {indices:?}"),
32 TicketIndexSelector::Range((lb, ub)) => write!(f, "with indices in {lb:?}..{ub:?}"),
33 }
34 }
35}
36
37#[derive(Clone, Debug)]
42pub struct TicketSelector {
43 pub channel_identifier: (ChannelId, u32),
45 pub index: TicketIndexSelector,
50 pub win_prob: (Bound<WinningProbability>, Bound<WinningProbability>),
53 pub amount: (Bound<HoprBalance>, Bound<HoprBalance>),
56 pub state: Option<AcknowledgedTicketStatus>,
58}
59
60impl Display for TicketSelector {
61 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
62 let out = format!(
63 "ticket selector in {:?} {}{}{}{}",
64 self.channel_identifier,
65 self.index,
66 self.state
67 .map(|state| format!(" in state {state}"))
68 .unwrap_or("".into()),
69 match &self.win_prob {
70 (Bound::Unbounded, Bound::Unbounded) => "".to_string(),
71 bounds => format!(" with winning probability in {bounds:?}"),
72 },
73 match &self.amount {
74 (Bound::Unbounded, Bound::Unbounded) => "".to_string(),
75 bounds => format!(" with amount in {bounds:?}"),
76 },
77 );
78 write!(f, "{}", out.trim())
79 }
80}
81
82fn approx_cmp_bounds(b1: Bound<WinningProbability>, b2: Bound<WinningProbability>) -> bool {
83 match (b1, b2) {
84 (Bound::Unbounded, Bound::Unbounded) => true,
85 (Bound::Included(a), Bound::Included(b)) => b.approx_eq(&a),
86 (Bound::Excluded(a), Bound::Excluded(b)) => b.approx_eq(&a),
87 _ => false,
88 }
89}
90
91impl PartialEq for TicketSelector {
92 fn eq(&self, other: &Self) -> bool {
93 self.channel_identifier == other.channel_identifier
94 && self.index == other.index
95 && self.state == other.state
96 && self.amount == other.amount
97 && approx_cmp_bounds(self.win_prob.0, other.win_prob.0)
98 && approx_cmp_bounds(self.win_prob.1, other.win_prob.1)
99 }
100}
101
102impl TicketSelector {
103 pub fn new(channel_id: ChannelId, epoch: u32) -> Self {
105 Self {
106 channel_identifier: (channel_id, epoch),
107 index: TicketIndexSelector::None,
108 win_prob: (Bound::Unbounded, Bound::Unbounded),
109 amount: (Bound::Unbounded, Bound::Unbounded),
110 state: None,
111 }
112 }
113
114 pub fn is_unique(&self) -> bool {
116 matches!(&self.index, TicketIndexSelector::Single(_))
117 || matches!(&self.index, TicketIndexSelector::Multiple(indices) if indices.len() == 1)
118 }
119
120 #[must_use]
125 pub fn with_index(mut self, index: u64) -> Self {
126 self.index = match self.index {
127 TicketIndexSelector::None | TicketIndexSelector::Range(_) => TicketIndexSelector::Single(index),
128 TicketIndexSelector::Single(existing) => {
129 TicketIndexSelector::Multiple(HashSet::from_iter([existing, index]))
130 }
131 TicketIndexSelector::Multiple(mut existing) => {
132 existing.insert(index);
133 TicketIndexSelector::Multiple(existing)
134 }
135 };
136 self
137 }
138
139 #[must_use]
141 pub fn only_channel(mut self) -> Self {
142 self.index = TicketIndexSelector::None;
143 self.win_prob = (Bound::Unbounded, Bound::Unbounded);
144 self.amount = (Bound::Unbounded, Bound::Unbounded);
145 self.state = None;
146 self
147 }
148
149 #[must_use]
152 pub fn with_index_range<T: RangeBounds<u64>>(mut self, index_bound: T) -> Self {
153 self.index = TicketIndexSelector::Range((index_bound.start_bound().cloned(), index_bound.end_bound().cloned()));
154 self
155 }
156
157 #[must_use]
159 pub fn with_state(mut self, state: AcknowledgedTicketStatus) -> Self {
160 self.state = Some(state);
161 self
162 }
163
164 #[must_use]
166 pub fn with_no_state(mut self) -> Self {
167 self.state = None;
168 self
169 }
170
171 #[must_use]
173 pub fn with_winning_probability<T: RangeBounds<WinningProbability>>(mut self, range: T) -> Self {
174 self.win_prob = (range.start_bound().cloned(), range.end_bound().cloned());
175 self
176 }
177
178 #[must_use]
180 pub fn with_amount<T: RangeBounds<HoprBalance>>(mut self, range: T) -> Self {
181 self.amount = (range.start_bound().cloned(), range.end_bound().cloned());
182 self
183 }
184}
185
186impl From<&AcknowledgedTicket> for TicketSelector {
187 fn from(value: &AcknowledgedTicket) -> Self {
188 Self::from(&value.ticket)
189 }
190}
191
192impl From<&RedeemableTicket> for TicketSelector {
193 fn from(value: &RedeemableTicket) -> Self {
194 Self::from(&value.ticket)
195 }
196}
197
198impl From<&VerifiedTicket> for TicketSelector {
199 fn from(value: &VerifiedTicket) -> Self {
200 Self {
201 channel_identifier: (*value.channel_id(), value.verified_ticket().channel_epoch),
202 index: TicketIndexSelector::Single(value.verified_ticket().index),
203 win_prob: (Bound::Unbounded, Bound::Unbounded),
204 amount: (Bound::Unbounded, Bound::Unbounded),
205 state: None,
206 }
207 }
208}
209
210impl From<&ChannelEntry> for TicketSelector {
211 fn from(value: &ChannelEntry) -> Self {
212 Self {
213 channel_identifier: (*value.get_id(), value.channel_epoch),
214 index: TicketIndexSelector::None,
215 win_prob: (Bound::Unbounded, Bound::Unbounded),
216 amount: (Bound::Unbounded, Bound::Unbounded),
217 state: None,
218 }
219 }
220}
221
222impl From<ChannelEntry> for TicketSelector {
223 fn from(value: ChannelEntry) -> Self {
224 TicketSelector::from(&value)
225 }
226}
227
228#[derive(Debug, Copy, Clone, PartialEq, Eq, strum::Display, Hash)]
231#[strum(serialize_all = "lowercase")]
232pub enum TicketMarker {
233 Redeemed,
235 Rejected,
237 Neglected,
239}
240
241#[async_trait::async_trait]
255#[auto_impl::auto_impl(&, Box, Arc)]
256pub trait HoprDbTicketOperations {
257 type Error: std::error::Error + Send + Sync + 'static;
258
259 async fn stream_tickets<'c, S: Into<TicketSelector>, I: IntoIterator<Item = S> + Send>(
263 &'c self,
264 selectors: I,
265 ) -> Result<BoxStream<'c, RedeemableTicket>, Self::Error>;
266
267 async fn insert_ticket(&self, ticket: RedeemableTicket) -> Result<(), Self::Error>;
271
272 async fn mark_tickets_as<S: Into<TicketSelector> + Send, I: IntoIterator<Item = S> + Send>(
277 &self,
278 selectors: I,
279 mark_as: TicketMarker,
280 ) -> Result<usize, Self::Error>;
281
282 async fn mark_unsaved_ticket_rejected(&self, issuer: &Address, ticket: &Ticket) -> Result<(), Self::Error>;
289
290 async fn update_ticket_states_and_fetch<'a, S: Into<TicketSelector>, I: IntoIterator<Item = S> + Send>(
297 &'a self,
298 selectors: I,
299 new_state: AcknowledgedTicketStatus,
300 ) -> Result<BoxStream<'a, RedeemableTicket>, Self::Error>;
301
302 async fn update_ticket_states<S: Into<TicketSelector>, I: IntoIterator<Item = S> + Send>(
304 &self,
305 selectors: I,
306 new_state: AcknowledgedTicketStatus,
307 ) -> Result<usize, Self::Error>;
308
309 async fn get_ticket_statistics(
313 &self,
314 channel_id: Option<ChannelId>,
315 ) -> Result<ChannelTicketStatistics, Self::Error>;
316
317 async fn reset_ticket_statistics(&self) -> Result<(), Self::Error>;
319
320 async fn get_tickets_value(&self, id: &ChannelId, epoch: u32) -> Result<HoprBalance, Self::Error>;
324
325 async fn get_or_create_outgoing_ticket_index(
329 &self,
330 channel_id: &ChannelId,
331 epoch: u32,
332 ) -> Result<Option<u64>, Self::Error>;
333
334 async fn update_outgoing_ticket_index(
339 &self,
340 channel_id: &ChannelId,
341 epoch: u32,
342 index: u64,
343 ) -> Result<(), Self::Error>;
344
345 async fn remove_outgoing_ticket_index(&self, channel_id: &ChannelId, epoch: u32) -> Result<(), Self::Error>;
349}
350
351#[derive(Clone, Debug, PartialEq, Eq)]
353pub struct ChannelTicketStatistics {
354 pub winning_tickets: u128,
356 pub finalized_values: HashMap<TicketMarker, HoprBalance>,
358 pub unredeemed_value: HoprBalance,
360}
361
362impl Default for ChannelTicketStatistics {
363 fn default() -> Self {
364 Self {
365 winning_tickets: 0,
366 finalized_values: HashMap::from([
367 (TicketMarker::Neglected, HoprBalance::zero()),
368 (TicketMarker::Rejected, HoprBalance::zero()),
369 (TicketMarker::Redeemed, HoprBalance::zero()),
370 ]),
371 unredeemed_value: HoprBalance::zero(),
372 }
373 }
374}
375
376impl ChannelTicketStatistics {
377 pub fn neglected_value(&self) -> HoprBalance {
379 self.finalized_values
380 .get(&TicketMarker::Neglected)
381 .copied()
382 .unwrap_or_default()
383 }
384
385 pub fn rejected_value(&self) -> HoprBalance {
387 self.finalized_values
388 .get(&TicketMarker::Rejected)
389 .copied()
390 .unwrap_or_default()
391 }
392
393 pub fn redeemed_value(&self) -> HoprBalance {
395 self.finalized_values
396 .get(&TicketMarker::Redeemed)
397 .copied()
398 .unwrap_or_default()
399 }
400}