diff --git a/crates/cdk-common/src/database/mod.rs b/crates/cdk-common/src/database/mod.rs index 586c6a86b..5cf962535 100644 --- a/crates/cdk-common/src/database/mod.rs +++ b/crates/cdk-common/src/database/mod.rs @@ -30,10 +30,7 @@ pub use mint::{ #[cfg(all(feature = "mint", feature = "auth"))] pub use mint::{DynMintAuthDatabase, MintAuthDatabase, MintAuthTransaction}; #[cfg(feature = "wallet")] -pub use wallet::{ - Database as WalletDatabase, DatabaseTransaction as WalletDatabaseTransaction, - DynWalletDatabaseTransaction, -}; +pub use wallet::Database as WalletDatabase; /// A wrapper indicating that a resource has been acquired with a database lock. /// diff --git a/crates/cdk-common/src/database/wallet/mod.rs b/crates/cdk-common/src/database/wallet/mod.rs index dc489f32d..3eceff2d7 100644 --- a/crates/cdk-common/src/database/wallet/mod.rs +++ b/crates/cdk-common/src/database/wallet/mod.rs @@ -6,9 +6,9 @@ use std::fmt::Debug; use async_trait::async_trait; use cashu::KeySet; -use super::{DbTransactionFinalizer, Error}; +use super::Error; use crate::common::ProofInfo; -use crate::database::{KVStoreDatabase, KVStoreTransaction}; +use crate::database::KVStoreDatabase; use crate::mint_url::MintUrl; use crate::nuts::{ CurrencyUnit, Id, KeySetInfo, Keys, MintInfo, PublicKey, SpendingConditions, State, @@ -20,101 +20,6 @@ use crate::wallet::{ #[cfg(feature = "test")] pub mod test; -/// Easy to use Dynamic Database type alias -pub type DynWalletDatabaseTransaction = Box + Sync + Send>; - -/// Database transaction -/// -/// This trait encapsulates all the changes to be done in the wallet -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -pub trait DatabaseTransaction: - KVStoreTransaction + DbTransactionFinalizer -{ - /// Add Mint to storage - async fn add_mint( - &mut self, - mint_url: MintUrl, - mint_info: Option, - ) -> Result<(), Error>; - - /// Remove Mint from storage - async fn remove_mint(&mut self, mint_url: MintUrl) -> Result<(), Error>; - - /// Update mint url - async fn update_mint_url( - &mut self, - old_mint_url: MintUrl, - new_mint_url: MintUrl, - ) -> Result<(), Error>; - - /// Get mint keyset by id - async fn get_keyset_by_id(&mut self, keyset_id: &Id) -> Result, Error>; - - /// Get [`Keys`] from storage - async fn get_keys(&mut self, id: &Id) -> Result, Error>; - - /// Add mint keyset to storage - async fn add_mint_keysets( - &mut self, - mint_url: MintUrl, - keysets: Vec, - ) -> Result<(), Error>; - - /// Get mint quote from storage. This function locks the returned minted quote for update - async fn get_mint_quote(&mut self, quote_id: &str) -> Result, Error>; - - /// Add mint quote to storage - async fn add_mint_quote(&mut self, quote: WalletMintQuote) -> Result<(), Error>; - - /// Remove mint quote from storage - async fn remove_mint_quote(&mut self, quote_id: &str) -> Result<(), Error>; - - /// Get melt quote from storage - async fn get_melt_quote(&mut self, quote_id: &str) -> Result, Error>; - - /// Add melt quote to storage - async fn add_melt_quote(&mut self, quote: wallet::MeltQuote) -> Result<(), Error>; - - /// Remove melt quote from storage - async fn remove_melt_quote(&mut self, quote_id: &str) -> Result<(), Error>; - - /// Add [`Keys`] to storage - async fn add_keys(&mut self, keyset: KeySet) -> Result<(), Error>; - - /// Remove [`Keys`] from storage - async fn remove_keys(&mut self, id: &Id) -> Result<(), Error>; - - /// Get proofs from storage and lock them for update - async fn get_proofs( - &mut self, - mint_url: Option, - unit: Option, - state: Option>, - spending_conditions: Option>, - ) -> Result, Error>; - - /// Update the proofs in storage by adding new proofs or removing proofs by - /// their Y value. - async fn update_proofs( - &mut self, - added: Vec, - removed_ys: Vec, - ) -> Result<(), Error>; - - /// Update proofs state in storage - async fn update_proofs_state(&mut self, ys: Vec, state: State) -> Result<(), Error>; - - /// Atomically increment Keyset counter and return new value - async fn increment_keyset_counter(&mut self, keyset_id: &Id, count: u32) -> Result; - - /// Add transaction to storage - async fn add_transaction(&mut self, transaction: Transaction) -> Result<(), Error>; - - /// Remove transaction from storage - async fn remove_transaction(&mut self, transaction_id: TransactionId) -> Result<(), Error>; -} - /// Wallet Database trait #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] @@ -122,11 +27,6 @@ pub trait Database: KVStoreDatabase + Debug where Err: Into + From, { - /// Begins a DB transaction - async fn begin_db_transaction( - &self, - ) -> Result + Send + Sync>, Err>; - /// Get mint from storage async fn get_mint(&self, mint_url: MintUrl) -> Result, Err>; @@ -191,4 +91,81 @@ where direction: Option, unit: Option, ) -> Result, Err>; + + /// Update the proofs in storage by adding new proofs or removing proofs by + /// their Y value (without transaction) + async fn update_proofs( + &self, + added: Vec, + removed_ys: Vec, + ) -> Result<(), Err>; + + /// Update proofs state in storage (without transaction) + async fn update_proofs_state(&self, ys: Vec, state: State) -> Result<(), Err>; + + /// Add transaction to storage (without transaction) + async fn add_transaction(&self, transaction: Transaction) -> Result<(), Err>; + + /// Update mint url (without transaction) + async fn update_mint_url( + &self, + old_mint_url: MintUrl, + new_mint_url: MintUrl, + ) -> Result<(), Err>; + + /// Atomically increment Keyset counter and return new value (without transaction) + async fn increment_keyset_counter(&self, keyset_id: &Id, count: u32) -> Result; + + /// Add Mint to storage + async fn add_mint(&self, mint_url: MintUrl, mint_info: Option) -> Result<(), Err>; + + /// Remove Mint from storage + async fn remove_mint(&self, mint_url: MintUrl) -> Result<(), Err>; + + /// Add mint keyset to storage + async fn add_mint_keysets( + &self, + mint_url: MintUrl, + keysets: Vec, + ) -> Result<(), Err>; + + /// Add mint quote to storage + async fn add_mint_quote(&self, quote: WalletMintQuote) -> Result<(), Err>; + + /// Remove mint quote from storage + async fn remove_mint_quote(&self, quote_id: &str) -> Result<(), Err>; + + /// Add melt quote to storage + async fn add_melt_quote(&self, quote: wallet::MeltQuote) -> Result<(), Err>; + + /// Remove melt quote from storage + async fn remove_melt_quote(&self, quote_id: &str) -> Result<(), Err>; + + /// Add [`Keys`] to storage + async fn add_keys(&self, keyset: KeySet) -> Result<(), Err>; + + /// Remove [`Keys`] from storage + async fn remove_keys(&self, id: &Id) -> Result<(), Err>; + + /// Remove transaction from storage + async fn remove_transaction(&self, transaction_id: TransactionId) -> Result<(), Err>; + + // KV Store write methods (non-transactional) + + /// Write a value to the key-value store + async fn kv_write( + &self, + primary_namespace: &str, + secondary_namespace: &str, + key: &str, + value: &[u8], + ) -> Result<(), Err>; + + /// Remove a value from the key-value store + async fn kv_remove( + &self, + primary_namespace: &str, + secondary_namespace: &str, + key: &str, + ) -> Result<(), Err>; } diff --git a/crates/cdk-common/src/database/wallet/test/mod.rs b/crates/cdk-common/src/database/wallet/test/mod.rs index 8725fec1c..243247324 100644 --- a/crates/cdk-common/src/database/wallet/test/mod.rs +++ b/crates/cdk-common/src/database/wallet/test/mod.rs @@ -164,11 +164,9 @@ where let mint_info = MintInfo::default(); // Add mint - let mut tx = db.begin_db_transaction().await.unwrap(); - tx.add_mint(mint_url.clone(), Some(mint_info.clone())) + db.add_mint(mint_url.clone(), Some(mint_info.clone())) .await .unwrap(); - tx.commit().await.unwrap(); // Get mint let retrieved = db.get_mint(mint_url.clone()).await.unwrap(); @@ -186,9 +184,7 @@ where { let mint_url = test_mint_url(); - let mut tx = db.begin_db_transaction().await.unwrap(); - tx.add_mint(mint_url.clone(), None).await.unwrap(); - tx.commit().await.unwrap(); + db.add_mint(mint_url.clone(), None).await.unwrap(); // Verify mint exists in the database let mints = db.get_mints().await.unwrap(); @@ -203,14 +199,10 @@ where let mint_url = test_mint_url(); // Add mint - let mut tx = db.begin_db_transaction().await.unwrap(); - tx.add_mint(mint_url.clone(), None).await.unwrap(); - tx.commit().await.unwrap(); + db.add_mint(mint_url.clone(), None).await.unwrap(); // Remove mint - let mut tx = db.begin_db_transaction().await.unwrap(); - tx.remove_mint(mint_url.clone()).await.unwrap(); - tx.commit().await.unwrap(); + db.remove_mint(mint_url.clone()).await.unwrap(); let result = db.get_mint(mint_url).await.unwrap(); assert!(result.is_none()); @@ -225,16 +217,12 @@ where let new_url = test_mint_url_2(); // Add mint with old URL - let mut tx = db.begin_db_transaction().await.unwrap(); - tx.add_mint(old_url.clone(), None).await.unwrap(); - tx.commit().await.unwrap(); + db.add_mint(old_url.clone(), None).await.unwrap(); // Update URL - let mut tx = db.begin_db_transaction().await.unwrap(); - tx.update_mint_url(old_url.clone(), new_url.clone()) + db.update_mint_url(old_url.clone(), new_url.clone()) .await .unwrap(); - tx.commit().await.unwrap(); } // ============================================================================= @@ -251,12 +239,10 @@ where let keyset_info = test_keyset_info(keyset_id, &mint_url); // Add mint first - let mut tx = db.begin_db_transaction().await.unwrap(); - tx.add_mint(mint_url.clone(), None).await.unwrap(); - tx.add_mint_keysets(mint_url.clone(), vec![keyset_info.clone()]) + db.add_mint(mint_url.clone(), None).await.unwrap(); + db.add_mint_keysets(mint_url.clone(), vec![keyset_info.clone()]) .await .unwrap(); - tx.commit().await.unwrap(); // Get keyset by ID let retrieved = db.get_keyset_by_id(&keyset_id).await.unwrap(); @@ -279,18 +265,14 @@ where let keyset_info = test_keyset_info(keyset_id, &mint_url); // Add keyset - let mut tx = db.begin_db_transaction().await.unwrap(); - tx.add_mint(mint_url.clone(), None).await.unwrap(); - tx.add_mint_keysets(mint_url.clone(), vec![keyset_info]) + db.add_mint(mint_url.clone(), None).await.unwrap(); + db.add_mint_keysets(mint_url.clone(), vec![keyset_info]) .await .unwrap(); - tx.commit().await.unwrap(); // Get in transaction - let mut tx = db.begin_db_transaction().await.unwrap(); - let retrieved = tx.get_keyset_by_id(&keyset_id).await.unwrap(); + let retrieved = db.get_keyset_by_id(&keyset_id).await.unwrap(); assert!(retrieved.is_some()); - tx.rollback().await.unwrap(); } /// Test adding and retrieving keys @@ -308,9 +290,7 @@ where }; // Add keys - let mut tx = db.begin_db_transaction().await.unwrap(); - tx.add_keys(keyset).await.unwrap(); - tx.commit().await.unwrap(); + db.add_keys(keyset).await.unwrap(); // Get keys let retrieved = db.get_keys(&keyset_id).await.unwrap(); @@ -334,17 +314,13 @@ where }; // Add keys - let mut tx = db.begin_db_transaction().await.unwrap(); - tx.add_keys(keyset).await.unwrap(); - tx.commit().await.unwrap(); + db.add_keys(keyset).await.unwrap(); // Get in transaction - let mut tx = db.begin_db_transaction().await.unwrap(); - let retrieved = tx.get_keys(&keyset_id).await.unwrap(); + let retrieved = db.get_keys(&keyset_id).await.unwrap(); assert!(retrieved.is_some()); let retrieved_keys = retrieved.unwrap(); assert_eq!(retrieved_keys.len(), keys.len()); - tx.rollback().await.unwrap(); } /// Test removing keys @@ -362,18 +338,14 @@ where }; // Add keys - let mut tx = db.begin_db_transaction().await.unwrap(); - tx.add_keys(keyset).await.unwrap(); - tx.commit().await.unwrap(); + db.add_keys(keyset).await.unwrap(); // Verify keys were added let retrieved = db.get_keys(&keyset_id).await.unwrap(); assert!(retrieved.is_some()); // Remove keys - let mut tx = db.begin_db_transaction().await.unwrap(); - tx.remove_keys(&keyset_id).await.unwrap(); - tx.commit().await.unwrap(); + db.remove_keys(&keyset_id).await.unwrap(); let retrieved = db.get_keys(&keyset_id).await.unwrap(); assert!(retrieved.is_none()); @@ -392,9 +364,7 @@ where let quote = test_mint_quote(mint_url); // Add quote - let mut tx = db.begin_db_transaction().await.unwrap(); - tx.add_mint_quote(quote.clone()).await.unwrap(); - tx.commit().await.unwrap(); + db.add_mint_quote(quote.clone()).await.unwrap(); // Get quote let retrieved = db.get_mint_quote("e.id).await.unwrap(); @@ -415,15 +385,11 @@ where let quote = test_mint_quote(mint_url); // Add quote - let mut tx = db.begin_db_transaction().await.unwrap(); - tx.add_mint_quote(quote.clone()).await.unwrap(); - tx.commit().await.unwrap(); + db.add_mint_quote(quote.clone()).await.unwrap(); // Get in transaction - let mut tx = db.begin_db_transaction().await.unwrap(); - let retrieved = tx.get_mint_quote("e.id).await.unwrap(); + let retrieved = db.get_mint_quote("e.id).await.unwrap(); assert!(retrieved.is_some()); - tx.rollback().await.unwrap(); } /// Test removing mint quote @@ -435,14 +401,10 @@ where let quote = test_mint_quote(mint_url); // Add quote - let mut tx = db.begin_db_transaction().await.unwrap(); - tx.add_mint_quote(quote.clone()).await.unwrap(); - tx.commit().await.unwrap(); + db.add_mint_quote(quote.clone()).await.unwrap(); // Remove quote - let mut tx = db.begin_db_transaction().await.unwrap(); - tx.remove_mint_quote("e.id).await.unwrap(); - tx.commit().await.unwrap(); + db.remove_mint_quote("e.id).await.unwrap(); let retrieved = db.get_mint_quote("e.id).await.unwrap(); assert!(retrieved.is_none()); @@ -460,9 +422,7 @@ where let quote = test_melt_quote(); // Add quote - let mut tx = db.begin_db_transaction().await.unwrap(); - tx.add_melt_quote(quote.clone()).await.unwrap(); - tx.commit().await.unwrap(); + db.add_melt_quote(quote.clone()).await.unwrap(); // Get quote let retrieved = db.get_melt_quote("e.id).await.unwrap(); @@ -482,15 +442,11 @@ where let quote = test_melt_quote(); // Add quote - let mut tx = db.begin_db_transaction().await.unwrap(); - tx.add_melt_quote(quote.clone()).await.unwrap(); - tx.commit().await.unwrap(); + db.add_melt_quote(quote.clone()).await.unwrap(); // Get in transaction - let mut tx = db.begin_db_transaction().await.unwrap(); - let retrieved = tx.get_melt_quote("e.id).await.unwrap(); + let retrieved = db.get_melt_quote("e.id).await.unwrap(); assert!(retrieved.is_some()); - tx.rollback().await.unwrap(); } /// Test removing melt quote @@ -501,14 +457,10 @@ where let quote = test_melt_quote(); // Add quote - let mut tx = db.begin_db_transaction().await.unwrap(); - tx.add_melt_quote(quote.clone()).await.unwrap(); - tx.commit().await.unwrap(); + db.add_melt_quote(quote.clone()).await.unwrap(); // Remove quote - let mut tx = db.begin_db_transaction().await.unwrap(); - tx.remove_melt_quote("e.id).await.unwrap(); - tx.commit().await.unwrap(); + db.remove_melt_quote("e.id).await.unwrap(); let retrieved = db.get_melt_quote("e.id).await.unwrap(); assert!(retrieved.is_none()); @@ -528,11 +480,9 @@ where let proof_info = test_proof_info(keyset_id, 100, mint_url.clone()); // Add proof - let mut tx = db.begin_db_transaction().await.unwrap(); - tx.update_proofs(vec![proof_info.clone()], vec![]) + db.update_proofs(vec![proof_info.clone()], vec![]) .await .unwrap(); - tx.commit().await.unwrap(); // Get proofs let proofs = db.get_proofs(None, None, None, None).await.unwrap(); @@ -561,17 +511,13 @@ where let proof_info = test_proof_info(keyset_id, 100, mint_url.clone()); // Add proof - let mut tx = db.begin_db_transaction().await.unwrap(); - tx.update_proofs(vec![proof_info.clone()], vec![]) + db.update_proofs(vec![proof_info.clone()], vec![]) .await .unwrap(); - tx.commit().await.unwrap(); // Get proofs in transaction - let mut tx = db.begin_db_transaction().await.unwrap(); - let proofs = tx.get_proofs(None, None, None, None).await.unwrap(); + let proofs = db.get_proofs(None, None, None, None).await.unwrap(); assert!(!proofs.is_empty()); - tx.rollback().await.unwrap(); } /// Test updating proofs (add and remove) @@ -585,18 +531,14 @@ where let proof_info_2 = test_proof_info(keyset_id, 200, mint_url.clone()); // Add first proof - let mut tx = db.begin_db_transaction().await.unwrap(); - tx.update_proofs(vec![proof_info_1.clone()], vec![]) + db.update_proofs(vec![proof_info_1.clone()], vec![]) .await .unwrap(); - tx.commit().await.unwrap(); // Add second, remove first - let mut tx = db.begin_db_transaction().await.unwrap(); - tx.update_proofs(vec![proof_info_2.clone()], vec![proof_info_1.y]) + db.update_proofs(vec![proof_info_2.clone()], vec![proof_info_1.y]) .await .unwrap(); - tx.commit().await.unwrap(); // Verify let proofs = db.get_proofs(None, None, None, None).await.unwrap(); @@ -614,18 +556,14 @@ where let proof_info = test_proof_info(keyset_id, 100, mint_url.clone()); // Add proof - let mut tx = db.begin_db_transaction().await.unwrap(); - tx.update_proofs(vec![proof_info.clone()], vec![]) + db.update_proofs(vec![proof_info.clone()], vec![]) .await .unwrap(); - tx.commit().await.unwrap(); // Update state - let mut tx = db.begin_db_transaction().await.unwrap(); - tx.update_proofs_state(vec![proof_info.y], State::Pending) + db.update_proofs_state(vec![proof_info.y], State::Pending) .await .unwrap(); - tx.commit().await.unwrap(); // Verify let proofs = db @@ -645,11 +583,9 @@ where let proof_info = test_proof_info(keyset_id, 100, mint_url.clone()); // Add proof - let mut tx = db.begin_db_transaction().await.unwrap(); - tx.update_proofs(vec![proof_info.clone()], vec![]) + db.update_proofs(vec![proof_info.clone()], vec![]) .await .unwrap(); - tx.commit().await.unwrap(); // Filter by unit let proofs = db @@ -676,11 +612,9 @@ where let proof_info = test_proof_info(keyset_id, 100, mint_url.clone()); // Add proof - let mut tx = db.begin_db_transaction().await.unwrap(); - tx.update_proofs(vec![proof_info.clone()], vec![]) + db.update_proofs(vec![proof_info.clone()], vec![]) .await .unwrap(); - tx.commit().await.unwrap(); // Filter by state let proofs = db @@ -712,11 +646,9 @@ where let proof_info_2 = test_proof_info(keyset_id, 200, mint_url.clone()); // Add proofs - let mut tx = db.begin_db_transaction().await.unwrap(); - tx.update_proofs(vec![proof_info_1, proof_info_2], vec![]) + db.update_proofs(vec![proof_info_1, proof_info_2], vec![]) .await .unwrap(); - tx.commit().await.unwrap(); // Get total balance let balance = db.get_balance(None, None, None).await.unwrap(); @@ -737,11 +669,9 @@ where let proof_info = test_proof_info(keyset_id, 100, mint_url.clone()); // Add proof - let mut tx = db.begin_db_transaction().await.unwrap(); - tx.update_proofs(vec![proof_info.clone()], vec![]) + db.update_proofs(vec![proof_info.clone()], vec![]) .await .unwrap(); - tx.commit().await.unwrap(); // Get balance by state let balance = db @@ -770,16 +700,12 @@ where let keyset_id = test_keyset_id(); // Increment counter - let mut tx = db.begin_db_transaction().await.unwrap(); - let counter1 = tx.increment_keyset_counter(&keyset_id, 5).await.unwrap(); - tx.commit().await.unwrap(); + let counter1 = db.increment_keyset_counter(&keyset_id, 5).await.unwrap(); assert_eq!(counter1, 5); // Increment again - let mut tx = db.begin_db_transaction().await.unwrap(); - let counter2 = tx.increment_keyset_counter(&keyset_id, 10).await.unwrap(); - tx.commit().await.unwrap(); + let counter2 = db.increment_keyset_counter(&keyset_id, 10).await.unwrap(); assert_eq!(counter2, 15); } @@ -793,22 +719,16 @@ where let keyset_id_2 = test_keyset_id_2(); // Increment first keyset - let mut tx = db.begin_db_transaction().await.unwrap(); - tx.increment_keyset_counter(&keyset_id_1, 5).await.unwrap(); - tx.commit().await.unwrap(); + db.increment_keyset_counter(&keyset_id_1, 5).await.unwrap(); // Increment second keyset - let mut tx = db.begin_db_transaction().await.unwrap(); - let counter2 = tx.increment_keyset_counter(&keyset_id_2, 10).await.unwrap(); - tx.commit().await.unwrap(); + let counter2 = db.increment_keyset_counter(&keyset_id_2, 10).await.unwrap(); // Second keyset should start from 0 assert_eq!(counter2, 10); // First keyset should still be at 5 - let mut tx = db.begin_db_transaction().await.unwrap(); - let counter1 = tx.increment_keyset_counter(&keyset_id_1, 0).await.unwrap(); - tx.rollback().await.unwrap(); + let counter1 = db.increment_keyset_counter(&keyset_id_1, 0).await.unwrap(); assert_eq!(counter1, 5); } @@ -827,9 +747,7 @@ where let tx_id = transaction.id(); // Add transaction - let mut tx = db.begin_db_transaction().await.unwrap(); - tx.add_transaction(transaction.clone()).await.unwrap(); - tx.commit().await.unwrap(); + db.add_transaction(transaction.clone()).await.unwrap(); // Get transaction let retrieved = db.get_transaction(tx_id).await.unwrap(); @@ -847,10 +765,8 @@ where let tx_outgoing = test_transaction(mint_url.clone(), TransactionDirection::Outgoing); // Add transactions - let mut tx = db.begin_db_transaction().await.unwrap(); - tx.add_transaction(tx_incoming).await.unwrap(); - tx.add_transaction(tx_outgoing).await.unwrap(); - tx.commit().await.unwrap(); + db.add_transaction(tx_incoming).await.unwrap(); + db.add_transaction(tx_outgoing).await.unwrap(); // List all let transactions = db.list_transactions(None, None, None).await.unwrap(); @@ -881,10 +797,8 @@ where let tx_2 = test_transaction(mint_url_2.clone(), TransactionDirection::Incoming); // Add transactions - let mut tx = db.begin_db_transaction().await.unwrap(); - tx.add_transaction(tx_1).await.unwrap(); - tx.add_transaction(tx_2).await.unwrap(); - tx.commit().await.unwrap(); + db.add_transaction(tx_1).await.unwrap(); + db.add_transaction(tx_2).await.unwrap(); // Filter by mint let transactions = db @@ -904,57 +818,200 @@ where let tx_id = transaction.id(); // Add transaction - let mut tx = db.begin_db_transaction().await.unwrap(); - tx.add_transaction(transaction).await.unwrap(); - tx.commit().await.unwrap(); + db.add_transaction(transaction).await.unwrap(); // Remove transaction - let mut tx = db.begin_db_transaction().await.unwrap(); - tx.remove_transaction(tx_id).await.unwrap(); - tx.commit().await.unwrap(); + db.remove_transaction(tx_id).await.unwrap(); let retrieved = db.get_transaction(tx_id).await.unwrap(); assert!(retrieved.is_none()); } -// ============================================================================= -// Transaction Rollback Tests +// KV Store Tests // ============================================================================= -/// Test transaction rollback -pub async fn transaction_rollback(db: DB) +/// Test KV store write and read operations +pub async fn kvstore_write_and_read(db: DB) where DB: Database, { - let mint_url = test_mint_url(); + // Write some test data + db.kv_write("test_namespace", "sub_namespace", "key1", b"value1") + .await + .unwrap(); + db.kv_write("test_namespace", "sub_namespace", "key2", b"value2") + .await + .unwrap(); + db.kv_write("test_namespace", "other_sub", "key3", b"value3") + .await + .unwrap(); - // Add mint but rollback - let mut tx = db.begin_db_transaction().await.unwrap(); - tx.add_mint(mint_url.clone(), None).await.unwrap(); - tx.rollback().await.unwrap(); + // Read back the data + let value1 = db + .kv_read("test_namespace", "sub_namespace", "key1") + .await + .unwrap(); + assert_eq!(value1, Some(b"value1".to_vec())); - // Verify mint was not added - let result = db.get_mint(mint_url).await.unwrap(); - assert!(result.is_none()); + let value2 = db + .kv_read("test_namespace", "sub_namespace", "key2") + .await + .unwrap(); + assert_eq!(value2, Some(b"value2".to_vec())); + + let value3 = db + .kv_read("test_namespace", "other_sub", "key3") + .await + .unwrap(); + assert_eq!(value3, Some(b"value3".to_vec())); + + // Read non-existent key + let missing = db + .kv_read("test_namespace", "sub_namespace", "missing") + .await + .unwrap(); + assert_eq!(missing, None); } -/// Test proof rollback -pub async fn proof_rollback(db: DB) +/// Test KV store list operation +pub async fn kvstore_list(db: DB) where DB: Database, { - let mint_url = test_mint_url(); - let keyset_id = test_keyset_id(); - let proof_info = test_proof_info(keyset_id, 100, mint_url); + // Write some test data + db.kv_write("test_namespace", "sub_namespace", "key1", b"value1") + .await + .unwrap(); + db.kv_write("test_namespace", "sub_namespace", "key2", b"value2") + .await + .unwrap(); + db.kv_write("test_namespace", "other_sub", "key3", b"value3") + .await + .unwrap(); - // Add proof but rollback - let mut tx = db.begin_db_transaction().await.unwrap(); - tx.update_proofs(vec![proof_info], vec![]).await.unwrap(); - tx.rollback().await.unwrap(); + // List keys in namespace + let mut keys = db.kv_list("test_namespace", "sub_namespace").await.unwrap(); + keys.sort(); + assert_eq!(keys, vec!["key1", "key2"]); - // Verify proof was not added - let proofs = db.get_proofs(None, None, None, None).await.unwrap(); - assert!(proofs.is_empty()); + // List keys in other namespace + let other_keys = db.kv_list("test_namespace", "other_sub").await.unwrap(); + assert_eq!(other_keys, vec!["key3"]); + + // List keys in empty namespace + let empty_keys = db.kv_list("test_namespace", "empty_sub").await.unwrap(); + assert!(empty_keys.is_empty()); +} + +/// Test KV store update operation +pub async fn kvstore_update(db: DB) +where + DB: Database, +{ + // Write initial value + db.kv_write("test_namespace", "sub_namespace", "key1", b"value1") + .await + .unwrap(); + + // Verify initial value + let value = db + .kv_read("test_namespace", "sub_namespace", "key1") + .await + .unwrap(); + assert_eq!(value, Some(b"value1".to_vec())); + + // Update value + db.kv_write("test_namespace", "sub_namespace", "key1", b"updated_value1") + .await + .unwrap(); + + // Verify updated value + let value = db + .kv_read("test_namespace", "sub_namespace", "key1") + .await + .unwrap(); + assert_eq!(value, Some(b"updated_value1".to_vec())); +} + +/// Test KV store remove operation +pub async fn kvstore_remove(db: DB) +where + DB: Database, +{ + // Write some test data + db.kv_write("test_namespace", "sub_namespace", "key1", b"value1") + .await + .unwrap(); + db.kv_write("test_namespace", "sub_namespace", "key2", b"value2") + .await + .unwrap(); + + // Verify data exists + let keys = db.kv_list("test_namespace", "sub_namespace").await.unwrap(); + assert_eq!(keys.len(), 2); + + // Remove one key + db.kv_remove("test_namespace", "sub_namespace", "key1") + .await + .unwrap(); + + // Verify key is removed + let value = db + .kv_read("test_namespace", "sub_namespace", "key1") + .await + .unwrap(); + assert_eq!(value, None); + + // Verify other key still exists + let value = db + .kv_read("test_namespace", "sub_namespace", "key2") + .await + .unwrap(); + assert_eq!(value, Some(b"value2".to_vec())); + + // Verify list is updated + let keys = db.kv_list("test_namespace", "sub_namespace").await.unwrap(); + assert_eq!(keys, vec!["key2"]); +} + +/// Test KV store namespace isolation +pub async fn kvstore_namespace_isolation(db: DB) +where + DB: Database, +{ + // Write same key to different namespaces + db.kv_write("ns1", "sub", "key", b"value_ns1") + .await + .unwrap(); + db.kv_write("ns2", "sub", "key", b"value_ns2") + .await + .unwrap(); + db.kv_write("ns1", "sub2", "key", b"value_sub2") + .await + .unwrap(); + + // Verify isolation by primary namespace + let value1 = db.kv_read("ns1", "sub", "key").await.unwrap(); + assert_eq!(value1, Some(b"value_ns1".to_vec())); + + let value2 = db.kv_read("ns2", "sub", "key").await.unwrap(); + assert_eq!(value2, Some(b"value_ns2".to_vec())); + + // Verify isolation by secondary namespace + let value3 = db.kv_read("ns1", "sub2", "key").await.unwrap(); + assert_eq!(value3, Some(b"value_sub2".to_vec())); + + // Remove from one namespace shouldn't affect others + db.kv_remove("ns1", "sub", "key").await.unwrap(); + + let value1 = db.kv_read("ns1", "sub", "key").await.unwrap(); + assert_eq!(value1, None); + + let value2 = db.kv_read("ns2", "sub", "key").await.unwrap(); + assert_eq!(value2, Some(b"value_ns2".to_vec())); + + let value3 = db.kv_read("ns1", "sub2", "key").await.unwrap(); + assert_eq!(value3, Some(b"value_sub2".to_vec())); } /// Unit test that is expected to be passed for a correct wallet database implementation @@ -992,8 +1049,11 @@ macro_rules! wallet_db_test { list_transactions, filter_transactions_by_mint, remove_transaction, - transaction_rollback, - proof_rollback + kvstore_write_and_read, + kvstore_list, + kvstore_update, + kvstore_remove, + kvstore_namespace_isolation ); }; ($make_db_fn:ident, $($name:ident),+ $(,)?) => { diff --git a/crates/cdk-ffi/src/database.rs b/crates/cdk-ffi/src/database.rs index bed761208..081987692 100644 --- a/crates/cdk-ffi/src/database.rs +++ b/crates/cdk-ffi/src/database.rs @@ -1,17 +1,13 @@ //! FFI Database bindings use std::collections::HashMap; -use std::sync::atomic::AtomicBool; use std::sync::Arc; use cdk_common::database::{ - DbTransactionFinalizer, DynWalletDatabaseTransaction, KVStoreDatabase as CdkKVStoreDatabase, - WalletDatabase as CdkWalletDatabase, WalletDatabaseTransaction as CdkWalletDatabaseTransaction, + KVStoreDatabase as CdkKVStoreDatabase, WalletDatabase as CdkWalletDatabase, }; -use cdk_common::task::spawn; use cdk_sql_common::pool::DatabasePool; use cdk_sql_common::SQLWalletDatabase; -use tokio::sync::Mutex; use crate::error::FfiError; #[cfg(feature = "postgres")] @@ -19,14 +15,12 @@ use crate::postgres::WalletPostgresDatabase; use crate::sqlite::WalletSqliteDatabase; use crate::types::*; -/// FFI-compatible wallet database trait (read-only operations + begin_db_transaction) +/// FFI-compatible wallet database trait with all read and write operations /// This trait mirrors the CDK WalletDatabase trait structure -#[uniffi::export] +#[uniffi::export(with_foreign)] #[async_trait::async_trait] pub trait WalletDatabase: Send + Sync { - /// Begin a database transaction - async fn begin_db_transaction(&self) - -> Result, FfiError>; + // ========== Read methods ========== /// Get mint from storage async fn get_mint(&self, mint_url: MintUrl) -> Result, FfiError>; @@ -110,87 +104,25 @@ pub trait WalletDatabase: Send + Sync { primary_namespace: String, secondary_namespace: String, ) -> Result, FfiError>; -} - -/// FFI-compatible transaction trait for wallet database write operations -/// This trait mirrors the CDK WalletDatabaseTransaction trait but uses FFI-compatible types -#[uniffi::export(with_foreign)] -#[async_trait::async_trait] -pub trait WalletDatabaseTransaction: Send + Sync { - /// Commit the transaction - async fn commit(self: Arc) -> Result<(), FfiError>; - - /// Rollback the transaction - async fn rollback(self: Arc) -> Result<(), FfiError>; - - // Mint Management - /// Add Mint to storage - async fn add_mint( - &self, - mint_url: MintUrl, - mint_info: Option, - ) -> Result<(), FfiError>; - - /// Remove Mint from storage - async fn remove_mint(&self, mint_url: MintUrl) -> Result<(), FfiError>; - /// Update mint url - async fn update_mint_url( + /// Write a value to the KV store + async fn kv_write( &self, - old_mint_url: MintUrl, - new_mint_url: MintUrl, + primary_namespace: String, + secondary_namespace: String, + key: String, + value: Vec, ) -> Result<(), FfiError>; - // Keyset Management - /// Add mint keyset to storage - async fn add_mint_keysets( + /// Remove a value from the KV store + async fn kv_remove( &self, - mint_url: MintUrl, - keysets: Vec, + primary_namespace: String, + secondary_namespace: String, + key: String, ) -> Result<(), FfiError>; - /// Get mint keyset by id (transaction-scoped) - async fn get_keyset_by_id(&self, keyset_id: Id) -> Result, FfiError>; - - /// Get Keys from storage (transaction-scoped) - async fn get_keys(&self, id: Id) -> Result, FfiError>; - - // Mint Quote Management - /// Get mint quote from storage (transaction-scoped, with locking) - async fn get_mint_quote(&self, quote_id: String) -> Result, FfiError>; - - /// Add mint quote to storage - async fn add_mint_quote(&self, quote: MintQuote) -> Result<(), FfiError>; - - /// Remove mint quote from storage - async fn remove_mint_quote(&self, quote_id: String) -> Result<(), FfiError>; - - // Melt Quote Management - /// Get melt quote from storage (transaction-scoped) - async fn get_melt_quote(&self, quote_id: String) -> Result, FfiError>; - - /// Add melt quote to storage - async fn add_melt_quote(&self, quote: MeltQuote) -> Result<(), FfiError>; - - /// Remove melt quote from storage - async fn remove_melt_quote(&self, quote_id: String) -> Result<(), FfiError>; - - // Keys Management - /// Add Keys to storage - async fn add_keys(&self, keyset: KeySet) -> Result<(), FfiError>; - - /// Remove Keys from storage - async fn remove_keys(&self, id: Id) -> Result<(), FfiError>; - - // Proof Management - /// Get proofs from storage (transaction-scoped, with locking) - async fn get_proofs( - &self, - mint_url: Option, - unit: Option, - state: Option>, - spending_conditions: Option>, - ) -> Result, FfiError>; + // ========== Write methods ========== /// Update the proofs in storage by adding new proofs or removing proofs by their Y value async fn update_proofs( @@ -206,248 +138,56 @@ pub trait WalletDatabaseTransaction: Send + Sync { state: ProofState, ) -> Result<(), FfiError>; - // Keyset Counter Management - /// Increment Keyset counter - async fn increment_keyset_counter(&self, keyset_id: Id, count: u32) -> Result; - - // Transaction Management /// Add transaction to storage async fn add_transaction(&self, transaction: Transaction) -> Result<(), FfiError>; /// Remove transaction from storage async fn remove_transaction(&self, transaction_id: TransactionId) -> Result<(), FfiError>; - // KV Store Methods - /// Read a value from the KV store - async fn kv_read( - &self, - primary_namespace: String, - secondary_namespace: String, - key: String, - ) -> Result>, FfiError>; - - /// Write a value to the KV store - async fn kv_write( - &self, - primary_namespace: String, - secondary_namespace: String, - key: String, - value: Vec, - ) -> Result<(), FfiError>; - - /// Remove a value from the KV store - async fn kv_remove( + /// Update mint url + async fn update_mint_url( &self, - primary_namespace: String, - secondary_namespace: String, - key: String, + old_mint_url: MintUrl, + new_mint_url: MintUrl, ) -> Result<(), FfiError>; - /// List keys in the KV store - async fn kv_list( - &self, - primary_namespace: String, - secondary_namespace: String, - ) -> Result, FfiError>; -} - -/// Wallet database transaction wrapper -#[derive(uniffi::Object)] -pub struct WalletDatabaseTransactionWrapper { - inner: Arc, -} - -#[uniffi::export(async_runtime = "tokio")] -impl WalletDatabaseTransactionWrapper { - /// Commit the transaction - pub async fn commit(&self) -> Result<(), FfiError> { - self.inner.clone().commit().await - } - - /// Rollback the transaction - pub async fn rollback(&self) -> Result<(), FfiError> { - self.inner.clone().rollback().await - } + /// Atomically increment Keyset counter and return new value + async fn increment_keyset_counter(&self, keyset_id: Id, count: u32) -> Result; /// Add Mint to storage - pub async fn add_mint( + async fn add_mint( &self, mint_url: MintUrl, mint_info: Option, - ) -> Result<(), FfiError> { - self.inner.add_mint(mint_url, mint_info).await - } + ) -> Result<(), FfiError>; /// Remove Mint from storage - pub async fn remove_mint(&self, mint_url: MintUrl) -> Result<(), FfiError> { - self.inner.remove_mint(mint_url).await - } - - /// Update mint url - pub async fn update_mint_url( - &self, - old_mint_url: MintUrl, - new_mint_url: MintUrl, - ) -> Result<(), FfiError> { - self.inner.update_mint_url(old_mint_url, new_mint_url).await - } + async fn remove_mint(&self, mint_url: MintUrl) -> Result<(), FfiError>; /// Add mint keyset to storage - pub async fn add_mint_keysets( + async fn add_mint_keysets( &self, mint_url: MintUrl, keysets: Vec, - ) -> Result<(), FfiError> { - self.inner.add_mint_keysets(mint_url, keysets).await - } - - /// Get mint keyset by id (transaction-scoped) - pub async fn get_keyset_by_id(&self, keyset_id: Id) -> Result, FfiError> { - self.inner.get_keyset_by_id(keyset_id).await - } - - /// Get Keys from storage (transaction-scoped) - pub async fn get_keys(&self, id: Id) -> Result, FfiError> { - self.inner.get_keys(id).await - } - - /// Get mint quote from storage (transaction-scoped, with locking) - pub async fn get_mint_quote(&self, quote_id: String) -> Result, FfiError> { - self.inner.get_mint_quote(quote_id).await - } + ) -> Result<(), FfiError>; /// Add mint quote to storage - pub async fn add_mint_quote(&self, quote: MintQuote) -> Result<(), FfiError> { - self.inner.add_mint_quote(quote).await - } + async fn add_mint_quote(&self, quote: MintQuote) -> Result<(), FfiError>; /// Remove mint quote from storage - pub async fn remove_mint_quote(&self, quote_id: String) -> Result<(), FfiError> { - self.inner.remove_mint_quote(quote_id).await - } - - /// Get melt quote from storage (transaction-scoped) - pub async fn get_melt_quote(&self, quote_id: String) -> Result, FfiError> { - self.inner.get_melt_quote(quote_id).await - } + async fn remove_mint_quote(&self, quote_id: String) -> Result<(), FfiError>; /// Add melt quote to storage - pub async fn add_melt_quote(&self, quote: MeltQuote) -> Result<(), FfiError> { - self.inner.add_melt_quote(quote).await - } + async fn add_melt_quote(&self, quote: MeltQuote) -> Result<(), FfiError>; /// Remove melt quote from storage - pub async fn remove_melt_quote(&self, quote_id: String) -> Result<(), FfiError> { - self.inner.remove_melt_quote(quote_id).await - } + async fn remove_melt_quote(&self, quote_id: String) -> Result<(), FfiError>; /// Add Keys to storage - pub async fn add_keys(&self, keyset: KeySet) -> Result<(), FfiError> { - self.inner.add_keys(keyset).await - } + async fn add_keys(&self, keyset: KeySet) -> Result<(), FfiError>; /// Remove Keys from storage - pub async fn remove_keys(&self, id: Id) -> Result<(), FfiError> { - self.inner.remove_keys(id).await - } - - /// Get proofs from storage (transaction-scoped, with locking) - pub async fn get_proofs( - &self, - mint_url: Option, - unit: Option, - state: Option>, - spending_conditions: Option>, - ) -> Result, FfiError> { - self.inner - .get_proofs(mint_url, unit, state, spending_conditions) - .await - } - - /// Update the proofs in storage by adding new proofs or removing proofs by their Y value - pub async fn update_proofs( - &self, - added: Vec, - removed_ys: Vec, - ) -> Result<(), FfiError> { - self.inner.update_proofs(added, removed_ys).await - } - - /// Update proofs state in storage - pub async fn update_proofs_state( - &self, - ys: Vec, - state: ProofState, - ) -> Result<(), FfiError> { - self.inner.update_proofs_state(ys, state).await - } - - /// Increment Keyset counter - pub async fn increment_keyset_counter( - &self, - keyset_id: Id, - count: u32, - ) -> Result { - self.inner.increment_keyset_counter(keyset_id, count).await - } - - /// Add transaction to storage - pub async fn add_transaction(&self, transaction: Transaction) -> Result<(), FfiError> { - self.inner.add_transaction(transaction).await - } - - /// Remove transaction from storage - pub async fn remove_transaction(&self, transaction_id: TransactionId) -> Result<(), FfiError> { - self.inner.remove_transaction(transaction_id).await - } - - /// Read a value from the KV store - pub async fn kv_read( - &self, - primary_namespace: String, - secondary_namespace: String, - key: String, - ) -> Result>, FfiError> { - self.inner - .kv_read(primary_namespace, secondary_namespace, key) - .await - } - - /// Write a value to the KV store - pub async fn kv_write( - &self, - primary_namespace: String, - secondary_namespace: String, - key: String, - value: Vec, - ) -> Result<(), FfiError> { - self.inner - .kv_write(primary_namespace, secondary_namespace, key, value) - .await - } - - /// Remove a value from the KV store - pub async fn kv_remove( - &self, - primary_namespace: String, - secondary_namespace: String, - key: String, - ) -> Result<(), FfiError> { - self.inner - .kv_remove(primary_namespace, secondary_namespace, key) - .await - } - - /// List keys in the KV store - pub async fn kv_list( - &self, - primary_namespace: String, - secondary_namespace: String, - ) -> Result, FfiError> { - self.inner - .kv_list(primary_namespace, secondary_namespace) - .await - } + async fn remove_keys(&self, id: Id) -> Result<(), FfiError>; } /// Internal bridge trait to convert from the FFI trait to the CDK database trait @@ -824,342 +564,180 @@ impl CdkWalletDatabase for WalletDatabaseBridge { .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into())) } - async fn begin_db_transaction( - &self, - ) -> Result< - Box + Send + Sync>, - cdk::cdk_database::Error, - > { - let ffi_tx = self - .ffi_db - .begin_db_transaction() - .await - .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))?; - - Ok(Box::new(WalletDatabaseTransactionBridge { - ffi_tx, - is_finalized: false, - })) - } -} - -/// Transaction bridge for FFI wallet database -struct WalletDatabaseTransactionBridge { - ffi_tx: Arc, - is_finalized: bool, -} + // Write methods (non-transactional) -#[async_trait::async_trait] -impl CdkWalletDatabaseTransaction for WalletDatabaseTransactionBridge { - async fn add_mint( - &mut self, - mint_url: cdk::mint_url::MintUrl, - mint_info: Option, + async fn update_proofs( + &self, + added: Vec, + removed_ys: Vec, ) -> Result<(), cdk::cdk_database::Error> { - let ffi_mint_url = mint_url.into(); - let ffi_mint_info = mint_info.map(Into::into); - self.ffi_tx - .add_mint(ffi_mint_url, ffi_mint_info) + let ffi_added: Vec = added.into_iter().map(Into::into).collect(); + let ffi_removed_ys: Vec = removed_ys.into_iter().map(Into::into).collect(); + self.ffi_db + .update_proofs(ffi_added, ffi_removed_ys) .await .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into())) } - async fn remove_mint( - &mut self, - mint_url: cdk::mint_url::MintUrl, + async fn update_proofs_state( + &self, + ys: Vec, + state: cdk::nuts::State, ) -> Result<(), cdk::cdk_database::Error> { - let ffi_mint_url = mint_url.into(); - self.ffi_tx - .remove_mint(ffi_mint_url) + let ffi_ys: Vec = ys.into_iter().map(Into::into).collect(); + let ffi_state = state.into(); + self.ffi_db + .update_proofs_state(ffi_ys, ffi_state) + .await + .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into())) + } + + async fn add_transaction( + &self, + transaction: cdk::wallet::types::Transaction, + ) -> Result<(), cdk::cdk_database::Error> { + let ffi_transaction = transaction.into(); + self.ffi_db + .add_transaction(ffi_transaction) .await .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into())) } async fn update_mint_url( - &mut self, + &self, old_mint_url: cdk::mint_url::MintUrl, new_mint_url: cdk::mint_url::MintUrl, ) -> Result<(), cdk::cdk_database::Error> { - let ffi_old_mint_url = old_mint_url.into(); - let ffi_new_mint_url = new_mint_url.into(); - self.ffi_tx - .update_mint_url(ffi_old_mint_url, ffi_new_mint_url) + let ffi_old = old_mint_url.into(); + let ffi_new = new_mint_url.into(); + self.ffi_db + .update_mint_url(ffi_old, ffi_new) + .await + .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into())) + } + + async fn increment_keyset_counter( + &self, + keyset_id: &cdk::nuts::Id, + count: u32, + ) -> Result { + let ffi_id = (*keyset_id).into(); + self.ffi_db + .increment_keyset_counter(ffi_id, count) + .await + .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into())) + } + + async fn add_mint( + &self, + mint_url: cdk::mint_url::MintUrl, + mint_info: Option, + ) -> Result<(), cdk::cdk_database::Error> { + let ffi_mint_url = mint_url.into(); + let ffi_mint_info = mint_info.map(Into::into); + self.ffi_db + .add_mint(ffi_mint_url, ffi_mint_info) + .await + .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into())) + } + + async fn remove_mint( + &self, + mint_url: cdk::mint_url::MintUrl, + ) -> Result<(), cdk::cdk_database::Error> { + let ffi_mint_url = mint_url.into(); + self.ffi_db + .remove_mint(ffi_mint_url) .await .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into())) } async fn add_mint_keysets( - &mut self, + &self, mint_url: cdk::mint_url::MintUrl, keysets: Vec, ) -> Result<(), cdk::cdk_database::Error> { let ffi_mint_url = mint_url.into(); let ffi_keysets: Vec = keysets.into_iter().map(Into::into).collect(); - self.ffi_tx + self.ffi_db .add_mint_keysets(ffi_mint_url, ffi_keysets) .await .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into())) } async fn add_mint_quote( - &mut self, + &self, quote: cdk::wallet::MintQuote, ) -> Result<(), cdk::cdk_database::Error> { let ffi_quote = quote.into(); - self.ffi_tx + self.ffi_db .add_mint_quote(ffi_quote) .await .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into())) } - async fn remove_mint_quote(&mut self, quote_id: &str) -> Result<(), cdk::cdk_database::Error> { - self.ffi_tx + async fn remove_mint_quote(&self, quote_id: &str) -> Result<(), cdk::cdk_database::Error> { + self.ffi_db .remove_mint_quote(quote_id.to_string()) .await .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into())) } async fn add_melt_quote( - &mut self, + &self, quote: cdk::wallet::MeltQuote, ) -> Result<(), cdk::cdk_database::Error> { let ffi_quote = quote.into(); - self.ffi_tx + self.ffi_db .add_melt_quote(ffi_quote) .await .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into())) } - async fn remove_melt_quote(&mut self, quote_id: &str) -> Result<(), cdk::cdk_database::Error> { - self.ffi_tx + async fn remove_melt_quote(&self, quote_id: &str) -> Result<(), cdk::cdk_database::Error> { + self.ffi_db .remove_melt_quote(quote_id.to_string()) .await .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into())) } - async fn add_keys( - &mut self, - keyset: cdk::nuts::KeySet, - ) -> Result<(), cdk::cdk_database::Error> { + async fn add_keys(&self, keyset: cdk::nuts::KeySet) -> Result<(), cdk::cdk_database::Error> { let ffi_keyset: KeySet = keyset.into(); - self.ffi_tx + self.ffi_db .add_keys(ffi_keyset) .await .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into())) } - async fn remove_keys(&mut self, id: &cdk::nuts::Id) -> Result<(), cdk::cdk_database::Error> { + async fn remove_keys(&self, id: &cdk::nuts::Id) -> Result<(), cdk::cdk_database::Error> { let ffi_id = (*id).into(); - self.ffi_tx + self.ffi_db .remove_keys(ffi_id) .await .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into())) } - async fn update_proofs( - &mut self, - added: Vec, - removed_ys: Vec, - ) -> Result<(), cdk::cdk_database::Error> { - let ffi_added: Vec = added.into_iter().map(Into::into).collect(); - let ffi_removed_ys: Vec = removed_ys.into_iter().map(Into::into).collect(); - self.ffi_tx - .update_proofs(ffi_added, ffi_removed_ys) - .await - .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into())) - } - - async fn update_proofs_state( - &mut self, - ys: Vec, - state: cdk::nuts::State, - ) -> Result<(), cdk::cdk_database::Error> { - let ffi_ys: Vec = ys.into_iter().map(Into::into).collect(); - let ffi_state = state.into(); - self.ffi_tx - .update_proofs_state(ffi_ys, ffi_state) - .await - .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into())) - } - - async fn increment_keyset_counter( - &mut self, - keyset_id: &cdk::nuts::Id, - count: u32, - ) -> Result { - let ffi_id = (*keyset_id).into(); - self.ffi_tx - .increment_keyset_counter(ffi_id, count) - .await - .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into())) - } - - async fn add_transaction( - &mut self, - transaction: cdk::wallet::types::Transaction, - ) -> Result<(), cdk::cdk_database::Error> { - let ffi_transaction = transaction.into(); - self.ffi_tx - .add_transaction(ffi_transaction) - .await - .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into())) - } - async fn remove_transaction( - &mut self, + &self, transaction_id: cdk::wallet::types::TransactionId, ) -> Result<(), cdk::cdk_database::Error> { let ffi_id = transaction_id.into(); - self.ffi_tx + self.ffi_db .remove_transaction(ffi_id) .await .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into())) } - // Read methods needed during transactions - async fn get_keyset_by_id( - &mut self, - keyset_id: &cdk::nuts::Id, - ) -> Result, cdk::cdk_database::Error> { - let ffi_id = (*keyset_id).into(); - let result = self - .ffi_tx - .get_keyset_by_id(ffi_id) - .await - .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))?; - Ok(result.map(Into::into)) - } - - async fn get_keys( - &mut self, - id: &cdk::nuts::Id, - ) -> Result, cdk::cdk_database::Error> { - let ffi_id = (*id).into(); - let result = self - .ffi_tx - .get_keys(ffi_id) - .await - .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))?; - match result { - Some(keys) => Ok(Some(keys.try_into().map_err(|e: FfiError| { - cdk::cdk_database::Error::Database(e.to_string().into()) - })?)), - None => Ok(None), - } - } - - async fn get_mint_quote( - &mut self, - quote_id: &str, - ) -> Result, cdk::cdk_database::Error> { - let result = self - .ffi_tx - .get_mint_quote(quote_id.to_string()) - .await - .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))?; - Ok(result - .map(|q| { - q.try_into() - .map_err(|e: FfiError| cdk::cdk_database::Error::Database(e.to_string().into())) - }) - .transpose()?) - } - - async fn get_melt_quote( - &mut self, - quote_id: &str, - ) -> Result, cdk::cdk_database::Error> { - let result = self - .ffi_tx - .get_melt_quote(quote_id.to_string()) - .await - .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))?; - Ok(result - .map(|q| { - q.try_into() - .map_err(|e: FfiError| cdk::cdk_database::Error::Database(e.to_string().into())) - }) - .transpose()?) - } - - async fn get_proofs( - &mut self, - mint_url: Option, - unit: Option, - state: Option>, - spending_conditions: Option>, - ) -> Result, cdk::cdk_database::Error> { - let ffi_mint_url = mint_url.map(Into::into); - let ffi_unit = unit.map(Into::into); - let ffi_state = state.map(|s| s.into_iter().map(Into::into).collect()); - let ffi_spending_conditions = - spending_conditions.map(|sc| sc.into_iter().map(Into::into).collect()); - - let result = self - .ffi_tx - .get_proofs(ffi_mint_url, ffi_unit, ffi_state, ffi_spending_conditions) - .await - .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))?; - - // Convert back to CDK ProofInfo - let cdk_result: Result, cdk::cdk_database::Error> = result - .into_iter() - .map(|info| { - Ok(cdk::types::ProofInfo { - proof: info.proof.try_into().map_err(|e: FfiError| { - cdk::cdk_database::Error::Database(e.to_string().into()) - })?, - y: info.y.try_into().map_err(|e: FfiError| { - cdk::cdk_database::Error::Database(e.to_string().into()) - })?, - mint_url: info.mint_url.try_into().map_err(|e: FfiError| { - cdk::cdk_database::Error::Database(e.to_string().into()) - })?, - state: info.state.into(), - spending_condition: info - .spending_condition - .map(|sc| sc.try_into()) - .transpose() - .map_err(|e: FfiError| { - cdk::cdk_database::Error::Database(e.to_string().into()) - })?, - unit: info.unit.into(), - }) - }) - .collect(); - - cdk_result - } -} - -#[async_trait::async_trait] -impl cdk_common::database::KVStoreTransaction - for WalletDatabaseTransactionBridge -{ - async fn kv_read( - &mut self, - primary_namespace: &str, - secondary_namespace: &str, - key: &str, - ) -> Result>, cdk::cdk_database::Error> { - self.ffi_tx - .kv_read( - primary_namespace.to_string(), - secondary_namespace.to_string(), - key.to_string(), - ) - .await - .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into())) - } + // KV Store write methods async fn kv_write( - &mut self, + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, value: &[u8], ) -> Result<(), cdk::cdk_database::Error> { - self.ffi_tx + self.ffi_db .kv_write( primary_namespace.to_string(), secondary_namespace.to_string(), @@ -1171,12 +749,12 @@ impl cdk_common::database::KVStoreTransaction } async fn kv_remove( - &mut self, + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, ) -> Result<(), cdk::cdk_database::Error> { - self.ffi_tx + self.ffi_db .kv_remove( primary_namespace.to_string(), secondary_namespace.to_string(), @@ -1185,52 +763,6 @@ impl cdk_common::database::KVStoreTransaction .await .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into())) } - - async fn kv_list( - &mut self, - primary_namespace: &str, - secondary_namespace: &str, - ) -> Result, cdk::cdk_database::Error> { - self.ffi_tx - .kv_list( - primary_namespace.to_string(), - secondary_namespace.to_string(), - ) - .await - .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into())) - } -} - -#[async_trait::async_trait] -impl DbTransactionFinalizer for WalletDatabaseTransactionBridge { - type Err = cdk::cdk_database::Error; - - async fn commit(mut self: Box) -> Result<(), cdk::cdk_database::Error> { - self.is_finalized = true; - let tx = self.ffi_tx.clone(); - tx.commit() - .await - .map_err(|e: FfiError| cdk::cdk_database::Error::Database(e.to_string().into())) - } - - async fn rollback(mut self: Box) -> Result<(), cdk::cdk_database::Error> { - self.is_finalized = true; - let tx = self.ffi_tx.clone(); - tx.rollback() - .await - .map_err(|e: FfiError| cdk::cdk_database::Error::Database(e.to_string().into())) - } -} - -impl Drop for WalletDatabaseTransactionBridge { - fn drop(&mut self) { - if !self.is_finalized { - let tx = self.ffi_tx.clone(); - spawn(async move { - let _ = tx.rollback().await; - }); - } - } } pub(crate) struct FfiWalletSQLDatabase @@ -1250,53 +782,13 @@ where } } -/// Transaction wrapper for FFI -pub(crate) struct FfiWalletTransaction { - tx: Arc>>, - is_finalized: AtomicBool, -} - -impl Drop for FfiWalletTransaction { - fn drop(&mut self) { - if !self.is_finalized.load(std::sync::atomic::Ordering::SeqCst) { - let tx = self.tx.clone(); - spawn(async move { - if let Some(s) = tx.lock().await.take() { - let _ = s.rollback().await; - } - }); - } - } -} - -impl FfiWalletTransaction { - pub fn new(tx: DynWalletDatabaseTransaction) -> Arc { - Arc::new(Self { - tx: Arc::new(Mutex::new(Some(tx))), - is_finalized: false.into(), - }) - } -} - -// Implement WalletDatabaseFfi trait - only read methods + begin_db_transaction +// Implement WalletDatabase trait - all read and write methods #[async_trait::async_trait] impl WalletDatabase for FfiWalletSQLDatabase where RM: DatabasePool + 'static, { - async fn begin_db_transaction( - &self, - ) -> Result, FfiError> { - let tx = self - .inner - .begin_db_transaction() - .await - .map_err(|e| FfiError::Database { msg: e.to_string() })?; - - Ok(Arc::new(WalletDatabaseTransactionWrapper { - inner: FfiWalletTransaction::new(tx), - })) - } + // ========== Read methods ========== async fn get_proofs_by_ys(&self, ys: Vec) -> Result, FfiError> { let cdk_ys: Vec = ys @@ -1511,257 +1003,39 @@ where .await .map_err(|e| FfiError::Database { msg: e.to_string() }) } -} - -// Implement WalletDatabaseTransactionFfi trait - all write methods -#[async_trait::async_trait] -impl WalletDatabaseTransaction for FfiWalletTransaction { - async fn commit(self: Arc) -> Result<(), FfiError> { - self.is_finalized - .store(true, std::sync::atomic::Ordering::SeqCst); - self.tx - .lock() - .await - .take() - .ok_or(FfiError::Database { - msg: "Transaction already finalized".to_owned(), - })? - .commit() - .await - .map_err(|e| FfiError::Database { msg: e.to_string() }) - } - - async fn rollback(self: Arc) -> Result<(), FfiError> { - self.is_finalized - .store(true, std::sync::atomic::Ordering::SeqCst); - self.tx - .lock() - .await - .take() - .ok_or(FfiError::Database { - msg: "Transaction already finalized".to_owned(), - })? - .rollback() - .await - .map_err(|e| FfiError::Database { msg: e.to_string() }) - } - - async fn add_mint( - &self, - mint_url: MintUrl, - mint_info: Option, - ) -> Result<(), FfiError> { - let mut tx_guard = self.tx.lock().await; - let tx = tx_guard.as_mut().ok_or(FfiError::Database { - msg: "Transaction already finalized".to_owned(), - })?; - - let cdk_mint_url = mint_url.try_into()?; - let cdk_mint_info = mint_info.map(Into::into); - tx.add_mint(cdk_mint_url, cdk_mint_info) - .await - .map_err(|e| FfiError::Database { msg: e.to_string() }) - } - - async fn remove_mint(&self, mint_url: MintUrl) -> Result<(), FfiError> { - let mut tx_guard = self.tx.lock().await; - let tx = tx_guard.as_mut().ok_or(FfiError::Database { - msg: "Transaction already finalized".to_owned(), - })?; - let cdk_mint_url = mint_url.try_into()?; - tx.remove_mint(cdk_mint_url) - .await - .map_err(|e| FfiError::Database { msg: e.to_string() }) - } - async fn update_mint_url( + async fn kv_write( &self, - old_mint_url: MintUrl, - new_mint_url: MintUrl, + primary_namespace: String, + secondary_namespace: String, + key: String, + value: Vec, ) -> Result<(), FfiError> { - let mut tx_guard = self.tx.lock().await; - let tx = tx_guard.as_mut().ok_or(FfiError::Database { - msg: "Transaction already finalized".to_owned(), - })?; - let cdk_old_mint_url = old_mint_url.try_into()?; - let cdk_new_mint_url = new_mint_url.try_into()?; - tx.update_mint_url(cdk_old_mint_url, cdk_new_mint_url) + self.inner + .kv_write(&primary_namespace, &secondary_namespace, &key, &value) .await .map_err(|e| FfiError::Database { msg: e.to_string() }) } - async fn add_mint_keysets( + async fn kv_remove( &self, - mint_url: MintUrl, - keysets: Vec, + primary_namespace: String, + secondary_namespace: String, + key: String, ) -> Result<(), FfiError> { - let mut tx_guard = self.tx.lock().await; - let tx = tx_guard.as_mut().ok_or(FfiError::Database { - msg: "Transaction already finalized".to_owned(), - })?; - let cdk_mint_url = mint_url.try_into()?; - let cdk_keysets: Vec = keysets.into_iter().map(Into::into).collect(); - tx.add_mint_keysets(cdk_mint_url, cdk_keysets) - .await - .map_err(|e| FfiError::Database { msg: e.to_string() }) - } - - async fn get_keyset_by_id(&self, keyset_id: Id) -> Result, FfiError> { - let mut tx_guard = self.tx.lock().await; - let tx = tx_guard.as_mut().ok_or(FfiError::Database { - msg: "Transaction already finalized".to_owned(), - })?; - let cdk_id = keyset_id.into(); - let result = tx - .get_keyset_by_id(&cdk_id) - .await - .map_err(|e| FfiError::Database { msg: e.to_string() })?; - Ok(result.map(Into::into)) - } - - async fn get_keys(&self, id: Id) -> Result, FfiError> { - let mut tx_guard = self.tx.lock().await; - let tx = tx_guard.as_mut().ok_or(FfiError::Database { - msg: "Transaction already finalized".to_owned(), - })?; - let cdk_id = id.into(); - let result = tx - .get_keys(&cdk_id) - .await - .map_err(|e| FfiError::Database { msg: e.to_string() })?; - Ok(result.map(Into::into)) - } - - async fn get_mint_quote(&self, quote_id: String) -> Result, FfiError> { - let mut tx_guard = self.tx.lock().await; - let tx = tx_guard.as_mut().ok_or(FfiError::Database { - msg: "Transaction already finalized".to_owned(), - })?; - let result = tx - .get_mint_quote("e_id) - .await - .map_err(|e| FfiError::Database { msg: e.to_string() })?; - Ok(result.map(|q| q.into())) - } - - async fn add_mint_quote(&self, quote: MintQuote) -> Result<(), FfiError> { - let mut tx_guard = self.tx.lock().await; - let tx = tx_guard.as_mut().ok_or(FfiError::Database { - msg: "Transaction already finalized".to_owned(), - })?; - let cdk_quote = quote.try_into()?; - tx.add_mint_quote(cdk_quote) - .await - .map_err(|e| FfiError::Database { msg: e.to_string() }) - } - - async fn remove_mint_quote(&self, quote_id: String) -> Result<(), FfiError> { - let mut tx_guard = self.tx.lock().await; - let tx = tx_guard.as_mut().ok_or(FfiError::Database { - msg: "Transaction already finalized".to_owned(), - })?; - tx.remove_mint_quote("e_id) - .await - .map_err(|e| FfiError::Database { msg: e.to_string() }) - } - - async fn get_melt_quote(&self, quote_id: String) -> Result, FfiError> { - let mut tx_guard = self.tx.lock().await; - let tx = tx_guard.as_mut().ok_or(FfiError::Database { - msg: "Transaction already finalized".to_owned(), - })?; - let result = tx - .get_melt_quote("e_id) - .await - .map_err(|e| FfiError::Database { msg: e.to_string() })?; - Ok(result.map(|q| q.into())) - } - - async fn add_melt_quote(&self, quote: MeltQuote) -> Result<(), FfiError> { - let mut tx_guard = self.tx.lock().await; - let tx = tx_guard.as_mut().ok_or(FfiError::Database { - msg: "Transaction already finalized".to_owned(), - })?; - let cdk_quote = quote.try_into()?; - tx.add_melt_quote(cdk_quote) - .await - .map_err(|e| FfiError::Database { msg: e.to_string() }) - } - - async fn remove_melt_quote(&self, quote_id: String) -> Result<(), FfiError> { - let mut tx_guard = self.tx.lock().await; - let tx = tx_guard.as_mut().ok_or(FfiError::Database { - msg: "Transaction already finalized".to_owned(), - })?; - tx.remove_melt_quote("e_id) - .await - .map_err(|e| FfiError::Database { msg: e.to_string() }) - } - - async fn add_keys(&self, keyset: KeySet) -> Result<(), FfiError> { - let mut tx_guard = self.tx.lock().await; - let tx = tx_guard.as_mut().ok_or(FfiError::Database { - msg: "Transaction already finalized".to_owned(), - })?; - let cdk_keyset: cdk::nuts::KeySet = keyset.try_into()?; - tx.add_keys(cdk_keyset) - .await - .map_err(|e| FfiError::Database { msg: e.to_string() }) - } - - async fn remove_keys(&self, id: Id) -> Result<(), FfiError> { - let mut tx_guard = self.tx.lock().await; - let tx = tx_guard.as_mut().ok_or(FfiError::Database { - msg: "Transaction already finalized".to_owned(), - })?; - let cdk_id = id.into(); - tx.remove_keys(&cdk_id) + self.inner + .kv_remove(&primary_namespace, &secondary_namespace, &key) .await .map_err(|e| FfiError::Database { msg: e.to_string() }) } - async fn get_proofs( - &self, - mint_url: Option, - unit: Option, - state: Option>, - spending_conditions: Option>, - ) -> Result, FfiError> { - let mut tx_guard = self.tx.lock().await; - let tx = tx_guard.as_mut().ok_or(FfiError::Database { - msg: "Transaction already finalized".to_owned(), - })?; - - let cdk_mint_url = mint_url.map(|u| u.try_into()).transpose()?; - let cdk_unit = unit.map(Into::into); - let cdk_state = state.map(|s| s.into_iter().map(Into::into).collect()); - let cdk_spending_conditions: Option> = - spending_conditions - .map(|sc| { - sc.into_iter() - .map(|c| c.try_into()) - .collect::, FfiError>>() - }) - .transpose()?; - - let result = tx - .get_proofs(cdk_mint_url, cdk_unit, cdk_state, cdk_spending_conditions) - .await - .map_err(|e| FfiError::Database { msg: e.to_string() })?; - - Ok(result.into_iter().map(Into::into).collect()) - } + // ========== Write methods ========== async fn update_proofs( &self, added: Vec, removed_ys: Vec, ) -> Result<(), FfiError> { - let mut tx_guard = self.tx.lock().await; - let tx = tx_guard.as_mut().ok_or(FfiError::Database { - msg: "Transaction already finalized".to_owned(), - })?; - let cdk_added: Result, FfiError> = added .into_iter() .map(|info| { @@ -1784,7 +1058,8 @@ impl WalletDatabaseTransaction for FfiWalletTransaction { removed_ys.into_iter().map(|pk| pk.try_into()).collect(); let cdk_removed_ys = cdk_removed_ys?; - tx.update_proofs(cdk_added, cdk_removed_ys) + self.inner + .update_proofs(cdk_added, cdk_removed_ys) .await .map_err(|e| FfiError::Database { msg: e.to_string() }) } @@ -1794,111 +1069,130 @@ impl WalletDatabaseTransaction for FfiWalletTransaction { ys: Vec, state: ProofState, ) -> Result<(), FfiError> { - let mut tx_guard = self.tx.lock().await; - let tx = tx_guard.as_mut().ok_or(FfiError::Database { - msg: "Transaction already finalized".to_owned(), - })?; let cdk_ys: Result, FfiError> = ys.into_iter().map(|pk| pk.try_into()).collect(); let cdk_ys = cdk_ys?; let cdk_state = state.into(); - tx.update_proofs_state(cdk_ys, cdk_state) - .await - .map_err(|e| FfiError::Database { msg: e.to_string() }) - } - - async fn increment_keyset_counter(&self, keyset_id: Id, count: u32) -> Result { - let mut tx_guard = self.tx.lock().await; - let tx = tx_guard.as_mut().ok_or(FfiError::Database { - msg: "Transaction already finalized".to_owned(), - })?; - let cdk_id = keyset_id.into(); - tx.increment_keyset_counter(&cdk_id, count) + self.inner + .update_proofs_state(cdk_ys, cdk_state) .await .map_err(|e| FfiError::Database { msg: e.to_string() }) } async fn add_transaction(&self, transaction: Transaction) -> Result<(), FfiError> { - let mut tx_guard = self.tx.lock().await; - let tx = tx_guard.as_mut().ok_or(FfiError::Database { - msg: "Transaction already finalized".to_owned(), - })?; - let cdk_transaction: cdk::wallet::types::Transaction = transaction.try_into()?; - - tx.add_transaction(cdk_transaction) + self.inner + .add_transaction(cdk_transaction) .await .map_err(|e| FfiError::Database { msg: e.to_string() }) } async fn remove_transaction(&self, transaction_id: TransactionId) -> Result<(), FfiError> { - let mut tx_guard = self.tx.lock().await; - let tx = tx_guard.as_mut().ok_or(FfiError::Database { - msg: "Transaction already finalized".to_owned(), - })?; let cdk_id = transaction_id.try_into()?; - tx.remove_transaction(cdk_id) + self.inner + .remove_transaction(cdk_id) .await .map_err(|e| FfiError::Database { msg: e.to_string() }) } - async fn kv_read( + async fn update_mint_url( &self, - primary_namespace: String, - secondary_namespace: String, - key: String, - ) -> Result>, FfiError> { - let mut tx_guard = self.tx.lock().await; - let tx = tx_guard.as_mut().ok_or(FfiError::Database { - msg: "Transaction already finalized".to_owned(), - })?; - tx.kv_read(&primary_namespace, &secondary_namespace, &key) + old_mint_url: MintUrl, + new_mint_url: MintUrl, + ) -> Result<(), FfiError> { + let cdk_old = old_mint_url.try_into()?; + let cdk_new = new_mint_url.try_into()?; + self.inner + .update_mint_url(cdk_old, cdk_new) .await .map_err(|e| FfiError::Database { msg: e.to_string() }) } - async fn kv_write( + async fn increment_keyset_counter(&self, keyset_id: Id, count: u32) -> Result { + let cdk_id = keyset_id.into(); + self.inner + .increment_keyset_counter(&cdk_id, count) + .await + .map_err(|e| FfiError::Database { msg: e.to_string() }) + } + + async fn add_mint( &self, - primary_namespace: String, - secondary_namespace: String, - key: String, - value: Vec, + mint_url: MintUrl, + mint_info: Option, ) -> Result<(), FfiError> { - let mut tx_guard = self.tx.lock().await; - let tx = tx_guard.as_mut().ok_or(FfiError::Database { - msg: "Transaction already finalized".to_owned(), - })?; - tx.kv_write(&primary_namespace, &secondary_namespace, &key, &value) + let cdk_mint_url = mint_url.try_into()?; + let cdk_mint_info = mint_info.map(Into::into); + self.inner + .add_mint(cdk_mint_url, cdk_mint_info) .await .map_err(|e| FfiError::Database { msg: e.to_string() }) } - async fn kv_remove( + async fn remove_mint(&self, mint_url: MintUrl) -> Result<(), FfiError> { + let cdk_mint_url = mint_url.try_into()?; + self.inner + .remove_mint(cdk_mint_url) + .await + .map_err(|e| FfiError::Database { msg: e.to_string() }) + } + + async fn add_mint_keysets( &self, - primary_namespace: String, - secondary_namespace: String, - key: String, + mint_url: MintUrl, + keysets: Vec, ) -> Result<(), FfiError> { - let mut tx_guard = self.tx.lock().await; - let tx = tx_guard.as_mut().ok_or(FfiError::Database { - msg: "Transaction already finalized".to_owned(), - })?; - tx.kv_remove(&primary_namespace, &secondary_namespace, &key) + let cdk_mint_url = mint_url.try_into()?; + let cdk_keysets: Vec = keysets.into_iter().map(Into::into).collect(); + self.inner + .add_mint_keysets(cdk_mint_url, cdk_keysets) .await .map_err(|e| FfiError::Database { msg: e.to_string() }) } - async fn kv_list( - &self, - primary_namespace: String, - secondary_namespace: String, - ) -> Result, FfiError> { - let mut tx_guard = self.tx.lock().await; - let tx = tx_guard.as_mut().ok_or(FfiError::Database { - msg: "Transaction already finalized".to_owned(), - })?; - tx.kv_list(&primary_namespace, &secondary_namespace) + async fn add_mint_quote(&self, quote: MintQuote) -> Result<(), FfiError> { + let cdk_quote = quote.try_into()?; + self.inner + .add_mint_quote(cdk_quote) + .await + .map_err(|e| FfiError::Database { msg: e.to_string() }) + } + + async fn remove_mint_quote(&self, quote_id: String) -> Result<(), FfiError> { + self.inner + .remove_mint_quote("e_id) + .await + .map_err(|e| FfiError::Database { msg: e.to_string() }) + } + + async fn add_melt_quote(&self, quote: MeltQuote) -> Result<(), FfiError> { + let cdk_quote = quote.try_into()?; + self.inner + .add_melt_quote(cdk_quote) + .await + .map_err(|e| FfiError::Database { msg: e.to_string() }) + } + + async fn remove_melt_quote(&self, quote_id: String) -> Result<(), FfiError> { + self.inner + .remove_melt_quote("e_id) + .await + .map_err(|e| FfiError::Database { msg: e.to_string() }) + } + + async fn add_keys(&self, keyset: KeySet) -> Result<(), FfiError> { + let cdk_keyset: cdk::nuts::KeySet = keyset.try_into()?; + self.inner + .add_keys(cdk_keyset) + .await + .map_err(|e| FfiError::Database { msg: e.to_string() }) + } + + async fn remove_keys(&self, id: Id) -> Result<(), FfiError> { + let cdk_id = id.into(); + self.inner + .remove_keys(&cdk_id) .await .map_err(|e| FfiError::Database { msg: e.to_string() }) } diff --git a/crates/cdk-ffi/src/postgres.rs b/crates/cdk-ffi/src/postgres.rs index 974552dee..1a9aeae24 100644 --- a/crates/cdk-ffi/src/postgres.rs +++ b/crates/cdk-ffi/src/postgres.rs @@ -5,9 +5,9 @@ use std::sync::Arc; use cdk_postgres::PgConnectionPool; use crate::{ - CurrencyUnit, FfiError, FfiWalletSQLDatabase, Id, KeySetInfo, Keys, MeltQuote, MintInfo, - MintQuote, MintUrl, ProofInfo, ProofState, PublicKey, SpendingConditions, Transaction, - TransactionDirection, TransactionId, WalletDatabase, WalletDatabaseTransactionWrapper, + CurrencyUnit, FfiError, FfiWalletSQLDatabase, Id, KeySet, KeySetInfo, Keys, MeltQuote, + MintInfo, MintQuote, MintUrl, ProofInfo, ProofState, PublicKey, SpendingConditions, + Transaction, TransactionDirection, TransactionId, WalletDatabase, }; #[derive(uniffi::Object)] @@ -62,11 +62,7 @@ impl WalletPostgresDatabase { #[uniffi::export(async_runtime = "tokio")] #[async_trait::async_trait] impl WalletDatabase for WalletPostgresDatabase { - async fn begin_db_transaction( - &self, - ) -> Result, FfiError> { - self.inner.begin_db_transaction().await - } + // ========== Read methods ========== async fn get_proofs_by_ys(&self, ys: Vec) -> Result, FfiError> { self.inner.get_proofs_by_ys(ys).await @@ -174,4 +170,109 @@ impl WalletDatabase for WalletPostgresDatabase { .kv_list(primary_namespace, secondary_namespace) .await } + + async fn kv_write( + &self, + primary_namespace: String, + secondary_namespace: String, + key: String, + value: Vec, + ) -> Result<(), FfiError> { + self.inner + .kv_write(primary_namespace, secondary_namespace, key, value) + .await + } + + async fn kv_remove( + &self, + primary_namespace: String, + secondary_namespace: String, + key: String, + ) -> Result<(), FfiError> { + self.inner + .kv_remove(primary_namespace, secondary_namespace, key) + .await + } + + // ========== Write methods ========== + + async fn update_proofs( + &self, + added: Vec, + removed_ys: Vec, + ) -> Result<(), FfiError> { + self.inner.update_proofs(added, removed_ys).await + } + + async fn update_proofs_state( + &self, + ys: Vec, + state: ProofState, + ) -> Result<(), FfiError> { + self.inner.update_proofs_state(ys, state).await + } + + async fn add_transaction(&self, transaction: Transaction) -> Result<(), FfiError> { + self.inner.add_transaction(transaction).await + } + + async fn remove_transaction(&self, transaction_id: TransactionId) -> Result<(), FfiError> { + self.inner.remove_transaction(transaction_id).await + } + + async fn update_mint_url( + &self, + old_mint_url: MintUrl, + new_mint_url: MintUrl, + ) -> Result<(), FfiError> { + self.inner.update_mint_url(old_mint_url, new_mint_url).await + } + + async fn increment_keyset_counter(&self, keyset_id: Id, count: u32) -> Result { + self.inner.increment_keyset_counter(keyset_id, count).await + } + + async fn add_mint( + &self, + mint_url: MintUrl, + mint_info: Option, + ) -> Result<(), FfiError> { + self.inner.add_mint(mint_url, mint_info).await + } + + async fn remove_mint(&self, mint_url: MintUrl) -> Result<(), FfiError> { + self.inner.remove_mint(mint_url).await + } + + async fn add_mint_keysets( + &self, + mint_url: MintUrl, + keysets: Vec, + ) -> Result<(), FfiError> { + self.inner.add_mint_keysets(mint_url, keysets).await + } + + async fn add_mint_quote(&self, quote: MintQuote) -> Result<(), FfiError> { + self.inner.add_mint_quote(quote).await + } + + async fn remove_mint_quote(&self, quote_id: String) -> Result<(), FfiError> { + self.inner.remove_mint_quote(quote_id).await + } + + async fn add_melt_quote(&self, quote: MeltQuote) -> Result<(), FfiError> { + self.inner.add_melt_quote(quote).await + } + + async fn remove_melt_quote(&self, quote_id: String) -> Result<(), FfiError> { + self.inner.remove_melt_quote(quote_id).await + } + + async fn add_keys(&self, keyset: KeySet) -> Result<(), FfiError> { + self.inner.add_keys(keyset).await + } + + async fn remove_keys(&self, id: Id) -> Result<(), FfiError> { + self.inner.remove_keys(id).await + } } diff --git a/crates/cdk-ffi/src/sqlite.rs b/crates/cdk-ffi/src/sqlite.rs index 6d1701855..48f975c79 100644 --- a/crates/cdk-ffi/src/sqlite.rs +++ b/crates/cdk-ffi/src/sqlite.rs @@ -5,9 +5,9 @@ use cdk_sqlite::wallet::WalletSqliteDatabase as CdkWalletSqliteDatabase; use cdk_sqlite::SqliteConnectionManager; use crate::{ - CurrencyUnit, FfiError, FfiWalletSQLDatabase, Id, KeySetInfo, Keys, MeltQuote, MintInfo, - MintQuote, MintUrl, ProofInfo, ProofState, PublicKey, SpendingConditions, Transaction, - TransactionDirection, TransactionId, WalletDatabase, + CurrencyUnit, FfiError, FfiWalletSQLDatabase, Id, KeySet, KeySetInfo, Keys, MeltQuote, + MintInfo, MintQuote, MintUrl, ProofInfo, ProofState, PublicKey, SpendingConditions, + Transaction, TransactionDirection, TransactionId, WalletDatabase, }; /// FFI-compatible WalletSqliteDatabase implementation that implements the WalletDatabaseFfi trait @@ -67,10 +67,10 @@ impl WalletSqliteDatabase { #[uniffi::export(async_runtime = "tokio")] #[async_trait::async_trait] impl WalletDatabase for WalletSqliteDatabase { - async fn begin_db_transaction( - &self, - ) -> Result, FfiError> { - self.inner.begin_db_transaction().await + // ========== Read methods ========== + + async fn get_proofs_by_ys(&self, ys: Vec) -> Result, FfiError> { + self.inner.get_proofs_by_ys(ys).await } async fn get_mint(&self, mint_url: MintUrl) -> Result, FfiError> { @@ -128,10 +128,6 @@ impl WalletDatabase for WalletSqliteDatabase { .await } - async fn get_proofs_by_ys(&self, ys: Vec) -> Result, FfiError> { - self.inner.get_proofs_by_ys(ys).await - } - async fn get_balance( &self, mint_url: Option, @@ -179,4 +175,109 @@ impl WalletDatabase for WalletSqliteDatabase { .kv_list(primary_namespace, secondary_namespace) .await } + + async fn kv_write( + &self, + primary_namespace: String, + secondary_namespace: String, + key: String, + value: Vec, + ) -> Result<(), FfiError> { + self.inner + .kv_write(primary_namespace, secondary_namespace, key, value) + .await + } + + async fn kv_remove( + &self, + primary_namespace: String, + secondary_namespace: String, + key: String, + ) -> Result<(), FfiError> { + self.inner + .kv_remove(primary_namespace, secondary_namespace, key) + .await + } + + // ========== Write methods ========== + + async fn update_proofs( + &self, + added: Vec, + removed_ys: Vec, + ) -> Result<(), FfiError> { + self.inner.update_proofs(added, removed_ys).await + } + + async fn update_proofs_state( + &self, + ys: Vec, + state: ProofState, + ) -> Result<(), FfiError> { + self.inner.update_proofs_state(ys, state).await + } + + async fn add_transaction(&self, transaction: Transaction) -> Result<(), FfiError> { + self.inner.add_transaction(transaction).await + } + + async fn remove_transaction(&self, transaction_id: TransactionId) -> Result<(), FfiError> { + self.inner.remove_transaction(transaction_id).await + } + + async fn update_mint_url( + &self, + old_mint_url: MintUrl, + new_mint_url: MintUrl, + ) -> Result<(), FfiError> { + self.inner.update_mint_url(old_mint_url, new_mint_url).await + } + + async fn increment_keyset_counter(&self, keyset_id: Id, count: u32) -> Result { + self.inner.increment_keyset_counter(keyset_id, count).await + } + + async fn add_mint( + &self, + mint_url: MintUrl, + mint_info: Option, + ) -> Result<(), FfiError> { + self.inner.add_mint(mint_url, mint_info).await + } + + async fn remove_mint(&self, mint_url: MintUrl) -> Result<(), FfiError> { + self.inner.remove_mint(mint_url).await + } + + async fn add_mint_keysets( + &self, + mint_url: MintUrl, + keysets: Vec, + ) -> Result<(), FfiError> { + self.inner.add_mint_keysets(mint_url, keysets).await + } + + async fn add_mint_quote(&self, quote: MintQuote) -> Result<(), FfiError> { + self.inner.add_mint_quote(quote).await + } + + async fn remove_mint_quote(&self, quote_id: String) -> Result<(), FfiError> { + self.inner.remove_mint_quote(quote_id).await + } + + async fn add_melt_quote(&self, quote: MeltQuote) -> Result<(), FfiError> { + self.inner.add_melt_quote(quote).await + } + + async fn remove_melt_quote(&self, quote_id: String) -> Result<(), FfiError> { + self.inner.remove_melt_quote(quote_id).await + } + + async fn add_keys(&self, keyset: KeySet) -> Result<(), FfiError> { + self.inner.add_keys(keyset).await + } + + async fn remove_keys(&self, id: Id) -> Result<(), FfiError> { + self.inner.remove_keys(id).await + } } diff --git a/crates/cdk-ffi/tests/test_transactions.py b/crates/cdk-ffi/tests/test_transactions.py index e59f9407b..04aef4d99 100755 --- a/crates/cdk-ffi/tests/test_transactions.py +++ b/crates/cdk-ffi/tests/test_transactions.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 """ -Test suite for CDK FFI wallet and transaction operations +Test suite for CDK FFI wallet database operations """ import asyncio @@ -29,333 +29,7 @@ import cdk_ffi -# Transaction Tests (using explicit transactions) - -async def test_increment_keyset_counter_commit(): - """Test that increment_keyset_counter works and persists after commit""" - print("\n=== Test: Increment Keyset Counter with Commit ===") - - with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as tmp: - db_path = tmp.name - - try: - backend = cdk_ffi.WalletDbBackend.SQLITE(path=db_path) - db = cdk_ffi.create_wallet_db(backend) - - keyset_id = cdk_ffi.Id(hex="004146bdf4a9afab") - mint_url = cdk_ffi.MintUrl(url="https://testmint.example.com") - keyset_info = cdk_ffi.KeySetInfo( - id=keyset_id.hex, - unit=cdk_ffi.CurrencyUnit.SAT(), - active=True, - input_fee_ppk=0 - ) - - # Setup - tx = await db.begin_db_transaction() - await tx.add_mint(mint_url, None) - await tx.add_mint_keysets(mint_url, [keyset_info]) - await tx.commit() - - # Increment counter in transaction - tx = await db.begin_db_transaction() - counter1 = await tx.increment_keyset_counter(keyset_id, 1) - counter2 = await tx.increment_keyset_counter(keyset_id, 5) - await tx.commit() - - assert counter1 == 1, f"Expected counter 1, got {counter1}" - assert counter2 == 6, f"Expected counter 6, got {counter2}" - print("✓ Counters incremented correctly") - - # Verify persistence - tx_read = await db.begin_db_transaction() - counter3 = await tx_read.increment_keyset_counter(keyset_id, 0) - await tx_read.rollback() - assert counter3 == 6, f"Expected persisted counter 6, got {counter3}" - print("✓ Counter persisted after commit") - - print("✓ Test passed: Counter increments and commits work") - - finally: - if os.path.exists(db_path): - os.unlink(db_path) - - -async def test_implicit_rollback_on_drop(): - """Test that transactions are implicitly rolled back when dropped""" - print("\n=== Test: Implicit Rollback on Drop ===") - - with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as tmp: - db_path = tmp.name - - try: - backend = cdk_ffi.WalletDbBackend.SQLITE(path=db_path) - db = cdk_ffi.create_wallet_db(backend) - - keyset_id = cdk_ffi.Id(hex="004146bdf4a9afab") - mint_url = cdk_ffi.MintUrl(url="https://testmint.example.com") - - # Setup - tx = await db.begin_db_transaction() - await tx.add_mint(mint_url, None) - keyset_info = cdk_ffi.KeySetInfo( - id=keyset_id.hex, - unit=cdk_ffi.CurrencyUnit.SAT(), - active=True, - input_fee_ppk=0 - ) - await tx.add_mint_keysets(mint_url, [keyset_info]) - await tx.commit() - - # Get initial counter - tx_read = await db.begin_db_transaction() - initial_counter = await tx_read.increment_keyset_counter(keyset_id, 0) - await tx_read.rollback() - print(f"Initial counter: {initial_counter}") - - # Increment without commit - tx_no_commit = await db.begin_db_transaction() - incremented = await tx_no_commit.increment_keyset_counter(keyset_id, 10) - print(f"Counter incremented to {incremented} (not committed)") - del tx_no_commit - - await asyncio.sleep(0.5) - print("Transaction dropped (should trigger implicit rollback)") - - # Verify rollback - tx_verify = await db.begin_db_transaction() - final_counter = await tx_verify.increment_keyset_counter(keyset_id, 0) - await tx_verify.rollback() - - assert final_counter == initial_counter, \ - f"Expected counter to rollback to {initial_counter}, got {final_counter}" - print("✓ Implicit rollback works correctly") - - print("✓ Test passed: Implicit rollback on drop works") - - finally: - if os.path.exists(db_path): - os.unlink(db_path) - - -async def test_explicit_rollback(): - """Test explicit rollback of transaction changes""" - print("\n=== Test: Explicit Rollback ===") - - with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as tmp: - db_path = tmp.name - - try: - backend = cdk_ffi.WalletDbBackend.SQLITE(path=db_path) - db = cdk_ffi.create_wallet_db(backend) - - keyset_id = cdk_ffi.Id(hex="004146bdf4a9afab") - mint_url = cdk_ffi.MintUrl(url="https://testmint.example.com") - - # Setup - tx = await db.begin_db_transaction() - await tx.add_mint(mint_url, None) - keyset_info = cdk_ffi.KeySetInfo( - id=keyset_id.hex, - unit=cdk_ffi.CurrencyUnit.SAT(), - active=True, - input_fee_ppk=0 - ) - await tx.add_mint_keysets(mint_url, [keyset_info]) - counter_initial = await tx.increment_keyset_counter(keyset_id, 5) - await tx.commit() - print(f"Initial counter: {counter_initial}") - - # Increment and rollback - tx_rollback = await db.begin_db_transaction() - counter_incremented = await tx_rollback.increment_keyset_counter(keyset_id, 100) - print(f"Counter incremented to {counter_incremented} in transaction") - await tx_rollback.rollback() - print("Explicitly rolled back transaction") - - # Verify rollback - tx_verify = await db.begin_db_transaction() - counter_after = await tx_verify.increment_keyset_counter(keyset_id, 0) - await tx_verify.rollback() - - assert counter_after == counter_initial, \ - f"Expected counter {counter_initial}, got {counter_after}" - print("✓ Explicit rollback works correctly") - - print("✓ Test passed: Explicit rollback works") - - finally: - if os.path.exists(db_path): - os.unlink(db_path) - - -async def test_transaction_reads(): - """Test reading data within transactions""" - print("\n=== Test: Transaction Reads ===") - - with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as tmp: - db_path = tmp.name - - try: - backend = cdk_ffi.WalletDbBackend.SQLITE(path=db_path) - db = cdk_ffi.create_wallet_db(backend) - - keyset_id = cdk_ffi.Id(hex="004146bdf4a9afab") - mint_url = cdk_ffi.MintUrl(url="https://testmint.example.com") - - # Add keyset in transaction and read within same transaction - tx = await db.begin_db_transaction() - await tx.add_mint(mint_url, None) - keyset_info = cdk_ffi.KeySetInfo( - id=keyset_id.hex, - unit=cdk_ffi.CurrencyUnit.SAT(), - active=True, - input_fee_ppk=0 - ) - await tx.add_mint_keysets(mint_url, [keyset_info]) - - keyset_read = await tx.get_keyset_by_id(keyset_id) - assert keyset_read is not None, "Should read within transaction" - assert keyset_read.id == keyset_id.hex, "Keyset ID should match" - print("✓ Read keyset within transaction") - - await tx.commit() - - # Read from new transaction - tx_new = await db.begin_db_transaction() - keyset_read2 = await tx_new.get_keyset_by_id(keyset_id) - assert keyset_read2 is not None, "Should read committed keyset" - await tx_new.rollback() - print("✓ Read keyset in new transaction") - - print("✓ Test passed: Transaction reads work") - - finally: - if os.path.exists(db_path): - os.unlink(db_path) - - -async def test_multiple_increments_same_transaction(): - """Test multiple increments in same transaction""" - print("\n=== Test: Multiple Increments in Same Transaction ===") - - with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as tmp: - db_path = tmp.name - - try: - backend = cdk_ffi.WalletDbBackend.SQLITE(path=db_path) - db = cdk_ffi.create_wallet_db(backend) - - keyset_id = cdk_ffi.Id(hex="004146bdf4a9afab") - mint_url = cdk_ffi.MintUrl(url="https://testmint.example.com") - - # Setup - tx = await db.begin_db_transaction() - await tx.add_mint(mint_url, None) - keyset_info = cdk_ffi.KeySetInfo( - id=keyset_id.hex, - unit=cdk_ffi.CurrencyUnit.SAT(), - active=True, - input_fee_ppk=0 - ) - await tx.add_mint_keysets(mint_url, [keyset_info]) - await tx.commit() - - # Multiple increments in one transaction - tx = await db.begin_db_transaction() - counters = [] - for i in range(1, 6): - counter = await tx.increment_keyset_counter(keyset_id, 1) - counters.append(counter) - - expected = list(range(1, 6)) - assert counters == expected, f"Expected {expected}, got {counters}" - print(f"✓ Counters incremented: {counters}") - - await tx.commit() - - # Verify final value - tx_verify = await db.begin_db_transaction() - final = await tx_verify.increment_keyset_counter(keyset_id, 0) - await tx_verify.rollback() - assert final == 5, f"Expected final counter 5, got {final}" - print("✓ Final counter value correct") - - print("✓ Test passed: Multiple increments work") - - finally: - if os.path.exists(db_path): - os.unlink(db_path) - - -async def test_transaction_atomicity(): - """Test that transaction rollback reverts all changes""" - print("\n=== Test: Transaction Atomicity ===") - - with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as tmp: - db_path = tmp.name - - try: - backend = cdk_ffi.WalletDbBackend.SQLITE(path=db_path) - db = cdk_ffi.create_wallet_db(backend) - - mint_url1 = cdk_ffi.MintUrl(url="https://mint1.example.com") - mint_url2 = cdk_ffi.MintUrl(url="https://mint2.example.com") - keyset_id = cdk_ffi.Id(hex="004146bdf4a9afab") - - # Transaction with multiple operations - tx = await db.begin_db_transaction() - await tx.add_mint(mint_url1, None) - await tx.add_mint(mint_url2, None) - keyset_info = cdk_ffi.KeySetInfo( - id=keyset_id.hex, - unit=cdk_ffi.CurrencyUnit.SAT(), - active=True, - input_fee_ppk=0 - ) - await tx.add_mint_keysets(mint_url1, [keyset_info]) - await tx.increment_keyset_counter(keyset_id, 42) - print("✓ Performed multiple operations") - - # Rollback - await tx.rollback() - print("✓ Rolled back transaction") - - # Verify nothing persisted - tx_read = await db.begin_db_transaction() - keyset_read = await tx_read.get_keyset_by_id(keyset_id) - await tx_read.rollback() - assert keyset_read is None, "Keyset should not exist after rollback" - print("✓ Nothing persisted after rollback") - - # Now commit - tx2 = await db.begin_db_transaction() - await tx2.add_mint(mint_url1, None) - await tx2.add_mint(mint_url2, None) - await tx2.add_mint_keysets(mint_url1, [keyset_info]) - await tx2.increment_keyset_counter(keyset_id, 42) - await tx2.commit() - print("✓ Committed transaction") - - # Verify persistence - tx_verify = await db.begin_db_transaction() - keyset_after = await tx_verify.get_keyset_by_id(keyset_id) - assert keyset_after is not None, "Keyset should exist after commit" - counter_after = await tx_verify.increment_keyset_counter(keyset_id, 0) - await tx_verify.rollback() - assert counter_after == 42, f"Expected counter 42, got {counter_after}" - print("✓ All operations persisted after commit") - - print("✓ Test passed: Transaction atomicity works") - - finally: - if os.path.exists(db_path): - os.unlink(db_path) - - - - -# Wallet Tests (using direct wallet methods without explicit transactions) +# Wallet Database Tests async def test_wallet_creation(): """Test creating a wallet with SQLite backend""" @@ -394,20 +68,16 @@ async def test_wallet_mint_management(): mint_url = cdk_ffi.MintUrl(url="https://testmint.example.com") - # Add mint (using transaction) - tx = await db.begin_db_transaction() - await tx.add_mint(mint_url, None) - await tx.commit() + # Add mint + await db.add_mint(mint_url, None) print("✓ Added mint to wallet") - # Get specific mint (read-only, can use db directly) + # Get specific mint await db.get_mint(mint_url) print("✓ Retrieved mint from database") - # Remove mint (using transaction) - tx = await db.begin_db_transaction() - await tx.remove_mint(mint_url) - await tx.commit() + # Remove mint + await db.remove_mint(mint_url) print("✓ Removed mint from wallet") # Verify removal @@ -436,26 +106,24 @@ async def test_wallet_keyset_management(): mint_url = cdk_ffi.MintUrl(url="https://testmint.example.com") keyset_id = cdk_ffi.Id(hex="004146bdf4a9afab") - # Add mint and keyset (using transaction) - tx = await db.begin_db_transaction() - await tx.add_mint(mint_url, None) + # Add mint and keyset + await db.add_mint(mint_url, None) keyset_info = cdk_ffi.KeySetInfo( id=keyset_id.hex, unit=cdk_ffi.CurrencyUnit.SAT(), active=True, input_fee_ppk=0 ) - await tx.add_mint_keysets(mint_url, [keyset_info]) - await tx.commit() + await db.add_mint_keysets(mint_url, [keyset_info]) print("✓ Added mint and keyset") - # Query keyset by ID (read-only) + # Query keyset by ID keyset = await db.get_keyset_by_id(keyset_id) assert keyset is not None, "Keyset should exist" assert keyset.id == keyset_id.hex, "Keyset ID should match" print(f"✓ Retrieved keyset: {keyset.id}") - # Query keysets for mint (read-only) + # Query keysets for mint keysets = await db.get_mint_keysets(mint_url) assert keysets is not None and len(keysets) > 0, "Should have keysets for mint" print(f"✓ Retrieved {len(keysets)} keyset(s) for mint") @@ -481,25 +149,21 @@ async def test_wallet_keyset_counter(): mint_url = cdk_ffi.MintUrl(url="https://testmint.example.com") keyset_id = cdk_ffi.Id(hex="004146bdf4a9afab") - # Setup (using transaction) - tx = await db.begin_db_transaction() - await tx.add_mint(mint_url, None) + # Setup + await db.add_mint(mint_url, None) keyset_info = cdk_ffi.KeySetInfo( id=keyset_id.hex, unit=cdk_ffi.CurrencyUnit.SAT(), active=True, input_fee_ppk=0 ) - await tx.add_mint_keysets(mint_url, [keyset_info]) - await tx.commit() + await db.add_mint_keysets(mint_url, [keyset_info]) print("✓ Setup complete") - # Increment counter (using transaction) - tx = await db.begin_db_transaction() - counter1 = await tx.increment_keyset_counter(keyset_id, 1) - counter2 = await tx.increment_keyset_counter(keyset_id, 5) - counter3 = await tx.increment_keyset_counter(keyset_id, 0) - await tx.commit() + # Increment counter + counter1 = await db.increment_keyset_counter(keyset_id, 1) + counter2 = await db.increment_keyset_counter(keyset_id, 5) + counter3 = await db.increment_keyset_counter(keyset_id, 0) print(f"✓ Counter after +1: {counter1}") assert counter1 == 1, f"Expected counter 1, got {counter1}" @@ -528,13 +192,11 @@ async def test_wallet_quotes(): mint_url = cdk_ffi.MintUrl(url="https://testmint.example.com") - # Add mint (using transaction) - tx = await db.begin_db_transaction() - await tx.add_mint(mint_url, None) - await tx.commit() + # Add mint + await db.add_mint(mint_url, None) print("✓ Added mint") - # Query quotes (read-only) + # Query quotes mint_quotes = await db.get_mint_quotes() assert isinstance(mint_quotes, list), "get_mint_quotes should return a list" print(f"✓ Retrieved {len(mint_quotes)} mint quote(s)") @@ -575,18 +237,10 @@ async def test_wallet_proofs_by_ys(): async def main(): """Run all tests""" - print("Starting CDK FFI Wallet and Transaction Tests") + print("Starting CDK FFI Wallet Database Tests") print("=" * 50) tests = [ - # Transaction tests - ("Increment Counter with Commit", test_increment_keyset_counter_commit), - ("Implicit Rollback on Drop", test_implicit_rollback_on_drop), - ("Explicit Rollback", test_explicit_rollback), - ("Transaction Reads", test_transaction_reads), - ("Multiple Increments", test_multiple_increments_same_transaction), - ("Transaction Atomicity", test_transaction_atomicity), - # Wallet tests (read methods + write via transactions) ("Wallet Creation", test_wallet_creation), ("Wallet Mint Management", test_wallet_mint_management), ("Wallet Keyset Management", test_wallet_keyset_management), diff --git a/crates/cdk-integration-tests/tests/fake_auth.rs b/crates/cdk-integration-tests/tests/fake_auth.rs index 6efd66c91..75c306c07 100644 --- a/crates/cdk-integration-tests/tests/fake_auth.rs +++ b/crates/cdk-integration-tests/tests/fake_auth.rs @@ -515,9 +515,11 @@ async fn test_reuse_auth_proof() { assert!(quote.amount == Some(10.into())); } - let mut tx = wallet.localstore.begin_db_transaction().await.unwrap(); - tx.update_proofs(proofs, vec![]).await.unwrap(); - tx.commit().await.unwrap(); + wallet + .localstore + .update_proofs(proofs, vec![]) + .await + .unwrap(); { let quote_res = wallet.mint_quote(10.into(), None).await; diff --git a/crates/cdk-integration-tests/tests/happy_path_mint_wallet.rs b/crates/cdk-integration-tests/tests/happy_path_mint_wallet.rs index 7a82d27e1..02ad75e5f 100644 --- a/crates/cdk-integration-tests/tests/happy_path_mint_wallet.rs +++ b/crates/cdk-integration-tests/tests/happy_path_mint_wallet.rs @@ -406,13 +406,11 @@ async fn test_restore_with_counter_gap() { // This simulates failed operations or multi-device usage where counter values // were consumed but no signatures were obtained let gap_size = 50u32; - { - let mut tx = wallet.localstore.begin_db_transaction().await.unwrap(); - tx.increment_keyset_counter(&keyset_id, gap_size) - .await - .unwrap(); - tx.commit().await.unwrap(); - } + wallet + .localstore + .increment_keyset_counter(&keyset_id, gap_size) + .await + .unwrap(); // Mint second batch of proofs (uses counters after the gap) let mint_quote2 = wallet.mint_quote(100.into(), None).await.unwrap(); diff --git a/crates/cdk-redb/src/wallet/mod.rs b/crates/cdk-redb/src/wallet/mod.rs index c7cc9ca52..3e8f02dc8 100644 --- a/crates/cdk-redb/src/wallet/mod.rs +++ b/crates/cdk-redb/src/wallet/mod.rs @@ -8,10 +8,7 @@ use std::sync::Arc; use async_trait::async_trait; use cdk_common::common::ProofInfo; -use cdk_common::database::{ - validate_kvstore_params, DbTransactionFinalizer, KVStore, KVStoreDatabase, KVStoreTransaction, - WalletDatabase, WalletDatabaseTransaction, -}; +use cdk_common::database::{validate_kvstore_params, KVStoreDatabase, WalletDatabase}; use cdk_common::mint_url::MintUrl; use cdk_common::nut00::KnownMethod; use cdk_common::util::unix_time; @@ -23,79 +20,6 @@ use cdk_common::{ use redb::{Database, MultimapTableDefinition, ReadableTable, TableDefinition}; use tracing::instrument; -/// Enum to abstract over read-only and read-write table access for KV store operations -enum KvTable<'txn> { - ReadOnly(redb::ReadOnlyTable<(&'static str, &'static str, &'static str), &'static [u8]>), - ReadWrite(redb::Table<'txn, (&'static str, &'static str, &'static str), &'static [u8]>), -} - -impl KvTable<'_> { - /// Read a value from the KV store table - #[inline(always)] - fn kv_read( - &self, - primary_namespace: &str, - secondary_namespace: &str, - key: &str, - ) -> Result>, Error> { - let result = match self { - KvTable::ReadOnly(table) => table - .get((primary_namespace, secondary_namespace, key)) - .map_err(Error::from)? - .map(|v| v.value().to_vec()), - KvTable::ReadWrite(table) => table - .get((primary_namespace, secondary_namespace, key)) - .map_err(Error::from)? - .map(|v| v.value().to_vec()), - }; - - Ok(result) - } - - /// List all keys in a namespace from the KV store table - #[inline(always)] - fn kv_list( - &self, - primary_namespace: &str, - secondary_namespace: &str, - ) -> Result, Error> { - let mut keys = Vec::new(); - - // Use range iterator for efficient lookup by namespace prefix - let start = (primary_namespace, secondary_namespace, ""); - - match self { - KvTable::ReadOnly(table) => { - for result in table.range(start..).map_err(Error::from)? { - let (key_tuple, _) = result.map_err(Error::from)?; - let (primary_from_db, secondary_from_db, k) = key_tuple.value(); - if primary_from_db != primary_namespace - || secondary_from_db != secondary_namespace - { - break; - } - keys.push(k.to_string()); - } - } - KvTable::ReadWrite(table) => { - for result in table.range(start..).map_err(Error::from)? { - let (key_tuple, _) = result.map_err(Error::from)?; - let (primary_from_db, secondary_from_db, k) = key_tuple.value(); - if primary_from_db != primary_namespace - || secondary_from_db != secondary_namespace - { - break; - } - keys.push(k.to_string()); - } - } - } - - // Keys are already sorted by the B-tree structure - Ok(keys) - } -} - use super::error::Error; use crate::migrations::migrate_00_to_01; use crate::wallet::migrations::{migrate_01_to_02, migrate_02_to_03, migrate_03_to_04}; @@ -133,30 +57,6 @@ pub struct WalletRedbDatabase { db: Arc, } -/// Redb Wallet Transaction -#[allow(missing_debug_implementations)] -pub struct RedbWalletTransaction { - write_txn: Option, -} - -impl RedbWalletTransaction { - /// Create a new transaction - fn new(write_txn: redb::WriteTransaction) -> Self { - Self { - write_txn: Some(write_txn), - } - } - - /// Get a mutable reference to the write transaction - fn txn(&mut self) -> Result<&mut redb::WriteTransaction, Error> { - self.write_txn.as_mut().ok_or_else(|| { - Error::CDKDatabase(database::Error::Internal( - "Transaction already consumed".to_owned(), - )) - }) - } -} - impl WalletRedbDatabase { /// Create new [`WalletRedbDatabase`] pub fn new(path: &Path) -> Result { @@ -597,189 +497,156 @@ impl WalletDatabase for WalletRedbDatabase { Ok(transactions) } - async fn begin_db_transaction( + #[instrument(skip(self, added, removed_ys))] + async fn update_proofs( &self, - ) -> Result + Send + Sync>, database::Error> - { + added: Vec, + removed_ys: Vec, + ) -> Result<(), database::Error> { let write_txn = self.db.begin_write().map_err(Error::from)?; - Ok(Box::new(RedbWalletTransaction::new(write_txn))) - } -} - -#[async_trait] -impl KVStoreDatabase for WalletRedbDatabase { - type Err = database::Error; - - #[instrument(skip_all)] - async fn kv_read( - &self, - primary_namespace: &str, - secondary_namespace: &str, - key: &str, - ) -> Result>, Self::Err> { - // Validate parameters according to KV store requirements - validate_kvstore_params(primary_namespace, secondary_namespace, Some(key))?; - - let read_txn = self.db.begin_read().map_err(Error::from)?; - let table = KvTable::ReadOnly(read_txn.open_table(KV_STORE_TABLE).map_err(Error::from)?); - - Ok(table.kv_read(primary_namespace, secondary_namespace, key)?) - } - - #[instrument(skip_all)] - async fn kv_list( - &self, - primary_namespace: &str, - secondary_namespace: &str, - ) -> Result, Self::Err> { - validate_kvstore_params(primary_namespace, secondary_namespace, None)?; + { + let mut table = write_txn.open_table(PROOFS_TABLE).map_err(Error::from)?; - let read_txn = self.db.begin_read().map_err(Error::from)?; - let table = KvTable::ReadOnly(read_txn.open_table(KV_STORE_TABLE).map_err(Error::from)?); + for proof_info in added.iter() { + table + .insert( + proof_info.y.to_bytes().as_slice(), + serde_json::to_string(&proof_info) + .map_err(Error::from)? + .as_str(), + ) + .map_err(Error::from)?; + } - Ok(table.kv_list(primary_namespace, secondary_namespace)?) + for y in removed_ys.iter() { + table.remove(y.to_bytes().as_slice()).map_err(Error::from)?; + } + } + write_txn.commit().map_err(Error::from)?; + Ok(()) } -} -#[async_trait] -impl KVStore for WalletRedbDatabase { - async fn begin_transaction( + async fn update_proofs_state( &self, - ) -> Result + Send + Sync>, database::Error> { + ys: Vec, + state: State, + ) -> Result<(), database::Error> { let write_txn = self.db.begin_write().map_err(Error::from)?; - Ok(Box::new(RedbWalletTransaction::new(write_txn))) - } -} - -#[async_trait] -impl WalletDatabaseTransaction for RedbWalletTransaction { - #[instrument(skip(self), fields(keyset_id = %keyset_id))] - async fn get_keyset_by_id( - &mut self, - keyset_id: &Id, - ) -> Result, database::Error> { - let txn = self.txn().map_err(Into::::into)?; - let table = txn.open_table(KEYSETS_TABLE).map_err(Error::from)?; - - let result = match table - .get(keyset_id.to_bytes().as_slice()) - .map_err(Error::from)? { - Some(keyset) => { - let keyset: KeySetInfo = - serde_json::from_str(keyset.value()).map_err(Error::from)?; + let mut table = write_txn.open_table(PROOFS_TABLE).map_err(Error::from)?; - Ok(Some(keyset)) - } - None => Ok(None), - }; + for y in ys { + let y_slice = y.to_bytes(); + let proof = table + .get(y_slice.as_slice()) + .map_err(Error::from)? + .ok_or(Error::UnknownY)?; - result - } + let mut proof_info = + serde_json::from_str::(proof.value()).map_err(Error::from)?; + drop(proof); - #[instrument(skip(self), fields(keyset_id = %keyset_id))] - async fn get_keys(&mut self, keyset_id: &Id) -> Result, database::Error> { - let txn = self.txn().map_err(Into::::into)?; - let table = txn.open_table(MINT_KEYS_TABLE).map_err(Error::from)?; + proof_info.state = state; - if let Some(mint_info) = table - .get(keyset_id.to_string().as_str()) - .map_err(Error::from)? - { - return Ok(serde_json::from_str(mint_info.value()).map_err(Error::from)?); + table + .insert( + y_slice.as_slice(), + serde_json::to_string(&proof_info) + .map_err(Error::from)? + .as_str(), + ) + .map_err(Error::from)?; + } } - - Ok(None) - } - - #[instrument(skip(self))] - async fn add_mint( - &mut self, - mint_url: MintUrl, - mint_info: Option, - ) -> Result<(), database::Error> { - let txn = self.txn()?; - let mut table = txn.open_table(MINTS_TABLE).map_err(Error::from)?; - table - .insert( - mint_url.to_string().as_str(), - serde_json::to_string(&mint_info) - .map_err(Error::from)? - .as_str(), - ) - .map_err(Error::from)?; + write_txn.commit().map_err(Error::from)?; Ok(()) } #[instrument(skip(self))] - async fn remove_mint(&mut self, mint_url: MintUrl) -> Result<(), database::Error> { - let txn = self.txn()?; - let mut table = txn.open_table(MINTS_TABLE).map_err(Error::from)?; - table - .remove(mint_url.to_string().as_str()) - .map_err(Error::from)?; + async fn add_transaction(&self, transaction: Transaction) -> Result<(), database::Error> { + let id = transaction.id(); + let write_txn = self.db.begin_write().map_err(Error::from)?; + { + let mut table = write_txn + .open_table(TRANSACTIONS_TABLE) + .map_err(Error::from)?; + table + .insert( + id.as_slice(), + serde_json::to_string(&transaction) + .map_err(Error::from)? + .as_str(), + ) + .map_err(Error::from)?; + } + write_txn.commit().map_err(Error::from)?; Ok(()) } #[instrument(skip(self))] async fn update_mint_url( - &mut self, + &self, old_mint_url: MintUrl, new_mint_url: MintUrl, ) -> Result<(), database::Error> { + let write_txn = self.db.begin_write().map_err(Error::from)?; + // Update proofs table { - let proofs = self - .get_proofs(Some(old_mint_url.clone()), None, None, None) - .await - .map_err(Error::from)?; - - // Proofs with new url - let updated_proofs: Vec = proofs - .clone() - .into_iter() - .map(|mut p| { - p.mint_url = new_mint_url.clone(); - p + let read_table = write_txn.open_table(PROOFS_TABLE).map_err(Error::from)?; + let proofs: Vec = read_table + .iter() + .map_err(Error::from)? + .flatten() + .filter_map(|(_k, v)| { + let proof_info = serde_json::from_str::(v.value()).ok()?; + if proof_info.mint_url == old_mint_url { + Some(proof_info) + } else { + None + } }) .collect(); - - if !updated_proofs.is_empty() { - self.update_proofs(updated_proofs, vec![]).await?; + drop(read_table); + + if !proofs.is_empty() { + let mut write_table = write_txn.open_table(PROOFS_TABLE).map_err(Error::from)?; + for mut proof_info in proofs { + proof_info.mint_url = new_mint_url.clone(); + write_table + .insert( + proof_info.y.to_bytes().as_slice(), + serde_json::to_string(&proof_info) + .map_err(Error::from)? + .as_str(), + ) + .map_err(Error::from)?; + } } } // Update mint quotes { - let read_txn = self.txn()?; - let mut table = read_txn + let mut table = write_txn .open_table(MINT_QUOTES_TABLE) .map_err(Error::from)?; let unix_time = unix_time(); - let quotes = table + let quotes: Vec = table .iter() .map_err(Error::from)? .flatten() .filter_map(|(_, quote)| { - let mut q: MintQuote = serde_json::from_str(quote.value()) - .inspect_err(|err| { - tracing::warn!( - "Failed to deserialize {} with error {}", - quote.value(), - err - ) - }) - .ok()?; - if q.expiry < unix_time { + let mut q: MintQuote = serde_json::from_str(quote.value()).ok()?; + if q.mint_url == old_mint_url && q.expiry >= unix_time { q.mint_url = new_mint_url.clone(); Some(q) } else { None } }) - .collect::>(); + .collect(); for quote in quotes { table @@ -791,457 +658,390 @@ impl WalletDatabaseTransaction for RedbWalletTransaction { } } + write_txn.commit().map_err(Error::from)?; Ok(()) } - #[instrument(skip(self))] - async fn add_mint_keysets( - &mut self, - mint_url: MintUrl, - keysets: Vec, - ) -> Result<(), database::Error> { - let txn = self.txn()?; - let mut table = txn - .open_multimap_table(MINT_KEYSETS_TABLE) - .map_err(Error::from)?; - let mut keysets_table = txn.open_table(KEYSETS_TABLE).map_err(Error::from)?; - let mut u32_table = txn.open_table(KEYSET_U32_MAPPING).map_err(Error::from)?; - - let mut existing_u32 = false; - - for keyset in keysets { - // Check if keyset already exists - let existing_keyset = { - let existing_keyset = keysets_table - .get(keyset.id.to_bytes().as_slice()) - .map_err(Error::from)?; + #[instrument(skip(self), fields(keyset_id = %keyset_id))] + async fn increment_keyset_counter( + &self, + keyset_id: &Id, + count: u32, + ) -> Result { + let write_txn = self.db.begin_write().map_err(Error::from)?; + let new_counter = { + let mut table = write_txn.open_table(KEYSET_COUNTER).map_err(Error::from)?; + let current_counter = table + .get(keyset_id.to_string().as_str()) + .map_err(Error::from)? + .map(|x| x.value()) + .unwrap_or_default(); - existing_keyset.map(|r| r.value().to_string()) - }; + let new_counter = current_counter + count; - let existing = u32_table - .insert(u32::from(keyset.id), keyset.id.to_string().as_str()) + table + .insert(keyset_id.to_string().as_str(), new_counter) .map_err(Error::from)?; - match existing { - None => existing_u32 = false, - Some(id) => { - let id = Id::from_str(id.value())?; - - if id == keyset.id { - existing_u32 = false; - } else { - existing_u32 = true; - break; - } - } - } - - let keyset = if let Some(existing_keyset) = existing_keyset { - let mut existing_keyset: KeySetInfo = serde_json::from_str(&existing_keyset)?; - - existing_keyset.active = keyset.active; - existing_keyset.input_fee_ppk = keyset.input_fee_ppk; - - existing_keyset - } else { - table - .insert( - mint_url.to_string().as_str(), - keyset.id.to_bytes().as_slice(), - ) - .map_err(Error::from)?; - - keyset - }; + new_counter + }; + write_txn.commit().map_err(Error::from)?; + Ok(new_counter) + } - keysets_table + #[instrument(skip(self))] + async fn add_mint( + &self, + mint_url: MintUrl, + mint_info: Option, + ) -> Result<(), database::Error> { + let write_txn = self.db.begin_write().map_err(Error::from)?; + { + let mut table = write_txn.open_table(MINTS_TABLE).map_err(Error::from)?; + table .insert( - keyset.id.to_bytes().as_slice(), - serde_json::to_string(&keyset) + mint_url.to_string().as_str(), + serde_json::to_string(&mint_info) .map_err(Error::from)? .as_str(), ) .map_err(Error::from)?; } - - if existing_u32 { - tracing::warn!("Keyset already exists for keyset id"); - return Err(database::Error::Duplicate); - } - + write_txn.commit().map_err(Error::from)?; Ok(()) } - #[instrument(skip_all)] - async fn get_mint_quote( - &mut self, - quote_id: &str, - ) -> Result, database::Error> { - let txn = self.txn()?; - let table = txn.open_table(MINT_QUOTES_TABLE).map_err(Error::from)?; - - if let Some(mint_info) = table.get(quote_id).map_err(Error::from)? { - return Ok(serde_json::from_str(mint_info.value()).map_err(Error::from)?); + #[instrument(skip(self))] + async fn remove_mint(&self, mint_url: MintUrl) -> Result<(), database::Error> { + let write_txn = self.db.begin_write().map_err(Error::from)?; + { + let mut table = write_txn.open_table(MINTS_TABLE).map_err(Error::from)?; + table + .remove(mint_url.to_string().as_str()) + .map_err(Error::from)?; } - - Ok(None) - } - - #[instrument(skip_all)] - async fn add_mint_quote(&mut self, quote: MintQuote) -> Result<(), database::Error> { - let txn = self.txn()?; - let mut table = txn.open_table(MINT_QUOTES_TABLE).map_err(Error::from)?; - table - .insert( - quote.id.as_str(), - serde_json::to_string("e).map_err(Error::from)?.as_str(), - ) - .map_err(Error::from)?; + write_txn.commit().map_err(Error::from)?; Ok(()) } - #[instrument(skip_all)] - async fn remove_mint_quote(&mut self, quote_id: &str) -> Result<(), database::Error> { - let txn = self.txn()?; - let mut table = txn.open_table(MINT_QUOTES_TABLE).map_err(Error::from)?; - table.remove(quote_id).map_err(Error::from)?; - Ok(()) - } - - #[instrument(skip_all)] - async fn get_melt_quote( - &mut self, - quote_id: &str, - ) -> Result, database::Error> { - let txn = self.txn()?; - let table = txn.open_table(MELT_QUOTES_TABLE).map_err(Error::from)?; + #[instrument(skip(self))] + async fn add_mint_keysets( + &self, + mint_url: MintUrl, + keysets: Vec, + ) -> Result<(), database::Error> { + let write_txn = self.db.begin_write().map_err(Error::from)?; + { + let mut table = write_txn + .open_multimap_table(MINT_KEYSETS_TABLE) + .map_err(Error::from)?; + let mut keysets_table = write_txn.open_table(KEYSETS_TABLE).map_err(Error::from)?; + let mut u32_table = write_txn + .open_table(KEYSET_U32_MAPPING) + .map_err(Error::from)?; - if let Some(mint_info) = table.get(quote_id).map_err(Error::from)? { - return Ok(serde_json::from_str(mint_info.value()).map_err(Error::from)?); - } + let mut existing_u32 = false; - Ok(None) - } + for keyset in keysets { + // Check if keyset already exists + let existing_keyset = { + let existing_keyset = keysets_table + .get(keyset.id.to_bytes().as_slice()) + .map_err(Error::from)?; - #[instrument(skip_all)] - async fn add_melt_quote(&mut self, quote: wallet::MeltQuote) -> Result<(), database::Error> { - let txn = self.txn()?; - let mut table = txn.open_table(MELT_QUOTES_TABLE).map_err(Error::from)?; - table - .insert( - quote.id.as_str(), - serde_json::to_string("e).map_err(Error::from)?.as_str(), - ) - .map_err(Error::from)?; - Ok(()) - } + existing_keyset.map(|r| r.value().to_string()) + }; - #[instrument(skip_all)] - async fn remove_melt_quote(&mut self, quote_id: &str) -> Result<(), database::Error> { - let txn = self.txn()?; - let mut table = txn.open_table(MELT_QUOTES_TABLE).map_err(Error::from)?; - table.remove(quote_id).map_err(Error::from)?; - Ok(()) - } + let existing = u32_table + .insert(u32::from(keyset.id), keyset.id.to_string().as_str()) + .map_err(Error::from)?; - #[instrument(skip_all)] - async fn add_keys(&mut self, keyset: KeySet) -> Result<(), database::Error> { - let txn = self.txn()?; + match existing { + None => existing_u32 = false, + Some(id) => { + let id = Id::from_str(id.value())?; - keyset.verify_id()?; + if id == keyset.id { + existing_u32 = false; + } else { + existing_u32 = true; + break; + } + } + } - let mut table = txn.open_table(MINT_KEYS_TABLE).map_err(Error::from)?; + let keyset = if let Some(existing_keyset) = existing_keyset { + let mut existing_keyset: KeySetInfo = serde_json::from_str(&existing_keyset)?; - let existing_keys = table - .insert( - keyset.id.to_string().as_str(), - serde_json::to_string(&keyset.keys) - .map_err(Error::from)? - .as_str(), - ) - .map_err(Error::from)? - .is_some(); + existing_keyset.active = keyset.active; + existing_keyset.input_fee_ppk = keyset.input_fee_ppk; - let mut table = txn.open_table(KEYSET_U32_MAPPING).map_err(Error::from)?; + existing_keyset + } else { + table + .insert( + mint_url.to_string().as_str(), + keyset.id.to_bytes().as_slice(), + ) + .map_err(Error::from)?; - let existing = table - .insert(u32::from(keyset.id), keyset.id.to_string().as_str()) - .map_err(Error::from)?; + keyset + }; - let existing_u32 = match existing { - None => false, - Some(id) => { - let id = Id::from_str(id.value())?; - id != keyset.id + keysets_table + .insert( + keyset.id.to_bytes().as_slice(), + serde_json::to_string(&keyset) + .map_err(Error::from)? + .as_str(), + ) + .map_err(Error::from)?; } - }; - if existing_keys || existing_u32 { - tracing::warn!("Keys already exist for keyset id"); - return Err(database::Error::Duplicate); + if existing_u32 { + tracing::warn!("Keyset already exists for keyset id"); + return Err(database::Error::Duplicate); + } } - + write_txn.commit().map_err(Error::from)?; Ok(()) } - #[instrument(skip(self), fields(keyset_id = %keyset_id))] - async fn remove_keys(&mut self, keyset_id: &Id) -> Result<(), database::Error> { - let txn = self.txn()?; - let mut table = txn.open_table(MINT_KEYS_TABLE).map_err(Error::from)?; - - table - .remove(keyset_id.to_string().as_str()) - .map_err(Error::from)?; - + #[instrument(skip_all)] + async fn add_mint_quote(&self, quote: MintQuote) -> Result<(), database::Error> { + let write_txn = self.db.begin_write().map_err(Error::from)?; + { + let mut table = write_txn + .open_table(MINT_QUOTES_TABLE) + .map_err(Error::from)?; + table + .insert( + quote.id.as_str(), + serde_json::to_string("e).map_err(Error::from)?.as_str(), + ) + .map_err(Error::from)?; + } + write_txn.commit().map_err(Error::from)?; Ok(()) } #[instrument(skip_all)] - async fn get_proofs( - &mut self, - mint_url: Option, - unit: Option, - state: Option>, - spending_conditions: Option>, - ) -> Result, database::Error> { - let txn = self.txn()?; - let table = txn.open_table(PROOFS_TABLE).map_err(Error::from)?; - - let proofs: Vec = table - .iter() - .map_err(Error::from)? - .flatten() - .filter_map(|(_k, v)| { - let mut proof = None; - - if let Ok(proof_info) = serde_json::from_str::(v.value()) { - if proof_info.matches_conditions(&mint_url, &unit, &state, &spending_conditions) - { - proof = Some(proof_info) - } - } - - proof - }) - .collect(); - - Ok(proofs) + async fn remove_mint_quote(&self, quote_id: &str) -> Result<(), database::Error> { + let write_txn = self.db.begin_write().map_err(Error::from)?; + { + let mut table = write_txn + .open_table(MINT_QUOTES_TABLE) + .map_err(Error::from)?; + table.remove(quote_id).map_err(Error::from)?; + } + write_txn.commit().map_err(Error::from)?; + Ok(()) } - #[instrument(skip(self, added, deleted_ys))] - async fn update_proofs( - &mut self, - added: Vec, - deleted_ys: Vec, - ) -> Result<(), database::Error> { - let txn = self.txn()?; - let mut table = txn.open_table(PROOFS_TABLE).map_err(Error::from)?; - - for proof_info in added.iter() { + #[instrument(skip_all)] + async fn add_melt_quote(&self, quote: wallet::MeltQuote) -> Result<(), database::Error> { + let write_txn = self.db.begin_write().map_err(Error::from)?; + { + let mut table = write_txn + .open_table(MELT_QUOTES_TABLE) + .map_err(Error::from)?; table .insert( - proof_info.y.to_bytes().as_slice(), - serde_json::to_string(&proof_info) - .map_err(Error::from)? - .as_str(), + quote.id.as_str(), + serde_json::to_string("e).map_err(Error::from)?.as_str(), ) .map_err(Error::from)?; } + write_txn.commit().map_err(Error::from)?; + Ok(()) + } - for y in deleted_ys.iter() { - table.remove(y.to_bytes().as_slice()).map_err(Error::from)?; + #[instrument(skip_all)] + async fn remove_melt_quote(&self, quote_id: &str) -> Result<(), database::Error> { + let write_txn = self.db.begin_write().map_err(Error::from)?; + { + let mut table = write_txn + .open_table(MELT_QUOTES_TABLE) + .map_err(Error::from)?; + table.remove(quote_id).map_err(Error::from)?; } - + write_txn.commit().map_err(Error::from)?; Ok(()) } - async fn update_proofs_state( - &mut self, - ys: Vec, - state: State, - ) -> Result<(), database::Error> { - let txn = self.txn()?; - let mut table = txn.open_table(PROOFS_TABLE).map_err(Error::from)?; - - for y in ys { - let y_slice = y.to_bytes(); - let proof = table - .get(y_slice.as_slice()) - .map_err(Error::from)? - .ok_or(Error::UnknownY)?; + #[instrument(skip_all)] + async fn add_keys(&self, keyset: KeySet) -> Result<(), database::Error> { + let write_txn = self.db.begin_write().map_err(Error::from)?; - let mut proof_info = - serde_json::from_str::(proof.value()).map_err(Error::from)?; - drop(proof); + keyset.verify_id()?; - proof_info.state = state; + { + let mut table = write_txn.open_table(MINT_KEYS_TABLE).map_err(Error::from)?; - table + let existing_keys = table .insert( - y_slice.as_slice(), - serde_json::to_string(&proof_info) + keyset.id.to_string().as_str(), + serde_json::to_string(&keyset.keys) .map_err(Error::from)? .as_str(), ) + .map_err(Error::from)? + .is_some(); + + let mut table = write_txn + .open_table(KEYSET_U32_MAPPING) + .map_err(Error::from)?; + + let existing = table + .insert(u32::from(keyset.id), keyset.id.to_string().as_str()) .map_err(Error::from)?; - } + let existing_u32 = match existing { + None => false, + Some(id) => { + let id = Id::from_str(id.value())?; + id != keyset.id + } + }; + + if existing_keys || existing_u32 { + tracing::warn!("Keys already exist for keyset id"); + return Err(database::Error::Duplicate); + } + } + write_txn.commit().map_err(Error::from)?; Ok(()) } #[instrument(skip(self), fields(keyset_id = %keyset_id))] - async fn increment_keyset_counter( - &mut self, - keyset_id: &Id, - count: u32, - ) -> Result { - let txn = self.txn()?; - let mut table = txn.open_table(KEYSET_COUNTER).map_err(Error::from)?; - let current_counter = table - .get(keyset_id.to_string().as_str()) - .map_err(Error::from)? - .map(|x| x.value()) - .unwrap_or_default(); - - let new_counter = current_counter + count; - - table - .insert(keyset_id.to_string().as_str(), new_counter) - .map_err(Error::from)?; - - Ok(new_counter) - } + async fn remove_keys(&self, keyset_id: &Id) -> Result<(), database::Error> { + let write_txn = self.db.begin_write().map_err(Error::from)?; + { + let mut table = write_txn.open_table(MINT_KEYS_TABLE).map_err(Error::from)?; - #[instrument(skip(self))] - async fn add_transaction(&mut self, transaction: Transaction) -> Result<(), database::Error> { - let id = transaction.id(); - let txn = self.txn()?; - let mut table = txn.open_table(TRANSACTIONS_TABLE).map_err(Error::from)?; - table - .insert( - id.as_slice(), - serde_json::to_string(&transaction) - .map_err(Error::from)? - .as_str(), - ) - .map_err(Error::from)?; + table + .remove(keyset_id.to_string().as_str()) + .map_err(Error::from)?; + } + write_txn.commit().map_err(Error::from)?; Ok(()) } #[instrument(skip(self))] async fn remove_transaction( - &mut self, + &self, transaction_id: TransactionId, ) -> Result<(), database::Error> { - let txn = self.txn()?; - let mut table = txn.open_table(TRANSACTIONS_TABLE).map_err(Error::from)?; - table - .remove(transaction_id.as_slice()) - .map_err(Error::from)?; + let write_txn = self.db.begin_write().map_err(Error::from)?; + { + let mut table = write_txn + .open_table(TRANSACTIONS_TABLE) + .map_err(Error::from)?; + table + .remove(transaction_id.as_slice()) + .map_err(Error::from)?; + } + write_txn.commit().map_err(Error::from)?; Ok(()) } -} -#[async_trait] -impl KVStoreTransaction for RedbWalletTransaction { - #[instrument(skip_all)] - async fn kv_read( - &mut self, + // KV Store write methods (non-transactional) + + #[instrument(skip(self, value))] + async fn kv_write( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, - ) -> Result>, database::Error> { + value: &[u8], + ) -> Result<(), database::Error> { // Validate parameters according to KV store requirements validate_kvstore_params(primary_namespace, secondary_namespace, Some(key))?; - let txn = self.txn()?; - let table = KvTable::ReadWrite(txn.open_table(KV_STORE_TABLE).map_err(Error::from)?); + let write_txn = self.db.begin_write().map_err(Error::from)?; + { + let mut table = write_txn.open_table(KV_STORE_TABLE).map_err(Error::from)?; + table + .insert((primary_namespace, secondary_namespace, key), value) + .map_err(Error::from)?; + } + write_txn.commit().map_err(Error::from)?; - Ok(table.kv_read(primary_namespace, secondary_namespace, key)?) + Ok(()) } - #[instrument(skip_all)] - async fn kv_write( - &mut self, + #[instrument(skip(self))] + async fn kv_remove( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, - value: &[u8], ) -> Result<(), database::Error> { // Validate parameters according to KV store requirements validate_kvstore_params(primary_namespace, secondary_namespace, Some(key))?; - let txn = self.txn()?; - let mut table = txn.open_table(KV_STORE_TABLE).map_err(Error::from)?; - - table - .insert((primary_namespace, secondary_namespace, key), value) - .map_err(Error::from)?; + let write_txn = self.db.begin_write().map_err(Error::from)?; + { + let mut table = write_txn.open_table(KV_STORE_TABLE).map_err(Error::from)?; + table + .remove((primary_namespace, secondary_namespace, key)) + .map_err(Error::from)?; + } + write_txn.commit().map_err(Error::from)?; Ok(()) } +} + +#[async_trait] +impl KVStoreDatabase for WalletRedbDatabase { + type Err = database::Error; #[instrument(skip_all)] - async fn kv_remove( - &mut self, + async fn kv_read( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, - ) -> Result<(), database::Error> { + ) -> Result>, Self::Err> { // Validate parameters according to KV store requirements validate_kvstore_params(primary_namespace, secondary_namespace, Some(key))?; - let txn = self.txn()?; - let mut table = txn.open_table(KV_STORE_TABLE).map_err(Error::from)?; + let read_txn = self.db.begin_read().map_err(Error::from)?; + let table = read_txn.open_table(KV_STORE_TABLE).map_err(Error::from)?; - table - .remove((primary_namespace, secondary_namespace, key)) - .map_err(Error::from)?; + let result = table + .get((primary_namespace, secondary_namespace, key)) + .map_err(Error::from)? + .map(|v| v.value().to_vec()); - Ok(()) + Ok(result) } #[instrument(skip_all)] async fn kv_list( - &mut self, + &self, primary_namespace: &str, secondary_namespace: &str, - ) -> Result, database::Error> { - // Validate namespace parameters according to KV store requirements + ) -> Result, Self::Err> { validate_kvstore_params(primary_namespace, secondary_namespace, None)?; - let txn = self.txn()?; - let table = KvTable::ReadWrite(txn.open_table(KV_STORE_TABLE).map_err(Error::from)?); - - Ok(table.kv_list(primary_namespace, secondary_namespace)?) - } -} - -#[async_trait] -impl DbTransactionFinalizer for RedbWalletTransaction { - type Err = database::Error; + let read_txn = self.db.begin_read().map_err(Error::from)?; + let table = read_txn.open_table(KV_STORE_TABLE).map_err(Error::from)?; - async fn commit(mut self: Box) -> Result<(), database::Error> { - if let Some(txn) = self.write_txn.take() { - txn.commit().map_err(Error::from)?; - } - Ok(()) - } + let mut keys = Vec::new(); + let start = (primary_namespace, secondary_namespace, ""); - async fn rollback(mut self: Box) -> Result<(), database::Error> { - if let Some(txn) = self.write_txn.take() { - txn.abort().map_err(Error::from)?; + for result in table.range(start..).map_err(Error::from)? { + let (key_tuple, _) = result.map_err(Error::from)?; + let (primary_from_db, secondary_from_db, k) = key_tuple.value(); + if primary_from_db != primary_namespace || secondary_from_db != secondary_namespace { + break; + } + keys.push(k.to_string()); } - Ok(()) - } -} -impl Drop for RedbWalletTransaction { - fn drop(&mut self) { - if let Some(txn) = self.write_txn.take() { - let _ = txn.abort(); - } + Ok(keys) } } diff --git a/crates/cdk-sql-common/src/keyvalue.rs b/crates/cdk-sql-common/src/keyvalue.rs index 78cda7e8c..d9d00be02 100644 --- a/crates/cdk-sql-common/src/keyvalue.rs +++ b/crates/cdk-sql-common/src/keyvalue.rs @@ -9,11 +9,15 @@ use cdk_common::database::{validate_kvstore_params, Error}; use cdk_common::util::unix_time; use crate::column_as_string; +#[cfg(feature = "mint")] use crate::database::ConnectionWithTransaction; -use crate::pool::{DatabasePool, Pool, PooledResource}; +#[cfg(feature = "mint")] +use crate::pool::PooledResource; +use crate::pool::{DatabasePool, Pool}; use crate::stmt::{query, Column}; /// Generic implementation of KVStoreTransaction for SQL databases +#[cfg(feature = "mint")] pub(crate) async fn kv_read_in_transaction( conn: &ConnectionWithTransaction>, primary_namespace: &str, @@ -46,6 +50,7 @@ where } /// Generic implementation of kv_write for transactions +#[cfg(feature = "mint")] pub(crate) async fn kv_write_in_transaction( conn: &ConnectionWithTransaction>, primary_namespace: &str, @@ -85,6 +90,7 @@ where } /// Generic implementation of kv_remove for transactions +#[cfg(feature = "mint")] pub(crate) async fn kv_remove_in_transaction( conn: &ConnectionWithTransaction>, primary_namespace: &str, @@ -114,6 +120,7 @@ where } /// Generic implementation of kv_list for transactions +#[cfg(feature = "mint")] pub(crate) async fn kv_list_in_transaction( conn: &ConnectionWithTransaction>, primary_namespace: &str, @@ -206,3 +213,73 @@ where .map(|row| Ok(column_as_string!(&row[0]))) .collect::, Error>>() } + +/// Generic implementation of kv_write for database (non-transactional, standalone) +#[cfg(feature = "wallet")] +pub(crate) async fn kv_write_standalone( + conn: &C, + primary_namespace: &str, + secondary_namespace: &str, + key: &str, + value: &[u8], +) -> Result<(), Error> +where + C: crate::database::DatabaseExecutor, +{ + // Validate parameters according to KV store requirements + validate_kvstore_params(primary_namespace, secondary_namespace, Some(key))?; + + let current_time = unix_time(); + + query( + r#" + INSERT INTO kv_store + (primary_namespace, secondary_namespace, key, value, created_time, updated_time) + VALUES (:primary_namespace, :secondary_namespace, :key, :value, :created_time, :updated_time) + ON CONFLICT(primary_namespace, secondary_namespace, key) + DO UPDATE SET + value = excluded.value, + updated_time = excluded.updated_time + "#, + )? + .bind("primary_namespace", primary_namespace.to_owned()) + .bind("secondary_namespace", secondary_namespace.to_owned()) + .bind("key", key.to_owned()) + .bind("value", value.to_vec()) + .bind("created_time", current_time as i64) + .bind("updated_time", current_time as i64) + .execute(conn) + .await?; + + Ok(()) +} + +/// Generic implementation of kv_remove for database (non-transactional, standalone) +#[cfg(feature = "wallet")] +pub(crate) async fn kv_remove_standalone( + conn: &C, + primary_namespace: &str, + secondary_namespace: &str, + key: &str, +) -> Result<(), Error> +where + C: crate::database::DatabaseExecutor, +{ + // Validate parameters according to KV store requirements + validate_kvstore_params(primary_namespace, secondary_namespace, Some(key))?; + query( + r#" + DELETE FROM kv_store + WHERE primary_namespace = :primary_namespace + AND secondary_namespace = :secondary_namespace + AND key = :key + "#, + )? + .bind("primary_namespace", primary_namespace.to_owned()) + .bind("secondary_namespace", secondary_namespace.to_owned()) + .bind("key", key.to_owned()) + .execute(conn) + .await?; + + Ok(()) +} diff --git a/crates/cdk-sql-common/src/wallet/mod.rs b/crates/cdk-sql-common/src/wallet/mod.rs index fecfa1410..ad327e6d9 100644 --- a/crates/cdk-sql-common/src/wallet/mod.rs +++ b/crates/cdk-sql-common/src/wallet/mod.rs @@ -7,9 +7,7 @@ use std::sync::Arc; use async_trait::async_trait; use cdk_common::common::ProofInfo; -use cdk_common::database::{ - ConversionError, DbTransactionFinalizer, Error, WalletDatabase, WalletDatabaseTransaction, -}; +use cdk_common::database::{ConversionError, Error, WalletDatabase}; use cdk_common::mint_url::MintUrl; use cdk_common::nuts::{MeltQuoteState, MintQuoteState}; use cdk_common::secret::Secret; @@ -43,441 +41,715 @@ where pool: Arc>, } -/// SQL Transaction Writer -#[allow(missing_debug_implementations)] -pub struct SQLWalletTransaction +// Inline helper functions that work with both connections and transactions +#[inline] +async fn get_keyset_by_id_inner( + executor: &T, + keyset_id: &Id, + for_update: bool, +) -> Result, Error> where - RM: DatabasePool + 'static, + T: DatabaseExecutor, { - inner: ConnectionWithTransaction>, + let for_update_clause = if for_update { "FOR UPDATE" } else { "" }; + let query_str = format!( + r#" + SELECT + id, + unit, + active, + input_fee_ppk, + final_expiry + FROM + keyset + WHERE id = :id + {for_update_clause} + "# + ); + + query(&query_str)? + .bind("id", keyset_id.to_string()) + .fetch_one(executor) + .await? + .map(sql_row_to_keyset) + .transpose() } -#[async_trait] -impl WalletDatabaseTransaction for SQLWalletTransaction +#[inline] +async fn get_keys_inner(executor: &T, id: &Id) -> Result, Error> where - RM: DatabasePool + 'static, + T: DatabaseExecutor, { - #[instrument(skip(self, mint_info))] - async fn add_mint( - &mut self, - mint_url: MintUrl, - mint_info: Option, - ) -> Result<(), Error> { - let ( - name, - pubkey, - version, - description, - description_long, - contact, - nuts, - icon_url, - urls, - motd, - time, - tos_url, - ) = match mint_info { - Some(mint_info) => { - let MintInfo { - name, - pubkey, - version, - description, - description_long, - contact, - nuts, - icon_url, - urls, - motd, - time, - tos_url, - } = mint_info; - - ( - name, - pubkey.map(|p| p.to_bytes().to_vec()), - version.map(|v| serde_json::to_string(&v).ok()), - description, - description_long, - contact.map(|c| serde_json::to_string(&c).ok()), - serde_json::to_string(&nuts).ok(), - icon_url, - urls.map(|c| serde_json::to_string(&c).ok()), - motd, - time, - tos_url, - ) - } - None => ( - None, None, None, None, None, None, None, None, None, None, None, None, - ), - }; - - query( - r#" - INSERT INTO mint - ( - mint_url, name, pubkey, version, description, description_long, - contact, nuts, icon_url, urls, motd, mint_time, tos_url - ) - VALUES - ( - :mint_url, :name, :pubkey, :version, :description, :description_long, - :contact, :nuts, :icon_url, :urls, :motd, :mint_time, :tos_url - ) - ON CONFLICT(mint_url) DO UPDATE SET - name = excluded.name, - pubkey = excluded.pubkey, - version = excluded.version, - description = excluded.description, - description_long = excluded.description_long, - contact = excluded.contact, - nuts = excluded.nuts, - icon_url = excluded.icon_url, - urls = excluded.urls, - motd = excluded.motd, - mint_time = excluded.mint_time, - tos_url = excluded.tos_url - ; - "#, - )? - .bind("mint_url", mint_url.to_string()) - .bind("name", name) - .bind("pubkey", pubkey) - .bind("version", version) - .bind("description", description) - .bind("description_long", description_long) - .bind("contact", contact) - .bind("nuts", nuts) - .bind("icon_url", icon_url) - .bind("urls", urls) - .bind("motd", motd) - .bind("mint_time", time.map(|v| v as i64)) - .bind("tos_url", tos_url) - .execute(&self.inner) - .await?; - - Ok(()) - } - - #[instrument(skip(self))] - async fn remove_mint(&mut self, mint_url: MintUrl) -> Result<(), Error> { - query(r#"DELETE FROM mint WHERE mint_url=:mint_url"#)? - .bind("mint_url", mint_url.to_string()) - .execute(&self.inner) - .await?; + query( + r#" + SELECT + keys + FROM key + WHERE id = :id + "#, + )? + .bind("id", id.to_string()) + .pluck(executor) + .await? + .map(|keys| { + let keys = column_as_string!(keys); + serde_json::from_str(&keys).map_err(Error::from) + }) + .transpose() +} - Ok(()) - } +#[inline] +async fn get_mint_quote_inner( + executor: &T, + quote_id: &str, + for_update: bool, +) -> Result, Error> +where + T: DatabaseExecutor, +{ + let for_update_clause = if for_update { "FOR UPDATE" } else { "" }; + let query_str = format!( + r#" + SELECT + id, + mint_url, + amount, + unit, + request, + state, + expiry, + secret_key, + payment_method, + amount_issued, + amount_paid + FROM + mint_quote + WHERE + id = :id + {for_update_clause} + "# + ); - #[instrument(skip(self))] - async fn update_mint_url( - &mut self, - old_mint_url: MintUrl, - new_mint_url: MintUrl, - ) -> Result<(), Error> { - let tables = ["mint_quote", "proof"]; + query(&query_str)? + .bind("id", quote_id.to_string()) + .fetch_one(executor) + .await? + .map(sql_row_to_mint_quote) + .transpose() +} - for table in &tables { - query(&format!( - r#" - UPDATE {table} - SET mint_url = :new_mint_url - WHERE mint_url = :old_mint_url - "# - ))? - .bind("new_mint_url", new_mint_url.to_string()) - .bind("old_mint_url", old_mint_url.to_string()) - .execute(&self.inner) - .await?; - } +#[inline] +async fn get_melt_quote_inner( + executor: &T, + quote_id: &str, + for_update: bool, +) -> Result, Error> +where + T: DatabaseExecutor, +{ + let for_update_clause = if for_update { "FOR UPDATE" } else { "" }; + let query_str = format!( + r#" + SELECT + id, + unit, + amount, + request, + fee_reserve, + state, + expiry, + payment_preimage, + payment_method + FROM + melt_quote + WHERE + id=:id + {for_update_clause} + "# + ); - Ok(()) - } + query(&query_str)? + .bind("id", quote_id.to_owned()) + .fetch_one(executor) + .await? + .map(sql_row_to_melt_quote) + .transpose() +} - #[instrument(skip(self, keysets))] - async fn add_mint_keysets( - &mut self, - mint_url: MintUrl, - keysets: Vec, - ) -> Result<(), Error> { - for keyset in keysets { - query( - r#" - INSERT INTO keyset - (mint_url, id, unit, active, input_fee_ppk, final_expiry, keyset_u32) - VALUES - (:mint_url, :id, :unit, :active, :input_fee_ppk, :final_expiry, :keyset_u32) - ON CONFLICT(id) DO UPDATE SET - active = excluded.active, - input_fee_ppk = excluded.input_fee_ppk - "#, - )? - .bind("mint_url", mint_url.to_string()) - .bind("id", keyset.id.to_string()) - .bind("unit", keyset.unit.to_string()) - .bind("active", keyset.active) - .bind("input_fee_ppk", keyset.input_fee_ppk as i64) - .bind("final_expiry", keyset.final_expiry.map(|v| v as i64)) - .bind("keyset_u32", u32::from(keyset.id)) - .execute(&self.inner) - .await?; - } +#[inline] +async fn get_proofs_inner( + executor: &T, + mint_url: Option, + unit: Option, + state: Option>, + spending_conditions: Option>, + for_update: bool, +) -> Result, Error> +where + T: DatabaseExecutor, +{ + let for_update_clause = if for_update { "FOR UPDATE" } else { "" }; + let query_str = format!( + r#" + SELECT + amount, + unit, + keyset_id, + secret, + c, + witness, + dleq_e, + dleq_s, + dleq_r, + y, + mint_url, + state, + spending_condition + FROM proof + {for_update_clause} + "# + ); - Ok(()) - } + Ok(query(&query_str)? + .fetch_all(executor) + .await? + .into_iter() + .filter_map(|row| { + let row = sql_row_to_proof_info(row).ok()?; - #[instrument(skip_all)] - async fn add_mint_quote(&mut self, quote: MintQuote) -> Result<(), Error> { - query( - r#" - INSERT INTO mint_quote - (id, mint_url, amount, unit, request, state, expiry, secret_key, payment_method, amount_issued, amount_paid) - VALUES - (:id, :mint_url, :amount, :unit, :request, :state, :expiry, :secret_key, :payment_method, :amount_issued, :amount_paid) - ON CONFLICT(id) DO UPDATE SET - mint_url = excluded.mint_url, - amount = excluded.amount, - unit = excluded.unit, - request = excluded.request, - state = excluded.state, - expiry = excluded.expiry, - secret_key = excluded.secret_key, - payment_method = excluded.payment_method, - amount_issued = excluded.amount_issued, - amount_paid = excluded.amount_paid - ; - "#, - )? - .bind("id", quote.id.to_string()) - .bind("mint_url", quote.mint_url.to_string()) - .bind("amount", quote.amount.map(|a| a.to_i64())) - .bind("unit", quote.unit.to_string()) - .bind("request", quote.request) - .bind("state", quote.state.to_string()) - .bind("expiry", quote.expiry as i64) - .bind("secret_key", quote.secret_key.map(|p| p.to_string())) - .bind("payment_method", quote.payment_method.to_string()) - .bind("amount_issued", quote.amount_issued.to_i64()) - .bind("amount_paid", quote.amount_paid.to_i64()) - .execute(&self.inner).await?; + if row.matches_conditions(&mint_url, &unit, &state, &spending_conditions) { + Some(row) + } else { + None + } + }) + .collect::>()) +} - Ok(()) +impl SQLWalletDatabase +where + RM: DatabasePool + 'static, +{ + /// Creates a new instance + pub async fn new(db: X) -> Result + where + X: Into, + { + let pool = Pool::new(db.into()); + Self::migrate(pool.get().map_err(|e| Error::Database(Box::new(e)))?).await?; + + Ok(Self { pool }) } - #[instrument(skip(self))] - async fn remove_mint_quote(&mut self, quote_id: &str) -> Result<(), Error> { - query(r#"DELETE FROM mint_quote WHERE id=:id"#)? - .bind("id", quote_id.to_string()) - .execute(&self.inner) - .await?; + /// Migrate [`WalletSqliteDatabase`] + async fn migrate(conn: PooledResource) -> Result<(), Error> { + let tx = ConnectionWithTransaction::new(conn).await?; + migrate(&tx, RM::Connection::name(), migrations::MIGRATIONS).await?; + // Update any existing keys with missing keyset_u32 values + Self::add_keyset_u32(&tx).await?; + tx.commit().await?; Ok(()) } - #[instrument(skip_all)] - async fn add_melt_quote(&mut self, quote: wallet::MeltQuote) -> Result<(), Error> { - query( + async fn add_keyset_u32(conn: &T) -> Result<(), Error> + where + T: DatabaseExecutor, + { + // First get the keysets where keyset_u32 on key is null + let keys_without_u32: Vec> = query( r#" - INSERT INTO melt_quote - (id, unit, amount, request, fee_reserve, state, expiry, payment_method) - VALUES - (:id, :unit, :amount, :request, :fee_reserve, :state, :expiry, :payment_method) - ON CONFLICT(id) DO UPDATE SET - unit = excluded.unit, - amount = excluded.amount, - request = excluded.request, - fee_reserve = excluded.fee_reserve, - state = excluded.state, - expiry = excluded.expiry, - payment_method = excluded.payment_method - ; - "#, + SELECT + id + FROM key + WHERE keyset_u32 IS NULL + "#, )? - .bind("id", quote.id.to_string()) - .bind("unit", quote.unit.to_string()) - .bind("amount", u64::from(quote.amount) as i64) - .bind("request", quote.request) - .bind("fee_reserve", u64::from(quote.fee_reserve) as i64) - .bind("state", quote.state.to_string()) - .bind("expiry", quote.expiry as i64) - .bind("payment_method", quote.payment_method.to_string()) - .execute(&self.inner) + .fetch_all(conn) .await?; - Ok(()) - } - - #[instrument(skip(self))] - async fn remove_melt_quote(&mut self, quote_id: &str) -> Result<(), Error> { - query(r#"DELETE FROM melt_quote WHERE id=:id"#)? - .bind("id", quote_id.to_owned()) - .execute(&self.inner) - .await?; - - Ok(()) - } + for row in keys_without_u32 { + unpack_into!(let (id) = row); + let id = column_as_string!(id); - #[instrument(skip_all)] - async fn add_keys(&mut self, keyset: KeySet) -> Result<(), Error> { - // Recompute ID for verification - keyset.verify_id()?; + if let Ok(id) = Id::from_str(&id) { + query( + r#" + UPDATE + key + SET keyset_u32 = :u32_keyset + WHERE id = :keyset_id + "#, + )? + .bind("u32_keyset", u32::from(id)) + .bind("keyset_id", id.to_string()) + .execute(conn) + .await?; + } + } - query( + // Also update keysets where keyset_u32 is null + let keysets_without_u32: Vec> = query( r#" - INSERT INTO key - (id, keys, keyset_u32) - VALUES - (:id, :keys, :keyset_u32) + SELECT + id + FROM keyset + WHERE keyset_u32 IS NULL "#, )? - .bind("id", keyset.id.to_string()) - .bind( - "keys", - serde_json::to_string(&keyset.keys).map_err(Error::from)?, - ) - .bind("keyset_u32", u32::from(keyset.id)) - .execute(&self.inner) + .fetch_all(conn) .await?; + for row in keysets_without_u32 { + unpack_into!(let (id) = row); + let id = column_as_string!(id); + + if let Ok(id) = Id::from_str(&id) { + query( + r#" + UPDATE + keyset + SET keyset_u32 = :u32_keyset + WHERE id = :keyset_id + "#, + )? + .bind("u32_keyset", u32::from(id)) + .bind("keyset_id", id.to_string()) + .execute(conn) + .await?; + } + } + Ok(()) } +} +#[async_trait] +impl WalletDatabase for SQLWalletDatabase +where + RM: DatabasePool + 'static, +{ #[instrument(skip(self))] - async fn add_transaction(&mut self, transaction: Transaction) -> Result<(), Error> { - let mint_url = transaction.mint_url.to_string(); - let direction = transaction.direction.to_string(); - let unit = transaction.unit.to_string(); - let amount = u64::from(transaction.amount) as i64; - let fee = u64::from(transaction.fee) as i64; - let ys = transaction - .ys - .iter() - .flat_map(|y| y.to_bytes().to_vec()) - .collect::>(); - - let id = transaction.id(); + async fn get_melt_quotes(&self) -> Result, database::Error> { + let conn = self.pool.get().map_err(|e| Error::Database(Box::new(e)))?; - query( - r#" - INSERT INTO transactions - (id, mint_url, direction, unit, amount, fee, ys, timestamp, memo, metadata, quote_id, payment_request, payment_proof, payment_method) - VALUES - (:id, :mint_url, :direction, :unit, :amount, :fee, :ys, :timestamp, :memo, :metadata, :quote_id, :payment_request, :payment_proof, :payment_method) - ON CONFLICT(id) DO UPDATE SET - mint_url = excluded.mint_url, - direction = excluded.direction, - unit = excluded.unit, - amount = excluded.amount, - fee = excluded.fee, - timestamp = excluded.timestamp, - memo = excluded.memo, - metadata = excluded.metadata, - quote_id = excluded.quote_id, - payment_request = excluded.payment_request, - payment_proof = excluded.payment_proof, - payment_method = excluded.payment_method - ; - "#, - )? - .bind("id", id.as_slice().to_vec()) - .bind("mint_url", mint_url) - .bind("direction", direction) - .bind("unit", unit) - .bind("amount", amount) - .bind("fee", fee) - .bind("ys", ys) - .bind("timestamp", transaction.timestamp as i64) - .bind("memo", transaction.memo) - .bind( - "metadata", - serde_json::to_string(&transaction.metadata).map_err(Error::from)?, - ) - .bind("quote_id", transaction.quote_id) - .bind("payment_request", transaction.payment_request) - .bind("payment_proof", transaction.payment_proof) - .bind("payment_method", transaction.payment_method.map(|pm| pm.to_string())) - .execute(&self.inner) - .await?; - - Ok(()) + Ok(query( + r#" + SELECT + id, + unit, + amount, + request, + fee_reserve, + state, + expiry, + payment_preimage, + payment_method + FROM + melt_quote + "#, + )? + .fetch_all(&*conn) + .await? + .into_iter() + .map(sql_row_to_melt_quote) + .collect::>()?) } #[instrument(skip(self))] - async fn remove_transaction(&mut self, transaction_id: TransactionId) -> Result<(), Error> { - query(r#"DELETE FROM transactions WHERE id=:id"#)? - .bind("id", transaction_id.as_slice().to_vec()) - .execute(&self.inner) - .await?; - - Ok(()) + async fn get_mint(&self, mint_url: MintUrl) -> Result, database::Error> { + let conn = self.pool.get().map_err(|e| Error::Database(Box::new(e)))?; + Ok(query( + r#" + SELECT + name, + pubkey, + version, + description, + description_long, + contact, + nuts, + icon_url, + motd, + urls, + mint_time, + tos_url + FROM + mint + WHERE mint_url = :mint_url + "#, + )? + .bind("mint_url", mint_url.to_string()) + .fetch_one(&*conn) + .await? + .map(sql_row_to_mint_info) + .transpose()?) } - #[instrument(skip(self), fields(keyset_id = %keyset_id))] - async fn increment_keyset_counter(&mut self, keyset_id: &Id, count: u32) -> Result { - // Lock the row and get current counter from keyset_counter table - let current_counter = query( + #[instrument(skip(self))] + async fn get_mints(&self) -> Result>, database::Error> { + let conn = self.pool.get().map_err(|e| Error::Database(Box::new(e)))?; + Ok(query( r#" - SELECT counter - FROM keyset_counter - WHERE keyset_id=:keyset_id - FOR UPDATE - "#, + SELECT + name, + pubkey, + version, + description, + description_long, + contact, + nuts, + icon_url, + motd, + urls, + mint_time, + tos_url, + mint_url + FROM + mint + "#, )? - .bind("keyset_id", keyset_id.to_string()) - .pluck(&self.inner) + .fetch_all(&*conn) .await? - .map(|n| Ok::<_, Error>(column_as_number!(n))) - .transpose()? - .unwrap_or(0); + .into_iter() + .map(|mut row| { + let url = column_as_string!( + row.pop().ok_or(ConversionError::MissingColumn(0, 1))?, + MintUrl::from_str + ); - let new_counter = current_counter + count; + Ok((url, sql_row_to_mint_info(row).ok())) + }) + .collect::, Error>>()?) + } - // Upsert the new counter value - query( + #[instrument(skip(self))] + async fn get_mint_keysets( + &self, + mint_url: MintUrl, + ) -> Result>, database::Error> { + let conn = self.pool.get().map_err(|e| Error::Database(Box::new(e)))?; + + let keysets = query( r#" - INSERT INTO keyset_counter (keyset_id, counter) - VALUES (:keyset_id, :new_counter) - ON CONFLICT(keyset_id) DO UPDATE SET - counter = excluded.counter - "#, + SELECT + id, + unit, + active, + input_fee_ppk, + final_expiry + FROM + keyset + WHERE mint_url = :mint_url + "#, )? - .bind("keyset_id", keyset_id.to_string()) - .bind("new_counter", new_counter) - .execute(&self.inner) - .await?; + .bind("mint_url", mint_url.to_string()) + .fetch_all(&*conn) + .await? + .into_iter() + .map(sql_row_to_keyset) + .collect::, Error>>()?; - Ok(new_counter) + match keysets.is_empty() { + false => Ok(Some(keysets)), + true => Ok(None), + } } - #[instrument(skip(self))] - async fn remove_keys(&mut self, id: &Id) -> Result<(), Error> { - query(r#"DELETE FROM key WHERE id = :id"#)? - .bind("id", id.to_string()) - .pluck(&self.inner) - .await?; + #[instrument(skip(self), fields(keyset_id = %keyset_id))] + async fn get_keyset_by_id( + &self, + keyset_id: &Id, + ) -> Result, database::Error> { + let conn = self.pool.get().map_err(|e| Error::Database(Box::new(e)))?; + get_keyset_by_id_inner(&*conn, keyset_id, false).await + } - Ok(()) + #[instrument(skip(self))] + async fn get_mint_quote(&self, quote_id: &str) -> Result, database::Error> { + let conn = self.pool.get().map_err(|e| Error::Database(Box::new(e)))?; + get_mint_quote_inner(&*conn, quote_id, false).await } - async fn update_proofs_state(&mut self, ys: Vec, state: State) -> Result<(), Error> { - query("UPDATE proof SET state = :state WHERE y IN (:ys)")? - .bind_vec("ys", ys.iter().map(|y| y.to_bytes().to_vec()).collect()) - .bind("state", state.to_string()) - .execute(&self.inner) - .await?; + #[instrument(skip(self))] + async fn get_mint_quotes(&self) -> Result, database::Error> { + let conn = self.pool.get().map_err(|e| Error::Database(Box::new(e)))?; + Ok(query( + r#" + SELECT + id, + mint_url, + amount, + unit, + request, + state, + expiry, + secret_key, + payment_method, + amount_issued, + amount_paid + FROM + mint_quote + "#, + )? + .fetch_all(&*conn) + .await? + .into_iter() + .map(sql_row_to_mint_quote) + .collect::>()?) + } - Ok(()) + #[instrument(skip(self))] + async fn get_unissued_mint_quotes(&self) -> Result, Self::Err> { + let conn = self.pool.get().map_err(|e| Error::Database(Box::new(e)))?; + Ok(query( + r#" + SELECT + id, + mint_url, + amount, + unit, + request, + state, + expiry, + secret_key, + payment_method, + amount_issued, + amount_paid + FROM + mint_quote + WHERE + amount_issued = 0 + OR + payment_method = 'bolt12' + "#, + )? + .fetch_all(&*conn) + .await? + .into_iter() + .map(sql_row_to_mint_quote) + .collect::>()?) } - async fn update_proofs( - &mut self, - added: Vec, - removed_ys: Vec, - ) -> Result<(), Error> { - // TODO: Use a transaction for all these operations - for proof in added { - query( + #[instrument(skip(self))] + async fn get_melt_quote( + &self, + quote_id: &str, + ) -> Result, database::Error> { + let conn = self.pool.get().map_err(|e| Error::Database(Box::new(e)))?; + get_melt_quote_inner(&*conn, quote_id, false).await + } + + #[instrument(skip(self), fields(keyset_id = %keyset_id))] + async fn get_keys(&self, keyset_id: &Id) -> Result, database::Error> { + let conn = self.pool.get().map_err(|e| Error::Database(Box::new(e)))?; + get_keys_inner(&*conn, keyset_id).await + } + + #[instrument(skip(self, state, spending_conditions))] + async fn get_proofs( + &self, + mint_url: Option, + unit: Option, + state: Option>, + spending_conditions: Option>, + ) -> Result, database::Error> { + let conn = self.pool.get().map_err(|e| Error::Database(Box::new(e)))?; + get_proofs_inner(&*conn, mint_url, unit, state, spending_conditions, false).await + } + + #[instrument(skip(self, ys))] + async fn get_proofs_by_ys( + &self, + ys: Vec, + ) -> Result, database::Error> { + if ys.is_empty() { + return Ok(Vec::new()); + } + + let conn = self.pool.get().map_err(|e| Error::Database(Box::new(e)))?; + Ok(query( + r#" + SELECT + amount, + unit, + keyset_id, + secret, + c, + witness, + dleq_e, + dleq_s, + dleq_r, + y, + mint_url, + state, + spending_condition + FROM proof + WHERE y IN (:ys) + "#, + )? + .bind_vec("ys", ys.iter().map(|y| y.to_bytes().to_vec()).collect()) + .fetch_all(&*conn) + .await? + .into_iter() + .filter_map(|row| sql_row_to_proof_info(row).ok()) + .collect::>()) + } + + async fn get_balance( + &self, + mint_url: Option, + unit: Option, + states: Option>, + ) -> Result { + let conn = self.pool.get().map_err(|e| Error::Database(Box::new(e)))?; + + let mut query_str = "SELECT COALESCE(SUM(amount), 0) as total FROM proof".to_string(); + let mut where_clauses = Vec::new(); + let states = states + .unwrap_or_default() + .into_iter() + .map(|x| x.to_string()) + .collect::>(); + + if mint_url.is_some() { + where_clauses.push("mint_url = :mint_url"); + } + if unit.is_some() { + where_clauses.push("unit = :unit"); + } + if !states.is_empty() { + where_clauses.push("state IN (:states)"); + } + + if !where_clauses.is_empty() { + query_str.push_str(" WHERE "); + query_str.push_str(&where_clauses.join(" AND ")); + } + + let mut q = query(&query_str)?; + + if let Some(ref mint_url) = mint_url { + q = q.bind("mint_url", mint_url.to_string()); + } + if let Some(ref unit) = unit { + q = q.bind("unit", unit.to_string()); + } + + if !states.is_empty() { + q = q.bind_vec("states", states); + } + + let balance = q + .pluck(&*conn) + .await? + .map(|n| { + // SQLite SUM returns INTEGER which we need to convert to u64 + match n { + crate::stmt::Column::Integer(i) => Ok(i as u64), + crate::stmt::Column::Real(f) => Ok(f as u64), + _ => Err(Error::Database(Box::new(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "Invalid balance type", + )))), + } + }) + .transpose()? + .unwrap_or(0); + + Ok(balance) + } + + #[instrument(skip(self))] + async fn get_transaction( + &self, + transaction_id: TransactionId, + ) -> Result, database::Error> { + let conn = self.pool.get().map_err(|e| Error::Database(Box::new(e)))?; + Ok(query( + r#" + SELECT + mint_url, + direction, + unit, + amount, + fee, + ys, + timestamp, + memo, + metadata, + quote_id, + payment_request, + payment_proof, + payment_method + FROM + transactions + WHERE + id = :id + "#, + )? + .bind("id", transaction_id.as_slice().to_vec()) + .fetch_one(&*conn) + .await? + .map(sql_row_to_transaction) + .transpose()?) + } + + #[instrument(skip(self))] + async fn list_transactions( + &self, + mint_url: Option, + direction: Option, + unit: Option, + ) -> Result, database::Error> { + let conn = self.pool.get().map_err(|e| Error::Database(Box::new(e)))?; + + Ok(query( + r#" + SELECT + mint_url, + direction, + unit, + amount, + fee, + ys, + timestamp, + memo, + metadata, + quote_id, + payment_request, + payment_proof, + payment_method + FROM + transactions + "#, + )? + .fetch_all(&*conn) + .await? + .into_iter() + .filter_map(|row| { + // TODO: Avoid a table scan by passing the heavy lifting of checking to the DB engine + let transaction = sql_row_to_transaction(row).ok()?; + if transaction.matches_conditions(&mint_url, &direction, &unit) { + Some(transaction) + } else { + None + } + }) + .collect::>()) + } + + #[instrument(skip(self))] + async fn update_proofs( + &self, + added: Vec, + removed_ys: Vec, + ) -> Result<(), database::Error> { + let conn = self.pool.get().map_err(|e| Error::Database(Box::new(e)))?; + let tx = ConnectionWithTransaction::new(conn).await?; + + for proof in added { + query( r#" INSERT INTO proof (y, mint_url, state, spending_condition, unit, amount, keyset_id, secret, c, witness, dleq_e, dleq_s, dleq_r) @@ -501,7 +773,7 @@ where )? .bind("y", proof.y.to_bytes().to_vec()) .bind("mint_url", proof.mint_url.to_string()) - .bind("state",proof.state.to_string()) + .bind("state", proof.state.to_string()) .bind( "spending_condition", proof @@ -532,785 +804,514 @@ where "dleq_r", proof.proof.dleq.as_ref().map(|dleq| dleq.r.to_secret_bytes().to_vec()), ) - .execute(&self.inner).await?; + .execute(&tx) + .await?; } + if !removed_ys.is_empty() { query(r#"DELETE FROM proof WHERE y IN (:ys)"#)? .bind_vec( "ys", removed_ys.iter().map(|y| y.to_bytes().to_vec()).collect(), ) - .execute(&self.inner) + .execute(&tx) .await?; } - Ok(()) - } - - #[instrument(skip(self), fields(keyset_id = %keyset_id))] - async fn get_keyset_by_id(&mut self, keyset_id: &Id) -> Result, Error> { - get_keyset_by_id_inner(&self.inner, keyset_id, true).await - } + tx.commit().await?; - #[instrument(skip(self), fields(keyset_id = %id))] - async fn get_keys(&mut self, id: &Id) -> Result, Error> { - get_keys_inner(&self.inner, id).await + Ok(()) } #[instrument(skip(self))] - async fn get_mint_quote(&mut self, quote_id: &str) -> Result, Error> { - get_mint_quote_inner(&self.inner, quote_id, true).await - } + async fn update_proofs_state( + &self, + ys: Vec, + state: State, + ) -> Result<(), database::Error> { + let conn = self.pool.get().map_err(|e| Error::Database(Box::new(e)))?; - #[instrument(skip(self))] - async fn get_melt_quote(&mut self, quote_id: &str) -> Result, Error> { - get_melt_quote_inner(&self.inner, quote_id, true).await - } + query("UPDATE proof SET state = :state WHERE y IN (:ys)")? + .bind_vec("ys", ys.iter().map(|y| y.to_bytes().to_vec()).collect()) + .bind("state", state.to_string()) + .execute(&*conn) + .await?; - #[instrument(skip(self, state, spending_conditions))] - async fn get_proofs( - &mut self, - mint_url: Option, - unit: Option, - state: Option>, - spending_conditions: Option>, - ) -> Result, Error> { - get_proofs_inner( - &self.inner, - mint_url, - unit, - state, - spending_conditions, - true, - ) - .await + Ok(()) } -} -#[async_trait] -impl DbTransactionFinalizer for SQLWalletTransaction -where - RM: DatabasePool + 'static, -{ - type Err = Error; + #[instrument(skip(self))] + async fn add_transaction(&self, transaction: Transaction) -> Result<(), database::Error> { + let conn = self.pool.get().map_err(|e| Error::Database(Box::new(e)))?; - async fn commit(self: Box) -> Result<(), Error> { - Ok(self.inner.commit().await?) - } - - async fn rollback(self: Box) -> Result<(), Error> { - Ok(self.inner.rollback().await?) - } -} - -// Inline helper functions that work with both connections and transactions -#[inline] -async fn get_keyset_by_id_inner( - executor: &T, - keyset_id: &Id, - for_update: bool, -) -> Result, Error> -where - T: DatabaseExecutor, -{ - let for_update_clause = if for_update { "FOR UPDATE" } else { "" }; - let query_str = format!( - r#" - SELECT - id, - unit, - active, - input_fee_ppk, - final_expiry - FROM - keyset - WHERE id = :id - {for_update_clause} - "# - ); - - query(&query_str)? - .bind("id", keyset_id.to_string()) - .fetch_one(executor) - .await? - .map(sql_row_to_keyset) - .transpose() -} - -#[inline] -async fn get_keys_inner(executor: &T, id: &Id) -> Result, Error> -where - T: DatabaseExecutor, -{ - query( - r#" - SELECT - keys - FROM key - WHERE id = :id - "#, - )? - .bind("id", id.to_string()) - .pluck(executor) - .await? - .map(|keys| { - let keys = column_as_string!(keys); - serde_json::from_str(&keys).map_err(Error::from) - }) - .transpose() -} - -#[inline] -async fn get_mint_quote_inner( - executor: &T, - quote_id: &str, - for_update: bool, -) -> Result, Error> -where - T: DatabaseExecutor, -{ - let for_update_clause = if for_update { "FOR UPDATE" } else { "" }; - let query_str = format!( - r#" - SELECT - id, - mint_url, - amount, - unit, - request, - state, - expiry, - secret_key, - payment_method, - amount_issued, - amount_paid - FROM - mint_quote - WHERE - id = :id - {for_update_clause} - "# - ); - - query(&query_str)? - .bind("id", quote_id.to_string()) - .fetch_one(executor) - .await? - .map(sql_row_to_mint_quote) - .transpose() -} - -#[inline] -async fn get_melt_quote_inner( - executor: &T, - quote_id: &str, - for_update: bool, -) -> Result, Error> -where - T: DatabaseExecutor, -{ - let for_update_clause = if for_update { "FOR UPDATE" } else { "" }; - let query_str = format!( - r#" - SELECT - id, - unit, - amount, - request, - fee_reserve, - state, - expiry, - payment_preimage, - payment_method - FROM - melt_quote - WHERE - id=:id - {for_update_clause} - "# - ); - - query(&query_str)? - .bind("id", quote_id.to_owned()) - .fetch_one(executor) - .await? - .map(sql_row_to_melt_quote) - .transpose() -} + let mint_url = transaction.mint_url.to_string(); + let direction = transaction.direction.to_string(); + let unit = transaction.unit.to_string(); + let amount = u64::from(transaction.amount) as i64; + let fee = u64::from(transaction.fee) as i64; + let ys = transaction + .ys + .iter() + .flat_map(|y| y.to_bytes().to_vec()) + .collect::>(); -#[inline] -async fn get_proofs_inner( - executor: &T, - mint_url: Option, - unit: Option, - state: Option>, - spending_conditions: Option>, - for_update: bool, -) -> Result, Error> -where - T: DatabaseExecutor, -{ - let for_update_clause = if for_update { "FOR UPDATE" } else { "" }; - let query_str = format!( - r#" - SELECT - amount, - unit, - keyset_id, - secret, - c, - witness, - dleq_e, - dleq_s, - dleq_r, - y, - mint_url, - state, - spending_condition - FROM proof - {for_update_clause} - "# - ); + let id = transaction.id(); - Ok(query(&query_str)? - .fetch_all(executor) - .await? - .into_iter() - .filter_map(|row| { - let row = sql_row_to_proof_info(row).ok()?; + query( + r#" + INSERT INTO transactions + (id, mint_url, direction, unit, amount, fee, ys, timestamp, memo, metadata, quote_id, payment_request, payment_proof, payment_method) + VALUES + (:id, :mint_url, :direction, :unit, :amount, :fee, :ys, :timestamp, :memo, :metadata, :quote_id, :payment_request, :payment_proof, :payment_method) + ON CONFLICT(id) DO UPDATE SET + mint_url = excluded.mint_url, + direction = excluded.direction, + unit = excluded.unit, + amount = excluded.amount, + fee = excluded.fee, + timestamp = excluded.timestamp, + memo = excluded.memo, + metadata = excluded.metadata, + quote_id = excluded.quote_id, + payment_request = excluded.payment_request, + payment_proof = excluded.payment_proof, + payment_method = excluded.payment_method + ; + "#, + )? + .bind("id", id.as_slice().to_vec()) + .bind("mint_url", mint_url) + .bind("direction", direction) + .bind("unit", unit) + .bind("amount", amount) + .bind("fee", fee) + .bind("ys", ys) + .bind("timestamp", transaction.timestamp as i64) + .bind("memo", transaction.memo) + .bind( + "metadata", + serde_json::to_string(&transaction.metadata).map_err(Error::from)?, + ) + .bind("quote_id", transaction.quote_id) + .bind("payment_request", transaction.payment_request) + .bind("payment_proof", transaction.payment_proof) + .bind("payment_method", transaction.payment_method.map(|pm| pm.to_string())) + .execute(&*conn) + .await?; - if row.matches_conditions(&mint_url, &unit, &state, &spending_conditions) { - Some(row) - } else { - None - } - }) - .collect::>()) -} + Ok(()) + } -impl SQLWalletDatabase -where - RM: DatabasePool + 'static, -{ - /// Creates a new instance - pub async fn new(db: X) -> Result - where - X: Into, - { - let pool = Pool::new(db.into()); - Self::migrate(pool.get().map_err(|e| Error::Database(Box::new(e)))?).await?; + #[instrument(skip(self))] + async fn update_mint_url( + &self, + old_mint_url: MintUrl, + new_mint_url: MintUrl, + ) -> Result<(), database::Error> { + let conn = self.pool.get().map_err(|e| Error::Database(Box::new(e)))?; + let tx = ConnectionWithTransaction::new(conn).await?; + let tables = ["mint_quote", "proof"]; - Ok(Self { pool }) - } + for table in &tables { + query(&format!( + r#" + UPDATE {table} + SET mint_url = :new_mint_url + WHERE mint_url = :old_mint_url + "# + ))? + .bind("new_mint_url", new_mint_url.to_string()) + .bind("old_mint_url", old_mint_url.to_string()) + .execute(&tx) + .await?; + } - /// Migrate [`WalletSqliteDatabase`] - async fn migrate(conn: PooledResource) -> Result<(), Error> { - let tx = ConnectionWithTransaction::new(conn).await?; - migrate(&tx, RM::Connection::name(), migrations::MIGRATIONS).await?; - // Update any existing keys with missing keyset_u32 values - Self::add_keyset_u32(&tx).await?; tx.commit().await?; Ok(()) } - async fn add_keyset_u32(conn: &T) -> Result<(), Error> - where - T: DatabaseExecutor, - { - // First get the keysets where keyset_u32 on key is null - let keys_without_u32: Vec> = query( + #[instrument(skip(self), fields(keyset_id = %keyset_id))] + async fn increment_keyset_counter( + &self, + keyset_id: &Id, + count: u32, + ) -> Result { + let conn = self.pool.get().map_err(|e| Error::Database(Box::new(e)))?; + + let tx = ConnectionWithTransaction::new(conn).await?; + + let current_counter = query( r#" - SELECT - id - FROM key - WHERE keyset_u32 IS NULL - "#, + SELECT counter + FROM keyset_counter + WHERE keyset_id=:keyset_id + "#, )? - .fetch_all(conn) - .await?; - - for row in keys_without_u32 { - unpack_into!(let (id) = row); - let id = column_as_string!(id); + .bind("keyset_id", keyset_id.to_string()) + .pluck(&tx) + .await? + .map(|n| Ok::<_, Error>(column_as_number!(n))) + .transpose()? + .unwrap_or(0); - if let Ok(id) = Id::from_str(&id) { - query( - r#" - UPDATE - key - SET keyset_u32 = :u32_keyset - WHERE id = :keyset_id - "#, - )? - .bind("u32_keyset", u32::from(id)) - .bind("keyset_id", id.to_string()) - .execute(conn) - .await?; - } - } + let new_counter = current_counter + count; - // Also update keysets where keyset_u32 is null - let keysets_without_u32: Vec> = query( + query( r#" - SELECT - id - FROM keyset - WHERE keyset_u32 IS NULL - "#, + INSERT INTO keyset_counter (keyset_id, counter) + VALUES (:keyset_id, :new_counter) + ON CONFLICT(keyset_id) DO UPDATE SET + counter = excluded.counter + "#, )? - .fetch_all(conn) + .bind("keyset_id", keyset_id.to_string()) + .bind("new_counter", new_counter) + .execute(&tx) .await?; - for row in keysets_without_u32 { - unpack_into!(let (id) = row); - let id = column_as_string!(id); - - if let Ok(id) = Id::from_str(&id) { - query( - r#" - UPDATE - keyset - SET keyset_u32 = :u32_keyset - WHERE id = :keyset_id - "#, - )? - .bind("u32_keyset", u32::from(id)) - .bind("keyset_id", id.to_string()) - .execute(conn) - .await?; - } - } + tx.commit().await?; - Ok(()) + Ok(new_counter) } -} -#[async_trait] -impl WalletDatabase for SQLWalletDatabase -where - RM: DatabasePool + 'static, -{ - async fn begin_db_transaction( + #[instrument(skip(self, mint_info))] + async fn add_mint( &self, - ) -> Result + Send + Sync>, database::Error> - { - Ok(Box::new(SQLWalletTransaction { - inner: ConnectionWithTransaction::new( - self.pool.get().map_err(|e| Error::Database(Box::new(e)))?, - ) - .await?, - })) - } - - #[instrument(skip(self))] - async fn get_melt_quotes(&self) -> Result, database::Error> { + mint_url: MintUrl, + mint_info: Option, + ) -> Result<(), database::Error> { let conn = self.pool.get().map_err(|e| Error::Database(Box::new(e)))?; - Ok(query( - r#" - SELECT - id, - unit, - amount, - request, - fee_reserve, - state, - expiry, - payment_preimage, - payment_method - FROM - melt_quote - "#, - )? - .fetch_all(&*conn) - .await? - .into_iter() - .map(sql_row_to_melt_quote) - .collect::>()?) - } + let ( + name, + pubkey, + version, + description, + description_long, + contact, + nuts, + icon_url, + urls, + motd, + time, + tos_url, + ) = match mint_info { + Some(mint_info) => { + let MintInfo { + name, + pubkey, + version, + description, + description_long, + contact, + nuts, + icon_url, + urls, + motd, + time, + tos_url, + } = mint_info; - #[instrument(skip(self))] - async fn get_mint(&self, mint_url: MintUrl) -> Result, database::Error> { - let conn = self.pool.get().map_err(|e| Error::Database(Box::new(e)))?; - Ok(query( + ( + name, + pubkey.map(|p| p.to_bytes().to_vec()), + version.map(|v| serde_json::to_string(&v).ok()), + description, + description_long, + contact.map(|c| serde_json::to_string(&c).ok()), + serde_json::to_string(&nuts).ok(), + icon_url, + urls.map(|c| serde_json::to_string(&c).ok()), + motd, + time, + tos_url, + ) + } + None => ( + None, None, None, None, None, None, None, None, None, None, None, None, + ), + }; + + query( r#" - SELECT - name, - pubkey, - version, - description, - description_long, - contact, - nuts, - icon_url, - motd, - urls, - mint_time, - tos_url - FROM - mint - WHERE mint_url = :mint_url - "#, + INSERT INTO mint + ( + mint_url, name, pubkey, version, description, description_long, + contact, nuts, icon_url, urls, motd, mint_time, tos_url + ) + VALUES + ( + :mint_url, :name, :pubkey, :version, :description, :description_long, + :contact, :nuts, :icon_url, :urls, :motd, :mint_time, :tos_url + ) + ON CONFLICT(mint_url) DO UPDATE SET + name = excluded.name, + pubkey = excluded.pubkey, + version = excluded.version, + description = excluded.description, + description_long = excluded.description_long, + contact = excluded.contact, + nuts = excluded.nuts, + icon_url = excluded.icon_url, + urls = excluded.urls, + motd = excluded.motd, + mint_time = excluded.mint_time, + tos_url = excluded.tos_url + ; + "#, )? .bind("mint_url", mint_url.to_string()) - .fetch_one(&*conn) - .await? - .map(sql_row_to_mint_info) - .transpose()?) + .bind("name", name) + .bind("pubkey", pubkey) + .bind("version", version) + .bind("description", description) + .bind("description_long", description_long) + .bind("contact", contact) + .bind("nuts", nuts) + .bind("icon_url", icon_url) + .bind("urls", urls) + .bind("motd", motd) + .bind("mint_time", time.map(|v| v as i64)) + .bind("tos_url", tos_url) + .execute(&*conn) + .await?; + + Ok(()) } #[instrument(skip(self))] - async fn get_mints(&self) -> Result>, database::Error> { + async fn remove_mint(&self, mint_url: MintUrl) -> Result<(), database::Error> { let conn = self.pool.get().map_err(|e| Error::Database(Box::new(e)))?; - Ok(query( - r#" - SELECT - name, - pubkey, - version, - description, - description_long, - contact, - nuts, - icon_url, - motd, - urls, - mint_time, - tos_url, - mint_url - FROM - mint - "#, - )? - .fetch_all(&*conn) - .await? - .into_iter() - .map(|mut row| { - let url = column_as_string!( - row.pop().ok_or(ConversionError::MissingColumn(0, 1))?, - MintUrl::from_str - ); - Ok((url, sql_row_to_mint_info(row).ok())) - }) - .collect::, Error>>()?) + query(r#"DELETE FROM mint WHERE mint_url=:mint_url"#)? + .bind("mint_url", mint_url.to_string()) + .execute(&*conn) + .await?; + + Ok(()) } - #[instrument(skip(self))] - async fn get_mint_keysets( + #[instrument(skip(self, keysets))] + async fn add_mint_keysets( &self, mint_url: MintUrl, - ) -> Result>, database::Error> { + keysets: Vec, + ) -> Result<(), database::Error> { let conn = self.pool.get().map_err(|e| Error::Database(Box::new(e)))?; + let tx = ConnectionWithTransaction::new(conn).await?; - let keysets = query( - r#" - SELECT - id, - unit, - active, - input_fee_ppk, - final_expiry - FROM - keyset - WHERE mint_url = :mint_url - "#, - )? - .bind("mint_url", mint_url.to_string()) - .fetch_all(&*conn) - .await? - .into_iter() - .map(sql_row_to_keyset) - .collect::, Error>>()?; - - match keysets.is_empty() { - false => Ok(Some(keysets)), - true => Ok(None), + for keyset in keysets { + query( + r#" + INSERT INTO keyset + (mint_url, id, unit, active, input_fee_ppk, final_expiry, keyset_u32) + VALUES + (:mint_url, :id, :unit, :active, :input_fee_ppk, :final_expiry, :keyset_u32) + ON CONFLICT(id) DO UPDATE SET + active = excluded.active, + input_fee_ppk = excluded.input_fee_ppk + "#, + )? + .bind("mint_url", mint_url.to_string()) + .bind("id", keyset.id.to_string()) + .bind("unit", keyset.unit.to_string()) + .bind("active", keyset.active) + .bind("input_fee_ppk", keyset.input_fee_ppk as i64) + .bind("final_expiry", keyset.final_expiry.map(|v| v as i64)) + .bind("keyset_u32", u32::from(keyset.id)) + .execute(&tx) + .await?; } - } - #[instrument(skip(self), fields(keyset_id = %keyset_id))] - async fn get_keyset_by_id( - &self, - keyset_id: &Id, - ) -> Result, database::Error> { - let conn = self.pool.get().map_err(|e| Error::Database(Box::new(e)))?; - get_keyset_by_id_inner(&*conn, keyset_id, false).await - } + tx.commit().await?; - #[instrument(skip(self))] - async fn get_mint_quote(&self, quote_id: &str) -> Result, database::Error> { - let conn = self.pool.get().map_err(|e| Error::Database(Box::new(e)))?; - get_mint_quote_inner(&*conn, quote_id, false).await + Ok(()) } - #[instrument(skip(self))] - async fn get_mint_quotes(&self) -> Result, database::Error> { + #[instrument(skip_all)] + async fn add_mint_quote(&self, quote: MintQuote) -> Result<(), database::Error> { let conn = self.pool.get().map_err(|e| Error::Database(Box::new(e)))?; - Ok(query( - r#" - SELECT - id, - mint_url, - amount, - unit, - request, - state, - expiry, - secret_key, - payment_method, - amount_issued, - amount_paid - FROM - mint_quote - "#, - )? - .fetch_all(&*conn) - .await? - .into_iter() - .map(sql_row_to_mint_quote) - .collect::>()?) - } - #[instrument(skip(self))] - async fn get_unissued_mint_quotes(&self) -> Result, Self::Err> { - let conn = self.pool.get().map_err(|e| Error::Database(Box::new(e)))?; - Ok(query( - r#" - SELECT - id, - mint_url, - amount, - unit, - request, - state, - expiry, - secret_key, - payment_method, - amount_issued, - amount_paid - FROM - mint_quote - WHERE - amount_issued = 0 - OR - payment_method = 'bolt12' + query( + r#" + INSERT INTO mint_quote + (id, mint_url, amount, unit, request, state, expiry, secret_key, payment_method, amount_issued, amount_paid) + VALUES + (:id, :mint_url, :amount, :unit, :request, :state, :expiry, :secret_key, :payment_method, :amount_issued, :amount_paid) + ON CONFLICT(id) DO UPDATE SET + mint_url = excluded.mint_url, + amount = excluded.amount, + unit = excluded.unit, + request = excluded.request, + state = excluded.state, + expiry = excluded.expiry, + secret_key = excluded.secret_key, + payment_method = excluded.payment_method, + amount_issued = excluded.amount_issued, + amount_paid = excluded.amount_paid + ; "#, - )? - .fetch_all(&*conn) - .await? - .into_iter() - .map(sql_row_to_mint_quote) - .collect::>()?) + )? + .bind("id", quote.id.to_string()) + .bind("mint_url", quote.mint_url.to_string()) + .bind("amount", quote.amount.map(|a| a.to_i64())) + .bind("unit", quote.unit.to_string()) + .bind("request", quote.request) + .bind("state", quote.state.to_string()) + .bind("expiry", quote.expiry as i64) + .bind("secret_key", quote.secret_key.map(|p| p.to_string())) + .bind("payment_method", quote.payment_method.to_string()) + .bind("amount_issued", quote.amount_issued.to_i64()) + .bind("amount_paid", quote.amount_paid.to_i64()) + .execute(&*conn).await?; + + Ok(()) } #[instrument(skip(self))] - async fn get_melt_quote( - &self, - quote_id: &str, - ) -> Result, database::Error> { + async fn remove_mint_quote(&self, quote_id: &str) -> Result<(), database::Error> { let conn = self.pool.get().map_err(|e| Error::Database(Box::new(e)))?; - get_melt_quote_inner(&*conn, quote_id, false).await - } - #[instrument(skip(self), fields(keyset_id = %keyset_id))] - async fn get_keys(&self, keyset_id: &Id) -> Result, database::Error> { - let conn = self.pool.get().map_err(|e| Error::Database(Box::new(e)))?; - get_keys_inner(&*conn, keyset_id).await - } + query(r#"DELETE FROM mint_quote WHERE id=:id"#)? + .bind("id", quote_id.to_string()) + .execute(&*conn) + .await?; - #[instrument(skip(self, state, spending_conditions))] - async fn get_proofs( - &self, - mint_url: Option, - unit: Option, - state: Option>, - spending_conditions: Option>, - ) -> Result, database::Error> { - let conn = self.pool.get().map_err(|e| Error::Database(Box::new(e)))?; - get_proofs_inner(&*conn, mint_url, unit, state, spending_conditions, false).await + Ok(()) } - #[instrument(skip(self, ys))] - async fn get_proofs_by_ys( - &self, - ys: Vec, - ) -> Result, database::Error> { - if ys.is_empty() { - return Ok(Vec::new()); - } - + #[instrument(skip_all)] + async fn add_melt_quote(&self, quote: wallet::MeltQuote) -> Result<(), database::Error> { let conn = self.pool.get().map_err(|e| Error::Database(Box::new(e)))?; - Ok(query( + + query( r#" - SELECT - amount, - unit, - keyset_id, - secret, - c, - witness, - dleq_e, - dleq_s, - dleq_r, - y, - mint_url, - state, - spending_condition - FROM proof - WHERE y IN (:ys) - "#, + INSERT INTO melt_quote + (id, unit, amount, request, fee_reserve, state, expiry, payment_method) + VALUES + (:id, :unit, :amount, :request, :fee_reserve, :state, :expiry, :payment_method) + ON CONFLICT(id) DO UPDATE SET + unit = excluded.unit, + amount = excluded.amount, + request = excluded.request, + fee_reserve = excluded.fee_reserve, + state = excluded.state, + expiry = excluded.expiry, + payment_method = excluded.payment_method + ; + "#, )? - .bind_vec("ys", ys.iter().map(|y| y.to_bytes().to_vec()).collect()) - .fetch_all(&*conn) - .await? - .into_iter() - .filter_map(|row| sql_row_to_proof_info(row).ok()) - .collect::>()) + .bind("id", quote.id.to_string()) + .bind("unit", quote.unit.to_string()) + .bind("amount", u64::from(quote.amount) as i64) + .bind("request", quote.request) + .bind("fee_reserve", u64::from(quote.fee_reserve) as i64) + .bind("state", quote.state.to_string()) + .bind("expiry", quote.expiry as i64) + .bind("payment_method", quote.payment_method.to_string()) + .execute(&*conn) + .await?; + + Ok(()) } - async fn get_balance( - &self, - mint_url: Option, - unit: Option, - states: Option>, - ) -> Result { + #[instrument(skip(self))] + async fn remove_melt_quote(&self, quote_id: &str) -> Result<(), database::Error> { let conn = self.pool.get().map_err(|e| Error::Database(Box::new(e)))?; - let mut query_str = "SELECT COALESCE(SUM(amount), 0) as total FROM proof".to_string(); - let mut where_clauses = Vec::new(); - let states = states - .unwrap_or_default() - .into_iter() - .map(|x| x.to_string()) - .collect::>(); + query(r#"DELETE FROM melt_quote WHERE id=:id"#)? + .bind("id", quote_id.to_owned()) + .execute(&*conn) + .await?; - if mint_url.is_some() { - where_clauses.push("mint_url = :mint_url"); - } - if unit.is_some() { - where_clauses.push("unit = :unit"); - } - if !states.is_empty() { - where_clauses.push("state IN (:states)"); - } + Ok(()) + } - if !where_clauses.is_empty() { - query_str.push_str(" WHERE "); - query_str.push_str(&where_clauses.join(" AND ")); - } + #[instrument(skip_all)] + async fn add_keys(&self, keyset: KeySet) -> Result<(), database::Error> { + let conn = self.pool.get().map_err(|e| Error::Database(Box::new(e)))?; - let mut q = query(&query_str)?; + keyset.verify_id()?; - if let Some(ref mint_url) = mint_url { - q = q.bind("mint_url", mint_url.to_string()); - } - if let Some(ref unit) = unit { - q = q.bind("unit", unit.to_string()); - } + query( + r#" + INSERT INTO key + (id, keys, keyset_u32) + VALUES + (:id, :keys, :keyset_u32) + "#, + )? + .bind("id", keyset.id.to_string()) + .bind( + "keys", + serde_json::to_string(&keyset.keys).map_err(Error::from)?, + ) + .bind("keyset_u32", u32::from(keyset.id)) + .execute(&*conn) + .await?; - if !states.is_empty() { - q = q.bind_vec("states", states); - } + Ok(()) + } - let balance = q - .pluck(&*conn) - .await? - .map(|n| { - // SQLite SUM returns INTEGER which we need to convert to u64 - match n { - crate::stmt::Column::Integer(i) => Ok(i as u64), - crate::stmt::Column::Real(f) => Ok(f as u64), - _ => Err(Error::Database(Box::new(std::io::Error::new( - std::io::ErrorKind::InvalidData, - "Invalid balance type", - )))), - } - }) - .transpose()? - .unwrap_or(0); + #[instrument(skip(self))] + async fn remove_keys(&self, id: &Id) -> Result<(), database::Error> { + let conn = self.pool.get().map_err(|e| Error::Database(Box::new(e)))?; - Ok(balance) + query(r#"DELETE FROM key WHERE id = :id"#)? + .bind("id", id.to_string()) + .execute(&*conn) + .await?; + + Ok(()) } #[instrument(skip(self))] - async fn get_transaction( + async fn remove_transaction( &self, transaction_id: TransactionId, - ) -> Result, database::Error> { + ) -> Result<(), database::Error> { let conn = self.pool.get().map_err(|e| Error::Database(Box::new(e)))?; - Ok(query( - r#" - SELECT - mint_url, - direction, - unit, - amount, - fee, - ys, - timestamp, - memo, - metadata, - quote_id, - payment_request, - payment_proof, - payment_method - FROM - transactions - WHERE - id = :id - "#, - )? - .bind("id", transaction_id.as_slice().to_vec()) - .fetch_one(&*conn) - .await? - .map(sql_row_to_transaction) - .transpose()?) + + query(r#"DELETE FROM transactions WHERE id=:id"#)? + .bind("id", transaction_id.as_slice().to_vec()) + .execute(&*conn) + .await?; + + Ok(()) } - #[instrument(skip(self))] - async fn list_transactions( + // KV Store write methods (non-transactional) + + async fn kv_write( &self, - mint_url: Option, - direction: Option, - unit: Option, - ) -> Result, database::Error> { + primary_namespace: &str, + secondary_namespace: &str, + key: &str, + value: &[u8], + ) -> Result<(), database::Error> { let conn = self.pool.get().map_err(|e| Error::Database(Box::new(e)))?; + crate::keyvalue::kv_write_standalone( + &*conn, + primary_namespace, + secondary_namespace, + key, + value, + ) + .await?; + Ok(()) + } - Ok(query( - r#" - SELECT - mint_url, - direction, - unit, - amount, - fee, - ys, - timestamp, - memo, - metadata, - quote_id, - payment_request, - payment_proof, - payment_method - FROM - transactions - "#, - )? - .fetch_all(&*conn) - .await? - .into_iter() - .filter_map(|row| { - // TODO: Avoid a table scan by passing the heavy lifting of checking to the DB engine - let transaction = sql_row_to_transaction(row).ok()?; - if transaction.matches_conditions(&mint_url, &direction, &unit) { - Some(transaction) - } else { - None - } - }) - .collect::>()) + async fn kv_remove( + &self, + primary_namespace: &str, + secondary_namespace: &str, + key: &str, + ) -> Result<(), database::Error> { + let conn = self.pool.get().map_err(|e| Error::Database(Box::new(e)))?; + crate::keyvalue::kv_remove_standalone(&*conn, primary_namespace, secondary_namespace, key) + .await?; + Ok(()) } } @@ -1557,68 +1558,6 @@ fn sql_row_to_transaction(row: Vec) -> Result { // KVStore implementations for wallet -#[async_trait] -impl database::KVStoreTransaction for SQLWalletTransaction -where - RM: DatabasePool + 'static, -{ - async fn kv_read( - &mut self, - primary_namespace: &str, - secondary_namespace: &str, - key: &str, - ) -> Result>, Error> { - crate::keyvalue::kv_read_in_transaction( - &self.inner, - primary_namespace, - secondary_namespace, - key, - ) - .await - } - - async fn kv_write( - &mut self, - primary_namespace: &str, - secondary_namespace: &str, - key: &str, - value: &[u8], - ) -> Result<(), Error> { - crate::keyvalue::kv_write_in_transaction( - &self.inner, - primary_namespace, - secondary_namespace, - key, - value, - ) - .await - } - - async fn kv_remove( - &mut self, - primary_namespace: &str, - secondary_namespace: &str, - key: &str, - ) -> Result<(), Error> { - crate::keyvalue::kv_remove_in_transaction( - &self.inner, - primary_namespace, - secondary_namespace, - key, - ) - .await - } - - async fn kv_list( - &mut self, - primary_namespace: &str, - secondary_namespace: &str, - ) -> Result, Error> { - crate::keyvalue::kv_list_in_transaction(&self.inner, primary_namespace, secondary_namespace) - .await - } -} - #[async_trait] impl database::KVStoreDatabase for SQLWalletDatabase where @@ -1643,20 +1582,3 @@ where crate::keyvalue::kv_list(&self.pool, primary_namespace, secondary_namespace).await } } - -#[async_trait] -impl database::KVStore for SQLWalletDatabase -where - RM: DatabasePool + 'static, -{ - async fn begin_transaction( - &self, - ) -> Result + Send + Sync>, Error> { - Ok(Box::new(SQLWalletTransaction { - inner: ConnectionWithTransaction::new( - self.pool.get().map_err(|e| Error::Database(Box::new(e)))?, - ) - .await?, - })) - } -} diff --git a/crates/cdk-sqlite/src/wallet/mod.rs b/crates/cdk-sqlite/src/wallet/mod.rs index 21abe5adf..e1a40c94c 100644 --- a/crates/cdk-sqlite/src/wallet/mod.rs +++ b/crates/cdk-sqlite/src/wallet/mod.rs @@ -46,14 +46,10 @@ mod tests { let mint_info = MintInfo::new().description("test"); let mint_url = MintUrl::from_str("https://mint.xyz").unwrap(); - let mut tx = db.begin_db_transaction().await.expect("tx"); - - tx.add_mint(mint_url.clone(), Some(mint_info.clone())) + db.add_mint(mint_url.clone(), Some(mint_info.clone())) .await .unwrap(); - tx.commit().await.expect("commit"); - let res = db.get_mint(mint_url).await.unwrap(); assert_eq!(mint_info, res.clone().unwrap()); assert_eq!("test", &res.unwrap().description.unwrap()); @@ -108,15 +104,11 @@ mod tests { let proof_info = ProofInfo::new(proof, mint_url.clone(), State::Unspent, CurrencyUnit::Sat).unwrap(); - let mut tx = db.begin_db_transaction().await.expect("tx"); - // Store the proof in the database - tx.update_proofs(vec![proof_info.clone()], vec![]) + db.update_proofs(vec![proof_info.clone()], vec![]) .await .unwrap(); - tx.commit().await.expect("commit"); - // Retrieve the proof from the database let retrieved_proofs = db .get_proofs( @@ -172,8 +164,6 @@ mod tests { PaymentMethod::Custom("custom".to_string()), ]; - let mut tx = db.begin_db_transaction().await.expect("begin"); - for (i, payment_method) in payment_methods.iter().enumerate() { let quote = MintQuote { id: format!("test_quote_{}", i), @@ -190,15 +180,14 @@ mod tests { }; // Store the quote - tx.add_mint_quote(quote.clone()).await.unwrap(); + db.add_mint_quote(quote.clone()).await.unwrap(); // Retrieve and verify - let retrieved = tx.get_mint_quote("e.id).await.unwrap().unwrap(); + let retrieved = db.get_mint_quote("e.id).await.unwrap().unwrap(); assert_eq!(retrieved.payment_method, *payment_method); assert_eq!(retrieved.amount_issued, Amount::from(0)); assert_eq!(retrieved.amount_paid, Amount::from(0)); } - tx.commit().await.expect("commit"); } #[tokio::test] @@ -247,9 +236,7 @@ mod tests { } // Store all proofs in the database - let mut tx = db.begin_db_transaction().await.unwrap(); - tx.update_proofs(proof_infos.clone(), vec![]).await.unwrap(); - tx.commit().await.unwrap(); + db.update_proofs(proof_infos.clone(), vec![]).await.unwrap(); // Test 1: Retrieve all proofs by their Y values let retrieved_proofs = db.get_proofs_by_ys(expected_ys.clone()).await.unwrap(); @@ -375,17 +362,11 @@ mod tests { amount_paid: Amount::from(0), }; - { - let mut tx = db.begin_db_transaction().await.unwrap(); - - // Add all quotes to the database - tx.add_mint_quote(quote1).await.unwrap(); - tx.add_mint_quote(quote2.clone()).await.unwrap(); - tx.add_mint_quote(quote3.clone()).await.unwrap(); - tx.add_mint_quote(quote4.clone()).await.unwrap(); - - tx.commit().await.unwrap(); - } + // Add all quotes to the database + db.add_mint_quote(quote1).await.unwrap(); + db.add_mint_quote(quote2.clone()).await.unwrap(); + db.add_mint_quote(quote3.clone()).await.unwrap(); + db.add_mint_quote(quote4.clone()).await.unwrap(); // Get unissued mint quotes let unissued_quotes = db.get_unissued_mint_quotes().await.unwrap(); diff --git a/crates/cdk/src/wallet/auth/auth_wallet.rs b/crates/cdk/src/wallet/auth/auth_wallet.rs index ee666db77..7a50d5677 100644 --- a/crates/cdk/src/wallet/auth/auth_wallet.rs +++ b/crates/cdk/src/wallet/auth/auth_wallet.rs @@ -286,9 +286,8 @@ impl AuthWallet { /// Get Auth Token #[instrument(skip(self))] pub async fn get_blind_auth_token(&self) -> Result, Error> { - let mut tx = self.localstore.begin_db_transaction().await?; - - let auth_proof = match tx + let auth_proof = match self + .localstore .get_proofs( Some(self.mint_url.clone()), Some(CurrencyUnit::Auth), @@ -299,8 +298,9 @@ impl AuthWallet { .pop() { Some(proof) => { - tx.update_proofs(vec![], vec![proof.proof.y()?]).await?; - tx.commit().await?; + self.localstore + .update_proofs(vec![], vec![proof.proof.y()?]) + .await?; proof.proof.try_into()? } None => return Ok(None), @@ -458,9 +458,7 @@ impl AuthWallet { .collect::, _>>()?; // Add new proofs to store - let mut tx = self.localstore.begin_db_transaction().await?; - tx.update_proofs(proof_infos, vec![]).await?; - tx.commit().await?; + self.localstore.update_proofs(proof_infos, vec![]).await?; Ok(proofs) } diff --git a/crates/cdk/src/wallet/issue/bolt11.rs b/crates/cdk/src/wallet/issue/bolt11.rs index 84fa96018..a4a368ec2 100644 --- a/crates/cdk/src/wallet/issue/bolt11.rs +++ b/crates/cdk/src/wallet/issue/bolt11.rs @@ -94,9 +94,7 @@ impl Wallet { Some(secret_key), ); - let mut tx = self.localstore.begin_db_transaction().await?; - tx.add_mint_quote(quote.clone()).await?; - tx.commit().await?; + self.localstore.add_mint_quote(quote.clone()).await?; Ok(quote) } @@ -109,22 +107,18 @@ impl Wallet { ) -> Result, Error> { let response = self.client.get_mint_quote_status(quote_id).await?; - let mut tx = self.localstore.begin_db_transaction().await?; - - match tx.get_mint_quote(quote_id).await? { + match self.localstore.get_mint_quote(quote_id).await? { Some(quote) => { let mut quote = quote; quote.state = response.state; - tx.add_mint_quote(quote).await?; + self.localstore.add_mint_quote(quote).await?; } None => { tracing::info!("Quote mint {} unknown", quote_id); } } - tx.commit().await?; - Ok(response) } @@ -233,8 +227,8 @@ impl Wallet { .get_keyset_fees_and_amounts_by_id(active_keyset_id) .await?; - let mut tx = self.localstore.begin_db_transaction().await?; - let quote_info = tx + let quote_info = self + .localstore .get_mint_quote(quote_id) .await? .ok_or(Error::UnknownQuote)?; @@ -258,7 +252,7 @@ impl Wallet { let split_target = match amount_split_target { SplitTarget::None => { - self.determine_split_target_values(&mut tx, amount_mintable, &fee_and_amounts) + self.determine_split_target_values(amount_mintable, &fee_and_amounts) .await? } s => s, @@ -284,7 +278,8 @@ impl Wallet { ); // Atomically get the counter range we need - let new_counter = tx + let new_counter = self + .localstore .increment_keyset_counter(&active_keyset_id, num_secrets) .await?; @@ -311,8 +306,6 @@ impl Wallet { request.sign(secret_key.clone())?; } - tx.commit().await?; - let mint_res = self.client.post_mint(request).await?; let keys = self.load_keyset_keys(active_keyset_id).await?; @@ -336,11 +329,8 @@ impl Wallet { &keys, )?; - // Start new transaction for post-mint operations - let mut tx = self.localstore.begin_db_transaction().await?; - // Remove filled quote from store - tx.remove_mint_quote("e_info.id).await?; + self.localstore.remove_mint_quote("e_info.id).await?; let proof_infos = proofs .iter() @@ -355,27 +345,26 @@ impl Wallet { .collect::, _>>()?; // Add new proofs to store - tx.update_proofs(proof_infos, vec![]).await?; + self.localstore.update_proofs(proof_infos, vec![]).await?; // Add transaction to store - tx.add_transaction(Transaction { - mint_url: self.mint_url.clone(), - direction: TransactionDirection::Incoming, - amount: proofs.total_amount()?, - fee: Amount::ZERO, - unit: self.unit.clone(), - ys: proofs.ys()?, - timestamp: unix_time, - memo: None, - metadata: HashMap::new(), - quote_id: Some(quote_id.to_string()), - payment_request: Some(quote_info.request), - payment_proof: None, - payment_method: Some(quote_info.payment_method), - }) - .await?; - - tx.commit().await?; + self.localstore + .add_transaction(Transaction { + mint_url: self.mint_url.clone(), + direction: TransactionDirection::Incoming, + amount: proofs.total_amount()?, + fee: Amount::ZERO, + unit: self.unit.clone(), + ys: proofs.ys()?, + timestamp: unix_time, + memo: None, + metadata: HashMap::new(), + quote_id: Some(quote_id.to_string()), + payment_request: Some(quote_info.request), + payment_proof: None, + payment_method: Some(quote_info.payment_method), + }) + .await?; Ok(proofs) } diff --git a/crates/cdk/src/wallet/issue/bolt12.rs b/crates/cdk/src/wallet/issue/bolt12.rs index aef75fc96..adbd94573 100644 --- a/crates/cdk/src/wallet/issue/bolt12.rs +++ b/crates/cdk/src/wallet/issue/bolt12.rs @@ -71,9 +71,7 @@ impl Wallet { Some(secret_key), ); - let mut tx = self.localstore.begin_db_transaction().await?; - tx.add_mint_quote(quote.clone()).await?; - tx.commit().await?; + self.localstore.add_mint_quote(quote.clone()).await?; Ok(quote) } @@ -92,8 +90,7 @@ impl Wallet { .get_keyset_fees_and_amounts_by_id(active_keyset_id) .await?; - let mut tx = self.localstore.begin_db_transaction().await?; - let quote_info = tx.get_mint_quote(quote_id).await?; + let quote_info = self.localstore.get_mint_quote(quote_id).await?; let quote_info = if let Some(quote) = quote_info { if quote.expiry.le(&unix_time()) && quote.expiry.ne(&0) { @@ -105,21 +102,20 @@ impl Wallet { return Err(Error::UnknownQuote); }; - let (mut tx, quote_info, amount) = match amount { - Some(amount) => (tx, quote_info, amount), + let (quote_info, amount) = match amount { + Some(amount) => (quote_info, amount), None => { // If an amount it not supplied with check the status of the quote // The mint will tell us how much can be minted - tx.commit().await?; let state = self.mint_bolt12_quote_state(quote_id).await?; - let mut tx = self.localstore.begin_db_transaction().await?; - let quote_info = tx + let quote_info = self + .localstore .get_mint_quote(quote_id) .await? .ok_or(Error::UnknownQuote)?; - (tx, quote_info, state.amount_paid - state.amount_issued) + (quote_info, state.amount_paid - state.amount_issued) } }; @@ -130,7 +126,7 @@ impl Wallet { let split_target = match amount_split_target { SplitTarget::None => { - self.determine_split_target_values(&mut tx, amount, &fee_and_amounts) + self.determine_split_target_values(amount, &fee_and_amounts) .await? } s => s, @@ -155,7 +151,8 @@ impl Wallet { ); // Atomically get the counter range we need - let new_counter = tx + let new_counter = self + .localstore .increment_keyset_counter(&active_keyset_id, num_secrets) .await?; @@ -185,12 +182,8 @@ impl Wallet { return Err(Error::SignatureMissingOrInvalid); } - tx.commit().await?; - let mint_res = self.client.post_mint(request).await?; - let mut tx = self.localstore.begin_db_transaction().await?; - let keys = self.load_keyset_keys(active_keyset_id).await?; // Verify the signature DLEQ is valid @@ -213,13 +206,14 @@ impl Wallet { )?; // Update quote with issued amount - let mut quote_info = tx + let mut quote_info = self + .localstore .get_mint_quote(quote_id) .await? .ok_or(Error::UnpaidQuote)?; quote_info.amount_issued += proofs.total_amount()?; - tx.add_mint_quote(quote_info.clone()).await?; + self.localstore.add_mint_quote(quote_info.clone()).await?; let proof_infos = proofs .iter() @@ -234,27 +228,26 @@ impl Wallet { .collect::, _>>()?; // Add new proofs to store - tx.update_proofs(proof_infos, vec![]).await?; + self.localstore.update_proofs(proof_infos, vec![]).await?; // Add transaction to store - tx.add_transaction(Transaction { - mint_url: self.mint_url.clone(), - direction: TransactionDirection::Incoming, - amount: proofs.total_amount()?, - fee: Amount::ZERO, - unit: self.unit.clone(), - ys: proofs.ys()?, - timestamp: unix_time(), - memo: None, - metadata: HashMap::new(), - quote_id: Some(quote_id.to_string()), - payment_request: Some(quote_info.request), - payment_proof: None, - payment_method: Some(quote_info.payment_method), - }) - .await?; - - tx.commit().await?; + self.localstore + .add_transaction(Transaction { + mint_url: self.mint_url.clone(), + direction: TransactionDirection::Incoming, + amount: proofs.total_amount()?, + fee: Amount::ZERO, + unit: self.unit.clone(), + ys: proofs.ys()?, + timestamp: unix_time(), + memo: None, + metadata: HashMap::new(), + quote_id: Some(quote_id.to_string()), + payment_request: Some(quote_info.request), + payment_proof: None, + payment_method: Some(quote_info.payment_method), + }) + .await?; Ok(proofs) } @@ -267,23 +260,19 @@ impl Wallet { ) -> Result, Error> { let response = self.client.get_mint_quote_bolt12_status(quote_id).await?; - let mut tx = self.localstore.begin_db_transaction().await?; - - match tx.get_mint_quote(quote_id).await? { + match self.localstore.get_mint_quote(quote_id).await? { Some(quote) => { let mut quote = quote; quote.amount_issued = response.amount_issued; quote.amount_paid = response.amount_paid; - tx.add_mint_quote(quote).await?; + self.localstore.add_mint_quote(quote).await?; } None => { tracing::info!("Quote mint {} unknown", quote_id); } } - tx.commit().await?; - Ok(response) } } diff --git a/crates/cdk/src/wallet/issue/custom.rs b/crates/cdk/src/wallet/issue/custom.rs index c80665db2..b47b69bf0 100644 --- a/crates/cdk/src/wallet/issue/custom.rs +++ b/crates/cdk/src/wallet/issue/custom.rs @@ -77,10 +77,8 @@ impl Wallet { quote_res.expiry.unwrap_or(0), Some(secret_key), ); - let mut tx = self.localstore.begin_db_transaction().await?; + self.localstore.add_mint_quote(quote.clone()).await?; - tx.add_mint_quote(quote.clone()).await?; - tx.commit().await?; Ok(quote) } @@ -94,7 +92,6 @@ impl Wallet { spending_conditions: Option, ) -> Result { self.refresh_keysets().await?; - let mut tx = self.localstore.begin_db_transaction().await?; let quote_info = self .localstore @@ -146,7 +143,8 @@ impl Wallet { ); // Atomically get the counter range we need - let new_counter = tx + let new_counter = self + .localstore .increment_keyset_counter(&active_keyset_id, num_secrets) .await?; @@ -197,7 +195,7 @@ impl Wallet { )?; // Remove filled quote from store - tx.remove_mint_quote("e_info.id).await?; + self.localstore.remove_mint_quote("e_info.id).await?; let proof_infos = proofs .iter() @@ -212,26 +210,27 @@ impl Wallet { .collect::, _>>()?; // Add new proofs to store - tx.update_proofs(proof_infos, vec![]).await?; + self.localstore.update_proofs(proof_infos, vec![]).await?; // Add transaction to store - tx.add_transaction(Transaction { - mint_url: self.mint_url.clone(), - direction: TransactionDirection::Incoming, - amount: proofs.total_amount()?, - fee: Amount::ZERO, - unit: self.unit.clone(), - ys: proofs.ys()?, - timestamp: unix_time, - memo: None, - metadata: HashMap::new(), - quote_id: Some(quote_id.to_string()), - payment_request: Some(quote_info.request), - payment_proof: None, - payment_method: Some(quote_info.payment_method), - }) - .await?; - tx.commit().await?; + self.localstore + .add_transaction(Transaction { + mint_url: self.mint_url.clone(), + direction: TransactionDirection::Incoming, + amount: proofs.total_amount()?, + fee: Amount::ZERO, + unit: self.unit.clone(), + ys: proofs.ys()?, + timestamp: unix_time, + memo: None, + metadata: HashMap::new(), + quote_id: Some(quote_id.to_string()), + payment_request: Some(quote_info.request), + payment_proof: None, + payment_method: Some(quote_info.payment_method), + }) + .await?; + Ok(proofs) } } diff --git a/crates/cdk/src/wallet/issue/issue_bolt11.rs b/crates/cdk/src/wallet/issue/issue_bolt11.rs new file mode 100644 index 000000000..e03205ab6 --- /dev/null +++ b/crates/cdk/src/wallet/issue/issue_bolt11.rs @@ -0,0 +1,367 @@ +use std::collections::HashMap; + +use cdk_common::nut04::MintMethodOptions; +use cdk_common::wallet::{MintQuote, Transaction, TransactionDirection}; +use cdk_common::PaymentMethod; +use tracing::instrument; + +use crate::amount::SplitTarget; +use crate::dhke::construct_proofs; +use crate::nuts::nut00::ProofsMethods; +use crate::nuts::{ + nut12, MintQuoteBolt11Request, MintQuoteBolt11Response, MintRequest, PreMintSecrets, Proofs, + SecretKey, SpendingConditions, State, +}; +use crate::types::ProofInfo; +use crate::util::unix_time; +use crate::wallet::MintQuoteState; +use crate::{Amount, Error, Wallet}; + +impl Wallet { + /// Mint Quote + /// # Synopsis + /// ```rust,no_run + /// use std::sync::Arc; + /// + /// use cdk::amount::Amount; + /// use cdk::nuts::CurrencyUnit; + /// use cdk::wallet::Wallet; + /// use cdk_sqlite::wallet::memory; + /// use rand::random; + /// + /// #[tokio::main] + /// async fn main() -> anyhow::Result<()> { + /// let seed = random::<[u8; 64]>(); + /// let mint_url = "https://fake.thesimplekid.dev"; + /// let unit = CurrencyUnit::Sat; + /// + /// let localstore = memory::empty().await?; + /// let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), seed, None)?; + /// let amount = Amount::from(100); + /// + /// let quote = wallet.mint_quote(amount, None).await?; + /// Ok(()) + /// } + /// ``` + #[instrument(skip(self))] + pub async fn mint_quote( + &self, + amount: Amount, + description: Option, + ) -> Result { + let mint_info = self.load_mint_info().await?; + + let mint_url = self.mint_url.clone(); + let unit = self.unit.clone(); + + // If we have a description, we check that the mint supports it. + if description.is_some() { + let settings = mint_info + .nuts + .nut04 + .get_settings(&unit, &crate::nuts::PaymentMethod::Bolt11) + .ok_or(Error::UnsupportedUnit)?; + + match settings.options { + Some(MintMethodOptions::Bolt11 { description }) if description => (), + _ => return Err(Error::InvoiceDescriptionUnsupported), + } + } + + let secret_key = SecretKey::generate(); + + let request = MintQuoteBolt11Request { + amount, + unit: unit.clone(), + description, + pubkey: Some(secret_key.public_key()), + }; + + let quote_res = self.client.post_mint_quote(request).await?; + + let quote = MintQuote::new( + quote_res.quote, + mint_url, + PaymentMethod::Bolt11, + Some(amount), + unit, + quote_res.request, + quote_res.expiry.unwrap_or(0), + Some(secret_key), + ); + + self.localstore.add_mint_quote(quote.clone()).await?; + + Ok(quote) + } + + /// Check mint quote status + #[instrument(skip(self, quote_id))] + pub async fn mint_quote_state( + &self, + quote_id: &str, + ) -> Result, Error> { + let response = self.client.get_mint_quote_status(quote_id).await?; + + match self.localstore.get_mint_quote(quote_id).await? { + Some(quote) => { + let mut quote = quote; + + quote.state = response.state; + self.localstore.add_mint_quote(quote).await?; + } + None => { + tracing::info!("Quote mint {} unknown", quote_id); + } + } + + Ok(response) + } + + /// Check status of pending mint quotes + #[instrument(skip(self))] + pub async fn check_all_mint_quotes(&self) -> Result { + let mint_quotes = self.localstore.get_unissued_mint_quotes().await?; + let mut total_amount = Amount::ZERO; + + for mint_quote in mint_quotes { + match mint_quote.payment_method { + PaymentMethod::Bolt11 => { + let mint_quote_response = self.mint_quote_state(&mint_quote.id).await?; + + if mint_quote_response.state == MintQuoteState::Paid { + let proofs = self + .mint(&mint_quote.id, SplitTarget::default(), None) + .await?; + total_amount += proofs.total_amount()?; + } + } + PaymentMethod::Bolt12 => { + let mint_quote_response = self.mint_bolt12_quote_state(&mint_quote.id).await?; + if mint_quote_response.amount_paid > mint_quote_response.amount_issued { + let proofs = self + .mint_bolt12(&mint_quote.id, None, SplitTarget::default(), None) + .await?; + total_amount += proofs.total_amount()?; + } + } + PaymentMethod::Custom(_) => { + tracing::warn!("We cannot check unknown types"); + } + } + } + Ok(total_amount) + } + + /// Get active mint quotes + /// Returns mint quotes that are not expired and not yet issued. + #[instrument(skip(self))] + pub async fn get_active_mint_quotes(&self) -> Result, Error> { + let mut mint_quotes = self.localstore.get_mint_quotes().await?; + let unix_time = unix_time(); + mint_quotes.retain(|quote| { + quote.mint_url == self.mint_url + && quote.state != MintQuoteState::Issued + && quote.expiry > unix_time + }); + Ok(mint_quotes) + } + + /// Get unissued mint quotes + /// Returns bolt11 quotes where nothing has been issued yet (amount_issued = 0) and all bolt12 quotes. + /// Includes unpaid bolt11 quotes to allow checking with the mint if they've been paid (wallet state may be outdated). + /// Filters out quotes from other mints. Does not filter by expiry time to allow + /// checking with the mint if expired quotes can still be minted. + #[instrument(skip(self))] + pub async fn get_unissued_mint_quotes(&self) -> Result, Error> { + let mut pending_quotes = self.localstore.get_unissued_mint_quotes().await?; + pending_quotes.retain(|quote| quote.mint_url == self.mint_url); + Ok(pending_quotes) + } + + /// Mint + /// # Synopsis + /// ```rust,no_run + /// use std::sync::Arc; + /// + /// use anyhow::Result; + /// use cdk::amount::{Amount, SplitTarget}; + /// use cdk::nuts::nut00::ProofsMethods; + /// use cdk::nuts::CurrencyUnit; + /// use cdk::wallet::Wallet; + /// use cdk_sqlite::wallet::memory; + /// use rand::random; + /// + /// #[tokio::main] + /// async fn main() -> Result<()> { + /// let seed = random::<[u8; 64]>(); + /// let mint_url = "https://fake.thesimplekid.dev"; + /// let unit = CurrencyUnit::Sat; + /// + /// let localstore = memory::empty().await?; + /// let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), seed, None).unwrap(); + /// let amount = Amount::from(100); + /// + /// let quote = wallet.mint_quote(amount, None).await?; + /// let quote_id = quote.id; + /// // To be called after quote request is paid + /// let minted_proofs = wallet.mint("e_id, SplitTarget::default(), None).await?; + /// let minted_amount = minted_proofs.total_amount()?; + /// + /// Ok(()) + /// } + /// ``` + #[instrument(skip(self))] + pub async fn mint( + &self, + quote_id: &str, + amount_split_target: SplitTarget, + spending_conditions: Option, + ) -> Result { + let active_keyset_id = self.fetch_active_keyset().await?.id; + let fee_and_amounts = self + .get_keyset_fees_and_amounts_by_id(active_keyset_id) + .await?; + + let quote_info = self + .localstore + .get_mint_quote(quote_id) + .await? + .ok_or(Error::UnknownQuote)?; + + if quote_info.payment_method != PaymentMethod::Bolt11 { + return Err(Error::UnsupportedPaymentMethod); + } + + let amount_mintable = quote_info.amount_mintable(); + + if amount_mintable == Amount::ZERO { + tracing::debug!("Amount mintable 0."); + return Err(Error::AmountUndefined); + } + + let unix_time = unix_time(); + + if quote_info.expiry > unix_time { + tracing::warn!("Attempting to mint with expired quote."); + } + + let split_target = match amount_split_target { + SplitTarget::None => { + self.determine_split_target_values(amount_mintable, &fee_and_amounts) + .await? + } + s => s, + }; + + let premint_secrets = match &spending_conditions { + Some(spending_conditions) => PreMintSecrets::with_conditions( + active_keyset_id, + amount_mintable, + &split_target, + spending_conditions, + &fee_and_amounts, + )?, + None => { + let amount_split = + amount_mintable.split_targeted(&split_target, &fee_and_amounts)?; + let num_secrets = amount_split.len() as u32; + + tracing::debug!( + "Incrementing keyset {} counter by {}", + active_keyset_id, + num_secrets + ); + + // Atomically get the counter range we need + let new_counter = self + .localstore + .increment_keyset_counter(&active_keyset_id, num_secrets) + .await?; + + let count = new_counter - num_secrets; + + PreMintSecrets::from_seed( + active_keyset_id, + count, + &self.seed, + amount_mintable, + &split_target, + &fee_and_amounts, + )? + } + }; + + let mut request = MintRequest { + quote: quote_id.to_string(), + outputs: premint_secrets.blinded_messages(), + signature: None, + }; + + if let Some(secret_key) = "e_info.secret_key { + request.sign(secret_key.clone())?; + } + + let mint_res = self.client.post_mint(request).await?; + + let keys = self.load_keyset_keys(active_keyset_id).await?; + + // Verify the signature DLEQ is valid + { + for (sig, premint) in mint_res.signatures.iter().zip(&premint_secrets.secrets) { + let keys = self.load_keyset_keys(sig.keyset_id).await?; + let key = keys.amount_key(sig.amount).ok_or(Error::AmountKey)?; + match sig.verify_dleq(key, premint.blinded_message.blinded_secret) { + Ok(_) | Err(nut12::Error::MissingDleqProof) => (), + Err(_) => return Err(Error::CouldNotVerifyDleq), + } + } + } + + let proofs = construct_proofs( + mint_res.signatures, + premint_secrets.rs(), + premint_secrets.secrets(), + &keys, + )?; + + // Remove filled quote from store + self.localstore.remove_mint_quote("e_info.id).await?; + + let proof_infos = proofs + .iter() + .map(|proof| { + ProofInfo::new( + proof.clone(), + self.mint_url.clone(), + State::Unspent, + quote_info.unit.clone(), + ) + }) + .collect::, _>>()?; + + // Add new proofs to store + self.localstore.update_proofs(proof_infos, vec![]).await?; + + // Add transaction to store + self.localstore + .add_transaction(Transaction { + mint_url: self.mint_url.clone(), + direction: TransactionDirection::Incoming, + amount: proofs.total_amount()?, + fee: Amount::ZERO, + unit: self.unit.clone(), + ys: proofs.ys()?, + timestamp: unix_time, + memo: None, + metadata: HashMap::new(), + quote_id: Some(quote_id.to_string()), + payment_request: Some(quote_info.request), + payment_proof: None, + payment_method: Some(quote_info.payment_method), + }) + .await?; + + Ok(proofs) + } +} diff --git a/crates/cdk/src/wallet/issue/issue_bolt12.rs b/crates/cdk/src/wallet/issue/issue_bolt12.rs new file mode 100644 index 000000000..5efccb063 --- /dev/null +++ b/crates/cdk/src/wallet/issue/issue_bolt12.rs @@ -0,0 +1,274 @@ +use std::collections::HashMap; + +use cdk_common::nut04::MintMethodOptions; +use cdk_common::nut25::MintQuoteBolt12Request; +use cdk_common::wallet::{Transaction, TransactionDirection}; +use cdk_common::{Proofs, SecretKey}; +use tracing::instrument; + +use crate::amount::SplitTarget; +use crate::dhke::construct_proofs; +use crate::nuts::nut00::ProofsMethods; +use crate::nuts::{ + nut12, MintQuoteBolt12Response, MintRequest, PaymentMethod, PreMintSecrets, SpendingConditions, + State, +}; +use crate::types::ProofInfo; +use crate::util::unix_time; +use crate::wallet::MintQuote; +use crate::{Amount, Error, Wallet}; + +impl Wallet { + /// Mint Bolt12 + #[instrument(skip(self))] + pub async fn mint_bolt12_quote( + &self, + amount: Option, + description: Option, + ) -> Result { + let mint_info = self.load_mint_info().await?; + + let mint_url = self.mint_url.clone(); + let unit = &self.unit; + + // If we have a description, we check that the mint supports it. + if description.is_some() { + let mint_method_settings = mint_info + .nuts + .nut04 + .get_settings(unit, &crate::nuts::PaymentMethod::Bolt12) + .ok_or(Error::UnsupportedUnit)?; + + match mint_method_settings.options { + Some(MintMethodOptions::Bolt11 { description }) if description => (), + _ => return Err(Error::InvoiceDescriptionUnsupported), + } + } + + let secret_key = SecretKey::generate(); + + let mint_request = MintQuoteBolt12Request { + amount, + unit: self.unit.clone(), + description, + pubkey: secret_key.public_key(), + }; + + let quote_res = self.client.post_mint_bolt12_quote(mint_request).await?; + + let quote = MintQuote::new( + quote_res.quote, + mint_url, + PaymentMethod::Bolt12, + amount, + unit.clone(), + quote_res.request, + quote_res.expiry.unwrap_or(0), + Some(secret_key), + ); + + self.localstore.add_mint_quote(quote.clone()).await?; + + Ok(quote) + } + + /// Mint bolt12 + #[instrument(skip(self))] + pub async fn mint_bolt12( + &self, + quote_id: &str, + amount: Option, + amount_split_target: SplitTarget, + spending_conditions: Option, + ) -> Result { + let active_keyset_id = self.fetch_active_keyset().await?.id; + let fee_and_amounts = self + .get_keyset_fees_and_amounts_by_id(active_keyset_id) + .await?; + + let quote_info = self.localstore.get_mint_quote(quote_id).await?; + + let quote_info = if let Some(quote) = quote_info { + if quote.expiry.le(&unix_time()) && quote.expiry.ne(&0) { + tracing::info!("Attempting to mint expired quote."); + } + + quote.clone() + } else { + return Err(Error::UnknownQuote); + }; + + let (quote_info, amount) = match amount { + Some(amount) => (quote_info, amount), + None => { + // If an amount it not supplied with check the status of the quote + // The mint will tell us how much can be minted + let state = self.mint_bolt12_quote_state(quote_id).await?; + + let quote_info = self + .localstore + .get_mint_quote(quote_id) + .await? + .ok_or(Error::UnknownQuote)?; + + (quote_info, state.amount_paid - state.amount_issued) + } + }; + + if amount == Amount::ZERO { + tracing::error!("Cannot mint zero amount."); + return Err(Error::UnpaidQuote); + } + + let split_target = match amount_split_target { + SplitTarget::None => { + self.determine_split_target_values(amount, &fee_and_amounts) + .await? + } + s => s, + }; + + let premint_secrets = match &spending_conditions { + Some(spending_conditions) => PreMintSecrets::with_conditions( + active_keyset_id, + amount, + &split_target, + spending_conditions, + &fee_and_amounts, + )?, + None => { + let amount_split = amount.split_targeted(&split_target, &fee_and_amounts)?; + let num_secrets = amount_split.len() as u32; + + tracing::debug!( + "Incrementing keyset {} counter by {}", + active_keyset_id, + num_secrets + ); + + // Atomically get the counter range we need + let new_counter = self + .localstore + .increment_keyset_counter(&active_keyset_id, num_secrets) + .await?; + + let count = new_counter - num_secrets; + + PreMintSecrets::from_seed( + active_keyset_id, + count, + &self.seed, + amount, + &split_target, + &fee_and_amounts, + )? + } + }; + + let mut request = MintRequest { + quote: quote_id.to_string(), + outputs: premint_secrets.blinded_messages(), + signature: None, + }; + + if let Some(secret_key) = quote_info.secret_key.clone() { + request.sign(secret_key)?; + } else { + tracing::error!("Signature is required for bolt12."); + return Err(Error::SignatureMissingOrInvalid); + } + + let mint_res = self.client.post_mint(request).await?; + + let keys = self.load_keyset_keys(active_keyset_id).await?; + + // Verify the signature DLEQ is valid + { + for (sig, premint) in mint_res.signatures.iter().zip(&premint_secrets.secrets) { + let keys = self.load_keyset_keys(sig.keyset_id).await?; + let key = keys.amount_key(sig.amount).ok_or(Error::AmountKey)?; + match sig.verify_dleq(key, premint.blinded_message.blinded_secret) { + Ok(_) | Err(nut12::Error::MissingDleqProof) => (), + Err(_) => return Err(Error::CouldNotVerifyDleq), + } + } + } + + let proofs = construct_proofs( + mint_res.signatures, + premint_secrets.rs(), + premint_secrets.secrets(), + &keys, + )?; + + // Update quote with issued amount + let mut quote_info = self + .localstore + .get_mint_quote(quote_id) + .await? + .ok_or(Error::UnpaidQuote)?; + quote_info.amount_issued += proofs.total_amount()?; + + self.localstore.add_mint_quote(quote_info.clone()).await?; + + let proof_infos = proofs + .iter() + .map(|proof| { + ProofInfo::new( + proof.clone(), + self.mint_url.clone(), + State::Unspent, + quote_info.unit.clone(), + ) + }) + .collect::, _>>()?; + + // Add new proofs to store + self.localstore.update_proofs(proof_infos, vec![]).await?; + + // Add transaction to store + self.localstore + .add_transaction(Transaction { + mint_url: self.mint_url.clone(), + direction: TransactionDirection::Incoming, + amount: proofs.total_amount()?, + fee: Amount::ZERO, + unit: self.unit.clone(), + ys: proofs.ys()?, + timestamp: unix_time(), + memo: None, + metadata: HashMap::new(), + quote_id: Some(quote_id.to_string()), + payment_request: Some(quote_info.request), + payment_proof: None, + payment_method: Some(quote_info.payment_method), + }) + .await?; + + Ok(proofs) + } + + /// Check mint quote status + #[instrument(skip(self, quote_id))] + pub async fn mint_bolt12_quote_state( + &self, + quote_id: &str, + ) -> Result, Error> { + let response = self.client.get_mint_quote_bolt12_status(quote_id).await?; + + match self.localstore.get_mint_quote(quote_id).await? { + Some(quote) => { + let mut quote = quote; + quote.amount_issued = response.amount_issued; + quote.amount_paid = response.amount_paid; + + self.localstore.add_mint_quote(quote).await?; + } + None => { + tracing::info!("Quote mint {} unknown", quote_id); + } + } + + Ok(response) + } +} diff --git a/crates/cdk/src/wallet/melt/bolt11.rs b/crates/cdk/src/wallet/melt/bolt11.rs index c50e8dfce..37f4172f5 100644 --- a/crates/cdk/src/wallet/melt/bolt11.rs +++ b/crates/cdk/src/wallet/melt/bolt11.rs @@ -93,9 +93,7 @@ impl Wallet { payment_method: PaymentMethod::Known(KnownMethod::Bolt11), }; - let mut tx = self.localstore.begin_db_transaction().await?; - tx.add_melt_quote(quote.clone()).await?; - tx.commit().await?; + self.localstore.add_melt_quote(quote.clone()).await?; Ok(quote) } @@ -108,29 +106,25 @@ impl Wallet { ) -> Result, Error> { let response = self.client.get_melt_quote_status(quote_id).await?; - let mut tx = self.localstore.begin_db_transaction().await?; - - match tx.get_melt_quote(quote_id).await? { + match self.localstore.get_melt_quote(quote_id).await? { Some(quote) => { let mut quote = quote; if let Err(e) = self - .add_transaction_for_pending_melt(&mut tx, "e, &response) + .add_transaction_for_pending_melt("e, &response) .await { tracing::error!("Failed to add transaction for pending melt: {}", e); } quote.state = response.state; - tx.add_melt_quote(quote).await?; + self.localstore.add_melt_quote(quote).await?; } None => { tracing::info!("Quote melt {} unknown", quote_id); } } - tx.commit().await?; - Ok(response) } @@ -150,8 +144,8 @@ impl Wallet { metadata: HashMap, ) -> Result { let active_keyset_id = self.fetch_active_keyset().await?.id; - let mut tx = self.localstore.begin_db_transaction().await?; - let mut quote_info = tx + let mut quote_info = self + .localstore .get_melt_quote(quote_id) .await? .ok_or(Error::UnknownQuote)?; @@ -173,7 +167,7 @@ impl Wallet { .map(|p| ProofInfo::new(p, self.mint_url.clone(), State::Pending, self.unit.clone())) .collect::, _>>()?; - tx.update_proofs(proofs_info, vec![]).await?; + self.localstore.update_proofs(proofs_info, vec![]).await?; // Calculate change accounting for input fees // The mint deducts input fees from available funds before calculating change @@ -195,7 +189,8 @@ impl Wallet { ); // Atomically get the counter range we need - let new_counter = tx + let new_counter = self + .localstore .increment_keyset_counter(&active_keyset_id, num_secrets) .await?; @@ -210,8 +205,6 @@ impl Wallet { Some(premint_secrets.blinded_messages()), ); - tx.commit().await?; - let melt_response = match quote_info.payment_method { cdk_common::PaymentMethod::Known(cdk_common::nut00::KnownMethod::Bolt11) => { self.try_proof_operation_or_reclaim( @@ -298,37 +291,36 @@ impl Wallet { None => Vec::new(), }; - let mut tx = self.localstore.begin_db_transaction().await?; - quote_info.state = cdk_common::MeltQuoteState::Paid; let payment_request = quote_info.request.clone(); let payment_method = quote_info.payment_method.clone(); - tx.add_melt_quote(quote_info).await?; + self.localstore.add_melt_quote(quote_info).await?; let deleted_ys = proofs.ys()?; - tx.update_proofs(change_proof_infos, deleted_ys).await?; + self.localstore + .update_proofs(change_proof_infos, deleted_ys) + .await?; // Add transaction to store - tx.add_transaction(Transaction { - mint_url: self.mint_url.clone(), - direction: TransactionDirection::Outgoing, - amount: melted.amount, - fee: melted.fee_paid, - unit: self.unit.clone(), - ys: proofs.ys()?, - timestamp: unix_time(), - memo: None, - metadata, - quote_id: Some(quote_id.to_string()), - payment_request: Some(payment_request), - payment_proof: payment_preimage, - payment_method: Some(payment_method), - }) - .await?; - - tx.commit().await?; + self.localstore + .add_transaction(Transaction { + mint_url: self.mint_url.clone(), + direction: TransactionDirection::Outgoing, + amount: melted.amount, + fee: melted.fee_paid, + unit: self.unit.clone(), + ys: proofs.ys()?, + timestamp: unix_time(), + memo: None, + metadata, + quote_id: Some(quote_id.to_string()), + payment_request: Some(payment_request), + payment_proof: payment_preimage, + payment_method: Some(payment_method), + }) + .await?; Ok(melted) } diff --git a/crates/cdk/src/wallet/melt/bolt12.rs b/crates/cdk/src/wallet/melt/bolt12.rs index f28ca7fc4..e3aac0fa6 100644 --- a/crates/cdk/src/wallet/melt/bolt12.rs +++ b/crates/cdk/src/wallet/melt/bolt12.rs @@ -63,9 +63,7 @@ impl Wallet { payment_method: PaymentMethod::Known(KnownMethod::Bolt12), }; - let mut tx = self.localstore.begin_db_transaction().await?; - tx.add_melt_quote(quote.clone()).await?; - tx.commit().await?; + self.localstore.add_melt_quote(quote.clone()).await?; Ok(quote) } @@ -78,29 +76,25 @@ impl Wallet { ) -> Result, Error> { let response = self.client.get_melt_bolt12_quote_status(quote_id).await?; - let mut tx = self.localstore.begin_db_transaction().await?; - - match tx.get_melt_quote(quote_id).await? { + match self.localstore.get_melt_quote(quote_id).await? { Some(quote) => { let mut quote = quote; if let Err(e) = self - .add_transaction_for_pending_melt(&mut tx, "e, &response) + .add_transaction_for_pending_melt("e, &response) .await { tracing::error!("Failed to add transaction for pending melt: {}", e); } quote.state = response.state; - tx.add_melt_quote(quote).await?; + self.localstore.add_melt_quote(quote).await?; } None => { tracing::info!("Quote melt {} unknown", quote_id); } } - tx.commit().await?; - Ok(response) } } diff --git a/crates/cdk/src/wallet/melt/custom.rs b/crates/cdk/src/wallet/melt/custom.rs index e8274c832..4c0c51149 100644 --- a/crates/cdk/src/wallet/melt/custom.rs +++ b/crates/cdk/src/wallet/melt/custom.rs @@ -42,9 +42,7 @@ impl Wallet { payment_preimage: quote_res.payment_preimage, payment_method: PaymentMethod::Custom(method.to_string()), }; - let mut tx = self.localstore.begin_db_transaction().await?; - tx.add_melt_quote(quote.clone()).await?; - tx.commit().await?; + self.localstore.add_melt_quote(quote.clone()).await?; Ok(quote) } diff --git a/crates/cdk/src/wallet/melt/melt_bolt11.rs b/crates/cdk/src/wallet/melt/melt_bolt11.rs new file mode 100644 index 000000000..3c4302091 --- /dev/null +++ b/crates/cdk/src/wallet/melt/melt_bolt11.rs @@ -0,0 +1,519 @@ +use std::collections::HashMap; +use std::str::FromStr; + +use cdk_common::amount::SplitTarget; +use cdk_common::wallet::{Transaction, TransactionDirection}; +use cdk_common::PaymentMethod; +use lightning_invoice::Bolt11Invoice; +use tracing::instrument; + +use crate::amount::to_unit; +use crate::dhke::construct_proofs; +use crate::nuts::nut00::ProofsMethods; +use crate::nuts::{ + CurrencyUnit, MeltOptions, MeltQuoteBolt11Request, MeltQuoteBolt11Response, MeltRequest, + PreMintSecrets, Proofs, State, +}; +use crate::types::{Melted, ProofInfo}; +use crate::util::unix_time; +use crate::wallet::send::split_proofs_for_send; +use crate::wallet::MeltQuote; +use crate::{ensure_cdk, Amount, Error, Wallet}; + +impl Wallet { + /// Melt Quote + /// # Synopsis + /// ```rust,no_run + /// use std::sync::Arc; + /// + /// use cdk_sqlite::wallet::memory; + /// use cdk::nuts::CurrencyUnit; + /// use cdk::wallet::Wallet; + /// use rand::random; + /// + /// #[tokio::main] + /// async fn main() -> anyhow::Result<()> { + /// let seed = random::<[u8; 64]>(); + /// let mint_url = "https://fake.thesimplekid.dev"; + /// let unit = CurrencyUnit::Sat; + /// + /// let localstore = memory::empty().await?; + /// let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), seed, None).unwrap(); + /// let bolt11 = "lnbc100n1pnvpufspp5djn8hrq49r8cghwye9kqw752qjncwyfnrprhprpqk43mwcy4yfsqdq5g9kxy7fqd9h8vmmfvdjscqzzsxqyz5vqsp5uhpjt36rj75pl7jq2sshaukzfkt7uulj456s4mh7uy7l6vx7lvxs9qxpqysgqedwz08acmqwtk8g4vkwm2w78suwt2qyzz6jkkwcgrjm3r3hs6fskyhvud4fan3keru7emjm8ygqpcrwtlmhfjfmer3afs5hhwamgr4cqtactdq".to_string(); + /// let quote = wallet.melt_quote(bolt11, None).await?; + /// + /// Ok(()) + /// } + /// ``` + #[instrument(skip(self, request))] + pub async fn melt_quote( + &self, + request: String, + options: Option, + ) -> Result { + let invoice = Bolt11Invoice::from_str(&request)?; + + let quote_request = MeltQuoteBolt11Request { + request: invoice.clone(), + unit: self.unit.clone(), + options, + }; + + let quote_res = self.client.post_melt_quote(quote_request).await?; + + if self.unit == CurrencyUnit::Msat || self.unit == CurrencyUnit::Sat { + let amount_msat = options + .map(|opt| opt.amount_msat().into()) + .or_else(|| invoice.amount_milli_satoshis()) + .ok_or(Error::InvoiceAmountUndefined)?; + + let amount_quote_unit = to_unit(amount_msat, &CurrencyUnit::Msat, &self.unit)?; + + if quote_res.amount != amount_quote_unit { + tracing::warn!( + "Mint returned incorrect quote amount. Expected {}, got {}", + amount_quote_unit, + quote_res.amount + ); + return Err(Error::IncorrectQuoteAmount); + } + } + + let quote = MeltQuote { + id: quote_res.quote, + amount: quote_res.amount, + request, + unit: self.unit.clone(), + fee_reserve: quote_res.fee_reserve, + state: quote_res.state, + expiry: quote_res.expiry, + payment_preimage: quote_res.payment_preimage, + payment_method: PaymentMethod::Bolt11, + }; + + self.localstore.add_melt_quote(quote.clone()).await?; + + Ok(quote) + } + + /// Melt quote status + #[instrument(skip(self, quote_id))] + pub async fn melt_quote_status( + &self, + quote_id: &str, + ) -> Result, Error> { + let response = self.client.get_melt_quote_status(quote_id).await?; + + match self.localstore.get_melt_quote(quote_id).await? { + Some(quote) => { + let mut quote = quote; + + if let Err(e) = self + .add_transaction_for_pending_melt("e, &response) + .await + { + tracing::error!("Failed to add transaction for pending melt: {}", e); + } + + quote.state = response.state; + self.localstore.add_melt_quote(quote).await?; + } + None => { + tracing::info!("Quote melt {} unknown", quote_id); + } + } + + Ok(response) + } + + /// Melt specific proofs + #[instrument(skip(self, proofs))] + pub async fn melt_proofs(&self, quote_id: &str, proofs: Proofs) -> Result { + self.melt_proofs_with_metadata(quote_id, proofs, HashMap::new()) + .await + } + + /// Melt specific proofs + #[instrument(skip(self, proofs))] + pub async fn melt_proofs_with_metadata( + &self, + quote_id: &str, + proofs: Proofs, + metadata: HashMap, + ) -> Result { + let active_keyset_id = self.fetch_active_keyset().await?.id; + let mut quote_info = self + .localstore + .get_melt_quote(quote_id) + .await? + .ok_or(Error::UnknownQuote)?; + + ensure_cdk!( + quote_info.expiry.gt(&unix_time()), + Error::ExpiredQuote(quote_info.expiry, unix_time()) + ); + + let proofs_total = proofs.total_amount()?; + if proofs_total < quote_info.amount + quote_info.fee_reserve { + return Err(Error::InsufficientFunds); + } + + // Since the proofs may be external (not in our database), add them first + let proofs_info = proofs + .clone() + .into_iter() + .map(|p| ProofInfo::new(p, self.mint_url.clone(), State::Pending, self.unit.clone())) + .collect::, _>>()?; + + self.localstore.update_proofs(proofs_info, vec![]).await?; + + // Calculate change accounting for input fees + // The mint deducts input fees from available funds before calculating change + let input_fee = self.get_proofs_fee(&proofs).await?.total; + let change_amount = proofs_total - quote_info.amount - input_fee; + + let premint_secrets = if change_amount <= Amount::ZERO { + PreMintSecrets::new(active_keyset_id) + } else { + // TODO: consolidate this calculation with from_seed_blank into a shared function + // Calculate how many secrets will be needed using the same logic as from_seed_blank + let num_secrets = + ((u64::from(change_amount) as f64).log2().ceil() as u64).max(1) as u32; + + tracing::debug!( + "Incrementing keyset {} counter by {}", + active_keyset_id, + num_secrets + ); + + // Atomically get the counter range we need + let new_counter = self + .localstore + .increment_keyset_counter(&active_keyset_id, num_secrets) + .await?; + + let count = new_counter - num_secrets; + + PreMintSecrets::from_seed_blank(active_keyset_id, count, &self.seed, change_amount)? + }; + + let request = MeltRequest::new( + quote_id.to_string(), + proofs.clone(), + Some(premint_secrets.blinded_messages()), + ); + + let melt_response = match quote_info.payment_method { + cdk_common::PaymentMethod::Bolt11 => { + self.try_proof_operation_or_reclaim( + request.inputs().clone(), + self.client.post_melt(request), + ) + .await? + } + cdk_common::PaymentMethod::Bolt12 => { + self.try_proof_operation_or_reclaim( + request.inputs().clone(), + self.client.post_melt_bolt12(request), + ) + .await? + } + cdk_common::PaymentMethod::Custom(_) => { + return Err(Error::UnsupportedPaymentMethod); + } + }; + + let active_keys = self.load_keyset_keys(active_keyset_id).await?; + + let change_proofs = match melt_response.change { + Some(change) => { + let num_change_proof = change.len(); + + let num_change_proof = match ( + premint_secrets.len() < num_change_proof, + premint_secrets.secrets().len() < num_change_proof, + ) { + (true, _) | (_, true) => { + tracing::error!("Mismatch in change promises to change"); + premint_secrets.len() + } + _ => num_change_proof, + }; + + Some(construct_proofs( + change, + premint_secrets.rs()[..num_change_proof].to_vec(), + premint_secrets.secrets()[..num_change_proof].to_vec(), + &active_keys, + )?) + } + None => None, + }; + + let payment_preimage = melt_response.payment_preimage.clone(); + + let melted = Melted::from_proofs( + melt_response.state, + melt_response.payment_preimage, + quote_info.amount, + proofs.clone(), + change_proofs.clone(), + )?; + + let change_proof_infos = match change_proofs { + Some(change_proofs) => { + tracing::debug!( + "Change amount returned from melt: {}", + change_proofs.total_amount()? + ); + + change_proofs + .into_iter() + .map(|proof| { + ProofInfo::new( + proof, + self.mint_url.clone(), + State::Unspent, + quote_info.unit.clone(), + ) + }) + .collect::, _>>()? + } + None => Vec::new(), + }; + + quote_info.state = cdk_common::MeltQuoteState::Paid; + + let payment_request = quote_info.request.clone(); + let payment_method = quote_info.payment_method.clone(); + self.localstore.add_melt_quote(quote_info).await?; + + let deleted_ys = proofs.ys()?; + + self.localstore + .update_proofs(change_proof_infos, deleted_ys) + .await?; + + // Add transaction to store + self.localstore + .add_transaction(Transaction { + mint_url: self.mint_url.clone(), + direction: TransactionDirection::Outgoing, + amount: melted.amount, + fee: melted.fee_paid, + unit: self.unit.clone(), + ys: proofs.ys()?, + timestamp: unix_time(), + memo: None, + metadata, + quote_id: Some(quote_id.to_string()), + payment_request: Some(payment_request), + payment_proof: payment_preimage, + payment_method: Some(payment_method), + }) + .await?; + + Ok(melted) + } + + /// Melt + /// # Synopsis + /// ```rust, no_run + /// use std::sync::Arc; + /// + /// use cdk_sqlite::wallet::memory; + /// use cdk::nuts::CurrencyUnit; + /// use cdk::wallet::Wallet; + /// use rand::random; + /// + /// #[tokio::main] + /// async fn main() -> anyhow::Result<()> { + /// let seed = random::<[u8; 64]>(); + /// let mint_url = "https://fake.thesimplekid.dev"; + /// let unit = CurrencyUnit::Sat; + /// + /// let localstore = memory::empty().await?; + /// let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), seed, None).unwrap(); + /// let bolt11 = "lnbc100n1pnvpufspp5djn8hrq49r8cghwye9kqw752qjncwyfnrprhprpqk43mwcy4yfsqdq5g9kxy7fqd9h8vmmfvdjscqzzsxqyz5vqsp5uhpjt36rj75pl7jq2sshaukzfkt7uulj456s4mh7uy7l6vx7lvxs9qxpqysgqedwz08acmqwtk8g4vkwm2w78suwt2qyzz6jkkwcgrjm3r3hs6fskyhvud4fan3keru7emjm8ygqpcrwtlmhfjfmer3afs5hhwamgr4cqtactdq".to_string(); + /// let quote = wallet.melt_quote(bolt11, None).await?; + /// let quote_id = quote.id; + /// + /// let _ = wallet.melt("e_id).await?; + /// + /// Ok(()) + /// } + #[instrument(skip(self))] + pub async fn melt(&self, quote_id: &str) -> Result { + self.melt_with_metadata(quote_id, HashMap::new()).await + } + + /// Melt with additional metadata to be saved locally with the transaction + /// # Synopsis + /// ```rust, no_run + /// use std::sync::Arc; + /// + /// use cdk_sqlite::wallet::memory; + /// use cdk::nuts::CurrencyUnit; + /// use cdk::wallet::Wallet; + /// use rand::random; + /// + /// #[tokio::main] + /// async fn main() -> anyhow::Result<()> { + /// let seed = random::<[u8; 64]>(); + /// let mint_url = "https://fake.thesimplekid.dev"; + /// let unit = CurrencyUnit::Sat; + /// + /// let localstore = memory::empty().await?; + /// let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), seed, None).unwrap(); + /// let bolt11 = "lnbc100n1pnvpufspp5djn8hrq49r8cghwye9kqw752qjncwyfnrprhprpqk43mwcy4yfsqdq5g9kxy7fqd9h8vmmfvdjscqzzsxqyz5vqsp5uhpjt36rj75pl7jq2sshaukzfkt7uulj456s4mh7uy7l6vx7lvxs9qxpqysgqedwz08acmqwtk8g4vkwm2w78suwt2qyzz6jkkwcgrjm3r3hs6fskyhvud4fan3keru7emjm8ygqpcrwtlmhfjfmer3afs5hhwamgr4cqtactdq".to_string(); + /// let quote = wallet.melt_quote(bolt11, None).await?; + /// let quote_id = quote.id; + /// + /// let mut metadata = std::collections::HashMap::new(); + /// metadata.insert("my key".to_string(), "my value".to_string()); + /// + /// let _ = wallet.melt_with_metadata("e_id, metadata).await?; + /// + /// Ok(()) + /// } + #[instrument(skip(self))] + pub async fn melt_with_metadata( + &self, + quote_id: &str, + metadata: HashMap, + ) -> Result { + let quote_info = self + .localstore + .get_melt_quote(quote_id) + .await? + .ok_or(Error::UnknownQuote)?; + + ensure_cdk!( + quote_info.expiry.gt(&unix_time()), + Error::ExpiredQuote(quote_info.expiry, unix_time()) + ); + + let inputs_needed_amount = quote_info.amount + quote_info.fee_reserve; + + let active_keyset_ids = self + .get_mint_keysets() + .await? + .into_iter() + .map(|k| k.id) + .collect(); + let keyset_fees_and_amounts = self.get_keyset_fees_and_amounts().await?; + + let available_proofs = self.get_unspent_proofs().await?; + + // Two-step proof selection for melt: + // Step 1: Try to select proofs that exactly match inputs_needed_amount. + // If successful, no swap is required and we avoid paying swap fees. + // Step 2: If exact match not possible, we need to swap to get optimal denominations. + // In this case, we must select more proofs to cover the additional swap fees. + { + let input_proofs = Wallet::select_proofs( + inputs_needed_amount, + available_proofs.clone(), + &active_keyset_ids, + &keyset_fees_and_amounts, + true, + )?; + let proofs_total = input_proofs.total_amount()?; + + // If exact match, use proofs directly without swap + if proofs_total == inputs_needed_amount { + return self + .melt_proofs_with_metadata(quote_id, input_proofs, metadata) + .await; + } + } + + let active_keyset_id = self.get_active_keyset().await?.id; + let fee_and_amounts = self + .get_keyset_fees_and_amounts_by_id(active_keyset_id) + .await?; + + // Calculate optimal denomination split and the fee for those proofs + // First estimate based on inputs_needed_amount to get target_fee + let initial_split = inputs_needed_amount.split(&fee_and_amounts); + let target_fee = self + .get_proofs_fee_by_count( + vec![(active_keyset_id, initial_split.len() as u64)] + .into_iter() + .collect(), + ) + .await? + .total; + + // Since we could not select the correct inputs amount needed for melting, + // we select again this time including the amount we will now have to pay as a fee for the swap. + let inputs_total_needed = inputs_needed_amount + target_fee; + + // Recalculate target amounts based on the actual total we need (including fee) + let target_amounts = inputs_total_needed.split(&fee_and_amounts); + let input_proofs = Wallet::select_proofs( + inputs_total_needed, + available_proofs, + &active_keyset_ids, + &keyset_fees_and_amounts, + true, + )?; + let proofs_total = input_proofs.total_amount()?; + + // Need to swap to get exact denominations + tracing::debug!( + "Proofs total {} != inputs needed {}, swapping to get exact amount", + proofs_total, + inputs_total_needed + ); + + let keyset_fees: HashMap = keyset_fees_and_amounts + .iter() + .map(|(key, values)| (*key, values.fee())) + .collect(); + + let split_result = split_proofs_for_send( + input_proofs, + &target_amounts, + inputs_total_needed, + target_fee, + &keyset_fees, + false, + false, + )?; + + let mut final_proofs = split_result.proofs_to_send; + + if !split_result.proofs_to_swap.is_empty() { + let swap_amount = inputs_total_needed + .checked_sub(final_proofs.total_amount()?) + .ok_or(Error::AmountOverflow)?; + + tracing::debug!( + "Swapping {} proofs to get {} sats (swap fee: {} sats)", + split_result.proofs_to_swap.len(), + swap_amount, + split_result.swap_fee + ); + + if let Some(swapped) = self + .try_proof_operation_or_reclaim( + split_result.proofs_to_swap.clone(), + self.swap( + Some(swap_amount), + SplitTarget::None, + split_result.proofs_to_swap, + None, + false, // fees already accounted for in inputs_total_needed + ), + ) + .await? + { + final_proofs.extend(swapped); + } + } + + self.melt_proofs_with_metadata(quote_id, final_proofs, metadata) + .await + } +} diff --git a/crates/cdk/src/wallet/melt/melt_bolt12.rs b/crates/cdk/src/wallet/melt/melt_bolt12.rs new file mode 100644 index 000000000..c6ec23994 --- /dev/null +++ b/crates/cdk/src/wallet/melt/melt_bolt12.rs @@ -0,0 +1,98 @@ +//! Melt BOLT12 +//! +//! Implementation of melt functionality for BOLT12 offers + +use std::str::FromStr; + +use cdk_common::amount::amount_for_offer; +use cdk_common::wallet::MeltQuote; +use cdk_common::PaymentMethod; +use lightning::offers::offer::Offer; +use tracing::instrument; + +use crate::amount::to_unit; +use crate::nuts::{CurrencyUnit, MeltOptions, MeltQuoteBolt11Response, MeltQuoteBolt12Request}; +use crate::{Error, Wallet}; + +impl Wallet { + /// Melt Quote for BOLT12 offer + #[instrument(skip(self, request))] + pub async fn melt_bolt12_quote( + &self, + request: String, + options: Option, + ) -> Result { + let quote_request = MeltQuoteBolt12Request { + request: request.clone(), + unit: self.unit.clone(), + options, + }; + + let quote_res = self.client.post_melt_bolt12_quote(quote_request).await?; + + if self.unit == CurrencyUnit::Sat || self.unit == CurrencyUnit::Msat { + let offer = Offer::from_str(&request).map_err(|_| Error::Bolt12parse)?; + // Get amount from offer or options + let amount_msat = options + .map(|opt| opt.amount_msat()) + .or_else(|| amount_for_offer(&offer, &CurrencyUnit::Msat).ok()) + .ok_or(Error::AmountUndefined)?; + let amount_quote_unit = to_unit(amount_msat, &CurrencyUnit::Msat, &self.unit)?; + + if quote_res.amount != amount_quote_unit { + tracing::warn!( + "Mint returned incorrect quote amount. Expected {}, got {}", + amount_quote_unit, + quote_res.amount + ); + return Err(Error::IncorrectQuoteAmount); + } + } + + let quote = MeltQuote { + id: quote_res.quote, + amount: quote_res.amount, + request, + unit: self.unit.clone(), + fee_reserve: quote_res.fee_reserve, + state: quote_res.state, + expiry: quote_res.expiry, + payment_preimage: quote_res.payment_preimage, + payment_method: PaymentMethod::Bolt12, + }; + + self.localstore.add_melt_quote(quote.clone()).await?; + + Ok(quote) + } + + /// BOLT12 melt quote status + #[instrument(skip(self, quote_id))] + pub async fn melt_bolt12_quote_status( + &self, + quote_id: &str, + ) -> Result, Error> { + let response = self.client.get_melt_bolt12_quote_status(quote_id).await?; + + match self.localstore.get_melt_quote(quote_id).await? { + Some(quote) => { + let mut quote = quote; + + if let Err(e) = self + .add_transaction_for_pending_melt("e, &response) + .await + { + tracing::error!("Failed to add transaction for pending melt: {}", e); + } + + quote.state = response.state; + self.localstore.add_melt_quote(quote).await?; + } + None => { + tracing::info!("Quote melt {} unknown", quote_id); + } + } + + Ok(response) + } +} diff --git a/crates/cdk/src/wallet/melt/mod.rs b/crates/cdk/src/wallet/melt/mod.rs index 0ec62e80f..0073d7bd4 100644 --- a/crates/cdk/src/wallet/melt/mod.rs +++ b/crates/cdk/src/wallet/melt/mod.rs @@ -1,6 +1,5 @@ use std::collections::HashMap; -use cdk_common::database::DynWalletDatabaseTransaction; use cdk_common::util::unix_time; use cdk_common::wallet::{MeltQuote, Transaction, TransactionDirection}; use cdk_common::{ @@ -54,7 +53,6 @@ impl Wallet { pub(crate) async fn add_transaction_for_pending_melt( &self, - tx: &mut DynWalletDatabaseTransaction, quote: &MeltQuote, response: &MeltQuoteBolt11Response, ) -> Result<(), Error> { @@ -67,30 +65,31 @@ impl Wallet { ); if response.state == MeltQuoteState::Paid { let pending_proofs = self - .get_proofs_with(Some(tx), Some(vec![State::Pending]), None) + .get_proofs_with(Some(vec![State::Pending]), None) .await?; let proofs_total = pending_proofs.total_amount().unwrap_or_default(); let change_total = response.change_amount().unwrap_or_default(); - tx.add_transaction(Transaction { - mint_url: self.mint_url.clone(), - direction: TransactionDirection::Outgoing, - amount: response.amount, - fee: proofs_total - .checked_sub(response.amount) - .and_then(|amt| amt.checked_sub(change_total)) - .unwrap_or_default(), - unit: quote.unit.clone(), - ys: pending_proofs.ys()?, - timestamp: unix_time(), - memo: None, - metadata: HashMap::new(), - quote_id: Some(quote.id.clone()), - payment_request: Some(quote.request.clone()), - payment_proof: response.payment_preimage.clone(), - payment_method: Some(quote.payment_method.clone()), - }) - .await?; + self.localstore + .add_transaction(Transaction { + mint_url: self.mint_url.clone(), + direction: TransactionDirection::Outgoing, + amount: response.amount, + fee: proofs_total + .checked_sub(response.amount) + .and_then(|amt| amt.checked_sub(change_total)) + .unwrap_or_default(), + unit: quote.unit.clone(), + ys: pending_proofs.ys()?, + timestamp: unix_time(), + memo: None, + metadata: HashMap::new(), + quote_id: Some(quote.id.clone()), + payment_request: Some(quote.request.clone()), + payment_proof: response.payment_preimage.clone(), + payment_method: Some(quote.payment_method.clone()), + }) + .await?; } } Ok(()) diff --git a/crates/cdk/src/wallet/mint_metadata_cache.rs b/crates/cdk/src/wallet/mint_metadata_cache.rs index cecb8576b..4473d41d7 100644 --- a/crates/cdk/src/wallet/mint_metadata_cache.rs +++ b/crates/cdk/src/wallet/mint_metadata_cache.rs @@ -462,18 +462,9 @@ impl MintMetadataCache { versions.insert(storage_id, metadata.status.version); } - let mut tx = if let Ok(ok) = storage - .begin_db_transaction() - .await - .inspect_err(|err| tracing::warn!("Could not begin database transaction: {err}")) - { - ok - } else { - return; - }; - // Save mint info - tx.add_mint(mint_url.clone(), Some(metadata.mint_info.clone())) + storage + .add_mint(mint_url.clone(), Some(metadata.mint_info.clone())) .await .inspect_err(|e| tracing::warn!("Failed to save mint info for {}: {}", mint_url, e)) .ok(); @@ -482,7 +473,8 @@ impl MintMetadataCache { let keysets: Vec<_> = metadata.keysets.values().map(|ks| (**ks).clone()).collect(); if !keysets.is_empty() { - tx.add_mint_keysets(mint_url.clone(), keysets) + storage + .add_mint_keysets(mint_url.clone(), keysets) .await .inspect_err(|e| tracing::warn!("Failed to save keysets for {}: {}", mint_url, e)) .ok(); @@ -492,7 +484,7 @@ impl MintMetadataCache { for (keyset_id, keys) in &metadata.keys { if let Some(keyset_info) = metadata.keysets.get(keyset_id) { // Check if keys already exist in database to avoid duplicate insertion - if tx.get_keys(keyset_id).await.ok().flatten().is_some() { + if storage.get_keys(keyset_id).await.ok().flatten().is_some() { tracing::trace!( "Keys for keyset {} already in database, skipping insert", keyset_id @@ -507,7 +499,8 @@ impl MintMetadataCache { keys: (**keys).clone(), }; - tx.add_keys(keyset) + storage + .add_keys(keyset) .await .inspect_err(|e| { tracing::warn!( @@ -520,8 +513,6 @@ impl MintMetadataCache { .ok(); } } - - let _ = tx.commit().await.ok(); } /// Fetch fresh metadata from mint HTTP API and update cache diff --git a/crates/cdk/src/wallet/mod.rs b/crates/cdk/src/wallet/mod.rs index b0c89cf7a..890dd3fbe 100644 --- a/crates/cdk/src/wallet/mod.rs +++ b/crates/cdk/src/wallet/mod.rs @@ -8,7 +8,7 @@ use std::sync::Arc; use std::time::Duration; use cdk_common::amount::FeeAndAmounts; -use cdk_common::database::{self, DynWalletDatabaseTransaction, WalletDatabase}; +use cdk_common::database::{self, WalletDatabase}; use cdk_common::parking_lot::RwLock; use cdk_common::subscription::WalletParams; use getrandom::getrandom; @@ -280,10 +280,9 @@ impl Wallet { #[instrument(skip(self))] pub async fn update_mint_url(&mut self, new_mint_url: MintUrl) -> Result<(), Error> { // Update the mint URL in the wallet DB - let mut tx = self.localstore.begin_db_transaction().await?; - tx.update_mint_url(self.mint_url.clone(), new_mint_url.clone()) + self.localstore + .update_mint_url(self.mint_url.clone(), new_mint_url.clone()) .await?; - tx.commit().await?; // Update the mint URL in the wallet struct field self.mint_url = new_mint_url; @@ -377,14 +376,13 @@ impl Wallet { } /// Get amounts needed to refill proof state - #[instrument(skip(self, tx))] + #[instrument(skip(self))] pub(crate) async fn amounts_needed_for_state_target( &self, - tx: &mut DynWalletDatabaseTransaction, fee_and_amounts: &FeeAndAmounts, ) -> Result, Error> { let unspent_proofs = self - .get_proofs_with(Some(tx), Some(vec![State::Unspent]), None) + .get_proofs_with(Some(vec![State::Unspent]), None) .await?; let amounts_count: HashMap = @@ -415,15 +413,14 @@ impl Wallet { } /// Determine [`SplitTarget`] for amount based on state - #[instrument(skip(self, tx))] + #[instrument(skip(self))] async fn determine_split_target_values( &self, - tx: &mut DynWalletDatabaseTransaction, change_amount: Amount, fee_and_amounts: &FeeAndAmounts, ) -> Result { let mut amounts_needed_refill = self - .amounts_needed_for_state_target(tx, fee_and_amounts) + .amounts_needed_for_state_target(fee_and_amounts) .await?; amounts_needed_refill.sort(); @@ -552,9 +549,9 @@ impl Wallet { }) .collect::, _>>()?; - let mut tx = self.localstore.begin_db_transaction().await?; - tx.update_proofs(unspent_proofs, vec![]).await?; - tx.commit().await?; + self.localstore + .update_proofs(unspent_proofs, vec![]) + .await?; empty_batch = 0; start_counter += 100; @@ -563,9 +560,9 @@ impl Wallet { // Set counter to highest found + 1 to avoid reusing any counter values // that already have signatures at the mint if let Some(highest) = highest_counter { - let mut tx = self.localstore.begin_db_transaction().await?; - tx.increment_keyset_counter(&keyset.id, highest + 1).await?; - tx.commit().await?; + self.localstore + .increment_keyset_counter(&keyset.id, highest + 1) + .await?; tracing::debug!( "Set keyset {} counter to {} after restore", keyset.id, diff --git a/crates/cdk/src/wallet/proofs.rs b/crates/cdk/src/wallet/proofs.rs index 9e22b2b64..4cc07eed8 100644 --- a/crates/cdk/src/wallet/proofs.rs +++ b/crates/cdk/src/wallet/proofs.rs @@ -1,7 +1,6 @@ use std::collections::{HashMap, HashSet}; use cdk_common::amount::KeysetFeeAndAmounts; -use cdk_common::database::DynWalletDatabaseTransaction; use cdk_common::wallet::TransactionId; use cdk_common::Id; use tracing::instrument; @@ -19,40 +18,38 @@ impl Wallet { /// Get unspent proofs for mint #[instrument(skip(self))] pub async fn get_unspent_proofs(&self) -> Result { - self.get_proofs_with(None, Some(vec![State::Unspent]), None) - .await + self.get_proofs_with(Some(vec![State::Unspent]), None).await } /// Get pending [`Proofs`] #[instrument(skip(self))] pub async fn get_pending_proofs(&self) -> Result { - self.get_proofs_with(None, Some(vec![State::Pending]), None) - .await + self.get_proofs_with(Some(vec![State::Pending]), None).await } /// Get reserved [`Proofs`] #[instrument(skip(self))] pub async fn get_reserved_proofs(&self) -> Result { - self.get_proofs_with(None, Some(vec![State::Reserved]), None) + self.get_proofs_with(Some(vec![State::Reserved]), None) .await } /// Get pending spent [`Proofs`] #[instrument(skip(self))] pub async fn get_pending_spent_proofs(&self) -> Result { - self.get_proofs_with(None, Some(vec![State::PendingSpent]), None) + self.get_proofs_with(Some(vec![State::PendingSpent]), None) .await } /// Get this wallet's [Proofs] that match the args pub async fn get_proofs_with( &self, - tx: Option<&mut DynWalletDatabaseTransaction>, state: Option>, spending_conditions: Option>, ) -> Result { - Ok(if let Some(tx) = tx { - tx.get_proofs( + Ok(self + .localstore + .get_proofs( Some(self.mint_url.clone()), Some(self.unit.clone()), state, @@ -61,28 +58,16 @@ impl Wallet { .await? .into_iter() .map(|p| p.proof) - .collect() - } else { - self.localstore - .get_proofs( - Some(self.mint_url.clone()), - Some(self.unit.clone()), - state, - spending_conditions, - ) - .await? - .into_iter() - .map(|p| p.proof) - .collect() - }) + .collect()) } /// Return proofs to unspent allowing them to be selected and spent #[instrument(skip(self))] pub async fn unreserve_proofs(&self, ys: Vec) -> Result<(), Error> { - let mut tx = self.localstore.begin_db_transaction().await?; - tx.update_proofs_state(ys, State::Unspent).await?; - Ok(tx.commit().await?) + self.localstore + .update_proofs_state(ys, State::Unspent) + .await?; + Ok(()) } /// Reclaim unspent proofs @@ -108,14 +93,13 @@ impl Wallet { self.swap(None, SplitTarget::default(), unspent, None, false) .await?; - let mut tx = self.localstore.begin_db_transaction().await?; - let _ = tx + let _ = self + .localstore .remove_transaction(transaction_id) .await .inspect_err(|err| { tracing::warn!("Failed to remove transaction: {:?}", err); }); - tx.commit().await?; Ok(()) } @@ -137,9 +121,7 @@ impl Wallet { }) .collect(); - let mut tx = self.localstore.begin_db_transaction().await?; - tx.update_proofs(vec![], spent_ys).await?; - tx.commit().await?; + self.localstore.update_proofs(vec![], spent_ys).await?; Ok(spendable.states) } @@ -183,13 +165,12 @@ impl Wallet { let amount = Amount::try_sum(pending_proofs.iter().map(|p| p.proof.amount))?; - let mut tx = self.localstore.begin_db_transaction().await?; - tx.update_proofs( - vec![], - non_pending_proofs.into_iter().map(|p| p.y).collect(), - ) - .await?; - tx.commit().await?; + self.localstore + .update_proofs( + vec![], + non_pending_proofs.into_iter().map(|p| p.y).collect(), + ) + .await?; balance += amount; diff --git a/crates/cdk/src/wallet/receive.rs b/crates/cdk/src/wallet/receive.rs index 9684f6f22..e08f9d618 100644 --- a/crates/cdk/src/wallet/receive.rs +++ b/crates/cdk/src/wallet/receive.rs @@ -120,12 +120,12 @@ impl Wallet { .map(|p| ProofInfo::new(p, self.mint_url.clone(), State::Pending, self.unit.clone())) .collect::, _>>()?; - let mut tx = self.localstore.begin_db_transaction().await?; - tx.update_proofs(proofs_info.clone(), vec![]).await?; + self.localstore + .update_proofs(proofs_info.clone(), vec![]) + .await?; let mut pre_swap = self .create_swap( - tx, active_keyset_id, &fee_and_amounts, None, @@ -151,10 +151,9 @@ impl Wallet { tracing::error!("Failed to post swap request: {}", err); // Remove the pending proofs we added since the swap failed - let mut tx = self.localstore.begin_db_transaction().await?; - tx.update_proofs(vec![], proofs_info.into_iter().map(|p| p.y).collect()) + self.localstore + .update_proofs(vec![], proofs_info.into_iter().map(|p| p.y).collect()) .await?; - tx.commit().await?; return Err(err); } @@ -168,8 +167,8 @@ impl Wallet { &keys, )?; - let mut tx = self.localstore.begin_db_transaction().await?; - tx.increment_keyset_counter(&active_keyset_id, recv_proofs.len() as u32) + self.localstore + .increment_keyset_counter(&active_keyset_id, recv_proofs.len() as u32) .await?; let total_amount = recv_proofs.total_amount()?; @@ -179,31 +178,31 @@ impl Wallet { .map(|proof| ProofInfo::new(proof, mint_url.clone(), State::Unspent, self.unit.clone())) .collect::, _>>()?; - tx.update_proofs( - recv_proof_infos, - proofs_info.into_iter().map(|p| p.y).collect(), - ) - .await?; + self.localstore + .update_proofs( + recv_proof_infos, + proofs_info.into_iter().map(|p| p.y).collect(), + ) + .await?; // Add transaction to store - tx.add_transaction(Transaction { - mint_url: self.mint_url.clone(), - direction: TransactionDirection::Incoming, - amount: total_amount, - fee: proofs_amount - total_amount, - unit: self.unit.clone(), - ys: proofs_ys, - timestamp: unix_time(), - memo, - metadata: opts.metadata, - quote_id: None, - payment_request: None, - payment_proof: None, - payment_method: None, - }) - .await?; - - tx.commit().await?; + self.localstore + .add_transaction(Transaction { + mint_url: self.mint_url.clone(), + direction: TransactionDirection::Incoming, + amount: total_amount, + fee: proofs_amount - total_amount, + unit: self.unit.clone(), + ys: proofs_ys, + timestamp: unix_time(), + memo, + metadata: opts.metadata, + quote_id: None, + payment_request: None, + payment_proof: None, + payment_method: None, + }) + .await?; Ok(total_amount) } diff --git a/crates/cdk/src/wallet/reclaim.rs b/crates/cdk/src/wallet/reclaim.rs index bc22e6ca1..aa25d0db9 100644 --- a/crates/cdk/src/wallet/reclaim.rs +++ b/crates/cdk/src/wallet/reclaim.rs @@ -45,8 +45,6 @@ impl Wallet { .await? .states; - let mut tx = self.localstore.begin_db_transaction().await?; - for (state, unspent) in proofs .into_iter() .zip(statuses) @@ -56,18 +54,17 @@ impl Wallet { acc }) { - tx.update_proofs_state( - unspent - .iter() - .map(|x| x.y()) - .collect::, _>>()?, - state, - ) - .await?; + self.localstore + .update_proofs_state( + unspent + .iter() + .map(|x| x.y()) + .collect::, _>>()?, + state, + ) + .await?; } - tx.commit().await?; - Ok(()) } diff --git a/crates/cdk/src/wallet/send.rs b/crates/cdk/src/wallet/send.rs index fd48e06e9..b4dab782a 100644 --- a/crates/cdk/src/wallet/send.rs +++ b/crates/cdk/src/wallet/send.rs @@ -46,7 +46,6 @@ impl Wallet { // Get available proofs matching conditions let mut available_proofs = self .get_proofs_with( - None, Some(vec![State::Unspent]), opts.conditions.clone().map(|c| vec![c]), ) @@ -184,10 +183,9 @@ impl Wallet { tracing::debug!("Send amounts: {:?}", send_amounts); tracing::debug!("Send fee: {:?}", send_fee); - let mut tx = self.localstore.begin_db_transaction().await?; - // Reserve proofs - tx.update_proofs_state(proofs.ys()?, State::Reserved) + self.localstore + .update_proofs_state(proofs.ys()?, State::Reserved) .await?; // Check if proofs are exact send amount (and does not exceed max_proofs) @@ -218,8 +216,6 @@ impl Wallet { is_exact_or_offline, )?; - tx.commit().await?; - // Return prepared send Ok(PreparedSend { wallet: self.clone(), @@ -340,13 +336,10 @@ impl PreparedSend { return Err(Error::InsufficientFunds); } - let mut tx = self.wallet.localstore.begin_db_transaction().await?; - // Check if proofs are reserved or unspent let sendable_proof_ys = self .wallet .get_proofs_with( - Some(&mut tx), Some(vec![State::Reserved, State::Unspent]), self.options.conditions.clone().map(|c| vec![c]), ) @@ -367,7 +360,9 @@ impl PreparedSend { proofs_to_send.ys()? ); - tx.update_proofs_state(proofs_to_send.ys()?, State::PendingSpent) + self.wallet + .localstore + .update_proofs_state(proofs_to_send.ys()?, State::PendingSpent) .await?; // Include token memo @@ -375,24 +370,24 @@ impl PreparedSend { let memo = send_memo.and_then(|m| if m.include_memo { Some(m.memo) } else { None }); // Add transaction to store - tx.add_transaction(Transaction { - mint_url: self.wallet.mint_url.clone(), - direction: TransactionDirection::Outgoing, - amount: self.amount, - fee: total_send_fee, - unit: self.wallet.unit.clone(), - ys: proofs_to_send.ys()?, - timestamp: unix_time(), - memo: memo.clone(), - metadata: self.options.metadata, - quote_id: None, - payment_request: None, - payment_proof: None, - payment_method: None, - }) - .await?; - - tx.commit().await?; + self.wallet + .localstore + .add_transaction(Transaction { + mint_url: self.wallet.mint_url.clone(), + direction: TransactionDirection::Outgoing, + amount: self.amount, + fee: total_send_fee, + unit: self.wallet.unit.clone(), + ys: proofs_to_send.ys()?, + timestamp: unix_time(), + memo: memo.clone(), + metadata: self.options.metadata, + quote_id: None, + payment_request: None, + payment_proof: None, + payment_method: None, + }) + .await?; // Create and return token Ok(Token::new( @@ -407,12 +402,10 @@ impl PreparedSend { pub async fn cancel(self) -> Result<(), Error> { tracing::info!("Cancelling prepared send"); - let mut tx = self.wallet.localstore.begin_db_transaction().await?; - // Double-check proofs state let reserved_proofs = self .wallet - .get_proofs_with(Some(&mut tx), Some(vec![State::Reserved]), None) + .get_proofs_with(Some(vec![State::Reserved]), None) .await? .ys()?; @@ -425,11 +418,11 @@ impl PreparedSend { return Err(Error::UnexpectedProofState); } - tx.update_proofs_state(self.proofs().ys()?, State::Unspent) + self.wallet + .localstore + .update_proofs_state(self.proofs().ys()?, State::Unspent) .await?; - tx.commit().await?; - Ok(()) } } diff --git a/crates/cdk/src/wallet/swap.rs b/crates/cdk/src/wallet/swap.rs index 5337ca3b7..19df61363 100644 --- a/crates/cdk/src/wallet/swap.rs +++ b/crates/cdk/src/wallet/swap.rs @@ -1,5 +1,4 @@ use cdk_common::amount::FeeAndAmounts; -use cdk_common::database::DynWalletDatabaseTransaction; use cdk_common::nut02::KeySetInfosMethods; use cdk_common::Id; use tracing::instrument; @@ -37,7 +36,6 @@ impl Wallet { let pre_swap = self .create_swap( - self.localstore.begin_db_transaction().await?, active_keyset_id, &fee_and_amounts, amount, @@ -134,10 +132,9 @@ impl Wallet { .map(|proof| proof.y()) .collect::, _>>()?; - let mut tx = self.localstore.begin_db_transaction().await?; - - tx.update_proofs(added_proofs, deleted_ys).await?; - tx.commit().await?; + self.localstore + .update_proofs(added_proofs, deleted_ys) + .await?; Ok(send_proofs) } @@ -199,11 +196,10 @@ impl Wallet { } /// Create Swap Payload - #[instrument(skip(self, proofs, tx))] + #[instrument(skip(self, proofs))] #[allow(clippy::too_many_arguments)] pub async fn create_swap( &self, - mut tx: DynWalletDatabaseTransaction, active_keyset_id: Id, fee_and_amounts: &FeeAndAmounts, amount: Option, @@ -219,7 +215,9 @@ impl Wallet { let proofs_total = proofs.total_amount()?; let ys: Vec = proofs.ys()?; - tx.update_proofs_state(ys, State::Reserved).await?; + self.localstore + .update_proofs_state(ys, State::Reserved) + .await?; let total_to_subtract = amount .unwrap_or(Amount::ZERO) @@ -257,7 +255,7 @@ impl Wallet { // else use state refill let change_split_target = match amount_split_target { SplitTarget::None => { - self.determine_split_target_values(&mut tx, change_amount, fee_and_amounts) + self.determine_split_target_values(change_amount, fee_and_amounts) .await? } s => s, @@ -294,7 +292,8 @@ impl Wallet { total_secrets_needed ); - let new_counter = tx + let new_counter = self + .localstore .increment_keyset_counter(&active_keyset_id, total_secrets_needed) .await?; @@ -363,8 +362,6 @@ impl Wallet { let swap_request = SwapRequest::new(proofs, desired_messages.blinded_messages()); - tx.commit().await?; - Ok(PreSwap { pre_mint_secrets: desired_messages, swap_request,