nym_bandwidth_controller/
ticketbooks.rs1use std::fmt;
5
6use nym_credential_storage::models::BasicTicketbookInformation;
7use nym_credentials_interface::TicketType;
8use nym_ecash_time::{Date, EcashTime, OffsetDateTime};
9use nym_validator_client::nym_api::EpochId;
10use strum::IntoEnumIterator;
11
12use crate::{config::BandwidthControllerConfig, error::BandwidthControllerError};
13
14#[derive(Debug, Clone, PartialEq)]
15pub struct AvailableTicketbook {
16 pub id: i64,
17 pub typ: TicketType,
18 pub expiration: Date,
19 pub epoch_id: EpochId,
20 pub issued_tickets: u32,
21 pub claimed_tickets: u32,
22 pub ticket_size: u64,
23}
24
25impl AvailableTicketbook {
26 pub fn issued_tickets_si(&self) -> String {
27 si_scale::helpers::bibytes2(self.issued_tickets as u64 * self.ticket_size)
28 }
29
30 pub fn remaining_tickets(&self) -> u32 {
31 self.issued_tickets.saturating_sub(self.claimed_tickets)
32 }
33
34 pub fn remaining_tickets_si(&self) -> String {
35 si_scale::helpers::bibytes2(self.remaining_tickets() as u64 * self.ticket_size)
36 }
37
38 pub fn ticket_size_si(&self) -> String {
39 si_scale::helpers::bibytes2(self.ticket_size)
40 }
41
42 pub fn has_expired(&self) -> bool {
43 self.expiration <= nym_ecash_time::ecash_today().date()
44 }
45
46 pub fn expired_soon(
48 &self,
49 datetime: OffsetDateTime,
50 bc_config: &BandwidthControllerConfig,
51 ) -> bool {
52 self.expiration.ecash_datetime() < datetime + bc_config.soon_expiry_threshold
53 }
54}
55
56impl fmt::Display for AvailableTicketbook {
57 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58 let ecash_today = nym_ecash_time::ecash_today().date();
59
60 let expiration = if self.expiration <= ecash_today {
61 format!("EXPIRED ON: {}", self.expiration)
62 } else {
63 format!("expires: {}", self.expiration)
64 };
65
66 write!(
67 f,
68 "{{ id: {}, type: {}, epoch_id: {}, tickets: {}/{}, size: {}, remaining: {}/{}, {} }}",
69 self.id,
70 self.typ,
71 self.epoch_id,
72 self.remaining_tickets(),
73 self.issued_tickets,
74 self.ticket_size_si(),
75 self.remaining_tickets_si(),
76 self.issued_tickets_si(),
77 expiration
78 )
79 }
80}
81
82impl TryFrom<BasicTicketbookInformation> for AvailableTicketbook {
83 type Error = BandwidthControllerError;
84
85 fn try_from(value: BasicTicketbookInformation) -> Result<Self, Self::Error> {
86 let typ = value
87 .ticketbook_type
88 .parse()
89 .map_err(|_| BandwidthControllerError::ParseTicketType(value.ticketbook_type))?;
90 Ok(AvailableTicketbook {
91 id: value.id,
92 typ,
93 expiration: value.expiration_date,
94 epoch_id: EpochId::from(value.epoch_id),
95 issued_tickets: value.total_tickets,
96 claimed_tickets: value.used_tickets,
97 ticket_size: typ.to_repr().bandwidth_value(),
98 })
99 }
100}
101
102#[derive(Debug, Clone, PartialEq)]
103pub struct AvailableTicketbooks {
104 pub ticketbooks: Vec<AvailableTicketbook>,
105}
106
107impl AvailableTicketbooks {
108 pub fn remaining_tickets(&self, typ: TicketType) -> u64 {
109 self.tickets_by_type(typ)
110 .filter(|ticketbook| !ticketbook.has_expired())
111 .map(|ticketbook| ticketbook.remaining_tickets())
112 .fold(0, |acc, remaining| acc.saturating_add(remaining.into()))
113 }
114
115 pub fn remaining_data(&self, typ: TicketType) -> u64 {
116 self.remaining_tickets(typ) * typ.to_repr().bandwidth_value()
117 }
118
119 pub fn remaining_data_si(&self, typ: TicketType) -> String {
120 si_scale::helpers::bibytes2(self.remaining_data(typ) as f64)
121 }
122
123 fn tickets_by_type(&self, typ: TicketType) -> impl Iterator<Item = &AvailableTicketbook> {
124 self.ticketbooks
125 .iter()
126 .filter(move |ticketbook| ticketbook.typ == typ)
127 }
128
129 pub(crate) fn next_usable(&self, typ: TicketType) -> Option<&AvailableTicketbook> {
132 self.tickets_by_type(typ)
133 .filter(|ticketbook| !ticketbook.has_expired() && ticketbook.remaining_tickets() > 0)
134 .min_by_key(|ticketbook| ticketbook.expiration)
135 }
136
137 pub fn remaining_tickets_long_lasting(
138 &self,
139 typ: TicketType,
140 bc_config: &BandwidthControllerConfig,
141 ) -> u64 {
142 self.tickets_by_type(typ)
143 .filter(|ticketbook| !ticketbook.expired_soon(OffsetDateTime::now_utc(), bc_config))
144 .map(|ticketbook| ticketbook.remaining_tickets())
145 .fold(0, |acc, remaining| acc.saturating_add(remaining.into()))
146 }
147
148 pub fn remaining_unexpired_tickets(&self, typ: TicketType) -> u64 {
149 self.tickets_by_type(typ)
150 .filter(|ticketbook| !ticketbook.has_expired())
151 .map(|ticketbook| ticketbook.remaining_tickets())
152 .fold(0, |acc, remaining| acc.saturating_add(remaining.into()))
153 }
154
155 pub fn needs_restock(&self, typ: TicketType, bc_config: &BandwidthControllerConfig) -> bool {
157 let remaining = self.remaining_tickets_long_lasting(typ, bc_config);
158 remaining <= bc_config.nb_ticket_restock
159 }
160
161 pub fn contains_minimal_tickets(
162 &self,
163 typ: TicketType,
164 bc_config: &BandwidthControllerConfig,
165 ) -> bool {
166 let remaining = self.remaining_unexpired_tickets(typ);
167 remaining > bc_config.min_nb_ticket_needed
168 }
169
170 pub fn len(&self) -> usize {
171 self.ticketbooks.len()
172 }
173
174 pub fn is_empty(&self) -> bool {
175 self.ticketbooks.is_empty()
176 }
177
178 pub fn len_not_expired(&self) -> usize {
179 self.ticketbooks
180 .iter()
181 .filter(|ticketbook| !ticketbook.has_expired())
182 .count()
183 }
184
185 pub fn ticketbook_types() -> Vec<TicketType> {
186 TicketType::iter()
188 .filter(|&t| t != TicketType::V1MixnetExit)
189 .collect()
190 }
191}
192
193impl Iterator for AvailableTicketbooks {
194 type Item = AvailableTicketbook;
195
196 fn next(&mut self) -> Option<Self::Item> {
197 self.ticketbooks.pop()
198 }
199}
200
201impl From<Vec<AvailableTicketbook>> for AvailableTicketbooks {
202 fn from(ticketbooks: Vec<AvailableTicketbook>) -> Self {
203 Self { ticketbooks }
204 }
205}
206
207impl TryFrom<Vec<BasicTicketbookInformation>> for AvailableTicketbooks {
208 type Error = BandwidthControllerError;
209
210 fn try_from(value: Vec<BasicTicketbookInformation>) -> Result<Self, Self::Error> {
211 let ticketbooks: Vec<_> = value
212 .into_iter()
213 .filter_map(|ticketbook| {
214 AvailableTicketbook::try_from(ticketbook)
215 .inspect_err(|err| {
216 tracing::error!("Failed to parse ticketbook {err}");
217 })
218 .ok()
219 })
220 .collect();
221 Ok(AvailableTicketbooks::from(ticketbooks))
222 }
223}