Browse Source

Introduce `cdk-sql-common`

The primary purpose of this new crate is to have a common and shared codebase
for all SQL storage systems. It would force us to write standard SQL using best
practices for all databases.

This crate has been extracted from #878
Cesar Rodas 3 weeks ago
parent
commit
37a2cb4233
79 changed files with 4542 additions and 3731 deletions
  1. 1 0
      Cargo.toml
  2. 1 1
      crates/cdk-cli/src/main.rs
  3. 109 0
      crates/cdk-common/src/database/mod.rs
  4. 2 2
      crates/cdk-integration-tests/src/init_pure_tests.rs
  5. 2 2
      crates/cdk-mintd/src/main.rs
  6. 1 1
      crates/cdk-signatory/src/bin/cli/mod.rs
  7. 30 0
      crates/cdk-sql-common/Cargo.toml
  8. 24 0
      crates/cdk-sql-common/README.md
  9. 7 4
      crates/cdk-sql-common/build.rs
  10. 46 0
      crates/cdk-sql-common/src/common.rs
  11. 53 0
      crates/cdk-sql-common/src/database.rs
  12. 23 0
      crates/cdk-sql-common/src/lib.rs
  13. 52 34
      crates/cdk-sql-common/src/macros.rs
  14. 5 0
      crates/cdk-sql-common/src/mint/auth/migrations.rs
  15. 0 0
      crates/cdk-sql-common/src/mint/auth/migrations/sqlite/20250109143347_init.sql
  16. 100 93
      crates/cdk-sql-common/src/mint/auth/mod.rs
  17. 27 0
      crates/cdk-sql-common/src/mint/migrations.rs
  18. 82 0
      crates/cdk-sql-common/src/mint/migrations/postgres/20250710212391_init.sql
  19. 20 0
      crates/cdk-sql-common/src/mint/migrations/sqlite/1_fix_sqlx_migration.sql
  20. 0 0
      crates/cdk-sql-common/src/mint/migrations/sqlite/20240612124932_init.sql
  21. 0 0
      crates/cdk-sql-common/src/mint/migrations/sqlite/20240618195700_quote_state.sql
  22. 0 0
      crates/cdk-sql-common/src/mint/migrations/sqlite/20240626092101_nut04_state.sql
  23. 0 0
      crates/cdk-sql-common/src/mint/migrations/sqlite/20240703122347_request_lookup_id.sql
  24. 0 0
      crates/cdk-sql-common/src/mint/migrations/sqlite/20240710145043_input_fee.sql
  25. 0 0
      crates/cdk-sql-common/src/mint/migrations/sqlite/20240711183109_derivation_path_index.sql
  26. 0 0
      crates/cdk-sql-common/src/mint/migrations/sqlite/20240718203721_allow_unspent.sql
  27. 0 0
      crates/cdk-sql-common/src/mint/migrations/sqlite/20240811031111_update_mint_url.sql
  28. 0 0
      crates/cdk-sql-common/src/mint/migrations/sqlite/20240919103407_proofs_quote_id.sql
  29. 0 0
      crates/cdk-sql-common/src/mint/migrations/sqlite/20240923153640_melt_requests.sql
  30. 0 0
      crates/cdk-sql-common/src/mint/migrations/sqlite/20240930101140_dleq_for_sigs.sql
  31. 0 0
      crates/cdk-sql-common/src/mint/migrations/sqlite/20241108093102_mint_mint_quote_pubkey.sql
  32. 0 0
      crates/cdk-sql-common/src/mint/migrations/sqlite/20250103201327_amount_to_pay_msats.sql
  33. 0 0
      crates/cdk-sql-common/src/mint/migrations/sqlite/20250129200912_remove_mint_url.sql
  34. 0 0
      crates/cdk-sql-common/src/mint/migrations/sqlite/20250129230326_add_config_table.sql
  35. 1 1
      crates/cdk-sql-common/src/mint/migrations/sqlite/20250307213652_keyset_id_as_foreign_key.sql
  36. 0 0
      crates/cdk-sql-common/src/mint/migrations/sqlite/20250406091754_mint_time_of_quotes.sql
  37. 0 0
      crates/cdk-sql-common/src/mint/migrations/sqlite/20250406093755_mint_created_time_signature.sql
  38. 0 0
      crates/cdk-sql-common/src/mint/migrations/sqlite/20250415093121_drop_keystore_foreign.sql
  39. 0 0
      crates/cdk-sql-common/src/mint/migrations/sqlite/20250626120251_rename_blind_message_y_to_b.sql
  40. 0 0
      crates/cdk-sql-common/src/mint/migrations/sqlite/20250706101057_bolt12.sql
  41. 1796 0
      crates/cdk-sql-common/src/mint/mod.rs
  42. 26 16
      crates/cdk-sql-common/src/pool.rs
  43. 292 0
      crates/cdk-sql-common/src/stmt.rs
  44. 82 0
      crates/cdk-sql-common/src/value.rs
  45. 1 1
      crates/cdk-sql-common/src/wallet/error.rs
  46. 21 0
      crates/cdk-sql-common/src/wallet/migrations.rs
  47. 0 0
      crates/cdk-sql-common/src/wallet/migrations/sqlite/20240612132920_init.sql
  48. 0 0
      crates/cdk-sql-common/src/wallet/migrations/sqlite/20240618200350_quote_state.sql
  49. 0 0
      crates/cdk-sql-common/src/wallet/migrations/sqlite/20240626091921_nut04_state.sql
  50. 0 0
      crates/cdk-sql-common/src/wallet/migrations/sqlite/20240710144711_input_fee.sql
  51. 0 0
      crates/cdk-sql-common/src/wallet/migrations/sqlite/20240810214105_mint_icon_url.sql
  52. 0 0
      crates/cdk-sql-common/src/wallet/migrations/sqlite/20240810233905_update_mint_url.sql
  53. 0 0
      crates/cdk-sql-common/src/wallet/migrations/sqlite/20240902151515_icon_url.sql
  54. 0 0
      crates/cdk-sql-common/src/wallet/migrations/sqlite/20240902210905_mint_time.sql
  55. 0 0
      crates/cdk-sql-common/src/wallet/migrations/sqlite/20241011125207_mint_urls.sql
  56. 0 0
      crates/cdk-sql-common/src/wallet/migrations/sqlite/20241108092756_wallet_mint_quote_secretkey.sql
  57. 0 0
      crates/cdk-sql-common/src/wallet/migrations/sqlite/20250214135017_mint_tos.sql
  58. 0 0
      crates/cdk-sql-common/src/wallet/migrations/sqlite/20250310111513_drop_nostr_last_checked.sql
  59. 0 0
      crates/cdk-sql-common/src/wallet/migrations/sqlite/20250314082116_allow_pending_spent.sql
  60. 0 0
      crates/cdk-sql-common/src/wallet/migrations/sqlite/20250323152040_wallet_dleq_proofs.sql
  61. 0 0
      crates/cdk-sql-common/src/wallet/migrations/sqlite/20250401120000_add_transactions_table.sql
  62. 0 0
      crates/cdk-sql-common/src/wallet/migrations/sqlite/20250616144830_add_keyset_expiry.sql
  63. 0 0
      crates/cdk-sql-common/src/wallet/migrations/sqlite/20250707093445_bolt12.sql
  64. 1122 0
      crates/cdk-sql-common/src/wallet/mod.rs
  65. 97 0
      crates/cdk-sql-common/tests/legacy-sqlx.sql
  66. 5 4
      crates/cdk-sqlite/Cargo.toml
  67. 28 57
      crates/cdk-sqlite/src/common.rs
  68. 0 3
      crates/cdk-sqlite/src/lib.rs
  69. 327 173
      crates/cdk-sqlite/src/mint/async_rusqlite.rs
  70. 0 5
      crates/cdk-sqlite/src/mint/auth/migrations.rs
  71. 0 116
      crates/cdk-sqlite/src/mint/error.rs
  72. 4 3
      crates/cdk-sqlite/src/mint/memory.rs
  73. 0 25
      crates/cdk-sqlite/src/mint/migrations.rs
  74. 11 1896
      crates/cdk-sqlite/src/mint/mod.rs
  75. 0 184
      crates/cdk-sqlite/src/stmt.rs
  76. 5 3
      crates/cdk-sqlite/src/wallet/memory.rs
  77. 0 21
      crates/cdk-sqlite/src/wallet/migrations.rs
  78. 138 1085
      crates/cdk-sqlite/src/wallet/mod.rs
  79. 1 1
      crates/cdk-sqlite/tests/legacy-sqlx.sql

+ 1 - 0
Cargo.toml

@@ -54,6 +54,7 @@ cdk-fake-wallet = { path = "./crates/cdk-fake-wallet", version = "=0.11.0" }
 cdk-payment-processor = { path = "./crates/cdk-payment-processor", default-features = true, version = "=0.11.0" }
 cdk-mint-rpc = { path = "./crates/cdk-mint-rpc", version = "=0.11.0" }
 cdk-redb = { path = "./crates/cdk-redb", default-features = true, version = "=0.11.0" }
+cdk-sql-common = { path = "./crates/cdk-sql-common", default-features = true, version = "=0.11.0" }
 cdk-sqlite = { path = "./crates/cdk-sqlite", default-features = true, version = "=0.11.0" }
 cdk-signatory = { path = "./crates/cdk-signatory", version = "=0.11.0", default-features = false }
 clap = { version = "4.5.31", features = ["derive"] }

+ 1 - 1
crates/cdk-cli/src/main.rs

@@ -128,7 +128,7 @@ async fn main() -> Result<()> {
                 #[cfg(feature = "sqlcipher")]
                 let sql = {
                     match args.password {
-                        Some(pass) => WalletSqliteDatabase::new(&sql_path, pass).await?,
+                        Some(pass) => WalletSqliteDatabase::new((sql_path, pass)).await?,
                         None => bail!("Missing database password"),
                     }
                 };

+ 109 - 0
crates/cdk-common/src/database/mod.rs

@@ -19,6 +19,80 @@ pub use mint::{MintAuthDatabase, MintAuthTransaction};
 #[cfg(feature = "wallet")]
 pub use wallet::Database as WalletDatabase;
 
+/// Data conversion error
+#[derive(thiserror::Error, Debug)]
+pub enum ConversionError {
+    /// Missing columns
+    #[error("Not enough elements: expected {0}, got {1}")]
+    MissingColumn(usize, usize),
+
+    /// Missing parameter
+    #[error("Missing parameter {0}")]
+    MissingParameter(String),
+
+    /// Invalid db type
+    #[error("Invalid type from db, expected {0} got {1}")]
+    InvalidType(String, String),
+
+    /// Invalid data conversion in column
+    #[error("Error converting {1}, expecting type {0}")]
+    InvalidConversion(String, String),
+
+    /// Mint Url Error
+    #[error(transparent)]
+    MintUrl(#[from] crate::mint_url::Error),
+
+    /// NUT00 Error
+    #[error(transparent)]
+    CDKNUT00(#[from] crate::nuts::nut00::Error),
+
+    /// NUT01 Error
+    #[error(transparent)]
+    CDKNUT01(#[from] crate::nuts::nut01::Error),
+
+    /// NUT02 Error
+    #[error(transparent)]
+    CDKNUT02(#[from] crate::nuts::nut02::Error),
+
+    /// NUT04 Error
+    #[error(transparent)]
+    CDKNUT04(#[from] crate::nuts::nut04::Error),
+
+    /// NUT05 Error
+    #[error(transparent)]
+    CDKNUT05(#[from] crate::nuts::nut05::Error),
+
+    /// NUT07 Error
+    #[error(transparent)]
+    CDKNUT07(#[from] crate::nuts::nut07::Error),
+
+    /// NUT23 Error
+    #[error(transparent)]
+    CDKNUT23(#[from] crate::nuts::nut23::Error),
+
+    /// Secret Error
+    #[error(transparent)]
+    CDKSECRET(#[from] crate::secret::Error),
+
+    /// Serde Error
+    #[error(transparent)]
+    Serde(#[from] serde_json::Error),
+
+    /// BIP32 Error
+    #[error(transparent)]
+    BIP32(#[from] bitcoin::bip32::Error),
+
+    /// Generic error
+    #[error(transparent)]
+    Generic(#[from] Box<crate::Error>),
+}
+
+impl From<crate::Error> for ConversionError {
+    fn from(err: crate::Error) -> Self {
+        ConversionError::Generic(Box::new(err))
+    }
+}
+
 /// CDK_database error
 #[derive(Debug, thiserror::Error)]
 pub enum Error {
@@ -39,6 +113,9 @@ pub enum Error {
     /// NUT00 Error
     #[error(transparent)]
     NUT00(#[from] crate::nuts::nut00::Error),
+    /// NUT01 Error
+    #[error(transparent)]
+    NUT01(#[from] crate::nuts::nut01::Error),
     /// NUT02 Error
     #[error(transparent)]
     NUT02(#[from] crate::nuts::nut02::Error),
@@ -68,6 +145,38 @@ pub enum Error {
     /// Invalid state transition
     #[error("Invalid state transition")]
     InvalidStateTransition(crate::state::Error),
+
+    /// Invalid connection settings
+    #[error("Invalid credentials {0}")]
+    InvalidConnectionSettings(String),
+
+    /// Unexpected database response
+    #[error("Invalid database response")]
+    InvalidDbResponse,
+
+    /// Internal error
+    #[error("Internal {0}")]
+    Internal(String),
+
+    /// Data conversion error
+    #[error(transparent)]
+    Conversion(#[from] ConversionError),
+
+    /// Missing Placeholder value
+    #[error("Missing placeholder value {0}")]
+    MissingPlaceholder(String),
+
+    /// Unknown quote ttl
+    #[error("Unknown quote ttl")]
+    UnknownQuoteTTL,
+
+    /// Invalid UUID
+    #[error("Invalid UUID: {0}")]
+    InvalidUuid(String),
+
+    /// QuoteNotFound
+    #[error("Quote not found")]
+    QuoteNotFound,
 }
 
 #[cfg(feature = "mint")]

+ 2 - 2
crates/cdk-integration-tests/src/init_pure_tests.rs

@@ -234,7 +234,7 @@ pub async fn create_and_start_test_mint() -> Result<Mint> {
             let temp_dir = create_temp_dir("cdk-test-sqlite-mint")?;
             let path = temp_dir.join("mint.db").to_str().unwrap().to_string();
             Arc::new(
-                cdk_sqlite::MintSqliteDatabase::new(&path)
+                cdk_sqlite::MintSqliteDatabase::new(path.as_str())
                     .await
                     .expect("Could not create sqlite db"),
             )
@@ -315,7 +315,7 @@ pub async fn create_test_wallet_for_mint(mint: Mint) -> Result<Wallet> {
                 // Create a temporary directory for SQLite database
                 let temp_dir = create_temp_dir("cdk-test-sqlite-wallet")?;
                 let path = temp_dir.join("wallet.db").to_str().unwrap().to_string();
-                let database = cdk_sqlite::WalletSqliteDatabase::new(&path)
+                let database = cdk_sqlite::WalletSqliteDatabase::new(path.as_str())
                     .await
                     .expect("Could not create sqlite db");
                 Arc::new(database)

+ 2 - 2
crates/cdk-mintd/src/main.rs

@@ -212,7 +212,7 @@ async fn setup_sqlite_database(
     #[cfg(feature = "sqlcipher")]
     let db = {
         // Get password from command line arguments for sqlcipher
-        MintSqliteDatabase::new(&sql_db_path, _password.unwrap()).await?
+        MintSqliteDatabase::new((sql_db_path, _password.unwrap())).await?
     };
     Ok(Arc::new(db))
 }
@@ -487,7 +487,7 @@ async fn setup_authentication(
                 #[cfg(feature = "sqlcipher")]
                 let password = CLIArgs::parse().password;
                 #[cfg(feature = "sqlcipher")]
-                let sqlite_db = MintSqliteAuthDatabase::new(&sql_db_path, password).await?;
+                let sqlite_db = MintSqliteAuthDatabase::new((sql_db_path, password)).await?;
                 #[cfg(not(feature = "sqlcipher"))]
                 let sqlite_db = MintSqliteAuthDatabase::new(&sql_db_path).await?;
                 Arc::new(sqlite_db)

+ 1 - 1
crates/cdk-signatory/src/bin/cli/mod.rs

@@ -108,7 +108,7 @@ pub async fn cli_main() -> Result<()> {
                     #[cfg(feature = "sqlcipher")]
                     let db = {
                         match args.password {
-                            Some(pass) => MintSqliteDatabase::new(&sql_path, pass).await?,
+                            Some(pass) => MintSqliteDatabase::new((&sql_path, pass)).await?,
                             None => bail!("Missing database password"),
                         }
                     };

+ 30 - 0
crates/cdk-sql-common/Cargo.toml

@@ -0,0 +1,30 @@
+[package]
+name = "cdk-sql-common"
+version.workspace = true
+edition.workspace = true
+authors = ["CDK Developers"]
+description = "Generic SQL storage backend for CDK"
+license.workspace = true
+homepage = "https://github.com/cashubtc/cdk"
+repository = "https://github.com/cashubtc/cdk.git"
+rust-version.workspace = true                            # MSRV
+readme = "README.md"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+[features]
+default = ["mint", "wallet", "auth"]
+mint = ["cdk-common/mint"]
+wallet = ["cdk-common/wallet"]
+auth = ["cdk-common/auth"]
+
+[dependencies]
+async-trait.workspace = true
+cdk-common = { workspace = true, features = ["test"] }
+bitcoin.workspace = true
+thiserror.workspace = true
+tokio.workspace = true
+tracing.workspace = true
+serde.workspace = true
+serde_json.workspace = true
+lightning-invoice.workspace = true
+uuid.workspace = true

+ 24 - 0
crates/cdk-sql-common/README.md

@@ -0,0 +1,24 @@
+# CDK SQL Base
+
+This is a private crate offering a common framework to interact with SQL databases.
+
+This crate uses standard SQL, a generic migration framework a traits to implement blocking or
+non-blocking clients.
+
+
+**ALPHA** This library is in early development, the API will change and should be used with caution.
+
+## Features
+
+The following crate feature flags are available:
+
+| Feature     | Default | Description                        |
+|-------------|:-------:|------------------------------------|
+| `wallet`    |   Yes   | Enable cashu wallet features       |
+| `mint`      |   Yes   | Enable cashu mint wallet features  |
+| `auth`      |   Yes   | Enable cashu mint auth features    |
+
+
+## License
+
+This project is licensed under the [MIT License](../../LICENSE).

+ 7 - 4
crates/cdk-sqlite/build.rs → crates/cdk-sql-common/build.rs

@@ -18,18 +18,21 @@ fn main() {
         let dest_path = parent.join("migrations.rs");
         let mut out_file = File::create(&dest_path).expect("Failed to create migrations.rs");
 
-        writeln!(out_file, "// @generated").unwrap();
-        writeln!(out_file, "// Auto-generated by build.rs").unwrap();
+        let skip_name = migration_path.to_str().unwrap_or_default().len();
+
+        writeln!(out_file, "/// @generated").unwrap();
+        writeln!(out_file, "/// Auto-generated by build.rs").unwrap();
         writeln!(out_file, "pub static MIGRATIONS: &[(&str, &str)] = &[").unwrap();
 
         for path in &files {
-            let name = path.file_name().unwrap().to_string_lossy();
+            let rel_name = &path.to_str().unwrap().replace("\\", "/")[skip_name + 1..]; // for Windows
             let rel_path = &path.to_str().unwrap().replace("\\", "/")[skip_path..]; // for Windows
             writeln!(
                 out_file,
-                "    (\"{name}\", include_str!(r#\".{rel_path}\"#)),"
+                "    (\"{rel_name}\", include_str!(r#\".{rel_path}\"#)),"
             )
             .unwrap();
+            println!("cargo:rerun-if-changed={}", path.display());
         }
 
         writeln!(out_file, "];").unwrap();

+ 46 - 0
crates/cdk-sql-common/src/common.rs

@@ -0,0 +1,46 @@
+use crate::database::DatabaseExecutor;
+use crate::stmt::query;
+
+/// Migrates the migration generated by `build.rs`
+#[inline(always)]
+pub async fn migrate<C: DatabaseExecutor>(
+    conn: &C,
+    db_prefix: &str,
+    migrations: &[(&str, &str)],
+) -> Result<(), cdk_common::database::Error> {
+    query(
+        r#"
+           CREATE TABLE IF NOT EXISTS migrations (
+               name TEXT PRIMARY KEY,
+               applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+           )
+           "#,
+    )?
+    .execute(conn)
+    .await?;
+
+    // Apply each migration if it hasn’t been applied yet
+    for (name, sql) in migrations {
+        if let Some((prefix, _)) = name.split_once(['/', '\\']) {
+            if prefix != db_prefix {
+                continue;
+            }
+        }
+
+        let is_missing = query("SELECT name FROM migrations WHERE name = :name")?
+            .bind("name", name)
+            .pluck(conn)
+            .await?
+            .is_none();
+
+        if is_missing {
+            query(sql)?.batch(conn).await?;
+            query(r#"INSERT INTO migrations (name) VALUES (:name)"#)?
+                .bind("name", name)
+                .execute(conn)
+                .await?;
+        }
+    }
+
+    Ok(())
+}

+ 53 - 0
crates/cdk-sql-common/src/database.rs

@@ -0,0 +1,53 @@
+//! Database traits definition
+
+use std::fmt::Debug;
+
+use cdk_common::database::Error;
+
+use crate::stmt::{Column, Statement};
+
+/// Database Executor
+///
+/// This trait defines the expectations of a database execution
+#[async_trait::async_trait]
+pub trait DatabaseExecutor: Debug + Sync + Send {
+    /// Database driver name
+    fn name() -> &'static str;
+
+    /// Executes a query and returns the affected rows
+    async fn execute(&self, statement: Statement) -> Result<usize, Error>;
+
+    /// Runs the query and returns the first row or None
+    async fn fetch_one(&self, statement: Statement) -> Result<Option<Vec<Column>>, Error>;
+
+    /// Runs the query and returns the first row or None
+    async fn fetch_all(&self, statement: Statement) -> Result<Vec<Vec<Column>>, Error>;
+
+    /// Fetches the first row and column from a query
+    async fn pluck(&self, statement: Statement) -> Result<Option<Column>, Error>;
+
+    /// Batch execution
+    async fn batch(&self, statement: Statement) -> Result<(), Error>;
+}
+
+/// Database transaction trait
+#[async_trait::async_trait]
+pub trait DatabaseTransaction<'a>: Debug + DatabaseExecutor + Send + Sync {
+    /// Consumes the current transaction committing the changes
+    async fn commit(self) -> Result<(), Error>;
+
+    /// Consumes the transaction rolling back all changes
+    async fn rollback(self) -> Result<(), Error>;
+}
+
+/// Database connector
+#[async_trait::async_trait]
+pub trait DatabaseConnector: Debug + DatabaseExecutor + Send + Sync {
+    /// Transaction type for this database connection
+    type Transaction<'a>: DatabaseTransaction<'a>
+    where
+        Self: 'a;
+
+    /// Begin a new transaction
+    async fn begin(&self) -> Result<Self::Transaction<'_>, Error>;
+}

+ 23 - 0
crates/cdk-sql-common/src/lib.rs

@@ -0,0 +1,23 @@
+//! SQLite storage backend for cdk
+
+#![warn(missing_docs)]
+#![warn(rustdoc::bare_urls)]
+
+mod common;
+pub mod database;
+mod macros;
+pub mod pool;
+pub mod stmt;
+pub mod value;
+
+pub use cdk_common::database::ConversionError;
+
+#[cfg(feature = "mint")]
+pub mod mint;
+#[cfg(feature = "wallet")]
+pub mod wallet;
+
+#[cfg(feature = "mint")]
+pub use mint::SQLMintDatabase;
+#[cfg(feature = "wallet")]
+pub use wallet::SQLWalletDatabase;

+ 52 - 34
crates/cdk-sqlite/src/macros.rs → crates/cdk-sql-common/src/macros.rs

@@ -1,4 +1,4 @@
-//! Collection of macros to generate code to digest data from SQLite
+//! Collection of macros to generate code to digest data from a generic SQL databasex
 
 /// Unpacks a vector of Column, and consumes it, parsing into individual variables, checking the
 /// vector is big enough.
@@ -10,9 +10,9 @@ macro_rules! unpack_into {
             vec.reverse();
             let required = 0 $(+ {let _ = stringify!($var); 1})+;
             if vec.len() < required {
-                return Err(Error::MissingColumn(required, vec.len()));
+                 Err($crate::ConversionError::MissingColumn(required, vec.len()))?;
             }
-            Ok::<_, Error>((
+            Ok::<_, cdk_common::database::Error>((
                 $(
                     vec.pop().expect(&format!("Checked length already for {}", stringify!($var)))
                 ),+
@@ -21,7 +21,7 @@ macro_rules! unpack_into {
     };
 }
 
-/// Parses a SQLite column as a string or NULL
+/// Parses a SQL column as a string or NULL
 #[macro_export]
 macro_rules! column_as_nullable_string {
     ($col:expr, $callback_str:expr, $callback_bytes:expr) => {
@@ -29,9 +29,9 @@ macro_rules! column_as_nullable_string {
             $crate::stmt::Column::Text(text) => Ok(Some(text).and_then($callback_str)),
             $crate::stmt::Column::Blob(bytes) => Ok(Some(bytes).and_then($callback_bytes)),
             $crate::stmt::Column::Null => Ok(None),
-            other => Err(Error::InvalidType(
+            _ => Err($crate::ConversionError::InvalidType(
                 "String".to_owned(),
-                other.data_type().to_string(),
+                stringify!($col).to_owned(),
             )),
         })?
     };
@@ -42,9 +42,9 @@ macro_rules! column_as_nullable_string {
                 Ok(Some(String::from_utf8_lossy(&bytes)).and_then($callback_str))
             }
             $crate::stmt::Column::Null => Ok(None),
-            other => Err(Error::InvalidType(
+            _ => Err($crate::ConversionError::InvalidType(
                 "String".to_owned(),
-                other.data_type().to_string(),
+                stringify!($col).to_owned(),
             )),
         })?
     };
@@ -55,9 +55,9 @@ macro_rules! column_as_nullable_string {
                 Ok(Some(String::from_utf8_lossy(&bytes).to_string()))
             }
             $crate::stmt::Column::Null => Ok(None),
-            other => Err(Error::InvalidType(
+            _ => Err($crate::ConversionError::InvalidType(
                 "String".to_owned(),
-                other.data_type().to_string(),
+                stringify!($col).to_owned(),
             )),
         })?
     };
@@ -69,15 +69,21 @@ macro_rules! column_as_nullable_number {
     ($col:expr) => {
         (match $col {
             $crate::stmt::Column::Text(text) => Ok(Some(text.parse().map_err(|_| {
-                Error::InvalidConversion(stringify!($col).to_owned(), "Number".to_owned())
+                $crate::ConversionError::InvalidConversion(
+                    stringify!($col).to_owned(),
+                    "Number".to_owned(),
+                )
             })?)),
             $crate::stmt::Column::Integer(n) => Ok(Some(n.try_into().map_err(|_| {
-                Error::InvalidConversion(stringify!($col).to_owned(), "Number".to_owned())
+                $crate::ConversionError::InvalidConversion(
+                    stringify!($col).to_owned(),
+                    "Number".to_owned(),
+                )
             })?)),
             $crate::stmt::Column::Null => Ok(None),
-            other => Err(Error::InvalidType(
+            _ => Err($crate::ConversionError::InvalidType(
                 "Number".to_owned(),
-                other.data_type().to_string(),
+                stringify!($col).to_owned(),
             )),
         })?
     };
@@ -89,14 +95,20 @@ macro_rules! column_as_number {
     ($col:expr) => {
         (match $col {
             $crate::stmt::Column::Text(text) => text.parse().map_err(|_| {
-                Error::InvalidConversion(stringify!($col).to_owned(), "Number".to_owned())
+                $crate::ConversionError::InvalidConversion(
+                    stringify!($col).to_owned(),
+                    "Number".to_owned(),
+                )
             }),
             $crate::stmt::Column::Integer(n) => n.try_into().map_err(|_| {
-                Error::InvalidConversion(stringify!($col).to_owned(), "Number".to_owned())
+                $crate::ConversionError::InvalidConversion(
+                    stringify!($col).to_owned(),
+                    "Number".to_owned(),
+                )
             }),
-            other => Err(Error::InvalidType(
+            _ => Err($crate::ConversionError::InvalidType(
                 "Number".to_owned(),
-                other.data_type().to_string(),
+                stringify!($col).to_owned(),
             )),
         })?
     };
@@ -110,51 +122,57 @@ macro_rules! column_as_nullable_binary {
             $crate::stmt::Column::Text(text) => Ok(Some(text.as_bytes().to_vec())),
             $crate::stmt::Column::Blob(bytes) => Ok(Some(bytes.to_owned())),
             $crate::stmt::Column::Null => Ok(None),
-            other => Err(Error::InvalidType(
+            _ => Err($crate::ConversionError::InvalidType(
                 "String".to_owned(),
-                other.data_type().to_string(),
+                stringify!($col).to_owned(),
             )),
         })?
     };
 }
 
-/// Parses a SQLite column as a binary
+/// Parses a SQL column as a binary
 #[macro_export]
 macro_rules! column_as_binary {
     ($col:expr) => {
         (match $col {
             $crate::stmt::Column::Text(text) => Ok(text.as_bytes().to_vec()),
             $crate::stmt::Column::Blob(bytes) => Ok(bytes.to_owned()),
-            other => Err(Error::InvalidType(
+            _ => Err($crate::ConversionError::InvalidType(
                 "String".to_owned(),
-                other.data_type().to_string(),
+                stringify!($col).to_owned(),
             )),
         })?
     };
 }
 
-/// Parses a SQLite column as a string
+/// Parses a SQL column as a string
 #[macro_export]
 macro_rules! column_as_string {
     ($col:expr, $callback_str:expr, $callback_bytes:expr) => {
         (match $col {
-            $crate::stmt::Column::Text(text) => $callback_str(&text).map_err(Error::from),
-            $crate::stmt::Column::Blob(bytes) => $callback_bytes(&bytes).map_err(Error::from),
-            other => Err(Error::InvalidType(
+            $crate::stmt::Column::Text(text) => {
+                $callback_str(&text).map_err($crate::ConversionError::from)
+            }
+            $crate::stmt::Column::Blob(bytes) => {
+                $callback_bytes(&bytes).map_err($crate::ConversionError::from)
+            }
+            _ => Err($crate::ConversionError::InvalidType(
                 "String".to_owned(),
-                other.data_type().to_string(),
+                stringify!($col).to_owned(),
             )),
         })?
     };
     ($col:expr, $callback:expr) => {
         (match $col {
-            $crate::stmt::Column::Text(text) => $callback(&text).map_err(Error::from),
+            $crate::stmt::Column::Text(text) => {
+                $callback(&text).map_err($crate::ConversionError::from)
+            }
             $crate::stmt::Column::Blob(bytes) => {
-                $callback(&String::from_utf8_lossy(&bytes)).map_err(Error::from)
+                $callback(&String::from_utf8_lossy(&bytes)).map_err($crate::ConversionError::from)
             }
-            other => Err(Error::InvalidType(
+            _ => Err($crate::ConversionError::InvalidType(
                 "String".to_owned(),
-                other.data_type().to_string(),
+                stringify!($col).to_owned(),
             )),
         })?
     };
@@ -162,9 +180,9 @@ macro_rules! column_as_string {
         (match $col {
             $crate::stmt::Column::Text(text) => Ok(text.to_owned()),
             $crate::stmt::Column::Blob(bytes) => Ok(String::from_utf8_lossy(&bytes).to_string()),
-            other => Err(Error::InvalidType(
+            _ => Err($crate::ConversionError::InvalidType(
                 "String".to_owned(),
-                other.data_type().to_string(),
+                stringify!($col).to_owned(),
             )),
         })?
     };

+ 5 - 0
crates/cdk-sql-common/src/mint/auth/migrations.rs

@@ -0,0 +1,5 @@
+/// @generated
+/// Auto-generated by build.rs
+pub static MIGRATIONS: &[(&str, &str)] = &[
+    ("sqlite/20250109143347_init.sql", include_str!(r#"./migrations/sqlite/20250109143347_init.sql"#)),
+];

+ 0 - 0
crates/cdk-sqlite/src/mint/auth/migrations/20250109143347_init.sql → crates/cdk-sql-common/src/mint/auth/migrations/sqlite/20250109143347_init.sql


+ 100 - 93
crates/cdk-sqlite/src/mint/auth/mod.rs → crates/cdk-sql-common/src/mint/auth/mod.rs

@@ -1,8 +1,7 @@
-//! SQLite Mint Auth
+//! SQL Mint Auth
 
 use std::collections::HashMap;
-use std::ops::DerefMut;
-use std::path::Path;
+use std::marker::PhantomData;
 use std::str::FromStr;
 
 use async_trait::async_trait;
@@ -10,53 +9,57 @@ use cdk_common::database::{self, MintAuthDatabase, MintAuthTransaction};
 use cdk_common::mint::MintKeySetInfo;
 use cdk_common::nuts::{AuthProof, BlindSignature, Id, PublicKey, State};
 use cdk_common::{AuthRequired, ProtectedEndpoint};
+use migrations::MIGRATIONS;
 use tracing::instrument;
 
-use super::async_rusqlite::AsyncRusqlite;
-use super::{sqlite_row_to_blind_signature, sqlite_row_to_keyset_info, SqliteTransaction};
+use super::{sql_row_to_blind_signature, sql_row_to_keyset_info, SQLTransaction};
 use crate::column_as_string;
-use crate::common::{create_sqlite_pool, migrate};
-use crate::mint::async_rusqlite::query;
+use crate::common::migrate;
+use crate::database::{DatabaseConnector, DatabaseTransaction};
 use crate::mint::Error;
+use crate::stmt::query;
 
-/// Mint SQLite Database
+/// Mint SQL Database
 #[derive(Debug, Clone)]
-pub struct MintSqliteAuthDatabase {
-    pool: AsyncRusqlite,
+pub struct SQLMintAuthDatabase<DB>
+where
+    DB: DatabaseConnector,
+{
+    db: DB,
 }
 
-#[rustfmt::skip]
-mod migrations;
-
-impl MintSqliteAuthDatabase {
-    /// Create new [`MintSqliteAuthDatabase`]
-    #[cfg(not(feature = "sqlcipher"))]
-    pub async fn new<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
-        let pool = create_sqlite_pool(path.as_ref().to_str().ok_or(Error::InvalidDbPath)?);
-        migrate(pool.get()?.deref_mut(), migrations::MIGRATIONS)?;
-
-        Ok(Self {
-            pool: AsyncRusqlite::new(pool),
-        })
+impl<DB> SQLMintAuthDatabase<DB>
+where
+    DB: DatabaseConnector,
+{
+    /// Creates a new instance
+    pub async fn new<X>(db: X) -> Result<Self, Error>
+    where
+        X: Into<DB>,
+    {
+        let db = db.into();
+        Self::migrate(&db).await?;
+        Ok(Self { db })
     }
 
-    /// Create new [`MintSqliteAuthDatabase`]
-    #[cfg(feature = "sqlcipher")]
-    pub async fn new<P: AsRef<Path>>(path: P, password: String) -> Result<Self, Error> {
-        let pool = create_sqlite_pool(
-            path.as_ref().to_str().ok_or(Error::InvalidDbPath)?,
-            password,
-        );
-        migrate(pool.get()?.deref_mut(), migrations::MIGRATIONS)?;
-
-        Ok(Self {
-            pool: AsyncRusqlite::new(pool),
-        })
+    /// Migrate
+    async fn migrate(conn: &DB) -> Result<(), Error> {
+        let tx = conn.begin().await?;
+        migrate(&tx, DB::name(), MIGRATIONS).await?;
+        tx.commit().await?;
+        Ok(())
     }
 }
 
+#[rustfmt::skip]
+mod migrations;
+
+
 #[async_trait]
-impl MintAuthTransaction<database::Error> for SqliteTransaction<'_> {
+impl<'a, T> MintAuthTransaction<database::Error> for SQLTransaction<'a, T>
+where
+    T: DatabaseTransaction<'a>,
+{
     #[instrument(skip(self))]
     async fn set_active_keyset(&mut self, id: Id) -> Result<(), database::Error> {
         tracing::info!("Setting auth keyset {id} active");
@@ -68,8 +71,8 @@ impl MintAuthTransaction<database::Error> for SqliteTransaction<'_> {
                 ELSE FALSE
             END;
             "#,
-        )
-        .bind(":id", id.to_string())
+        )?
+        .bind("id", id.to_string())
         .execute(&self.inner)
         .await?;
 
@@ -97,15 +100,15 @@ impl MintAuthTransaction<database::Error> for SqliteTransaction<'_> {
             max_order = excluded.max_order,
             derivation_path_index = excluded.derivation_path_index
         "#,
-        )
-        .bind(":id", keyset.id.to_string())
-        .bind(":unit", keyset.unit.to_string())
-        .bind(":active", keyset.active)
-        .bind(":valid_from", keyset.valid_from as i64)
-        .bind(":valid_to", keyset.final_expiry.map(|v| v as i64))
-        .bind(":derivation_path", keyset.derivation_path.to_string())
-        .bind(":max_order", keyset.max_order)
-        .bind(":derivation_path_index", keyset.derivation_path_index)
+        )?
+        .bind("id", keyset.id.to_string())
+        .bind("unit", keyset.unit.to_string())
+        .bind("active", keyset.active)
+        .bind("valid_from", keyset.valid_from as i64)
+        .bind("valid_to", keyset.final_expiry.map(|v| v as i64))
+        .bind("derivation_path", keyset.derivation_path.to_string())
+        .bind("max_order", keyset.max_order)
+        .bind("derivation_path_index", keyset.derivation_path_index)
         .execute(&self.inner)
         .await?;
 
@@ -120,12 +123,12 @@ impl MintAuthTransaction<database::Error> for SqliteTransaction<'_> {
                 VALUES
                 (:y, :keyset_id, :secret, :c, :state)
                 "#,
-        )
-        .bind(":y", proof.y()?.to_bytes().to_vec())
-        .bind(":keyset_id", proof.keyset_id.to_string())
-        .bind(":secret", proof.secret.to_string())
-        .bind(":c", proof.c.to_bytes().to_vec())
-        .bind(":state", "UNSPENT".to_string())
+        )?
+        .bind("y", proof.y()?.to_bytes().to_vec())
+        .bind("keyset_id", proof.keyset_id.to_string())
+        .bind("secret", proof.secret.to_string())
+        .bind("c", proof.c.to_bytes().to_vec())
+        .bind("state", "UNSPENT".to_string())
         .execute(&self.inner)
         .await
         {
@@ -139,20 +142,20 @@ impl MintAuthTransaction<database::Error> for SqliteTransaction<'_> {
         y: &PublicKey,
         proofs_state: State,
     ) -> Result<Option<State>, Self::Err> {
-        let current_state = query(r#"SELECT state FROM proof WHERE y = :y"#)
-            .bind(":y", y.to_bytes().to_vec())
+        let current_state = query(r#"SELECT state FROM proof WHERE y = :y"#)?
+            .bind("y", y.to_bytes().to_vec())
             .pluck(&self.inner)
             .await?
             .map(|state| Ok::<_, Error>(column_as_string!(state, State::from_str)))
             .transpose()?;
 
-        query(r#"UPDATE proof SET state = :new_state WHERE state = :state AND y = :y"#)
-            .bind(":y", y.to_bytes().to_vec())
+        query(r#"UPDATE proof SET state = :new_state WHERE state = :state AND y = :y"#)?
+            .bind("y", y.to_bytes().to_vec())
             .bind(
-                ":state",
+                "state",
                 current_state.as_ref().map(|state| state.to_string()),
             )
-            .bind(":new_state", proofs_state.to_string())
+            .bind("new_state", proofs_state.to_string())
             .execute(&self.inner)
             .await?;
 
@@ -173,11 +176,11 @@ impl MintAuthTransaction<database::Error> for SqliteTransaction<'_> {
                        VALUES
                        (:y, :amount, :keyset_id, :c)
                    "#,
-            )
-            .bind(":y", message.to_bytes().to_vec())
-            .bind(":amount", u64::from(signature.amount) as i64)
-            .bind(":keyset_id", signature.keyset_id.to_string())
-            .bind(":c", signature.c.to_bytes().to_vec())
+            )?
+            .bind("y", message.to_bytes().to_vec())
+            .bind("amount", u64::from(signature.amount) as i64)
+            .bind("keyset_id", signature.keyset_id.to_string())
+            .bind("c", signature.c.to_bytes().to_vec())
             .execute(&self.inner)
             .await?;
         }
@@ -196,9 +199,9 @@ impl MintAuthTransaction<database::Error> for SqliteTransaction<'_> {
                  (endpoint, auth)
                  VALUES (:endpoint, :auth);
                  "#,
-            )
-            .bind(":endpoint", serde_json::to_string(endpoint)?)
-            .bind(":auth", serde_json::to_string(auth)?)
+            )?
+            .bind("endpoint", serde_json::to_string(endpoint)?)
+            .bind("auth", serde_json::to_string(auth)?)
             .execute(&self.inner)
             .await
             {
@@ -215,9 +218,9 @@ impl MintAuthTransaction<database::Error> for SqliteTransaction<'_> {
         &mut self,
         protected_endpoints: Vec<ProtectedEndpoint>,
     ) -> Result<(), database::Error> {
-        query(r#"DELETE FROM protected_endpoints WHERE endpoint IN (:endpoints)"#)
+        query(r#"DELETE FROM protected_endpoints WHERE endpoint IN (:endpoints)"#)?
             .bind_vec(
-                ":endpoints",
+                "endpoints",
                 protected_endpoints
                     .iter()
                     .map(serde_json::to_string)
@@ -230,15 +233,19 @@ impl MintAuthTransaction<database::Error> for SqliteTransaction<'_> {
 }
 
 #[async_trait]
-impl MintAuthDatabase for MintSqliteAuthDatabase {
+impl<DB> MintAuthDatabase for SQLMintAuthDatabase<DB>
+where
+    DB: DatabaseConnector,
+{
     type Err = database::Error;
 
     async fn begin_transaction<'a>(
         &'a self,
     ) -> Result<Box<dyn MintAuthTransaction<database::Error> + Send + Sync + 'a>, database::Error>
     {
-        Ok(Box::new(SqliteTransaction {
-            inner: self.pool.begin().await?,
+        Ok(Box::new(SQLTransaction {
+            inner: self.db.begin().await?,
+            _phantom: PhantomData,
         }))
     }
 
@@ -252,8 +259,8 @@ impl MintAuthDatabase for MintSqliteAuthDatabase {
             WHERE
                 active = 1;
             "#,
-        )
-        .pluck(&self.pool)
+        )?
+        .pluck(&self.db)
         .await?
         .map(|id| Ok::<_, Error>(column_as_string!(id, Id::from_str, Id::from_bytes)))
         .transpose()?)
@@ -274,11 +281,11 @@ impl MintAuthDatabase for MintSqliteAuthDatabase {
             FROM
                 keyset
                 WHERE id=:id"#,
-        )
-        .bind(":id", id.to_string())
-        .fetch_one(&self.pool)
+        )?
+        .bind("id", id.to_string())
+        .fetch_one(&self.db)
         .await?
-        .map(sqlite_row_to_keyset_info)
+        .map(sql_row_to_keyset_info)
         .transpose()?)
     }
 
@@ -297,18 +304,18 @@ impl MintAuthDatabase for MintSqliteAuthDatabase {
             FROM
                 keyset
                 WHERE id=:id"#,
-        )
-        .fetch_all(&self.pool)
+        )?
+        .fetch_all(&self.db)
         .await?
         .into_iter()
-        .map(sqlite_row_to_keyset_info)
+        .map(sql_row_to_keyset_info)
         .collect::<Result<Vec<_>, _>>()?)
     }
 
     async fn get_proofs_states(&self, ys: &[PublicKey]) -> Result<Vec<Option<State>>, Self::Err> {
-        let mut current_states = query(r#"SELECT y, state FROM proof WHERE y IN (:ys)"#)
-            .bind_vec(":ys", ys.iter().map(|y| y.to_bytes().to_vec()).collect())
-            .fetch_all(&self.pool)
+        let mut current_states = query(r#"SELECT y, state FROM proof WHERE y IN (:ys)"#)?
+            .bind_vec("ys", ys.iter().map(|y| y.to_bytes().to_vec()).collect())
+            .fetch_all(&self.db)
             .await?
             .into_iter()
             .map(|row| {
@@ -338,15 +345,15 @@ impl MintAuthDatabase for MintSqliteAuthDatabase {
                 blind_signature
             WHERE y IN (:y)
             "#,
-        )
+        )?
         .bind_vec(
-            ":y",
+            "y",
             blinded_messages
                 .iter()
                 .map(|y| y.to_bytes().to_vec())
                 .collect(),
         )
-        .fetch_all(&self.pool)
+        .fetch_all(&self.db)
         .await?
         .into_iter()
         .map(|mut row| {
@@ -356,7 +363,7 @@ impl MintAuthDatabase for MintSqliteAuthDatabase {
                     PublicKey::from_hex,
                     PublicKey::from_slice
                 ),
-                sqlite_row_to_blind_signature(row)?,
+                sql_row_to_blind_signature(row)?,
             ))
         })
         .collect::<Result<HashMap<_, _>, Error>>()?;
@@ -371,9 +378,9 @@ impl MintAuthDatabase for MintSqliteAuthDatabase {
         protected_endpoint: ProtectedEndpoint,
     ) -> Result<Option<AuthRequired>, Self::Err> {
         Ok(
-            query(r#"SELECT auth FROM protected_endpoints WHERE endpoint = :endpoint"#)
-                .bind(":endpoint", serde_json::to_string(&protected_endpoint)?)
-                .pluck(&self.pool)
+            query(r#"SELECT auth FROM protected_endpoints WHERE endpoint = :endpoint"#)?
+                .bind("endpoint", serde_json::to_string(&protected_endpoint)?)
+                .pluck(&self.db)
                 .await?
                 .map(|auth| {
                     Ok::<_, Error>(column_as_string!(
@@ -389,8 +396,8 @@ impl MintAuthDatabase for MintSqliteAuthDatabase {
     async fn get_auth_for_endpoints(
         &self,
     ) -> Result<HashMap<ProtectedEndpoint, Option<AuthRequired>>, Self::Err> {
-        Ok(query(r#"SELECT endpoint, auth FROM protected_endpoints"#)
-            .fetch_all(&self.pool)
+        Ok(query(r#"SELECT endpoint, auth FROM protected_endpoints"#)?
+            .fetch_all(&self.db)
             .await?
             .into_iter()
             .map(|row| {

+ 27 - 0
crates/cdk-sql-common/src/mint/migrations.rs

@@ -0,0 +1,27 @@
+/// @generated
+/// Auto-generated by build.rs
+pub static MIGRATIONS: &[(&str, &str)] = &[
+    ("postgres/20250710212391_init.sql", include_str!(r#"./migrations/postgres/20250710212391_init.sql"#)),
+    ("sqlite/1_fix_sqlx_migration.sql", include_str!(r#"./migrations/sqlite/1_fix_sqlx_migration.sql"#)),
+    ("sqlite/20240612124932_init.sql", include_str!(r#"./migrations/sqlite/20240612124932_init.sql"#)),
+    ("sqlite/20240618195700_quote_state.sql", include_str!(r#"./migrations/sqlite/20240618195700_quote_state.sql"#)),
+    ("sqlite/20240626092101_nut04_state.sql", include_str!(r#"./migrations/sqlite/20240626092101_nut04_state.sql"#)),
+    ("sqlite/20240703122347_request_lookup_id.sql", include_str!(r#"./migrations/sqlite/20240703122347_request_lookup_id.sql"#)),
+    ("sqlite/20240710145043_input_fee.sql", include_str!(r#"./migrations/sqlite/20240710145043_input_fee.sql"#)),
+    ("sqlite/20240711183109_derivation_path_index.sql", include_str!(r#"./migrations/sqlite/20240711183109_derivation_path_index.sql"#)),
+    ("sqlite/20240718203721_allow_unspent.sql", include_str!(r#"./migrations/sqlite/20240718203721_allow_unspent.sql"#)),
+    ("sqlite/20240811031111_update_mint_url.sql", include_str!(r#"./migrations/sqlite/20240811031111_update_mint_url.sql"#)),
+    ("sqlite/20240919103407_proofs_quote_id.sql", include_str!(r#"./migrations/sqlite/20240919103407_proofs_quote_id.sql"#)),
+    ("sqlite/20240923153640_melt_requests.sql", include_str!(r#"./migrations/sqlite/20240923153640_melt_requests.sql"#)),
+    ("sqlite/20240930101140_dleq_for_sigs.sql", include_str!(r#"./migrations/sqlite/20240930101140_dleq_for_sigs.sql"#)),
+    ("sqlite/20241108093102_mint_mint_quote_pubkey.sql", include_str!(r#"./migrations/sqlite/20241108093102_mint_mint_quote_pubkey.sql"#)),
+    ("sqlite/20250103201327_amount_to_pay_msats.sql", include_str!(r#"./migrations/sqlite/20250103201327_amount_to_pay_msats.sql"#)),
+    ("sqlite/20250129200912_remove_mint_url.sql", include_str!(r#"./migrations/sqlite/20250129200912_remove_mint_url.sql"#)),
+    ("sqlite/20250129230326_add_config_table.sql", include_str!(r#"./migrations/sqlite/20250129230326_add_config_table.sql"#)),
+    ("sqlite/20250307213652_keyset_id_as_foreign_key.sql", include_str!(r#"./migrations/sqlite/20250307213652_keyset_id_as_foreign_key.sql"#)),
+    ("sqlite/20250406091754_mint_time_of_quotes.sql", include_str!(r#"./migrations/sqlite/20250406091754_mint_time_of_quotes.sql"#)),
+    ("sqlite/20250406093755_mint_created_time_signature.sql", include_str!(r#"./migrations/sqlite/20250406093755_mint_created_time_signature.sql"#)),
+    ("sqlite/20250415093121_drop_keystore_foreign.sql", include_str!(r#"./migrations/sqlite/20250415093121_drop_keystore_foreign.sql"#)),
+    ("sqlite/20250626120251_rename_blind_message_y_to_b.sql", include_str!(r#"./migrations/sqlite/20250626120251_rename_blind_message_y_to_b.sql"#)),
+    ("sqlite/20250706101057_bolt12.sql", include_str!(r#"./migrations/sqlite/20250706101057_bolt12.sql"#)),
+];

+ 82 - 0
crates/cdk-sql-common/src/mint/migrations/postgres/20250710212391_init.sql

@@ -0,0 +1,82 @@
+CREATE TABLE keyset (
+  id TEXT PRIMARY KEY, unit TEXT NOT NULL,
+  active BOOL NOT NULL, valid_from INTEGER NOT NULL,
+  valid_to INTEGER, derivation_path TEXT NOT NULL,
+  max_order INTEGER NOT NULL, input_fee_ppk INTEGER,
+  derivation_path_index INTEGER
+);
+CREATE TABLE mint_quote (
+  id TEXT PRIMARY KEY,
+  amount INTEGER NOT NULL,
+  unit TEXT NOT NULL,
+  request TEXT NOT NULL,
+  expiry INTEGER NOT NULL,
+  state TEXT CHECK (
+    state IN (
+      'UNPAID', 'PENDING', 'PAID', 'ISSUED'
+    )
+  ) NOT NULL DEFAULT 'UNPAID',
+  request_lookup_id TEXT,
+  pubkey TEXT,
+  created_time INTEGER NOT NULL DEFAULT 0,
+  paid_time INTEGER,
+  issued_time INTEGER
+);
+CREATE TABLE melt_quote (
+  id TEXT PRIMARY KEY,
+  unit TEXT NOT NULL,
+  amount INTEGER NOT NULL,
+  request TEXT NOT NULL,
+  fee_reserve INTEGER NOT NULL,
+  expiry INTEGER NOT NULL,
+  state TEXT CHECK (
+    state IN ('UNPAID', 'PENDING', 'PAID')
+  ) NOT NULL DEFAULT 'UNPAID',
+  payment_preimage TEXT,
+  request_lookup_id TEXT,
+  msat_to_pay INTEGER,
+  created_time INTEGER NOT NULL DEFAULT 0,
+  paid_time INTEGER
+);
+CREATE TABLE melt_request (
+  id TEXT PRIMARY KEY, inputs TEXT NOT NULL,
+  outputs TEXT, method TEXT NOT NULL,
+  unit TEXT NOT NULL
+);
+CREATE TABLE config (
+  id TEXT PRIMARY KEY, value TEXT NOT NULL
+);
+CREATE TABLE IF NOT EXISTS "proof" (
+  y BYTEA PRIMARY KEY,
+  amount INTEGER NOT NULL,
+  keyset_id TEXT NOT NULL,
+  secret TEXT NOT NULL,
+  c BYTEA NOT NULL,
+  witness TEXT,
+  state TEXT CHECK (
+    state IN (
+      'SPENT', 'PENDING', 'UNSPENT', 'RESERVED',
+      'UNKNOWN'
+    )
+  ) NOT NULL,
+  quote_id TEXT,
+  created_time INTEGER NOT NULL DEFAULT 0
+);
+CREATE TABLE IF NOT EXISTS "blind_signature" (
+  y BYTEA PRIMARY KEY,
+  amount INTEGER NOT NULL,
+  keyset_id TEXT NOT NULL,
+  c BYTEA NOT NULL,
+  dleq_e TEXT,
+  dleq_s TEXT,
+  quote_id TEXT,
+  created_time INTEGER NOT NULL DEFAULT 0
+);
+CREATE INDEX unit_index ON keyset(unit);
+CREATE INDEX active_index ON keyset(active);
+CREATE INDEX request_index ON mint_quote(request);
+CREATE INDEX expiry_index ON mint_quote(expiry);
+CREATE INDEX melt_quote_state_index ON melt_quote(state);
+CREATE INDEX mint_quote_state_index ON mint_quote(state);
+CREATE UNIQUE INDEX unique_request_lookup_id_mint ON mint_quote(request_lookup_id);
+CREATE UNIQUE INDEX unique_request_lookup_id_melt ON melt_quote(request_lookup_id);

+ 20 - 0
crates/cdk-sql-common/src/mint/migrations/sqlite/1_fix_sqlx_migration.sql

@@ -0,0 +1,20 @@
+-- Migrate `_sqlx_migrations` to our new migration system
+CREATE TABLE IF NOT EXISTS _sqlx_migrations AS
+SELECT
+    '' AS version,
+    '' AS description,
+    0 AS execution_time
+WHERE 0;
+
+INSERT INTO migrations
+SELECT
+    version || '_' ||  REPLACE(description, ' ', '_') || '.sql',
+    execution_time
+FROM _sqlx_migrations
+WHERE EXISTS (
+    SELECT 1
+    FROM sqlite_master
+    WHERE type = 'table' AND name = '_sqlx_migrations'
+);
+
+DROP TABLE _sqlx_migrations;

+ 0 - 0
crates/cdk-sqlite/src/mint/migrations/20240612124932_init.sql → crates/cdk-sql-common/src/mint/migrations/sqlite/20240612124932_init.sql


+ 0 - 0
crates/cdk-sqlite/src/mint/migrations/20240618195700_quote_state.sql → crates/cdk-sql-common/src/mint/migrations/sqlite/20240618195700_quote_state.sql


+ 0 - 0
crates/cdk-sqlite/src/mint/migrations/20240626092101_nut04_state.sql → crates/cdk-sql-common/src/mint/migrations/sqlite/20240626092101_nut04_state.sql


+ 0 - 0
crates/cdk-sqlite/src/mint/migrations/20240703122347_request_lookup_id.sql → crates/cdk-sql-common/src/mint/migrations/sqlite/20240703122347_request_lookup_id.sql


+ 0 - 0
crates/cdk-sqlite/src/mint/migrations/20240710145043_input_fee.sql → crates/cdk-sql-common/src/mint/migrations/sqlite/20240710145043_input_fee.sql


+ 0 - 0
crates/cdk-sqlite/src/mint/migrations/20240711183109_derivation_path_index.sql → crates/cdk-sql-common/src/mint/migrations/sqlite/20240711183109_derivation_path_index.sql


+ 0 - 0
crates/cdk-sqlite/src/mint/migrations/20240718203721_allow_unspent.sql → crates/cdk-sql-common/src/mint/migrations/sqlite/20240718203721_allow_unspent.sql


+ 0 - 0
crates/cdk-sqlite/src/mint/migrations/20240811031111_update_mint_url.sql → crates/cdk-sql-common/src/mint/migrations/sqlite/20240811031111_update_mint_url.sql


+ 0 - 0
crates/cdk-sqlite/src/mint/migrations/20240919103407_proofs_quote_id.sql → crates/cdk-sql-common/src/mint/migrations/sqlite/20240919103407_proofs_quote_id.sql


+ 0 - 0
crates/cdk-sqlite/src/mint/migrations/20240923153640_melt_requests.sql → crates/cdk-sql-common/src/mint/migrations/sqlite/20240923153640_melt_requests.sql


+ 0 - 0
crates/cdk-sqlite/src/mint/migrations/20240930101140_dleq_for_sigs.sql → crates/cdk-sql-common/src/mint/migrations/sqlite/20240930101140_dleq_for_sigs.sql


+ 0 - 0
crates/cdk-sqlite/src/mint/migrations/20241108093102_mint_mint_quote_pubkey.sql → crates/cdk-sql-common/src/mint/migrations/sqlite/20241108093102_mint_mint_quote_pubkey.sql


+ 0 - 0
crates/cdk-sqlite/src/mint/migrations/20250103201327_amount_to_pay_msats.sql → crates/cdk-sql-common/src/mint/migrations/sqlite/20250103201327_amount_to_pay_msats.sql


+ 0 - 0
crates/cdk-sqlite/src/mint/migrations/20250129200912_remove_mint_url.sql → crates/cdk-sql-common/src/mint/migrations/sqlite/20250129200912_remove_mint_url.sql


+ 0 - 0
crates/cdk-sqlite/src/mint/migrations/20250129230326_add_config_table.sql → crates/cdk-sql-common/src/mint/migrations/sqlite/20250129230326_add_config_table.sql


+ 1 - 1
crates/cdk-sqlite/src/mint/migrations/20250307213652_keyset_id_as_foreign_key.sql → crates/cdk-sql-common/src/mint/migrations/sqlite/20250307213652_keyset_id_as_foreign_key.sql

@@ -1,5 +1,5 @@
 -- Add foreign key constraints for keyset_id in SQLite
--- SQLite requires recreating tables to add foreign keys
+-- SQL requires recreating tables to add foreign keys
 
 -- First, ensure we have the right schema information
 PRAGMA foreign_keys = OFF;

+ 0 - 0
crates/cdk-sqlite/src/mint/migrations/20250406091754_mint_time_of_quotes.sql → crates/cdk-sql-common/src/mint/migrations/sqlite/20250406091754_mint_time_of_quotes.sql


+ 0 - 0
crates/cdk-sqlite/src/mint/migrations/20250406093755_mint_created_time_signature.sql → crates/cdk-sql-common/src/mint/migrations/sqlite/20250406093755_mint_created_time_signature.sql


+ 0 - 0
crates/cdk-sqlite/src/mint/migrations/20250415093121_drop_keystore_foreign.sql → crates/cdk-sql-common/src/mint/migrations/sqlite/20250415093121_drop_keystore_foreign.sql


+ 0 - 0
crates/cdk-sqlite/src/mint/migrations/20250626120251_rename_blind_message_y_to_b.sql → crates/cdk-sql-common/src/mint/migrations/sqlite/20250626120251_rename_blind_message_y_to_b.sql


+ 0 - 0
crates/cdk-sqlite/src/mint/migrations/20250706101057_bolt12.sql → crates/cdk-sql-common/src/mint/migrations/sqlite/20250706101057_bolt12.sql


+ 1796 - 0
crates/cdk-sql-common/src/mint/mod.rs

@@ -0,0 +1,1796 @@
+//! SQL database implementation of the Mint
+//!
+//! This is a generic SQL implementation for the mint storage layer. Any database can be plugged in
+//! as long as standard ANSI SQL is used, as Postgres and SQLite would understand it.
+//!
+//! This implementation also has a rudimentary but standard migration and versioning system.
+//!
+//! The trait expects an asynchronous interaction, but it also provides tools to spawn blocking
+//! clients in a pool and expose them to an asynchronous environment, making them compatible with
+//! Mint.
+use std::collections::HashMap;
+use std::marker::PhantomData;
+use std::str::FromStr;
+
+use async_trait::async_trait;
+use bitcoin::bip32::DerivationPath;
+use cdk_common::common::QuoteTTL;
+use cdk_common::database::{
+    self, ConversionError, Error, MintDatabase, MintDbWriterFinalizer, MintKeyDatabaseTransaction,
+    MintKeysDatabase, MintProofsDatabase, MintQuotesDatabase, MintQuotesTransaction,
+    MintSignatureTransaction, MintSignaturesDatabase,
+};
+use cdk_common::mint::{
+    self, IncomingPayment, Issuance, MeltPaymentRequest, MeltQuote, MintKeySetInfo, MintQuote,
+};
+use cdk_common::nut00::ProofsMethods;
+use cdk_common::payment::PaymentIdentifier;
+use cdk_common::secret::Secret;
+use cdk_common::state::check_state_transition;
+use cdk_common::util::unix_time;
+use cdk_common::{
+    Amount, BlindSignature, BlindSignatureDleq, CurrencyUnit, Id, MeltQuoteState, MintInfo,
+    PaymentMethod, Proof, Proofs, PublicKey, SecretKey, State,
+};
+use lightning_invoice::Bolt11Invoice;
+use migrations::MIGRATIONS;
+use tracing::instrument;
+use uuid::Uuid;
+
+use crate::common::migrate;
+use crate::database::{DatabaseConnector, DatabaseExecutor, DatabaseTransaction};
+use crate::stmt::{query, Column};
+use crate::{
+    column_as_nullable_number, column_as_nullable_string, column_as_number, column_as_string,
+    unpack_into,
+};
+
+#[cfg(feature = "auth")]
+mod auth;
+
+#[rustfmt::skip]
+mod migrations;
+
+
+#[cfg(feature = "auth")]
+pub use auth::SQLMintAuthDatabase;
+
+/// Mint SQL Database
+#[derive(Debug, Clone)]
+pub struct SQLMintDatabase<DB>
+where
+    DB: DatabaseConnector,
+{
+    db: DB,
+}
+
+/// SQL Transaction Writer
+pub struct SQLTransaction<'a, T>
+where
+    T: DatabaseTransaction<'a>,
+{
+    inner: T,
+    _phantom: PhantomData<&'a ()>,
+}
+
+#[inline(always)]
+async fn get_current_states<C>(
+    conn: &C,
+    ys: &[PublicKey],
+) -> Result<HashMap<PublicKey, State>, Error>
+where
+    C: DatabaseExecutor + Send + Sync,
+{
+    query(r#"SELECT y, state 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()
+        .map(|row| {
+            Ok((
+                column_as_string!(&row[0], PublicKey::from_hex, PublicKey::from_slice),
+                column_as_string!(&row[1], State::from_str),
+            ))
+        })
+        .collect::<Result<HashMap<_, _>, _>>()
+}
+
+#[inline(always)]
+async fn set_to_config<C, V>(conn: &C, id: &str, value: &V) -> Result<(), Error>
+where
+    C: DatabaseExecutor + Send + Sync,
+    V: ?Sized + serde::Serialize,
+{
+    query(
+        r#"
+        INSERT INTO config (id, value) VALUES (:id, :value)
+            ON CONFLICT(id) DO UPDATE SET value = excluded.value
+            "#,
+    )?
+    .bind("id", id.to_owned())
+    .bind("value", serde_json::to_string(&value)?)
+    .execute(conn)
+    .await?;
+
+    Ok(())
+}
+
+impl<DB> SQLMintDatabase<DB>
+where
+    DB: DatabaseConnector,
+{
+    /// Creates a new instance
+    pub async fn new<X>(db: X) -> Result<Self, Error>
+    where
+        X: Into<DB>,
+    {
+        let db = db.into();
+        Self::migrate(&db).await?;
+        Ok(Self { db })
+    }
+
+    /// Migrate
+    async fn migrate(conn: &DB) -> Result<(), Error> {
+        let tx = conn.begin().await?;
+        migrate(&tx, DB::name(), MIGRATIONS).await?;
+        tx.commit().await?;
+        Ok(())
+    }
+
+    #[inline(always)]
+    async fn fetch_from_config<R>(&self, id: &str) -> Result<R, Error>
+    where
+        R: serde::de::DeserializeOwned,
+    {
+        let value = column_as_string!(query(r#"SELECT value FROM config WHERE id = :id LIMIT 1"#)?
+            .bind("id", id.to_owned())
+            .pluck(&self.db)
+            .await?
+            .ok_or(Error::UnknownQuoteTTL)?);
+
+        Ok(serde_json::from_str(&value)?)
+    }
+}
+
+#[async_trait]
+impl<'a, T> database::MintProofsTransaction<'a> for SQLTransaction<'a, T>
+where
+    T: DatabaseTransaction<'a>,
+{
+    type Err = Error;
+
+    async fn add_proofs(
+        &mut self,
+        proofs: Proofs,
+        quote_id: Option<Uuid>,
+    ) -> Result<(), Self::Err> {
+        let current_time = unix_time();
+
+        // Check any previous proof, this query should return None in order to proceed storing
+        // Any result here would error
+        match query(r#"SELECT state FROM proof WHERE y IN (:ys) LIMIT 1 FOR UPDATE"#)?
+            .bind_vec(
+                "ys",
+                proofs
+                    .iter()
+                    .map(|y| y.y().map(|y| y.to_bytes().to_vec()))
+                    .collect::<Result<_, _>>()?,
+            )
+            .pluck(&self.inner)
+            .await?
+            .map(|state| Ok::<_, Error>(column_as_string!(&state, State::from_str)))
+            .transpose()?
+        {
+            Some(State::Spent) => Err(database::Error::AttemptUpdateSpentProof),
+            Some(_) => Err(database::Error::Duplicate),
+            None => Ok(()), // no previous record
+        }?;
+
+        for proof in proofs {
+            query(
+                r#"
+                  INSERT INTO proof
+                  (y, amount, keyset_id, secret, c, witness, state, quote_id, created_time)
+                  VALUES
+                  (:y, :amount, :keyset_id, :secret, :c, :witness, :state, :quote_id, :created_time)
+                  "#,
+            )?
+            .bind("y", proof.y()?.to_bytes().to_vec())
+            .bind("amount", proof.amount.to_i64())
+            .bind("keyset_id", proof.keyset_id.to_string())
+            .bind("secret", proof.secret.to_string())
+            .bind("c", proof.c.to_bytes().to_vec())
+            .bind(
+                "witness",
+                proof.witness.map(|w| serde_json::to_string(&w).unwrap()),
+            )
+            .bind("state", "UNSPENT".to_string())
+            .bind("quote_id", quote_id.map(|q| q.hyphenated().to_string()))
+            .bind("created_time", current_time as i64)
+            .execute(&self.inner)
+            .await?;
+        }
+
+        Ok(())
+    }
+
+    async fn update_proofs_states(
+        &mut self,
+        ys: &[PublicKey],
+        new_state: State,
+    ) -> Result<Vec<Option<State>>, Self::Err> {
+        let mut current_states = get_current_states(&self.inner, ys).await?;
+
+        if current_states.len() != ys.len() {
+            tracing::warn!(
+                "Attempted to update state of non-existent proof {} {}",
+                current_states.len(),
+                ys.len()
+            );
+            return Err(database::Error::ProofNotFound);
+        }
+
+        for state in current_states.values() {
+            check_state_transition(*state, new_state)?;
+        }
+
+        query(r#"UPDATE proof SET state = :new_state WHERE y IN (:ys)"#)?
+            .bind("new_state", new_state.to_string())
+            .bind_vec("ys", ys.iter().map(|y| y.to_bytes().to_vec()).collect())
+            .execute(&self.inner)
+            .await?;
+
+        Ok(ys.iter().map(|y| current_states.remove(y)).collect())
+    }
+
+    async fn remove_proofs(
+        &mut self,
+        ys: &[PublicKey],
+        _quote_id: Option<Uuid>,
+    ) -> Result<(), Self::Err> {
+        let total_deleted = query(
+            r#"
+            DELETE FROM proof WHERE y IN (:ys) AND state NOT IN (:exclude_state)
+            "#,
+        )?
+        .bind_vec("ys", ys.iter().map(|y| y.to_bytes().to_vec()).collect())
+        .bind_vec("exclude_state", vec![State::Spent.to_string()])
+        .execute(&self.inner)
+        .await?;
+
+        if total_deleted != ys.len() {
+            return Err(Self::Err::AttemptRemoveSpentProof);
+        }
+
+        Ok(())
+    }
+}
+
+#[async_trait]
+impl<'a, T> database::MintTransaction<'a, Error> for SQLTransaction<'a, T>
+where
+    T: DatabaseTransaction<'a>,
+{
+    async fn set_mint_info(&mut self, mint_info: MintInfo) -> Result<(), Error> {
+        Ok(set_to_config(&self.inner, "mint_info", &mint_info).await?)
+    }
+
+    async fn set_quote_ttl(&mut self, quote_ttl: QuoteTTL) -> Result<(), Error> {
+        Ok(set_to_config(&self.inner, "quote_ttl", &quote_ttl).await?)
+    }
+}
+
+#[async_trait]
+impl<'a, T> MintDbWriterFinalizer for SQLTransaction<'a, T>
+where
+    T: DatabaseTransaction<'a>,
+{
+    type Err = Error;
+
+    async fn commit(self: Box<Self>) -> Result<(), Error> {
+        Ok(self.inner.commit().await?)
+    }
+
+    async fn rollback(self: Box<Self>) -> Result<(), Error> {
+        Ok(self.inner.rollback().await?)
+    }
+}
+
+#[inline(always)]
+async fn get_mint_quote_payments<C>(
+    conn: &C,
+    quote_id: &Uuid,
+) -> Result<Vec<IncomingPayment>, Error>
+where
+    C: DatabaseExecutor + Send + Sync,
+{
+    // Get payment IDs and timestamps from the mint_quote_payments table
+    query(
+        r#"
+SELECT payment_id, timestamp, amount
+FROM mint_quote_payments
+WHERE quote_id=:quote_id;
+            "#,
+    )?
+    .bind("quote_id", quote_id.as_hyphenated().to_string())
+    .fetch_all(conn)
+    .await?
+    .into_iter()
+    .map(|row| {
+        let amount: u64 = column_as_number!(row[2].clone());
+        let time: u64 = column_as_number!(row[1].clone());
+        Ok(IncomingPayment::new(
+            amount.into(),
+            column_as_string!(&row[0]),
+            time,
+        ))
+    })
+    .collect()
+}
+
+#[inline(always)]
+async fn get_mint_quote_issuance<C>(conn: &C, quote_id: &Uuid) -> Result<Vec<Issuance>, Error>
+where
+    C: DatabaseExecutor + Send + Sync,
+{
+    // Get payment IDs and timestamps from the mint_quote_payments table
+    query(
+        r#"
+SELECT amount, timestamp
+FROM mint_quote_issued
+WHERE quote_id=:quote_id
+            "#,
+    )?
+    .bind("quote_id", quote_id.as_hyphenated().to_string())
+    .fetch_all(conn)
+    .await?
+    .into_iter()
+    .map(|row| {
+        let time: u64 = column_as_number!(row[1].clone());
+        Ok(Issuance::new(
+            Amount::from_i64(column_as_number!(row[0].clone()))
+                .expect("Is amount when put into db"),
+            time,
+        ))
+    })
+    .collect()
+}
+
+#[async_trait]
+impl<'a, T> MintKeyDatabaseTransaction<'a, Error> for SQLTransaction<'a, T>
+where
+    T: DatabaseTransaction<'a>,
+{
+    async fn add_keyset_info(&mut self, keyset: MintKeySetInfo) -> Result<(), Error> {
+        query(
+            r#"
+        INSERT INTO
+            keyset (
+                id, unit, active, valid_from, valid_to, derivation_path,
+                max_order, input_fee_ppk, derivation_path_index
+            )
+        VALUES (
+            :id, :unit, :active, :valid_from, :valid_to, :derivation_path,
+            :max_order, :input_fee_ppk, :derivation_path_index
+        )
+        ON CONFLICT(id) DO UPDATE SET
+            unit = excluded.unit,
+            active = excluded.active,
+            valid_from = excluded.valid_from,
+            valid_to = excluded.valid_to,
+            derivation_path = excluded.derivation_path,
+            max_order = excluded.max_order,
+            input_fee_ppk = excluded.input_fee_ppk,
+            derivation_path_index = excluded.derivation_path_index
+        "#,
+        )?
+        .bind("id", keyset.id.to_string())
+        .bind("unit", keyset.unit.to_string())
+        .bind("active", keyset.active)
+        .bind("valid_from", keyset.valid_from as i64)
+        .bind("valid_to", keyset.final_expiry.map(|v| v as i64))
+        .bind("derivation_path", keyset.derivation_path.to_string())
+        .bind("max_order", keyset.max_order)
+        .bind("input_fee_ppk", keyset.input_fee_ppk as i64)
+        .bind("derivation_path_index", keyset.derivation_path_index)
+        .execute(&self.inner)
+        .await?;
+
+        Ok(())
+    }
+
+    async fn set_active_keyset(&mut self, unit: CurrencyUnit, id: Id) -> Result<(), Error> {
+        query(r#"UPDATE keyset SET active=FALSE WHERE unit IS :unit"#)?
+            .bind("unit", unit.to_string())
+            .execute(&self.inner)
+            .await?;
+
+        query(r#"UPDATE keyset SET active=TRUE WHERE unit IS :unit AND id IS :id"#)?
+            .bind("unit", unit.to_string())
+            .bind("id", id.to_string())
+            .execute(&self.inner)
+            .await?;
+
+        Ok(())
+    }
+}
+
+#[async_trait]
+impl<DB> MintKeysDatabase for SQLMintDatabase<DB>
+where
+    DB: DatabaseConnector,
+{
+    type Err = Error;
+
+    async fn begin_transaction<'a>(
+        &'a self,
+    ) -> Result<Box<dyn MintKeyDatabaseTransaction<'a, Error> + Send + Sync + 'a>, Error> {
+        Ok(Box::new(SQLTransaction {
+            inner: self.db.begin().await?,
+            _phantom: PhantomData,
+        }))
+    }
+
+    async fn get_active_keyset_id(&self, unit: &CurrencyUnit) -> Result<Option<Id>, Self::Err> {
+        Ok(
+            query(r#" SELECT id FROM keyset WHERE active = 1 AND unit IS :unit"#)?
+                .bind("unit", unit.to_string())
+                .pluck(&self.db)
+                .await?
+                .map(|id| match id {
+                    Column::Text(text) => Ok(Id::from_str(&text)?),
+                    Column::Blob(id) => Ok(Id::from_bytes(&id)?),
+                    _ => Err(Error::InvalidKeysetId),
+                })
+                .transpose()?,
+        )
+    }
+
+    async fn get_active_keysets(&self) -> Result<HashMap<CurrencyUnit, Id>, Self::Err> {
+        Ok(query(r#"SELECT id, unit FROM keyset WHERE active = 1"#)?
+            .fetch_all(&self.db)
+            .await?
+            .into_iter()
+            .map(|row| {
+                Ok((
+                    column_as_string!(&row[1], CurrencyUnit::from_str),
+                    column_as_string!(&row[0], Id::from_str, Id::from_bytes),
+                ))
+            })
+            .collect::<Result<HashMap<_, _>, Error>>()?)
+    }
+
+    async fn get_keyset_info(&self, id: &Id) -> Result<Option<MintKeySetInfo>, Self::Err> {
+        Ok(query(
+            r#"SELECT
+                id,
+                unit,
+                active,
+                valid_from,
+                valid_to,
+                derivation_path,
+                derivation_path_index,
+                max_order,
+                input_fee_ppk
+            FROM
+                keyset
+                WHERE id=:id"#,
+        )?
+        .bind("id", id.to_string())
+        .fetch_one(&self.db)
+        .await?
+        .map(sql_row_to_keyset_info)
+        .transpose()?)
+    }
+
+    async fn get_keyset_infos(&self) -> Result<Vec<MintKeySetInfo>, Self::Err> {
+        Ok(query(
+            r#"SELECT
+                id,
+                unit,
+                active,
+                valid_from,
+                valid_to,
+                derivation_path,
+                derivation_path_index,
+                max_order,
+                input_fee_ppk
+            FROM
+                keyset
+            "#,
+        )?
+        .fetch_all(&self.db)
+        .await?
+        .into_iter()
+        .map(sql_row_to_keyset_info)
+        .collect::<Result<Vec<_>, _>>()?)
+    }
+}
+
+#[async_trait]
+impl<'a, T> MintQuotesTransaction<'a> for SQLTransaction<'a, T>
+where
+    T: DatabaseTransaction<'a>,
+{
+    type Err = Error;
+
+    #[instrument(skip(self))]
+    async fn increment_mint_quote_amount_paid(
+        &mut self,
+        quote_id: &Uuid,
+        amount_paid: Amount,
+        payment_id: String,
+    ) -> Result<Amount, Self::Err> {
+        // Check if payment_id already exists in mint_quote_payments
+        let exists = query(
+            r#"
+            SELECT payment_id
+            FROM mint_quote_payments
+            WHERE payment_id = :payment_id
+            FOR UPDATE
+            "#,
+        )?
+        .bind("payment_id", payment_id.clone())
+        .fetch_one(&self.inner)
+        .await?;
+
+        if exists.is_some() {
+            tracing::error!("Payment ID already exists: {}", payment_id);
+            return Err(database::Error::Duplicate);
+        }
+
+        // Get current amount_paid from quote
+        let current_amount = query(
+            r#"
+            SELECT amount_paid
+            FROM mint_quote
+            WHERE id = :quote_id
+            FOR UPDATE
+            "#,
+        )?
+        .bind("quote_id", quote_id.as_hyphenated().to_string())
+        .fetch_one(&self.inner)
+        .await
+        .map_err(|err| {
+            tracing::error!("SQLite could not get mint quote amount_paid");
+            err
+        })?;
+
+        let current_amount_paid = if let Some(current_amount) = current_amount {
+            let amount: u64 = column_as_number!(current_amount[0].clone());
+            Amount::from(amount)
+        } else {
+            Amount::ZERO
+        };
+
+        // Calculate new amount_paid with overflow check
+        let new_amount_paid = current_amount_paid
+            .checked_add(amount_paid)
+            .ok_or_else(|| database::Error::AmountOverflow)?;
+
+        // Update the amount_paid
+        query(
+            r#"
+            UPDATE mint_quote
+            SET amount_paid = :amount_paid
+            WHERE id = :quote_id
+            "#,
+        )?
+        .bind("amount_paid", new_amount_paid.to_i64())
+        .bind("quote_id", quote_id.as_hyphenated().to_string())
+        .execute(&self.inner)
+        .await
+        .map_err(|err| {
+            tracing::error!("SQLite could not update mint quote amount_paid");
+            err
+        })?;
+
+        // Add payment_id to mint_quote_payments table
+        query(
+            r#"
+            INSERT INTO mint_quote_payments
+            (quote_id, payment_id, amount, timestamp)
+            VALUES (:quote_id, :payment_id, :amount, :timestamp)
+            "#,
+        )?
+        .bind("quote_id", quote_id.as_hyphenated().to_string())
+        .bind("payment_id", payment_id)
+        .bind("amount", amount_paid.to_i64())
+        .bind("timestamp", unix_time() as i64)
+        .execute(&self.inner)
+        .await
+        .map_err(|err| {
+            tracing::error!("SQLite could not insert payment ID: {}", err);
+            err
+        })?;
+
+        Ok(new_amount_paid)
+    }
+
+    #[instrument(skip_all)]
+    async fn increment_mint_quote_amount_issued(
+        &mut self,
+        quote_id: &Uuid,
+        amount_issued: Amount,
+    ) -> Result<Amount, Self::Err> {
+        // Get current amount_issued from quote
+        let current_amount = query(
+            r#"
+            SELECT amount_issued
+            FROM mint_quote
+            WHERE id = :quote_id
+            FOR UPDATE
+            "#,
+        )?
+        .bind("quote_id", quote_id.as_hyphenated().to_string())
+        .fetch_one(&self.inner)
+        .await
+        .map_err(|err| {
+            tracing::error!("SQLite could not get mint quote amount_issued");
+            err
+        })?;
+
+        let current_amount_issued = if let Some(current_amount) = current_amount {
+            let amount: u64 = column_as_number!(current_amount[0].clone());
+            Amount::from(amount)
+        } else {
+            Amount::ZERO
+        };
+
+        // Calculate new amount_issued with overflow check
+        let new_amount_issued = current_amount_issued
+            .checked_add(amount_issued)
+            .ok_or_else(|| database::Error::AmountOverflow)?;
+
+        // Update the amount_issued
+        query(
+            r#"
+            UPDATE mint_quote
+            SET amount_issued = :amount_issued
+            WHERE id = :quote_id
+            FOR UPDATE
+            "#,
+        )?
+        .bind("amount_issued", new_amount_issued.to_i64())
+        .bind("quote_id", quote_id.as_hyphenated().to_string())
+        .execute(&self.inner)
+        .await
+        .map_err(|err| {
+            tracing::error!("SQLite could not update mint quote amount_issued");
+            err
+        })?;
+
+        let current_time = unix_time();
+
+        query(
+            r#"
+INSERT INTO mint_quote_issued
+(quote_id, amount, timestamp)
+VALUES (:quote_id, :amount, :timestamp);
+            "#,
+        )?
+        .bind("quote_id", quote_id.as_hyphenated().to_string())
+        .bind("amount", amount_issued.to_i64())
+        .bind("timestamp", current_time as i64)
+        .execute(&self.inner)
+        .await?;
+
+        Ok(new_amount_issued)
+    }
+
+    #[instrument(skip_all)]
+    async fn add_mint_quote(&mut self, quote: MintQuote) -> Result<(), Self::Err> {
+        tracing::debug!("Adding quote with: {}", quote.payment_method.to_string());
+        println!("Adding quote with: {}", quote.payment_method.to_string());
+        query(
+            r#"
+                INSERT INTO mint_quote (
+                id, amount, unit, request, expiry, request_lookup_id, pubkey, created_time, payment_method, request_lookup_id_kind
+                )
+                VALUES (
+                :id, :amount, :unit, :request, :expiry, :request_lookup_id, :pubkey, :created_time, :payment_method, :request_lookup_id_kind
+                )
+            "#,
+        )?
+        .bind("id", quote.id.to_string())
+        .bind("amount", quote.amount.map(|a| a.to_i64()))
+        .bind("unit", quote.unit.to_string())
+        .bind("request", quote.request)
+        .bind("expiry", quote.expiry as i64)
+        .bind(
+            "request_lookup_id",
+            quote.request_lookup_id.to_string(),
+        )
+        .bind("pubkey", quote.pubkey.map(|p| p.to_string()))
+        .bind("created_time", quote.created_time as i64)
+        .bind("payment_method", quote.payment_method.to_string())
+        .bind("request_lookup_id_kind", quote.request_lookup_id.kind())
+        .execute(&self.inner)
+        .await?;
+
+        Ok(())
+    }
+
+    async fn remove_mint_quote(&mut self, quote_id: &Uuid) -> Result<(), Self::Err> {
+        query(r#"DELETE FROM mint_quote WHERE id=:id"#)?
+            .bind("id", quote_id.as_hyphenated().to_string())
+            .execute(&self.inner)
+            .await?;
+        Ok(())
+    }
+
+    async fn add_melt_quote(&mut self, quote: mint::MeltQuote) -> Result<(), Self::Err> {
+        // First try to find and replace any expired UNPAID quotes with the same request_lookup_id
+        let current_time = unix_time();
+        let row_affected = query(
+            r#"
+            DELETE FROM melt_quote
+            WHERE request_lookup_id = :request_lookup_id
+            AND state = :state
+            AND expiry < :current_time
+            "#,
+        )?
+        .bind("request_lookup_id", quote.request_lookup_id.to_string())
+        .bind("state", MeltQuoteState::Unpaid.to_string())
+        .bind("current_time", current_time as i64)
+        .execute(&self.inner)
+        .await?;
+
+        if row_affected > 0 {
+            tracing::info!("Received new melt quote for existing invoice with expired quote.");
+        }
+
+        // Now insert the new quote
+        query(
+            r#"
+            INSERT INTO melt_quote
+            (
+                id, unit, amount, request, fee_reserve, state,
+                expiry, payment_preimage, request_lookup_id,
+                created_time, paid_time, options, request_lookup_id_kind
+            )
+            VALUES
+            (
+                :id, :unit, :amount, :request, :fee_reserve, :state,
+                :expiry, :payment_preimage, :request_lookup_id,
+                :created_time, :paid_time, :options, :request_lookup_id_kind
+            )
+        "#,
+        )?
+        .bind("id", quote.id.to_string())
+        .bind("unit", quote.unit.to_string())
+        .bind("amount", quote.amount.to_i64())
+        .bind("request", serde_json::to_string(&quote.request)?)
+        .bind("fee_reserve", quote.fee_reserve.to_i64())
+        .bind("state", quote.state.to_string())
+        .bind("expiry", quote.expiry as i64)
+        .bind("payment_preimage", quote.payment_preimage)
+        .bind("request_lookup_id", quote.request_lookup_id.to_string())
+        .bind("created_time", quote.created_time as i64)
+        .bind("paid_time", quote.paid_time.map(|t| t as i64))
+        .bind(
+            "options",
+            quote.options.map(|o| serde_json::to_string(&o).ok()),
+        )
+        .bind("request_lookup_id_kind", quote.request_lookup_id.kind())
+        .execute(&self.inner)
+        .await?;
+
+        Ok(())
+    }
+
+    async fn update_melt_quote_request_lookup_id(
+        &mut self,
+        quote_id: &Uuid,
+        new_request_lookup_id: &PaymentIdentifier,
+    ) -> Result<(), Self::Err> {
+        query(r#"UPDATE melt_quote SET request_lookup_id = :new_req_id, request_lookup_id_kind = :new_kind WHERE id = :id"#)?
+            .bind("new_req_id", new_request_lookup_id.to_string())
+            .bind("new_kind",new_request_lookup_id.kind() )
+            .bind("id", quote_id.as_hyphenated().to_string())
+            .execute(&self.inner)
+            .await?;
+        Ok(())
+    }
+
+    async fn update_melt_quote_state(
+        &mut self,
+        quote_id: &Uuid,
+        state: MeltQuoteState,
+        payment_proof: Option<String>,
+    ) -> Result<(MeltQuoteState, mint::MeltQuote), Self::Err> {
+        let mut quote = query(
+            r#"
+            SELECT
+                id,
+                unit,
+                amount,
+                request,
+                fee_reserve,
+                expiry,
+                state,
+                payment_preimage,
+                request_lookup_id,
+                created_time,
+                paid_time,
+                payment_method,
+                options,
+                request_lookup_id_kind
+            FROM
+                melt_quote
+            WHERE
+                id=:id
+                AND state != :state
+            "#,
+        )?
+        .bind("id", quote_id.as_hyphenated().to_string())
+        .bind("state", state.to_string())
+        .fetch_one(&self.inner)
+        .await?
+        .map(sql_row_to_melt_quote)
+        .transpose()?
+        .ok_or(Error::QuoteNotFound)?;
+
+        let rec = if state == MeltQuoteState::Paid {
+            let current_time = unix_time();
+            query(r#"UPDATE melt_quote SET state = :state, paid_time = :paid_time, payment_preimage = :payment_preimage WHERE id = :id"#)?
+                .bind("state", state.to_string())
+                .bind("paid_time", current_time as i64)
+                .bind("payment_preimage", payment_proof)
+                .bind("id", quote_id.as_hyphenated().to_string())
+                .execute(&self.inner)
+                .await
+        } else {
+            query(r#"UPDATE melt_quote SET state = :state WHERE id = :id"#)?
+                .bind("state", state.to_string())
+                .bind("id", quote_id.as_hyphenated().to_string())
+                .execute(&self.inner)
+                .await
+        };
+
+        match rec {
+            Ok(_) => {}
+            Err(err) => {
+                tracing::error!("SQLite Could not update melt quote");
+                return Err(err);
+            }
+        };
+
+        let old_state = quote.state;
+        quote.state = state;
+
+        Ok((old_state, quote))
+    }
+
+    async fn remove_melt_quote(&mut self, quote_id: &Uuid) -> Result<(), Self::Err> {
+        query(
+            r#"
+            DELETE FROM melt_quote
+            WHERE id=?
+            "#,
+        )?
+        .bind("id", quote_id.as_hyphenated().to_string())
+        .execute(&self.inner)
+        .await?;
+
+        Ok(())
+    }
+
+    async fn get_mint_quote(&mut self, quote_id: &Uuid) -> Result<Option<MintQuote>, Self::Err> {
+        let payments = get_mint_quote_payments(&self.inner, quote_id).await?;
+        let issuance = get_mint_quote_issuance(&self.inner, quote_id).await?;
+
+        Ok(query(
+            r#"
+            SELECT
+                id,
+                amount,
+                unit,
+                request,
+                expiry,
+                request_lookup_id,
+                pubkey,
+                created_time,
+                amount_paid,
+                amount_issued,
+                payment_method,
+                request_lookup_id_kind
+            FROM
+                mint_quote
+            WHERE id = :id
+            FOR UPDATE
+            "#,
+        )?
+        .bind("id", quote_id.as_hyphenated().to_string())
+        .fetch_one(&self.inner)
+        .await?
+        .map(|row| sql_row_to_mint_quote(row, payments, issuance))
+        .transpose()?)
+    }
+
+    async fn get_melt_quote(
+        &mut self,
+        quote_id: &Uuid,
+    ) -> Result<Option<mint::MeltQuote>, Self::Err> {
+        Ok(query(
+            r#"
+            SELECT
+                id,
+                unit,
+                amount,
+                request,
+                fee_reserve,
+                expiry,
+                state,
+                payment_preimage,
+                request_lookup_id,
+                created_time,
+                paid_time,
+                payment_method,
+                options,
+                request_lookup_id
+            FROM
+                melt_quote
+            WHERE
+                id=:id
+            "#,
+        )?
+        .bind("id", quote_id.as_hyphenated().to_string())
+        .fetch_one(&self.inner)
+        .await?
+        .map(sql_row_to_melt_quote)
+        .transpose()?)
+    }
+
+    async fn get_mint_quote_by_request(
+        &mut self,
+        request: &str,
+    ) -> Result<Option<MintQuote>, Self::Err> {
+        let mut mint_quote = query(
+            r#"
+            SELECT
+                id,
+                amount,
+                unit,
+                request,
+                expiry,
+                request_lookup_id,
+                pubkey,
+                created_time,
+                amount_paid,
+                amount_issued,
+                payment_method,
+                request_lookup_id_kind
+            FROM
+                mint_quote
+            WHERE request = :request
+            FOR UPDATE
+            "#,
+        )?
+        .bind("request", request.to_string())
+        .fetch_one(&self.inner)
+        .await?
+        .map(|row| sql_row_to_mint_quote(row, vec![], vec![]))
+        .transpose()?;
+
+        if let Some(quote) = mint_quote.as_mut() {
+            let payments = get_mint_quote_payments(&self.inner, &quote.id).await?;
+            let issuance = get_mint_quote_issuance(&self.inner, &quote.id).await?;
+            quote.issuance = issuance;
+            quote.payments = payments;
+        }
+
+        Ok(mint_quote)
+    }
+
+    async fn get_mint_quote_by_request_lookup_id(
+        &mut self,
+        request_lookup_id: &PaymentIdentifier,
+    ) -> Result<Option<MintQuote>, Self::Err> {
+        let mut mint_quote = query(
+            r#"
+            SELECT
+                id,
+                amount,
+                unit,
+                request,
+                expiry,
+                request_lookup_id,
+                pubkey,
+                created_time,
+                amount_paid,
+                amount_issued,
+                payment_method,
+                request_lookup_id_kind
+            FROM
+                mint_quote
+            WHERE request_lookup_id = :request_lookup_id
+            AND request_lookup_id_kind = :request_lookup_id_kind
+            FOR UPDATE
+            "#,
+        )?
+        .bind("request_lookup_id", request_lookup_id.to_string())
+        .bind("request_lookup_id_kind", request_lookup_id.kind())
+        .fetch_one(&self.inner)
+        .await?
+        .map(|row| sql_row_to_mint_quote(row, vec![], vec![]))
+        .transpose()?;
+
+        if let Some(quote) = mint_quote.as_mut() {
+            let payments = get_mint_quote_payments(&self.inner, &quote.id).await?;
+            let issuance = get_mint_quote_issuance(&self.inner, &quote.id).await?;
+            quote.issuance = issuance;
+            quote.payments = payments;
+        }
+
+        Ok(mint_quote)
+    }
+}
+
+#[async_trait]
+impl<DB> MintQuotesDatabase for SQLMintDatabase<DB>
+where
+    DB: DatabaseConnector,
+{
+    type Err = Error;
+
+    async fn get_mint_quote(&self, quote_id: &Uuid) -> Result<Option<MintQuote>, Self::Err> {
+        let payments = get_mint_quote_payments(&self.db, quote_id).await?;
+        let issuance = get_mint_quote_issuance(&self.db, quote_id).await?;
+
+        Ok(query(
+            r#"
+            SELECT
+                id,
+                amount,
+                unit,
+                request,
+                expiry,
+                request_lookup_id,
+                pubkey,
+                created_time,
+                amount_paid,
+                amount_issued,
+                payment_method,
+                request_lookup_id_kind
+            FROM
+                mint_quote
+            WHERE id = :id"#,
+        )?
+        .bind("id", quote_id.as_hyphenated().to_string())
+        .fetch_one(&self.db)
+        .await?
+        .map(|row| sql_row_to_mint_quote(row, payments, issuance))
+        .transpose()?)
+    }
+
+    async fn get_mint_quote_by_request(
+        &self,
+        request: &str,
+    ) -> Result<Option<MintQuote>, Self::Err> {
+        let mut mint_quote = query(
+            r#"
+            SELECT
+                id,
+                amount,
+                unit,
+                request,
+                expiry,
+                request_lookup_id,
+                pubkey,
+                created_time,
+                amount_paid,
+                amount_issued,
+                payment_method,
+                request_lookup_id_kind
+            FROM
+                mint_quote
+            WHERE request = :request"#,
+        )?
+        .bind("request", request.to_owned())
+        .fetch_one(&self.db)
+        .await?
+        .map(|row| sql_row_to_mint_quote(row, vec![], vec![]))
+        .transpose()?;
+
+        if let Some(quote) = mint_quote.as_mut() {
+            let payments = get_mint_quote_payments(&self.db, &quote.id).await?;
+            let issuance = get_mint_quote_issuance(&self.db, &quote.id).await?;
+            quote.issuance = issuance;
+            quote.payments = payments;
+        }
+
+        Ok(mint_quote)
+    }
+
+    async fn get_mint_quote_by_request_lookup_id(
+        &self,
+        request_lookup_id: &PaymentIdentifier,
+    ) -> Result<Option<MintQuote>, Self::Err> {
+        let mut mint_quote = query(
+            r#"
+            SELECT
+                id,
+                amount,
+                unit,
+                request,
+                expiry,
+                request_lookup_id,
+                pubkey,
+                created_time,
+                amount_paid,
+                amount_issued,
+                payment_method,
+                request_lookup_id_kind
+            FROM
+                mint_quote
+            WHERE request_lookup_id = :request_lookup_id"#,
+        )?
+        .bind(
+            "request_lookup_id",
+            serde_json::to_string(request_lookup_id)?,
+        )
+        .fetch_one(&self.db)
+        .await?
+        .map(|row| sql_row_to_mint_quote(row, vec![], vec![]))
+        .transpose()?;
+
+        // TODO: these should use an sql join so they can be done in one query
+        if let Some(quote) = mint_quote.as_mut() {
+            let payments = get_mint_quote_payments(&self.db, &quote.id).await?;
+            let issuance = get_mint_quote_issuance(&self.db, &quote.id).await?;
+            quote.issuance = issuance;
+            quote.payments = payments;
+        }
+
+        Ok(mint_quote)
+    }
+
+    async fn get_mint_quotes(&self) -> Result<Vec<MintQuote>, Self::Err> {
+        let mut mint_quotes = query(
+            r#"
+            SELECT
+                id,
+                amount,
+                unit,
+                request,
+                expiry,
+                request_lookup_id,
+                pubkey,
+                created_time,
+                amount_paid,
+                amount_issued,
+                payment_method,
+                request_lookup_id_kind
+            FROM
+                mint_quote
+            "#,
+        )?
+        .fetch_all(&self.db)
+        .await?
+        .into_iter()
+        .map(|row| sql_row_to_mint_quote(row, vec![], vec![]))
+        .collect::<Result<Vec<_>, _>>()?;
+
+        for quote in mint_quotes.as_mut_slice() {
+            let payments = get_mint_quote_payments(&self.db, &quote.id).await?;
+            let issuance = get_mint_quote_issuance(&self.db, &quote.id).await?;
+            quote.issuance = issuance;
+            quote.payments = payments;
+        }
+
+        Ok(mint_quotes)
+    }
+
+    async fn get_melt_quote(&self, quote_id: &Uuid) -> Result<Option<mint::MeltQuote>, Self::Err> {
+        Ok(query(
+            r#"
+            SELECT
+                id,
+                unit,
+                amount,
+                request,
+                fee_reserve,
+                expiry,
+                state,
+                payment_preimage,
+                request_lookup_id,
+                created_time,
+                paid_time,
+                payment_method,
+                options,
+                request_lookup_id_kind
+            FROM
+                melt_quote
+            WHERE
+                id=:id
+            "#,
+        )?
+        .bind("id", quote_id.as_hyphenated().to_string())
+        .fetch_one(&self.db)
+        .await?
+        .map(sql_row_to_melt_quote)
+        .transpose()?)
+    }
+
+    async fn get_melt_quotes(&self) -> Result<Vec<mint::MeltQuote>, Self::Err> {
+        Ok(query(
+            r#"
+            SELECT
+                id,
+                unit,
+                amount,
+                request,
+                fee_reserve,
+                expiry,
+                state,
+                payment_preimage,
+                request_lookup_id,
+                created_time,
+                paid_time,
+                payment_method,
+                options,
+                request_lookup_id_kind
+            FROM
+                melt_quote
+            "#,
+        )?
+        .fetch_all(&self.db)
+        .await?
+        .into_iter()
+        .map(sql_row_to_melt_quote)
+        .collect::<Result<Vec<_>, _>>()?)
+    }
+}
+
+#[async_trait]
+impl<DB> MintProofsDatabase for SQLMintDatabase<DB>
+where
+    DB: DatabaseConnector,
+{
+    type Err = Error;
+
+    async fn get_proofs_by_ys(&self, ys: &[PublicKey]) -> Result<Vec<Option<Proof>>, Self::Err> {
+        let mut proofs = query(
+            r#"
+            SELECT
+                amount,
+                keyset_id,
+                secret,
+                c,
+                witness,
+                y
+            FROM
+                proof
+            WHERE
+                y IN (:ys)
+            "#,
+        )?
+        .bind_vec("ys", ys.iter().map(|y| y.to_bytes().to_vec()).collect())
+        .fetch_all(&self.db)
+        .await?
+        .into_iter()
+        .map(|mut row| {
+            Ok((
+                column_as_string!(
+                    row.pop().ok_or(Error::InvalidDbResponse)?,
+                    PublicKey::from_hex,
+                    PublicKey::from_slice
+                ),
+                sql_row_to_proof(row)?,
+            ))
+        })
+        .collect::<Result<HashMap<_, _>, Error>>()?;
+
+        Ok(ys.iter().map(|y| proofs.remove(y)).collect())
+    }
+
+    async fn get_proof_ys_by_quote_id(&self, quote_id: &Uuid) -> Result<Vec<PublicKey>, Self::Err> {
+        Ok(query(
+            r#"
+            SELECT
+                amount,
+                keyset_id,
+                secret,
+                c,
+                witness
+            FROM
+                proof
+            WHERE
+                quote_id = :quote_id
+            "#,
+        )?
+        .bind("quote_id", quote_id.as_hyphenated().to_string())
+        .fetch_all(&self.db)
+        .await?
+        .into_iter()
+        .map(sql_row_to_proof)
+        .collect::<Result<Vec<Proof>, _>>()?
+        .ys()?)
+    }
+
+    async fn get_proofs_states(&self, ys: &[PublicKey]) -> Result<Vec<Option<State>>, Self::Err> {
+        let mut current_states = get_current_states(&self.db, ys).await?;
+
+        Ok(ys.iter().map(|y| current_states.remove(y)).collect())
+    }
+
+    async fn get_proofs_by_keyset_id(
+        &self,
+        keyset_id: &Id,
+    ) -> Result<(Proofs, Vec<Option<State>>), Self::Err> {
+        Ok(query(
+            r#"
+            SELECT
+               keyset_id,
+               amount,
+               secret,
+               c,
+               witness,
+               state
+            FROM
+                proof
+            WHERE
+                keyset_id=?
+            "#,
+        )?
+        .bind("keyset_id", keyset_id.to_string())
+        .fetch_all(&self.db)
+        .await?
+        .into_iter()
+        .map(sql_row_to_proof_with_state)
+        .collect::<Result<Vec<_>, _>>()?
+        .into_iter()
+        .unzip())
+    }
+}
+
+#[async_trait]
+impl<'a, T> MintSignatureTransaction<'a> for SQLTransaction<'a, T>
+where
+    T: DatabaseTransaction<'a>,
+{
+    type Err = Error;
+
+    async fn add_blind_signatures(
+        &mut self,
+        blinded_messages: &[PublicKey],
+        blind_signatures: &[BlindSignature],
+        quote_id: Option<Uuid>,
+    ) -> Result<(), Self::Err> {
+        let current_time = unix_time();
+
+        for (message, signature) in blinded_messages.iter().zip(blind_signatures) {
+            query(
+                r#"
+                    INSERT INTO blind_signature
+                    (blinded_message, amount, keyset_id, c, quote_id, dleq_e, dleq_s, created_time)
+                    VALUES
+                    (:blinded_message, :amount, :keyset_id, :c, :quote_id, :dleq_e, :dleq_s, :created_time)
+                "#,
+            )?
+            .bind("blinded_message", message.to_bytes().to_vec())
+            .bind("amount", u64::from(signature.amount) as i64)
+            .bind("keyset_id", signature.keyset_id.to_string())
+            .bind("c", signature.c.to_bytes().to_vec())
+            .bind("quote_id", quote_id.map(|q| q.hyphenated().to_string()))
+            .bind(
+                "dleq_e",
+                signature.dleq.as_ref().map(|dleq| dleq.e.to_secret_hex()),
+            )
+            .bind(
+                "dleq_s",
+                signature.dleq.as_ref().map(|dleq| dleq.s.to_secret_hex()),
+            )
+            .bind("created_time", current_time as i64)
+            .execute(&self.inner)
+            .await?;
+        }
+
+        Ok(())
+    }
+
+    async fn get_blind_signatures(
+        &mut self,
+        blinded_messages: &[PublicKey],
+    ) -> Result<Vec<Option<BlindSignature>>, Self::Err> {
+        let mut blinded_signatures = query(
+            r#"SELECT
+                keyset_id,
+                amount,
+                c,
+                dleq_e,
+                dleq_s,
+                blinded_message
+            FROM
+                blind_signature
+            WHERE blinded_message IN (:y)
+            "#,
+        )?
+        .bind_vec(
+            "y",
+            blinded_messages
+                .iter()
+                .map(|y| y.to_bytes().to_vec())
+                .collect(),
+        )
+        .fetch_all(&self.inner)
+        .await?
+        .into_iter()
+        .map(|mut row| {
+            Ok((
+                column_as_string!(
+                    &row.pop().ok_or(Error::InvalidDbResponse)?,
+                    PublicKey::from_hex,
+                    PublicKey::from_slice
+                ),
+                sql_row_to_blind_signature(row)?,
+            ))
+        })
+        .collect::<Result<HashMap<_, _>, Error>>()?;
+        Ok(blinded_messages
+            .iter()
+            .map(|y| blinded_signatures.remove(y))
+            .collect())
+    }
+}
+
+#[async_trait]
+impl<DB> MintSignaturesDatabase for SQLMintDatabase<DB>
+where
+    DB: DatabaseConnector,
+{
+    type Err = Error;
+
+    async fn get_blind_signatures(
+        &self,
+        blinded_messages: &[PublicKey],
+    ) -> Result<Vec<Option<BlindSignature>>, Self::Err> {
+        let mut blinded_signatures = query(
+            r#"SELECT
+                keyset_id,
+                amount,
+                c,
+                dleq_e,
+                dleq_s,
+                blinded_message
+            FROM
+                blind_signature
+            WHERE blinded_message IN (:blinded_message)
+            "#,
+        )?
+        .bind_vec(
+            "blinded_message",
+            blinded_messages
+                .iter()
+                .map(|b_| b_.to_bytes().to_vec())
+                .collect(),
+        )
+        .fetch_all(&self.db)
+        .await?
+        .into_iter()
+        .map(|mut row| {
+            Ok((
+                column_as_string!(
+                    &row.pop().ok_or(Error::InvalidDbResponse)?,
+                    PublicKey::from_hex,
+                    PublicKey::from_slice
+                ),
+                sql_row_to_blind_signature(row)?,
+            ))
+        })
+        .collect::<Result<HashMap<_, _>, Error>>()?;
+        Ok(blinded_messages
+            .iter()
+            .map(|y| blinded_signatures.remove(y))
+            .collect())
+    }
+
+    async fn get_blind_signatures_for_keyset(
+        &self,
+        keyset_id: &Id,
+    ) -> Result<Vec<BlindSignature>, Self::Err> {
+        Ok(query(
+            r#"
+            SELECT
+                keyset_id,
+                amount,
+                c,
+                dleq_e,
+                dleq_s
+            FROM
+                blind_signature
+            WHERE
+                keyset_id=:keyset_id
+            "#,
+        )?
+        .bind("keyset_id", keyset_id.to_string())
+        .fetch_all(&self.db)
+        .await?
+        .into_iter()
+        .map(sql_row_to_blind_signature)
+        .collect::<Result<Vec<BlindSignature>, _>>()?)
+    }
+
+    /// Get [`BlindSignature`]s for quote
+    async fn get_blind_signatures_for_quote(
+        &self,
+        quote_id: &Uuid,
+    ) -> Result<Vec<BlindSignature>, Self::Err> {
+        Ok(query(
+            r#"
+            SELECT
+                keyset_id,
+                amount,
+                c,
+                dleq_e,
+                dleq_s
+            FROM
+                blind_signature
+            WHERE
+                quote_id=:quote_id
+            "#,
+        )?
+        .bind("quote_id", quote_id.to_string())
+        .fetch_all(&self.db)
+        .await?
+        .into_iter()
+        .map(sql_row_to_blind_signature)
+        .collect::<Result<Vec<BlindSignature>, _>>()?)
+    }
+}
+
+#[async_trait]
+impl<DB> MintDatabase<Error> for SQLMintDatabase<DB>
+where
+    DB: DatabaseConnector,
+{
+    async fn begin_transaction<'a>(
+        &'a self,
+    ) -> Result<Box<dyn database::MintTransaction<'a, Error> + Send + Sync + 'a>, Error> {
+        Ok(Box::new(SQLTransaction {
+            inner: self.db.begin().await?,
+            _phantom: PhantomData,
+        }))
+    }
+
+    async fn get_mint_info(&self) -> Result<MintInfo, Error> {
+        Ok(self.fetch_from_config("mint_info").await?)
+    }
+
+    async fn get_quote_ttl(&self) -> Result<QuoteTTL, Error> {
+        Ok(self.fetch_from_config("quote_ttl").await?)
+    }
+}
+
+fn sql_row_to_keyset_info(row: Vec<Column>) -> Result<MintKeySetInfo, Error> {
+    unpack_into!(
+        let (
+            id,
+            unit,
+            active,
+            valid_from,
+            valid_to,
+            derivation_path,
+            derivation_path_index,
+            max_order,
+            row_keyset_ppk
+        ) = row
+    );
+
+    Ok(MintKeySetInfo {
+        id: column_as_string!(id, Id::from_str, Id::from_bytes),
+        unit: column_as_string!(unit, CurrencyUnit::from_str),
+        active: matches!(active, Column::Integer(1)),
+        valid_from: column_as_number!(valid_from),
+        derivation_path: column_as_string!(derivation_path, DerivationPath::from_str),
+        derivation_path_index: column_as_nullable_number!(derivation_path_index),
+        max_order: column_as_number!(max_order),
+        input_fee_ppk: column_as_number!(row_keyset_ppk),
+        final_expiry: column_as_nullable_number!(valid_to),
+    })
+}
+
+#[instrument(skip_all)]
+fn sql_row_to_mint_quote(
+    row: Vec<Column>,
+    payments: Vec<IncomingPayment>,
+    issueances: Vec<Issuance>,
+) -> Result<MintQuote, Error> {
+    unpack_into!(
+        let (
+            id, amount, unit, request, expiry, request_lookup_id,
+            pubkey, created_time, amount_paid, amount_issued, payment_method, request_lookup_id_kind
+        ) = row
+    );
+
+    let request_str = column_as_string!(&request);
+    let request_lookup_id = column_as_nullable_string!(&request_lookup_id).unwrap_or_else(|| {
+        Bolt11Invoice::from_str(&request_str)
+            .map(|invoice| invoice.payment_hash().to_string())
+            .unwrap_or_else(|_| request_str.clone())
+    });
+    let request_lookup_id_kind = column_as_string!(request_lookup_id_kind);
+
+    let pubkey = column_as_nullable_string!(&pubkey)
+        .map(|pk| PublicKey::from_hex(&pk))
+        .transpose()?;
+
+    let id = column_as_string!(id);
+    let amount: Option<u64> = column_as_nullable_number!(amount);
+    let amount_paid: u64 = column_as_number!(amount_paid);
+    let amount_issued: u64 = column_as_number!(amount_issued);
+    let payment_method = column_as_string!(payment_method, PaymentMethod::from_str);
+
+    Ok(MintQuote::new(
+        Some(Uuid::parse_str(&id).map_err(|_| Error::InvalidUuid(id))?),
+        request_str,
+        column_as_string!(unit, CurrencyUnit::from_str),
+        amount.map(Amount::from),
+        column_as_number!(expiry),
+        PaymentIdentifier::new(&request_lookup_id_kind, &request_lookup_id)
+            .map_err(|_| ConversionError::MissingParameter("Payment id".to_string()))?,
+        pubkey,
+        amount_paid.into(),
+        amount_issued.into(),
+        payment_method,
+        column_as_number!(created_time),
+        payments,
+        issueances,
+    ))
+}
+
+fn sql_row_to_melt_quote(row: Vec<Column>) -> Result<mint::MeltQuote, Error> {
+    unpack_into!(
+        let (
+                id,
+                unit,
+                amount,
+                request,
+                fee_reserve,
+                expiry,
+                state,
+                payment_preimage,
+                request_lookup_id,
+                created_time,
+                paid_time,
+                payment_method,
+                options,
+                request_lookup_id_kind
+        ) = row
+    );
+
+    let id = column_as_string!(id);
+    let amount: u64 = column_as_number!(amount);
+    let fee_reserve: u64 = column_as_number!(fee_reserve);
+
+    let expiry = column_as_number!(expiry);
+    let payment_preimage = column_as_nullable_string!(payment_preimage);
+    let options = column_as_nullable_string!(options);
+    let options = options.and_then(|o| serde_json::from_str(&o).ok());
+    let created_time: i64 = column_as_number!(created_time);
+    let paid_time = column_as_nullable_number!(paid_time);
+    let payment_method = PaymentMethod::from_str(&column_as_string!(payment_method))?;
+
+    let state =
+        MeltQuoteState::from_str(&column_as_string!(&state)).map_err(ConversionError::from)?;
+
+    let unit = column_as_string!(unit);
+    let request = column_as_string!(request);
+
+    let mut request_lookup_id_kind = column_as_string!(request_lookup_id_kind);
+
+    let request_lookup_id = column_as_nullable_string!(&request_lookup_id).unwrap_or_else(|| {
+        Bolt11Invoice::from_str(&request)
+            .map(|invoice| invoice.payment_hash().to_string())
+            .unwrap_or_else(|_| {
+                request_lookup_id_kind = "custom".to_string();
+                request.clone()
+            })
+    });
+
+    let request_lookup_id = PaymentIdentifier::new(&request_lookup_id_kind, &request_lookup_id)
+        .map_err(|_| ConversionError::MissingParameter("Payment id".to_string()))?;
+
+    let request = match serde_json::from_str(&request) {
+        Ok(req) => req,
+        Err(err) => {
+            tracing::debug!(
+                "Melt quote from pre migrations defaulting to bolt11 {}.",
+                err
+            );
+            let bolt11 = Bolt11Invoice::from_str(&request).unwrap();
+            MeltPaymentRequest::Bolt11 { bolt11 }
+        }
+    };
+
+    Ok(MeltQuote {
+        id: Uuid::parse_str(&id).map_err(|_| Error::InvalidUuid(id))?,
+        unit: CurrencyUnit::from_str(&unit)?,
+        amount: Amount::from(amount),
+        request,
+        fee_reserve: Amount::from(fee_reserve),
+        state,
+        expiry,
+        payment_preimage,
+        request_lookup_id,
+        options,
+        created_time: created_time as u64,
+        paid_time,
+        payment_method,
+    })
+}
+
+fn sql_row_to_proof(row: Vec<Column>) -> Result<Proof, Error> {
+    unpack_into!(
+        let (
+            amount,
+            keyset_id,
+            secret,
+            c,
+            witness
+        ) = row
+    );
+
+    let amount: u64 = column_as_number!(amount);
+    Ok(Proof {
+        amount: Amount::from(amount),
+        keyset_id: column_as_string!(keyset_id, Id::from_str),
+        secret: column_as_string!(secret, Secret::from_str),
+        c: column_as_string!(c, PublicKey::from_hex, PublicKey::from_slice),
+        witness: column_as_nullable_string!(witness).and_then(|w| serde_json::from_str(&w).ok()),
+        dleq: None,
+    })
+}
+
+fn sql_row_to_proof_with_state(row: Vec<Column>) -> Result<(Proof, Option<State>), Error> {
+    unpack_into!(
+        let (
+            keyset_id, amount, secret, c, witness, state
+        ) = row
+    );
+
+    let amount: u64 = column_as_number!(amount);
+    let state = column_as_nullable_string!(state).and_then(|s| State::from_str(&s).ok());
+
+    Ok((
+        Proof {
+            amount: Amount::from(amount),
+            keyset_id: column_as_string!(keyset_id, Id::from_str, Id::from_bytes),
+            secret: column_as_string!(secret, Secret::from_str),
+            c: column_as_string!(c, PublicKey::from_hex, PublicKey::from_slice),
+            witness: column_as_nullable_string!(witness)
+                .and_then(|w| serde_json::from_str(&w).ok()),
+            dleq: None,
+        },
+        state,
+    ))
+}
+
+fn sql_row_to_blind_signature(row: Vec<Column>) -> Result<BlindSignature, Error> {
+    unpack_into!(
+        let (
+            keyset_id, amount, c, dleq_e, dleq_s
+        ) = row
+    );
+
+    let dleq = match (
+        column_as_nullable_string!(dleq_e),
+        column_as_nullable_string!(dleq_s),
+    ) {
+        (Some(e), Some(s)) => Some(BlindSignatureDleq {
+            e: SecretKey::from_hex(e)?,
+            s: SecretKey::from_hex(s)?,
+        }),
+        _ => None,
+    };
+
+    let amount: u64 = column_as_number!(amount);
+
+    Ok(BlindSignature {
+        amount: Amount::from(amount),
+        keyset_id: column_as_string!(keyset_id, Id::from_str, Id::from_bytes),
+        c: column_as_string!(c, PublicKey::from_hex, PublicKey::from_slice),
+        dleq,
+    })
+}

+ 26 - 16
crates/cdk-sqlite/src/pool.rs → crates/cdk-sql-common/src/pool.rs

@@ -4,7 +4,7 @@
 
 use std::fmt::Debug;
 use std::ops::{Deref, DerefMut};
-use std::sync::atomic::{AtomicUsize, Ordering};
+use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
 use std::sync::{Arc, Condvar, Mutex};
 use std::time::Duration;
 
@@ -30,13 +30,17 @@ pub trait ResourceManager: Debug {
     type Resource: Debug;
 
     /// The configuration that is needed in order to create the resource
-    type Config: Debug;
+    type Config: Clone + Debug;
 
     /// The error the resource may return when creating a new instance
     type Error: Debug;
 
     /// Creates a new resource with a given config
-    fn new_resource(config: &Self::Config) -> Result<Self::Resource, Error<Self::Error>>;
+    fn new_resource(
+        config: &Self::Config,
+        still_valid: Arc<AtomicBool>,
+        timeout: Duration,
+    ) -> Result<Self::Resource, Error<Self::Error>>;
 
     /// The object is dropped
     fn drop(_resource: Self::Resource) {}
@@ -49,7 +53,7 @@ where
     RM: ResourceManager,
 {
     config: RM::Config,
-    queue: Mutex<Vec<RM::Resource>>,
+    queue: Mutex<Vec<(Arc<AtomicBool>, RM::Resource)>>,
     in_use: AtomicUsize,
     max_size: usize,
     default_timeout: Duration,
@@ -61,7 +65,7 @@ pub struct PooledResource<RM>
 where
     RM: ResourceManager,
 {
-    resource: Option<RM::Resource>,
+    resource: Option<(Arc<AtomicBool>, RM::Resource)>,
     pool: Arc<Pool<RM>>,
 }
 
@@ -88,7 +92,7 @@ where
     type Target = RM::Resource;
 
     fn deref(&self) -> &Self::Target {
-        self.resource.as_ref().expect("resource already dropped")
+        &self.resource.as_ref().expect("resource already dropped").1
     }
 }
 
@@ -97,7 +101,7 @@ where
     RM: ResourceManager,
 {
     fn deref_mut(&mut self) -> &mut Self::Target {
-        self.resource.as_mut().expect("resource already dropped")
+        &mut self.resource.as_mut().expect("resource already dropped").1
     }
 }
 
@@ -136,21 +140,27 @@ where
 
         loop {
             if let Some(resource) = resources.pop() {
-                drop(resources);
-                self.in_use.fetch_add(1, Ordering::AcqRel);
-
-                return Ok(PooledResource {
-                    resource: Some(resource),
-                    pool: self.clone(),
-                });
+                if resource.0.load(Ordering::SeqCst) {
+                    drop(resources);
+                    self.in_use.fetch_add(1, Ordering::AcqRel);
+
+                    return Ok(PooledResource {
+                        resource: Some(resource),
+                        pool: self.clone(),
+                    });
+                }
             }
 
             if self.in_use.load(Ordering::Relaxed) < self.max_size {
                 drop(resources);
                 self.in_use.fetch_add(1, Ordering::AcqRel);
+                let still_valid: Arc<AtomicBool> = Arc::new(true.into());
 
                 return Ok(PooledResource {
-                    resource: Some(RM::new_resource(&self.config)?),
+                    resource: Some((
+                        still_valid.clone(),
+                        RM::new_resource(&self.config, still_valid, timeout)?,
+                    )),
                     pool: self.clone(),
                 });
             }
@@ -178,7 +188,7 @@ where
         if let Ok(mut resources) = self.queue.lock() {
             loop {
                 while let Some(resource) = resources.pop() {
-                    RM::drop(resource);
+                    RM::drop(resource.1);
                 }
 
                 if self.in_use.load(Ordering::Relaxed) == 0 {

+ 292 - 0
crates/cdk-sql-common/src/stmt.rs

@@ -0,0 +1,292 @@
+//! Stataments mod
+use std::sync::Arc;
+
+use cdk_common::database::Error;
+
+use crate::database::DatabaseExecutor;
+use crate::value::Value;
+
+/// The Column type
+pub type Column = Value;
+
+/// Expected response type for a given SQL statement
+#[derive(Debug, Clone, Copy, Default)]
+pub enum ExpectedSqlResponse {
+    /// A single row
+    SingleRow,
+    /// All the rows that matches a query
+    #[default]
+    ManyRows,
+    /// How many rows were affected by the query
+    AffectedRows,
+    /// Return the first column of the first row
+    Pluck,
+    /// Batch
+    Batch,
+}
+
+/// Part value
+#[derive(Debug, Clone)]
+pub enum PlaceholderValue {
+    /// Value
+    Value(Value),
+    /// Set
+    Set(Vec<Value>),
+}
+
+impl From<Value> for PlaceholderValue {
+    fn from(value: Value) -> Self {
+        PlaceholderValue::Value(value)
+    }
+}
+
+impl From<Vec<Value>> for PlaceholderValue {
+    fn from(value: Vec<Value>) -> Self {
+        PlaceholderValue::Set(value)
+    }
+}
+
+/// SQL Part
+#[derive(Debug, Clone)]
+pub enum SqlPart {
+    /// Raw SQL statement
+    Raw(Arc<str>),
+    /// Placeholder
+    Placeholder(Arc<str>, Option<PlaceholderValue>),
+}
+
+/// SQL parser error
+#[derive(Debug, PartialEq, thiserror::Error)]
+pub enum SqlParseError {
+    /// Invalid SQL
+    #[error("Unterminated String literal")]
+    UnterminatedStringLiteral,
+    /// Invalid placeholder name
+    #[error("Invalid placeholder name")]
+    InvalidPlaceholder,
+}
+
+/// Rudimentary SQL parser.
+///
+/// This function does not validate the SQL statement, it only extracts the placeholder to be
+/// database agnostic.
+pub fn split_sql_parts(input: &str) -> Result<Vec<SqlPart>, SqlParseError> {
+    let mut parts = Vec::new();
+    let mut current = String::new();
+    let mut chars = input.chars().peekable();
+
+    while let Some(&c) = chars.peek() {
+        match c {
+            '\'' | '"' => {
+                // Start of string literal
+                let quote = c;
+                current.push(chars.next().unwrap());
+
+                let mut closed = false;
+                while let Some(&next) = chars.peek() {
+                    current.push(chars.next().unwrap());
+
+                    if next == quote {
+                        if chars.peek() == Some(&quote) {
+                            // Escaped quote (e.g. '' inside strings)
+                            current.push(chars.next().unwrap());
+                        } else {
+                            closed = true;
+                            break;
+                        }
+                    }
+                }
+
+                if !closed {
+                    return Err(SqlParseError::UnterminatedStringLiteral);
+                }
+            }
+
+            ':' => {
+                // Flush current raw SQL
+                if !current.is_empty() {
+                    parts.push(SqlPart::Raw(current.clone().into()));
+                    current.clear();
+                }
+
+                chars.next(); // consume ':'
+                let mut name = String::new();
+
+                while let Some(&next) = chars.peek() {
+                    if next.is_alphanumeric() || next == '_' {
+                        name.push(chars.next().unwrap());
+                    } else {
+                        break;
+                    }
+                }
+
+                if name.is_empty() {
+                    return Err(SqlParseError::InvalidPlaceholder);
+                }
+
+                parts.push(SqlPart::Placeholder(name.into(), None));
+            }
+
+            _ => {
+                current.push(chars.next().unwrap());
+            }
+        }
+    }
+
+    if !current.is_empty() {
+        parts.push(SqlPart::Raw(current.into()));
+    }
+
+    Ok(parts)
+}
+
+/// Sql message
+#[derive(Debug, Default)]
+pub struct Statement {
+    /// The SQL statement
+    pub parts: Vec<SqlPart>,
+    /// The expected response type
+    pub expected_response: ExpectedSqlResponse,
+}
+
+impl Statement {
+    /// Creates a new statement
+    pub fn new(sql: &str) -> Result<Self, SqlParseError> {
+        Ok(Self {
+            parts: split_sql_parts(sql)?,
+            ..Default::default()
+        })
+    }
+
+    /// Convert Statement into a SQL statement and the list of placeholders
+    ///
+    /// By default it converts the statement into placeholder using $1..$n placeholders which seems
+    /// to be more widely supported, although it can be reimplemented with other formats since part
+    /// is public
+    pub fn to_sql(self) -> Result<(String, Vec<Value>), Error> {
+        let mut placeholder_values = Vec::new();
+        let sql = self
+            .parts
+            .into_iter()
+            .map(|x| match x {
+                SqlPart::Placeholder(name, value) => {
+                    match value.ok_or(Error::MissingPlaceholder(name.to_string()))? {
+                        PlaceholderValue::Value(value) => {
+                            placeholder_values.push(value);
+                            Ok::<_, Error>(format!("${}", placeholder_values.len()))
+                        }
+                        PlaceholderValue::Set(mut values) => {
+                            let start_size = placeholder_values.len();
+                            placeholder_values.append(&mut values);
+                            let placeholders = (start_size + 1..=placeholder_values.len())
+                                .map(|i| format!("${i}"))
+                                .collect::<Vec<_>>()
+                                .join(", ");
+                            Ok(placeholders)
+                        }
+                    }
+                }
+                SqlPart::Raw(raw) => Ok(raw.trim().to_string()),
+            })
+            .collect::<Result<Vec<String>, _>>()?
+            .join(" ");
+
+        Ok((sql, placeholder_values))
+    }
+
+    /// Binds a given placeholder to a value.
+    #[inline]
+    pub fn bind<C, V>(mut self, name: C, value: V) -> Self
+    where
+        C: ToString,
+        V: Into<Value>,
+    {
+        let name = name.to_string();
+        let value = value.into();
+        let value: PlaceholderValue = value.into();
+
+        for part in self.parts.iter_mut() {
+            if let SqlPart::Placeholder(part_name, part_value) = part {
+                if **part_name == *name.as_str() {
+                    *part_value = Some(value.clone());
+                }
+            }
+        }
+
+        self
+    }
+
+    /// Binds a single variable with a vector.
+    ///
+    /// This will rewrite the function from `:foo` (where value is vec![1, 2, 3]) to `:foo0, :foo1,
+    /// :foo2` and binds each value from the value vector accordingly.
+    #[inline]
+    pub fn bind_vec<C, V>(mut self, name: C, value: Vec<V>) -> Self
+    where
+        C: ToString,
+        V: Into<Value>,
+    {
+        let name = name.to_string();
+        let value: PlaceholderValue = value
+            .into_iter()
+            .map(|x| x.into())
+            .collect::<Vec<Value>>()
+            .into();
+
+        for part in self.parts.iter_mut() {
+            if let SqlPart::Placeholder(part_name, part_value) = part {
+                if **part_name == *name.as_str() {
+                    *part_value = Some(value.clone());
+                }
+            }
+        }
+
+        self
+    }
+
+    /// Executes a query and returns the affected rows
+    pub async fn pluck<C>(self, conn: &C) -> Result<Option<Value>, Error>
+    where
+        C: DatabaseExecutor,
+    {
+        conn.pluck(self).await
+    }
+
+    /// Executes a query and returns the affected rows
+    pub async fn batch<C>(self, conn: &C) -> Result<(), Error>
+    where
+        C: DatabaseExecutor,
+    {
+        conn.batch(self).await
+    }
+
+    /// Executes a query and returns the affected rows
+    pub async fn execute<C>(self, conn: &C) -> Result<usize, Error>
+    where
+        C: DatabaseExecutor,
+    {
+        conn.execute(self).await
+    }
+
+    /// Runs the query and returns the first row or None
+    pub async fn fetch_one<C>(self, conn: &C) -> Result<Option<Vec<Column>>, Error>
+    where
+        C: DatabaseExecutor,
+    {
+        conn.fetch_one(self).await
+    }
+
+    /// Runs the query and returns the first row or None
+    pub async fn fetch_all<C>(self, conn: &C) -> Result<Vec<Vec<Column>>, Error>
+    where
+        C: DatabaseExecutor,
+    {
+        conn.fetch_all(self).await
+    }
+}
+
+/// Creates a new query statement
+#[inline(always)]
+pub fn query(sql: &str) -> Result<Statement, Error> {
+    Statement::new(sql).map_err(|e| Error::Database(Box::new(e)))
+}

+ 82 - 0
crates/cdk-sql-common/src/value.rs

@@ -0,0 +1,82 @@
+//! Generic Rust value representation for data from the database
+
+/// Generic Value representation of data from the any database
+#[derive(Clone, Debug, PartialEq)]
+pub enum Value {
+    /// The value is a `NULL` value.
+    Null,
+    /// The value is a signed integer.
+    Integer(i64),
+    /// The value is a floating point number.
+    Real(f64),
+    /// The value is a text string.
+    Text(String),
+    /// The value is a blob of data
+    Blob(Vec<u8>),
+}
+
+impl From<String> for Value {
+    fn from(value: String) -> Self {
+        Self::Text(value)
+    }
+}
+
+impl From<&str> for Value {
+    fn from(value: &str) -> Self {
+        Self::Text(value.to_owned())
+    }
+}
+
+impl From<&&str> for Value {
+    fn from(value: &&str) -> Self {
+        Self::Text(value.to_string())
+    }
+}
+
+impl From<Vec<u8>> for Value {
+    fn from(value: Vec<u8>) -> Self {
+        Self::Blob(value)
+    }
+}
+
+impl From<&[u8]> for Value {
+    fn from(value: &[u8]) -> Self {
+        Self::Blob(value.to_owned())
+    }
+}
+
+impl From<u8> for Value {
+    fn from(value: u8) -> Self {
+        Self::Integer(value.into())
+    }
+}
+
+impl From<i64> for Value {
+    fn from(value: i64) -> Self {
+        Self::Integer(value)
+    }
+}
+
+impl From<u32> for Value {
+    fn from(value: u32) -> Self {
+        Self::Integer(value.into())
+    }
+}
+
+impl From<bool> for Value {
+    fn from(value: bool) -> Self {
+        Self::Integer(if value { 1 } else { 0 })
+    }
+}
+
+impl<T> From<Option<T>> for Value
+where
+    T: Into<Value>,
+{
+    fn from(value: Option<T>) -> Self {
+        match value {
+            Some(v) => v.into(),
+            None => Value::Null,
+        }
+    }
+}

+ 1 - 1
crates/cdk-sqlite/src/wallet/error.rs → crates/cdk-sql-common/src/wallet/error.rs

@@ -2,7 +2,7 @@
 
 use thiserror::Error;
 
-/// SQLite Wallet Error
+/// SQL Wallet Error
 #[derive(Debug, Error)]
 pub enum Error {
     /// SQLX Error

+ 21 - 0
crates/cdk-sql-common/src/wallet/migrations.rs

@@ -0,0 +1,21 @@
+/// @generated
+/// Auto-generated by build.rs
+pub static MIGRATIONS: &[(&str, &str)] = &[
+    ("sqlite/20240612132920_init.sql", include_str!(r#"./migrations/sqlite/20240612132920_init.sql"#)),
+    ("sqlite/20240618200350_quote_state.sql", include_str!(r#"./migrations/sqlite/20240618200350_quote_state.sql"#)),
+    ("sqlite/20240626091921_nut04_state.sql", include_str!(r#"./migrations/sqlite/20240626091921_nut04_state.sql"#)),
+    ("sqlite/20240710144711_input_fee.sql", include_str!(r#"./migrations/sqlite/20240710144711_input_fee.sql"#)),
+    ("sqlite/20240810214105_mint_icon_url.sql", include_str!(r#"./migrations/sqlite/20240810214105_mint_icon_url.sql"#)),
+    ("sqlite/20240810233905_update_mint_url.sql", include_str!(r#"./migrations/sqlite/20240810233905_update_mint_url.sql"#)),
+    ("sqlite/20240902151515_icon_url.sql", include_str!(r#"./migrations/sqlite/20240902151515_icon_url.sql"#)),
+    ("sqlite/20240902210905_mint_time.sql", include_str!(r#"./migrations/sqlite/20240902210905_mint_time.sql"#)),
+    ("sqlite/20241011125207_mint_urls.sql", include_str!(r#"./migrations/sqlite/20241011125207_mint_urls.sql"#)),
+    ("sqlite/20241108092756_wallet_mint_quote_secretkey.sql", include_str!(r#"./migrations/sqlite/20241108092756_wallet_mint_quote_secretkey.sql"#)),
+    ("sqlite/20250214135017_mint_tos.sql", include_str!(r#"./migrations/sqlite/20250214135017_mint_tos.sql"#)),
+    ("sqlite/20250310111513_drop_nostr_last_checked.sql", include_str!(r#"./migrations/sqlite/20250310111513_drop_nostr_last_checked.sql"#)),
+    ("sqlite/20250314082116_allow_pending_spent.sql", include_str!(r#"./migrations/sqlite/20250314082116_allow_pending_spent.sql"#)),
+    ("sqlite/20250323152040_wallet_dleq_proofs.sql", include_str!(r#"./migrations/sqlite/20250323152040_wallet_dleq_proofs.sql"#)),
+    ("sqlite/20250401120000_add_transactions_table.sql", include_str!(r#"./migrations/sqlite/20250401120000_add_transactions_table.sql"#)),
+    ("sqlite/20250616144830_add_keyset_expiry.sql", include_str!(r#"./migrations/sqlite/20250616144830_add_keyset_expiry.sql"#)),
+    ("sqlite/20250707093445_bolt12.sql", include_str!(r#"./migrations/sqlite/20250707093445_bolt12.sql"#)),
+];

+ 0 - 0
crates/cdk-sqlite/src/wallet/migrations/20240612132920_init.sql → crates/cdk-sql-common/src/wallet/migrations/sqlite/20240612132920_init.sql


+ 0 - 0
crates/cdk-sqlite/src/wallet/migrations/20240618200350_quote_state.sql → crates/cdk-sql-common/src/wallet/migrations/sqlite/20240618200350_quote_state.sql


+ 0 - 0
crates/cdk-sqlite/src/wallet/migrations/20240626091921_nut04_state.sql → crates/cdk-sql-common/src/wallet/migrations/sqlite/20240626091921_nut04_state.sql


+ 0 - 0
crates/cdk-sqlite/src/wallet/migrations/20240710144711_input_fee.sql → crates/cdk-sql-common/src/wallet/migrations/sqlite/20240710144711_input_fee.sql


+ 0 - 0
crates/cdk-sqlite/src/wallet/migrations/20240810214105_mint_icon_url.sql → crates/cdk-sql-common/src/wallet/migrations/sqlite/20240810214105_mint_icon_url.sql


+ 0 - 0
crates/cdk-sqlite/src/wallet/migrations/20240810233905_update_mint_url.sql → crates/cdk-sql-common/src/wallet/migrations/sqlite/20240810233905_update_mint_url.sql


+ 0 - 0
crates/cdk-sqlite/src/wallet/migrations/20240902151515_icon_url.sql → crates/cdk-sql-common/src/wallet/migrations/sqlite/20240902151515_icon_url.sql


+ 0 - 0
crates/cdk-sqlite/src/wallet/migrations/20240902210905_mint_time.sql → crates/cdk-sql-common/src/wallet/migrations/sqlite/20240902210905_mint_time.sql


+ 0 - 0
crates/cdk-sqlite/src/wallet/migrations/20241011125207_mint_urls.sql → crates/cdk-sql-common/src/wallet/migrations/sqlite/20241011125207_mint_urls.sql


+ 0 - 0
crates/cdk-sqlite/src/wallet/migrations/20241108092756_wallet_mint_quote_secretkey.sql → crates/cdk-sql-common/src/wallet/migrations/sqlite/20241108092756_wallet_mint_quote_secretkey.sql


+ 0 - 0
crates/cdk-sqlite/src/wallet/migrations/20250214135017_mint_tos.sql → crates/cdk-sql-common/src/wallet/migrations/sqlite/20250214135017_mint_tos.sql


+ 0 - 0
crates/cdk-sqlite/src/wallet/migrations/20250310111513_drop_nostr_last_checked.sql → crates/cdk-sql-common/src/wallet/migrations/sqlite/20250310111513_drop_nostr_last_checked.sql


+ 0 - 0
crates/cdk-sqlite/src/wallet/migrations/20250314082116_allow_pending_spent.sql → crates/cdk-sql-common/src/wallet/migrations/sqlite/20250314082116_allow_pending_spent.sql


+ 0 - 0
crates/cdk-sqlite/src/wallet/migrations/20250323152040_wallet_dleq_proofs.sql → crates/cdk-sql-common/src/wallet/migrations/sqlite/20250323152040_wallet_dleq_proofs.sql


+ 0 - 0
crates/cdk-sqlite/src/wallet/migrations/20250401120000_add_transactions_table.sql → crates/cdk-sql-common/src/wallet/migrations/sqlite/20250401120000_add_transactions_table.sql


+ 0 - 0
crates/cdk-sqlite/src/wallet/migrations/20250616144830_add_keyset_expiry.sql → crates/cdk-sql-common/src/wallet/migrations/sqlite/20250616144830_add_keyset_expiry.sql


+ 0 - 0
crates/cdk-sqlite/src/wallet/migrations/20250707093445_bolt12.sql → crates/cdk-sql-common/src/wallet/migrations/sqlite/20250707093445_bolt12.sql


+ 1122 - 0
crates/cdk-sql-common/src/wallet/mod.rs

@@ -0,0 +1,1122 @@
+//! SQLite Wallet Database
+
+use std::collections::HashMap;
+use std::str::FromStr;
+
+use async_trait::async_trait;
+use cdk_common::common::ProofInfo;
+use cdk_common::database::{ConversionError, Error, WalletDatabase};
+use cdk_common::mint_url::MintUrl;
+use cdk_common::nuts::{MeltQuoteState, MintQuoteState};
+use cdk_common::secret::Secret;
+use cdk_common::wallet::{self, MintQuote, Transaction, TransactionDirection, TransactionId};
+use cdk_common::{
+    database, Amount, CurrencyUnit, Id, KeySet, KeySetInfo, Keys, MintInfo, PaymentMethod, Proof,
+    ProofDleq, PublicKey, SecretKey, SpendingConditions, State,
+};
+use tracing::instrument;
+
+use crate::common::migrate;
+use crate::database::DatabaseExecutor;
+use crate::stmt::{query, Column};
+use crate::{
+    column_as_binary, column_as_nullable_binary, column_as_nullable_number,
+    column_as_nullable_string, column_as_number, column_as_string, unpack_into,
+};
+
+#[rustfmt::skip]
+mod migrations;
+
+/// Wallet SQLite Database
+#[derive(Debug, Clone)]
+pub struct SQLWalletDatabase<T>
+where
+    T: DatabaseExecutor,
+{
+    db: T,
+}
+
+impl<DB> SQLWalletDatabase<DB>
+where
+    DB: DatabaseExecutor,
+{
+    /// Creates a new instance
+    pub async fn new<X>(db: X) -> Result<Self, Error>
+    where
+        X: Into<DB>,
+    {
+        let db = db.into();
+        Self::migrate(&db).await?;
+        Ok(Self { db })
+    }
+
+    /// Migrate [`WalletSqliteDatabase`]
+    async fn migrate(conn: &DB) -> Result<(), Error> {
+        migrate(conn, DB::name(), migrations::MIGRATIONS).await?;
+        Ok(())
+    }
+}
+
+#[async_trait]
+impl<T> WalletDatabase for SQLWalletDatabase<T>
+where
+    T: DatabaseExecutor,
+{
+    type Err = database::Error;
+
+    #[instrument(skip(self))]
+    async fn get_melt_quotes(&self) -> Result<Vec<wallet::MeltQuote>, Self::Err> {
+        Ok(query(
+            r#"
+              SELECT
+                  id,
+                  unit,
+                  amount,
+                  request,
+                  fee_reserve,
+                  state,
+                  expiry,
+                  payment_preimage
+              FROM
+                  melt_quote
+              "#,
+        )?
+        .fetch_all(&self.db)
+        .await?
+        .into_iter()
+        .map(sql_row_to_melt_quote)
+        .collect::<Result<_, _>>()?)
+    }
+
+    #[instrument(skip(self, mint_info))]
+    async fn add_mint(
+        &self,
+        mint_url: MintUrl,
+        mint_info: Option<MintInfo>,
+    ) -> Result<(), Self::Err> {
+        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.db)
+        .await?;
+
+        Ok(())
+    }
+
+    #[instrument(skip(self))]
+    async fn remove_mint(&self, mint_url: MintUrl) -> Result<(), Self::Err> {
+        query(r#"DELETE FROM mint WHERE mint_url=:mint_url"#)?
+            .bind("mint_url", mint_url.to_string())
+            .execute(&self.db)
+            .await?;
+
+        Ok(())
+    }
+
+    #[instrument(skip(self))]
+    async fn get_mint(&self, mint_url: MintUrl) -> Result<Option<MintInfo>, Self::Err> {
+        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(&self.db)
+        .await?
+        .map(sql_row_to_mint_info)
+        .transpose()?)
+    }
+
+    #[instrument(skip(self))]
+    async fn get_mints(&self) -> Result<HashMap<MintUrl, Option<MintInfo>>, Self::Err> {
+        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(&self.db)
+        .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::<Result<HashMap<_, _>, Error>>()?)
+    }
+
+    #[instrument(skip(self))]
+    async fn update_mint_url(
+        &self,
+        old_mint_url: MintUrl,
+        new_mint_url: MintUrl,
+    ) -> Result<(), Self::Err> {
+        let tables = ["mint_quote", "proof"];
+
+        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.db)
+            .await?;
+        }
+
+        Ok(())
+    }
+
+    #[instrument(skip(self, keysets))]
+    async fn add_mint_keysets(
+        &self,
+        mint_url: MintUrl,
+        keysets: Vec<KeySetInfo>,
+    ) -> Result<(), Self::Err> {
+        for keyset in keysets {
+            query(
+                r#"
+    INSERT INTO keyset
+    (mint_url, id, unit, active, input_fee_ppk, final_expiry)
+    VALUES
+    (:mint_url, :id, :unit, :active, :input_fee_ppk, :final_expiry)
+    ON CONFLICT(id) DO UPDATE SET
+        mint_url = excluded.mint_url,
+        unit = excluded.unit,
+        active = excluded.active,
+        input_fee_ppk = excluded.input_fee_ppk,
+        final_expiry = excluded.final_expiry;
+    "#,
+            )?
+            .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))
+            .execute(&self.db)
+            .await?;
+        }
+
+        Ok(())
+    }
+
+    #[instrument(skip(self))]
+    async fn get_mint_keysets(
+        &self,
+        mint_url: MintUrl,
+    ) -> Result<Option<Vec<KeySetInfo>>, Self::Err> {
+        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(&self.db)
+        .await?
+        .into_iter()
+        .map(sql_row_to_keyset)
+        .collect::<Result<Vec<_>, Error>>()?;
+
+        match keysets.is_empty() {
+            false => Ok(Some(keysets)),
+            true => Ok(None),
+        }
+    }
+
+    #[instrument(skip(self), fields(keyset_id = %keyset_id))]
+    async fn get_keyset_by_id(&self, keyset_id: &Id) -> Result<Option<KeySetInfo>, Self::Err> {
+        Ok(query(
+            r#"
+            SELECT
+                id,
+                unit,
+                active,
+                input_fee_ppk,
+                final_expiry
+            FROM
+                keyset
+            WHERE id = :id
+            "#,
+        )?
+        .bind("id", keyset_id.to_string())
+        .fetch_one(&self.db)
+        .await?
+        .map(sql_row_to_keyset)
+        .transpose()?)
+    }
+
+    #[instrument(skip_all)]
+    async fn add_mint_quote(&self, quote: MintQuote) -> Result<(), Self::Err> {
+        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.db).await?;
+
+        Ok(())
+    }
+
+    #[instrument(skip(self))]
+    async fn get_mint_quote(&self, quote_id: &str) -> Result<Option<MintQuote>, Self::Err> {
+        Ok(query(
+            r#"
+            SELECT
+                id,
+                mint_url,
+                amount,
+                unit,
+                request,
+                state,
+                expiry,
+                secret_key,
+                payment_method,
+                amount_issued,
+                amount_paid
+            FROM
+                mint_quote
+            WHERE
+                id = :id
+            "#,
+        )?
+        .bind("id", quote_id.to_string())
+        .fetch_one(&self.db)
+        .await?
+        .map(sql_row_to_mint_quote)
+        .transpose()?)
+    }
+
+    #[instrument(skip(self))]
+    async fn get_mint_quotes(&self) -> Result<Vec<MintQuote>, Self::Err> {
+        Ok(query(
+            r#"
+            SELECT
+                id,
+                mint_url,
+                amount,
+                unit,
+                request,
+                state,
+                expiry,
+                secret_key
+            FROM
+                mint_quote
+            "#,
+        )?
+        .fetch_all(&self.db)
+        .await?
+        .into_iter()
+        .map(sql_row_to_mint_quote)
+        .collect::<Result<_, _>>()?)
+    }
+
+    #[instrument(skip(self))]
+    async fn remove_mint_quote(&self, quote_id: &str) -> Result<(), Self::Err> {
+        query(r#"DELETE FROM mint_quote WHERE id=:id"#)?
+            .bind("id", quote_id.to_string())
+            .execute(&self.db)
+            .await?;
+
+        Ok(())
+    }
+
+    #[instrument(skip_all)]
+    async fn add_melt_quote(&self, quote: wallet::MeltQuote) -> Result<(), Self::Err> {
+        query(
+            r#"
+INSERT INTO melt_quote
+(id, unit, amount, request, fee_reserve, state, expiry)
+VALUES
+(:id, :unit, :amount, :request, :fee_reserve, :state, :expiry)
+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
+;
+        "#,
+        )?
+        .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)
+        .execute(&self.db)
+        .await?;
+
+        Ok(())
+    }
+
+    #[instrument(skip(self))]
+    async fn get_melt_quote(&self, quote_id: &str) -> Result<Option<wallet::MeltQuote>, Self::Err> {
+        Ok(query(
+            r#"
+            SELECT
+                id,
+                unit,
+                amount,
+                request,
+                fee_reserve,
+                state,
+                expiry,
+                payment_preimage
+            FROM
+                melt_quote
+            WHERE
+                id=:id
+            "#,
+        )?
+        .bind("id", quote_id.to_owned())
+        .fetch_one(&self.db)
+        .await?
+        .map(sql_row_to_melt_quote)
+        .transpose()?)
+    }
+
+    #[instrument(skip(self))]
+    async fn remove_melt_quote(&self, quote_id: &str) -> Result<(), Self::Err> {
+        query(r#"DELETE FROM melt_quote WHERE id=:id"#)?
+            .bind("id", quote_id.to_owned())
+            .execute(&self.db)
+            .await?;
+
+        Ok(())
+    }
+
+    #[instrument(skip_all)]
+    async fn add_keys(&self, keyset: KeySet) -> Result<(), Self::Err> {
+        // Recompute ID for verification
+        keyset.verify_id()?;
+
+        query(
+            r#"
+            INSERT INTO key
+            (id, keys)
+            VALUES
+            (:id, :keys)
+            ON CONFLICT(id) DO UPDATE SET
+                keys = excluded.keys
+        "#,
+        )?
+        .bind("id", keyset.id.to_string())
+        .bind(
+            "keys",
+            serde_json::to_string(&keyset.keys).map_err(Error::from)?,
+        )
+        .execute(&self.db)
+        .await?;
+
+        Ok(())
+    }
+
+    #[instrument(skip(self), fields(keyset_id = %keyset_id))]
+    async fn get_keys(&self, keyset_id: &Id) -> Result<Option<Keys>, Self::Err> {
+        Ok(query(
+            r#"
+            SELECT
+                keys
+            FROM key
+            WHERE id = :id
+            "#,
+        )?
+        .bind("id", keyset_id.to_string())
+        .pluck(&self.db)
+        .await?
+        .map(|keys| {
+            let keys = column_as_string!(keys);
+            serde_json::from_str(&keys).map_err(Error::from)
+        })
+        .transpose()?)
+    }
+
+    #[instrument(skip(self))]
+    async fn remove_keys(&self, id: &Id) -> Result<(), Self::Err> {
+        query(r#"DELETE FROM key WHERE id = :id"#)?
+            .bind("id", id.to_string())
+            .pluck(&self.db)
+            .await?;
+
+        Ok(())
+    }
+
+    async fn update_proofs(
+        &self,
+        added: Vec<ProofInfo>,
+        removed_ys: Vec<PublicKey>,
+    ) -> Result<(), Self::Err> {
+        // TODO: Use a transaction for all these operations
+        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)
+    VALUES
+    (:y, :mint_url, :state, :spending_condition, :unit, :amount, :keyset_id, :secret, :c, :witness, :dleq_e, :dleq_s, :dleq_r)
+    ON CONFLICT(y) DO UPDATE SET
+        mint_url = excluded.mint_url,
+        state = excluded.state,
+        spending_condition = excluded.spending_condition,
+        unit = excluded.unit,
+        amount = excluded.amount,
+        keyset_id = excluded.keyset_id,
+        secret = excluded.secret,
+        c = excluded.c,
+        witness = excluded.witness,
+        dleq_e = excluded.dleq_e,
+        dleq_s = excluded.dleq_s,
+        dleq_r = excluded.dleq_r
+    ;
+            "#,
+            )?
+            .bind("y", proof.y.to_bytes().to_vec())
+            .bind("mint_url", proof.mint_url.to_string())
+            .bind("state",proof.state.to_string())
+            .bind(
+                "spending_condition",
+                proof
+                    .spending_condition
+                    .map(|s| serde_json::to_string(&s).ok()),
+            )
+            .bind("unit", proof.unit.to_string())
+            .bind("amount", u64::from(proof.proof.amount) as i64)
+            .bind("keyset_id", proof.proof.keyset_id.to_string())
+            .bind("secret", proof.proof.secret.to_string())
+            .bind("c", proof.proof.c.to_bytes().to_vec())
+            .bind(
+                "witness",
+                proof
+                    .proof
+                    .witness
+                    .map(|w| serde_json::to_string(&w).unwrap()),
+            )
+            .bind(
+                "dleq_e",
+                proof.proof.dleq.as_ref().map(|dleq| dleq.e.to_secret_bytes().to_vec()),
+            )
+            .bind(
+                "dleq_s",
+                proof.proof.dleq.as_ref().map(|dleq| dleq.s.to_secret_bytes().to_vec()),
+            )
+            .bind(
+                "dleq_r",
+                proof.proof.dleq.as_ref().map(|dleq| dleq.r.to_secret_bytes().to_vec()),
+            )
+            .execute(&self.db).await?;
+        }
+
+        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.db)
+            .await?;
+
+        Ok(())
+    }
+
+    #[instrument(skip(self, state, spending_conditions))]
+    async fn get_proofs(
+        &self,
+        mint_url: Option<MintUrl>,
+        unit: Option<CurrencyUnit>,
+        state: Option<Vec<State>>,
+        spending_conditions: Option<Vec<SpendingConditions>>,
+    ) -> Result<Vec<ProofInfo>, Self::Err> {
+        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
+        "#,
+        )?
+        .fetch_all(&self.db)
+        .await?
+        .into_iter()
+        .filter_map(|row| {
+            let row = sql_row_to_proof_info(row).ok()?;
+
+            if row.matches_conditions(&mint_url, &unit, &state, &spending_conditions) {
+                Some(row)
+            } else {
+                None
+            }
+        })
+        .collect::<Vec<_>>())
+    }
+
+    async fn update_proofs_state(&self, ys: Vec<PublicKey>, state: State) -> Result<(), Self::Err> {
+        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.db)
+            .await?;
+
+        Ok(())
+    }
+
+    #[instrument(skip(self), fields(keyset_id = %keyset_id))]
+    async fn increment_keyset_counter(&self, keyset_id: &Id, count: u32) -> Result<(), Self::Err> {
+        query(
+            r#"
+            UPDATE keyset
+            SET counter=counter+:count
+            WHERE id=:id
+            "#,
+        )?
+        .bind("count", count)
+        .bind("id", keyset_id.to_string())
+        .execute(&self.db)
+        .await?;
+
+        Ok(())
+    }
+
+    #[instrument(skip(self), fields(keyset_id = %keyset_id))]
+    async fn get_keyset_counter(&self, keyset_id: &Id) -> Result<Option<u32>, Self::Err> {
+        Ok(query(
+            r#"
+            SELECT
+                counter
+            FROM
+                keyset
+            WHERE
+                id=:id
+            "#,
+        )?
+        .bind("id", keyset_id.to_string())
+        .pluck(&self.db)
+        .await?
+        .map(|n| Ok::<_, Error>(column_as_number!(n)))
+        .transpose()?)
+    }
+
+    #[instrument(skip(self))]
+    async fn add_transaction(&self, transaction: Transaction) -> Result<(), Self::Err> {
+        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::<Vec<_>>();
+
+        query(
+            r#"
+INSERT INTO transactions
+(id, mint_url, direction, unit, amount, fee, ys, timestamp, memo, metadata)
+VALUES
+(:id, :mint_url, :direction, :unit, :amount, :fee, :ys, :timestamp, :memo, :metadata)
+ON CONFLICT(id) DO UPDATE SET
+    mint_url = excluded.mint_url,
+    direction = excluded.direction,
+    unit = excluded.unit,
+    amount = excluded.amount,
+    fee = excluded.fee,
+    ys = excluded.ys,
+    timestamp = excluded.timestamp,
+    memo = excluded.memo,
+    metadata = excluded.metadata
+;
+        "#,
+        )?
+        .bind("id", transaction.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)?,
+        )
+        .execute(&self.db)
+        .await?;
+
+        Ok(())
+    }
+
+    #[instrument(skip(self))]
+    async fn get_transaction(
+        &self,
+        transaction_id: TransactionId,
+    ) -> Result<Option<Transaction>, Self::Err> {
+        Ok(query(
+            r#"
+            SELECT
+                mint_url,
+                direction,
+                unit,
+                amount,
+                fee,
+                ys,
+                timestamp,
+                memo,
+                metadata
+            FROM
+                transactions
+            WHERE
+                id = :id
+            "#,
+        )?
+        .bind("id", transaction_id.as_slice().to_vec())
+        .fetch_one(&self.db)
+        .await?
+        .map(sql_row_to_transaction)
+        .transpose()?)
+    }
+
+    #[instrument(skip(self))]
+    async fn list_transactions(
+        &self,
+        mint_url: Option<MintUrl>,
+        direction: Option<TransactionDirection>,
+        unit: Option<CurrencyUnit>,
+    ) -> Result<Vec<Transaction>, Self::Err> {
+        Ok(query(
+            r#"
+            SELECT
+                mint_url,
+                direction,
+                unit,
+                amount,
+                fee,
+                ys,
+                timestamp,
+                memo,
+                metadata
+            FROM
+                transactions
+            "#,
+        )?
+        .fetch_all(&self.db)
+        .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::<Vec<_>>())
+    }
+
+    #[instrument(skip(self))]
+    async fn remove_transaction(&self, transaction_id: TransactionId) -> Result<(), Self::Err> {
+        query(r#"DELETE FROM transactions WHERE id=:id"#)?
+            .bind("id", transaction_id.as_slice().to_vec())
+            .execute(&self.db)
+            .await?;
+
+        Ok(())
+    }
+}
+
+fn sql_row_to_mint_info(row: Vec<Column>) -> Result<MintInfo, Error> {
+    unpack_into!(
+        let (
+            name,
+            pubkey,
+            version,
+            description,
+            description_long,
+            contact,
+            nuts,
+            icon_url,
+            motd,
+            urls,
+            mint_time,
+            tos_url
+        ) = row
+    );
+
+    Ok(MintInfo {
+        name: column_as_nullable_string!(&name),
+        pubkey: column_as_nullable_string!(&pubkey, |v| serde_json::from_str(v).ok(), |v| {
+            serde_json::from_slice(v).ok()
+        }),
+        version: column_as_nullable_string!(&version).and_then(|v| serde_json::from_str(&v).ok()),
+        description: column_as_nullable_string!(description),
+        description_long: column_as_nullable_string!(description_long),
+        contact: column_as_nullable_string!(contact, |v| serde_json::from_str(&v).ok()),
+        nuts: column_as_nullable_string!(nuts, |v| serde_json::from_str(&v).ok())
+            .unwrap_or_default(),
+        urls: column_as_nullable_string!(urls, |v| serde_json::from_str(&v).ok()),
+        icon_url: column_as_nullable_string!(icon_url),
+        motd: column_as_nullable_string!(motd),
+        time: column_as_nullable_number!(mint_time).map(|t| t),
+        tos_url: column_as_nullable_string!(tos_url),
+    })
+}
+
+#[instrument(skip_all)]
+fn sql_row_to_keyset(row: Vec<Column>) -> Result<KeySetInfo, Error> {
+    unpack_into!(
+        let (
+            id,
+            unit,
+            active,
+            input_fee_ppk,
+            final_expiry
+        ) = row
+    );
+
+    Ok(KeySetInfo {
+        id: column_as_string!(id, Id::from_str, Id::from_bytes),
+        unit: column_as_string!(unit, CurrencyUnit::from_str),
+        active: matches!(active, Column::Integer(1)),
+        input_fee_ppk: column_as_nullable_number!(input_fee_ppk).unwrap_or_default(),
+        final_expiry: column_as_nullable_number!(final_expiry),
+    })
+}
+
+fn sql_row_to_mint_quote(row: Vec<Column>) -> Result<MintQuote, Error> {
+    unpack_into!(
+        let (
+            id,
+            mint_url,
+            amount,
+            unit,
+            request,
+            state,
+            expiry,
+            secret_key,
+            row_method,
+            row_amount_minted,
+            row_amount_paid
+        ) = row
+    );
+
+    let amount: Option<i64> = column_as_nullable_number!(amount);
+
+    let amount_paid: u64 = column_as_number!(row_amount_paid);
+    let amount_minted: u64 = column_as_number!(row_amount_minted);
+    let payment_method =
+        PaymentMethod::from_str(&column_as_string!(row_method)).map_err(Error::from)?;
+
+    Ok(MintQuote {
+        id: column_as_string!(id),
+        mint_url: column_as_string!(mint_url, MintUrl::from_str),
+        amount: amount.and_then(Amount::from_i64),
+        unit: column_as_string!(unit, CurrencyUnit::from_str),
+        request: column_as_string!(request),
+        state: column_as_string!(state, MintQuoteState::from_str),
+        expiry: column_as_number!(expiry),
+        secret_key: column_as_nullable_string!(secret_key)
+            .map(|v| SecretKey::from_str(&v))
+            .transpose()?,
+        payment_method,
+        amount_issued: amount_minted.into(),
+        amount_paid: amount_paid.into(),
+    })
+}
+
+fn sql_row_to_melt_quote(row: Vec<Column>) -> Result<wallet::MeltQuote, Error> {
+    unpack_into!(
+        let (
+            id,
+            unit,
+            amount,
+            request,
+            fee_reserve,
+            state,
+            expiry,
+            payment_preimage
+        ) = row
+    );
+
+    let amount: u64 = column_as_number!(amount);
+    let fee_reserve: u64 = column_as_number!(fee_reserve);
+
+    Ok(wallet::MeltQuote {
+        id: column_as_string!(id),
+        amount: Amount::from(amount),
+        unit: column_as_string!(unit, CurrencyUnit::from_str),
+        request: column_as_string!(request),
+        fee_reserve: Amount::from(fee_reserve),
+        state: column_as_string!(state, MeltQuoteState::from_str),
+        expiry: column_as_number!(expiry),
+        payment_preimage: column_as_nullable_string!(payment_preimage),
+    })
+}
+
+fn sql_row_to_proof_info(row: Vec<Column>) -> Result<ProofInfo, Error> {
+    unpack_into!(
+        let (
+            amount,
+            unit,
+            keyset_id,
+            secret,
+            c,
+            witness,
+            dleq_e,
+            dleq_s,
+            dleq_r,
+            y,
+            mint_url,
+            state,
+            spending_condition
+        ) = row
+    );
+
+    let dleq = match (
+        column_as_nullable_binary!(dleq_e),
+        column_as_nullable_binary!(dleq_s),
+        column_as_nullable_binary!(dleq_r),
+    ) {
+        (Some(e), Some(s), Some(r)) => {
+            let e_key = SecretKey::from_slice(&e)?;
+            let s_key = SecretKey::from_slice(&s)?;
+            let r_key = SecretKey::from_slice(&r)?;
+
+            Some(ProofDleq::new(e_key, s_key, r_key))
+        }
+        _ => None,
+    };
+
+    let amount: u64 = column_as_number!(amount);
+    let proof = Proof {
+        amount: Amount::from(amount),
+        keyset_id: column_as_string!(keyset_id, Id::from_str),
+        secret: column_as_string!(secret, Secret::from_str),
+        witness: column_as_nullable_string!(witness, |v| { serde_json::from_str(&v).ok() }, |v| {
+            serde_json::from_slice(&v).ok()
+        }),
+        c: column_as_string!(c, PublicKey::from_str, PublicKey::from_slice),
+        dleq,
+    };
+
+    Ok(ProofInfo {
+        proof,
+        y: column_as_string!(y, PublicKey::from_str, PublicKey::from_slice),
+        mint_url: column_as_string!(mint_url, MintUrl::from_str),
+        state: column_as_string!(state, State::from_str),
+        spending_condition: column_as_nullable_string!(
+            spending_condition,
+            |r| { serde_json::from_str(&r).ok() },
+            |r| { serde_json::from_slice(&r).ok() }
+        ),
+        unit: column_as_string!(unit, CurrencyUnit::from_str),
+    })
+}
+
+fn sql_row_to_transaction(row: Vec<Column>) -> Result<Transaction, Error> {
+    unpack_into!(
+        let (
+            mint_url,
+            direction,
+            unit,
+            amount,
+            fee,
+            ys,
+            timestamp,
+            memo,
+            metadata
+        ) = row
+    );
+
+    let amount: u64 = column_as_number!(amount);
+    let fee: u64 = column_as_number!(fee);
+
+    Ok(Transaction {
+        mint_url: column_as_string!(mint_url, MintUrl::from_str),
+        direction: column_as_string!(direction, TransactionDirection::from_str),
+        unit: column_as_string!(unit, CurrencyUnit::from_str),
+        amount: Amount::from(amount),
+        fee: Amount::from(fee),
+        ys: column_as_binary!(ys)
+            .chunks(33)
+            .map(PublicKey::from_slice)
+            .collect::<Result<Vec<_>, _>>()?,
+        timestamp: column_as_number!(timestamp),
+        memo: column_as_nullable_string!(memo),
+        metadata: column_as_nullable_string!(metadata, |v| serde_json::from_str(&v).ok(), |v| {
+            serde_json::from_slice(&v).ok()
+        })
+        .unwrap_or_default(),
+    })
+}

+ 97 - 0
crates/cdk-sql-common/tests/legacy-sqlx.sql

@@ -0,0 +1,97 @@
+PRAGMA foreign_keys=OFF;
+BEGIN TRANSACTION;
+CREATE TABLE _sqlx_migrations (
+    version BIGINT PRIMARY KEY,
+    description TEXT NOT NULL,
+    installed_on TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    success BOOLEAN NOT NULL,
+    checksum BYTEA NOT NULL,
+    execution_time BIGINT NOT NULL
+);
+INSERT INTO _sqlx_migrations VALUES(20240612124932,'init','2025-06-13 20:01:04',1,X'42664ceda25b07bca420c2f7480c90334cb8a720203c1b4b8971181d5d3afabda3171aa89c1c0c8a26421eded94b77fa',921834);
+INSERT INTO _sqlx_migrations VALUES(20240618195700,'quote state','2025-06-13 20:01:04',1,X'4b3a5a7f91032320f32b2c60a4348f0e80cef98fcf58153c4c942aa5124ddadce7c5c4338f29d2cb672fc4c08dd894a6',1019333);
+INSERT INTO _sqlx_migrations VALUES(20240626092101,'nut04 state','2025-06-13 20:01:04',1,X'3641316faa018b13892d2972010b26a68d48b499aa67f8c084587265d070b575f541f165a9e2c5653b9c81a8dc198843',814000);
+INSERT INTO _sqlx_migrations VALUES(20240703122347,'request lookup id','2025-06-13 20:01:04',1,X'234851aa0990048e119d07e9844f064ee71731c4e21021934e733359d6c50bc95a40051673f0a06e82d151c34fff6e8a',430875);
+INSERT INTO _sqlx_migrations VALUES(20240710145043,'input fee','2025-06-13 20:01:04',1,X'422d4ce6a1d94c2df4a7fd9400c3d45db35953e53ba46025df7d3ed4d373e04f948468dcbcd8155829a5441f8b46d7f3',302916);
+INSERT INTO _sqlx_migrations VALUES(20240711183109,'derivation path index','2025-06-13 20:01:04',1,X'83651c857135516fd578c5ee9f179a04964dc9a366a5b698c1cb54f2b5aa139dc912d34e28c5ff4cc157e6991032952f',225125);
+INSERT INTO _sqlx_migrations VALUES(20240718203721,'allow unspent','2025-06-13 20:01:04',1,X'9b900846657b9083cdeca3da6ca7d74487c400f715f7d455c6a662de6b60e2761c3d80ea67d820e9b1ec9fbfd596e267',776167);
+INSERT INTO _sqlx_migrations VALUES(20240811031111,'update mint url','2025-06-13 20:01:04',1,X'b8d771e08d3bbe3fc1e8beb1674714f0306d7f9f7cc09990fc0215850179a64366c8c46305ea0c1fb5dbc73a5fe48207',79334);
+INSERT INTO _sqlx_migrations VALUES(20240919103407,'proofs quote id','2025-06-13 20:01:04',1,X'e3df13daebbc7df1907c68963258ad3722a0f2398f5ee1e92ea1824ce1a22f5657411f9c08a1f72bfd250e40630fdca5',387875);
+INSERT INTO _sqlx_migrations VALUES(20240923153640,'melt requests','2025-06-13 20:01:04',1,X'8c35d740fbb1c0c13dc4594da50cce3e066cba2ff3926a5527629207678afe3a4fa3b7c8f5fab7e08525c676a4098154',188958);
+INSERT INTO _sqlx_migrations VALUES(20240930101140,'dleq for sigs','2025-06-13 20:01:04',1,X'23c61a60db9bb145c238bb305583ccc025cd17958e61a6ff97ef0e4385517fe87729f77de0c26ce9cfa3a0c70b273038',383542);
+INSERT INTO _sqlx_migrations VALUES(20241108093102,'mint mint quote pubkey','2025-06-13 20:01:04',1,X'00c83af91dc109368fcdc9a1360e1c893afcac3a649c7dfd04e841f1f8fe3d0e99a2ade6891ab752e1b942a738ac6b44',246875);
+INSERT INTO _sqlx_migrations VALUES(20250103201327,'amount to pay msats','2025-06-13 20:01:04',1,X'4cc8bd34aec65365271e2dc2a19735403c8551dbf738b541659399c900fb167577d3f02b1988679e6c7922fe018b9a32',235041);
+INSERT INTO _sqlx_migrations VALUES(20250129200912,'remove mint url','2025-06-13 20:01:04',1,X'f86b07a6b816683d72bdad637502a47cdeb21f6535aa8e2c0647d4b29f4f58931683b72062b3e313a5936264876bb2c3',638084);
+INSERT INTO _sqlx_migrations VALUES(20250129230326,'add config table','2025-06-13 20:01:04',1,X'c232f4cfa032105cdd48097197d7fb0eea290a593af0996434c3f1f5396efb41d1f225592b292367fd9d584672a347d8',163625);
+INSERT INTO _sqlx_migrations VALUES(20250307213652,'keyset id as foreign key','2025-06-13 20:01:04',1,X'50a36140780074b2730d429d664c2a7593f2c2237c1a36ed2a11e22c40bfa40b24dc3a5c8089959fae955fdbe2f06533',1498459);
+INSERT INTO _sqlx_migrations VALUES(20250406091754,'mint time of quotes','2025-06-13 20:01:04',1,X'ac0165a8371cf7ad424be08c0e6931e1dd1249354ea0e33b4a04ff48ab4188da105e1fd763c42f06aeb733eb33d85415',934250);
+INSERT INTO _sqlx_migrations VALUES(20250406093755,'mint created time signature','2025-06-13 20:01:04',1,X'7f2ff8e30f66ab142753cc2e0faec89560726d96298e9ce0c9e871974300fcbe7c2f8a9b2d48ed4ca8daf1b9a5043e95',447000);
+INSERT INTO _sqlx_migrations VALUES(20250415093121,'drop keystore foreign','2025-06-13 20:01:04',1,X'efa99131d37335d64c86680c9e5b1362c2bf4d03fbdb6f60c9160edc572add6422d871f76a245d6f55f7fb6f4491b825',1375084);
+CREATE TABLE keyset (
+    id TEXT PRIMARY KEY,
+    unit TEXT NOT NULL,
+    active BOOL NOT NULL,
+    valid_from INTEGER NOT NULL,
+    valid_to INTEGER,
+    derivation_path TEXT NOT NULL,
+    max_order INTEGER NOT NULL
+, input_fee_ppk INTEGER, derivation_path_index INTEGER);
+INSERT INTO keyset VALUES('0083a60439303340','sat',1,1749844864,NULL,'0''/0''/0''',32,0,0);
+INSERT INTO keyset VALUES('00b13456b2934304','auth',1,1749844864,NULL,'0''/4''/0''',1,0,0);
+INSERT INTO keyset VALUES('0002c733628bb92f','usd',1,1749844864,NULL,'0''/2''/0''',32,0,0);
+CREATE TABLE mint_quote (
+    id TEXT PRIMARY KEY,
+    amount INTEGER NOT NULL,
+    unit TEXT NOT NULL,
+    request TEXT NOT NULL,
+    expiry INTEGER NOT NULL
+, state TEXT CHECK ( state IN ('UNPAID', 'PENDING', 'PAID', 'ISSUED' ) ) NOT NULL DEFAULT 'UNPAID', request_lookup_id TEXT, pubkey TEXT, created_time INTEGER NOT NULL DEFAULT 0, paid_time INTEGER, issued_time INTEGER);
+CREATE TABLE melt_quote (
+    id TEXT PRIMARY KEY,
+    unit TEXT NOT NULL,
+    amount INTEGER NOT NULL,
+    request TEXT NOT NULL,
+    fee_reserve INTEGER NOT NULL,
+    expiry INTEGER NOT NULL
+, state TEXT CHECK ( state IN ('UNPAID', 'PENDING', 'PAID' ) ) NOT NULL DEFAULT 'UNPAID', payment_preimage TEXT, request_lookup_id TEXT, msat_to_pay INTEGER, created_time INTEGER NOT NULL DEFAULT 0, paid_time INTEGER);
+CREATE TABLE melt_request (
+id TEXT PRIMARY KEY,
+inputs TEXT NOT NULL,
+outputs TEXT,
+method TEXT NOT NULL,
+unit TEXT NOT NULL
+);
+CREATE TABLE config (
+    id TEXT PRIMARY KEY,
+    value TEXT NOT NULL
+);
+CREATE TABLE IF NOT EXISTS "proof" (
+    y BYTEA PRIMARY KEY,
+    amount INTEGER NOT NULL,
+    keyset_id TEXT NOT NULL, -- no FK constraint here
+    secret TEXT NOT NULL,
+    c BYTEA NOT NULL,
+    witness TEXT,
+    state TEXT CHECK (state IN ('SPENT', 'PENDING', 'UNSPENT', 'RESERVED', 'UNKNOWN')) NOT NULL,
+    quote_id TEXT,
+    created_time INTEGER NOT NULL DEFAULT 0
+);
+CREATE TABLE IF NOT EXISTS "blind_signature" (
+    y BYTEA PRIMARY KEY,
+    amount INTEGER NOT NULL,
+    keyset_id TEXT NOT NULL,  -- FK removed
+    c BYTEA NOT NULL,
+    dleq_e TEXT,
+    dleq_s TEXT,
+    quote_id TEXT,
+    created_time INTEGER NOT NULL DEFAULT 0
+);
+CREATE INDEX unit_index ON keyset(unit);
+CREATE INDEX active_index ON keyset(active);
+CREATE INDEX request_index ON mint_quote(request);
+CREATE INDEX expiry_index ON mint_quote(expiry);
+CREATE INDEX melt_quote_state_index ON melt_quote(state);
+CREATE INDEX mint_quote_state_index ON mint_quote(state);
+CREATE UNIQUE INDEX unique_request_lookup_id_mint ON mint_quote(request_lookup_id);
+CREATE UNIQUE INDEX unique_request_lookup_id_melt ON melt_quote(request_lookup_id);
+COMMIT;

+ 5 - 4
crates/cdk-sqlite/Cargo.toml

@@ -13,18 +13,19 @@ readme = "README.md"
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 [features]
 default = ["mint", "wallet", "auth"]
-mint = ["cdk-common/mint"]
-wallet = ["cdk-common/wallet"]
-auth = ["cdk-common/auth"]
+mint = ["cdk-common/mint", "cdk-sql-common/mint"]
+wallet = ["cdk-common/wallet", "cdk-sql-common/wallet"]
+auth = ["cdk-common/auth", "cdk-sql-common/auth"]
 sqlcipher = ["rusqlite/bundled-sqlcipher"]
 
 [dependencies]
 async-trait.workspace = true
 cdk-common = { workspace = true, features = ["test"] }
 bitcoin.workspace = true
+cdk-sql-common = { workspace = true }
 rusqlite = { version = "0.31", features = ["bundled"]}
 thiserror.workspace = true
-tokio.workspace = true
+tokio = { workspace = true, features = ["rt-multi-thread"]}
 tracing.workspace = true
 serde.workspace = true
 serde_json.workspace = true

+ 28 - 57
crates/cdk-sqlite/src/common.rs

@@ -1,12 +1,13 @@
+use std::sync::atomic::AtomicBool;
 use std::sync::Arc;
 use std::time::Duration;
 
-use rusqlite::{params, Connection};
-
-use crate::pool::{Pool, ResourceManager};
+use cdk_sql_common::pool::{self, Pool, ResourceManager};
+use cdk_sql_common::value::Value;
+use rusqlite::Connection;
 
 /// The config need to create a new SQLite connection
-#[derive(Debug)]
+#[derive(Clone, Debug)]
 pub struct Config {
     path: Option<String>,
     password: Option<String>,
@@ -25,7 +26,9 @@ impl ResourceManager for SqliteConnectionManager {
 
     fn new_resource(
         config: &Self::Config,
-    ) -> Result<Self::Resource, crate::pool::Error<Self::Error>> {
+        _still_valid: Arc<AtomicBool>,
+        _timeout: Duration,
+    ) -> Result<Self::Resource, pool::Error<Self::Error>> {
         let conn = if let Some(path) = config.path.as_ref() {
             Connection::open(path)?
         } else {
@@ -57,14 +60,8 @@ impl ResourceManager for SqliteConnectionManager {
 /// For SQLCipher support, enable the "sqlcipher" feature and pass a password.
 pub fn create_sqlite_pool(
     path: &str,
-    #[cfg(feature = "sqlcipher")] password: String,
+    password: Option<String>,
 ) -> Arc<Pool<SqliteConnectionManager>> {
-    #[cfg(feature = "sqlcipher")]
-    let password = Some(password);
-
-    #[cfg(not(feature = "sqlcipher"))]
-    let password = None;
-
     let (config, max_size) = if path.contains(":memory:") {
         (
             Config {
@@ -86,52 +83,26 @@ pub fn create_sqlite_pool(
     Pool::new(config, max_size, Duration::from_secs(10))
 }
 
-/// Migrates the migration generated by `build.rs`
-pub fn migrate(conn: &mut Connection, migrations: &[(&str, &str)]) -> Result<(), rusqlite::Error> {
-    let tx = conn.transaction()?;
-    tx.execute(
-        r#"
-           CREATE TABLE IF NOT EXISTS migrations (
-               name TEXT PRIMARY KEY,
-               applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
-           )
-           "#,
-        [],
-    )?;
-
-    if tx.query_row(
-        r#"select count(*) from sqlite_master where name = '_sqlx_migrations'"#,
-        [],
-        |row| row.get::<_, i32>(0),
-    )? == 1
-    {
-        tx.execute_batch(
-            r#"
-        INSERT INTO migrations
-        SELECT
-            version || '_' ||  REPLACE(description, ' ', '_') || '.sql',
-            execution_time
-        FROM _sqlx_migrations;
-        DROP TABLE _sqlx_migrations;
-        "#,
-        )?;
+/// Convert cdk_sql_common::value::Value to rusqlite Value
+#[inline(always)]
+pub fn to_sqlite(v: Value) -> rusqlite::types::Value {
+    match v {
+        Value::Blob(blob) => rusqlite::types::Value::Blob(blob),
+        Value::Integer(i) => rusqlite::types::Value::Integer(i),
+        Value::Null => rusqlite::types::Value::Null,
+        Value::Text(t) => rusqlite::types::Value::Text(t),
+        Value::Real(r) => rusqlite::types::Value::Real(r),
     }
+}
 
-    // Apply each migration if it hasn’t been applied yet
-    for (name, sql) in migrations {
-        let already_applied: bool = tx.query_row(
-            "SELECT EXISTS(SELECT 1 FROM migrations WHERE name = ?1)",
-            params![name],
-            |row| row.get(0),
-        )?;
-
-        if !already_applied {
-            tx.execute_batch(sql)?;
-            tx.execute("INSERT INTO migrations (name) VALUES (?1)", params![name])?;
-        }
+/// Convert from rusqlite Valute to cdk_sql_common::value::Value
+#[inline(always)]
+pub fn from_sqlite(v: rusqlite::types::Value) -> Value {
+    match v {
+        rusqlite::types::Value::Blob(blob) => Value::Blob(blob),
+        rusqlite::types::Value::Integer(i) => Value::Integer(i),
+        rusqlite::types::Value::Null => Value::Null,
+        rusqlite::types::Value::Text(t) => Value::Text(t),
+        rusqlite::types::Value::Real(r) => Value::Real(r),
     }
-
-    tx.commit()?;
-
-    Ok(())
 }

+ 0 - 3
crates/cdk-sqlite/src/lib.rs

@@ -4,9 +4,6 @@
 #![warn(rustdoc::bare_urls)]
 
 mod common;
-mod macros;
-mod pool;
-mod stmt;
 
 #[cfg(feature = "mint")]
 pub mod mint;

+ 327 - 173
crates/cdk-sqlite/src/mint/async_rusqlite.rs

@@ -1,16 +1,20 @@
+//! Async, pipelined rusqlite client
 use std::marker::PhantomData;
+use std::path::PathBuf;
 use std::sync::atomic::{AtomicUsize, Ordering};
 use std::sync::{mpsc as std_mpsc, Arc, Mutex};
 use std::thread::spawn;
 use std::time::Instant;
 
+use cdk_common::database::Error;
+use cdk_sql_common::database::{DatabaseConnector, DatabaseExecutor, DatabaseTransaction};
+use cdk_sql_common::pool::{self, Pool, PooledResource};
+use cdk_sql_common::stmt::{Column, ExpectedSqlResponse, Statement as InnerStatement};
+use cdk_sql_common::ConversionError;
 use rusqlite::{ffi, Connection, ErrorCode, TransactionBehavior};
 use tokio::sync::{mpsc, oneshot};
 
-use crate::common::SqliteConnectionManager;
-use crate::mint::Error;
-use crate::pool::{Pool, PooledResource};
-use crate::stmt::{Column, ExpectedSqlResponse, Statement as InnerStatement, Value};
+use crate::common::{create_sqlite_pool, from_sqlite, to_sqlite, SqliteConnectionManager};
 
 /// The number of queued SQL statements before it start failing
 const SQL_QUEUE_SIZE: usize = 10_000;
@@ -25,9 +29,57 @@ pub struct AsyncRusqlite {
     inflight_requests: Arc<AtomicUsize>,
 }
 
+impl From<PathBuf> for AsyncRusqlite {
+    fn from(value: PathBuf) -> Self {
+        AsyncRusqlite::new(create_sqlite_pool(value.to_str().unwrap_or_default(), None))
+    }
+}
+
+impl From<&str> for AsyncRusqlite {
+    fn from(value: &str) -> Self {
+        AsyncRusqlite::new(create_sqlite_pool(value, None))
+    }
+}
+
+impl From<(&str, &str)> for AsyncRusqlite {
+    fn from((value, pass): (&str, &str)) -> Self {
+        AsyncRusqlite::new(create_sqlite_pool(value, Some(pass.to_owned())))
+    }
+}
+
+impl From<(PathBuf, &str)> for AsyncRusqlite {
+    fn from((value, pass): (PathBuf, &str)) -> Self {
+        AsyncRusqlite::new(create_sqlite_pool(
+            value.to_str().unwrap_or_default(),
+            Some(pass.to_owned()),
+        ))
+    }
+}
+
+impl From<(&str, String)> for AsyncRusqlite {
+    fn from((value, pass): (&str, String)) -> Self {
+        AsyncRusqlite::new(create_sqlite_pool(value, Some(pass)))
+    }
+}
+
+impl From<(PathBuf, String)> for AsyncRusqlite {
+    fn from((value, pass): (PathBuf, String)) -> Self {
+        AsyncRusqlite::new(create_sqlite_pool(
+            value.to_str().unwrap_or_default(),
+            Some(pass),
+        ))
+    }
+}
+
+impl From<&PathBuf> for AsyncRusqlite {
+    fn from(value: &PathBuf) -> Self {
+        AsyncRusqlite::new(create_sqlite_pool(value.to_str().unwrap_or_default(), None))
+    }
+}
+
 /// Internal request for the database thread
 #[derive(Debug)]
-pub enum DbRequest {
+enum DbRequest {
     Sql(InnerStatement, oneshot::Sender<DbResponse>),
     Begin(oneshot::Sender<DbResponse>),
     Commit(oneshot::Sender<DbResponse>),
@@ -35,97 +87,67 @@ pub enum DbRequest {
 }
 
 #[derive(Debug)]
-pub enum DbResponse {
+enum DbResponse {
     Transaction(mpsc::Sender<DbRequest>),
     AffectedRows(usize),
     Pluck(Option<Column>),
     Row(Option<Vec<Column>>),
     Rows(Vec<Vec<Column>>),
-    Error(Error),
+    Error(SqliteError),
     Unexpected,
     Ok,
 }
 
-/// Statement for the async_rusqlite wrapper
-pub struct Statement(InnerStatement);
-
-impl Statement {
-    /// Bind a variable
-    pub fn bind<C, V>(self, name: C, value: V) -> Self
-    where
-        C: ToString,
-        V: Into<Value>,
-    {
-        Self(self.0.bind(name, value))
-    }
+#[derive(thiserror::Error, Debug)]
+enum SqliteError {
+    #[error(transparent)]
+    Sqlite(#[from] rusqlite::Error),
 
-    /// Bind vec
-    pub fn bind_vec<C, V>(self, name: C, value: Vec<V>) -> Self
-    where
-        C: ToString,
-        V: Into<Value>,
-    {
-        Self(self.0.bind_vec(name, value))
-    }
+    #[error(transparent)]
+    Inner(#[from] Error),
 
-    /// Executes a query and return the number of affected rows
-    pub async fn execute<C>(self, conn: &C) -> Result<usize, Error>
-    where
-        C: DatabaseExecutor + Send + Sync,
-    {
-        conn.execute(self.0).await
-    }
+    #[error(transparent)]
+    Pool(#[from] pool::Error<rusqlite::Error>),
 
-    /// Returns the first column of the first row of the query result
-    pub async fn pluck<C>(self, conn: &C) -> Result<Option<Column>, Error>
-    where
-        C: DatabaseExecutor + Send + Sync,
-    {
-        conn.pluck(self.0).await
-    }
+    /// Duplicate entry
+    #[error("Duplicate")]
+    Duplicate,
 
-    /// Returns the first row of the query result
-    pub async fn fetch_one<C>(self, conn: &C) -> Result<Option<Vec<Column>>, Error>
-    where
-        C: DatabaseExecutor + Send + Sync,
-    {
-        conn.fetch_one(self.0).await
-    }
+    #[error(transparent)]
+    Conversion(#[from] ConversionError),
+}
 
-    /// Returns all rows of the query result
-    pub async fn fetch_all<C>(self, conn: &C) -> Result<Vec<Vec<Column>>, Error>
-    where
-        C: DatabaseExecutor + Send + Sync,
-    {
-        conn.fetch_all(self.0).await
+impl From<SqliteError> for Error {
+    fn from(val: SqliteError) -> Self {
+        match val {
+            SqliteError::Duplicate => Error::Duplicate,
+            SqliteError::Conversion(e) => e.into(),
+            o => Error::Internal(o.to_string()),
+        }
     }
 }
 
 /// Process a query
 #[inline(always)]
-fn process_query(conn: &Connection, sql: InnerStatement) -> Result<DbResponse, Error> {
+fn process_query(conn: &Connection, statement: InnerStatement) -> Result<DbResponse, SqliteError> {
     let start = Instant::now();
-    let mut args = sql.args;
-    let mut stmt = conn.prepare_cached(&sql.sql)?;
-    let total_parameters = stmt.parameter_count();
-
-    for index in 1..=total_parameters {
-        let value = if let Some(value) = stmt.parameter_name(index).map(|name| {
-            args.remove(name)
-                .ok_or(Error::MissingParameter(name.to_owned()))
-        }) {
-            value?
-        } else {
-            continue;
-        };
+    let expected_response = statement.expected_response;
+    let (sql, placeholder_values) = statement.to_sql()?;
+    let sql = sql.trim_end_matches("FOR UPDATE");
 
-        stmt.raw_bind_parameter(index, value)?;
+    let mut stmt = conn.prepare_cached(sql)?;
+    for (i, value) in placeholder_values.into_iter().enumerate() {
+        stmt.raw_bind_parameter(i + 1, to_sqlite(value))?;
     }
 
     let columns = stmt.column_count();
 
-    let to_return = match sql.expected_response {
+    let to_return = match expected_response {
         ExpectedSqlResponse::AffectedRows => DbResponse::AffectedRows(stmt.raw_execute()?),
+        ExpectedSqlResponse::Batch => {
+            conn.execute_batch(sql)?;
+            DbResponse::Ok
+        }
         ExpectedSqlResponse::ManyRows => {
             let mut rows = stmt.raw_query();
             let mut results = vec![];
@@ -133,7 +155,7 @@ fn process_query(conn: &Connection, sql: InnerStatement) -> Result<DbResponse, E
             while let Some(row) = rows.next()? {
                 results.push(
                     (0..columns)
-                        .map(|i| row.get(i))
+                        .map(|i| row.get(i).map(from_sqlite))
                         .collect::<Result<Vec<_>, _>>()?,
                 )
             }
@@ -142,7 +164,11 @@ fn process_query(conn: &Connection, sql: InnerStatement) -> Result<DbResponse, E
         }
         ExpectedSqlResponse::Pluck => {
             let mut rows = stmt.raw_query();
-            DbResponse::Pluck(rows.next()?.map(|row| row.get(0usize)).transpose()?)
+            DbResponse::Pluck(
+                rows.next()?
+                    .map(|row| row.get(0usize).map(from_sqlite))
+                    .transpose()?,
+            )
         }
         ExpectedSqlResponse::SingleRow => {
             let mut rows = stmt.raw_query();
@@ -150,7 +176,7 @@ fn process_query(conn: &Connection, sql: InnerStatement) -> Result<DbResponse, E
                 .next()?
                 .map(|row| {
                     (0..columns)
-                        .map(|i| row.get(i))
+                        .map(|i| row.get(i).map(from_sqlite))
                         .collect::<Result<Vec<_>, _>>()
                 })
                 .transpose()?;
@@ -161,7 +187,7 @@ fn process_query(conn: &Connection, sql: InnerStatement) -> Result<DbResponse, E
     let duration = start.elapsed();
 
     if duration.as_millis() > SLOW_QUERY_THRESHOLD_MS {
-        tracing::warn!("[SLOW QUERY] Took {} ms: {}", duration.as_millis(), sql.sql);
+        tracing::warn!("[SLOW QUERY] Took {} ms: {}", duration.as_millis(), sql);
     }
 
     Ok(to_return)
@@ -196,13 +222,12 @@ fn rusqlite_spawn_worker_threads(
         let inflight_requests = inflight_requests.clone();
         spawn(move || loop {
             while let Ok((conn, sql, reply_to)) = rx.lock().expect("failed to acquire").recv() {
-                tracing::trace!("Execute query: {}", sql.sql);
                 let result = process_query(&conn, sql);
                 let _ = match result {
                     Ok(ok) => reply_to.send(ok),
                     Err(err) => {
                         tracing::error!("Failed query with error {:?}", err);
-                        let err = if let Error::Sqlite(rusqlite::Error::SqliteFailure(
+                        let err = if let SqliteError::Sqlite(rusqlite::Error::SqliteFailure(
                             ffi::Error {
                                 code,
                                 extended_code,
@@ -214,7 +239,7 @@ fn rusqlite_spawn_worker_threads(
                                 && (*extended_code == ffi::SQLITE_CONSTRAINT_PRIMARYKEY
                                     || *extended_code == ffi::SQLITE_CONSTRAINT_UNIQUE)
                             {
-                                Error::Duplicate
+                                SqliteError::Duplicate
                             } else {
                                 err
                             }
@@ -256,7 +281,7 @@ fn rusqlite_worker_manager(
     while let Some(request) = receiver.blocking_recv() {
         inflight_requests.fetch_add(1, Ordering::Relaxed);
         match request {
-            DbRequest::Sql(sql, reply_to) => {
+            DbRequest::Sql(statement, reply_to) => {
                 let conn = match pool.get() {
                     Ok(conn) => conn,
                     Err(err) => {
@@ -267,7 +292,7 @@ fn rusqlite_worker_manager(
                     }
                 };
 
-                let _ = send_sql_to_thread.send((conn, sql, reply_to));
+                let _ = send_sql_to_thread.send((conn, statement, reply_to));
                 continue;
             }
             DbRequest::Begin(reply_to) => {
@@ -341,9 +366,9 @@ fn rusqlite_worker_manager(
                         DbRequest::Begin(reply_to) => {
                             let _ = reply_to.send(DbResponse::Unexpected);
                         }
-                        DbRequest::Sql(sql, reply_to) => {
-                            tracing::trace!("Tx {}: SQL {}", tx_id, sql.sql);
-                            let _ = match process_query(&tx, sql) {
+                        DbRequest::Sql(statement, reply_to) => {
+                            tracing::trace!("Tx {}: SQL {:?}", tx_id, statement);
+                            let _ = match process_query(&tx, statement) {
                                 Ok(ok) => reply_to.send(ok),
                                 Err(err) => {
                                     tracing::error!(
@@ -351,7 +376,7 @@ fn rusqlite_worker_manager(
                                         tx_id,
                                         err
                                     );
-                                    let err = if let Error::Sqlite(
+                                    let err = if let SqliteError::Sqlite(
                                         rusqlite::Error::SqliteFailure(
                                             ffi::Error {
                                                 code,
@@ -365,7 +390,7 @@ fn rusqlite_worker_manager(
                                             && (*extended_code == ffi::SQLITE_CONSTRAINT_PRIMARYKEY
                                                 || *extended_code == ffi::SQLITE_CONSTRAINT_UNIQUE)
                                         {
-                                            Error::Duplicate
+                                            SqliteError::Duplicate
                                         } else {
                                             err
                                         }
@@ -395,138 +420,171 @@ fn rusqlite_worker_manager(
     }
 }
 
+impl AsyncRusqlite {
+    /// Creates a new Async Rusqlite wrapper.
+    pub fn new(pool: Arc<Pool<SqliteConnectionManager>>) -> Self {
+        let (sender, receiver) = mpsc::channel(SQL_QUEUE_SIZE);
+        let inflight_requests = Arc::new(AtomicUsize::new(0));
+        let inflight_requests_for_thread = inflight_requests.clone();
+        spawn(move || {
+            rusqlite_worker_manager(receiver, pool, inflight_requests_for_thread);
+        });
+
+        Self {
+            sender,
+            inflight_requests,
+        }
+    }
+
+    fn get_queue_sender(&self) -> &mpsc::Sender<DbRequest> {
+        &self.sender
+    }
+
+    /// Show how many inflight requests
+    #[allow(dead_code)]
+    pub fn inflight_requests(&self) -> usize {
+        self.inflight_requests.load(Ordering::Relaxed)
+    }
+}
+
 #[async_trait::async_trait]
-pub trait DatabaseExecutor {
-    /// Returns the connection to the database thread (or the on-going transaction)
-    fn get_queue_sender(&self) -> mpsc::Sender<DbRequest>;
+impl DatabaseConnector for AsyncRusqlite {
+    type Transaction<'a> = Transaction<'a>;
 
-    /// Executes a query and returns the affected rows
-    async fn execute(&self, mut statement: InnerStatement) -> Result<usize, Error> {
+    /// Begins a transaction
+    ///
+    /// If the transaction is Drop it will trigger a rollback operation
+    async fn begin(&self) -> Result<Self::Transaction<'_>, Error> {
         let (sender, receiver) = oneshot::channel();
-        statement.expected_response = ExpectedSqlResponse::AffectedRows;
-        self.get_queue_sender()
-            .send(DbRequest::Sql(statement, sender))
+        self.sender
+            .send(DbRequest::Begin(sender))
             .await
-            .map_err(|_| Error::Communication)?;
+            .map_err(|_| Error::Internal("Communication".to_owned()))?;
 
-        match receiver.await.map_err(|_| Error::Communication)? {
-            DbResponse::AffectedRows(n) => Ok(n),
-            DbResponse::Error(err) => Err(err),
+        match receiver
+            .await
+            .map_err(|_| Error::Internal("Communication".to_owned()))?
+        {
+            DbResponse::Transaction(db_sender) => Ok(Transaction {
+                db_sender,
+                _marker: PhantomData,
+            }),
+            DbResponse::Error(err) => Err(err.into()),
             _ => Err(Error::InvalidDbResponse),
         }
     }
+}
+
+#[async_trait::async_trait]
+impl DatabaseExecutor for AsyncRusqlite {
+    fn name() -> &'static str {
+        "sqlite"
+    }
 
-    /// Runs the query and returns the first row or None
     async fn fetch_one(&self, mut statement: InnerStatement) -> Result<Option<Vec<Column>>, Error> {
         let (sender, receiver) = oneshot::channel();
         statement.expected_response = ExpectedSqlResponse::SingleRow;
         self.get_queue_sender()
             .send(DbRequest::Sql(statement, sender))
             .await
-            .map_err(|_| Error::Communication)?;
+            .map_err(|_| Error::Internal("Communication".to_owned()))?;
 
-        match receiver.await.map_err(|_| Error::Communication)? {
+        match receiver
+            .await
+            .map_err(|_| Error::Internal("Communication".to_owned()))?
+        {
             DbResponse::Row(row) => Ok(row),
-            DbResponse::Error(err) => Err(err),
+            DbResponse::Error(err) => Err(err.into()),
             _ => Err(Error::InvalidDbResponse),
         }
     }
 
-    /// Runs the query and returns the first row or None
-    async fn fetch_all(&self, mut statement: InnerStatement) -> Result<Vec<Vec<Column>>, Error> {
+    async fn batch(&self, mut statement: InnerStatement) -> Result<(), Error> {
         let (sender, receiver) = oneshot::channel();
-        statement.expected_response = ExpectedSqlResponse::ManyRows;
+        statement.expected_response = ExpectedSqlResponse::Batch;
         self.get_queue_sender()
             .send(DbRequest::Sql(statement, sender))
             .await
-            .map_err(|_| Error::Communication)?;
+            .map_err(|_| Error::Internal("Communication".to_owned()))?;
 
-        match receiver.await.map_err(|_| Error::Communication)? {
-            DbResponse::Rows(rows) => Ok(rows),
-            DbResponse::Error(err) => Err(err),
+        match receiver
+            .await
+            .map_err(|_| Error::Internal("Communication".to_owned()))?
+        {
+            DbResponse::Ok => Ok(()),
+            DbResponse::Error(err) => Err(err.into()),
             _ => Err(Error::InvalidDbResponse),
         }
     }
 
-    async fn pluck(&self, mut statement: InnerStatement) -> Result<Option<Column>, Error> {
+    async fn fetch_all(&self, mut statement: InnerStatement) -> Result<Vec<Vec<Column>>, Error> {
         let (sender, receiver) = oneshot::channel();
-        statement.expected_response = ExpectedSqlResponse::Pluck;
+        statement.expected_response = ExpectedSqlResponse::ManyRows;
         self.get_queue_sender()
             .send(DbRequest::Sql(statement, sender))
             .await
-            .map_err(|_| Error::Communication)?;
+            .map_err(|_| Error::Internal("Communication".to_owned()))?;
 
-        match receiver.await.map_err(|_| Error::Communication)? {
-            DbResponse::Pluck(value) => Ok(value),
-            DbResponse::Error(err) => Err(err),
+        match receiver
+            .await
+            .map_err(|_| Error::Internal("Communication".to_owned()))?
+        {
+            DbResponse::Rows(row) => Ok(row),
+            DbResponse::Error(err) => Err(err.into()),
             _ => Err(Error::InvalidDbResponse),
         }
     }
-}
-
-#[inline(always)]
-pub fn query<T>(sql: T) -> Statement
-where
-    T: ToString,
-{
-    Statement(crate::stmt::Statement::new(sql))
-}
 
-impl AsyncRusqlite {
-    /// Creates a new Async Rusqlite wrapper.
-    pub fn new(pool: Arc<Pool<SqliteConnectionManager>>) -> Self {
-        let (sender, receiver) = mpsc::channel(SQL_QUEUE_SIZE);
-        let inflight_requests = Arc::new(AtomicUsize::new(0));
-        let inflight_requests_for_thread = inflight_requests.clone();
-        spawn(move || {
-            rusqlite_worker_manager(receiver, pool, inflight_requests_for_thread);
-        });
+    async fn execute(&self, mut statement: InnerStatement) -> Result<usize, Error> {
+        let (sender, receiver) = oneshot::channel();
+        statement.expected_response = ExpectedSqlResponse::AffectedRows;
+        self.get_queue_sender()
+            .send(DbRequest::Sql(statement, sender))
+            .await
+            .map_err(|_| Error::Internal("Communication".to_owned()))?;
 
-        Self {
-            sender,
-            inflight_requests,
+        match receiver
+            .await
+            .map_err(|_| Error::Internal("Communication".to_owned()))?
+        {
+            DbResponse::AffectedRows(total) => Ok(total),
+            DbResponse::Error(err) => Err(err.into()),
+            _ => Err(Error::InvalidDbResponse),
         }
     }
 
-    /// Show how many inflight requests
-    #[allow(dead_code)]
-    pub fn inflight_requests(&self) -> usize {
-        self.inflight_requests.load(Ordering::Relaxed)
-    }
-
-    /// Begins a transaction
-    ///
-    /// If the transaction is Drop it will trigger a rollback operation
-    pub async fn begin(&self) -> Result<Transaction<'_>, Error> {
+    async fn pluck(&self, mut statement: InnerStatement) -> Result<Option<Column>, Error> {
         let (sender, receiver) = oneshot::channel();
-        self.sender
-            .send(DbRequest::Begin(sender))
+        statement.expected_response = ExpectedSqlResponse::Pluck;
+        self.get_queue_sender()
+            .send(DbRequest::Sql(statement, sender))
             .await
-            .map_err(|_| Error::Communication)?;
+            .map_err(|_| Error::Internal("Communication".to_owned()))?;
 
-        match receiver.await.map_err(|_| Error::Communication)? {
-            DbResponse::Transaction(db_sender) => Ok(Transaction {
-                db_sender,
-                _marker: PhantomData,
-            }),
-            DbResponse::Error(err) => Err(err),
+        match receiver
+            .await
+            .map_err(|_| Error::Internal("Communication".to_owned()))?
+        {
+            DbResponse::Pluck(value) => Ok(value),
+            DbResponse::Error(err) => Err(err.into()),
             _ => Err(Error::InvalidDbResponse),
         }
     }
 }
 
-impl DatabaseExecutor for AsyncRusqlite {
-    #[inline(always)]
-    fn get_queue_sender(&self) -> mpsc::Sender<DbRequest> {
-        self.sender.clone()
-    }
-}
-
+/// Database transaction
+#[derive(Debug)]
 pub struct Transaction<'conn> {
     db_sender: mpsc::Sender<DbRequest>,
     _marker: PhantomData<&'conn ()>,
 }
 
+impl Transaction<'_> {
+    fn get_queue_sender(&self) -> &mpsc::Sender<DbRequest> {
+        &self.db_sender
+    }
+}
+
 impl Drop for Transaction<'_> {
     fn drop(&mut self) {
         let (sender, _) = oneshot::channel();
@@ -534,40 +592,136 @@ impl Drop for Transaction<'_> {
     }
 }
 
-impl Transaction<'_> {
-    pub async fn commit(self) -> Result<(), Error> {
+#[async_trait::async_trait]
+impl<'a> DatabaseTransaction<'a> for Transaction<'a> {
+    async fn commit(self) -> Result<(), Error> {
         let (sender, receiver) = oneshot::channel();
         self.db_sender
             .send(DbRequest::Commit(sender))
             .await
-            .map_err(|_| Error::Communication)?;
+            .map_err(|_| Error::Internal("Communication".to_owned()))?;
 
-        match receiver.await.map_err(|_| Error::Communication)? {
+        match receiver
+            .await
+            .map_err(|_| Error::Internal("Communication".to_owned()))?
+        {
             DbResponse::Ok => Ok(()),
-            DbResponse::Error(err) => Err(err),
+            DbResponse::Error(err) => Err(err.into()),
             _ => Err(Error::InvalidDbResponse),
         }
     }
 
-    pub async fn rollback(self) -> Result<(), Error> {
+    async fn rollback(self) -> Result<(), Error> {
         let (sender, receiver) = oneshot::channel();
         self.db_sender
             .send(DbRequest::Rollback(sender))
             .await
-            .map_err(|_| Error::Communication)?;
+            .map_err(|_| Error::Internal("Communication".to_owned()))?;
 
-        match receiver.await.map_err(|_| Error::Communication)? {
+        match receiver
+            .await
+            .map_err(|_| Error::Internal("Communication".to_owned()))?
+        {
             DbResponse::Ok => Ok(()),
-            DbResponse::Error(err) => Err(err),
+            DbResponse::Error(err) => Err(err.into()),
             _ => Err(Error::InvalidDbResponse),
         }
     }
 }
 
+#[async_trait::async_trait]
 impl DatabaseExecutor for Transaction<'_> {
-    /// Get the internal sender to the SQL queue
-    #[inline(always)]
-    fn get_queue_sender(&self) -> mpsc::Sender<DbRequest> {
-        self.db_sender.clone()
+    fn name() -> &'static str {
+        "sqlite"
+    }
+
+    async fn fetch_one(&self, mut statement: InnerStatement) -> Result<Option<Vec<Column>>, Error> {
+        let (sender, receiver) = oneshot::channel();
+        statement.expected_response = ExpectedSqlResponse::SingleRow;
+        self.get_queue_sender()
+            .send(DbRequest::Sql(statement, sender))
+            .await
+            .map_err(|_| Error::Internal("Communication".to_owned()))?;
+
+        match receiver
+            .await
+            .map_err(|_| Error::Internal("Communication".to_owned()))?
+        {
+            DbResponse::Row(row) => Ok(row),
+            DbResponse::Error(err) => Err(err.into()),
+            _ => Err(Error::InvalidDbResponse),
+        }
+    }
+
+    async fn batch(&self, mut statement: InnerStatement) -> Result<(), Error> {
+        let (sender, receiver) = oneshot::channel();
+        statement.expected_response = ExpectedSqlResponse::Batch;
+        self.get_queue_sender()
+            .send(DbRequest::Sql(statement, sender))
+            .await
+            .map_err(|_| Error::Internal("Communication".to_owned()))?;
+
+        match receiver
+            .await
+            .map_err(|_| Error::Internal("Communication".to_owned()))?
+        {
+            DbResponse::Ok => Ok(()),
+            DbResponse::Error(err) => Err(err.into()),
+            _ => Err(Error::InvalidDbResponse),
+        }
+    }
+
+    async fn fetch_all(&self, mut statement: InnerStatement) -> Result<Vec<Vec<Column>>, Error> {
+        let (sender, receiver) = oneshot::channel();
+        statement.expected_response = ExpectedSqlResponse::ManyRows;
+        self.get_queue_sender()
+            .send(DbRequest::Sql(statement, sender))
+            .await
+            .map_err(|_| Error::Internal("Communication".to_owned()))?;
+
+        match receiver
+            .await
+            .map_err(|_| Error::Internal("Communication".to_owned()))?
+        {
+            DbResponse::Rows(row) => Ok(row),
+            DbResponse::Error(err) => Err(err.into()),
+            _ => Err(Error::InvalidDbResponse),
+        }
+    }
+
+    async fn execute(&self, mut statement: InnerStatement) -> Result<usize, Error> {
+        let (sender, receiver) = oneshot::channel();
+        statement.expected_response = ExpectedSqlResponse::AffectedRows;
+        self.get_queue_sender()
+            .send(DbRequest::Sql(statement, sender))
+            .await
+            .map_err(|_| Error::Internal("Communication".to_owned()))?;
+
+        match receiver
+            .await
+            .map_err(|_| Error::Internal("Communication".to_owned()))?
+        {
+            DbResponse::AffectedRows(total) => Ok(total),
+            DbResponse::Error(err) => Err(err.into()),
+            _ => Err(Error::InvalidDbResponse),
+        }
+    }
+
+    async fn pluck(&self, mut statement: InnerStatement) -> Result<Option<Column>, Error> {
+        let (sender, receiver) = oneshot::channel();
+        statement.expected_response = ExpectedSqlResponse::Pluck;
+        self.get_queue_sender()
+            .send(DbRequest::Sql(statement, sender))
+            .await
+            .map_err(|_| Error::Internal("Communication".to_owned()))?;
+
+        match receiver
+            .await
+            .map_err(|_| Error::Internal("Communication".to_owned()))?
+        {
+            DbResponse::Pluck(value) => Ok(value),
+            DbResponse::Error(err) => Err(err.into()),
+            _ => Err(Error::InvalidDbResponse),
+        }
     }
 }

+ 0 - 5
crates/cdk-sqlite/src/mint/auth/migrations.rs

@@ -1,5 +0,0 @@
-// @generated
-// Auto-generated by build.rs
-pub static MIGRATIONS: &[(&str, &str)] = &[
-    ("20250109143347_init.sql", include_str!(r#"./migrations/20250109143347_init.sql"#)),
-];

+ 0 - 116
crates/cdk-sqlite/src/mint/error.rs

@@ -1,116 +0,0 @@
-//! SQLite Database Error
-
-use thiserror::Error;
-
-/// SQLite Database Error
-#[derive(Debug, Error)]
-pub enum Error {
-    /// SQLX Error
-    #[error(transparent)]
-    Sqlite(#[from] rusqlite::Error),
-
-    /// Duplicate entry
-    #[error("Record already exists")]
-    Duplicate,
-
-    /// Pool error
-    #[error(transparent)]
-    Pool(#[from] crate::pool::Error<rusqlite::Error>),
-    /// Invalid UUID
-    #[error("Invalid UUID: {0}")]
-    InvalidUuid(String),
-    /// QuoteNotFound
-    #[error("Quote not found")]
-    QuoteNotFound,
-
-    /// Missing named parameter
-    #[error("Missing named parameter {0}")]
-    MissingParameter(String),
-
-    /// Communication error with the database
-    #[error("Internal communication error")]
-    Communication,
-
-    /// Invalid response from the database thread
-    #[error("Unexpected database response")]
-    InvalidDbResponse,
-
-    /// Invalid db type
-    #[error("Invalid type from db, expected {0} got {1}")]
-    InvalidType(String, String),
-
-    /// Missing columns
-    #[error("Not enough elements: expected {0}, got {1}")]
-    MissingColumn(usize, usize),
-
-    /// Invalid data conversion in column
-    #[error("Error converting {0} to {1}")]
-    InvalidConversion(String, String),
-
-    /// NUT00 Error
-    #[error(transparent)]
-    CDKNUT00(#[from] cdk_common::nuts::nut00::Error),
-    /// NUT01 Error
-    #[error(transparent)]
-    CDKNUT01(#[from] cdk_common::nuts::nut01::Error),
-    /// NUT02 Error
-    #[error(transparent)]
-    CDKNUT02(#[from] cdk_common::nuts::nut02::Error),
-    /// NUT04 Error
-    #[error(transparent)]
-    CDKNUT04(#[from] cdk_common::nuts::nut04::Error),
-    /// NUT05 Error
-    #[error(transparent)]
-    CDKNUT05(#[from] cdk_common::nuts::nut05::Error),
-    /// NUT07 Error
-    #[error(transparent)]
-    CDKNUT07(#[from] cdk_common::nuts::nut07::Error),
-    /// NUT23 Error
-    #[error(transparent)]
-    CDKNUT23(#[from] cdk_common::nuts::nut23::Error),
-    /// Secret Error
-    #[error(transparent)]
-    CDKSECRET(#[from] cdk_common::secret::Error),
-    /// BIP32 Error
-    #[error(transparent)]
-    BIP32(#[from] bitcoin::bip32::Error),
-    /// Mint Url Error
-    #[error(transparent)]
-    MintUrl(#[from] cdk_common::mint_url::Error),
-    /// Could Not Initialize Database
-    #[error("Could not initialize database")]
-    CouldNotInitialize,
-    /// Invalid Database Path
-    #[error("Invalid database path")]
-    InvalidDbPath,
-    /// Serde Error
-    #[error(transparent)]
-    Serde(#[from] serde_json::Error),
-    /// Unknown Mint Info
-    #[error("Unknown mint info")]
-    UnknownMintInfo,
-    /// Unknown quote TTL
-    #[error("Unknown quote TTL")]
-    UnknownQuoteTTL,
-    /// Unknown config key
-    #[error("Unknown config key: {0}")]
-    UnknownConfigKey(String),
-    /// Proof not found
-    #[error("Proof not found")]
-    ProofNotFound,
-    /// Invalid keyset ID
-    #[error("Invalid keyset ID")]
-    InvalidKeysetId,
-    /// Invalid melt payment request
-    #[error("Invalid melt payment request")]
-    InvalidMeltPaymentRequest,
-}
-
-impl From<Error> for cdk_common::database::Error {
-    fn from(e: Error) -> Self {
-        match e {
-            Error::Duplicate => Self::Duplicate,
-            e => Self::Database(Box::new(e)),
-        }
-    }
-}

+ 4 - 3
crates/cdk-sqlite/src/mint/memory.rs

@@ -11,10 +11,11 @@ use super::MintSqliteDatabase;
 /// Creates a new in-memory [`MintSqliteDatabase`] instance
 pub async fn empty() -> Result<MintSqliteDatabase, database::Error> {
     #[cfg(not(feature = "sqlcipher"))]
-    let db = MintSqliteDatabase::new(":memory:").await?;
+    let path = ":memory:";
     #[cfg(feature = "sqlcipher")]
-    let db = MintSqliteDatabase::new(":memory:", "memory".to_string()).await?;
-    Ok(db)
+    let path = (":memory:", "memory");
+
+    MintSqliteDatabase::new(path).await
 }
 
 /// Creates a new in-memory [`MintSqliteDatabase`] instance with the given state

+ 0 - 25
crates/cdk-sqlite/src/mint/migrations.rs

@@ -1,25 +0,0 @@
-// @generated
-// Auto-generated by build.rs
-pub static MIGRATIONS: &[(&str, &str)] = &[
-    ("20240612124932_init.sql", include_str!(r#"./migrations/20240612124932_init.sql"#)),
-    ("20240618195700_quote_state.sql", include_str!(r#"./migrations/20240618195700_quote_state.sql"#)),
-    ("20240626092101_nut04_state.sql", include_str!(r#"./migrations/20240626092101_nut04_state.sql"#)),
-    ("20240703122347_request_lookup_id.sql", include_str!(r#"./migrations/20240703122347_request_lookup_id.sql"#)),
-    ("20240710145043_input_fee.sql", include_str!(r#"./migrations/20240710145043_input_fee.sql"#)),
-    ("20240711183109_derivation_path_index.sql", include_str!(r#"./migrations/20240711183109_derivation_path_index.sql"#)),
-    ("20240718203721_allow_unspent.sql", include_str!(r#"./migrations/20240718203721_allow_unspent.sql"#)),
-    ("20240811031111_update_mint_url.sql", include_str!(r#"./migrations/20240811031111_update_mint_url.sql"#)),
-    ("20240919103407_proofs_quote_id.sql", include_str!(r#"./migrations/20240919103407_proofs_quote_id.sql"#)),
-    ("20240923153640_melt_requests.sql", include_str!(r#"./migrations/20240923153640_melt_requests.sql"#)),
-    ("20240930101140_dleq_for_sigs.sql", include_str!(r#"./migrations/20240930101140_dleq_for_sigs.sql"#)),
-    ("20241108093102_mint_mint_quote_pubkey.sql", include_str!(r#"./migrations/20241108093102_mint_mint_quote_pubkey.sql"#)),
-    ("20250103201327_amount_to_pay_msats.sql", include_str!(r#"./migrations/20250103201327_amount_to_pay_msats.sql"#)),
-    ("20250129200912_remove_mint_url.sql", include_str!(r#"./migrations/20250129200912_remove_mint_url.sql"#)),
-    ("20250129230326_add_config_table.sql", include_str!(r#"./migrations/20250129230326_add_config_table.sql"#)),
-    ("20250307213652_keyset_id_as_foreign_key.sql", include_str!(r#"./migrations/20250307213652_keyset_id_as_foreign_key.sql"#)),
-    ("20250406091754_mint_time_of_quotes.sql", include_str!(r#"./migrations/20250406091754_mint_time_of_quotes.sql"#)),
-    ("20250406093755_mint_created_time_signature.sql", include_str!(r#"./migrations/20250406093755_mint_created_time_signature.sql"#)),
-    ("20250415093121_drop_keystore_foreign.sql", include_str!(r#"./migrations/20250415093121_drop_keystore_foreign.sql"#)),
-    ("20250626120251_rename_blind_message_y_to_b.sql", include_str!(r#"./migrations/20250626120251_rename_blind_message_y_to_b.sql"#)),
-    ("20250706101057_bolt12.sql", include_str!(r#"./migrations/20250706101057_bolt12.sql"#)),
-];

File diff suppressed because it is too large
+ 11 - 1896
crates/cdk-sqlite/src/mint/mod.rs


+ 0 - 184
crates/cdk-sqlite/src/stmt.rs

@@ -1,184 +0,0 @@
-use std::collections::HashMap;
-
-use rusqlite::{self, CachedStatement};
-
-use crate::common::SqliteConnectionManager;
-use crate::pool::PooledResource;
-
-/// The Value coming from SQLite
-pub type Value = rusqlite::types::Value;
-
-/// The Column type
-pub type Column = Value;
-
-/// Expected response type for a given SQL statement
-#[derive(Debug, Clone, Copy, Default)]
-pub enum ExpectedSqlResponse {
-    /// A single row
-    SingleRow,
-    /// All the rows that matches a query
-    #[default]
-    ManyRows,
-    /// How many rows were affected by the query
-    AffectedRows,
-    /// Return the first column of the first row
-    Pluck,
-}
-
-/// Sql message
-#[derive(Default, Debug)]
-pub struct Statement {
-    /// The SQL statement
-    pub sql: String,
-    /// The list of arguments for the placeholders. It only supports named arguments for simplicity
-    /// sake
-    pub args: HashMap<String, Value>,
-    /// The expected response type
-    pub expected_response: ExpectedSqlResponse,
-}
-
-impl Statement {
-    /// Creates a new statement
-    pub fn new<T>(sql: T) -> Self
-    where
-        T: ToString,
-    {
-        Self {
-            sql: sql.to_string(),
-            ..Default::default()
-        }
-    }
-
-    /// Binds a given placeholder to a value.
-    #[inline]
-    pub fn bind<C, V>(mut self, name: C, value: V) -> Self
-    where
-        C: ToString,
-        V: Into<Value>,
-    {
-        self.args.insert(name.to_string(), value.into());
-        self
-    }
-
-    /// Binds a single variable with a vector.
-    ///
-    /// This will rewrite the function from `:foo` (where value is vec![1, 2, 3]) to `:foo0, :foo1,
-    /// :foo2` and binds each value from the value vector accordingly.
-    #[inline]
-    pub fn bind_vec<C, V>(mut self, name: C, value: Vec<V>) -> Self
-    where
-        C: ToString,
-        V: Into<Value>,
-    {
-        let mut new_sql = String::with_capacity(self.sql.len());
-        let target = name.to_string();
-        let mut i = 0;
-
-        let placeholders = value
-            .into_iter()
-            .enumerate()
-            .map(|(key, value)| {
-                let key = format!("{target}{key}");
-                self.args.insert(key.clone(), value.into());
-                key
-            })
-            .collect::<Vec<_>>()
-            .join(",");
-
-        while let Some(pos) = self.sql[i..].find(&target) {
-            let abs_pos = i + pos;
-            let after = abs_pos + target.len();
-            let is_word_boundary = self.sql[after..]
-                .chars()
-                .next()
-                .map_or(true, |c| !c.is_alphanumeric() && c != '_');
-
-            if is_word_boundary {
-                new_sql.push_str(&self.sql[i..abs_pos]);
-                new_sql.push_str(&placeholders);
-                i = after;
-            } else {
-                new_sql.push_str(&self.sql[i..=abs_pos]);
-                i = abs_pos + 1;
-            }
-        }
-
-        new_sql.push_str(&self.sql[i..]);
-
-        self.sql = new_sql;
-        self
-    }
-
-    fn get_stmt(
-        self,
-        conn: &PooledResource<SqliteConnectionManager>,
-    ) -> rusqlite::Result<CachedStatement<'_>> {
-        let mut stmt = conn.prepare_cached(&self.sql)?;
-        for (name, value) in self.args {
-            let index = stmt
-                .parameter_index(&name)
-                .map_err(|_| rusqlite::Error::InvalidColumnName(name.clone()))?
-                .ok_or(rusqlite::Error::InvalidColumnName(name))?;
-
-            stmt.raw_bind_parameter(index, value)?;
-        }
-
-        Ok(stmt)
-    }
-
-    /// Executes a query and returns the affected rows
-    pub fn plunk(
-        self,
-        conn: &PooledResource<SqliteConnectionManager>,
-    ) -> rusqlite::Result<Option<Value>> {
-        let mut stmt = self.get_stmt(conn)?;
-        let mut rows = stmt.raw_query();
-        rows.next()?.map(|row| row.get(0)).transpose()
-    }
-
-    /// Executes a query and returns the affected rows
-    pub fn execute(
-        self,
-        conn: &PooledResource<SqliteConnectionManager>,
-    ) -> rusqlite::Result<usize> {
-        self.get_stmt(conn)?.raw_execute()
-    }
-
-    /// Runs the query and returns the first row or None
-    pub fn fetch_one(
-        self,
-        conn: &PooledResource<SqliteConnectionManager>,
-    ) -> rusqlite::Result<Option<Vec<Column>>> {
-        let mut stmt = self.get_stmt(conn)?;
-        let columns = stmt.column_count();
-        let mut rows = stmt.raw_query();
-        rows.next()?
-            .map(|row| {
-                (0..columns)
-                    .map(|i| row.get(i))
-                    .collect::<Result<Vec<_>, _>>()
-            })
-            .transpose()
-    }
-
-    /// Runs the query and returns the first row or None
-    pub fn fetch_all(
-        self,
-        conn: &PooledResource<SqliteConnectionManager>,
-    ) -> rusqlite::Result<Vec<Vec<Column>>> {
-        let mut stmt = self.get_stmt(conn)?;
-        let columns = stmt.column_count();
-        let mut rows = stmt.raw_query();
-        let mut results = vec![];
-
-        while let Some(row) = rows.next()? {
-            results.push(
-                (0..columns)
-                    .map(|i| row.get(i))
-                    .collect::<Result<Vec<_>, _>>()?,
-            );
-        }
-
-        Ok(results)
-    }
-}

+ 5 - 3
crates/cdk-sqlite/src/wallet/memory.rs

@@ -7,8 +7,10 @@ use super::WalletSqliteDatabase;
 /// Creates a new in-memory [`WalletSqliteDatabase`] instance
 pub async fn empty() -> Result<WalletSqliteDatabase, Error> {
     #[cfg(not(feature = "sqlcipher"))]
-    let db = WalletSqliteDatabase::new(":memory:").await?;
+    let path = ":memory:";
+
     #[cfg(feature = "sqlcipher")]
-    let db = WalletSqliteDatabase::new(":memory:", "memory".to_owned()).await?;
-    Ok(db)
+    let path = (":memory:", "memory");
+
+    WalletSqliteDatabase::new(path).await
 }

+ 0 - 21
crates/cdk-sqlite/src/wallet/migrations.rs

@@ -1,21 +0,0 @@
-// @generated
-// Auto-generated by build.rs
-pub static MIGRATIONS: &[(&str, &str)] = &[
-    ("20240612132920_init.sql", include_str!(r#"./migrations/20240612132920_init.sql"#)),
-    ("20240618200350_quote_state.sql", include_str!(r#"./migrations/20240618200350_quote_state.sql"#)),
-    ("20240626091921_nut04_state.sql", include_str!(r#"./migrations/20240626091921_nut04_state.sql"#)),
-    ("20240710144711_input_fee.sql", include_str!(r#"./migrations/20240710144711_input_fee.sql"#)),
-    ("20240810214105_mint_icon_url.sql", include_str!(r#"./migrations/20240810214105_mint_icon_url.sql"#)),
-    ("20240810233905_update_mint_url.sql", include_str!(r#"./migrations/20240810233905_update_mint_url.sql"#)),
-    ("20240902151515_icon_url.sql", include_str!(r#"./migrations/20240902151515_icon_url.sql"#)),
-    ("20240902210905_mint_time.sql", include_str!(r#"./migrations/20240902210905_mint_time.sql"#)),
-    ("20241011125207_mint_urls.sql", include_str!(r#"./migrations/20241011125207_mint_urls.sql"#)),
-    ("20241108092756_wallet_mint_quote_secretkey.sql", include_str!(r#"./migrations/20241108092756_wallet_mint_quote_secretkey.sql"#)),
-    ("20250214135017_mint_tos.sql", include_str!(r#"./migrations/20250214135017_mint_tos.sql"#)),
-    ("20250310111513_drop_nostr_last_checked.sql", include_str!(r#"./migrations/20250310111513_drop_nostr_last_checked.sql"#)),
-    ("20250314082116_allow_pending_spent.sql", include_str!(r#"./migrations/20250314082116_allow_pending_spent.sql"#)),
-    ("20250323152040_wallet_dleq_proofs.sql", include_str!(r#"./migrations/20250323152040_wallet_dleq_proofs.sql"#)),
-    ("20250401120000_add_transactions_table.sql", include_str!(r#"./migrations/20250401120000_add_transactions_table.sql"#)),
-    ("20250616144830_add_keyset_expiry.sql", include_str!(r#"./migrations/20250616144830_add_keyset_expiry.sql"#)),
-    ("20250707093445_bolt12.sql", include_str!(r#"./migrations/20250707093445_bolt12.sql"#)),
-];

File diff suppressed because it is too large
+ 138 - 1085
crates/cdk-sqlite/src/wallet/mod.rs


+ 1 - 1
crates/cdk-sqlite/tests/legacy-sqlx.sql

@@ -70,7 +70,7 @@ CREATE TABLE IF NOT EXISTS "proof" (
     amount INTEGER NOT NULL,
     keyset_id TEXT NOT NULL, -- no FK constraint here
     secret TEXT NOT NULL,
-    c BLOB NOT NULL,
+    c BLOBNOT NULL,
     witness TEXT,
     state TEXT CHECK (state IN ('SPENT', 'PENDING', 'UNSPENT', 'RESERVED', 'UNKNOWN')) NOT NULL,
     quote_id TEXT,

Some files were not shown because too many files changed in this diff