Răsfoiți Sursa

feat: extract `cdk-sql-base` as a reusable SQL backend abstraction

This commit introduces the new `cdk-sql-base` crate, refactored from the
previous `cdk-sqlite` implementation. The goal is to decouple SQL schema and
migration logic from the underlying database driver, enabling support for
multiple backends such as SQLite and Postgres.

`cdk-sql-base` now contains shared schema definitions and migration logic for
core components, including the wallet, authentication, and mint. These are
expressed using standard SQL and are designed to be portable across drivers.

Each driver crate (e.g., `cdk-sqlite`, and the upcoming `cdk-sql-postgres`) is
responsible for:
- Setting up and managing the connection pool.
- Executing queries and handling transactions.
- Providing a concrete implementation of traits.

- The **wallet (SQLite)** uses a single-threaded connection model. It is
  optimized for local, embedded environments where simplicity and reliability
  are prioritized over parallelism.
- The **mint** requires a multithreaded environment capable of handling
  concurrent requests. The abstraction allows Postgres or mutex-guarded pools
  to serve this role, enabling safe parallel processing.

Each subsystem (wallet, auth, mint) contributes its own schema fragment. These
are registered with `cdk-sql-base`, which ensures they are applied in order
through a consistent migration process.

This abstraction improves code reuse across database backends, simplifies
future migrations to new engines, and provides a solid foundation for
introducing Postgres support in a follow-up commit.
Cesar Rodas 1 lună în urmă
părinte
comite
0d1b95419c
75 a modificat fișierele cu 4083 adăugiri și 3239 ștergeri
  1. 1 0
      Cargo.toml
  2. 2 2
      crates/cdk-cli/src/main.rs
  3. 141 1
      crates/cdk-common/src/database/mint/test.rs
  4. 104 0
      crates/cdk-common/src/database/mod.rs
  5. 2 2
      crates/cdk-integration-tests/src/init_pure_tests.rs
  6. 7 4
      crates/cdk-mintd/src/main.rs
  7. 1 1
      crates/cdk-signatory/src/bin/cli/mod.rs
  8. 30 0
      crates/cdk-sql-base/Cargo.toml
  9. 24 0
      crates/cdk-sql-base/README.md
  10. 2 2
      crates/cdk-sql-base/build.rs
  11. 38 0
      crates/cdk-sql-base/run_test.sh
  12. 57 0
      crates/cdk-sql-base/src/common.rs
  13. 50 0
      crates/cdk-sql-base/src/database.rs
  14. 23 0
      crates/cdk-sql-base/src/lib.rs
  15. 52 34
      crates/cdk-sql-base/src/macros.rs
  16. 6 0
      crates/cdk-sql-base/src/mint/auth/migrations.rs
  17. 0 0
      crates/cdk-sql-base/src/mint/auth/migrations/20250109143347_init.sql
  18. 56 49
      crates/cdk-sql-base/src/mint/auth/mod.rs
  19. 46 0
      crates/cdk-sql-base/src/mint/migrations.rs
  20. 0 0
      crates/cdk-sql-base/src/mint/migrations/20240612124932_init.sql
  21. 0 0
      crates/cdk-sql-base/src/mint/migrations/20240618195700_quote_state.sql
  22. 0 0
      crates/cdk-sql-base/src/mint/migrations/20240626092101_nut04_state.sql
  23. 0 0
      crates/cdk-sql-base/src/mint/migrations/20240703122347_request_lookup_id.sql
  24. 0 0
      crates/cdk-sql-base/src/mint/migrations/20240710145043_input_fee.sql
  25. 0 0
      crates/cdk-sql-base/src/mint/migrations/20240711183109_derivation_path_index.sql
  26. 0 0
      crates/cdk-sql-base/src/mint/migrations/20240718203721_allow_unspent.sql
  27. 0 0
      crates/cdk-sql-base/src/mint/migrations/20240811031111_update_mint_url.sql
  28. 0 0
      crates/cdk-sql-base/src/mint/migrations/20240919103407_proofs_quote_id.sql
  29. 0 0
      crates/cdk-sql-base/src/mint/migrations/20240923153640_melt_requests.sql
  30. 0 0
      crates/cdk-sql-base/src/mint/migrations/20240930101140_dleq_for_sigs.sql
  31. 0 0
      crates/cdk-sql-base/src/mint/migrations/20241108093102_mint_mint_quote_pubkey.sql
  32. 0 0
      crates/cdk-sql-base/src/mint/migrations/20250103201327_amount_to_pay_msats.sql
  33. 0 0
      crates/cdk-sql-base/src/mint/migrations/20250129200912_remove_mint_url.sql
  34. 0 0
      crates/cdk-sql-base/src/mint/migrations/20250129230326_add_config_table.sql
  35. 1 1
      crates/cdk-sql-base/src/mint/migrations/20250307213652_keyset_id_as_foreign_key.sql
  36. 0 0
      crates/cdk-sql-base/src/mint/migrations/20250406091754_mint_time_of_quotes.sql
  37. 0 0
      crates/cdk-sql-base/src/mint/migrations/20250406093755_mint_created_time_signature.sql
  38. 0 0
      crates/cdk-sql-base/src/mint/migrations/20250415093121_drop_keystore_foreign.sql
  39. 0 0
      crates/cdk-sql-base/src/mint/migrations/20250626120251_rename_blind_message_y_to_b.sql
  40. 1505 0
      crates/cdk-sql-base/src/mint/mod.rs
  41. 0 0
      crates/cdk-sql-base/src/pool.rs
  42. 42 66
      crates/cdk-sql-base/src/stmt.rs
  43. 82 0
      crates/cdk-sql-base/src/value.rs
  44. 1 1
      crates/cdk-sql-base/src/wallet/error.rs
  45. 36 0
      crates/cdk-sql-base/src/wallet/migrations.rs
  46. 0 0
      crates/cdk-sql-base/src/wallet/migrations/20240612132920_init.sql
  47. 0 0
      crates/cdk-sql-base/src/wallet/migrations/20240618200350_quote_state.sql
  48. 0 0
      crates/cdk-sql-base/src/wallet/migrations/20240626091921_nut04_state.sql
  49. 0 0
      crates/cdk-sql-base/src/wallet/migrations/20240710144711_input_fee.sql
  50. 0 0
      crates/cdk-sql-base/src/wallet/migrations/20240810214105_mint_icon_url.sql
  51. 0 0
      crates/cdk-sql-base/src/wallet/migrations/20240810233905_update_mint_url.sql
  52. 0 0
      crates/cdk-sql-base/src/wallet/migrations/20240902151515_icon_url.sql
  53. 0 0
      crates/cdk-sql-base/src/wallet/migrations/20240902210905_mint_time.sql
  54. 0 0
      crates/cdk-sql-base/src/wallet/migrations/20241011125207_mint_urls.sql
  55. 0 0
      crates/cdk-sql-base/src/wallet/migrations/20241108092756_wallet_mint_quote_secretkey.sql
  56. 0 0
      crates/cdk-sql-base/src/wallet/migrations/20250214135017_mint_tos.sql
  57. 0 0
      crates/cdk-sql-base/src/wallet/migrations/20250310111513_drop_nostr_last_checked.sql
  58. 0 0
      crates/cdk-sql-base/src/wallet/migrations/20250314082116_allow_pending_spent.sql
  59. 0 0
      crates/cdk-sql-base/src/wallet/migrations/20250323152040_wallet_dleq_proofs.sql
  60. 0 0
      crates/cdk-sql-base/src/wallet/migrations/20250401120000_add_transactions_table.sql
  61. 0 0
      crates/cdk-sql-base/src/wallet/migrations/20250616144830_add_keyset_expiry.sql
  62. 1198 0
      crates/cdk-sql-base/src/wallet/mod.rs
  63. 97 0
      crates/cdk-sql-base/tests/legacy-sqlx.sql
  64. 4 3
      crates/cdk-sqlite/Cargo.toml
  65. 24 58
      crates/cdk-sqlite/src/common.rs
  66. 0 3
      crates/cdk-sqlite/src/lib.rs
  67. 308 153
      crates/cdk-sqlite/src/mint/async_rusqlite.rs
  68. 0 5
      crates/cdk-sqlite/src/mint/auth/migrations.rs
  69. 0 110
      crates/cdk-sqlite/src/mint/error.rs
  70. 4 3
      crates/cdk-sqlite/src/mint/memory.rs
  71. 0 24
      crates/cdk-sqlite/src/mint/migrations.rs
  72. 9 1635
      crates/cdk-sqlite/src/mint/mod.rs
  73. 5 3
      crates/cdk-sqlite/src/wallet/memory.rs
  74. 0 20
      crates/cdk-sqlite/src/wallet/migrations.rs
  75. 125 1059
      crates/cdk-sqlite/src/wallet/mod.rs

+ 1 - 0
Cargo.toml

@@ -55,6 +55,7 @@ cdk-payment-processor = { path = "./crates/cdk-payment-processor", default-featu
 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-sqlite = { path = "./crates/cdk-sqlite", default-features = true, version = "=0.11.0" }
+cdk-sql-base = { path = "./crates/cdk-sql-base", 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"] }
 ciborium = { version = "0.2.2", default-features = false, features = ["std"] }

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

@@ -122,11 +122,11 @@ async fn main() -> Result<()> {
             "sqlite" => {
                 let sql_path = work_dir.join("cdk-cli.sqlite");
                 #[cfg(not(feature = "sqlcipher"))]
-                let sql = WalletSqliteDatabase::new(&sql_path).await?;
+                let sql = WalletSqliteDatabase::new(sql_path).await?;
                 #[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"),
                     }
                 };

+ 141 - 1
crates/cdk-common/src/database/mint/test.rs

@@ -34,6 +34,144 @@ where
     keyset_id
 }
 
+/// Test update spend proofs fails
+#[inline]
+pub async fn test_update_spent_proofs<DB>(db: DB)
+where
+    DB: Database<database::Error> + KeysDatabase<Err = database::Error>,
+{
+    // Create a keyset and add it to the database
+    let keyset_id = Id::from_str("00916bbf7ef91a36").unwrap();
+    let keyset_info = MintKeySetInfo {
+        id: keyset_id,
+        unit: CurrencyUnit::Sat,
+        active: true,
+        valid_from: 0,
+        derivation_path: bitcoin::bip32::DerivationPath::from_str("m/0'/0'/0'").unwrap(),
+        derivation_path_index: Some(0),
+        max_order: 32,
+        input_fee_ppk: 0,
+        final_expiry: None,
+    };
+    let mut tx = KeysDatabase::begin_transaction(&db).await.expect("begin");
+    tx.add_keyset_info(keyset_info).await.unwrap();
+    tx.commit().await.expect("commit");
+
+    let proofs = vec![
+        Proof {
+            amount: Amount::from(100),
+            keyset_id,
+            secret: Secret::generate(),
+            c: SecretKey::generate().public_key(),
+            witness: None,
+            dleq: None,
+        },
+        Proof {
+            amount: Amount::from(200),
+            keyset_id,
+            secret: Secret::generate(),
+            c: SecretKey::generate().public_key(),
+            witness: None,
+            dleq: None,
+        },
+    ];
+
+    // Add proofs to database
+    let mut tx = Database::begin_transaction(&db).await.unwrap();
+    tx.add_proofs(proofs.clone(), None).await.unwrap();
+
+    // Mark one proof as spent
+    tx.update_proofs_states(&[proofs[0].y().unwrap()], State::Spent)
+        .await
+        .unwrap();
+
+    // Try to update both proofs - should fail because one is spent
+    let result = tx
+        .update_proofs_states(&[proofs[0].y().unwrap()], State::Unspent)
+        .await;
+
+    tx.commit().await.unwrap();
+
+    assert!(result.is_err());
+    assert!(matches!(
+        result.unwrap_err(),
+        Error::AttemptUpdateSpentProof
+    ));
+
+    // Verify states haven't changed
+    let states = db
+        .get_proofs_states(&[proofs[0].y().unwrap(), proofs[1].y().unwrap()])
+        .await
+        .unwrap();
+
+    assert_eq!(states.len(), 2);
+    assert_eq!(states[0], Some(State::Spent));
+    assert_eq!(states[1], Some(State::Unspent));
+}
+
+/// Test remove remove spent proof fails
+pub async fn test_remove_spent_proofs<DB>(db: DB)
+where
+    DB: Database<database::Error> + KeysDatabase<Err = database::Error>,
+{
+    // Create a keyset and add it to the database
+    let keyset_id = Id::from_str("00916bbf7ef91a36").unwrap();
+    let keyset_info = MintKeySetInfo {
+        id: keyset_id,
+        unit: CurrencyUnit::Sat,
+        active: true,
+        valid_from: 0,
+        derivation_path: bitcoin::bip32::DerivationPath::from_str("m/0'/0'/0'").unwrap(),
+        derivation_path_index: Some(0),
+        max_order: 32,
+        input_fee_ppk: 0,
+        final_expiry: None,
+    };
+    let mut tx = KeysDatabase::begin_transaction(&db).await.unwrap();
+    tx.add_keyset_info(keyset_info).await.unwrap();
+    tx.commit().await.unwrap();
+
+    let proofs = vec![
+        Proof {
+            amount: Amount::from(100),
+            keyset_id,
+            secret: Secret::generate(),
+            c: SecretKey::generate().public_key(),
+            witness: None,
+            dleq: None,
+        },
+        Proof {
+            amount: Amount::from(200),
+            keyset_id,
+            secret: Secret::generate(),
+            c: SecretKey::generate().public_key(),
+            witness: None,
+            dleq: None,
+        },
+    ];
+
+    // Add proofs to database
+    let mut tx = Database::begin_transaction(&db).await.unwrap();
+    tx.add_proofs(proofs.clone(), None).await.unwrap();
+
+    // Mark one proof as spent
+    tx.update_proofs_states(&[proofs[0].y().unwrap()], State::Spent)
+        .await
+        .unwrap();
+
+    tx.commit().await.unwrap();
+
+    // Verify both proofs still exist
+    let states = db
+        .get_proofs_states(&[proofs[0].y().unwrap(), proofs[1].y().unwrap()])
+        .await
+        .unwrap();
+
+    assert_eq!(states.len(), 2);
+    assert_eq!(states[0], Some(State::Spent));
+    assert_eq!(states[1], Some(State::Unspent));
+}
+
 /// State transition test
 pub async fn state_transition<DB>(db: DB)
 where
@@ -83,11 +221,13 @@ where
 macro_rules! mint_db_test {
     ($make_db_fn:ident) => {
         mint_db_test!(state_transition, $make_db_fn);
+        mint_db_test!(test_update_spent_proofs, $make_db_fn);
+        mint_db_test!(test_remove_spent_proofs, $make_db_fn);
     };
     ($name:ident, $make_db_fn:ident) => {
         #[tokio::test]
         async fn $name() {
-            cdk_common::database::mint::test::$name($make_db_fn().await).await;
+            $crate::database::mint::test::$name($make_db_fn().await).await;
         }
     };
 }

+ 104 - 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 {
@@ -26,10 +100,34 @@ pub enum Error {
     #[error(transparent)]
     Database(Box<dyn std::error::Error + Send + Sync>),
 
+    /// 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),
+
     /// Duplicate entry
     #[error("Duplicate entry")]
     Duplicate,
 
+    /// Invalid UUID
+    #[error("Invalid UUID: {0}")]
+    InvalidUuid(String),
+
+    /// QuoteNotFound
+    #[error("Quote not found")]
+    QuoteNotFound,
+
     /// DHKE error
     #[error(transparent)]
     DHKE(#[from] crate::dhke::Error),
@@ -38,6 +136,9 @@ pub enum Error {
     NUT00(#[from] crate::nuts::nut00::Error),
     /// NUT02 Error
     #[error(transparent)]
+    NUT01(#[from] crate::nuts::nut01::Error),
+    /// NUT02 Error
+    #[error(transparent)]
     NUT02(#[from] crate::nuts::nut02::Error),
     /// NUT22 Error
     #[error(transparent)]
@@ -65,6 +166,9 @@ pub enum Error {
     /// Invalid state transition
     #[error("Invalid state transition")]
     InvalidStateTransition(crate::state::Error),
+    /// Unknown quote ttl
+    #[error("Unknown quote ttl")]
+    UnknownQuoteTTL,
 }
 
 #[cfg(feature = "mint")]

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

@@ -182,7 +182,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();
             let database = Arc::new(
-                cdk_sqlite::MintSqliteDatabase::new(&path)
+                cdk_sqlite::MintSqliteDatabase::new(path.as_str())
                     .await
                     .expect("Could not create sqlite db"),
             );
@@ -269,7 +269,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)

+ 7 - 4
crates/cdk-mintd/src/main.rs

@@ -210,12 +210,15 @@ async fn setup_sqlite_database(
     _password: Option<String>,
 ) -> Result<Arc<MintSqliteDatabase>> {
     let sql_db_path = work_dir.join("cdk-mintd.sqlite");
+
+    let sql_db_path = sql_db_path.to_str().unwrap_or_default();
+
     #[cfg(not(feature = "sqlcipher"))]
-    let db = MintSqliteDatabase::new(&sql_db_path).await?;
+    let db = MintSqliteDatabase::new(sql_db_path).await?;
     #[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))
 }
@@ -510,9 +513,9 @@ 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?;
+                let sqlite_db = MintSqliteAuthDatabase::new(sql_db_path).await?;
                 Arc::new(sqlite_db)
             }
         };

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

@@ -104,7 +104,7 @@ pub async fn cli_main() -> Result<()> {
                 {
                     let sql_path = work_dir.join("cdk-cli.sqlite");
                     #[cfg(not(feature = "sqlcipher"))]
-                    let db = MintSqliteDatabase::new(&sql_path).await?;
+                    let db = MintSqliteDatabase::new(sql_path).await?;
                     #[cfg(feature = "sqlcipher")]
                     let db = {
                         match args.password {

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

@@ -0,0 +1,30 @@
+[package]
+name = "cdk-sql-base"
+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-base/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  |
+| `sqlcipher` |   No    | Enable encrypted database          |
+
+
+## License
+
+This project is licensed under the [MIT License](../../LICENSE).

+ 2 - 2
crates/cdk-sqlite/build.rs → crates/cdk-sql-base/build.rs

@@ -18,8 +18,8 @@ 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();
+        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 {

+ 38 - 0
crates/cdk-sql-base/run_test.sh

@@ -0,0 +1,38 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+CONTAINER_NAME="rust-test-pg"
+DB_USER="test"
+DB_PASS="test"
+DB_NAME="testdb"
+DB_PORT="5433"
+DB_URL="postgres://${DB_USER}:${DB_PASS}@localhost:${DB_PORT}/${DB_NAME}"
+
+cleanup() {
+  echo "Cleaning up..."
+  docker stop "${CONTAINER_NAME}" >/dev/null 2>&1 || true
+  docker rm "${CONTAINER_NAME}" >/dev/null 2>&1 || true
+}
+
+trap cleanup EXIT INT TERM
+
+echo "Starting fresh PostgreSQL container..."
+docker run -d --rm \
+  --name "${CONTAINER_NAME}" \
+  -e POSTGRES_USER="${DB_USER}" \
+  -e POSTGRES_PASSWORD="${DB_PASS}" \
+  -e POSTGRES_DB="${DB_NAME}" \
+  -p ${DB_PORT}:5432 \
+  -v "${PWD}/.docker/pg-init.sql:/docker-entrypoint-initdb.d/init.sql:ro" \
+  postgres:16
+
+echo "Waiting for PostgreSQL to be ready..."
+until docker exec "${CONTAINER_NAME}" pg_isready -U "${DB_USER}" >/dev/null 2>&1; do
+  sleep 0.5
+done
+
+export DATABASE_URL="${DB_URL}"
+
+echo "Running cargo tests..."
+cargo test
+

+ 57 - 0
crates/cdk-sql-base/src/common.rs

@@ -0,0 +1,57 @@
+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,
+    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?;
+
+    /*if query(
+        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;
+        "#,
+        )?;
+    }*/
+
+    // Apply each migration if it hasn’t been applied yet
+    for (name, sql) in migrations {
+        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(())
+}

+ 50 - 0
crates/cdk-sql-base/src/database.rs

@@ -0,0 +1,50 @@
+//! 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 {
+    /// 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 comitting 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-base/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-base/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(),
             )),
         })?
     };

+ 6 - 0
crates/cdk-sql-base/src/mint/auth/migrations.rs

@@ -0,0 +1,6 @@
+/// @generated
+/// Auto-generated by build.rs
+pub static MIGRATIONS: &[(&str, &str)] = &[
+    ("20250109143347_init.sql", include_str!(r#"./migrations/20250109143347_init.sql"#)),
+    ("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-base/src/mint/auth/migrations/20250109143347_init.sql


+ 56 - 49
crates/cdk-sqlite/src/mint/auth/mod.rs → crates/cdk-sql-base/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, 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");
@@ -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,
         }))
     }
 
@@ -253,7 +260,7 @@ impl MintAuthDatabase for MintSqliteAuthDatabase {
                 active = 1;
             "#,
         )
-        .pluck(&self.pool)
+        .pluck(&self.db)
         .await?
         .map(|id| Ok::<_, Error>(column_as_string!(id, Id::from_str, Id::from_bytes)))
         .transpose()?)
@@ -276,9 +283,9 @@ impl MintAuthDatabase for MintSqliteAuthDatabase {
                 WHERE id=:id"#,
         )
         .bind(":id", id.to_string())
-        .fetch_one(&self.pool)
+        .fetch_one(&self.db)
         .await?
-        .map(sqlite_row_to_keyset_info)
+        .map(sql_row_to_keyset_info)
         .transpose()?)
     }
 
@@ -298,17 +305,17 @@ impl MintAuthDatabase for MintSqliteAuthDatabase {
                 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)
+            .fetch_all(&self.db)
             .await?
             .into_iter()
             .map(|row| {
@@ -346,7 +353,7 @@ impl MintAuthDatabase for MintSqliteAuthDatabase {
                 .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>>()?;
@@ -373,7 +380,7 @@ impl MintAuthDatabase for MintSqliteAuthDatabase {
         Ok(
             query(r#"SELECT auth FROM protected_endpoints WHERE endpoint = :endpoint"#)
                 .bind(":endpoint", serde_json::to_string(&protected_endpoint)?)
-                .pluck(&self.pool)
+                .pluck(&self.db)
                 .await?
                 .map(|auth| {
                     Ok::<_, Error>(column_as_string!(
@@ -390,7 +397,7 @@ impl MintAuthDatabase for MintSqliteAuthDatabase {
         &self,
     ) -> Result<HashMap<ProtectedEndpoint, Option<AuthRequired>>, Self::Err> {
         Ok(query(r#"SELECT endpoint, auth FROM protected_endpoints"#)
-            .fetch_all(&self.pool)
+            .fetch_all(&self.db)
             .await?
             .into_iter()
             .map(|row| {

+ 46 - 0
crates/cdk-sql-base/src/mint/migrations.rs

@@ -0,0 +1,46 @@
+/// @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"#)),
+    ("20250710212391_init.sql", include_str!(r#"./migrations/postgres/20250710212391_init.sql"#)),
+    ("1_fix_sqlx_migration.sql", include_str!(r#"./migrations/sqlite/1_fix_sqlx_migration.sql"#)),
+    ("20240612124932_init.sql", include_str!(r#"./migrations/sqlite/20240612124932_init.sql"#)),
+    ("20240618195700_quote_state.sql", include_str!(r#"./migrations/sqlite/20240618195700_quote_state.sql"#)),
+    ("20240626092101_nut04_state.sql", include_str!(r#"./migrations/sqlite/20240626092101_nut04_state.sql"#)),
+    ("20240703122347_request_lookup_id.sql", include_str!(r#"./migrations/sqlite/20240703122347_request_lookup_id.sql"#)),
+    ("20240710145043_input_fee.sql", include_str!(r#"./migrations/sqlite/20240710145043_input_fee.sql"#)),
+    ("20240711183109_derivation_path_index.sql", include_str!(r#"./migrations/sqlite/20240711183109_derivation_path_index.sql"#)),
+    ("20240718203721_allow_unspent.sql", include_str!(r#"./migrations/sqlite/20240718203721_allow_unspent.sql"#)),
+    ("20240811031111_update_mint_url.sql", include_str!(r#"./migrations/sqlite/20240811031111_update_mint_url.sql"#)),
+    ("20240919103407_proofs_quote_id.sql", include_str!(r#"./migrations/sqlite/20240919103407_proofs_quote_id.sql"#)),
+    ("20240923153640_melt_requests.sql", include_str!(r#"./migrations/sqlite/20240923153640_melt_requests.sql"#)),
+    ("20240930101140_dleq_for_sigs.sql", include_str!(r#"./migrations/sqlite/20240930101140_dleq_for_sigs.sql"#)),
+    ("20241108093102_mint_mint_quote_pubkey.sql", include_str!(r#"./migrations/sqlite/20241108093102_mint_mint_quote_pubkey.sql"#)),
+    ("20250103201327_amount_to_pay_msats.sql", include_str!(r#"./migrations/sqlite/20250103201327_amount_to_pay_msats.sql"#)),
+    ("20250129200912_remove_mint_url.sql", include_str!(r#"./migrations/sqlite/20250129200912_remove_mint_url.sql"#)),
+    ("20250129230326_add_config_table.sql", include_str!(r#"./migrations/sqlite/20250129230326_add_config_table.sql"#)),
+    ("20250307213652_keyset_id_as_foreign_key.sql", include_str!(r#"./migrations/sqlite/20250307213652_keyset_id_as_foreign_key.sql"#)),
+    ("20250406091754_mint_time_of_quotes.sql", include_str!(r#"./migrations/sqlite/20250406091754_mint_time_of_quotes.sql"#)),
+    ("20250406093755_mint_created_time_signature.sql", include_str!(r#"./migrations/sqlite/20250406093755_mint_created_time_signature.sql"#)),
+    ("20250415093121_drop_keystore_foreign.sql", include_str!(r#"./migrations/sqlite/20250415093121_drop_keystore_foreign.sql"#)),
+    ("20250626120251_rename_blind_message_y_to_b.sql", include_str!(r#"./migrations/sqlite/20250626120251_rename_blind_message_y_to_b.sql"#)),
+];

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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


+ 1 - 1
crates/cdk-sqlite/src/mint/migrations/20250307213652_keyset_id_as_foreign_key.sql → crates/cdk-sql-base/src/mint/migrations/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-base/src/mint/migrations/20250406091754_mint_time_of_quotes.sql


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


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


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


+ 1505 - 0
crates/cdk-sql-base/src/mint/mod.rs

@@ -0,0 +1,1505 @@
+//! 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, Error, MintDatabase, MintDbWriterFinalizer, MintKeyDatabaseTransaction, MintKeysDatabase,
+    MintProofsDatabase, MintProofsTransaction, MintQuotesDatabase, MintQuotesTransaction,
+    MintSignatureTransaction, MintSignaturesDatabase,
+};
+use cdk_common::mint::{self, MintKeySetInfo, MintQuote};
+use cdk_common::nut00::ProofsMethods;
+use cdk_common::nut05::QuoteState;
+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,
+    MintQuoteState, Proof, Proofs, PublicKey, SecretKey, State,
+};
+use lightning_invoice::Bolt11Invoice;
+use migrations::MIGRATIONS;
+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, 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::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?)
+    }
+}
+
+#[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;
+
+    async fn add_or_replace_mint_quote(&mut self, quote: MintQuote) -> Result<(), Self::Err> {
+        query(
+            r#"
+                INSERT OR REPLACE INTO mint_quote (
+                    id, amount, unit, request, state, expiry, request_lookup_id,
+                    pubkey, created_time, paid_time, issued_time
+                )
+                VALUES (
+                    :id, :amount, :unit, :request, :state, :expiry, :request_lookup_id,
+                    :pubkey, :created_time, :paid_time, :issued_time
+                )
+            "#,
+        )
+        .bind(":id", quote.id.to_string())
+        .bind(":amount", u64::from(quote.amount) as i64)
+        .bind(":unit", quote.unit.to_string())
+        .bind(":request", quote.request)
+        .bind(":state", quote.state.to_string())
+        .bind(":expiry", quote.expiry as i64)
+        .bind(":request_lookup_id", quote.request_lookup_id)
+        .bind(":pubkey", quote.pubkey.map(|p| p.to_string()))
+        .bind(":created_time", quote.created_time as i64)
+        .bind(":paid_time", quote.paid_time.map(|t| t as i64))
+        .bind(":issued_time", quote.issued_time.map(|t| t as i64))
+        .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, msat_to_pay,
+                created_time, paid_time
+            )
+            VALUES
+            (
+                :id, :unit, :amount, :request, :fee_reserve, :state,
+                :expiry, :payment_preimage, :request_lookup_id, :msat_to_pay,
+                :created_time, :paid_time
+            )
+        "#,
+        )
+        .bind(":id", quote.id.to_string())
+        .bind(":unit", quote.unit.to_string())
+        .bind(":amount", u64::from(quote.amount) as i64)
+        .bind(":request", quote.request)
+        .bind(":fee_reserve", u64::from(quote.fee_reserve) as i64)
+        .bind(":state", quote.state.to_string())
+        .bind(":expiry", quote.expiry as i64)
+        .bind(":payment_preimage", quote.payment_preimage)
+        .bind(":request_lookup_id", quote.request_lookup_id)
+        .bind(
+            ":msat_to_pay",
+            quote.msat_to_pay.map(|a| u64::from(a) as i64),
+        )
+        .bind(":created_time", quote.created_time as i64)
+        .bind(":paid_time", quote.paid_time.map(|t| t as i64))
+        .execute(&self.inner)
+        .await?;
+
+        Ok(())
+    }
+
+    async fn update_melt_quote_request_lookup_id(
+        &mut self,
+        quote_id: &Uuid,
+        new_request_lookup_id: &str,
+    ) -> Result<(), Self::Err> {
+        query(r#"UPDATE melt_quote SET request_lookup_id = :new_req_id WHERE id = :id"#)
+            .bind(":new_req_id", new_request_lookup_id.to_owned())
+            .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,
+    ) -> Result<(MeltQuoteState, mint::MeltQuote), Self::Err> {
+        let mut quote = query(
+            r#"
+            SELECT
+                id,
+                unit,
+                amount,
+                request,
+                fee_reserve,
+                state,
+                expiry,
+                payment_preimage,
+                request_lookup_id,
+                msat_to_pay,
+                created_time,
+                paid_time
+            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 WHERE id = :id"#)
+                .bind(":state", state.to_string())
+                .bind(":paid_time", current_time as i64)
+                .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!("SQL 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 update_mint_quote_state(
+        &mut self,
+        quote_id: &Uuid,
+        state: MintQuoteState,
+    ) -> Result<MintQuoteState, Self::Err> {
+        let quote = query(
+            r#"
+            SELECT
+                id,
+                amount,
+                unit,
+                request,
+                state,
+                expiry,
+                request_lookup_id,
+                pubkey,
+                created_time,
+                paid_time,
+                issued_time
+            FROM
+                mint_quote
+            WHERE id = :id"#,
+        )
+        .bind(":id", quote_id.as_hyphenated().to_string())
+        .fetch_one(&self.inner)
+        .await?
+        .map(sql_row_to_mint_quote)
+        .ok_or(Error::QuoteNotFound)??;
+
+        let update_query = match state {
+            MintQuoteState::Paid => {
+                r#"UPDATE mint_quote SET state = :state, paid_time = :current_time WHERE id = :quote_id"#
+            }
+            MintQuoteState::Issued => {
+                r#"UPDATE mint_quote SET state = :state, issued_time = :current_time WHERE id = :quote_id"#
+            }
+            _ => r#"UPDATE mint_quote SET state = :state WHERE id = :quote_id"#,
+        };
+
+        let current_time = unix_time();
+
+        let update = match state {
+            MintQuoteState::Paid => query(update_query)
+                .bind(":state", state.to_string())
+                .bind(":current_time", current_time as i64)
+                .bind(":quote_id", quote_id.as_hyphenated().to_string()),
+            MintQuoteState::Issued => query(update_query)
+                .bind(":state", state.to_string())
+                .bind(":current_time", current_time as i64)
+                .bind(":quote_id", quote_id.as_hyphenated().to_string()),
+            _ => query(update_query)
+                .bind(":state", state.to_string())
+                .bind(":quote_id", quote_id.as_hyphenated().to_string()),
+        };
+
+        match update.execute(&self.inner).await {
+            Ok(_) => Ok(quote.state),
+            Err(err) => {
+                tracing::error!("SQL Could not update keyset: {:?}", err);
+
+                return Err(err);
+            }
+        }
+    }
+
+    async fn get_mint_quote(&mut self, quote_id: &Uuid) -> Result<Option<MintQuote>, Self::Err> {
+        Ok(query(
+            r#"
+            SELECT
+                id,
+                amount,
+                unit,
+                request,
+                state,
+                expiry,
+                request_lookup_id,
+                pubkey,
+                created_time,
+                paid_time,
+                issued_time
+            FROM
+                mint_quote
+            WHERE id = :id"#,
+        )
+        .bind(":id", quote_id.as_hyphenated().to_string())
+        .fetch_one(&self.inner)
+        .await?
+        .map(sql_row_to_mint_quote)
+        .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,
+                state,
+                expiry,
+                payment_preimage,
+                request_lookup_id,
+                msat_to_pay,
+                created_time,
+                paid_time
+            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> {
+        Ok(query(
+            r#"
+            SELECT
+                id,
+                amount,
+                unit,
+                request,
+                state,
+                expiry,
+                request_lookup_id,
+                pubkey,
+                created_time,
+                paid_time,
+                issued_time
+            FROM
+                mint_quote
+            WHERE request = :request"#,
+        )
+        .bind(":request", request.to_owned())
+        .fetch_one(&self.inner)
+        .await?
+        .map(sql_row_to_mint_quote)
+        .transpose()?)
+    }
+}
+
+#[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> {
+        Ok(query(
+            r#"
+            SELECT
+                id,
+                amount,
+                unit,
+                request,
+                state,
+                expiry,
+                request_lookup_id,
+                pubkey,
+                created_time,
+                paid_time,
+                issued_time
+            FROM
+                mint_quote
+            WHERE id = :id"#,
+        )
+        .bind(":id", quote_id.as_hyphenated().to_string())
+        .fetch_one(&self.db)
+        .await?
+        .map(sql_row_to_mint_quote)
+        .transpose()?)
+    }
+
+    async fn get_mint_quote_by_request(
+        &self,
+        request: &str,
+    ) -> Result<Option<MintQuote>, Self::Err> {
+        Ok(query(
+            r#"
+            SELECT
+                id,
+                amount,
+                unit,
+                request,
+                state,
+                expiry,
+                request_lookup_id,
+                pubkey,
+                created_time,
+                paid_time,
+                issued_time
+            FROM
+                mint_quote
+            WHERE request = :request"#,
+        )
+        .bind(":request", request.to_owned())
+        .fetch_one(&self.db)
+        .await?
+        .map(sql_row_to_mint_quote)
+        .transpose()?)
+    }
+
+    async fn get_mint_quote_by_request_lookup_id(
+        &self,
+        request_lookup_id: &str,
+    ) -> Result<Option<MintQuote>, Self::Err> {
+        Ok(query(
+            r#"
+            SELECT
+                id,
+                amount,
+                unit,
+                request,
+                state,
+                expiry,
+                request_lookup_id,
+                pubkey,
+                created_time,
+                paid_time,
+                issued_time
+            FROM
+                mint_quote
+            WHERE request_lookup_id = :request_lookup_id"#,
+        )
+        .bind(":request_lookup_id", request_lookup_id.to_owned())
+        .fetch_one(&self.db)
+        .await?
+        .map(sql_row_to_mint_quote)
+        .transpose()?)
+    }
+
+    async fn get_mint_quotes(&self) -> Result<Vec<MintQuote>, Self::Err> {
+        Ok(query(
+            r#"
+                   SELECT
+                       id,
+                       amount,
+                       unit,
+                       request,
+                       state,
+                       expiry,
+                       request_lookup_id,
+                       pubkey,
+                       created_time,
+                       paid_time,
+                       issued_time
+                   FROM
+                       mint_quote
+                  "#,
+        )
+        .fetch_all(&self.db)
+        .await?
+        .into_iter()
+        .map(sql_row_to_mint_quote)
+        .collect::<Result<Vec<_>, _>>()?)
+    }
+
+    async fn get_mint_quotes_with_state(
+        &self,
+        state: MintQuoteState,
+    ) -> Result<Vec<MintQuote>, Self::Err> {
+        Ok(query(
+            r#"
+                   SELECT
+                       id,
+                       amount,
+                       unit,
+                       request,
+                       state,
+                       expiry,
+                       request_lookup_id,
+                       pubkey,
+                       created_time,
+                       paid_time,
+                       issued_time
+                   FROM
+                       mint_quote
+                    WHERE
+                        state = :state
+                  "#,
+        )
+        .bind(":state", state.to_string())
+        .fetch_all(&self.db)
+        .await?
+        .into_iter()
+        .map(sql_row_to_mint_quote)
+        .collect::<Result<Vec<_>, _>>()?)
+    }
+
+    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,
+                state,
+                expiry,
+                payment_preimage,
+                request_lookup_id,
+                msat_to_pay,
+                created_time,
+                paid_time
+            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,
+                state,
+                expiry,
+                payment_preimage,
+                request_lookup_id,
+                msat_to_pay,
+                created_time,
+                paid_time
+            FROM
+                melt_quote
+            "#,
+        )
+        .fetch_all(&self.db)
+        .await?
+        .into_iter()
+        .map(sql_row_to_melt_quote)
+        .collect::<Result<Vec<_>, _>>()?)
+    }
+}
+
+#[async_trait]
+impl<'a, T> 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"#)
+            .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(Error::AttemptUpdateSpentProof),
+            Some(_) => Err(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", u64::from(proof.amount) as 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(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<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),
+    })
+}
+
+fn sql_row_to_mint_quote(row: Vec<Column>) -> Result<MintQuote, Error> {
+    unpack_into!(
+        let (
+            id, amount, unit, request, state, expiry, request_lookup_id,
+            pubkey, created_time, paid_time, issued_time
+        ) = row
+    );
+
+    let request = column_as_string!(&request);
+    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.clone())
+    });
+
+    let pubkey = column_as_nullable_string!(&pubkey)
+        .map(|pk| PublicKey::from_hex(&pk))
+        .transpose()?;
+
+    let id = column_as_string!(id);
+    let amount: u64 = column_as_number!(amount);
+
+    Ok(MintQuote {
+        id: Uuid::parse_str(&id).map_err(|_| Error::InvalidUuid(id))?,
+        amount: Amount::from(amount),
+        unit: column_as_string!(unit, CurrencyUnit::from_str),
+        request,
+        state: column_as_string!(state, MintQuoteState::from_str),
+        expiry: column_as_number!(expiry),
+        request_lookup_id,
+        pubkey,
+        created_time: column_as_number!(created_time),
+        paid_time: column_as_nullable_number!(paid_time).map(|p| p),
+        issued_time: column_as_nullable_number!(issued_time).map(|p| p),
+    })
+}
+
+fn sql_row_to_melt_quote(row: Vec<Column>) -> Result<mint::MeltQuote, Error> {
+    unpack_into!(
+        let (
+            id,
+            unit,
+            amount,
+            request,
+            fee_reserve,
+            state,
+            expiry,
+            payment_preimage,
+            request_lookup_id,
+            msat_to_pay,
+            created_time,
+            paid_time
+        ) = row
+    );
+
+    let id = column_as_string!(id);
+    let amount: u64 = column_as_number!(amount);
+    let fee_reserve: u64 = column_as_number!(fee_reserve);
+
+    let request = column_as_string!(&request);
+    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.clone())
+    });
+    let msat_to_pay: Option<u64> = column_as_nullable_number!(msat_to_pay);
+
+    Ok(mint::MeltQuote {
+        id: Uuid::parse_str(&id).map_err(|_| Error::InvalidUuid(id))?,
+        amount: Amount::from(amount),
+        fee_reserve: Amount::from(fee_reserve),
+        unit: column_as_string!(unit, CurrencyUnit::from_str),
+        request,
+        payment_preimage: column_as_nullable_string!(payment_preimage),
+        msat_to_pay: msat_to_pay.map(Amount::from),
+        state: column_as_string!(state, QuoteState::from_str),
+        expiry: column_as_number!(expiry),
+        request_lookup_id,
+        created_time: column_as_number!(created_time),
+        paid_time: column_as_nullable_number!(paid_time).map(|p| p),
+    })
+}
+
+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,
+    })
+}

+ 0 - 0
crates/cdk-sqlite/src/pool.rs → crates/cdk-sql-base/src/pool.rs


+ 42 - 66
crates/cdk-sqlite/src/stmt.rs → crates/cdk-sql-base/src/stmt.rs

@@ -1,12 +1,10 @@
+//! Stataments mod
 use std::collections::HashMap;
 
-use rusqlite::{self, CachedStatement};
+use cdk_common::database::Error;
 
-use crate::common::SqliteConnectionManager;
-use crate::pool::PooledResource;
-
-/// The Value coming from SQLite
-pub type Value = rusqlite::types::Value;
+use crate::database::DatabaseExecutor;
+use crate::value::Value;
 
 /// The Column type
 pub type Column = Value;
@@ -23,10 +21,12 @@ pub enum ExpectedSqlResponse {
     AffectedRows,
     /// Return the first column of the first row
     Pluck,
+    /// Batch
+    Batch,
 }
 
 /// Sql message
-#[derive(Default, Debug)]
+#[derive(Debug, Default)]
 pub struct Statement {
     /// The SQL statement
     pub sql: String,
@@ -109,76 +109,52 @@ impl Statement {
         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 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 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()
+    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 fn execute(
-        self,
-        conn: &PooledResource<SqliteConnectionManager>,
-    ) -> rusqlite::Result<usize> {
-        self.get_stmt(conn)?.raw_execute()
+    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 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()
+    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 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)
+    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<T>(sql: T) -> Statement
+where
+    T: ToString,
+{
+    Statement::new(sql)
+}

+ 82 - 0
crates/cdk-sql-base/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-base/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

+ 36 - 0
crates/cdk-sql-base/src/wallet/migrations.rs

@@ -0,0 +1,36 @@
+/// @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"#)),
+    ("20240612132920_init.sql", include_str!(r#"./migrations/sqlite/20240612132920_init.sql"#)),
+    ("20240618200350_quote_state.sql", include_str!(r#"./migrations/sqlite/20240618200350_quote_state.sql"#)),
+    ("20240626091921_nut04_state.sql", include_str!(r#"./migrations/sqlite/20240626091921_nut04_state.sql"#)),
+    ("20240710144711_input_fee.sql", include_str!(r#"./migrations/sqlite/20240710144711_input_fee.sql"#)),
+    ("20240810214105_mint_icon_url.sql", include_str!(r#"./migrations/sqlite/20240810214105_mint_icon_url.sql"#)),
+    ("20240810233905_update_mint_url.sql", include_str!(r#"./migrations/sqlite/20240810233905_update_mint_url.sql"#)),
+    ("20240902151515_icon_url.sql", include_str!(r#"./migrations/sqlite/20240902151515_icon_url.sql"#)),
+    ("20240902210905_mint_time.sql", include_str!(r#"./migrations/sqlite/20240902210905_mint_time.sql"#)),
+    ("20241011125207_mint_urls.sql", include_str!(r#"./migrations/sqlite/20241011125207_mint_urls.sql"#)),
+    ("20241108092756_wallet_mint_quote_secretkey.sql", include_str!(r#"./migrations/sqlite/20241108092756_wallet_mint_quote_secretkey.sql"#)),
+    ("20250214135017_mint_tos.sql", include_str!(r#"./migrations/sqlite/20250214135017_mint_tos.sql"#)),
+    ("20250310111513_drop_nostr_last_checked.sql", include_str!(r#"./migrations/sqlite/20250310111513_drop_nostr_last_checked.sql"#)),
+    ("20250314082116_allow_pending_spent.sql", include_str!(r#"./migrations/sqlite/20250314082116_allow_pending_spent.sql"#)),
+    ("20250323152040_wallet_dleq_proofs.sql", include_str!(r#"./migrations/sqlite/20250323152040_wallet_dleq_proofs.sql"#)),
+    ("20250401120000_add_transactions_table.sql", include_str!(r#"./migrations/sqlite/20250401120000_add_transactions_table.sql"#)),
+    ("20250616144830_add_keyset_expiry.sql", include_str!(r#"./migrations/sqlite/20250616144830_add_keyset_expiry.sql"#)),
+];

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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


+ 1198 - 0
crates/cdk-sql-base/src/wallet/mod.rs

@@ -0,0 +1,1198 @@
+//! 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, 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<T> SQLWalletDatabase<T>
+where
+    T: DatabaseExecutor,
+{
+    /// Creates a new instance
+    pub async fn new<X>(db: X) -> Result<Self, Error>
+    where
+        X: Into<T>,
+    {
+        let db = db.into();
+        Self::migrate(&db).await?;
+        Ok(Self { db })
+    }
+
+    /// Migrate [`WalletSqliteDatabase`]
+    async fn migrate(conn: &T) -> Result<(), Error> {
+        migrate(conn, migrations::MIGRATIONS).await?;
+        Ok(())
+    }
+}
+
+#[async_trait]
+impl<T> WalletDatabase for SQLWalletDatabase<T>
+where
+    T: DatabaseExecutor,
+{
+    type Err = database::Error;
+
+    #[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 {
+            let str_query = format!(
+                r#"
+                UPDATE {table}
+                SET mint_url = :new_mint_url
+                WHERE mint_url = :old_mint_url
+            "#
+            );
+
+            query(&str_query)
+                .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)
+VALUES
+(:id, :mint_url, :amount, :unit, :request, :state, :expiry, :secret_key)
+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
+;
+        "#,
+        )
+        .bind(":id", quote.id.to_string())
+        .bind(":mint_url", quote.mint_url.to_string())
+        .bind(":amount", u64::from(quote.amount) as 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()))
+        .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
+            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
+    );
+
+    let amount: u64 = column_as_number!(amount);
+
+    Ok(MintQuote {
+        id: column_as_string!(id),
+        mint_url: column_as_string!(mint_url, MintUrl::from_str),
+        amount: Amount::from(amount),
+        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()?,
+    })
+}
+
+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(),
+    })
+}
+
+#[cfg(test)]
+mod tests {
+    use cdk_common::database::WalletDatabase;
+    use cdk_common::nuts::{ProofDleq, State};
+    use cdk_common::secret::Secret;
+
+    use crate::SQLWalletDatabase;
+
+    #[tokio::test]
+    #[cfg(feature = "sqlcipher")]
+    async fn test_sqlcipher() {
+        use cdk_common::mint_url::MintUrl;
+        use cdk_common::MintInfo;
+
+        use super::*;
+        let path = std::env::temp_dir()
+            .to_path_buf()
+            .join(format!("cdk-test-{}.sqlite", uuid::Uuid::new_v4()));
+        let db = SQLWalletDatabase::new(path, "password".to_string())
+            .await
+            .unwrap();
+
+        let mint_info = MintInfo::new().description("test");
+        let mint_url = MintUrl::from_str("https://mint.xyz").unwrap();
+
+        db.add_mint(mint_url.clone(), Some(mint_info.clone()))
+            .await
+            .unwrap();
+
+        let res = db.get_mint(mint_url).await.unwrap();
+        assert_eq!(mint_info, res.clone().unwrap());
+        assert_eq!("test", &res.unwrap().description.unwrap());
+    }
+
+    #[tokio::test]
+    async fn test_proof_with_dleq() {
+        use std::str::FromStr;
+
+        use cdk_common::common::ProofInfo;
+        use cdk_common::mint_url::MintUrl;
+        use cdk_common::nuts::{CurrencyUnit, Id, Proof, PublicKey, SecretKey};
+        use cdk_common::Amount;
+
+        // Create a temporary database
+        let path = std::env::temp_dir()
+            .to_path_buf()
+            .join(format!("cdk-test-dleq-{}.sqlite", uuid::Uuid::new_v4()));
+
+        #[cfg(feature = "sqlcipher")]
+        let db = SQLWalletDatabase::new(path, "password".to_string())
+            .await
+            .unwrap();
+
+        #[cfg(not(feature = "sqlcipher"))]
+        let db = SQLWalletDatabase::new(path).await.unwrap();
+
+        // Create a proof with DLEQ
+        let keyset_id = Id::from_str("00deadbeef123456").unwrap();
+        let mint_url = MintUrl::from_str("https://example.com").unwrap();
+        let secret = Secret::new("test_secret_for_dleq");
+
+        // Create DLEQ components
+        let e = SecretKey::generate();
+        let s = SecretKey::generate();
+        let r = SecretKey::generate();
+
+        let dleq = ProofDleq::new(e.clone(), s.clone(), r.clone());
+
+        let mut proof = Proof::new(
+            Amount::from(64),
+            keyset_id,
+            secret,
+            PublicKey::from_hex(
+                "02deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
+            )
+            .unwrap(),
+        );
+
+        // Add DLEQ to the proof
+        proof.dleq = Some(dleq);
+
+        // Create ProofInfo
+        let proof_info =
+            ProofInfo::new(proof, mint_url.clone(), State::Unspent, CurrencyUnit::Sat).unwrap();
+
+        // Store the proof in the database
+        db.update_proofs(vec![proof_info.clone()], vec![])
+            .await
+            .unwrap();
+
+        // Retrieve the proof from the database
+        let retrieved_proofs = db
+            .get_proofs(
+                Some(mint_url),
+                Some(CurrencyUnit::Sat),
+                Some(vec![State::Unspent]),
+                None,
+            )
+            .await
+            .unwrap();
+
+        // Verify we got back exactly one proof
+        assert_eq!(retrieved_proofs.len(), 1);
+
+        // Verify the DLEQ data was preserved
+        let retrieved_proof = &retrieved_proofs[0];
+        assert!(retrieved_proof.proof.dleq.is_some());
+
+        let retrieved_dleq = retrieved_proof.proof.dleq.as_ref().unwrap();
+
+        // Verify DLEQ components match what we stored
+        assert_eq!(retrieved_dleq.e.to_string(), e.to_string());
+        assert_eq!(retrieved_dleq.s.to_string(), s.to_string());
+        assert_eq!(retrieved_dleq.r.to_string(), r.to_string());
+    }
+}

+ 97 - 0
crates/cdk-sql-base/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 BLOB 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 BLOB PRIMARY KEY,
+    amount INTEGER NOT NULL,
+    keyset_id TEXT NOT NULL, -- no FK constraint here
+    secret TEXT NOT NULL,
+    c BLOB 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 BLOB PRIMARY KEY,
+    amount INTEGER NOT NULL,
+    keyset_id TEXT NOT NULL,  -- FK removed
+    c BLOB 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;

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

@@ -13,15 +13,16 @@ 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-base/mint"]
+wallet = ["cdk-common/wallet", "cdk-sql-base/wallet"]
+auth = ["cdk-common/auth", "cdk-sql-base/auth"]
 sqlcipher = ["rusqlite/bundled-sqlcipher"]
 
 [dependencies]
 async-trait.workspace = true
 cdk-common = { workspace = true, features = ["test"] }
 bitcoin.workspace = true
+cdk-sql-base = { workspace = true }
 rusqlite = { version = "0.31", features = ["bundled"]}
 thiserror.workspace = true
 tokio.workspace = true

+ 24 - 58
crates/cdk-sqlite/src/common.rs

@@ -1,9 +1,9 @@
 use std::sync::Arc;
 use std::time::Duration;
 
-use rusqlite::{params, Connection};
-
-use crate::pool::{Pool, ResourceManager};
+use cdk_sql_base::pool::{self, Pool, ResourceManager};
+use cdk_sql_base::value::Value;
+use rusqlite::Connection;
 
 /// The config need to create a new SQLite connection
 #[derive(Debug)]
@@ -23,9 +23,7 @@ impl ResourceManager for SqliteConnectionManager {
 
     type Error = rusqlite::Error;
 
-    fn new_resource(
-        config: &Self::Config,
-    ) -> Result<Self::Resource, crate::pool::Error<Self::Error>> {
+    fn new_resource(config: &Self::Config) -> Result<Self::Resource, pool::Error<Self::Error>> {
         let conn = if let Some(path) = config.path.as_ref() {
             Connection::open(path)?
         } else {
@@ -57,14 +55,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 +78,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_base::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_base::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;

+ 308 - 153
crates/cdk-sqlite/src/mint/async_rusqlite.rs

@@ -1,16 +1,20 @@
+//! Async and concurrent rusqlite
 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_base::database::{DatabaseConnector, DatabaseExecutor, DatabaseTransaction};
+use cdk_sql_base::pool::{self, Pool, PooledResource};
+use cdk_sql_base::stmt::{Column, ExpectedSqlResponse, Statement as InnerStatement};
+use cdk_sql_base::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,51 @@ 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),
+        ))
+    }
+}
+
 /// 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 +81,81 @@ 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("Invalid usage")]
+    InvalidUsage,
 
-    /// 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, sql: 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();
+    let total_args = args.len();
 
     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()))
+                .ok_or(ConversionError::MissingParameter(name.to_owned()))
         }) {
             value?
         } else {
             continue;
         };
 
-        stmt.raw_bind_parameter(index, value)?;
+        stmt.raw_bind_parameter(index, to_sqlite(value))?;
     }
 
     let columns = stmt.column_count();
 
     let to_return = match sql.expected_response {
         ExpectedSqlResponse::AffectedRows => DbResponse::AffectedRows(stmt.raw_execute()?),
+        ExpectedSqlResponse::Batch => {
+            if total_args > 0 {
+                return Err(SqliteError::InvalidUsage);
+            }
+
+            conn.execute_batch(&sql.sql)?;
+
+            DbResponse::Ok
+        }
         ExpectedSqlResponse::ManyRows => {
             let mut rows = stmt.raw_query();
             let mut results = vec![];
@@ -133,7 +163,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 +172,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 +184,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()?;
@@ -202,7 +236,7 @@ fn rusqlite_spawn_worker_threads(
                     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 +248,7 @@ fn rusqlite_spawn_worker_threads(
                                 && (*extended_code == ffi::SQLITE_CONSTRAINT_PRIMARYKEY
                                     || *extended_code == ffi::SQLITE_CONSTRAINT_UNIQUE)
                             {
-                                Error::Duplicate
+                                SqliteError::Duplicate
                             } else {
                                 err
                             }
@@ -351,7 +385,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 +399,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 +429,167 @@ 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),
         }
     }
+}
 
-    /// Runs the query and returns the first row or None
+#[async_trait::async_trait]
+impl DatabaseExecutor for AsyncRusqlite {
     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<'conn> Transaction<'conn> {
+    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 +597,132 @@ 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()
+    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 - 110
crates/cdk-sqlite/src/mint/error.rs

@@ -1,110 +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,
-    /// Proof not found
-    #[error("Proof not found")]
-    ProofNotFound,
-    /// Invalid keyset ID
-    #[error("Invalid keyset ID")]
-    InvalidKeysetId,
-}
-
-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 - 24
crates/cdk-sqlite/src/mint/migrations.rs

@@ -1,24 +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"#)),
-];

+ 9 - 1635
crates/cdk-sqlite/src/mint/mod.rs

@@ -1,1654 +1,28 @@
 //! SQLite Mint
 
-use std::collections::HashMap;
-use std::ops::DerefMut;
-use std::path::Path;
-use std::str::FromStr;
-
-use async_rusqlite::{query, DatabaseExecutor, Transaction};
-use async_trait::async_trait;
-use bitcoin::bip32::DerivationPath;
-use cdk_common::common::QuoteTTL;
-use cdk_common::database::{
-    self, MintDatabase, MintDbWriterFinalizer, MintKeyDatabaseTransaction, MintKeysDatabase,
-    MintProofsDatabase, MintProofsTransaction, MintQuotesDatabase, MintQuotesTransaction,
-    MintSignatureTransaction, MintSignaturesDatabase,
-};
-use cdk_common::mint::{self, MintKeySetInfo, MintQuote};
-use cdk_common::nut00::ProofsMethods;
-use cdk_common::nut05::QuoteState;
-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,
-    MintQuoteState, Proof, Proofs, PublicKey, SecretKey, State,
-};
-use error::Error;
-use lightning_invoice::Bolt11Invoice;
-use uuid::Uuid;
-
-use crate::common::{create_sqlite_pool, migrate};
-use crate::stmt::Column;
-use crate::{
-    column_as_nullable_number, column_as_nullable_string, column_as_number, column_as_string,
-    unpack_into,
-};
+use cdk_sql_base::mint::SQLMintAuthDatabase;
+use cdk_sql_base::SQLMintDatabase;
 
 mod async_rusqlite;
-#[cfg(feature = "auth")]
-mod auth;
-pub mod error;
+
 pub mod memory;
 
-#[rustfmt::skip]
-mod migrations;
+/// Mint SQLite implementation with rusqlite
+pub type MintSqliteDatabase = SQLMintDatabase<async_rusqlite::AsyncRusqlite>;
 
+/// Mint Auth database with rusqlite
 #[cfg(feature = "auth")]
-pub use auth::MintSqliteAuthDatabase;
-
-/// Mint SQLite Database
-#[derive(Debug, Clone)]
-pub struct MintSqliteDatabase {
-    pool: async_rusqlite::AsyncRusqlite,
-}
-
-#[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<T, C>(conn: &C, id: &str, value: &T) -> Result<(), Error>
-where
-    T: ?Sized + serde::Serialize,
-    C: DatabaseExecutor + Send + Sync,
-{
-    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 MintSqliteDatabase {
-    /// Create new [`MintSqliteDatabase`]
-    #[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: async_rusqlite::AsyncRusqlite::new(pool),
-        })
-    }
-
-    /// Create new [`MintSqliteDatabase`]
-    #[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: async_rusqlite::AsyncRusqlite::new(pool),
-        })
-    }
-
-    #[inline(always)]
-    async fn fetch_from_config<T>(&self, id: &str) -> Result<T, Error>
-    where
-        T: 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.pool)
-            .await?
-            .ok_or::<Error>(Error::UnknownQuoteTTL)?);
-
-        Ok(serde_json::from_str(&value)?)
-    }
-}
-
-/// Sqlite Writer
-pub struct SqliteTransaction<'a> {
-    inner: Transaction<'a>,
-}
-
-#[async_trait]
-impl<'a> database::MintTransaction<'a, database::Error> for SqliteTransaction<'a> {
-    async fn set_mint_info(&mut self, mint_info: MintInfo) -> Result<(), database::Error> {
-        Ok(set_to_config(&self.inner, "mint_info", &mint_info).await?)
-    }
-
-    async fn set_quote_ttl(&mut self, quote_ttl: QuoteTTL) -> Result<(), database::Error> {
-        Ok(set_to_config(&self.inner, "quote_ttl", &quote_ttl).await?)
-    }
-}
-
-#[async_trait]
-impl MintDbWriterFinalizer for SqliteTransaction<'_> {
-    type Err = database::Error;
-
-    async fn commit(self: Box<Self>) -> Result<(), database::Error> {
-        Ok(self.inner.commit().await?)
-    }
-
-    async fn rollback(self: Box<Self>) -> Result<(), database::Error> {
-        Ok(self.inner.rollback().await?)
-    }
-}
-
-#[async_trait]
-impl<'a> MintKeyDatabaseTransaction<'a, database::Error> for SqliteTransaction<'a> {
-    async fn add_keyset_info(&mut self, keyset: MintKeySetInfo) -> Result<(), database::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<(), database::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 MintKeysDatabase for MintSqliteDatabase {
-    type Err = database::Error;
-
-    async fn begin_transaction<'a>(
-        &'a self,
-    ) -> Result<
-        Box<dyn MintKeyDatabaseTransaction<'a, database::Error> + Send + Sync + 'a>,
-        database::Error,
-    > {
-        Ok(Box::new(SqliteTransaction {
-            inner: self.pool.begin().await?,
-        }))
-    }
-
-    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.pool)
-                .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.pool)
-            .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.pool)
-        .await?
-        .map(sqlite_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.pool)
-        .await?
-        .into_iter()
-        .map(sqlite_row_to_keyset_info)
-        .collect::<Result<Vec<_>, _>>()?)
-    }
-}
-
-#[async_trait]
-impl<'a> MintQuotesTransaction<'a> for SqliteTransaction<'a> {
-    type Err = database::Error;
-
-    async fn add_or_replace_mint_quote(&mut self, quote: MintQuote) -> Result<(), Self::Err> {
-        query(
-            r#"
-                INSERT OR REPLACE INTO mint_quote (
-                    id, amount, unit, request, state, expiry, request_lookup_id,
-                    pubkey, created_time, paid_time, issued_time
-                )
-                VALUES (
-                    :id, :amount, :unit, :request, :state, :expiry, :request_lookup_id,
-                    :pubkey, :created_time, :paid_time, :issued_time
-                )
-            "#,
-        )
-        .bind(":id", quote.id.to_string())
-        .bind(":amount", u64::from(quote.amount) as i64)
-        .bind(":unit", quote.unit.to_string())
-        .bind(":request", quote.request)
-        .bind(":state", quote.state.to_string())
-        .bind(":expiry", quote.expiry as i64)
-        .bind(":request_lookup_id", quote.request_lookup_id)
-        .bind(":pubkey", quote.pubkey.map(|p| p.to_string()))
-        .bind(":created_time", quote.created_time as i64)
-        .bind(":paid_time", quote.paid_time.map(|t| t as i64))
-        .bind(":issued_time", quote.issued_time.map(|t| t as i64))
-        .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, msat_to_pay,
-                created_time, paid_time
-            )
-            VALUES
-            (
-                :id, :unit, :amount, :request, :fee_reserve, :state,
-                :expiry, :payment_preimage, :request_lookup_id, :msat_to_pay,
-                :created_time, :paid_time
-            )
-        "#,
-        )
-        .bind(":id", quote.id.to_string())
-        .bind(":unit", quote.unit.to_string())
-        .bind(":amount", u64::from(quote.amount) as i64)
-        .bind(":request", quote.request)
-        .bind(":fee_reserve", u64::from(quote.fee_reserve) as i64)
-        .bind(":state", quote.state.to_string())
-        .bind(":expiry", quote.expiry as i64)
-        .bind(":payment_preimage", quote.payment_preimage)
-        .bind(":request_lookup_id", quote.request_lookup_id)
-        .bind(
-            ":msat_to_pay",
-            quote.msat_to_pay.map(|a| u64::from(a) as i64),
-        )
-        .bind(":created_time", quote.created_time as i64)
-        .bind(":paid_time", quote.paid_time.map(|t| t as i64))
-        .execute(&self.inner)
-        .await?;
-
-        Ok(())
-    }
-
-    async fn update_melt_quote_request_lookup_id(
-        &mut self,
-        quote_id: &Uuid,
-        new_request_lookup_id: &str,
-    ) -> Result<(), Self::Err> {
-        query(r#"UPDATE melt_quote SET request_lookup_id = :new_req_id WHERE id = :id"#)
-            .bind(":new_req_id", new_request_lookup_id.to_owned())
-            .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,
-    ) -> Result<(MeltQuoteState, mint::MeltQuote), Self::Err> {
-        let mut quote = query(
-            r#"
-            SELECT
-                id,
-                unit,
-                amount,
-                request,
-                fee_reserve,
-                state,
-                expiry,
-                payment_preimage,
-                request_lookup_id,
-                msat_to_pay,
-                created_time,
-                paid_time
-            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(sqlite_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 WHERE id = :id"#)
-                .bind(":state", state.to_string())
-                .bind(":paid_time", current_time as i64)
-                .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.into());
-            }
-        };
-
-        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 update_mint_quote_state(
-        &mut self,
-        quote_id: &Uuid,
-        state: MintQuoteState,
-    ) -> Result<MintQuoteState, Self::Err> {
-        let quote = query(
-            r#"
-            SELECT
-                id,
-                amount,
-                unit,
-                request,
-                state,
-                expiry,
-                request_lookup_id,
-                pubkey,
-                created_time,
-                paid_time,
-                issued_time
-            FROM
-                mint_quote
-            WHERE id = :id"#,
-        )
-        .bind(":id", quote_id.as_hyphenated().to_string())
-        .fetch_one(&self.inner)
-        .await?
-        .map(sqlite_row_to_mint_quote)
-        .ok_or(Error::QuoteNotFound)??;
-
-        let update_query = match state {
-            MintQuoteState::Paid => {
-                r#"UPDATE mint_quote SET state = :state, paid_time = :current_time WHERE id = :quote_id"#
-            }
-            MintQuoteState::Issued => {
-                r#"UPDATE mint_quote SET state = :state, issued_time = :current_time WHERE id = :quote_id"#
-            }
-            _ => r#"UPDATE mint_quote SET state = :state WHERE id = :quote_id"#,
-        };
-
-        let current_time = unix_time();
-
-        let update = match state {
-            MintQuoteState::Paid => query(update_query)
-                .bind(":state", state.to_string())
-                .bind(":current_time", current_time as i64)
-                .bind(":quote_id", quote_id.as_hyphenated().to_string()),
-            MintQuoteState::Issued => query(update_query)
-                .bind(":state", state.to_string())
-                .bind(":current_time", current_time as i64)
-                .bind(":quote_id", quote_id.as_hyphenated().to_string()),
-            _ => query(update_query)
-                .bind(":state", state.to_string())
-                .bind(":quote_id", quote_id.as_hyphenated().to_string()),
-        };
-
-        match update.execute(&self.inner).await {
-            Ok(_) => Ok(quote.state),
-            Err(err) => {
-                tracing::error!("SQLite Could not update keyset: {:?}", err);
-
-                return Err(err.into());
-            }
-        }
-    }
-
-    async fn get_mint_quote(&mut self, quote_id: &Uuid) -> Result<Option<MintQuote>, Self::Err> {
-        Ok(query(
-            r#"
-            SELECT
-                id,
-                amount,
-                unit,
-                request,
-                state,
-                expiry,
-                request_lookup_id,
-                pubkey,
-                created_time,
-                paid_time,
-                issued_time
-            FROM
-                mint_quote
-            WHERE id = :id"#,
-        )
-        .bind(":id", quote_id.as_hyphenated().to_string())
-        .fetch_one(&self.inner)
-        .await?
-        .map(sqlite_row_to_mint_quote)
-        .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,
-                state,
-                expiry,
-                payment_preimage,
-                request_lookup_id,
-                msat_to_pay,
-                created_time,
-                paid_time
-            FROM
-                melt_quote
-            WHERE
-                id=:id
-            "#,
-        )
-        .bind(":id", quote_id.as_hyphenated().to_string())
-        .fetch_one(&self.inner)
-        .await?
-        .map(sqlite_row_to_melt_quote)
-        .transpose()?)
-    }
-
-    async fn get_mint_quote_by_request(
-        &mut self,
-        request: &str,
-    ) -> Result<Option<MintQuote>, Self::Err> {
-        Ok(query(
-            r#"
-            SELECT
-                id,
-                amount,
-                unit,
-                request,
-                state,
-                expiry,
-                request_lookup_id,
-                pubkey,
-                created_time,
-                paid_time,
-                issued_time
-            FROM
-                mint_quote
-            WHERE request = :request"#,
-        )
-        .bind(":request", request.to_owned())
-        .fetch_one(&self.inner)
-        .await?
-        .map(sqlite_row_to_mint_quote)
-        .transpose()?)
-    }
-}
-
-#[async_trait]
-impl MintQuotesDatabase for MintSqliteDatabase {
-    type Err = database::Error;
-
-    async fn get_mint_quote(&self, quote_id: &Uuid) -> Result<Option<MintQuote>, Self::Err> {
-        Ok(query(
-            r#"
-            SELECT
-                id,
-                amount,
-                unit,
-                request,
-                state,
-                expiry,
-                request_lookup_id,
-                pubkey,
-                created_time,
-                paid_time,
-                issued_time
-            FROM
-                mint_quote
-            WHERE id = :id"#,
-        )
-        .bind(":id", quote_id.as_hyphenated().to_string())
-        .fetch_one(&self.pool)
-        .await?
-        .map(sqlite_row_to_mint_quote)
-        .transpose()?)
-    }
-
-    async fn get_mint_quote_by_request(
-        &self,
-        request: &str,
-    ) -> Result<Option<MintQuote>, Self::Err> {
-        Ok(query(
-            r#"
-            SELECT
-                id,
-                amount,
-                unit,
-                request,
-                state,
-                expiry,
-                request_lookup_id,
-                pubkey,
-                created_time,
-                paid_time,
-                issued_time
-            FROM
-                mint_quote
-            WHERE request = :request"#,
-        )
-        .bind(":request", request.to_owned())
-        .fetch_one(&self.pool)
-        .await?
-        .map(sqlite_row_to_mint_quote)
-        .transpose()?)
-    }
-
-    async fn get_mint_quote_by_request_lookup_id(
-        &self,
-        request_lookup_id: &str,
-    ) -> Result<Option<MintQuote>, Self::Err> {
-        Ok(query(
-            r#"
-            SELECT
-                id,
-                amount,
-                unit,
-                request,
-                state,
-                expiry,
-                request_lookup_id,
-                pubkey,
-                created_time,
-                paid_time,
-                issued_time
-            FROM
-                mint_quote
-            WHERE request_lookup_id = :request_lookup_id"#,
-        )
-        .bind(":request_lookup_id", request_lookup_id.to_owned())
-        .fetch_one(&self.pool)
-        .await?
-        .map(sqlite_row_to_mint_quote)
-        .transpose()?)
-    }
-
-    async fn get_mint_quotes(&self) -> Result<Vec<MintQuote>, Self::Err> {
-        Ok(query(
-            r#"
-                   SELECT
-                       id,
-                       amount,
-                       unit,
-                       request,
-                       state,
-                       expiry,
-                       request_lookup_id,
-                       pubkey,
-                       created_time,
-                       paid_time,
-                       issued_time
-                   FROM
-                       mint_quote
-                  "#,
-        )
-        .fetch_all(&self.pool)
-        .await?
-        .into_iter()
-        .map(sqlite_row_to_mint_quote)
-        .collect::<Result<Vec<_>, _>>()?)
-    }
-
-    async fn get_mint_quotes_with_state(
-        &self,
-        state: MintQuoteState,
-    ) -> Result<Vec<MintQuote>, Self::Err> {
-        Ok(query(
-            r#"
-                   SELECT
-                       id,
-                       amount,
-                       unit,
-                       request,
-                       state,
-                       expiry,
-                       request_lookup_id,
-                       pubkey,
-                       created_time,
-                       paid_time,
-                       issued_time
-                   FROM
-                       mint_quote
-                    WHERE
-                        state = :state
-                  "#,
-        )
-        .bind(":state", state.to_string())
-        .fetch_all(&self.pool)
-        .await?
-        .into_iter()
-        .map(sqlite_row_to_mint_quote)
-        .collect::<Result<Vec<_>, _>>()?)
-    }
-
-    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,
-                state,
-                expiry,
-                payment_preimage,
-                request_lookup_id,
-                msat_to_pay,
-                created_time,
-                paid_time
-            FROM
-                melt_quote
-            WHERE
-                id=:id
-            "#,
-        )
-        .bind(":id", quote_id.as_hyphenated().to_string())
-        .fetch_one(&self.pool)
-        .await?
-        .map(sqlite_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,
-                state,
-                expiry,
-                payment_preimage,
-                request_lookup_id,
-                msat_to_pay,
-                created_time,
-                paid_time
-            FROM
-                melt_quote
-            "#,
-        )
-        .fetch_all(&self.pool)
-        .await?
-        .into_iter()
-        .map(sqlite_row_to_melt_quote)
-        .collect::<Result<Vec<_>, _>>()?)
-    }
-}
-
-#[async_trait]
-impl<'a> MintProofsTransaction<'a> for SqliteTransaction<'a> {
-    type Err = database::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"#)
-            .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", u64::from(proof.amount) as 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 MintProofsDatabase for MintSqliteDatabase {
-    type Err = database::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.pool)
-        .await?
-        .into_iter()
-        .map(|mut row| {
-            Ok((
-                column_as_string!(
-                    row.pop().ok_or(Error::InvalidDbPath)?,
-                    PublicKey::from_hex,
-                    PublicKey::from_slice
-                ),
-                sqlite_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.pool)
-        .await?
-        .into_iter()
-        .map(sqlite_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.pool, 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.pool)
-        .await?
-        .into_iter()
-        .map(sqlite_row_to_proof_with_state)
-        .collect::<Result<Vec<_>, _>>()?
-        .into_iter()
-        .unzip())
-    }
-}
-
-#[async_trait]
-impl<'a> MintSignatureTransaction<'a> for SqliteTransaction<'a> {
-    type Err = database::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
-                ),
-                sqlite_row_to_blind_signature(row)?,
-            ))
-        })
-        .collect::<Result<HashMap<_, _>, Error>>()?;
-        Ok(blinded_messages
-            .iter()
-            .map(|y| blinded_signatures.remove(y))
-            .collect())
-    }
-}
-
-#[async_trait]
-impl MintSignaturesDatabase for MintSqliteDatabase {
-    type Err = database::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.pool)
-        .await?
-        .into_iter()
-        .map(|mut row| {
-            Ok((
-                column_as_string!(
-                    &row.pop().ok_or(Error::InvalidDbResponse)?,
-                    PublicKey::from_hex,
-                    PublicKey::from_slice
-                ),
-                sqlite_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.pool)
-        .await?
-        .into_iter()
-        .map(sqlite_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.pool)
-        .await?
-        .into_iter()
-        .map(sqlite_row_to_blind_signature)
-        .collect::<Result<Vec<BlindSignature>, _>>()?)
-    }
-}
-
-#[async_trait]
-impl MintDatabase<database::Error> for MintSqliteDatabase {
-    async fn begin_transaction<'a>(
-        &'a self,
-    ) -> Result<
-        Box<dyn database::MintTransaction<'a, database::Error> + Send + Sync + 'a>,
-        database::Error,
-    > {
-        Ok(Box::new(SqliteTransaction {
-            inner: self.pool.begin().await?,
-        }))
-    }
-
-    async fn get_mint_info(&self) -> Result<MintInfo, database::Error> {
-        Ok(self.fetch_from_config("mint_info").await?)
-    }
-
-    async fn get_quote_ttl(&self) -> Result<QuoteTTL, database::Error> {
-        Ok(self.fetch_from_config("quote_ttl").await?)
-    }
-}
-
-fn sqlite_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),
-    })
-}
-
-fn sqlite_row_to_mint_quote(row: Vec<Column>) -> Result<MintQuote, Error> {
-    unpack_into!(
-        let (
-            id, amount, unit, request, state, expiry, request_lookup_id,
-            pubkey, created_time, paid_time, issued_time
-        ) = row
-    );
-
-    let request = column_as_string!(&request);
-    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.clone())
-    });
-
-    let pubkey = column_as_nullable_string!(&pubkey)
-        .map(|pk| PublicKey::from_hex(&pk))
-        .transpose()?;
-
-    let id = column_as_string!(id);
-    let amount: u64 = column_as_number!(amount);
-
-    Ok(MintQuote {
-        id: Uuid::parse_str(&id).map_err(|_| Error::InvalidUuid(id))?,
-        amount: Amount::from(amount),
-        unit: column_as_string!(unit, CurrencyUnit::from_str),
-        request,
-        state: column_as_string!(state, MintQuoteState::from_str),
-        expiry: column_as_number!(expiry),
-        request_lookup_id,
-        pubkey,
-        created_time: column_as_number!(created_time),
-        paid_time: column_as_nullable_number!(paid_time).map(|p| p),
-        issued_time: column_as_nullable_number!(issued_time).map(|p| p),
-    })
-}
-
-fn sqlite_row_to_melt_quote(row: Vec<Column>) -> Result<mint::MeltQuote, Error> {
-    unpack_into!(
-        let (
-            id,
-            unit,
-            amount,
-            request,
-            fee_reserve,
-            state,
-            expiry,
-            payment_preimage,
-            request_lookup_id,
-            msat_to_pay,
-            created_time,
-            paid_time
-        ) = row
-    );
-
-    let id = column_as_string!(id);
-    let amount: u64 = column_as_number!(amount);
-    let fee_reserve: u64 = column_as_number!(fee_reserve);
-
-    let request = column_as_string!(&request);
-    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.clone())
-    });
-    let msat_to_pay: Option<u64> = column_as_nullable_number!(msat_to_pay);
-
-    Ok(mint::MeltQuote {
-        id: Uuid::parse_str(&id).map_err(|_| Error::InvalidUuid(id))?,
-        amount: Amount::from(amount),
-        fee_reserve: Amount::from(fee_reserve),
-        unit: column_as_string!(unit, CurrencyUnit::from_str),
-        request,
-        payment_preimage: column_as_nullable_string!(payment_preimage),
-        msat_to_pay: msat_to_pay.map(Amount::from),
-        state: column_as_string!(state, QuoteState::from_str),
-        expiry: column_as_number!(expiry),
-        request_lookup_id,
-        created_time: column_as_number!(created_time),
-        paid_time: column_as_nullable_number!(paid_time).map(|p| p),
-    })
-}
-
-fn sqlite_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 sqlite_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 sqlite_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,
-    })
-}
+pub type MintSqliteAuthDatabase = SQLMintAuthDatabase<async_rusqlite::AsyncRusqlite>;
 
 #[cfg(test)]
-mod tests {
-    use std::fs::remove_file;
-
-    use cdk_common::mint::MintKeySetInfo;
-    use cdk_common::{mint_db_test, Amount};
+mod test {
+    use cdk_common::mint_db_test;
 
     use super::*;
 
-    #[tokio::test]
-    async fn test_remove_spent_proofs() {
-        let db = memory::empty().await.unwrap();
-
-        // Create a keyset and add it to the database
-        let keyset_id = Id::from_str("00916bbf7ef91a36").unwrap();
-        let keyset_info = MintKeySetInfo {
-            id: keyset_id,
-            unit: CurrencyUnit::Sat,
-            active: true,
-            valid_from: 0,
-            derivation_path: bitcoin::bip32::DerivationPath::from_str("m/0'/0'/0'").unwrap(),
-            derivation_path_index: Some(0),
-            max_order: 32,
-            input_fee_ppk: 0,
-            final_expiry: None,
-        };
-        let mut tx = MintKeysDatabase::begin_transaction(&db).await.unwrap();
-        tx.add_keyset_info(keyset_info).await.unwrap();
-        tx.commit().await.unwrap();
-
-        let proofs = vec![
-            Proof {
-                amount: Amount::from(100),
-                keyset_id,
-                secret: Secret::generate(),
-                c: SecretKey::generate().public_key(),
-                witness: None,
-                dleq: None,
-            },
-            Proof {
-                amount: Amount::from(200),
-                keyset_id,
-                secret: Secret::generate(),
-                c: SecretKey::generate().public_key(),
-                witness: None,
-                dleq: None,
-            },
-        ];
-
-        // Add proofs to database
-        let mut tx = MintDatabase::begin_transaction(&db).await.unwrap();
-        tx.add_proofs(proofs.clone(), None).await.unwrap();
-
-        // Mark one proof as spent
-        tx.update_proofs_states(&[proofs[0].y().unwrap()], State::Spent)
-            .await
-            .unwrap();
-
-        tx.commit().await.unwrap();
-
-        // Verify both proofs still exist
-        let states = db
-            .get_proofs_states(&[proofs[0].y().unwrap(), proofs[1].y().unwrap()])
-            .await
-            .unwrap();
-
-        assert_eq!(states.len(), 2);
-        assert_eq!(states[0], Some(State::Spent));
-        assert_eq!(states[1], Some(State::Unspent));
-    }
-
-    #[tokio::test]
-    async fn test_update_spent_proofs() {
-        let db = memory::empty().await.unwrap();
-
-        // Create a keyset and add it to the database
-        let keyset_id = Id::from_str("00916bbf7ef91a36").unwrap();
-        let keyset_info = MintKeySetInfo {
-            id: keyset_id,
-            unit: CurrencyUnit::Sat,
-            active: true,
-            valid_from: 0,
-            derivation_path: bitcoin::bip32::DerivationPath::from_str("m/0'/0'/0'").unwrap(),
-            derivation_path_index: Some(0),
-            max_order: 32,
-            input_fee_ppk: 0,
-            final_expiry: None,
-        };
-        let mut tx = MintKeysDatabase::begin_transaction(&db)
-            .await
-            .expect("begin");
-        tx.add_keyset_info(keyset_info).await.unwrap();
-        tx.commit().await.expect("commit");
-
-        let proofs = vec![
-            Proof {
-                amount: Amount::from(100),
-                keyset_id,
-                secret: Secret::generate(),
-                c: SecretKey::generate().public_key(),
-                witness: None,
-                dleq: None,
-            },
-            Proof {
-                amount: Amount::from(200),
-                keyset_id,
-                secret: Secret::generate(),
-                c: SecretKey::generate().public_key(),
-                witness: None,
-                dleq: None,
-            },
-        ];
-
-        // Add proofs to database
-        let mut tx = MintDatabase::begin_transaction(&db).await.unwrap();
-        tx.add_proofs(proofs.clone(), None).await.unwrap();
-
-        // Mark one proof as spent
-        tx.update_proofs_states(&[proofs[0].y().unwrap()], State::Spent)
-            .await
-            .unwrap();
-
-        // Try to update both proofs - should fail because one is spent
-        let result = tx
-            .update_proofs_states(&[proofs[0].y().unwrap()], State::Unspent)
-            .await;
-
-        tx.commit().await.unwrap();
-
-        assert!(result.is_err());
-        assert!(matches!(
-            result.unwrap_err(),
-            database::Error::AttemptUpdateSpentProof
-        ));
-
-        // Verify states haven't changed
-        let states = db
-            .get_proofs_states(&[proofs[0].y().unwrap(), proofs[1].y().unwrap()])
-            .await
-            .unwrap();
-
-        assert_eq!(states.len(), 2);
-        assert_eq!(states[0], Some(State::Spent));
-        assert_eq!(states[1], Some(State::Unspent));
-    }
-
     async fn provide_db() -> MintSqliteDatabase {
         memory::empty().await.unwrap()
     }
 
     mint_db_test!(provide_db);
-
-    #[tokio::test]
-    async fn open_legacy_and_migrate() {
-        let file = format!(
-            "{}/db.sqlite",
-            std::env::temp_dir().to_str().unwrap_or_default()
-        );
-
-        {
-            let _ = remove_file(&file);
-            #[cfg(not(feature = "sqlcipher"))]
-            let legacy = create_sqlite_pool(&file);
-            #[cfg(feature = "sqlcipher")]
-            let legacy = create_sqlite_pool(&file, "test".to_owned());
-            let y = legacy.get().expect("pool");
-            y.execute_batch(include_str!("../../tests/legacy-sqlx.sql"))
-                .expect("create former db failed");
-        }
-
-        #[cfg(not(feature = "sqlcipher"))]
-        let conn = MintSqliteDatabase::new(&file).await;
-
-        #[cfg(feature = "sqlcipher")]
-        let conn = MintSqliteDatabase::new(&file, "test".to_owned()).await;
-
-        assert!(conn.is_ok(), "Failed with {:?}", conn.unwrap_err());
-
-        let _ = remove_file(&file);
-    }
 }

+ 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 - 20
crates/cdk-sqlite/src/wallet/migrations.rs

@@ -1,20 +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"#)),
-];

Fișier diff suprimat deoarece este prea mare
+ 125 - 1059
crates/cdk-sqlite/src/wallet/mod.rs


Unele fișiere nu au fost afișate deoarece prea multe fișiere au fost modificate în acest diff