1use crate::config::BandwidthControllerConfig;
5use crate::error::BandwidthControllerError;
6use crate::readiness::{FetchFailure, ReadinessRequest, ReadinessSnapshot, ReadinessStatus};
7use crate::requests::{BandwidthControllerRequest, BandwidthControllerRequestSender};
8use crate::ticketbooks::AvailableTicketbooks;
9use crate::traits::{CredentialFetcher, CredentialPublicDataFetcher};
10use crate::NymCredential;
11use crate::{
12 BandwidthTicketProvider, PreparedCredential, PreparedCredentialMetadata, UPGRADE_MODE_JWT_TYPE,
13};
14
15use nym_credential_storage::models::EmergencyCredentialContent;
16use nym_credential_storage::models::RetrievedTicketbook;
17use nym_credential_storage::storage::Storage;
18use nym_credentials::ecash::bandwidth::CredentialSpendingData;
19use nym_credentials::IssuedTicketBook;
20use nym_credentials_interface::{
21 AnnotatedCoinIndexSignature, AnnotatedExpirationDateSignature, TicketType, VerificationKeyAuth,
22};
23use nym_crypto::asymmetric::ed25519;
24use nym_ecash_time::{Date, OffsetDateTime};
25use nym_task::ShutdownToken;
26use nym_validator_client::nym_api::EpochId;
27
28use async_trait::async_trait;
29use log::error;
30use std::collections::HashMap;
31use std::collections::HashSet;
32use std::sync::Arc;
33use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender};
34
35#[cfg(not(target_arch = "wasm32"))]
36use tokio::time::{interval, MissedTickBehavior};
37
38#[cfg(target_arch = "wasm32")]
39use wasmtimer::tokio::{interval, MissedTickBehavior};
40
41use crate::in_flight::{FetchResult, InFlightFetches};
42
43pub struct BandwidthController<St> {
64 storage: St,
65
66 request_channel: (
68 UnboundedSender<BandwidthControllerRequest>,
69 UnboundedReceiver<BandwidthControllerRequest>,
70 ),
71
72 config: BandwidthControllerConfig,
73
74 public_data_fetcher: Option<Arc<dyn CredentialPublicDataFetcher>>,
76
77 credential_fetcher: Option<Arc<dyn CredentialFetcher>>,
80
81 in_flight: InFlightFetches,
84
85 pending_readiness: Vec<ReadinessRequest>,
87}
88
89impl<St: Storage> BandwidthController<St> {
90 pub fn new(storage: St) -> Self {
95 let request_channel = mpsc::unbounded_channel();
96 BandwidthController {
97 storage,
98 request_channel,
99 config: Default::default(),
100 public_data_fetcher: None,
101 credential_fetcher: None,
102 in_flight: InFlightFetches::new(),
103 pending_readiness: Vec::new(),
104 }
105 }
106 #[must_use]
107 pub fn with_config(mut self, config: BandwidthControllerConfig) -> Self {
108 self.config = config;
109 self
110 }
111
112 #[must_use]
113 pub fn with_credential_fetcher(mut self, fetcher: impl CredentialFetcher + 'static) -> Self {
114 let fetcher = Arc::new(fetcher);
115 self.credential_fetcher = Some(fetcher.clone());
116 self.public_data_fetcher = Some(fetcher);
117 self
118 }
119
120 #[must_use]
121 pub fn with_credential_public_data_fetcher(
122 mut self,
123 fetcher: impl CredentialPublicDataFetcher + 'static,
124 ) -> Self {
125 self.public_data_fetcher = Some(Arc::new(fetcher));
126 self
127 }
128
129 pub fn get_request_sender(&self) -> BandwidthControllerRequestSender {
132 BandwidthControllerRequestSender::new(self.request_channel.0.clone())
133 }
134
135 pub async fn run(mut self, shutdown_token: ShutdownToken) {
143 tracing::info!("BandwidthController started successfully");
144
145 let mut topup_interval = interval(self.config.topup_interval);
146 topup_interval.set_missed_tick_behavior(MissedTickBehavior::Delay);
147
148 loop {
149 tokio::select! {
150 biased;
151 _ = shutdown_token.cancelled() => {
152 log::debug!("bandwidth controller received cancellation request; shutting down");
153 break;
154 }
155 _ = topup_interval.tick() => {
156 let _ = self.print_info().await;
157 self.ensure_global_data().await;
158 self.check_and_restock(AvailableTicketbooks::ticketbook_types()).await;
159 }
160 (typ, res) = self.in_flight.next_result(), if !self.in_flight.is_empty() => {
161 self.on_fetch_complete(typ, res).await;
162 }
163 request = self.request_channel.1.recv() => match request {
164 Some(request) => self.handle_request(request).await,
165 None => {
166 log::warn!("bandwidth controller request channel closed; this should never happen as we own a sender; shutting down");
167 break;
168 }
169 }
170 }
171 }
172
173 self.in_flight.cancel_and_join().await;
175 if let Some(fetcher) = self.credential_fetcher {
176 fetcher.cleanup().await;
177 }
178 self.storage.close().await;
179 }
180
181 async fn handle_request(&mut self, request: BandwidthControllerRequest) {
186 match request {
187 BandwidthControllerRequest::EcashTicket(return_sender, request) => {
188 let ticket_type = request.ticket_type;
189 let credential_result = self
190 .prepare_ecash_ticket(
191 ticket_type,
192 request.gateway_id.to_bytes(),
193 request.tickets_to_spend,
194 request.spend_time,
195 )
196 .await;
197 return_sender.send(credential_result);
198 self.check_and_restock(vec![ticket_type]).await;
200 }
201 BandwidthControllerRequest::UpgradeModeToken(return_sender) => {
202 return_sender.send(self.get_upgrade_mode_token().await)
203 }
204 BandwidthControllerRequest::AttemptRevertSpending(return_sender, metadata) => {
205 return_sender.send(self.attempt_revert_ticket_usage(metadata).await)
206 }
207 BandwidthControllerRequest::SetCredentialFetcher(return_sender, fetcher) => {
208 self.handle_set_credential_fetcher(fetcher).await;
209 return_sender.send(Ok(()))
210 }
211
212 BandwidthControllerRequest::SetPublicDataFetcher(return_sender, fetcher) => {
213 self.public_data_fetcher = fetcher;
214 return_sender.send(Ok(()))
215 }
216 BandwidthControllerRequest::Reset(return_sender) => {
217 return_sender.send(self.handle_reset().await)
218 }
219 BandwidthControllerRequest::ClearEmergencyCredentials(return_sender) => return_sender
220 .send(
221 self.storage
222 .clear_emergency_credentials()
223 .await
224 .map_err(BandwidthControllerError::credential_storage_error),
225 ),
226 BandwidthControllerRequest::GetAvailableTicketbooks(return_sender) => {
227 return_sender.send(self.handle_get_available_ticketbooks().await)
228 }
229 BandwidthControllerRequest::RestockTicketbooks(return_sender, ticket_types) => {
230 self.check_and_restock(ticket_types).await;
231 return_sender.send(Ok(()))
232 }
233 BandwidthControllerRequest::WaitForTicketbooks(return_sender, ticket_types) => {
234 self.handle_wait_for_ticketbooks(ReadinessRequest {
235 return_sender,
236 ticket_types,
237 })
238 .await
239 }
240 }
241 }
242
243 async fn handle_set_credential_fetcher(&mut self, fetcher: Option<Arc<dyn CredentialFetcher>>) {
244 self.in_flight.cancel_and_join().await;
245 if let Some(old_fetcher) = self.credential_fetcher.take() {
246 old_fetcher.cleanup().await;
247 }
248 self.public_data_fetcher = fetcher
250 .clone()
251 .map(|f| f as Arc<dyn CredentialPublicDataFetcher>);
252 self.credential_fetcher = fetcher;
253 self.check_and_restock(AvailableTicketbooks::ticketbook_types())
254 .await;
255 }
256
257 async fn handle_reset(&mut self) -> Result<(), BandwidthControllerError> {
259 self.in_flight.cancel_and_join().await;
264
265 if let Some(fetcher) = &self.credential_fetcher {
266 fetcher.cleanup().await;
267 }
268 self.credential_fetcher = None;
269
270 let requests = std::mem::take(&mut self.pending_readiness);
271 requests.into_iter().for_each(|r| r.cancel());
272
273 self.storage
275 .clear_ticketbooks()
276 .await
277 .map_err(BandwidthControllerError::credential_storage_error)?;
278 self.storage
279 .clear_emergency_credentials()
280 .await
281 .map_err(BandwidthControllerError::credential_storage_error)
282 }
283
284 async fn handle_get_available_ticketbooks(
285 &self,
286 ) -> Result<AvailableTicketbooks, BandwidthControllerError> {
287 self.print_info().await?;
288 self.get_available_ticketbooks().await
289 }
290
291 async fn handle_wait_for_ticketbooks(&mut self, request: ReadinessRequest) {
292 let snapshot = match self.build_readiness_snapshot(None).await {
293 Ok(snapshot) => snapshot,
294 Err(err) => {
295 tracing::warn!("could not assess ticketbook readiness: {err}");
297 self.pending_readiness.push(request);
298 return;
299 }
300 };
301 tracing::debug!("Readiness snapshot : {:#?}", snapshot);
302 if let Some(request) = request.try_resolve(&snapshot) {
303 self.pending_readiness.push(request);
304 }
305 }
306
307 pub async fn prepare_ecash_ticket(
312 &self,
313 ticketbook_type: TicketType,
314 provider_pk: [u8; 32],
315 tickets_to_spend: u32,
316 spend_time: OffsetDateTime,
317 ) -> Result<Option<PreparedCredential>, BandwidthControllerError> {
318 let Some(retrieved_ticketbook) = self
319 .get_next_usable_ticketbook(ticketbook_type, tickets_to_spend)
320 .await?
321 else {
322 return Ok(None);
323 };
324
325 let ticketbook_id = retrieved_ticketbook.ticketbook_id;
326 let epoch_id = retrieved_ticketbook.ticketbook.epoch_id();
327
328 let used_tickets =
329 retrieved_ticketbook.ticketbook.spent_tickets() as u32 + tickets_to_spend;
330 let metadata = PreparedCredentialMetadata {
331 ticketbook_id,
332 tickets_withdrawn: tickets_to_spend,
333 used_tickets,
334 };
335
336 match self
337 .prepare_ecash_ticket_inner(
338 provider_pk,
339 spend_time,
340 tickets_to_spend,
341 retrieved_ticketbook,
342 )
343 .await
344 {
345 Ok(data) => Ok(Some(PreparedCredential {
346 data,
347 epoch_id,
348 metadata,
349 })),
350 Err(err) => {
351 error!("failed to prepare credential spending request. attempting to revert withdrawal...");
352 self.attempt_revert_ticket_usage(metadata).await?;
353 Err(err)
354 }
355 }
356 }
357
358 async fn prepare_ecash_ticket_inner(
359 &self,
360 provider_pk: [u8; 32],
361 spend_time: OffsetDateTime,
362 tickets_to_spend: u32,
363 mut retrieved_ticketbook: RetrievedTicketbook,
364 ) -> Result<CredentialSpendingData, BandwidthControllerError> {
365 let epoch_id = retrieved_ticketbook.ticketbook.epoch_id();
366 let expiration_date = retrieved_ticketbook.ticketbook.expiration_date();
367
368 let verification_key = self.ensure_master_verification_key(epoch_id).await?;
369 let expiration_signatures = self
370 .ensure_expiration_date_signatures(epoch_id, expiration_date)
371 .await?;
372 let coin_indices_signatures = self.ensure_coin_index_signatures(epoch_id).await?;
373
374 let pay_info = retrieved_ticketbook
375 .ticketbook
376 .generate_pay_info(provider_pk, spend_time);
377
378 let spend_request = retrieved_ticketbook.ticketbook.prepare_for_spending(
379 &verification_key,
380 pay_info.into(),
381 &coin_indices_signatures,
382 &expiration_signatures,
383 tickets_to_spend as u64,
384 )?;
385 Ok(spend_request)
386 }
387
388 pub async fn get_next_usable_ticketbook(
390 &self,
391 ticketbook_type: TicketType,
392 tickets: u32,
393 ) -> Result<Option<RetrievedTicketbook>, BandwidthControllerError> {
394 self.storage
395 .get_next_unspent_usable_ticketbook(ticketbook_type.to_string(), tickets)
396 .await
397 .map_err(BandwidthControllerError::credential_storage_error)
398 }
399
400 async fn attempt_revert_ticket_usage(
401 &self,
402 info: PreparedCredentialMetadata,
403 ) -> Result<bool, BandwidthControllerError> {
404 self.storage
405 .attempt_revert_ticketbook_withdrawal(
406 info.ticketbook_id,
407 info.tickets_withdrawn,
408 info.used_tickets,
409 )
410 .await
411 .map_err(BandwidthControllerError::credential_storage_error)
412 }
413
414 pub async fn fetch_ticketbook(
422 &self,
423 ticketbook_type: TicketType,
424 ) -> Result<(), BandwidthControllerError> {
425 let Some(fetcher) = &self.credential_fetcher else {
426 return Err(BandwidthControllerError::MissingCredentialFetcher);
427 };
428 let credentials = fetcher
429 .fetch_ticketbooks(ticketbook_type)
430 .await
431 .map_err(BandwidthControllerError::fetcher_error)?;
432 self.store_fetched(credentials).await;
433 Ok(())
434 }
435
436 async fn check_and_restock(&mut self, ticketbook_types: Vec<TicketType>) {
438 let available = match self.get_available_ticketbooks().await {
439 Ok(available) => available,
440 Err(err) => {
441 tracing::warn!("could not assess ticket stock for restocking: {err}");
442 return;
443 }
444 };
445
446 for typ in ticketbook_types {
447 tracing::debug!("Checking credential stock for {typ} ticket");
448 if available.needs_restock(typ, self.config) {
449 tracing::debug!("{typ} tickets need a restock");
450 self.ensure_stocked(typ);
451 }
452 }
453 }
454
455 fn ensure_stocked(&mut self, ticketbook_type: TicketType) {
458 if self.in_flight.contains(ticketbook_type) {
459 tracing::debug!("{ticketbook_type} ticket restock already in flight");
461 return;
462 }
463 let Some(fetcher) = &self.credential_fetcher else {
464 tracing::debug!("No credential fetcher set. No restock possible");
465 return;
466 };
467 tracing::debug!("requesting more {ticketbook_type} ticketbooks");
468 self.in_flight.spawn(ticketbook_type, Arc::clone(fetcher));
469 }
470
471 async fn on_fetch_complete(&mut self, ticket_type: TicketType, received: FetchResult) {
474 let failure = match received {
476 Ok(Some(Ok(credentials))) => {
477 self.store_fetched(credentials).await;
478 tracing::info!("fetched and stored a {ticket_type} ticketbook");
479 None
480 }
481 Ok(Some(Err(err))) => {
482 tracing::warn!("failed to fetch {ticket_type} ticketbooks: {err}");
483 Some(FetchFailure {
484 ticket_type,
485 error: err,
486 })
487 }
488 Ok(None) => {
489 tracing::debug!("fetch for {ticket_type} ticketbooks was cancelled");
491 None
492 }
493 Err(_recv_err) => {
494 tracing::error!("a credential fetch task for {ticket_type} terminated abnormally");
497 None
498 }
499 };
500 self.resolve_pending_waiters(failure).await;
501 }
502
503 async fn store_fetched(&self, credentials: Vec<NymCredential>) {
505 for credential in credentials {
506 match credential {
507 NymCredential::Ticketbook(ticketbook) => self.store_ticketbook(*ticketbook).await,
508 NymCredential::UpgradeModeToken { jwt, expiration } => {
509 self.store_upgrade_token(jwt, expiration).await
510 }
511 }
512 }
513 }
514
515 async fn store_ticketbook(&self, ticketbook: IssuedTicketBook) {
521 let epoch_id = ticketbook.epoch_id();
523
524 if let Err(err) = self.ensure_master_verification_key(epoch_id).await {
525 tracing::warn!("failed to ensure master verification key for epoch {epoch_id}: {err}");
526 }
527 if let Err(err) = self.ensure_coin_index_signatures(epoch_id).await {
528 tracing::warn!("failed to ensure coin index signatures for epoch {epoch_id}: {err}");
529 }
530
531 if let Err(err) = self
532 .ensure_expiration_date_signatures(epoch_id, ticketbook.expiration_date())
533 .await
534 {
535 tracing::warn!(
536 "failed to ensure expiration date signatures for epoch {epoch_id}: {err}"
537 );
538 }
539
540 if let Err(err) = self.storage.insert_issued_ticketbook(&ticketbook).await {
541 tracing::warn!("failed to store ticketbook: {err}");
542 }
543 }
544
545 async fn store_upgrade_token(&self, jwt: String, expiration: OffsetDateTime) {
546 let credential_content = EmergencyCredentialContent {
547 typ: UPGRADE_MODE_JWT_TYPE.into(),
548 content: jwt.into_bytes(),
549 expiration: Some(expiration),
550 };
551 if let Err(e) = self
552 .storage
553 .insert_emergency_credential(&credential_content)
554 .await
555 {
556 tracing::warn!("failed to store emergency credential: {e}");
557 }
558 }
559
560 fn is_in_flight(&self, typ: TicketType) -> bool {
565 self.in_flight.contains(typ)
566 }
567
568 async fn build_readiness_snapshot(
569 &self,
570 failure: Option<FetchFailure>,
571 ) -> Result<ReadinessSnapshot, BandwidthControllerError> {
572 let upgrade_mode = self.get_upgrade_mode_token().await?.is_some();
573 let available = self.get_available_ticketbooks().await?;
574
575 let mut tickets_readiness = HashMap::new();
576 for typ in AvailableTicketbooks::ticketbook_types() {
577 let status = if available.contains_minimal_tickets(typ, self.config) {
578 ReadinessStatus::Ready
579 } else if self.is_in_flight(typ) {
580 ReadinessStatus::InFlight
581 } else {
582 match failure.as_ref() {
583 Some(failure) if failure.ticket_type == typ => {
584 ReadinessStatus::FetchFailed(failure.error.to_string())
585 }
586 _ => ReadinessStatus::Unavailable,
587 }
588 };
589 tickets_readiness.insert(typ, status);
590 }
591
592 Ok(ReadinessSnapshot {
593 upgrade_mode,
594 tickets_readiness,
595 })
596 }
597
598 async fn resolve_pending_waiters(&mut self, failure: Option<FetchFailure>) {
601 if self.pending_readiness.is_empty() {
602 return;
603 }
604 let snapshot = match self.build_readiness_snapshot(failure).await {
605 Ok(snapshot) => snapshot,
606 Err(err) => {
607 tracing::warn!("could not assess ticketbook readiness: {err}");
609 return;
610 }
611 };
612
613 tracing::debug!("Readiness snapshot : {:#?}", snapshot);
614
615 let requests = std::mem::take(&mut self.pending_readiness);
616
617 let still_waiting = requests
618 .into_iter()
619 .filter_map(|request| request.try_resolve(&snapshot))
620 .collect();
621
622 self.pending_readiness = still_waiting;
623 }
624
625 async fn ensure_master_verification_key(
632 &self,
633 epoch_id: EpochId,
634 ) -> Result<VerificationKeyAuth, BandwidthControllerError> {
635 if let Some(key) = self
636 .storage
637 .get_master_verification_key(epoch_id)
638 .await
639 .map_err(BandwidthControllerError::credential_storage_error)?
640 {
641 return Ok(key);
642 }
643 let Some(fetcher) = &self.public_data_fetcher else {
644 return Err(BandwidthControllerError::MissingVerificationKey { epoch_id });
645 };
646 let key = fetcher
647 .fetch_master_verification_key(epoch_id)
648 .await
649 .map_err(BandwidthControllerError::fetcher_error)?;
650 self.storage
651 .insert_master_verification_key(&key)
652 .await
653 .map_err(BandwidthControllerError::credential_storage_error)?;
654 Ok(key.key)
655 }
656
657 async fn ensure_coin_index_signatures(
660 &self,
661 epoch_id: EpochId,
662 ) -> Result<Vec<AnnotatedCoinIndexSignature>, BandwidthControllerError> {
663 if let Some(signatures) = self
664 .storage
665 .get_coin_index_signatures(epoch_id)
666 .await
667 .map_err(BandwidthControllerError::credential_storage_error)?
668 {
669 return Ok(signatures);
670 }
671 let Some(fetcher) = &self.public_data_fetcher else {
672 return Err(BandwidthControllerError::MissingCoinIndexSignatures { epoch_id });
673 };
674 let signatures = fetcher
675 .fetch_coin_index_signatures(epoch_id)
676 .await
677 .map_err(BandwidthControllerError::fetcher_error)?;
678 self.storage
679 .insert_coin_index_signatures(&signatures)
680 .await
681 .map_err(BandwidthControllerError::credential_storage_error)?;
682 Ok(signatures.signatures)
683 }
684
685 async fn ensure_expiration_date_signatures(
688 &self,
689 epoch_id: EpochId,
690 expiration_date: Date,
691 ) -> Result<Vec<AnnotatedExpirationDateSignature>, BandwidthControllerError> {
692 if let Some(signatures) = self
693 .storage
694 .get_expiration_date_signatures(expiration_date, epoch_id)
695 .await
696 .map_err(BandwidthControllerError::credential_storage_error)?
697 {
698 return Ok(signatures);
699 }
700 let Some(fetcher) = &self.public_data_fetcher else {
701 return Err(BandwidthControllerError::MissingExpirationDateSignatures { epoch_id });
702 };
703 let signatures = fetcher
704 .fetch_expiration_date_signatures(expiration_date, epoch_id)
705 .await
706 .map_err(BandwidthControllerError::fetcher_error)?;
707 self.storage
708 .insert_expiration_date_signatures(&signatures)
709 .await
710 .map_err(BandwidthControllerError::credential_storage_error)?;
711 Ok(signatures.signatures)
712 }
713
714 async fn ensure_global_data(&self) {
721 let ticketbooks = match self.storage.get_ticketbooks_info().await {
722 Ok(ticketbooks) => ticketbooks,
723 Err(err) => {
724 tracing::warn!("could not read ticketbooks to ensure global data: {err}");
725 return;
726 }
727 };
728
729 let epochs = ticketbooks
731 .iter()
732 .map(|ticketbook| EpochId::from(ticketbook.epoch_id))
733 .collect::<HashSet<_>>();
734 for epoch_id in epochs {
735 if let Err(err) = self.ensure_master_verification_key(epoch_id).await {
736 tracing::warn!(
737 "failed to ensure master verification key for epoch {epoch_id}: {err}"
738 );
739 }
740 if let Err(err) = self.ensure_coin_index_signatures(epoch_id).await {
741 tracing::warn!(
742 "failed to ensure coin index signatures for epoch {epoch_id}: {err}"
743 );
744 }
745 }
746
747 let expirations = ticketbooks
749 .iter()
750 .map(|ticketbook| {
751 (
752 EpochId::from(ticketbook.epoch_id),
753 ticketbook.expiration_date,
754 )
755 })
756 .collect::<HashSet<_>>();
757 for (epoch_id, expiration_date) in expirations {
758 if let Err(err) = self
759 .ensure_expiration_date_signatures(epoch_id, expiration_date)
760 .await
761 {
762 tracing::warn!(
763 "failed to ensure expiration date signatures for epoch {epoch_id}: {err}"
764 );
765 }
766 }
767 }
768
769 async fn get_upgrade_mode_token(&self) -> Result<Option<String>, BandwidthControllerError> {
774 let Some(emergency_credential) = self
775 .storage
776 .get_emergency_credential(UPGRADE_MODE_JWT_TYPE)
777 .await
778 .map_err(BandwidthControllerError::credential_storage_error)?
779 else {
780 return Ok(None);
781 };
782 let token = String::from_utf8(emergency_credential.data.content)
784 .map_err(|_| BandwidthControllerError::MalformedUpgradeModeToken)?;
785 Ok(Some(token))
786 }
787
788 async fn get_available_ticketbooks(
789 &self,
790 ) -> Result<AvailableTicketbooks, BandwidthControllerError> {
791 let ticketbooks_info = self
792 .storage
793 .get_ticketbooks_info()
794 .await
795 .map_err(BandwidthControllerError::credential_storage_error)?;
796 AvailableTicketbooks::try_from(ticketbooks_info)
797 }
798
799 async fn print_info(&self) -> Result<(), BandwidthControllerError> {
800 let ticketbooks_info = self.get_available_ticketbooks().await?;
801 let num_ticketbooks = ticketbooks_info.len_not_expired();
802 let num_total_ticketbooks = ticketbooks_info.len();
803 tracing::info!("Ticketbooks stored: {num_ticketbooks}");
804 tracing::debug!("Total ticketbooks stored: {num_total_ticketbooks}");
805 for ticketbook in ticketbooks_info {
806 if ticketbook.has_expired() {
807 tracing::debug!("Expired ticketbook: {ticketbook}");
808 } else if ticketbook.expired_soon(OffsetDateTime::now_utc(), self.config) {
809 tracing::info!("Soon expired ticketbook: {ticketbook}");
810 } else {
811 tracing::info!("Ticketbook: {ticketbook}");
812 }
813 }
814
815 Ok(())
816 }
817}
818
819#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
821#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
822impl<St: Storage> BandwidthTicketProvider for BandwidthController<St> {
823 async fn get_ecash_ticket(
824 &self,
825 ticket_type: TicketType,
826 gateway_id: ed25519::PublicKey,
827 tickets_to_spend: u32,
828 spend_time: OffsetDateTime,
829 ) -> Result<Option<PreparedCredential>, BandwidthControllerError> {
830 self.prepare_ecash_ticket(
831 ticket_type,
832 gateway_id.to_bytes(),
833 tickets_to_spend,
834 spend_time,
835 )
836 .await
837 }
838
839 async fn get_upgrade_mode_token(&self) -> Result<Option<String>, BandwidthControllerError> {
840 self.get_upgrade_mode_token().await
841 }
842
843 async fn attempt_revert_spending(
844 &self,
845 metadata: PreparedCredentialMetadata,
846 ) -> Result<bool, BandwidthControllerError> {
847 self.attempt_revert_ticket_usage(metadata).await
848 }
849
850 async fn close(&self) {
851 self.storage.close().await
852 }
853}