浏览代码

Introduce `cdk-sql-common`

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

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

+ 1 - 0
Cargo.toml

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

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

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

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

@@ -19,6 +19,80 @@ pub use mint::{MintAuthDatabase, MintAuthTransaction};
 #[cfg(feature = "wallet")]
 #[cfg(feature = "wallet")]
 pub use wallet::Database as WalletDatabase;
 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
 /// CDK_database error
 #[derive(Debug, thiserror::Error)]
 #[derive(Debug, thiserror::Error)]
 pub enum Error {
 pub enum Error {
@@ -39,6 +113,9 @@ pub enum Error {
     /// NUT00 Error
     /// NUT00 Error
     #[error(transparent)]
     #[error(transparent)]
     NUT00(#[from] crate::nuts::nut00::Error),
     NUT00(#[from] crate::nuts::nut00::Error),
+    /// NUT01 Error
+    #[error(transparent)]
+    NUT01(#[from] crate::nuts::nut01::Error),
     /// NUT02 Error
     /// NUT02 Error
     #[error(transparent)]
     #[error(transparent)]
     NUT02(#[from] crate::nuts::nut02::Error),
     NUT02(#[from] crate::nuts::nut02::Error),
@@ -68,6 +145,38 @@ pub enum Error {
     /// Invalid state transition
     /// Invalid state transition
     #[error("Invalid state transition")]
     #[error("Invalid state transition")]
     InvalidStateTransition(crate::state::Error),
     InvalidStateTransition(crate::state::Error),
+
+    /// Invalid connection settings
+    #[error("Invalid credentials {0}")]
+    InvalidConnectionSettings(String),
+
+    /// Unexpected database response
+    #[error("Invalid database response")]
+    InvalidDbResponse,
+
+    /// Internal error
+    #[error("Internal {0}")]
+    Internal(String),
+
+    /// Data conversion error
+    #[error(transparent)]
+    Conversion(#[from] ConversionError),
+
+    /// Missing Placeholder value
+    #[error("Missing placeholder value {0}")]
+    MissingPlaceholder(String),
+
+    /// Unknown quote ttl
+    #[error("Unknown quote ttl")]
+    UnknownQuoteTTL,
+
+    /// Invalid UUID
+    #[error("Invalid UUID: {0}")]
+    InvalidUuid(String),
+
+    /// QuoteNotFound
+    #[error("Quote not found")]
+    QuoteNotFound,
 }
 }
 
 
 #[cfg(feature = "mint")]
 #[cfg(feature = "mint")]

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

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

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

@@ -212,7 +212,7 @@ async fn setup_sqlite_database(
     #[cfg(feature = "sqlcipher")]
     #[cfg(feature = "sqlcipher")]
     let db = {
     let db = {
         // Get password from command line arguments for sqlcipher
         // 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))
     Ok(Arc::new(db))
 }
 }
@@ -487,7 +487,7 @@ async fn setup_authentication(
                 #[cfg(feature = "sqlcipher")]
                 #[cfg(feature = "sqlcipher")]
                 let password = CLIArgs::parse().password;
                 let password = CLIArgs::parse().password;
                 #[cfg(feature = "sqlcipher")]
                 #[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"))]
                 #[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)
                 Arc::new(sqlite_db)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@@ -1,4 +1,4 @@
-//! Collection of macros to generate code to digest data from SQLite
+//! Collection of macros to generate code to digest data from a generic SQL databasex
 
 
 /// Unpacks a vector of Column, and consumes it, parsing into individual variables, checking the
 /// Unpacks a vector of Column, and consumes it, parsing into individual variables, checking the
 /// vector is big enough.
 /// vector is big enough.
@@ -10,9 +10,9 @@ macro_rules! unpack_into {
             vec.reverse();
             vec.reverse();
             let required = 0 $(+ {let _ = stringify!($var); 1})+;
             let required = 0 $(+ {let _ = stringify!($var); 1})+;
             if vec.len() < required {
             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)))
                     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_export]
 macro_rules! column_as_nullable_string {
 macro_rules! column_as_nullable_string {
     ($col:expr, $callback_str:expr, $callback_bytes:expr) => {
     ($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::Text(text) => Ok(Some(text).and_then($callback_str)),
             $crate::stmt::Column::Blob(bytes) => Ok(Some(bytes).and_then($callback_bytes)),
             $crate::stmt::Column::Blob(bytes) => Ok(Some(bytes).and_then($callback_bytes)),
             $crate::stmt::Column::Null => Ok(None),
             $crate::stmt::Column::Null => Ok(None),
-            other => Err(Error::InvalidType(
+            _ => Err($crate::ConversionError::InvalidType(
                 "String".to_owned(),
                 "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))
                 Ok(Some(String::from_utf8_lossy(&bytes)).and_then($callback_str))
             }
             }
             $crate::stmt::Column::Null => Ok(None),
             $crate::stmt::Column::Null => Ok(None),
-            other => Err(Error::InvalidType(
+            _ => Err($crate::ConversionError::InvalidType(
                 "String".to_owned(),
                 "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()))
                 Ok(Some(String::from_utf8_lossy(&bytes).to_string()))
             }
             }
             $crate::stmt::Column::Null => Ok(None),
             $crate::stmt::Column::Null => Ok(None),
-            other => Err(Error::InvalidType(
+            _ => Err($crate::ConversionError::InvalidType(
                 "String".to_owned(),
                 "String".to_owned(),
-                other.data_type().to_string(),
+                stringify!($col).to_owned(),
             )),
             )),
         })?
         })?
     };
     };
@@ -69,15 +69,21 @@ macro_rules! column_as_nullable_number {
     ($col:expr) => {
     ($col:expr) => {
         (match $col {
         (match $col {
             $crate::stmt::Column::Text(text) => Ok(Some(text.parse().map_err(|_| {
             $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(|_| {
             $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),
             $crate::stmt::Column::Null => Ok(None),
-            other => Err(Error::InvalidType(
+            _ => Err($crate::ConversionError::InvalidType(
                 "Number".to_owned(),
                 "Number".to_owned(),
-                other.data_type().to_string(),
+                stringify!($col).to_owned(),
             )),
             )),
         })?
         })?
     };
     };
@@ -89,14 +95,20 @@ macro_rules! column_as_number {
     ($col:expr) => {
     ($col:expr) => {
         (match $col {
         (match $col {
             $crate::stmt::Column::Text(text) => text.parse().map_err(|_| {
             $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(|_| {
             $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(),
                 "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::Text(text) => Ok(Some(text.as_bytes().to_vec())),
             $crate::stmt::Column::Blob(bytes) => Ok(Some(bytes.to_owned())),
             $crate::stmt::Column::Blob(bytes) => Ok(Some(bytes.to_owned())),
             $crate::stmt::Column::Null => Ok(None),
             $crate::stmt::Column::Null => Ok(None),
-            other => Err(Error::InvalidType(
+            _ => Err($crate::ConversionError::InvalidType(
                 "String".to_owned(),
                 "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_export]
 macro_rules! column_as_binary {
 macro_rules! column_as_binary {
     ($col:expr) => {
     ($col:expr) => {
         (match $col {
         (match $col {
             $crate::stmt::Column::Text(text) => Ok(text.as_bytes().to_vec()),
             $crate::stmt::Column::Text(text) => Ok(text.as_bytes().to_vec()),
             $crate::stmt::Column::Blob(bytes) => Ok(bytes.to_owned()),
             $crate::stmt::Column::Blob(bytes) => Ok(bytes.to_owned()),
-            other => Err(Error::InvalidType(
+            _ => Err($crate::ConversionError::InvalidType(
                 "String".to_owned(),
                 "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_export]
 macro_rules! column_as_string {
 macro_rules! column_as_string {
     ($col:expr, $callback_str:expr, $callback_bytes:expr) => {
     ($col:expr, $callback_str:expr, $callback_bytes:expr) => {
         (match $col {
         (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(),
                 "String".to_owned(),
-                other.data_type().to_string(),
+                stringify!($col).to_owned(),
             )),
             )),
         })?
         })?
     };
     };
     ($col:expr, $callback:expr) => {
     ($col:expr, $callback:expr) => {
         (match $col {
         (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) => {
             $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(),
                 "String".to_owned(),
-                other.data_type().to_string(),
+                stringify!($col).to_owned(),
             )),
             )),
         })?
         })?
     };
     };
@@ -162,9 +180,9 @@ macro_rules! column_as_string {
         (match $col {
         (match $col {
             $crate::stmt::Column::Text(text) => Ok(text.to_owned()),
             $crate::stmt::Column::Text(text) => Ok(text.to_owned()),
             $crate::stmt::Column::Blob(bytes) => Ok(String::from_utf8_lossy(&bytes).to_string()),
             $crate::stmt::Column::Blob(bytes) => Ok(String::from_utf8_lossy(&bytes).to_string()),
-            other => Err(Error::InvalidType(
+            _ => Err($crate::ConversionError::InvalidType(
                 "String".to_owned(),
                 "String".to_owned(),
-                other.data_type().to_string(),
+                stringify!($col).to_owned(),
             )),
             )),
         })?
         })?
     };
     };

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

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

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


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

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

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

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

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

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

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

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

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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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

@@ -1,5 +1,5 @@
 -- Add foreign key constraints for keyset_id in SQLite
 -- 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
 -- First, ensure we have the right schema information
 PRAGMA foreign_keys = OFF;
 PRAGMA foreign_keys = OFF;

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


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


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


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


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


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

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

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

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

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

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

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

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

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

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

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

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

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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

文件差异内容过多而无法显示
+ 11 - 1896
crates/cdk-sqlite/src/mint/mod.rs


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

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

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

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

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

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

文件差异内容过多而无法显示
+ 138 - 1085
crates/cdk-sqlite/src/wallet/mod.rs


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

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

部分文件因为文件数量过多而无法显示