// Copyright 2022-2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 use crate::models::{ BasicTicketbookInformation, EmergencyCredential, EmergencyCredentialContent, RawCoinIndexSignatures, RawExpirationDateSignatures, RawVerificationKey, StoredIssuedTicketbook, StoredPendingTicketbook, }; use nym_ecash_time::Date; use nym_sqlx_pool_guard::SqlitePoolGuard; use sqlx::{Executor, Sqlite, Transaction}; #[derive(Clone)] pub struct SqliteEcashTicketbookManager { connection_pool: SqlitePoolGuard, } impl SqliteEcashTicketbookManager { /// Creates new instance of the `EcashTicketbookManager` with the provided sqlite connection pool. /// /// # Arguments /// /// * `connection_pool`: database connection pool to use. pub fn new(connection_pool: SqlitePoolGuard) -> Self { SqliteEcashTicketbookManager { connection_pool } } /// Closes the connection pool. pub async fn close(&self) { self.connection_pool.close().await } pub(crate) async fn cleanup_expired(&self, deadline: Date) -> Result<(), sqlx::Error> { sqlx::query!( "DELETE FROM ecash_ticketbook WHERE expiration_date <= ?", deadline ) .execute(&*self.connection_pool) .await?; Ok(()) } /// Starts a write (IMMEDIATE) transaction, to prevent issue when upgrading from a read one to a write one pub(crate) async fn begin_storage_write_tx( &self, ) -> Result, sqlx::Error> { self.connection_pool.begin_with("BEGIN IMMEDIATE").await } pub(crate) async fn insert_pending_ticketbook( &self, serialisation_revision: u8, deposit_id: u32, data: &[u8], expiration_date: Date, ) -> Result<(), sqlx::Error> { sqlx::query!( r#" INSERT INTO pending_issuance (deposit_id, serialization_revision, pending_ticketbook_data, expiration_date) VALUES (?, ?, ?, ?) "#, deposit_id, serialisation_revision, data, expiration_date, ) .execute(&*self.connection_pool) .await?; Ok(()) } #[allow(clippy::too_many_arguments)] pub(crate) async fn insert_new_ticketbook( &self, serialisation_revision: u8, data: &[u8], expiration_date: Date, typ: &str, epoch_id: u32, total_tickets: u32, used_tickets: u32, ) -> Result<(), sqlx::Error> { sqlx::query!( r#" INSERT INTO ecash_ticketbook (serialization_revision, ticketbook_data, expiration_date, ticketbook_type, epoch_id, total_tickets, used_tickets) VALUES (?, ?, ?, ?, ?, ?, ?) "#, serialisation_revision, data, expiration_date, typ, epoch_id, total_tickets, used_tickets, ).execute(&*self.connection_pool).await?; Ok(()) } pub(crate) async fn contains_ticketbook_data(&self, data: &[u8]) -> Result { let exists = sqlx::query( r#" SELECT 1 FROM ecash_ticketbook WHERE ticketbook_data = ? "#, ) .bind(data) .fetch_optional(&*self.connection_pool) .await? .is_some(); Ok(exists) } pub(crate) async fn get_ticketbooks_info( &self, ) -> Result, sqlx::Error> { sqlx::query_as( r#" SELECT id, expiration_date, ticketbook_type, epoch_id, total_tickets, used_tickets FROM ecash_ticketbook "#, ) .fetch_all(&*self.connection_pool) .await } pub(crate) async fn decrease_used_ticketbook_tickets( &self, ticketbook_id: i64, reverted_spent: u32, expected_current_total_spent: u32, ) -> Result { // the 'AND' clause will ensure this will only be executed if nobody else interacted with the row let affected = sqlx::query!( r#" UPDATE ecash_ticketbook SET used_tickets = used_tickets - ? WHERE id = ? AND used_tickets = ? "#, reverted_spent, ticketbook_id, expected_current_total_spent ) .execute(&*self.connection_pool) .await? .rows_affected(); Ok(affected > 0) } pub(crate) async fn get_pending_ticketbooks( &self, ) -> Result, sqlx::Error> { sqlx::query_as("SELECT * FROM pending_issuance") .fetch_all(&*self.connection_pool) .await } pub(crate) async fn remove_pending_ticketbook( &self, pending_id: i64, ) -> Result<(), sqlx::Error> { sqlx::query!( "DELETE FROM pending_issuance WHERE deposit_id = ?", pending_id ) .execute(&*self.connection_pool) .await?; Ok(()) } pub(crate) async fn get_master_verification_key( &self, epoch_id: i64, ) -> Result, sqlx::Error> { sqlx::query_as!( RawVerificationKey, r#" SELECT epoch_id as "epoch_id: u32", serialised_key, serialization_revision as "serialization_revision: u8" FROM master_verification_key WHERE epoch_id = ? "#, epoch_id ) .fetch_optional(&*self.connection_pool) .await } pub(crate) async fn insert_master_verification_key( &self, serialisation_revision: u8, epoch_id: i64, data: &[u8], ) -> Result<(), sqlx::Error> { sqlx::query!( r#" INSERT OR IGNORE INTO master_verification_key(epoch_id, serialised_key, serialization_revision) VALUES (?, ?, ?); UPDATE master_verification_key SET serialised_key = ?, serialization_revision = ? WHERE epoch_id = ? "#, epoch_id, data, serialisation_revision, data, serialisation_revision, epoch_id ) .execute(&*self.connection_pool) .await?; Ok(()) } pub(crate) async fn get_coin_index_signatures( &self, epoch_id: i64, ) -> Result, sqlx::Error> { sqlx::query_as!( RawCoinIndexSignatures, r#" SELECT epoch_id as "epoch_id: u32", serialised_signatures, serialization_revision as "serialization_revision: u8" FROM coin_indices_signatures WHERE epoch_id = ? "#, epoch_id ) .fetch_optional(&*self.connection_pool) .await } pub(crate) async fn insert_coin_index_signatures( &self, serialisation_revision: u8, epoch_id: i64, data: &[u8], ) -> Result<(), sqlx::Error> { sqlx::query!( r#" INSERT OR IGNORE INTO coin_indices_signatures(epoch_id, serialised_signatures, serialization_revision) VALUES (?, ?, ?); UPDATE coin_indices_signatures SET serialised_signatures = ?, serialization_revision = ? WHERE epoch_id = ? "#, epoch_id, data, serialisation_revision, data, serialisation_revision, epoch_id, ) .execute(&*self.connection_pool) .await?; Ok(()) } pub(crate) async fn get_expiration_date_signatures( &self, expiration_date: Date, epoch_id: i64, ) -> Result, sqlx::Error> { sqlx::query_as!( RawExpirationDateSignatures, r#" SELECT serialised_signatures, serialization_revision as "serialization_revision: u8" FROM expiration_date_signatures WHERE expiration_date = ? AND epoch_id = ? "#, expiration_date, epoch_id ) .fetch_optional(&*self.connection_pool) .await } pub(crate) async fn insert_expiration_date_signatures( &self, serialisation_revision: u8, epoch_id: i64, expiration_date: Date, data: &[u8], ) -> Result<(), sqlx::Error> { sqlx::query!( r#" INSERT OR IGNORE INTO expiration_date_signatures(expiration_date, epoch_id, serialised_signatures, serialization_revision) VALUES (?, ?, ?, ?); UPDATE expiration_date_signatures SET serialised_signatures = ?, serialization_revision = ? WHERE expiration_date = ? "#, expiration_date, epoch_id, data, serialisation_revision, data, serialisation_revision, expiration_date ) .execute(&*self.connection_pool) .await?; Ok(()) } pub(crate) async fn get_emergency_credential( &self, typ: &str, ) -> Result, sqlx::Error> { sqlx::query_as( r#" SELECT * FROM emergency_credential WHERE type = ? AND (expiration IS NULL OR expiration > CURRENT_TIMESTAMP) ORDER BY expiration DESC NULLS LAST LIMIT 1 "#, ) .bind(typ) .fetch_optional(&*self.connection_pool) .await } pub(crate) async fn insert_emergency_credential( &self, credential: &EmergencyCredentialContent, ) -> Result<(), sqlx::Error> { sqlx::query!( r#" INSERT INTO emergency_credential (type, content, expiration) VALUES (?, ?, ?) ON CONFLICT(type, content) DO NOTHING; "#, credential.typ, credential.content, credential.expiration, ) .execute(&*self.connection_pool) .await?; Ok(()) } pub(crate) async fn remove_emergency_credential(&self, id: i64) -> Result<(), sqlx::Error> { sqlx::query!( r#" DELETE FROM emergency_credential WHERE id = ? "#, id ) .execute(&*self.connection_pool) .await?; Ok(()) } pub(crate) async fn remove_emergency_credentials_of_type( &self, typ: &str, ) -> Result<(), sqlx::Error> { sqlx::query!( r#" DELETE FROM emergency_credential WHERE type = ? "#, typ ) .execute(&*self.connection_pool) .await?; Ok(()) } } pub(crate) async fn get_next_unspent_ticketbook<'a, E>( executor: E, ticketbook_type: String, deadline: Date, tickets: u32, ) -> Result, sqlx::Error> where E: Executor<'a, Database = Sqlite>, { sqlx::query_as( r#" SELECT * FROM ecash_ticketbook WHERE used_tickets + ? <= total_tickets AND expiration_date >= ? AND ticketbook_type = ? ORDER BY expiration_date ASC LIMIT 1 "#, ) .bind(tickets) .bind(deadline) .bind(ticketbook_type) .fetch_optional(executor) .await } pub(crate) async fn increase_used_ticketbook_tickets<'a, E>( executor: E, ticketbook_id: i64, extra_spent: u32, ) -> Result<(), sqlx::Error> where E: Executor<'a, Database = Sqlite>, { sqlx::query!( "UPDATE ecash_ticketbook SET used_tickets = used_tickets + ? WHERE id = ?", extra_spent, ticketbook_id ) .execute(executor) .await?; Ok(()) }