use std::collections::HashMap;
use nym_credential_storage::models::AvailableGlobalData;
use nym_credentials_interface::TicketType;
use nym_ecash_time::Date;
use nym_validator_client::nym_api::EpochId;
use crate::in_flight::global_data::GlobalDataRequest;
use crate::{
error::BandwidthControllerError, requests::ReturnSender, traits::CredentialFetcherError,
};
#[derive(Clone, Debug)]
pub(crate) enum ReadinessStatus {
Ready,
InFlight,
Unavailable,
FetchFailed(String),
}
impl ReadinessStatus {
fn severity(&self) -> u8 {
match self {
ReadinessStatus::Ready => 0,
ReadinessStatus::InFlight => 1,
ReadinessStatus::Unavailable => 2,
ReadinessStatus::FetchFailed(_) => 3,
}
}
}
#[derive(Debug)]
pub(crate) struct FetchFailure {
pub(crate) ticket_type: TicketType,
pub(crate) error: CredentialFetcherError,
}
#[derive(Debug)]
pub(crate) struct ReadinessSnapshot {
upgrade_mode: bool,
tickets_readiness: HashMap<TicketType, ReadinessStatus>,
master_key_readiness: HashMap<EpochId, ReadinessStatus>,
coin_index_readiness: HashMap<EpochId, ReadinessStatus>,
expiration_readiness: HashMap<(EpochId, Date), ReadinessStatus>,
}
impl ReadinessSnapshot {
pub(crate) fn build(
upgrade_mode: bool,
stocked_types: Vec<TicketType>,
tickets_in_flight: Vec<TicketType>,
ticket_failure: Option<FetchFailure>,
available_global_data: AvailableGlobalData,
global_data_in_flight: Vec<GlobalDataRequest>,
) -> Self {
let mut tickets_readiness = HashMap::new();
for ticket_type in stocked_types {
tickets_readiness.insert(ticket_type, ReadinessStatus::Ready);
}
for ticket_type in tickets_in_flight {
tickets_readiness
.entry(ticket_type)
.or_insert(ReadinessStatus::InFlight);
}
if let Some(failure) = ticket_failure {
tickets_readiness
.entry(failure.ticket_type)
.or_insert(ReadinessStatus::FetchFailed(failure.error.to_string()));
}
let mut master_key_readiness =
epochs_ready(available_global_data.master_verification_key_epochs);
let mut coin_index_readiness =
epochs_ready(available_global_data.coin_index_signature_epochs);
let mut expiration_readiness: HashMap<(EpochId, Date), ReadinessStatus> =
available_global_data
.expiration_date_signatures
.into_iter()
.map(|(epoch_id, date)| ((EpochId::from(epoch_id), date), ReadinessStatus::Ready))
.collect();
for request in global_data_in_flight {
match request {
GlobalDataRequest::MasterVerificationKey(epoch_id) => {
master_key_readiness
.entry(epoch_id)
.or_insert(ReadinessStatus::InFlight);
}
GlobalDataRequest::CoinIndexSignatures(epoch_id) => {
coin_index_readiness
.entry(epoch_id)
.or_insert(ReadinessStatus::InFlight);
}
GlobalDataRequest::ExpirationDateSignatures {
epoch_id,
expiration_date,
} => {
expiration_readiness
.entry((epoch_id, expiration_date))
.or_insert(ReadinessStatus::InFlight);
}
}
}
ReadinessSnapshot {
upgrade_mode,
tickets_readiness,
master_key_readiness,
coin_index_readiness,
expiration_readiness,
}
}
fn evaluate_readiness(&self, request: &ReadinessRequest) -> ReadinessStatus {
if self.upgrade_mode {
return ReadinessStatus::Ready;
}
let tickets = request.ticket_types.iter().map(|ticket_type| {
self.tickets_readiness
.get(ticket_type)
.cloned()
.unwrap_or(ReadinessStatus::Unavailable)
});
let global_data = request
.global_data
.iter()
.flat_map(|&(epoch_id, expiration_date)| {
[
self.master_key_readiness.get(&epoch_id),
self.coin_index_readiness.get(&epoch_id),
self.expiration_readiness.get(&(epoch_id, expiration_date)),
]
.map(|status| status.cloned().unwrap_or(ReadinessStatus::Unavailable))
});
tickets
.chain(global_data)
.max_by_key(|status| status.severity())
.unwrap_or(ReadinessStatus::Ready)
}
}
fn epochs_ready(epochs: Vec<u64>) -> HashMap<EpochId, ReadinessStatus> {
epochs
.into_iter()
.map(|epoch_id| (EpochId::from(epoch_id), ReadinessStatus::Ready))
.collect()
}
#[derive(Debug)]
pub(crate) struct ReadinessRequest {
pub(crate) return_sender: ReturnSender<()>,
pub(crate) ticket_types: Vec<TicketType>,
pub(crate) global_data: Vec<(EpochId, Date)>,
}
impl ReadinessRequest {
pub(crate) fn try_resolve(self, snapshot: &ReadinessSnapshot) -> Option<Self> {
match snapshot.evaluate_readiness(&self) {
ReadinessStatus::Ready => {
self.return_sender.send(Ok(()));
None
}
ReadinessStatus::InFlight => Some(self),
ReadinessStatus::Unavailable => {
self.return_sender
.send(Err(BandwidthControllerError::TicketbooksUnavailable));
None
}
ReadinessStatus::FetchFailed(reason) => {
self.return_sender
.send(Err(BandwidthControllerError::TicketbookFetchFailed {
reason,
}));
None
}
}
}
pub(crate) fn cancel(self) {
self.return_sender
.send(Err(BandwidthControllerError::TicketbooksUnavailable));
}
}