use std::fmt;
use nym_credential_storage::models::BasicTicketbookInformation;
use nym_credentials_interface::TicketType;
use nym_ecash_time::{Date, EcashTime, OffsetDateTime};
use strum::IntoEnumIterator;
use crate::{config::BandwidthControllerConfig, error::BandwidthControllerError};
#[derive(Debug, Clone, PartialEq)]
pub struct AvailableTicketbook {
pub id: i64,
pub typ: TicketType,
pub expiration: Date,
pub issued_tickets: u32,
pub claimed_tickets: u32,
pub ticket_size: u64,
}
impl AvailableTicketbook {
pub fn issued_tickets_si(&self) -> String {
si_scale::helpers::bibytes2(self.issued_tickets as u64 * self.ticket_size)
}
pub fn remaining_tickets(&self) -> u32 {
self.issued_tickets.saturating_sub(self.claimed_tickets)
}
pub fn remaining_tickets_si(&self) -> String {
si_scale::helpers::bibytes2(self.remaining_tickets() as u64 * self.ticket_size)
}
pub fn ticket_size_si(&self) -> String {
si_scale::helpers::bibytes2(self.ticket_size)
}
pub fn has_expired(&self) -> bool {
self.expiration <= nym_ecash_time::ecash_today().date()
}
pub fn expired_soon(
&self,
datetime: OffsetDateTime,
bc_config: BandwidthControllerConfig,
) -> bool {
self.expiration.ecash_datetime() < datetime + bc_config.soon_expiry_threshold
}
}
impl fmt::Display for AvailableTicketbook {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let ecash_today = nym_ecash_time::ecash_today().date();
let expiration = if self.expiration <= ecash_today {
format!("EXPIRED ON: {}", self.expiration)
} else {
format!("expires: {}", self.expiration)
};
write!(
f,
"{{ id: {}, type: {}, tickets: {}/{}, size: {}, remaining: {}/{}, {} }}",
self.id,
self.typ,
self.remaining_tickets(),
self.issued_tickets,
self.ticket_size_si(),
self.remaining_tickets_si(),
self.issued_tickets_si(),
expiration
)
}
}
impl TryFrom<BasicTicketbookInformation> for AvailableTicketbook {
type Error = BandwidthControllerError;
fn try_from(value: BasicTicketbookInformation) -> Result<Self, Self::Error> {
let typ = value
.ticketbook_type
.parse()
.map_err(|_| BandwidthControllerError::ParseTicketType(value.ticketbook_type))?;
Ok(AvailableTicketbook {
id: value.id,
typ,
expiration: value.expiration_date,
issued_tickets: value.total_tickets,
claimed_tickets: value.used_tickets,
ticket_size: typ.to_repr().bandwidth_value(),
})
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct AvailableTicketbooks {
pub ticketbooks: Vec<AvailableTicketbook>,
}
impl AvailableTicketbooks {
pub fn remaining_tickets(&self, typ: TicketType) -> u64 {
self.tickets_by_type(typ)
.filter(|ticketbook| !ticketbook.has_expired())
.map(|ticketbook| ticketbook.remaining_tickets())
.fold(0, |acc, remaining| acc.saturating_add(remaining.into()))
}
pub fn remaining_data(&self, typ: TicketType) -> u64 {
self.remaining_tickets(typ) * typ.to_repr().bandwidth_value()
}
pub fn remaining_data_si(&self, typ: TicketType) -> String {
si_scale::helpers::bibytes2(self.remaining_data(typ) as f64)
}
fn tickets_by_type(&self, typ: TicketType) -> impl Iterator<Item = &AvailableTicketbook> {
self.ticketbooks
.iter()
.filter(move |ticketbook| ticketbook.typ == typ)
}
pub fn remaining_tickets_long_lasting(
&self,
typ: TicketType,
bc_config: BandwidthControllerConfig,
) -> u64 {
self.tickets_by_type(typ)
.filter(|ticketbook| !ticketbook.expired_soon(OffsetDateTime::now_utc(), bc_config))
.map(|ticketbook| ticketbook.remaining_tickets())
.fold(0, |acc, remaining| acc.saturating_add(remaining.into()))
}
pub fn remaining_unexpired_tickets(&self, typ: TicketType) -> u64 {
self.tickets_by_type(typ)
.filter(|ticketbook| !ticketbook.has_expired())
.map(|ticketbook| ticketbook.remaining_tickets())
.fold(0, |acc, remaining| acc.saturating_add(remaining.into()))
}
pub fn needs_restock(&self, typ: TicketType, bc_config: BandwidthControllerConfig) -> bool {
let remaining = self.remaining_tickets_long_lasting(typ, bc_config);
remaining <= bc_config.nb_ticket_restock
}
pub fn contains_minimal_tickets(
&self,
typ: TicketType,
bc_config: BandwidthControllerConfig,
) -> bool {
let remaining = self.remaining_unexpired_tickets(typ);
remaining > bc_config.min_nb_ticket_needed
}
pub fn len(&self) -> usize {
self.ticketbooks.len()
}
pub fn is_empty(&self) -> bool {
self.ticketbooks.is_empty()
}
pub fn len_not_expired(&self) -> usize {
self.ticketbooks
.iter()
.filter(|ticketbook| !ticketbook.has_expired())
.count()
}
pub fn ticketbook_types() -> Vec<TicketType> {
TicketType::iter()
.filter(|&t| t != TicketType::V1MixnetExit)
.collect()
}
}
impl Iterator for AvailableTicketbooks {
type Item = AvailableTicketbook;
fn next(&mut self) -> Option<Self::Item> {
self.ticketbooks.pop()
}
}
impl From<Vec<AvailableTicketbook>> for AvailableTicketbooks {
fn from(ticketbooks: Vec<AvailableTicketbook>) -> Self {
Self { ticketbooks }
}
}
impl TryFrom<Vec<BasicTicketbookInformation>> for AvailableTicketbooks {
type Error = BandwidthControllerError;
fn try_from(value: Vec<BasicTicketbookInformation>) -> Result<Self, Self::Error> {
let ticketbooks: Vec<_> = value
.into_iter()
.filter_map(|ticketbook| {
AvailableTicketbook::try_from(ticketbook)
.inspect_err(|err| {
tracing::error!("Failed to parse ticketbook {err}");
})
.ok()
})
.collect();
Ok(AvailableTicketbooks::from(ticketbooks))
}
}