diff --git a/Cargo.toml b/Cargo.toml index 02418387..527ee25a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,6 +54,7 @@ cdk-fake-wallet = { path = "./crates/cdk-fake-wallet", version = "=0.11.0" } cdk-payment-processor = { path = "./crates/cdk-payment-processor", default-features = true, version = "=0.11.0" } cdk-mint-rpc = { path = "./crates/cdk-mint-rpc", version = "=0.11.0" } cdk-redb = { path = "./crates/cdk-redb", default-features = true, version = "=0.11.0" } +cdk-sql-common = { path = "./crates/cdk-sql-common", default-features = true, version = "=0.11.0" } cdk-sqlite = { path = "./crates/cdk-sqlite", default-features = true, version = "=0.11.0" } cdk-signatory = { path = "./crates/cdk-signatory", version = "=0.11.0", default-features = false } clap = { version = "4.5.31", features = ["derive"] } diff --git a/crates/cdk-cli/src/main.rs b/crates/cdk-cli/src/main.rs index 7eb554b5..11b4297b 100644 --- a/crates/cdk-cli/src/main.rs +++ b/crates/cdk-cli/src/main.rs @@ -128,7 +128,7 @@ async fn main() -> Result<()> { #[cfg(feature = "sqlcipher")] let sql = { match args.password { - Some(pass) => WalletSqliteDatabase::new(&sql_path, pass).await?, + Some(pass) => WalletSqliteDatabase::new((sql_path, pass)).await?, None => bail!("Missing database password"), } }; diff --git a/crates/cdk-common/src/database/mod.rs b/crates/cdk-common/src/database/mod.rs index 195e210b..ccf13797 100644 --- a/crates/cdk-common/src/database/mod.rs +++ b/crates/cdk-common/src/database/mod.rs @@ -19,6 +19,80 @@ pub use mint::{MintAuthDatabase, MintAuthTransaction}; #[cfg(feature = "wallet")] pub use wallet::Database as WalletDatabase; +/// Data conversion error +#[derive(thiserror::Error, Debug)] +pub enum ConversionError { + /// Missing columns + #[error("Not enough elements: expected {0}, got {1}")] + MissingColumn(usize, usize), + + /// Missing parameter + #[error("Missing parameter {0}")] + MissingParameter(String), + + /// Invalid db type + #[error("Invalid type from db, expected {0} got {1}")] + InvalidType(String, String), + + /// Invalid data conversion in column + #[error("Error converting {1}, expecting type {0}")] + InvalidConversion(String, String), + + /// Mint Url Error + #[error(transparent)] + MintUrl(#[from] crate::mint_url::Error), + + /// NUT00 Error + #[error(transparent)] + CDKNUT00(#[from] crate::nuts::nut00::Error), + + /// NUT01 Error + #[error(transparent)] + CDKNUT01(#[from] crate::nuts::nut01::Error), + + /// NUT02 Error + #[error(transparent)] + CDKNUT02(#[from] crate::nuts::nut02::Error), + + /// NUT04 Error + #[error(transparent)] + CDKNUT04(#[from] crate::nuts::nut04::Error), + + /// NUT05 Error + #[error(transparent)] + CDKNUT05(#[from] crate::nuts::nut05::Error), + + /// NUT07 Error + #[error(transparent)] + CDKNUT07(#[from] crate::nuts::nut07::Error), + + /// NUT23 Error + #[error(transparent)] + CDKNUT23(#[from] crate::nuts::nut23::Error), + + /// Secret Error + #[error(transparent)] + CDKSECRET(#[from] crate::secret::Error), + + /// Serde Error + #[error(transparent)] + Serde(#[from] serde_json::Error), + + /// BIP32 Error + #[error(transparent)] + BIP32(#[from] bitcoin::bip32::Error), + + /// Generic error + #[error(transparent)] + Generic(#[from] Box), +} + +impl From for ConversionError { + fn from(err: crate::Error) -> Self { + ConversionError::Generic(Box::new(err)) + } +} + /// CDK_database error #[derive(Debug, thiserror::Error)] pub enum Error { @@ -39,6 +113,9 @@ pub enum Error { /// NUT00 Error #[error(transparent)] NUT00(#[from] crate::nuts::nut00::Error), + /// NUT01 Error + #[error(transparent)] + NUT01(#[from] crate::nuts::nut01::Error), /// NUT02 Error #[error(transparent)] NUT02(#[from] crate::nuts::nut02::Error), @@ -68,6 +145,38 @@ pub enum Error { /// Invalid state transition #[error("Invalid state transition")] InvalidStateTransition(crate::state::Error), + + /// Invalid connection settings + #[error("Invalid credentials {0}")] + InvalidConnectionSettings(String), + + /// Unexpected database response + #[error("Invalid database response")] + InvalidDbResponse, + + /// Internal error + #[error("Internal {0}")] + Internal(String), + + /// Data conversion error + #[error(transparent)] + Conversion(#[from] ConversionError), + + /// Missing Placeholder value + #[error("Missing placeholder value {0}")] + MissingPlaceholder(String), + + /// Unknown quote ttl + #[error("Unknown quote ttl")] + UnknownQuoteTTL, + + /// Invalid UUID + #[error("Invalid UUID: {0}")] + InvalidUuid(String), + + /// QuoteNotFound + #[error("Quote not found")] + QuoteNotFound, } #[cfg(feature = "mint")] diff --git a/crates/cdk-integration-tests/src/init_pure_tests.rs b/crates/cdk-integration-tests/src/init_pure_tests.rs index f55e1d1d..853dddb1 100644 --- a/crates/cdk-integration-tests/src/init_pure_tests.rs +++ b/crates/cdk-integration-tests/src/init_pure_tests.rs @@ -234,7 +234,7 @@ pub async fn create_and_start_test_mint() -> Result { let temp_dir = create_temp_dir("cdk-test-sqlite-mint")?; let path = temp_dir.join("mint.db").to_str().unwrap().to_string(); Arc::new( - cdk_sqlite::MintSqliteDatabase::new(&path) + cdk_sqlite::MintSqliteDatabase::new(path.as_str()) .await .expect("Could not create sqlite db"), ) @@ -310,7 +310,7 @@ pub async fn create_test_wallet_for_mint(mint: Mint) -> Result { // Create a temporary directory for SQLite database let temp_dir = create_temp_dir("cdk-test-sqlite-wallet")?; let path = temp_dir.join("wallet.db").to_str().unwrap().to_string(); - let database = cdk_sqlite::WalletSqliteDatabase::new(&path) + let database = cdk_sqlite::WalletSqliteDatabase::new(path.as_str()) .await .expect("Could not create sqlite db"); Arc::new(database) diff --git a/crates/cdk-mintd/src/main.rs b/crates/cdk-mintd/src/main.rs index 59cbe46b..7c270043 100644 --- a/crates/cdk-mintd/src/main.rs +++ b/crates/cdk-mintd/src/main.rs @@ -211,7 +211,7 @@ async fn setup_sqlite_database( #[cfg(feature = "sqlcipher")] let db = { // Get password from command line arguments for sqlcipher - MintSqliteDatabase::new(&sql_db_path, _password.unwrap()).await? + MintSqliteDatabase::new((sql_db_path, _password.unwrap())).await? }; Ok(Arc::new(db)) } @@ -486,7 +486,7 @@ async fn setup_authentication( #[cfg(feature = "sqlcipher")] let password = CLIArgs::parse().password; #[cfg(feature = "sqlcipher")] - let sqlite_db = MintSqliteAuthDatabase::new(&sql_db_path, password).await?; + let sqlite_db = MintSqliteAuthDatabase::new((sql_db_path, password)).await?; #[cfg(not(feature = "sqlcipher"))] let sqlite_db = MintSqliteAuthDatabase::new(&sql_db_path).await?; Arc::new(sqlite_db) diff --git a/crates/cdk-signatory/src/bin/cli/mod.rs b/crates/cdk-signatory/src/bin/cli/mod.rs index 1eed9426..b389b31c 100644 --- a/crates/cdk-signatory/src/bin/cli/mod.rs +++ b/crates/cdk-signatory/src/bin/cli/mod.rs @@ -108,7 +108,7 @@ pub async fn cli_main() -> Result<()> { #[cfg(feature = "sqlcipher")] let db = { match args.password { - Some(pass) => MintSqliteDatabase::new(&sql_path, pass).await?, + Some(pass) => MintSqliteDatabase::new((&sql_path, pass)).await?, None => bail!("Missing database password"), } }; diff --git a/crates/cdk-sql-common/Cargo.toml b/crates/cdk-sql-common/Cargo.toml new file mode 100644 index 00000000..4ceb0903 --- /dev/null +++ b/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 +tracing.workspace = true +serde.workspace = true +serde_json.workspace = true +lightning-invoice.workspace = true +uuid.workspace = true +once_cell.workspace = true diff --git a/crates/cdk-sql-common/README.md b/crates/cdk-sql-common/README.md new file mode 100644 index 00000000..86192391 --- /dev/null +++ b/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). diff --git a/crates/cdk-sqlite/build.rs b/crates/cdk-sql-common/build.rs similarity index 68% rename from crates/cdk-sqlite/build.rs rename to crates/cdk-sql-common/build.rs index 0891729a..37d494fc 100644 --- a/crates/cdk-sqlite/build.rs +++ b/crates/cdk-sql-common/build.rs @@ -18,18 +18,36 @@ fn main() { let dest_path = parent.join("migrations.rs"); let mut out_file = File::create(&dest_path).expect("Failed to create migrations.rs"); - writeln!(out_file, "// @generated").unwrap(); - writeln!(out_file, "// Auto-generated by build.rs").unwrap(); - writeln!(out_file, "pub static MIGRATIONS: &[(&str, &str)] = &[").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, &str)] = &[" + ) + .unwrap(); for path in &files { - let name = path.file_name().unwrap().to_string_lossy(); + let parts = path.to_str().unwrap().replace("\\", "/")[skip_name + 1..] + .split("/") + .map(|x| x.to_owned()) + .collect::>(); + + let prefix = if parts.len() == 2 { + parts.first().map(|x| x.to_owned()).unwrap_or_default() + } else { + "".to_owned() + }; + + let rel_name = &path.file_name().unwrap().to_str().unwrap(); let rel_path = &path.to_str().unwrap().replace("\\", "/")[skip_path..]; // for Windows writeln!( out_file, - " (\"{name}\", include_str!(r#\".{rel_path}\"#))," + " (\"{prefix}\", \"{rel_name}\", include_str!(r#\".{rel_path}\"#))," ) .unwrap(); + println!("cargo:rerun-if-changed={}", path.display()); } writeln!(out_file, "];").unwrap(); diff --git a/crates/cdk-sql-common/src/common.rs b/crates/cdk-sql-common/src/common.rs new file mode 100644 index 00000000..ce52f7df --- /dev/null +++ b/crates/cdk-sql-common/src/common.rs @@ -0,0 +1,44 @@ +use crate::database::DatabaseExecutor; +use crate::stmt::query; + +/// Migrates the migration generated by `build.rs` +#[inline(always)] +pub async fn migrate( + conn: &C, + db_prefix: &str, + migrations: &[(&str, &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 (prefix, name, sql) in migrations { + if !prefix.is_empty() && *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(()) +} diff --git a/crates/cdk-sql-common/src/database.rs b/crates/cdk-sql-common/src/database.rs new file mode 100644 index 00000000..06c63a34 --- /dev/null +++ b/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; + + /// Runs the query and returns the first row or None + async fn fetch_one(&self, statement: Statement) -> Result>, Error>; + + /// Runs the query and returns the first row or None + async fn fetch_all(&self, statement: Statement) -> Result>, Error>; + + /// Fetches the first row and column from a query + async fn pluck(&self, statement: Statement) -> Result, 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, Error>; +} diff --git a/crates/cdk-sql-common/src/lib.rs b/crates/cdk-sql-common/src/lib.rs new file mode 100644 index 00000000..8c44b9a4 --- /dev/null +++ b/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; diff --git a/crates/cdk-sqlite/src/macros.rs b/crates/cdk-sql-common/src/macros.rs similarity index 62% rename from crates/cdk-sqlite/src/macros.rs rename to crates/cdk-sql-common/src/macros.rs index 7720d56b..094cb098 100644 --- a/crates/cdk-sqlite/src/macros.rs +++ b/crates/cdk-sql-common/src/macros.rs @@ -1,4 +1,4 @@ -//! Collection of macros to generate code to digest data from SQLite +//! Collection of macros to generate code to digest data from a generic SQL databasex /// Unpacks a vector of Column, and consumes it, parsing into individual variables, checking the /// vector is big enough. @@ -10,9 +10,9 @@ macro_rules! unpack_into { vec.reverse(); let required = 0 $(+ {let _ = stringify!($var); 1})+; if vec.len() < required { - return Err(Error::MissingColumn(required, vec.len())); + Err($crate::ConversionError::MissingColumn(required, vec.len()))?; } - Ok::<_, Error>(( + Ok::<_, cdk_common::database::Error>(( $( vec.pop().expect(&format!("Checked length already for {}", stringify!($var))) ),+ @@ -21,7 +21,7 @@ macro_rules! unpack_into { }; } -/// Parses a SQLite column as a string or NULL +/// Parses a SQL column as a string or NULL #[macro_export] macro_rules! column_as_nullable_string { ($col:expr, $callback_str:expr, $callback_bytes:expr) => { @@ -29,9 +29,9 @@ macro_rules! column_as_nullable_string { $crate::stmt::Column::Text(text) => Ok(Some(text).and_then($callback_str)), $crate::stmt::Column::Blob(bytes) => Ok(Some(bytes).and_then($callback_bytes)), $crate::stmt::Column::Null => Ok(None), - other => Err(Error::InvalidType( + _ => Err($crate::ConversionError::InvalidType( "String".to_owned(), - other.data_type().to_string(), + stringify!($col).to_owned(), )), })? }; @@ -42,9 +42,9 @@ macro_rules! column_as_nullable_string { Ok(Some(String::from_utf8_lossy(&bytes)).and_then($callback_str)) } $crate::stmt::Column::Null => Ok(None), - other => Err(Error::InvalidType( + _ => Err($crate::ConversionError::InvalidType( "String".to_owned(), - other.data_type().to_string(), + stringify!($col).to_owned(), )), })? }; @@ -55,9 +55,9 @@ macro_rules! column_as_nullable_string { Ok(Some(String::from_utf8_lossy(&bytes).to_string())) } $crate::stmt::Column::Null => Ok(None), - other => Err(Error::InvalidType( + _ => Err($crate::ConversionError::InvalidType( "String".to_owned(), - other.data_type().to_string(), + stringify!($col).to_owned(), )), })? }; @@ -69,15 +69,21 @@ macro_rules! column_as_nullable_number { ($col:expr) => { (match $col { $crate::stmt::Column::Text(text) => Ok(Some(text.parse().map_err(|_| { - Error::InvalidConversion(stringify!($col).to_owned(), "Number".to_owned()) + $crate::ConversionError::InvalidConversion( + stringify!($col).to_owned(), + "Number".to_owned(), + ) })?)), $crate::stmt::Column::Integer(n) => Ok(Some(n.try_into().map_err(|_| { - Error::InvalidConversion(stringify!($col).to_owned(), "Number".to_owned()) + $crate::ConversionError::InvalidConversion( + stringify!($col).to_owned(), + "Number".to_owned(), + ) })?)), $crate::stmt::Column::Null => Ok(None), - other => Err(Error::InvalidType( + _ => Err($crate::ConversionError::InvalidType( "Number".to_owned(), - other.data_type().to_string(), + stringify!($col).to_owned(), )), })? }; @@ -89,14 +95,20 @@ macro_rules! column_as_number { ($col:expr) => { (match $col { $crate::stmt::Column::Text(text) => text.parse().map_err(|_| { - Error::InvalidConversion(stringify!($col).to_owned(), "Number".to_owned()) + $crate::ConversionError::InvalidConversion( + stringify!($col).to_owned(), + "Number".to_owned(), + ) }), $crate::stmt::Column::Integer(n) => n.try_into().map_err(|_| { - Error::InvalidConversion(stringify!($col).to_owned(), "Number".to_owned()) + $crate::ConversionError::InvalidConversion( + stringify!($col).to_owned(), + "Number".to_owned(), + ) }), - other => Err(Error::InvalidType( + _ => Err($crate::ConversionError::InvalidType( "Number".to_owned(), - other.data_type().to_string(), + stringify!($col).to_owned(), )), })? }; @@ -110,51 +122,57 @@ macro_rules! column_as_nullable_binary { $crate::stmt::Column::Text(text) => Ok(Some(text.as_bytes().to_vec())), $crate::stmt::Column::Blob(bytes) => Ok(Some(bytes.to_owned())), $crate::stmt::Column::Null => Ok(None), - other => Err(Error::InvalidType( + _ => Err($crate::ConversionError::InvalidType( "String".to_owned(), - other.data_type().to_string(), + stringify!($col).to_owned(), )), })? }; } -/// Parses a SQLite column as a binary +/// Parses a SQL column as a binary #[macro_export] macro_rules! column_as_binary { ($col:expr) => { (match $col { $crate::stmt::Column::Text(text) => Ok(text.as_bytes().to_vec()), $crate::stmt::Column::Blob(bytes) => Ok(bytes.to_owned()), - other => Err(Error::InvalidType( + _ => Err($crate::ConversionError::InvalidType( "String".to_owned(), - other.data_type().to_string(), + stringify!($col).to_owned(), )), })? }; } -/// Parses a SQLite column as a string +/// Parses a SQL column as a string #[macro_export] macro_rules! column_as_string { ($col:expr, $callback_str:expr, $callback_bytes:expr) => { (match $col { - $crate::stmt::Column::Text(text) => $callback_str(&text).map_err(Error::from), - $crate::stmt::Column::Blob(bytes) => $callback_bytes(&bytes).map_err(Error::from), - other => Err(Error::InvalidType( + $crate::stmt::Column::Text(text) => { + $callback_str(&text).map_err($crate::ConversionError::from) + } + $crate::stmt::Column::Blob(bytes) => { + $callback_bytes(&bytes).map_err($crate::ConversionError::from) + } + _ => Err($crate::ConversionError::InvalidType( "String".to_owned(), - other.data_type().to_string(), + stringify!($col).to_owned(), )), })? }; ($col:expr, $callback:expr) => { (match $col { - $crate::stmt::Column::Text(text) => $callback(&text).map_err(Error::from), - $crate::stmt::Column::Blob(bytes) => { - $callback(&String::from_utf8_lossy(&bytes)).map_err(Error::from) + $crate::stmt::Column::Text(text) => { + $callback(&text).map_err($crate::ConversionError::from) } - other => Err(Error::InvalidType( + $crate::stmt::Column::Blob(bytes) => { + $callback(&String::from_utf8_lossy(&bytes)).map_err($crate::ConversionError::from) + } + _ => Err($crate::ConversionError::InvalidType( "String".to_owned(), - other.data_type().to_string(), + stringify!($col).to_owned(), )), })? }; @@ -162,9 +180,9 @@ macro_rules! column_as_string { (match $col { $crate::stmt::Column::Text(text) => Ok(text.to_owned()), $crate::stmt::Column::Blob(bytes) => Ok(String::from_utf8_lossy(&bytes).to_string()), - other => Err(Error::InvalidType( + _ => Err($crate::ConversionError::InvalidType( "String".to_owned(), - other.data_type().to_string(), + stringify!($col).to_owned(), )), })? }; diff --git a/crates/cdk-sql-common/src/mint/auth/migrations.rs b/crates/cdk-sql-common/src/mint/auth/migrations.rs new file mode 100644 index 00000000..06f5b0bb --- /dev/null +++ b/crates/cdk-sql-common/src/mint/auth/migrations.rs @@ -0,0 +1,5 @@ +/// @generated +/// Auto-generated by build.rs +pub static MIGRATIONS: &[(&str, &str, &str)] = &[ + ("sqlite", "20250109143347_init.sql", include_str!(r#"./migrations/sqlite/20250109143347_init.sql"#)), +]; diff --git a/crates/cdk-sqlite/src/mint/auth/migrations/20250109143347_init.sql b/crates/cdk-sql-common/src/mint/auth/migrations/sqlite/20250109143347_init.sql similarity index 100% rename from crates/cdk-sqlite/src/mint/auth/migrations/20250109143347_init.sql rename to crates/cdk-sql-common/src/mint/auth/migrations/sqlite/20250109143347_init.sql diff --git a/crates/cdk-sqlite/src/mint/auth/mod.rs b/crates/cdk-sql-common/src/mint/auth/mod.rs similarity index 72% rename from crates/cdk-sqlite/src/mint/auth/mod.rs rename to crates/cdk-sql-common/src/mint/auth/mod.rs index 4a002fa8..9d37c1d8 100644 --- a/crates/cdk-sqlite/src/mint/auth/mod.rs +++ b/crates/cdk-sql-common/src/mint/auth/mod.rs @@ -1,8 +1,7 @@ -//! SQLite Mint Auth +//! SQL Mint Auth use std::collections::HashMap; -use std::ops::DerefMut; -use std::path::Path; +use std::marker::PhantomData; use std::str::FromStr; use async_trait::async_trait; @@ -10,53 +9,57 @@ use cdk_common::database::{self, MintAuthDatabase, MintAuthTransaction}; use cdk_common::mint::MintKeySetInfo; use cdk_common::nuts::{AuthProof, BlindSignature, Id, PublicKey, State}; use cdk_common::{AuthRequired, ProtectedEndpoint}; +use migrations::MIGRATIONS; use tracing::instrument; -use super::async_rusqlite::AsyncRusqlite; -use super::{sqlite_row_to_blind_signature, sqlite_row_to_keyset_info, SqliteTransaction}; +use super::{sql_row_to_blind_signature, sql_row_to_keyset_info, SQLTransaction}; use crate::column_as_string; -use crate::common::{create_sqlite_pool, migrate}; -use crate::mint::async_rusqlite::query; +use crate::common::migrate; +use crate::database::{DatabaseConnector, DatabaseTransaction}; use crate::mint::Error; +use crate::stmt::query; -/// Mint SQLite Database +/// Mint SQL Database #[derive(Debug, Clone)] -pub struct MintSqliteAuthDatabase { - pool: AsyncRusqlite, +pub struct SQLMintAuthDatabase +where + DB: DatabaseConnector, +{ + db: DB, +} + +impl SQLMintAuthDatabase +where + DB: DatabaseConnector, +{ + /// Creates a new instance + pub async fn new(db: X) -> Result + where + X: Into, + { + 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(()) + } } #[rustfmt::skip] mod migrations; -impl MintSqliteAuthDatabase { - /// Create new [`MintSqliteAuthDatabase`] - #[cfg(not(feature = "sqlcipher"))] - pub async fn new>(path: P) -> Result { - 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), - }) - } - - /// Create new [`MintSqliteAuthDatabase`] - #[cfg(feature = "sqlcipher")] - pub async fn new>(path: P, password: String) -> Result { - 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), - }) - } -} #[async_trait] -impl MintAuthTransaction for SqliteTransaction<'_> { +impl<'a, T> MintAuthTransaction for SQLTransaction<'a, T> +where + T: DatabaseTransaction<'a>, +{ #[instrument(skip(self))] async fn set_active_keyset(&mut self, id: Id) -> Result<(), database::Error> { tracing::info!("Setting auth keyset {id} active"); @@ -68,8 +71,8 @@ impl MintAuthTransaction for SqliteTransaction<'_> { ELSE FALSE END; "#, - ) - .bind(":id", id.to_string()) + )? + .bind("id", id.to_string()) .execute(&self.inner) .await?; @@ -97,15 +100,15 @@ impl MintAuthTransaction for SqliteTransaction<'_> { max_order = excluded.max_order, derivation_path_index = excluded.derivation_path_index "#, - ) - .bind(":id", keyset.id.to_string()) - .bind(":unit", keyset.unit.to_string()) - .bind(":active", keyset.active) - .bind(":valid_from", keyset.valid_from as i64) - .bind(":valid_to", keyset.final_expiry.map(|v| v as i64)) - .bind(":derivation_path", keyset.derivation_path.to_string()) - .bind(":max_order", keyset.max_order) - .bind(":derivation_path_index", keyset.derivation_path_index) + )? + .bind("id", keyset.id.to_string()) + .bind("unit", keyset.unit.to_string()) + .bind("active", keyset.active) + .bind("valid_from", keyset.valid_from as i64) + .bind("valid_to", keyset.final_expiry.map(|v| v as i64)) + .bind("derivation_path", keyset.derivation_path.to_string()) + .bind("max_order", keyset.max_order) + .bind("derivation_path_index", keyset.derivation_path_index) .execute(&self.inner) .await?; @@ -120,12 +123,12 @@ impl MintAuthTransaction for SqliteTransaction<'_> { VALUES (:y, :keyset_id, :secret, :c, :state) "#, - ) - .bind(":y", proof.y()?.to_bytes().to_vec()) - .bind(":keyset_id", proof.keyset_id.to_string()) - .bind(":secret", proof.secret.to_string()) - .bind(":c", proof.c.to_bytes().to_vec()) - .bind(":state", "UNSPENT".to_string()) + )? + .bind("y", proof.y()?.to_bytes().to_vec()) + .bind("keyset_id", proof.keyset_id.to_string()) + .bind("secret", proof.secret.to_string()) + .bind("c", proof.c.to_bytes().to_vec()) + .bind("state", "UNSPENT".to_string()) .execute(&self.inner) .await { @@ -139,20 +142,20 @@ impl MintAuthTransaction for SqliteTransaction<'_> { y: &PublicKey, proofs_state: State, ) -> Result, Self::Err> { - let current_state = query(r#"SELECT state FROM proof WHERE y = :y"#) - .bind(":y", y.to_bytes().to_vec()) + let current_state = query(r#"SELECT state FROM proof WHERE y = :y"#)? + .bind("y", y.to_bytes().to_vec()) .pluck(&self.inner) .await? .map(|state| Ok::<_, Error>(column_as_string!(state, State::from_str))) .transpose()?; - query(r#"UPDATE proof SET state = :new_state WHERE state = :state AND y = :y"#) - .bind(":y", y.to_bytes().to_vec()) + query(r#"UPDATE proof SET state = :new_state WHERE state = :state AND y = :y"#)? + .bind("y", y.to_bytes().to_vec()) .bind( - ":state", + "state", current_state.as_ref().map(|state| state.to_string()), ) - .bind(":new_state", proofs_state.to_string()) + .bind("new_state", proofs_state.to_string()) .execute(&self.inner) .await?; @@ -173,11 +176,11 @@ impl MintAuthTransaction for SqliteTransaction<'_> { VALUES (:y, :amount, :keyset_id, :c) "#, - ) - .bind(":y", message.to_bytes().to_vec()) - .bind(":amount", u64::from(signature.amount) as i64) - .bind(":keyset_id", signature.keyset_id.to_string()) - .bind(":c", signature.c.to_bytes().to_vec()) + )? + .bind("y", message.to_bytes().to_vec()) + .bind("amount", u64::from(signature.amount) as i64) + .bind("keyset_id", signature.keyset_id.to_string()) + .bind("c", signature.c.to_bytes().to_vec()) .execute(&self.inner) .await?; } @@ -196,9 +199,9 @@ impl MintAuthTransaction for SqliteTransaction<'_> { (endpoint, auth) VALUES (:endpoint, :auth); "#, - ) - .bind(":endpoint", serde_json::to_string(endpoint)?) - .bind(":auth", serde_json::to_string(auth)?) + )? + .bind("endpoint", serde_json::to_string(endpoint)?) + .bind("auth", serde_json::to_string(auth)?) .execute(&self.inner) .await { @@ -215,9 +218,9 @@ impl MintAuthTransaction for SqliteTransaction<'_> { &mut self, protected_endpoints: Vec, ) -> Result<(), database::Error> { - query(r#"DELETE FROM protected_endpoints WHERE endpoint IN (:endpoints)"#) + query(r#"DELETE FROM protected_endpoints WHERE endpoint IN (:endpoints)"#)? .bind_vec( - ":endpoints", + "endpoints", protected_endpoints .iter() .map(serde_json::to_string) @@ -230,15 +233,19 @@ impl MintAuthTransaction for SqliteTransaction<'_> { } #[async_trait] -impl MintAuthDatabase for MintSqliteAuthDatabase { +impl MintAuthDatabase for SQLMintAuthDatabase +where + DB: DatabaseConnector, +{ type Err = database::Error; async fn begin_transaction<'a>( &'a self, ) -> Result + Send + Sync + 'a>, database::Error> { - Ok(Box::new(SqliteTransaction { - inner: self.pool.begin().await?, + Ok(Box::new(SQLTransaction { + inner: self.db.begin().await?, + _phantom: PhantomData, })) } @@ -252,8 +259,8 @@ impl MintAuthDatabase for MintSqliteAuthDatabase { WHERE active = 1; "#, - ) - .pluck(&self.pool) + )? + .pluck(&self.db) .await? .map(|id| Ok::<_, Error>(column_as_string!(id, Id::from_str, Id::from_bytes))) .transpose()?) @@ -274,11 +281,11 @@ impl MintAuthDatabase for MintSqliteAuthDatabase { FROM keyset WHERE id=:id"#, - ) - .bind(":id", id.to_string()) - .fetch_one(&self.pool) + )? + .bind("id", id.to_string()) + .fetch_one(&self.db) .await? - .map(sqlite_row_to_keyset_info) + .map(sql_row_to_keyset_info) .transpose()?) } @@ -297,18 +304,18 @@ impl MintAuthDatabase for MintSqliteAuthDatabase { FROM keyset WHERE id=:id"#, - ) - .fetch_all(&self.pool) + )? + .fetch_all(&self.db) .await? .into_iter() - .map(sqlite_row_to_keyset_info) + .map(sql_row_to_keyset_info) .collect::, _>>()?) } async fn get_proofs_states(&self, ys: &[PublicKey]) -> Result>, Self::Err> { - let mut current_states = query(r#"SELECT y, state FROM proof WHERE y IN (:ys)"#) - .bind_vec(":ys", ys.iter().map(|y| y.to_bytes().to_vec()).collect()) - .fetch_all(&self.pool) + let mut current_states = query(r#"SELECT y, state FROM proof WHERE y IN (:ys)"#)? + .bind_vec("ys", ys.iter().map(|y| y.to_bytes().to_vec()).collect()) + .fetch_all(&self.db) .await? .into_iter() .map(|row| { @@ -338,15 +345,15 @@ impl MintAuthDatabase for MintSqliteAuthDatabase { blind_signature WHERE y IN (:y) "#, - ) + )? .bind_vec( - ":y", + "y", blinded_messages .iter() .map(|y| y.to_bytes().to_vec()) .collect(), ) - .fetch_all(&self.pool) + .fetch_all(&self.db) .await? .into_iter() .map(|mut row| { @@ -356,7 +363,7 @@ impl MintAuthDatabase for MintSqliteAuthDatabase { PublicKey::from_hex, PublicKey::from_slice ), - sqlite_row_to_blind_signature(row)?, + sql_row_to_blind_signature(row)?, )) }) .collect::, Error>>()?; @@ -371,9 +378,9 @@ impl MintAuthDatabase for MintSqliteAuthDatabase { protected_endpoint: ProtectedEndpoint, ) -> Result, Self::Err> { Ok( - query(r#"SELECT auth FROM protected_endpoints WHERE endpoint = :endpoint"#) - .bind(":endpoint", serde_json::to_string(&protected_endpoint)?) - .pluck(&self.pool) + query(r#"SELECT auth FROM protected_endpoints WHERE endpoint = :endpoint"#)? + .bind("endpoint", serde_json::to_string(&protected_endpoint)?) + .pluck(&self.db) .await? .map(|auth| { Ok::<_, Error>(column_as_string!( @@ -389,8 +396,8 @@ impl MintAuthDatabase for MintSqliteAuthDatabase { async fn get_auth_for_endpoints( &self, ) -> Result>, Self::Err> { - Ok(query(r#"SELECT endpoint, auth FROM protected_endpoints"#) - .fetch_all(&self.pool) + Ok(query(r#"SELECT endpoint, auth FROM protected_endpoints"#)? + .fetch_all(&self.db) .await? .into_iter() .map(|row| { diff --git a/crates/cdk-sql-common/src/mint/migrations.rs b/crates/cdk-sql-common/src/mint/migrations.rs new file mode 100644 index 00000000..d97ccb4e --- /dev/null +++ b/crates/cdk-sql-common/src/mint/migrations.rs @@ -0,0 +1,26 @@ +/// @generated +/// Auto-generated by build.rs +pub static MIGRATIONS: &[(&str, &str, &str)] = &[ + ("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"#)), +]; diff --git a/crates/cdk-sql-common/src/mint/migrations/sqlite/1_fix_sqlx_migration.sql b/crates/cdk-sql-common/src/mint/migrations/sqlite/1_fix_sqlx_migration.sql new file mode 100644 index 00000000..9f7a0d82 --- /dev/null +++ b/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; diff --git a/crates/cdk-sqlite/src/mint/migrations/20240612124932_init.sql b/crates/cdk-sql-common/src/mint/migrations/sqlite/20240612124932_init.sql similarity index 100% rename from crates/cdk-sqlite/src/mint/migrations/20240612124932_init.sql rename to crates/cdk-sql-common/src/mint/migrations/sqlite/20240612124932_init.sql diff --git a/crates/cdk-sqlite/src/mint/migrations/20240618195700_quote_state.sql b/crates/cdk-sql-common/src/mint/migrations/sqlite/20240618195700_quote_state.sql similarity index 100% rename from crates/cdk-sqlite/src/mint/migrations/20240618195700_quote_state.sql rename to crates/cdk-sql-common/src/mint/migrations/sqlite/20240618195700_quote_state.sql diff --git a/crates/cdk-sqlite/src/mint/migrations/20240626092101_nut04_state.sql b/crates/cdk-sql-common/src/mint/migrations/sqlite/20240626092101_nut04_state.sql similarity index 100% rename from crates/cdk-sqlite/src/mint/migrations/20240626092101_nut04_state.sql rename to crates/cdk-sql-common/src/mint/migrations/sqlite/20240626092101_nut04_state.sql diff --git a/crates/cdk-sqlite/src/mint/migrations/20240703122347_request_lookup_id.sql b/crates/cdk-sql-common/src/mint/migrations/sqlite/20240703122347_request_lookup_id.sql similarity index 100% rename from crates/cdk-sqlite/src/mint/migrations/20240703122347_request_lookup_id.sql rename to crates/cdk-sql-common/src/mint/migrations/sqlite/20240703122347_request_lookup_id.sql diff --git a/crates/cdk-sqlite/src/mint/migrations/20240710145043_input_fee.sql b/crates/cdk-sql-common/src/mint/migrations/sqlite/20240710145043_input_fee.sql similarity index 100% rename from crates/cdk-sqlite/src/mint/migrations/20240710145043_input_fee.sql rename to crates/cdk-sql-common/src/mint/migrations/sqlite/20240710145043_input_fee.sql diff --git a/crates/cdk-sqlite/src/mint/migrations/20240711183109_derivation_path_index.sql b/crates/cdk-sql-common/src/mint/migrations/sqlite/20240711183109_derivation_path_index.sql similarity index 100% rename from crates/cdk-sqlite/src/mint/migrations/20240711183109_derivation_path_index.sql rename to crates/cdk-sql-common/src/mint/migrations/sqlite/20240711183109_derivation_path_index.sql diff --git a/crates/cdk-sqlite/src/mint/migrations/20240718203721_allow_unspent.sql b/crates/cdk-sql-common/src/mint/migrations/sqlite/20240718203721_allow_unspent.sql similarity index 100% rename from crates/cdk-sqlite/src/mint/migrations/20240718203721_allow_unspent.sql rename to crates/cdk-sql-common/src/mint/migrations/sqlite/20240718203721_allow_unspent.sql diff --git a/crates/cdk-sqlite/src/mint/migrations/20240811031111_update_mint_url.sql b/crates/cdk-sql-common/src/mint/migrations/sqlite/20240811031111_update_mint_url.sql similarity index 100% rename from crates/cdk-sqlite/src/mint/migrations/20240811031111_update_mint_url.sql rename to crates/cdk-sql-common/src/mint/migrations/sqlite/20240811031111_update_mint_url.sql diff --git a/crates/cdk-sqlite/src/mint/migrations/20240919103407_proofs_quote_id.sql b/crates/cdk-sql-common/src/mint/migrations/sqlite/20240919103407_proofs_quote_id.sql similarity index 100% rename from crates/cdk-sqlite/src/mint/migrations/20240919103407_proofs_quote_id.sql rename to crates/cdk-sql-common/src/mint/migrations/sqlite/20240919103407_proofs_quote_id.sql diff --git a/crates/cdk-sqlite/src/mint/migrations/20240923153640_melt_requests.sql b/crates/cdk-sql-common/src/mint/migrations/sqlite/20240923153640_melt_requests.sql similarity index 100% rename from crates/cdk-sqlite/src/mint/migrations/20240923153640_melt_requests.sql rename to crates/cdk-sql-common/src/mint/migrations/sqlite/20240923153640_melt_requests.sql diff --git a/crates/cdk-sqlite/src/mint/migrations/20240930101140_dleq_for_sigs.sql b/crates/cdk-sql-common/src/mint/migrations/sqlite/20240930101140_dleq_for_sigs.sql similarity index 100% rename from crates/cdk-sqlite/src/mint/migrations/20240930101140_dleq_for_sigs.sql rename to crates/cdk-sql-common/src/mint/migrations/sqlite/20240930101140_dleq_for_sigs.sql diff --git a/crates/cdk-sqlite/src/mint/migrations/20241108093102_mint_mint_quote_pubkey.sql b/crates/cdk-sql-common/src/mint/migrations/sqlite/20241108093102_mint_mint_quote_pubkey.sql similarity index 100% rename from crates/cdk-sqlite/src/mint/migrations/20241108093102_mint_mint_quote_pubkey.sql rename to crates/cdk-sql-common/src/mint/migrations/sqlite/20241108093102_mint_mint_quote_pubkey.sql diff --git a/crates/cdk-sqlite/src/mint/migrations/20250103201327_amount_to_pay_msats.sql b/crates/cdk-sql-common/src/mint/migrations/sqlite/20250103201327_amount_to_pay_msats.sql similarity index 100% rename from crates/cdk-sqlite/src/mint/migrations/20250103201327_amount_to_pay_msats.sql rename to crates/cdk-sql-common/src/mint/migrations/sqlite/20250103201327_amount_to_pay_msats.sql diff --git a/crates/cdk-sqlite/src/mint/migrations/20250129200912_remove_mint_url.sql b/crates/cdk-sql-common/src/mint/migrations/sqlite/20250129200912_remove_mint_url.sql similarity index 100% rename from crates/cdk-sqlite/src/mint/migrations/20250129200912_remove_mint_url.sql rename to crates/cdk-sql-common/src/mint/migrations/sqlite/20250129200912_remove_mint_url.sql diff --git a/crates/cdk-sqlite/src/mint/migrations/20250129230326_add_config_table.sql b/crates/cdk-sql-common/src/mint/migrations/sqlite/20250129230326_add_config_table.sql similarity index 100% rename from crates/cdk-sqlite/src/mint/migrations/20250129230326_add_config_table.sql rename to crates/cdk-sql-common/src/mint/migrations/sqlite/20250129230326_add_config_table.sql diff --git a/crates/cdk-sqlite/src/mint/migrations/20250307213652_keyset_id_as_foreign_key.sql b/crates/cdk-sql-common/src/mint/migrations/sqlite/20250307213652_keyset_id_as_foreign_key.sql similarity index 96% rename from crates/cdk-sqlite/src/mint/migrations/20250307213652_keyset_id_as_foreign_key.sql rename to crates/cdk-sql-common/src/mint/migrations/sqlite/20250307213652_keyset_id_as_foreign_key.sql index 1dfdc77f..c1866974 100644 --- a/crates/cdk-sqlite/src/mint/migrations/20250307213652_keyset_id_as_foreign_key.sql +++ b/crates/cdk-sql-common/src/mint/migrations/sqlite/20250307213652_keyset_id_as_foreign_key.sql @@ -1,5 +1,5 @@ -- Add foreign key constraints for keyset_id in SQLite --- SQLite requires recreating tables to add foreign keys +-- SQL requires recreating tables to add foreign keys -- First, ensure we have the right schema information PRAGMA foreign_keys = OFF; diff --git a/crates/cdk-sqlite/src/mint/migrations/20250406091754_mint_time_of_quotes.sql b/crates/cdk-sql-common/src/mint/migrations/sqlite/20250406091754_mint_time_of_quotes.sql similarity index 100% rename from crates/cdk-sqlite/src/mint/migrations/20250406091754_mint_time_of_quotes.sql rename to crates/cdk-sql-common/src/mint/migrations/sqlite/20250406091754_mint_time_of_quotes.sql diff --git a/crates/cdk-sqlite/src/mint/migrations/20250406093755_mint_created_time_signature.sql b/crates/cdk-sql-common/src/mint/migrations/sqlite/20250406093755_mint_created_time_signature.sql similarity index 100% rename from crates/cdk-sqlite/src/mint/migrations/20250406093755_mint_created_time_signature.sql rename to crates/cdk-sql-common/src/mint/migrations/sqlite/20250406093755_mint_created_time_signature.sql diff --git a/crates/cdk-sqlite/src/mint/migrations/20250415093121_drop_keystore_foreign.sql b/crates/cdk-sql-common/src/mint/migrations/sqlite/20250415093121_drop_keystore_foreign.sql similarity index 100% rename from crates/cdk-sqlite/src/mint/migrations/20250415093121_drop_keystore_foreign.sql rename to crates/cdk-sql-common/src/mint/migrations/sqlite/20250415093121_drop_keystore_foreign.sql diff --git a/crates/cdk-sqlite/src/mint/migrations/20250626120251_rename_blind_message_y_to_b.sql b/crates/cdk-sql-common/src/mint/migrations/sqlite/20250626120251_rename_blind_message_y_to_b.sql similarity index 100% rename from crates/cdk-sqlite/src/mint/migrations/20250626120251_rename_blind_message_y_to_b.sql rename to crates/cdk-sql-common/src/mint/migrations/sqlite/20250626120251_rename_blind_message_y_to_b.sql diff --git a/crates/cdk-sqlite/src/mint/migrations/20250706101057_bolt12.sql b/crates/cdk-sql-common/src/mint/migrations/sqlite/20250706101057_bolt12.sql similarity index 100% rename from crates/cdk-sqlite/src/mint/migrations/20250706101057_bolt12.sql rename to crates/cdk-sql-common/src/mint/migrations/sqlite/20250706101057_bolt12.sql diff --git a/crates/cdk-sql-common/src/mint/mod.rs b/crates/cdk-sql-common/src/mint/mod.rs new file mode 100644 index 00000000..6a937037 --- /dev/null +++ b/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 +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( + conn: &C, + ys: &[PublicKey], +) -> Result, 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::, _>>() +} + +#[inline(always)] +async fn set_to_config(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 SQLMintDatabase +where + DB: DatabaseConnector, +{ + /// Creates a new instance + pub async fn new(db: X) -> Result + where + X: Into, + { + 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(&self, id: &str) -> Result + 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, + ) -> 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::>()?, + ) + .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>, 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, + ) -> 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", "e_ttl).await?) + } +} + +#[async_trait] +impl<'a, T> MintDbWriterFinalizer for SQLTransaction<'a, T> +where + T: DatabaseTransaction<'a>, +{ + type Err = Error; + + async fn commit(self: Box) -> Result<(), Error> { + Ok(self.inner.commit().await?) + } + + async fn rollback(self: Box) -> Result<(), Error> { + Ok(self.inner.rollback().await?) + } +} + +#[inline(always)] +async fn get_mint_quote_payments( + conn: &C, + quote_id: &Uuid, +) -> Result, 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(conn: &C, quote_id: &Uuid) -> Result, 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 MintKeysDatabase for SQLMintDatabase +where + DB: DatabaseConnector, +{ + type Err = Error; + + async fn begin_transaction<'a>( + &'a self, + ) -> Result + 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, 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, 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::, Error>>()?) + } + + async fn get_keyset_info(&self, id: &Id) -> Result, 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, 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::, _>>()?) + } +} + +#[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 { + // 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 { + // 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("e.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, + ) -> 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, 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, 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, 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, "e.id).await?; + let issuance = get_mint_quote_issuance(&self.inner, "e.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, 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, "e.id).await?; + let issuance = get_mint_quote_issuance(&self.inner, "e.id).await?; + quote.issuance = issuance; + quote.payments = payments; + } + + Ok(mint_quote) + } +} + +#[async_trait] +impl MintQuotesDatabase for SQLMintDatabase +where + DB: DatabaseConnector, +{ + type Err = Error; + + async fn get_mint_quote(&self, quote_id: &Uuid) -> Result, 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, 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, "e.id).await?; + let issuance = get_mint_quote_issuance(&self.db, "e.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, 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 + "#, + )? + .bind("request_lookup_id", request_lookup_id.to_string()) + .bind("request_lookup_id_kind", request_lookup_id.kind()) + .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, "e.id).await?; + let issuance = get_mint_quote_issuance(&self.db, "e.id).await?; + quote.issuance = issuance; + quote.payments = payments; + } + + Ok(mint_quote) + } + + async fn get_mint_quotes(&self) -> Result, 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::, _>>()?; + + for quote in mint_quotes.as_mut_slice() { + let payments = get_mint_quote_payments(&self.db, "e.id).await?; + let issuance = get_mint_quote_issuance(&self.db, "e.id).await?; + quote.issuance = issuance; + quote.payments = payments; + } + + Ok(mint_quotes) + } + + async fn get_melt_quote(&self, quote_id: &Uuid) -> Result, 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, 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::, _>>()?) + } +} + +#[async_trait] +impl MintProofsDatabase for SQLMintDatabase +where + DB: DatabaseConnector, +{ + type Err = Error; + + async fn get_proofs_by_ys(&self, ys: &[PublicKey]) -> Result>, 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::, Error>>()?; + + Ok(ys.iter().map(|y| proofs.remove(y)).collect()) + } + + async fn get_proof_ys_by_quote_id(&self, quote_id: &Uuid) -> Result, 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::, _>>()? + .ys()?) + } + + async fn get_proofs_states(&self, ys: &[PublicKey]) -> Result>, 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>), 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::, _>>()? + .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, + ) -> 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>, 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::, Error>>()?; + Ok(blinded_messages + .iter() + .map(|y| blinded_signatures.remove(y)) + .collect()) + } +} + +#[async_trait] +impl MintSignaturesDatabase for SQLMintDatabase +where + DB: DatabaseConnector, +{ + type Err = Error; + + async fn get_blind_signatures( + &self, + blinded_messages: &[PublicKey], + ) -> Result>, 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::, Error>>()?; + Ok(blinded_messages + .iter() + .map(|y| blinded_signatures.remove(y)) + .collect()) + } + + async fn get_blind_signatures_for_keyset( + &self, + keyset_id: &Id, + ) -> Result, 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::, _>>()?) + } + + /// Get [`BlindSignature`]s for quote + async fn get_blind_signatures_for_quote( + &self, + quote_id: &Uuid, + ) -> Result, 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::, _>>()?) + } +} + +#[async_trait] +impl MintDatabase for SQLMintDatabase +where + DB: DatabaseConnector, +{ + async fn begin_transaction<'a>( + &'a self, + ) -> Result + Send + Sync + 'a>, Error> { + Ok(Box::new(SQLTransaction { + inner: self.db.begin().await?, + _phantom: PhantomData, + })) + } + + async fn get_mint_info(&self) -> Result { + Ok(self.fetch_from_config("mint_info").await?) + } + + async fn get_quote_ttl(&self) -> Result { + Ok(self.fetch_from_config("quote_ttl").await?) + } +} + +fn sql_row_to_keyset_info(row: Vec) -> Result { + 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, + payments: Vec, + issueances: Vec, +) -> Result { + 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 = 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) -> Result { + 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) -> Result { + 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) -> Result<(Proof, Option), 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) -> Result { + 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, + }) +} diff --git a/crates/cdk-sqlite/src/pool.rs b/crates/cdk-sql-common/src/pool.rs similarity index 76% rename from crates/cdk-sqlite/src/pool.rs rename to crates/cdk-sql-common/src/pool.rs index c1b411aa..3085a0ee 100644 --- a/crates/cdk-sqlite/src/pool.rs +++ b/crates/cdk-sql-common/src/pool.rs @@ -4,7 +4,7 @@ use std::fmt::Debug; use std::ops::{Deref, DerefMut}; -use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::sync::{Arc, Condvar, Mutex}; use std::time::Duration; @@ -30,13 +30,20 @@ pub trait ResourceManager: Debug { type Resource: Debug; /// The configuration that is needed in order to create the resource - type Config: Debug; + type Config: Clone + Debug; /// The error the resource may return when creating a new instance type Error: Debug; - /// Creates a new resource with a given config - fn new_resource(config: &Self::Config) -> Result>; + /// Creates a new resource with a given config. + /// + /// If `stale` is ever set to TRUE it is assumed the resource is no longer valid and it will be + /// dropped. + fn new_resource( + config: &Self::Config, + stale: Arc, + timeout: Duration, + ) -> Result>; /// The object is dropped fn drop(_resource: Self::Resource) {} @@ -49,7 +56,7 @@ where RM: ResourceManager, { config: RM::Config, - queue: Mutex>, + queue: Mutex, RM::Resource)>>, in_use: AtomicUsize, max_size: usize, default_timeout: Duration, @@ -61,7 +68,7 @@ pub struct PooledResource where RM: ResourceManager, { - resource: Option, + resource: Option<(Arc, RM::Resource)>, pool: Arc>, } @@ -88,7 +95,7 @@ where type Target = RM::Resource; fn deref(&self) -> &Self::Target { - self.resource.as_ref().expect("resource already dropped") + &self.resource.as_ref().expect("resource already dropped").1 } } @@ -97,7 +104,7 @@ where RM: ResourceManager, { fn deref_mut(&mut self) -> &mut Self::Target { - self.resource.as_mut().expect("resource already dropped") + &mut self.resource.as_mut().expect("resource already dropped").1 } } @@ -135,22 +142,28 @@ where let mut resources = self.queue.lock().map_err(|_| Error::Poison)?; loop { - if let Some(resource) = resources.pop() { - drop(resources); - self.in_use.fetch_add(1, Ordering::AcqRel); + if let Some((stale, resource)) = resources.pop() { + if !stale.load(Ordering::SeqCst) { + drop(resources); + self.in_use.fetch_add(1, Ordering::AcqRel); - return Ok(PooledResource { - resource: Some(resource), - pool: self.clone(), - }); + return Ok(PooledResource { + resource: Some((stale, resource)), + pool: self.clone(), + }); + } } if self.in_use.load(Ordering::Relaxed) < self.max_size { drop(resources); self.in_use.fetch_add(1, Ordering::AcqRel); + let stale: Arc = Arc::new(false.into()); return Ok(PooledResource { - resource: Some(RM::new_resource(&self.config)?), + resource: Some(( + stale.clone(), + RM::new_resource(&self.config, stale, timeout)?, + )), pool: self.clone(), }); } @@ -178,7 +191,7 @@ where if let Ok(mut resources) = self.queue.lock() { loop { while let Some(resource) = resources.pop() { - RM::drop(resource); + RM::drop(resource.1); } if self.in_use.load(Ordering::Relaxed) == 0 { diff --git a/crates/cdk-sql-common/src/stmt.rs b/crates/cdk-sql-common/src/stmt.rs new file mode 100644 index 00000000..9f686e54 --- /dev/null +++ b/crates/cdk-sql-common/src/stmt.rs @@ -0,0 +1,359 @@ +//! Stataments mod +use std::collections::HashMap; +use std::sync::{Arc, RwLock}; + +use cdk_common::database::Error; +use once_cell::sync::Lazy; + +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), +} + +impl From for PlaceholderValue { + fn from(value: Value) -> Self { + PlaceholderValue::Value(value) + } +} + +impl From> for PlaceholderValue { + fn from(value: Vec) -> Self { + PlaceholderValue::Set(value) + } +} + +/// SQL Part +#[derive(Debug, Clone)] +pub enum SqlPart { + /// Raw SQL statement + Raw(Arc), + /// Placeholder + Placeholder(Arc, Option), +} + +/// 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, 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("e) { + // 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) +} + +type Cache = HashMap, Option>)>; + +/// Sql message +#[derive(Debug, Default)] +pub struct Statement { + cache: Arc>, + cached_sql: Option>, + sql: Option, + /// The SQL statement + pub parts: Vec, + /// The expected response type + pub expected_response: ExpectedSqlResponse, +} + +impl Statement { + /// Creates a new statement + fn new(sql: &str, cache: Arc>) -> Result { + let parsed = cache + .read() + .map(|cache| cache.get(sql).cloned()) + .ok() + .flatten(); + + if let Some((parts, cached_sql)) = parsed { + Ok(Self { + parts, + cached_sql, + sql: None, + cache, + ..Default::default() + }) + } else { + let parts = split_sql_parts(sql)?; + + if let Ok(mut cache) = cache.write() { + cache.insert(sql.to_owned(), (parts.clone(), None)); + } else { + tracing::warn!("Failed to acquire write lock for SQL statement cache"); + } + + Ok(Self { + parts, + sql: Some(sql.to_owned()), + cache, + ..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), Error> { + if let Some(cached_sql) = self.cached_sql { + let sql = cached_sql.to_string(); + let values = self + .parts + .into_iter() + .map(|x| match x { + SqlPart::Placeholder(name, value) => { + match value.ok_or(Error::MissingPlaceholder(name.to_string()))? { + PlaceholderValue::Value(value) => Ok(vec![value]), + PlaceholderValue::Set(values) => Ok(values), + } + } + SqlPart::Raw(_) => Ok(vec![]), + }) + .collect::, Error>>()? + .into_iter() + .flatten() + .collect::>(); + return Ok((sql, values)); + } + + let mut placeholder_values = Vec::new(); + let mut can_be_cached = true; + 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) => { + can_be_cached = false; + let start_size = placeholder_values.len(); + placeholder_values.append(&mut values); + let placeholders = (start_size + 1..=placeholder_values.len()) + .map(|i| format!("${i}")) + .collect::>() + .join(", "); + Ok(placeholders) + } + } + } + SqlPart::Raw(raw) => Ok(raw.trim().to_string()), + }) + .collect::, _>>()? + .join(" "); + + if can_be_cached { + if let Some(original_sql) = self.sql { + let _ = self.cache.write().map(|mut cache| { + if let Some((_, cached_sql)) = cache.get_mut(&original_sql) { + *cached_sql = Some(sql.clone().into()); + } + }); + } + } + + Ok((sql, placeholder_values)) + } + + /// Binds a given placeholder to a value. + #[inline] + pub fn bind(mut self, name: C, value: V) -> Self + where + C: ToString, + V: Into, + { + 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(mut self, name: C, value: Vec) -> Self + where + C: ToString, + V: Into, + { + let name = name.to_string(); + let value: PlaceholderValue = value + .into_iter() + .map(|x| x.into()) + .collect::>() + .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(self, conn: &C) -> Result, Error> + where + C: DatabaseExecutor, + { + conn.pluck(self).await + } + + /// Executes a query and returns the affected rows + pub async fn batch(self, conn: &C) -> Result<(), Error> + where + C: DatabaseExecutor, + { + conn.batch(self).await + } + + /// Executes a query and returns the affected rows + pub async fn execute(self, conn: &C) -> Result + where + C: DatabaseExecutor, + { + conn.execute(self).await + } + + /// Runs the query and returns the first row or None + pub async fn fetch_one(self, conn: &C) -> Result>, Error> + where + C: DatabaseExecutor, + { + conn.fetch_one(self).await + } + + /// Runs the query and returns the first row or None + pub async fn fetch_all(self, conn: &C) -> Result>, Error> + where + C: DatabaseExecutor, + { + conn.fetch_all(self).await + } +} + +/// Creates a new query statement +#[inline(always)] +pub fn query(sql: &str) -> Result { + static CACHE: Lazy>> = Lazy::new(|| Arc::new(RwLock::new(HashMap::new()))); + Statement::new(sql, CACHE.clone()).map_err(|e| Error::Database(Box::new(e))) +} diff --git a/crates/cdk-sql-common/src/value.rs b/crates/cdk-sql-common/src/value.rs new file mode 100644 index 00000000..4fd0e77e --- /dev/null +++ b/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), +} + +impl From 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> for Value { + fn from(value: Vec) -> Self { + Self::Blob(value) + } +} + +impl From<&[u8]> for Value { + fn from(value: &[u8]) -> Self { + Self::Blob(value.to_owned()) + } +} + +impl From for Value { + fn from(value: u8) -> Self { + Self::Integer(value.into()) + } +} + +impl From for Value { + fn from(value: i64) -> Self { + Self::Integer(value) + } +} + +impl From for Value { + fn from(value: u32) -> Self { + Self::Integer(value.into()) + } +} + +impl From for Value { + fn from(value: bool) -> Self { + Self::Integer(if value { 1 } else { 0 }) + } +} + +impl From> for Value +where + T: Into, +{ + fn from(value: Option) -> Self { + match value { + Some(v) => v.into(), + None => Value::Null, + } + } +} diff --git a/crates/cdk-sqlite/src/wallet/error.rs b/crates/cdk-sql-common/src/wallet/error.rs similarity index 98% rename from crates/cdk-sqlite/src/wallet/error.rs rename to crates/cdk-sql-common/src/wallet/error.rs index e0886c27..8c5ae4c5 100644 --- a/crates/cdk-sqlite/src/wallet/error.rs +++ b/crates/cdk-sql-common/src/wallet/error.rs @@ -2,7 +2,7 @@ use thiserror::Error; -/// SQLite Wallet Error +/// SQL Wallet Error #[derive(Debug, Error)] pub enum Error { /// SQLX Error diff --git a/crates/cdk-sql-common/src/wallet/migrations.rs b/crates/cdk-sql-common/src/wallet/migrations.rs new file mode 100644 index 00000000..f1011b5e --- /dev/null +++ b/crates/cdk-sql-common/src/wallet/migrations.rs @@ -0,0 +1,21 @@ +/// @generated +/// Auto-generated by build.rs +pub static MIGRATIONS: &[(&str, &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"#)), +]; diff --git a/crates/cdk-sqlite/src/wallet/migrations/20240612132920_init.sql b/crates/cdk-sql-common/src/wallet/migrations/sqlite/20240612132920_init.sql similarity index 100% rename from crates/cdk-sqlite/src/wallet/migrations/20240612132920_init.sql rename to crates/cdk-sql-common/src/wallet/migrations/sqlite/20240612132920_init.sql diff --git a/crates/cdk-sqlite/src/wallet/migrations/20240618200350_quote_state.sql b/crates/cdk-sql-common/src/wallet/migrations/sqlite/20240618200350_quote_state.sql similarity index 100% rename from crates/cdk-sqlite/src/wallet/migrations/20240618200350_quote_state.sql rename to crates/cdk-sql-common/src/wallet/migrations/sqlite/20240618200350_quote_state.sql diff --git a/crates/cdk-sqlite/src/wallet/migrations/20240626091921_nut04_state.sql b/crates/cdk-sql-common/src/wallet/migrations/sqlite/20240626091921_nut04_state.sql similarity index 100% rename from crates/cdk-sqlite/src/wallet/migrations/20240626091921_nut04_state.sql rename to crates/cdk-sql-common/src/wallet/migrations/sqlite/20240626091921_nut04_state.sql diff --git a/crates/cdk-sqlite/src/wallet/migrations/20240710144711_input_fee.sql b/crates/cdk-sql-common/src/wallet/migrations/sqlite/20240710144711_input_fee.sql similarity index 100% rename from crates/cdk-sqlite/src/wallet/migrations/20240710144711_input_fee.sql rename to crates/cdk-sql-common/src/wallet/migrations/sqlite/20240710144711_input_fee.sql diff --git a/crates/cdk-sqlite/src/wallet/migrations/20240810214105_mint_icon_url.sql b/crates/cdk-sql-common/src/wallet/migrations/sqlite/20240810214105_mint_icon_url.sql similarity index 100% rename from crates/cdk-sqlite/src/wallet/migrations/20240810214105_mint_icon_url.sql rename to crates/cdk-sql-common/src/wallet/migrations/sqlite/20240810214105_mint_icon_url.sql diff --git a/crates/cdk-sqlite/src/wallet/migrations/20240810233905_update_mint_url.sql b/crates/cdk-sql-common/src/wallet/migrations/sqlite/20240810233905_update_mint_url.sql similarity index 100% rename from crates/cdk-sqlite/src/wallet/migrations/20240810233905_update_mint_url.sql rename to crates/cdk-sql-common/src/wallet/migrations/sqlite/20240810233905_update_mint_url.sql diff --git a/crates/cdk-sqlite/src/wallet/migrations/20240902151515_icon_url.sql b/crates/cdk-sql-common/src/wallet/migrations/sqlite/20240902151515_icon_url.sql similarity index 100% rename from crates/cdk-sqlite/src/wallet/migrations/20240902151515_icon_url.sql rename to crates/cdk-sql-common/src/wallet/migrations/sqlite/20240902151515_icon_url.sql diff --git a/crates/cdk-sqlite/src/wallet/migrations/20240902210905_mint_time.sql b/crates/cdk-sql-common/src/wallet/migrations/sqlite/20240902210905_mint_time.sql similarity index 100% rename from crates/cdk-sqlite/src/wallet/migrations/20240902210905_mint_time.sql rename to crates/cdk-sql-common/src/wallet/migrations/sqlite/20240902210905_mint_time.sql diff --git a/crates/cdk-sqlite/src/wallet/migrations/20241011125207_mint_urls.sql b/crates/cdk-sql-common/src/wallet/migrations/sqlite/20241011125207_mint_urls.sql similarity index 100% rename from crates/cdk-sqlite/src/wallet/migrations/20241011125207_mint_urls.sql rename to crates/cdk-sql-common/src/wallet/migrations/sqlite/20241011125207_mint_urls.sql diff --git a/crates/cdk-sqlite/src/wallet/migrations/20241108092756_wallet_mint_quote_secretkey.sql b/crates/cdk-sql-common/src/wallet/migrations/sqlite/20241108092756_wallet_mint_quote_secretkey.sql similarity index 100% rename from crates/cdk-sqlite/src/wallet/migrations/20241108092756_wallet_mint_quote_secretkey.sql rename to crates/cdk-sql-common/src/wallet/migrations/sqlite/20241108092756_wallet_mint_quote_secretkey.sql diff --git a/crates/cdk-sqlite/src/wallet/migrations/20250214135017_mint_tos.sql b/crates/cdk-sql-common/src/wallet/migrations/sqlite/20250214135017_mint_tos.sql similarity index 100% rename from crates/cdk-sqlite/src/wallet/migrations/20250214135017_mint_tos.sql rename to crates/cdk-sql-common/src/wallet/migrations/sqlite/20250214135017_mint_tos.sql diff --git a/crates/cdk-sqlite/src/wallet/migrations/20250310111513_drop_nostr_last_checked.sql b/crates/cdk-sql-common/src/wallet/migrations/sqlite/20250310111513_drop_nostr_last_checked.sql similarity index 100% rename from crates/cdk-sqlite/src/wallet/migrations/20250310111513_drop_nostr_last_checked.sql rename to crates/cdk-sql-common/src/wallet/migrations/sqlite/20250310111513_drop_nostr_last_checked.sql diff --git a/crates/cdk-sqlite/src/wallet/migrations/20250314082116_allow_pending_spent.sql b/crates/cdk-sql-common/src/wallet/migrations/sqlite/20250314082116_allow_pending_spent.sql similarity index 100% rename from crates/cdk-sqlite/src/wallet/migrations/20250314082116_allow_pending_spent.sql rename to crates/cdk-sql-common/src/wallet/migrations/sqlite/20250314082116_allow_pending_spent.sql diff --git a/crates/cdk-sqlite/src/wallet/migrations/20250323152040_wallet_dleq_proofs.sql b/crates/cdk-sql-common/src/wallet/migrations/sqlite/20250323152040_wallet_dleq_proofs.sql similarity index 100% rename from crates/cdk-sqlite/src/wallet/migrations/20250323152040_wallet_dleq_proofs.sql rename to crates/cdk-sql-common/src/wallet/migrations/sqlite/20250323152040_wallet_dleq_proofs.sql diff --git a/crates/cdk-sqlite/src/wallet/migrations/20250401120000_add_transactions_table.sql b/crates/cdk-sql-common/src/wallet/migrations/sqlite/20250401120000_add_transactions_table.sql similarity index 100% rename from crates/cdk-sqlite/src/wallet/migrations/20250401120000_add_transactions_table.sql rename to crates/cdk-sql-common/src/wallet/migrations/sqlite/20250401120000_add_transactions_table.sql diff --git a/crates/cdk-sqlite/src/wallet/migrations/20250616144830_add_keyset_expiry.sql b/crates/cdk-sql-common/src/wallet/migrations/sqlite/20250616144830_add_keyset_expiry.sql similarity index 100% rename from crates/cdk-sqlite/src/wallet/migrations/20250616144830_add_keyset_expiry.sql rename to crates/cdk-sql-common/src/wallet/migrations/sqlite/20250616144830_add_keyset_expiry.sql diff --git a/crates/cdk-sqlite/src/wallet/migrations/20250707093445_bolt12.sql b/crates/cdk-sql-common/src/wallet/migrations/sqlite/20250707093445_bolt12.sql similarity index 100% rename from crates/cdk-sqlite/src/wallet/migrations/20250707093445_bolt12.sql rename to crates/cdk-sql-common/src/wallet/migrations/sqlite/20250707093445_bolt12.sql diff --git a/crates/cdk-sql-common/src/wallet/mod.rs b/crates/cdk-sql-common/src/wallet/mod.rs new file mode 100644 index 00000000..584aefa2 --- /dev/null +++ b/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 +where + T: DatabaseExecutor, +{ + db: T, +} + +impl SQLWalletDatabase +where + DB: DatabaseExecutor, +{ + /// Creates a new instance + pub async fn new(db: X) -> Result + where + X: Into, + { + 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 WalletDatabase for SQLWalletDatabase +where + T: DatabaseExecutor, +{ + type Err = database::Error; + + #[instrument(skip(self))] + async fn get_melt_quotes(&self) -> Result, 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::>()?) + } + + #[instrument(skip(self, mint_info))] + async fn add_mint( + &self, + mint_url: MintUrl, + mint_info: Option, + ) -> 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, 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>, 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::, 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, + ) -> 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>, 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::, 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, 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, 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, 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::>()?) + } + + #[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, 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, 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, + removed_ys: Vec, + ) -> 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, + unit: Option, + state: Option>, + spending_conditions: Option>, + ) -> Result, 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::>()) + } + + async fn update_proofs_state(&self, ys: Vec, 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, 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::>(); + + 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, 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, + direction: Option, + unit: Option, + ) -> Result, 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::>()) + } + + #[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) -> Result { + 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) -> Result { + 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) -> Result { + 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 = 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) -> Result { + 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) -> Result { + 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) -> Result { + 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::, _>>()?, + 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(), + }) +} diff --git a/crates/cdk-sql-common/tests/legacy-sqlx.sql b/crates/cdk-sql-common/tests/legacy-sqlx.sql new file mode 100644 index 00000000..e1237f36 --- /dev/null +++ b/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; diff --git a/crates/cdk-sqlite/Cargo.toml b/crates/cdk-sqlite/Cargo.toml index 6cfb99a5..264271dd 100644 --- a/crates/cdk-sqlite/Cargo.toml +++ b/crates/cdk-sqlite/Cargo.toml @@ -13,18 +13,19 @@ readme = "README.md" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] default = ["mint", "wallet", "auth"] -mint = ["cdk-common/mint"] -wallet = ["cdk-common/wallet"] -auth = ["cdk-common/auth"] +mint = ["cdk-common/mint", "cdk-sql-common/mint"] +wallet = ["cdk-common/wallet", "cdk-sql-common/wallet"] +auth = ["cdk-common/auth", "cdk-sql-common/auth"] sqlcipher = ["rusqlite/bundled-sqlcipher"] [dependencies] async-trait.workspace = true cdk-common = { workspace = true, features = ["test"] } bitcoin.workspace = true +cdk-sql-common = { workspace = true } rusqlite = { version = "0.31", features = ["bundled"]} thiserror.workspace = true -tokio.workspace = true +tokio = { workspace = true, features = ["rt-multi-thread"]} tracing.workspace = true serde.workspace = true serde_json.workspace = true diff --git a/crates/cdk-sqlite/src/common.rs b/crates/cdk-sqlite/src/common.rs index e04a00de..e0ea6f9e 100644 --- a/crates/cdk-sqlite/src/common.rs +++ b/crates/cdk-sqlite/src/common.rs @@ -1,12 +1,13 @@ +use std::sync::atomic::AtomicBool; use std::sync::Arc; use std::time::Duration; -use rusqlite::{params, Connection}; - -use crate::pool::{Pool, ResourceManager}; +use cdk_sql_common::pool::{self, Pool, ResourceManager}; +use cdk_sql_common::value::Value; +use rusqlite::Connection; /// The config need to create a new SQLite connection -#[derive(Debug)] +#[derive(Clone, Debug)] pub struct Config { path: Option, password: Option, @@ -25,7 +26,9 @@ impl ResourceManager for SqliteConnectionManager { fn new_resource( config: &Self::Config, - ) -> Result> { + _stale: Arc, + _timeout: Duration, + ) -> Result> { let conn = if let Some(path) = config.path.as_ref() { Connection::open(path)? } else { @@ -57,14 +60,8 @@ impl ResourceManager for SqliteConnectionManager { /// For SQLCipher support, enable the "sqlcipher" feature and pass a password. pub fn create_sqlite_pool( path: &str, - #[cfg(feature = "sqlcipher")] password: String, + password: Option, ) -> Arc> { - #[cfg(feature = "sqlcipher")] - let password = Some(password); - - #[cfg(not(feature = "sqlcipher"))] - let password = None; - let (config, max_size) = if path.contains(":memory:") { ( Config { @@ -86,52 +83,26 @@ pub fn create_sqlite_pool( Pool::new(config, max_size, Duration::from_secs(10)) } -/// Migrates the migration generated by `build.rs` -pub fn migrate(conn: &mut Connection, migrations: &[(&str, &str)]) -> Result<(), rusqlite::Error> { - let tx = conn.transaction()?; - tx.execute( - r#" - CREATE TABLE IF NOT EXISTS migrations ( - name TEXT PRIMARY KEY, - applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ) - "#, - [], - )?; - - if tx.query_row( - r#"select count(*) from sqlite_master where name = '_sqlx_migrations'"#, - [], - |row| row.get::<_, i32>(0), - )? == 1 - { - tx.execute_batch( - r#" - INSERT INTO migrations - SELECT - version || '_' || REPLACE(description, ' ', '_') || '.sql', - execution_time - FROM _sqlx_migrations; - DROP TABLE _sqlx_migrations; - "#, - )?; +/// Convert cdk_sql_common::value::Value to rusqlite Value +#[inline(always)] +pub fn to_sqlite(v: Value) -> rusqlite::types::Value { + match v { + Value::Blob(blob) => rusqlite::types::Value::Blob(blob), + Value::Integer(i) => rusqlite::types::Value::Integer(i), + Value::Null => rusqlite::types::Value::Null, + Value::Text(t) => rusqlite::types::Value::Text(t), + Value::Real(r) => rusqlite::types::Value::Real(r), + } +} + +/// 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), } - - // 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])?; - } - } - - tx.commit()?; - - Ok(()) } diff --git a/crates/cdk-sqlite/src/lib.rs b/crates/cdk-sqlite/src/lib.rs index 0a4c5673..4c50a5ff 100644 --- a/crates/cdk-sqlite/src/lib.rs +++ b/crates/cdk-sqlite/src/lib.rs @@ -4,9 +4,6 @@ #![warn(rustdoc::bare_urls)] mod common; -mod macros; -mod pool; -mod stmt; #[cfg(feature = "mint")] pub mod mint; diff --git a/crates/cdk-sqlite/src/mint/async_rusqlite.rs b/crates/cdk-sqlite/src/mint/async_rusqlite.rs index 3cd3593a..920660f9 100644 --- a/crates/cdk-sqlite/src/mint/async_rusqlite.rs +++ b/crates/cdk-sqlite/src/mint/async_rusqlite.rs @@ -1,16 +1,20 @@ +//! Async, pipelined rusqlite client use std::marker::PhantomData; +use std::path::PathBuf; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::{mpsc as std_mpsc, Arc, Mutex}; use std::thread::spawn; use std::time::Instant; +use cdk_common::database::Error; +use cdk_sql_common::database::{DatabaseConnector, DatabaseExecutor, DatabaseTransaction}; +use cdk_sql_common::pool::{self, Pool, PooledResource}; +use cdk_sql_common::stmt::{Column, ExpectedSqlResponse, Statement as InnerStatement}; +use cdk_sql_common::ConversionError; use rusqlite::{ffi, Connection, ErrorCode, TransactionBehavior}; use tokio::sync::{mpsc, oneshot}; -use crate::common::SqliteConnectionManager; -use crate::mint::Error; -use crate::pool::{Pool, PooledResource}; -use crate::stmt::{Column, ExpectedSqlResponse, Statement as InnerStatement, Value}; +use crate::common::{create_sqlite_pool, from_sqlite, to_sqlite, SqliteConnectionManager}; /// The number of queued SQL statements before it start failing const SQL_QUEUE_SIZE: usize = 10_000; @@ -25,9 +29,57 @@ pub struct AsyncRusqlite { inflight_requests: Arc, } +impl From for AsyncRusqlite { + fn from(value: PathBuf) -> Self { + AsyncRusqlite::new(create_sqlite_pool(value.to_str().unwrap_or_default(), None)) + } +} + +impl From<&str> for AsyncRusqlite { + fn from(value: &str) -> Self { + AsyncRusqlite::new(create_sqlite_pool(value, None)) + } +} + +impl From<(&str, &str)> for AsyncRusqlite { + fn from((value, pass): (&str, &str)) -> Self { + AsyncRusqlite::new(create_sqlite_pool(value, Some(pass.to_owned()))) + } +} + +impl From<(PathBuf, &str)> for AsyncRusqlite { + fn from((value, pass): (PathBuf, &str)) -> Self { + AsyncRusqlite::new(create_sqlite_pool( + value.to_str().unwrap_or_default(), + Some(pass.to_owned()), + )) + } +} + +impl From<(&str, String)> for AsyncRusqlite { + fn from((value, pass): (&str, String)) -> Self { + AsyncRusqlite::new(create_sqlite_pool(value, Some(pass))) + } +} + +impl From<(PathBuf, String)> for AsyncRusqlite { + fn from((value, pass): (PathBuf, String)) -> Self { + AsyncRusqlite::new(create_sqlite_pool( + value.to_str().unwrap_or_default(), + Some(pass), + )) + } +} + +impl From<&PathBuf> for AsyncRusqlite { + fn from(value: &PathBuf) -> Self { + AsyncRusqlite::new(create_sqlite_pool(value.to_str().unwrap_or_default(), None)) + } +} + /// Internal request for the database thread #[derive(Debug)] -pub enum DbRequest { +enum DbRequest { Sql(InnerStatement, oneshot::Sender), Begin(oneshot::Sender), Commit(oneshot::Sender), @@ -35,97 +87,67 @@ pub enum DbRequest { } #[derive(Debug)] -pub enum DbResponse { +enum DbResponse { Transaction(mpsc::Sender), AffectedRows(usize), Pluck(Option), Row(Option>), Rows(Vec>), - Error(Error), + Error(SqliteError), Unexpected, Ok, } -/// Statement for the async_rusqlite wrapper -pub struct Statement(InnerStatement); +#[derive(thiserror::Error, Debug)] +enum SqliteError { + #[error(transparent)] + Sqlite(#[from] rusqlite::Error), -impl Statement { - /// Bind a variable - pub fn bind(self, name: C, value: V) -> Self - where - C: ToString, - V: Into, - { - Self(self.0.bind(name, value)) - } + #[error(transparent)] + Inner(#[from] Error), - /// Bind vec - pub fn bind_vec(self, name: C, value: Vec) -> Self - where - C: ToString, - V: Into, - { - Self(self.0.bind_vec(name, value)) - } + #[error(transparent)] + Pool(#[from] pool::Error), - /// Executes a query and return the number of affected rows - pub async fn execute(self, conn: &C) -> Result - where - C: DatabaseExecutor + Send + Sync, - { - conn.execute(self.0).await - } + /// Duplicate entry + #[error("Duplicate")] + Duplicate, - /// Returns the first column of the first row of the query result - pub async fn pluck(self, conn: &C) -> Result, Error> - where - C: DatabaseExecutor + Send + Sync, - { - conn.pluck(self.0).await - } + #[error(transparent)] + Conversion(#[from] ConversionError), +} - /// Returns the first row of the query result - pub async fn fetch_one(self, conn: &C) -> Result>, Error> - where - C: DatabaseExecutor + Send + Sync, - { - conn.fetch_one(self.0).await - } - - /// Returns all rows of the query result - pub async fn fetch_all(self, conn: &C) -> Result>, Error> - where - C: DatabaseExecutor + Send + Sync, - { - conn.fetch_all(self.0).await +impl From for Error { + fn from(val: SqliteError) -> Self { + match val { + SqliteError::Duplicate => Error::Duplicate, + SqliteError::Conversion(e) => e.into(), + o => Error::Internal(o.to_string()), + } } } /// Process a query #[inline(always)] -fn process_query(conn: &Connection, sql: InnerStatement) -> Result { +fn process_query(conn: &Connection, statement: InnerStatement) -> Result { let start = Instant::now(); - let mut args = sql.args; - let mut stmt = conn.prepare_cached(&sql.sql)?; - let total_parameters = stmt.parameter_count(); + let expected_response = statement.expected_response; + let (sql, placeholder_values) = statement.to_sql()?; + let sql = sql.trim_end_matches("FOR UPDATE"); - 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; - }; - - stmt.raw_bind_parameter(index, value)?; + let mut stmt = conn.prepare_cached(sql)?; + for (i, value) in placeholder_values.into_iter().enumerate() { + stmt.raw_bind_parameter(i + 1, to_sqlite(value))?; } let columns = stmt.column_count(); - let to_return = match sql.expected_response { + let to_return = match expected_response { ExpectedSqlResponse::AffectedRows => DbResponse::AffectedRows(stmt.raw_execute()?), + ExpectedSqlResponse::Batch => { + conn.execute_batch(sql)?; + DbResponse::Ok + } ExpectedSqlResponse::ManyRows => { let mut rows = stmt.raw_query(); let mut results = vec![]; @@ -133,7 +155,7 @@ fn process_query(conn: &Connection, sql: InnerStatement) -> Result, _>>()?, ) } @@ -142,7 +164,11 @@ fn process_query(conn: &Connection, sql: InnerStatement) -> Result { let mut rows = stmt.raw_query(); - DbResponse::Pluck(rows.next()?.map(|row| row.get(0usize)).transpose()?) + DbResponse::Pluck( + rows.next()? + .map(|row| row.get(0usize).map(from_sqlite)) + .transpose()?, + ) } ExpectedSqlResponse::SingleRow => { let mut rows = stmt.raw_query(); @@ -150,7 +176,7 @@ fn process_query(conn: &Connection, sql: InnerStatement) -> Result, _>>() }) .transpose()?; @@ -161,7 +187,7 @@ fn process_query(conn: &Connection, sql: InnerStatement) -> Result SLOW_QUERY_THRESHOLD_MS { - tracing::warn!("[SLOW QUERY] Took {} ms: {}", duration.as_millis(), sql.sql); + tracing::warn!("[SLOW QUERY] Took {} ms: {}", duration.as_millis(), sql); } Ok(to_return) @@ -196,13 +222,12 @@ fn rusqlite_spawn_worker_threads( let inflight_requests = inflight_requests.clone(); spawn(move || loop { while let Ok((conn, sql, reply_to)) = rx.lock().expect("failed to acquire").recv() { - tracing::trace!("Execute query: {}", sql.sql); let result = process_query(&conn, sql); let _ = match result { Ok(ok) => reply_to.send(ok), Err(err) => { tracing::error!("Failed query with error {:?}", err); - let err = if let Error::Sqlite(rusqlite::Error::SqliteFailure( + let err = if let SqliteError::Sqlite(rusqlite::Error::SqliteFailure( ffi::Error { code, extended_code, @@ -214,7 +239,7 @@ fn rusqlite_spawn_worker_threads( && (*extended_code == ffi::SQLITE_CONSTRAINT_PRIMARYKEY || *extended_code == ffi::SQLITE_CONSTRAINT_UNIQUE) { - Error::Duplicate + SqliteError::Duplicate } else { err } @@ -256,7 +281,7 @@ fn rusqlite_worker_manager( while let Some(request) = receiver.blocking_recv() { inflight_requests.fetch_add(1, Ordering::Relaxed); match request { - DbRequest::Sql(sql, reply_to) => { + DbRequest::Sql(statement, reply_to) => { let conn = match pool.get() { Ok(conn) => conn, Err(err) => { @@ -267,7 +292,7 @@ fn rusqlite_worker_manager( } }; - let _ = send_sql_to_thread.send((conn, sql, reply_to)); + let _ = send_sql_to_thread.send((conn, statement, reply_to)); continue; } DbRequest::Begin(reply_to) => { @@ -341,9 +366,9 @@ fn rusqlite_worker_manager( DbRequest::Begin(reply_to) => { let _ = reply_to.send(DbResponse::Unexpected); } - DbRequest::Sql(sql, reply_to) => { - tracing::trace!("Tx {}: SQL {}", tx_id, sql.sql); - let _ = match process_query(&tx, sql) { + DbRequest::Sql(statement, reply_to) => { + tracing::trace!("Tx {}: SQL {:?}", tx_id, statement); + let _ = match process_query(&tx, statement) { Ok(ok) => reply_to.send(ok), Err(err) => { tracing::error!( @@ -351,7 +376,7 @@ fn rusqlite_worker_manager( tx_id, err ); - let err = if let Error::Sqlite( + let err = if let SqliteError::Sqlite( rusqlite::Error::SqliteFailure( ffi::Error { code, @@ -365,7 +390,7 @@ fn rusqlite_worker_manager( && (*extended_code == ffi::SQLITE_CONSTRAINT_PRIMARYKEY || *extended_code == ffi::SQLITE_CONSTRAINT_UNIQUE) { - Error::Duplicate + SqliteError::Duplicate } else { err } @@ -395,83 +420,6 @@ fn rusqlite_worker_manager( } } -#[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; - - /// Executes a query and returns the affected rows - async fn execute(&self, mut statement: InnerStatement) -> Result { - let (sender, receiver) = oneshot::channel(); - statement.expected_response = ExpectedSqlResponse::AffectedRows; - self.get_queue_sender() - .send(DbRequest::Sql(statement, sender)) - .await - .map_err(|_| Error::Communication)?; - - match receiver.await.map_err(|_| Error::Communication)? { - DbResponse::AffectedRows(n) => Ok(n), - DbResponse::Error(err) => Err(err), - _ => Err(Error::InvalidDbResponse), - } - } - - /// Runs the query and returns the first row or None - async fn fetch_one(&self, mut statement: InnerStatement) -> Result>, Error> { - let (sender, receiver) = oneshot::channel(); - statement.expected_response = ExpectedSqlResponse::SingleRow; - self.get_queue_sender() - .send(DbRequest::Sql(statement, sender)) - .await - .map_err(|_| Error::Communication)?; - - match receiver.await.map_err(|_| Error::Communication)? { - DbResponse::Row(row) => Ok(row), - DbResponse::Error(err) => Err(err), - _ => Err(Error::InvalidDbResponse), - } - } - - /// Runs the query and returns the first row or None - async fn fetch_all(&self, mut statement: InnerStatement) -> Result>, Error> { - let (sender, receiver) = oneshot::channel(); - statement.expected_response = ExpectedSqlResponse::ManyRows; - self.get_queue_sender() - .send(DbRequest::Sql(statement, sender)) - .await - .map_err(|_| Error::Communication)?; - - match receiver.await.map_err(|_| Error::Communication)? { - DbResponse::Rows(rows) => Ok(rows), - DbResponse::Error(err) => Err(err), - _ => Err(Error::InvalidDbResponse), - } - } - - async fn pluck(&self, mut statement: InnerStatement) -> Result, Error> { - let (sender, receiver) = oneshot::channel(); - statement.expected_response = ExpectedSqlResponse::Pluck; - self.get_queue_sender() - .send(DbRequest::Sql(statement, sender)) - .await - .map_err(|_| Error::Communication)?; - - match receiver.await.map_err(|_| Error::Communication)? { - DbResponse::Pluck(value) => Ok(value), - DbResponse::Error(err) => Err(err), - _ => Err(Error::InvalidDbResponse), - } - } -} - -#[inline(always)] -pub fn query(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>) -> Self { @@ -488,45 +436,155 @@ impl AsyncRusqlite { } } + fn get_queue_sender(&self) -> &mpsc::Sender { + &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] +impl DatabaseConnector for AsyncRusqlite { + type Transaction<'a> = Transaction<'a>; /// Begins a transaction /// /// If the transaction is Drop it will trigger a rollback operation - pub async fn begin(&self) -> Result, Error> { + async fn begin(&self) -> Result, Error> { let (sender, receiver) = oneshot::channel(); self.sender .send(DbRequest::Begin(sender)) .await - .map_err(|_| Error::Communication)?; + .map_err(|_| Error::Internal("Communication".to_owned()))?; - match receiver.await.map_err(|_| Error::Communication)? { + 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), + DbResponse::Error(err) => Err(err.into()), _ => Err(Error::InvalidDbResponse), } } } +#[async_trait::async_trait] impl DatabaseExecutor for AsyncRusqlite { - #[inline(always)] - fn get_queue_sender(&self) -> mpsc::Sender { - self.sender.clone() + fn name() -> &'static str { + "sqlite" + } + + async fn fetch_one(&self, mut statement: InnerStatement) -> Result>, 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>, 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 { + 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, 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), + } } } +/// Database transaction +#[derive(Debug)] pub struct Transaction<'conn> { db_sender: mpsc::Sender, _marker: PhantomData<&'conn ()>, } +impl Transaction<'_> { + fn get_queue_sender(&self) -> &mpsc::Sender { + &self.db_sender + } +} + impl Drop for Transaction<'_> { fn drop(&mut self) { let (sender, _) = oneshot::channel(); @@ -534,40 +592,136 @@ impl Drop for Transaction<'_> { } } -impl Transaction<'_> { - pub async fn commit(self) -> Result<(), Error> { +#[async_trait::async_trait] +impl<'a> DatabaseTransaction<'a> for Transaction<'a> { + async fn commit(self) -> Result<(), Error> { let (sender, receiver) = oneshot::channel(); self.db_sender .send(DbRequest::Commit(sender)) .await - .map_err(|_| Error::Communication)?; + .map_err(|_| Error::Internal("Communication".to_owned()))?; - match receiver.await.map_err(|_| Error::Communication)? { + match receiver + .await + .map_err(|_| Error::Internal("Communication".to_owned()))? + { DbResponse::Ok => Ok(()), - DbResponse::Error(err) => Err(err), + DbResponse::Error(err) => Err(err.into()), _ => Err(Error::InvalidDbResponse), } } - pub async fn rollback(self) -> Result<(), Error> { + async fn rollback(self) -> Result<(), Error> { let (sender, receiver) = oneshot::channel(); self.db_sender .send(DbRequest::Rollback(sender)) .await - .map_err(|_| Error::Communication)?; + .map_err(|_| Error::Internal("Communication".to_owned()))?; - match receiver.await.map_err(|_| Error::Communication)? { + match receiver + .await + .map_err(|_| Error::Internal("Communication".to_owned()))? + { DbResponse::Ok => Ok(()), - DbResponse::Error(err) => Err(err), + DbResponse::Error(err) => Err(err.into()), _ => Err(Error::InvalidDbResponse), } } } +#[async_trait::async_trait] impl DatabaseExecutor for Transaction<'_> { - /// Get the internal sender to the SQL queue - #[inline(always)] - fn get_queue_sender(&self) -> mpsc::Sender { - self.db_sender.clone() + fn name() -> &'static str { + "sqlite" + } + + async fn fetch_one(&self, mut statement: InnerStatement) -> Result>, 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>, 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 { + 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, 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), + } } } diff --git a/crates/cdk-sqlite/src/mint/auth/migrations.rs b/crates/cdk-sqlite/src/mint/auth/migrations.rs deleted file mode 100644 index 4edbb850..00000000 --- a/crates/cdk-sqlite/src/mint/auth/migrations.rs +++ /dev/null @@ -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"#)), -]; diff --git a/crates/cdk-sqlite/src/mint/error.rs b/crates/cdk-sqlite/src/mint/error.rs deleted file mode 100644 index cbc10448..00000000 --- a/crates/cdk-sqlite/src/mint/error.rs +++ /dev/null @@ -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), - /// 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 for cdk_common::database::Error { - fn from(e: Error) -> Self { - match e { - Error::Duplicate => Self::Duplicate, - e => Self::Database(Box::new(e)), - } - } -} diff --git a/crates/cdk-sqlite/src/mint/memory.rs b/crates/cdk-sqlite/src/mint/memory.rs index 54864443..7bbb718b 100644 --- a/crates/cdk-sqlite/src/mint/memory.rs +++ b/crates/cdk-sqlite/src/mint/memory.rs @@ -11,10 +11,11 @@ use super::MintSqliteDatabase; /// Creates a new in-memory [`MintSqliteDatabase`] instance pub async fn empty() -> Result { #[cfg(not(feature = "sqlcipher"))] - let db = MintSqliteDatabase::new(":memory:").await?; + let path = ":memory:"; #[cfg(feature = "sqlcipher")] - let db = MintSqliteDatabase::new(":memory:", "memory".to_string()).await?; - Ok(db) + let path = (":memory:", "memory"); + + MintSqliteDatabase::new(path).await } /// Creates a new in-memory [`MintSqliteDatabase`] instance with the given state diff --git a/crates/cdk-sqlite/src/mint/migrations.rs b/crates/cdk-sqlite/src/mint/migrations.rs deleted file mode 100644 index d8e93c90..00000000 --- a/crates/cdk-sqlite/src/mint/migrations.rs +++ /dev/null @@ -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"#)), -]; diff --git a/crates/cdk-sqlite/src/mint/mod.rs b/crates/cdk-sqlite/src/mint/mod.rs index b0251b04..acfa0958 100644 --- a/crates/cdk-sqlite/src/mint/mod.rs +++ b/crates/cdk-sqlite/src/mint/mod.rs @@ -1,1913 +1,28 @@ //! SQLite Mint -use std::collections::HashMap; -use std::ops::DerefMut; -use std::path::Path; -use std::str::FromStr; - -use async_rusqlite::{query, DatabaseExecutor, Transaction}; -use async_trait::async_trait; -use bitcoin::bip32::DerivationPath; -use cdk_common::common::QuoteTTL; -use cdk_common::database::{ - self, MintDatabase, MintDbWriterFinalizer, MintKeyDatabaseTransaction, MintKeysDatabase, - MintProofsDatabase, MintProofsTransaction, MintQuotesDatabase, MintQuotesTransaction, - MintSignatureTransaction, MintSignaturesDatabase, -}; -use cdk_common::mint::{ - self, 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 error::Error; -use lightning_invoice::Bolt11Invoice; -use tracing::instrument; -use uuid::Uuid; - -use crate::common::{create_sqlite_pool, migrate}; -use crate::stmt::Column; -use crate::{ - column_as_nullable_number, column_as_nullable_string, column_as_number, column_as_string, - unpack_into, -}; +use cdk_sql_common::mint::SQLMintAuthDatabase; +use cdk_sql_common::SQLMintDatabase; mod async_rusqlite; -#[cfg(feature = "auth")] -mod auth; -pub mod error; + pub mod memory; -#[rustfmt::skip] -mod migrations; +/// Mint SQLite implementation with rusqlite +pub type MintSqliteDatabase = SQLMintDatabase; +/// Mint Auth database with rusqlite #[cfg(feature = "auth")] -pub use auth::MintSqliteAuthDatabase; - -/// Mint SQLite Database -#[derive(Debug, Clone)] -pub struct MintSqliteDatabase { - pool: async_rusqlite::AsyncRusqlite, -} - -#[inline(always)] -async fn get_current_states( - conn: &C, - ys: &[PublicKey], -) -> Result, 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::, _>>() -} - -#[inline(always)] -async fn set_to_config(conn: &C, id: &str, value: &T) -> Result<(), Error> -where - T: ?Sized + serde::Serialize, - C: DatabaseExecutor + Send + Sync, -{ - query( - r#" - INSERT INTO config (id, value) VALUES (:id, :value) - ON CONFLICT(id) DO UPDATE SET value = excluded.value - "#, - ) - .bind(":id", id.to_owned()) - .bind(":value", serde_json::to_string(&value)?) - .execute(conn) - .await?; - - Ok(()) -} - -#[inline(always)] -async fn get_mint_quote_payments( - conn: &C, - quote_id: &Uuid, -) -> Result, 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(conn: &C, quote_id: &Uuid) -> Result, 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() -} - -impl MintSqliteDatabase { - /// Create new [`MintSqliteDatabase`] - #[cfg(not(feature = "sqlcipher"))] - pub async fn new>(path: P) -> Result { - let pool = create_sqlite_pool(path.as_ref().to_str().ok_or(Error::InvalidDbPath)?); - migrate(pool.get()?.deref_mut(), migrations::MIGRATIONS)?; - - Ok(Self { - pool: async_rusqlite::AsyncRusqlite::new(pool), - }) - } - - /// Create new [`MintSqliteDatabase`] - #[cfg(feature = "sqlcipher")] - pub async fn new>(path: P, password: String) -> Result { - let pool = create_sqlite_pool( - path.as_ref().to_str().ok_or(Error::InvalidDbPath)?, - password, - ); - migrate(pool.get()?.deref_mut(), migrations::MIGRATIONS)?; - - Ok(Self { - pool: async_rusqlite::AsyncRusqlite::new(pool), - }) - } - - #[inline(always)] - async fn fetch_from_config(&self, id: &str) -> Result - where - T: serde::de::DeserializeOwned, - { - let value = column_as_string!(query(r#"SELECT value FROM config WHERE id = :id LIMIT 1"#) - .bind(":id", id.to_owned()) - .pluck(&self.pool) - .await? - .ok_or_else(|| match id { - "mint_info" => Error::UnknownMintInfo, - "quote_ttl" => Error::UnknownQuoteTTL, - unknown => Error::UnknownConfigKey(unknown.to_string()), - })?); - - Ok(serde_json::from_str(&value)?) - } -} - -/// Sqlite Writer -pub struct SqliteTransaction<'a> { - inner: Transaction<'a>, -} - -#[async_trait] -impl<'a> database::MintTransaction<'a, database::Error> for SqliteTransaction<'a> { - async fn set_mint_info(&mut self, mint_info: MintInfo) -> Result<(), database::Error> { - Ok(set_to_config(&self.inner, "mint_info", &mint_info).await?) - } - - async fn set_quote_ttl(&mut self, quote_ttl: QuoteTTL) -> Result<(), database::Error> { - Ok(set_to_config(&self.inner, "quote_ttl", "e_ttl).await?) - } -} - -#[async_trait] -impl MintDbWriterFinalizer for SqliteTransaction<'_> { - type Err = database::Error; - - async fn commit(self: Box) -> Result<(), database::Error> { - Ok(self.inner.commit().await?) - } - - async fn rollback(self: Box) -> Result<(), database::Error> { - Ok(self.inner.rollback().await?) - } -} - -#[async_trait] -impl<'a> MintKeyDatabaseTransaction<'a, database::Error> for SqliteTransaction<'a> { - async fn add_keyset_info(&mut self, keyset: MintKeySetInfo) -> Result<(), database::Error> { - query( - r#" - INSERT INTO - keyset ( - id, unit, active, valid_from, valid_to, derivation_path, - max_order, input_fee_ppk, derivation_path_index - ) - VALUES ( - :id, :unit, :active, :valid_from, :valid_to, :derivation_path, - :max_order, :input_fee_ppk, :derivation_path_index - ) - ON CONFLICT(id) DO UPDATE SET - unit = excluded.unit, - active = excluded.active, - valid_from = excluded.valid_from, - valid_to = excluded.valid_to, - derivation_path = excluded.derivation_path, - max_order = excluded.max_order, - input_fee_ppk = excluded.input_fee_ppk, - derivation_path_index = excluded.derivation_path_index - "#, - ) - .bind(":id", keyset.id.to_string()) - .bind(":unit", keyset.unit.to_string()) - .bind(":active", keyset.active) - .bind(":valid_from", keyset.valid_from as i64) - .bind(":valid_to", keyset.final_expiry.map(|v| v as i64)) - .bind(":derivation_path", keyset.derivation_path.to_string()) - .bind(":max_order", keyset.max_order) - .bind(":input_fee_ppk", keyset.input_fee_ppk as i64) - .bind(":derivation_path_index", keyset.derivation_path_index) - .execute(&self.inner) - .await?; - - Ok(()) - } - - async fn set_active_keyset( - &mut self, - unit: CurrencyUnit, - id: Id, - ) -> Result<(), database::Error> { - query(r#"UPDATE keyset SET active=FALSE WHERE unit IS :unit"#) - .bind(":unit", unit.to_string()) - .execute(&self.inner) - .await?; - - query(r#"UPDATE keyset SET active=TRUE WHERE unit IS :unit AND id IS :id"#) - .bind(":unit", unit.to_string()) - .bind(":id", id.to_string()) - .execute(&self.inner) - .await?; - - Ok(()) - } -} - -#[async_trait] -impl MintKeysDatabase for MintSqliteDatabase { - type Err = database::Error; - - async fn begin_transaction<'a>( - &'a self, - ) -> Result< - Box + Send + Sync + 'a>, - database::Error, - > { - Ok(Box::new(SqliteTransaction { - inner: self.pool.begin().await?, - })) - } - - async fn get_active_keyset_id(&self, unit: &CurrencyUnit) -> Result, Self::Err> { - Ok( - query(r#" SELECT id FROM keyset WHERE active = 1 AND unit IS :unit"#) - .bind(":unit", unit.to_string()) - .pluck(&self.pool) - .await? - .map(|id| match id { - Column::Text(text) => Ok(Id::from_str(&text)?), - Column::Blob(id) => Ok(Id::from_bytes(&id)?), - _ => Err(Error::InvalidKeysetId), - }) - .transpose()?, - ) - } - - async fn get_active_keysets(&self) -> Result, Self::Err> { - Ok(query(r#"SELECT id, unit FROM keyset WHERE active = 1"#) - .fetch_all(&self.pool) - .await? - .into_iter() - .map(|row| { - Ok(( - column_as_string!(&row[1], CurrencyUnit::from_str), - column_as_string!(&row[0], Id::from_str, Id::from_bytes), - )) - }) - .collect::, Error>>()?) - } - - async fn get_keyset_info(&self, id: &Id) -> Result, Self::Err> { - Ok(query( - r#"SELECT - id, - unit, - active, - valid_from, - valid_to, - derivation_path, - derivation_path_index, - max_order, - input_fee_ppk - FROM - keyset - WHERE id=:id"#, - ) - .bind(":id", id.to_string()) - .fetch_one(&self.pool) - .await? - .map(sqlite_row_to_keyset_info) - .transpose()?) - } - - async fn get_keyset_infos(&self) -> Result, Self::Err> { - Ok(query( - r#"SELECT - id, - unit, - active, - valid_from, - valid_to, - derivation_path, - derivation_path_index, - max_order, - input_fee_ppk - FROM - keyset - "#, - ) - .fetch_all(&self.pool) - .await? - .into_iter() - .map(sqlite_row_to_keyset_info) - .collect::, _>>()?) - } -} - -#[async_trait] -impl<'a> MintQuotesTransaction<'a> for SqliteTransaction<'a> { - type Err = database::Error; - - #[instrument(skip(self))] - async fn increment_mint_quote_amount_paid( - &mut self, - quote_id: &Uuid, - amount_paid: Amount, - payment_id: String, - ) -> Result { - // 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 - "#, - ) - .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 - "#, - ) - .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 { - // Get current amount_issued from quote - let current_amount = query( - r#" - SELECT amount_issued - FROM mint_quote - WHERE id = :quote_id - "#, - ) - .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 - "#, - ) - .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("e.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, - ) -> 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(sqlite_row_to_melt_quote) - .transpose()? - .ok_or(Error::QuoteNotFound)?; - - let rec = if state == MeltQuoteState::Paid { - let current_time = unix_time(); - query(r#"UPDATE melt_quote SET state = :state, paid_time = :paid_time, 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.into()); - } - }; - - let old_state = quote.state; - quote.state = state; - - Ok((old_state, quote)) - } - - async fn remove_melt_quote(&mut self, quote_id: &Uuid) -> Result<(), Self::Err> { - query( - r#" - DELETE FROM melt_quote - WHERE id=? - "#, - ) - .bind(":id", quote_id.as_hyphenated().to_string()) - .execute(&self.inner) - .await?; - - Ok(()) - } - - async fn get_mint_quote(&mut self, quote_id: &Uuid) -> Result, 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"#, - ) - .bind(":id", quote_id.as_hyphenated().to_string()) - .fetch_one(&self.inner) - .await? - .map(|row| sqlite_row_to_mint_quote(row, payments, issuance)) - .transpose()?) - } - - async fn get_melt_quote( - &mut self, - quote_id: &Uuid, - ) -> Result, 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(sqlite_row_to_melt_quote) - .transpose()?) - } - - async fn get_mint_quote_by_request( - &mut self, - request: &str, - ) -> Result, 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_string()) - .fetch_one(&self.inner) - .await? - .map(|row| sqlite_row_to_mint_quote(row, vec![], vec![])) - .transpose()?; - - if let Some(quote) = mint_quote.as_mut() { - let payments = get_mint_quote_payments(&self.inner, "e.id).await?; - let issuance = get_mint_quote_issuance(&self.inner, "e.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, 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 - "#, - ) - .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| sqlite_row_to_mint_quote(row, vec![], vec![])) - .transpose()?; - - if let Some(quote) = mint_quote.as_mut() { - let payments = get_mint_quote_payments(&self.inner, "e.id).await?; - let issuance = get_mint_quote_issuance(&self.inner, "e.id).await?; - quote.issuance = issuance; - quote.payments = payments; - } - - Ok(mint_quote) - } -} - -#[async_trait] -impl MintQuotesDatabase for MintSqliteDatabase { - type Err = database::Error; - - async fn get_mint_quote(&self, quote_id: &Uuid) -> Result, Self::Err> { - let payments = get_mint_quote_payments(&self.pool, quote_id).await?; - let issuance = get_mint_quote_issuance(&self.pool, 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.pool) - .await? - .map(|row| sqlite_row_to_mint_quote(row, payments, issuance)) - .transpose()?) - } - - async fn get_mint_quote_by_request( - &self, - request: &str, - ) -> Result, 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.pool) - .await? - .map(|row| sqlite_row_to_mint_quote(row, vec![], vec![])) - .transpose()?; - - if let Some(quote) = mint_quote.as_mut() { - let payments = get_mint_quote_payments(&self.pool, "e.id).await?; - let issuance = get_mint_quote_issuance(&self.pool, "e.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, 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 - "#, - ) - .bind(":request_lookup_id", request_lookup_id.to_string()) - .bind(":request_lookup_id_kind", request_lookup_id.kind()) - .fetch_one(&self.pool) - .await? - .map(|row| sqlite_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.pool, "e.id).await?; - let issuance = get_mint_quote_issuance(&self.pool, "e.id).await?; - quote.issuance = issuance; - quote.payments = payments; - } - - Ok(mint_quote) - } - - async fn get_mint_quotes(&self) -> Result, 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.pool) - .await? - .into_iter() - .map(|row| sqlite_row_to_mint_quote(row, vec![], vec![])) - .collect::, _>>()?; - - for quote in mint_quotes.as_mut_slice() { - let payments = get_mint_quote_payments(&self.pool, "e.id).await?; - let issuance = get_mint_quote_issuance(&self.pool, "e.id).await?; - quote.issuance = issuance; - quote.payments = payments; - } - - Ok(mint_quotes) - } - - async fn get_melt_quote(&self, quote_id: &Uuid) -> Result, 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.pool) - .await? - .map(sqlite_row_to_melt_quote) - .transpose()?) - } - - async fn get_melt_quotes(&self) -> Result, 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.pool) - .await? - .into_iter() - .map(sqlite_row_to_melt_quote) - .collect::, _>>()?) - } -} - -#[async_trait] -impl<'a> MintProofsTransaction<'a> for SqliteTransaction<'a> { - type Err = database::Error; - - async fn add_proofs( - &mut self, - proofs: Proofs, - quote_id: Option, - ) -> Result<(), Self::Err> { - let current_time = unix_time(); - - // Check any previous proof, this query should return None in order to proceed storing - // Any result here would error - match query(r#"SELECT state FROM proof WHERE y IN (:ys) LIMIT 1"#) - .bind_vec( - ":ys", - proofs - .iter() - .map(|y| y.y().map(|y| y.to_bytes().to_vec())) - .collect::>()?, - ) - .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>, 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, - ) -> Result<(), Self::Err> { - let total_deleted = query( - r#" - DELETE FROM proof WHERE y IN (:ys) AND state NOT IN (:exclude_state) - "#, - ) - .bind_vec(":ys", ys.iter().map(|y| y.to_bytes().to_vec()).collect()) - .bind_vec(":exclude_state", vec![State::Spent.to_string()]) - .execute(&self.inner) - .await?; - - if total_deleted != ys.len() { - return Err(Self::Err::AttemptRemoveSpentProof); - } - - Ok(()) - } -} - -#[async_trait] -impl MintProofsDatabase for MintSqliteDatabase { - type Err = database::Error; - - #[instrument(skip_all)] - async fn get_proofs_by_ys(&self, ys: &[PublicKey]) -> Result>, Self::Err> { - let mut proofs = query( - r#" - SELECT - amount, - keyset_id, - secret, - c, - witness, - y - FROM - proof - WHERE - y IN (:ys) - "#, - ) - .bind_vec(":ys", ys.iter().map(|y| y.to_bytes().to_vec()).collect()) - .fetch_all(&self.pool) - .await? - .into_iter() - .map(|mut row| { - Ok(( - column_as_string!( - row.pop().ok_or(Error::InvalidDbPath)?, - PublicKey::from_hex, - PublicKey::from_slice - ), - sqlite_row_to_proof(row)?, - )) - }) - .collect::, Error>>()?; - - Ok(ys.iter().map(|y| proofs.remove(y)).collect()) - } - - #[instrument(skip(self))] - async fn get_proof_ys_by_quote_id(&self, quote_id: &Uuid) -> Result, Self::Err> { - Ok(query( - r#" - SELECT - amount, - keyset_id, - secret, - c, - witness - FROM - proof - WHERE - quote_id = :quote_id - "#, - ) - .bind(":quote_id", quote_id.as_hyphenated().to_string()) - .fetch_all(&self.pool) - .await? - .into_iter() - .map(sqlite_row_to_proof) - .collect::, _>>()? - .ys()?) - } - - #[instrument(skip_all)] - async fn get_proofs_states(&self, ys: &[PublicKey]) -> Result>, Self::Err> { - let mut current_states = get_current_states(&self.pool, ys).await?; - - Ok(ys.iter().map(|y| current_states.remove(y)).collect()) - } - - #[instrument(skip_all)] - async fn get_proofs_by_keyset_id( - &self, - keyset_id: &Id, - ) -> Result<(Proofs, Vec>), Self::Err> { - Ok(query( - r#" - SELECT - keyset_id, - amount, - secret, - c, - witness, - state - FROM - proof - WHERE - keyset_id=? - "#, - ) - .bind(":keyset_id", keyset_id.to_string()) - .fetch_all(&self.pool) - .await? - .into_iter() - .map(sqlite_row_to_proof_with_state) - .collect::, _>>()? - .into_iter() - .unzip()) - } -} - -#[async_trait] -impl<'a> MintSignatureTransaction<'a> for SqliteTransaction<'a> { - type Err = database::Error; - - #[instrument(skip_all)] - async fn add_blind_signatures( - &mut self, - blinded_messages: &[PublicKey], - blind_signatures: &[BlindSignature], - quote_id: Option, - ) -> 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", signature.amount.to_i64()) - .bind(":keyset_id", signature.keyset_id.to_string()) - .bind(":c", signature.c.to_hex()) - .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(()) - } - - #[instrument(skip_all)] - async fn get_blind_signatures( - &mut self, - blinded_messages: &[PublicKey], - ) -> Result>, 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(|y| y.to_bytes().to_vec()) - .collect(), - ) - .fetch_all(&self.inner) - .await? - .into_iter() - .map(|mut row| { - Ok(( - column_as_string!( - &row.pop().ok_or(Error::InvalidDbResponse)?, - PublicKey::from_hex, - PublicKey::from_slice - ), - sqlite_row_to_blind_signature(row)?, - )) - }) - .collect::, Error>>()?; - - Ok(blinded_messages - .iter() - .map(|y| blinded_signatures.remove(y)) - .collect()) - } -} - -#[async_trait] -impl MintSignaturesDatabase for MintSqliteDatabase { - type Err = database::Error; - - async fn get_blind_signatures( - &self, - blinded_messages: &[PublicKey], - ) -> Result>, Self::Err> { - let mut blinded_signatures = query( - r#"SELECT - keyset_id, - amount, - c, - dleq_e, - dleq_s, - blinded_message - FROM - blind_signature - WHERE blinded_message IN (:blinded_message) - "#, - ) - .bind_vec( - ":blinded_message", - blinded_messages - .iter() - .map(|b_| b_.to_bytes().to_vec()) - .collect(), - ) - .fetch_all(&self.pool) - .await? - .into_iter() - .map(|mut row| { - Ok(( - column_as_string!( - &row.pop().ok_or(Error::InvalidDbResponse)?, - PublicKey::from_hex, - PublicKey::from_slice - ), - sqlite_row_to_blind_signature(row)?, - )) - }) - .collect::, Error>>()?; - Ok(blinded_messages - .iter() - .map(|y| blinded_signatures.remove(y)) - .collect()) - } - - async fn get_blind_signatures_for_keyset( - &self, - keyset_id: &Id, - ) -> Result, Self::Err> { - Ok(query( - r#" - SELECT - keyset_id, - amount, - c, - dleq_e, - dleq_s - FROM - blind_signature - WHERE - keyset_id=:keyset_id - "#, - ) - .bind(":keyset_id", keyset_id.to_string()) - .fetch_all(&self.pool) - .await? - .into_iter() - .map(sqlite_row_to_blind_signature) - .collect::, _>>()?) - } - - /// Get [`BlindSignature`]s for quote - async fn get_blind_signatures_for_quote( - &self, - quote_id: &Uuid, - ) -> Result, Self::Err> { - Ok(query( - r#" - SELECT - keyset_id, - amount, - c, - dleq_e, - dleq_s - FROM - blind_signature - WHERE - quote_id=:quote_id - "#, - ) - .bind(":quote_id", quote_id.to_string()) - .fetch_all(&self.pool) - .await? - .into_iter() - .map(sqlite_row_to_blind_signature) - .collect::, _>>()?) - } -} - -#[async_trait] -impl MintDatabase for MintSqliteDatabase { - async fn begin_transaction<'a>( - &'a self, - ) -> Result< - Box + Send + Sync + 'a>, - database::Error, - > { - Ok(Box::new(SqliteTransaction { - inner: self.pool.begin().await?, - })) - } - - async fn get_mint_info(&self) -> Result { - Ok(self.fetch_from_config("mint_info").await?) - } - - async fn get_quote_ttl(&self) -> Result { - Ok(self.fetch_from_config("quote_ttl").await?) - } -} - -fn sqlite_row_to_keyset_info(row: Vec) -> Result { - 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 sqlite_row_to_mint_quote( - row: Vec, - payments: Vec, - issueances: Vec, -) -> Result { - 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 = 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(|_| Error::MissingParameter("Payment id".to_string()))?, - pubkey, - amount_paid.into(), - amount_issued.into(), - payment_method, - column_as_number!(created_time), - payments, - issueances, - )) -} - -fn sqlite_row_to_melt_quote(row: Vec) -> Result { - 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))?; - - 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(|_| Error::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 sqlite_row_to_proof(row: Vec) -> Result { - unpack_into!( - let ( - amount, - keyset_id, - secret, - c, - witness - ) = row - ); - - let amount: u64 = column_as_number!(amount); - Ok(Proof { - amount: Amount::from(amount), - keyset_id: column_as_string!(keyset_id, Id::from_str), - secret: column_as_string!(secret, Secret::from_str), - c: column_as_string!(c, PublicKey::from_hex, PublicKey::from_slice), - witness: column_as_nullable_string!(witness).and_then(|w| serde_json::from_str(&w).ok()), - dleq: None, - }) -} - -fn sqlite_row_to_proof_with_state(row: Vec) -> Result<(Proof, Option), Error> { - unpack_into!( - let ( - keyset_id, amount, secret, c, witness, state - ) = row - ); - - let amount: u64 = column_as_number!(amount); - let state = column_as_nullable_string!(state).and_then(|s| State::from_str(&s).ok()); - - Ok(( - Proof { - amount: Amount::from(amount), - keyset_id: column_as_string!(keyset_id, Id::from_str, Id::from_bytes), - secret: column_as_string!(secret, Secret::from_str), - c: column_as_string!(c, PublicKey::from_hex, PublicKey::from_slice), - witness: column_as_nullable_string!(witness) - .and_then(|w| serde_json::from_str(&w).ok()), - dleq: None, - }, - state, - )) -} - -fn sqlite_row_to_blind_signature(row: Vec) -> Result { - unpack_into!( - let ( - keyset_id, amount, c, dleq_e, dleq_s - ) = row - ); - - let dleq = match ( - column_as_nullable_string!(dleq_e), - column_as_nullable_string!(dleq_s), - ) { - (Some(e), Some(s)) => Some(BlindSignatureDleq { - e: SecretKey::from_hex(e)?, - s: SecretKey::from_hex(s)?, - }), - _ => None, - }; - - let amount: u64 = column_as_number!(amount); - - Ok(BlindSignature { - amount: Amount::from(amount), - keyset_id: column_as_string!(keyset_id, Id::from_str, Id::from_bytes), - c: column_as_string!(c, PublicKey::from_hex, PublicKey::from_slice), - dleq, - }) -} +pub type MintSqliteAuthDatabase = SQLMintAuthDatabase; #[cfg(test)] -mod tests { +mod test { use std::fs::remove_file; - use cdk_common::mint::MintKeySetInfo; - use cdk_common::{mint_db_test, Amount}; + use cdk_common::mint_db_test; + use cdk_sql_common::stmt::query; use super::*; - - #[tokio::test] - async fn test_remove_spent_proofs() { - let db = memory::empty().await.unwrap(); - - // Create a keyset and add it to the database - let keyset_id = Id::from_str("00916bbf7ef91a36").unwrap(); - let keyset_info = MintKeySetInfo { - id: keyset_id, - unit: CurrencyUnit::Sat, - active: true, - valid_from: 0, - derivation_path: bitcoin::bip32::DerivationPath::from_str("m/0'/0'/0'").unwrap(), - derivation_path_index: Some(0), - max_order: 32, - input_fee_ppk: 0, - final_expiry: None, - }; - let mut tx = MintKeysDatabase::begin_transaction(&db).await.unwrap(); - tx.add_keyset_info(keyset_info).await.unwrap(); - tx.commit().await.unwrap(); - - let proofs = vec![ - Proof { - amount: Amount::from(100), - keyset_id, - secret: Secret::generate(), - c: SecretKey::generate().public_key(), - witness: None, - dleq: None, - }, - Proof { - amount: Amount::from(200), - keyset_id, - secret: Secret::generate(), - c: SecretKey::generate().public_key(), - witness: None, - dleq: None, - }, - ]; - - // Add proofs to database - let mut tx = MintDatabase::begin_transaction(&db).await.unwrap(); - tx.add_proofs(proofs.clone(), None).await.unwrap(); - - // Mark one proof as spent - tx.update_proofs_states(&[proofs[0].y().unwrap()], State::Spent) - .await - .unwrap(); - - tx.commit().await.unwrap(); - - // Verify both proofs still exist - let states = db - .get_proofs_states(&[proofs[0].y().unwrap(), proofs[1].y().unwrap()]) - .await - .unwrap(); - - assert_eq!(states.len(), 2); - assert_eq!(states[0], Some(State::Spent)); - assert_eq!(states[1], Some(State::Unspent)); - } - - #[tokio::test] - async fn test_update_spent_proofs() { - let db = memory::empty().await.unwrap(); - - // Create a keyset and add it to the database - let keyset_id = Id::from_str("00916bbf7ef91a36").unwrap(); - let keyset_info = MintKeySetInfo { - id: keyset_id, - unit: CurrencyUnit::Sat, - active: true, - valid_from: 0, - derivation_path: bitcoin::bip32::DerivationPath::from_str("m/0'/0'/0'").unwrap(), - derivation_path_index: Some(0), - max_order: 32, - input_fee_ppk: 0, - final_expiry: None, - }; - let mut tx = MintKeysDatabase::begin_transaction(&db) - .await - .expect("begin"); - tx.add_keyset_info(keyset_info).await.unwrap(); - tx.commit().await.expect("commit"); - - let proofs = vec![ - Proof { - amount: Amount::from(100), - keyset_id, - secret: Secret::generate(), - c: SecretKey::generate().public_key(), - witness: None, - dleq: None, - }, - Proof { - amount: Amount::from(200), - keyset_id, - secret: Secret::generate(), - c: SecretKey::generate().public_key(), - witness: None, - dleq: None, - }, - ]; - - // Add proofs to database - let mut tx = MintDatabase::begin_transaction(&db).await.unwrap(); - tx.add_proofs(proofs.clone(), None).await.unwrap(); - - // Mark one proof as spent - tx.update_proofs_states(&[proofs[0].y().unwrap()], State::Spent) - .await - .unwrap(); - - // Try to update both proofs - should fail because one is spent - let result = tx - .update_proofs_states(&[proofs[0].y().unwrap()], State::Unspent) - .await; - - tx.commit().await.unwrap(); - - assert!(result.is_err()); - assert!(matches!( - result.unwrap_err(), - database::Error::AttemptUpdateSpentProof - )); - - // Verify states haven't changed - let states = db - .get_proofs_states(&[proofs[0].y().unwrap(), proofs[1].y().unwrap()]) - .await - .unwrap(); - - assert_eq!(states.len(), 2); - assert_eq!(states[0], Some(State::Spent)); - assert_eq!(states[1], Some(State::Unspent)); - } + use crate::mint::async_rusqlite::AsyncRusqlite; async fn provide_db() -> MintSqliteDatabase { memory::empty().await.unwrap() @@ -1925,114 +40,25 @@ mod tests { { let _ = remove_file(&file); #[cfg(not(feature = "sqlcipher"))] - let legacy = create_sqlite_pool(&file); + let conn: AsyncRusqlite = file.as_str().into(); #[cfg(feature = "sqlcipher")] - let legacy = create_sqlite_pool(&file, "test".to_owned()); - let y = legacy.get().expect("pool"); - y.execute_batch(include_str!("../../tests/legacy-sqlx.sql")) + let conn: AsyncRusqlite = (file.as_str(), "test".to_owned()).into(); + + query(include_str!("../../tests/legacy-sqlx.sql")) + .expect("query") + .execute(&conn) + .await .expect("create former db failed"); } #[cfg(not(feature = "sqlcipher"))] - let conn = MintSqliteDatabase::new(&file).await; + let conn = MintSqliteDatabase::new(file.as_str()).await; #[cfg(feature = "sqlcipher")] - let conn = MintSqliteDatabase::new(&file, "test".to_owned()).await; + let conn = MintSqliteDatabase::new((file.as_str(), "test".to_owned())).await; assert!(conn.is_ok(), "Failed with {:?}", conn.unwrap_err()); let _ = remove_file(&file); } - - #[tokio::test] - async fn test_fetch_from_config_error_handling() { - use cdk_common::common::QuoteTTL; - use cdk_common::MintInfo; - - let db = memory::empty().await.unwrap(); - - // Test 1: Unknown mint_info should return UnknownMintInfo error - let result: Result = db.fetch_from_config("mint_info").await; - assert!(result.is_err()); - assert!(matches!(result.unwrap_err(), Error::UnknownMintInfo)); - - // Test 2: Unknown quote_ttl should return UnknownQuoteTTL error - let result: Result = db.fetch_from_config("quote_ttl").await; - assert!(result.is_err()); - assert!(matches!(result.unwrap_err(), Error::UnknownQuoteTTL)); - - // Test 3: Unknown config key should return UnknownConfigKey error - let result: Result = db.fetch_from_config("unknown_config_key").await; - assert!(result.is_err()); - match result.unwrap_err() { - Error::UnknownConfigKey(key) => { - assert_eq!(key, "unknown_config_key"); - } - other => panic!("Expected UnknownConfigKey error, got: {:?}", other), - } - - // Test 4: Another unknown config key with different name - let result: Result = db.fetch_from_config("some_other_key").await; - assert!(result.is_err()); - match result.unwrap_err() { - Error::UnknownConfigKey(key) => { - assert_eq!(key, "some_other_key"); - } - other => panic!("Expected UnknownConfigKey error, got: {:?}", other), - } - } - - #[tokio::test] - async fn test_config_round_trip() { - use cdk_common::common::QuoteTTL; - use cdk_common::{MintInfo, Nuts}; - - let db = memory::empty().await.unwrap(); - - // Test mint_info round trip - let mint_info = MintInfo { - name: Some("Test Mint".to_string()), - description: Some("A test mint".to_string()), - pubkey: None, - version: None, - description_long: None, - contact: None, - nuts: Nuts::default(), - icon_url: None, - urls: None, - motd: None, - time: None, - tos_url: None, - }; - - // Store mint_info - let mut tx = cdk_common::database::MintDatabase::begin_transaction(&db) - .await - .unwrap(); - tx.set_mint_info(mint_info.clone()).await.unwrap(); - tx.commit().await.unwrap(); - - // Retrieve mint_info - let retrieved_mint_info: MintInfo = db.fetch_from_config("mint_info").await.unwrap(); - assert_eq!(mint_info.name, retrieved_mint_info.name); - assert_eq!(mint_info.description, retrieved_mint_info.description); - - // Test quote_ttl round trip - let quote_ttl = QuoteTTL { - mint_ttl: 3600, - melt_ttl: 1800, - }; - - // Store quote_ttl - let mut tx = cdk_common::database::MintDatabase::begin_transaction(&db) - .await - .unwrap(); - tx.set_quote_ttl(quote_ttl.clone()).await.unwrap(); - tx.commit().await.unwrap(); - - // Retrieve quote_ttl - let retrieved_quote_ttl: QuoteTTL = db.fetch_from_config("quote_ttl").await.unwrap(); - assert_eq!(quote_ttl.mint_ttl, retrieved_quote_ttl.mint_ttl); - assert_eq!(quote_ttl.melt_ttl, retrieved_quote_ttl.melt_ttl); - } } diff --git a/crates/cdk-sqlite/src/stmt.rs b/crates/cdk-sqlite/src/stmt.rs deleted file mode 100644 index d578ef2b..00000000 --- a/crates/cdk-sqlite/src/stmt.rs +++ /dev/null @@ -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, - /// The expected response type - pub expected_response: ExpectedSqlResponse, -} - -impl Statement { - /// Creates a new statement - pub fn new(sql: T) -> Self - where - T: ToString, - { - Self { - sql: sql.to_string(), - ..Default::default() - } - } - - /// Binds a given placeholder to a value. - #[inline] - pub fn bind(mut self, name: C, value: V) -> Self - where - C: ToString, - V: Into, - { - 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(mut self, name: C, value: Vec) -> Self - where - C: ToString, - V: Into, - { - 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::>() - .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, - ) -> rusqlite::Result> { - 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, - ) -> rusqlite::Result> { - 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, - ) -> rusqlite::Result { - self.get_stmt(conn)?.raw_execute() - } - - /// Runs the query and returns the first row or None - pub fn fetch_one( - self, - conn: &PooledResource, - ) -> rusqlite::Result>> { - 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::, _>>() - }) - .transpose() - } - - /// Runs the query and returns the first row or None - pub fn fetch_all( - self, - conn: &PooledResource, - ) -> rusqlite::Result>> { - 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::, _>>()?, - ); - } - - Ok(results) - } -} diff --git a/crates/cdk-sqlite/src/wallet/memory.rs b/crates/cdk-sqlite/src/wallet/memory.rs index e916461e..d164abb9 100644 --- a/crates/cdk-sqlite/src/wallet/memory.rs +++ b/crates/cdk-sqlite/src/wallet/memory.rs @@ -7,8 +7,10 @@ use super::WalletSqliteDatabase; /// Creates a new in-memory [`WalletSqliteDatabase`] instance pub async fn empty() -> Result { #[cfg(not(feature = "sqlcipher"))] - let db = WalletSqliteDatabase::new(":memory:").await?; + let path = ":memory:"; + #[cfg(feature = "sqlcipher")] - let db = WalletSqliteDatabase::new(":memory:", "memory".to_owned()).await?; - Ok(db) + let path = (":memory:", "memory"); + + WalletSqliteDatabase::new(path).await } diff --git a/crates/cdk-sqlite/src/wallet/migrations.rs b/crates/cdk-sqlite/src/wallet/migrations.rs deleted file mode 100644 index 0c41bad7..00000000 --- a/crates/cdk-sqlite/src/wallet/migrations.rs +++ /dev/null @@ -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"#)), -]; diff --git a/crates/cdk-sqlite/src/wallet/mod.rs b/crates/cdk-sqlite/src/wallet/mod.rs index 95259ed2..28252a9e 100644 --- a/crates/cdk-sqlite/src/wallet/mod.rs +++ b/crates/cdk-sqlite/src/wallet/mod.rs @@ -1,1147 +1,200 @@ //! SQLite Wallet Database -use std::collections::HashMap; -use std::ops::DerefMut; -use std::path::Path; -use std::str::FromStr; +use std::path::PathBuf; use std::sync::Arc; -use async_trait::async_trait; -use cdk_common::common::ProofInfo; -use cdk_common::database::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 error::Error; -use tracing::instrument; +use cdk_common::database::Error; +use cdk_sql_common::database::DatabaseExecutor; +use cdk_sql_common::pool::{Pool, PooledResource}; +use cdk_sql_common::stmt::{Column, SqlPart, Statement}; +use cdk_sql_common::SQLWalletDatabase; +use rusqlite::CachedStatement; -use crate::common::{create_sqlite_pool, migrate, SqliteConnectionManager}; -use crate::pool::Pool; -use crate::stmt::{Column, Statement}; -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, -}; +use crate::common::{create_sqlite_pool, from_sqlite, to_sqlite, SqliteConnectionManager}; -pub mod error; pub mod memory; -#[rustfmt::skip] -mod migrations; +/// Simple Sqlite wapper, since the wallet may not need rusqlite with concurrency, a shared instance +/// may be enough +#[derive(Debug)] +pub struct SimpleAsyncRusqlite(Arc>); -/// Wallet SQLite Database -#[derive(Debug, Clone)] -pub struct WalletSqliteDatabase { - pool: Arc>, -} - -impl WalletSqliteDatabase { - /// Create new [`WalletSqliteDatabase`] - #[cfg(not(feature = "sqlcipher"))] - pub async fn new>(path: P) -> Result { - let db = Self { - pool: create_sqlite_pool(path.as_ref().to_str().ok_or(Error::InvalidDbPath)?), - }; - db.migrate()?; - Ok(db) - } - - /// Create new [`WalletSqliteDatabase`] - #[cfg(feature = "sqlcipher")] - pub async fn new>(path: P, password: String) -> Result { - let db = Self { - pool: create_sqlite_pool( - path.as_ref().to_str().ok_or(Error::InvalidDbPath)?, - password, - ), - }; - db.migrate()?; - Ok(db) - } - - /// Migrate [`WalletSqliteDatabase`] - fn migrate(&self) -> Result<(), Error> { - migrate(self.pool.get()?.deref_mut(), migrations::MIGRATIONS)?; - Ok(()) - } -} - -#[async_trait] -impl WalletDatabase for WalletSqliteDatabase { - type Err = database::Error; - - #[instrument(skip(self, mint_info))] - async fn add_mint( +impl SimpleAsyncRusqlite { + fn get_stmt<'a>( &self, - mint_url: MintUrl, - mint_info: Option, - ) -> 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; + conn: &'a PooledResource, + statement: Statement, + ) -> Result, Error> { + let (sql, placeholder_values) = statement.to_sql()?; + let mut stmt = conn + .prepare_cached(&sql) + .map_err(|e| Error::Database(Box::new(e)))?; - ( - 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, - ) + for (i, value) in placeholder_values.into_iter().enumerate() { + stmt.raw_bind_parameter(i + 1, to_sqlite(value)) + .map_err(|e| Error::Database(Box::new(e)))?; + } + + Ok(stmt) + } +} + +#[async_trait::async_trait] +impl DatabaseExecutor for SimpleAsyncRusqlite { + fn name() -> &'static str { + "sqlite" + } + + async fn execute(&self, statement: Statement) -> Result { + let conn = self.0.get().map_err(|e| Error::Database(Box::new(e)))?; + let mut stmt = self + .get_stmt(&conn, statement) + .map_err(|e| Error::Database(Box::new(e)))?; + + Ok(stmt + .raw_execute() + .map_err(|e| Error::Database(Box::new(e)))?) + } + + async fn fetch_one(&self, statement: Statement) -> Result>, Error> { + let conn = self.0.get().map_err(|e| Error::Database(Box::new(e)))?; + let mut stmt = self + .get_stmt(&conn, statement) + .map_err(|e| Error::Database(Box::new(e)))?; + + let columns = stmt.column_count(); + + let mut rows = stmt.raw_query(); + rows.next() + .map_err(|e| Error::Database(Box::new(e)))? + .map(|row| { + (0..columns) + .map(|i| row.get(i).map(from_sqlite)) + .collect::, _>>() + }) + .transpose() + .map_err(|e| Error::Database(Box::new(e))) + } + + async fn fetch_all(&self, statement: Statement) -> Result>, Error> { + let conn = self.0.get().map_err(|e| Error::Database(Box::new(e)))?; + let mut stmt = self + .get_stmt(&conn, statement) + .map_err(|e| Error::Database(Box::new(e)))?; + + let columns = stmt.column_count(); + + let mut rows = stmt.raw_query(); + let mut results = vec![]; + + while let Some(row) = rows.next().map_err(|e| Error::Database(Box::new(e)))? { + results.push( + (0..columns) + .map(|i| row.get(i).map(from_sqlite)) + .collect::, _>>() + .map_err(|e| Error::Database(Box::new(e)))?, + ) + } + + Ok(results) + } + + async fn pluck(&self, statement: Statement) -> Result, Error> { + let conn = self.0.get().map_err(|e| Error::Database(Box::new(e)))?; + let mut stmt = self + .get_stmt(&conn, statement) + .map_err(|e| Error::Database(Box::new(e)))?; + + let mut rows = stmt.raw_query(); + rows.next() + .map_err(|e| Error::Database(Box::new(e)))? + .map(|row| row.get(0usize).map(from_sqlite)) + .transpose() + .map_err(|e| Error::Database(Box::new(e))) + } + + async fn batch(&self, mut statement: Statement) -> Result<(), Error> { + let conn = self.0.get().map_err(|e| Error::Database(Box::new(e)))?; + + let sql = { + let part = statement + .parts + .pop() + .ok_or(Error::Internal("Empty SQL".to_owned()))?; + + if !statement.parts.is_empty() || matches!(part, SqlPart::Placeholder(_, _)) { + return Err(Error::Internal( + "Invalid usage, batch does not support placeholders".to_owned(), + )); } - None => ( - None, None, None, None, None, None, None, None, None, None, None, None, - ), - }; - Statement::new( - 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.pool.get().map_err(Error::Pool)?) - .map_err(Error::Sqlite)?; - - Ok(()) - } - - #[instrument(skip(self))] - async fn remove_mint(&self, mint_url: MintUrl) -> Result<(), Self::Err> { - let conn = self.pool.get().map_err(Error::Pool)?; - - Statement::new(r#"DELETE FROM mint WHERE mint_url=:mint_url"#) - .bind(":mint_url", mint_url.to_string()) - .execute(&conn) - .map_err(Error::Sqlite)?; - - Ok(()) - } - - #[instrument(skip(self))] - async fn get_mint(&self, mint_url: MintUrl) -> Result, Self::Err> { - Ok(Statement::new( - 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.pool.get().map_err(Error::Pool)?) - .map_err(Error::Sqlite)? - .map(sqlite_row_to_mint_info) - .transpose()?) - } - - #[instrument(skip(self))] - async fn get_mints(&self) -> Result>, Self::Err> { - Ok(Statement::new( - 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.pool.get().map_err(Error::Pool)?) - .map_err(Error::Sqlite)? - .into_iter() - .map(|mut row| { - let url = column_as_string!( - row.pop().ok_or(Error::MissingColumn(0, 1))?, - MintUrl::from_str - ); - - Ok((url, sqlite_row_to_mint_info(row).ok())) - }) - .collect::, 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"]; - let conn = self.pool.get().map_err(Error::Pool)?; - - for table in &tables { - let query = format!( - r#" - UPDATE {table} - SET mint_url = :new_mint_url - WHERE mint_url = :old_mint_url - "# - ); - - Statement::new(query) - .bind(":new_mint_url", new_mint_url.to_string()) - .bind(":old_mint_url", old_mint_url.to_string()) - .execute(&conn) - .map_err(Error::Sqlite)?; - } - - Ok(()) - } - - #[instrument(skip(self, keysets))] - async fn add_mint_keysets( - &self, - mint_url: MintUrl, - keysets: Vec, - ) -> Result<(), Self::Err> { - let conn = self.pool.get().map_err(Error::Pool)?; - for keyset in keysets { - Statement::new( - 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(&conn) - .map_err(Error::Sqlite)?; - } - - Ok(()) - } - - #[instrument(skip(self))] - async fn get_mint_keysets( - &self, - mint_url: MintUrl, - ) -> Result>, Self::Err> { - let keysets = Statement::new( - 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.pool.get().map_err(Error::Pool)?) - .map_err(Error::Sqlite)? - .into_iter() - .map(sqlite_row_to_keyset) - .collect::, 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, Self::Err> { - Ok(Statement::new( - r#" - SELECT - id, - unit, - active, - input_fee_ppk, - final_expiry - FROM - keyset - WHERE id = :id - "#, - ) - .bind(":id", keyset_id.to_string()) - .fetch_one(&self.pool.get().map_err(Error::Pool)?) - .map_err(Error::Sqlite)? - .map(sqlite_row_to_keyset) - .transpose()?) - } - - #[instrument(skip_all)] - async fn add_mint_quote(&self, quote: MintQuote) -> Result<(), Self::Err> { - Statement::new( - 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.pool.get().map_err(Error::Pool)?) - .map_err(Error::Sqlite)?; - - Ok(()) - } - - #[instrument(skip(self))] - async fn get_mint_quote(&self, quote_id: &str) -> Result, Self::Err> { - Ok(Statement::new( - 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.pool.get().map_err(Error::Pool)?) - .map_err(Error::Sqlite)? - .map(sqlite_row_to_mint_quote) - .transpose()?) - } - - #[instrument(skip(self))] - async fn get_mint_quotes(&self) -> Result, Self::Err> { - Ok(Statement::new( - r#" - SELECT - id, - mint_url, - amount, - unit, - request, - state, - expiry, - secret_key - FROM - mint_quote - "#, - ) - .fetch_all(&self.pool.get().map_err(Error::Pool)?) - .map_err(Error::Sqlite)? - .into_iter() - .map(sqlite_row_to_mint_quote) - .collect::>()?) - } - - #[instrument(skip(self))] - async fn remove_mint_quote(&self, quote_id: &str) -> Result<(), Self::Err> { - Statement::new(r#"DELETE FROM mint_quote WHERE id=:id"#) - .bind(":id", quote_id.to_string()) - .execute(&self.pool.get().map_err(Error::Pool)?) - .map_err(Error::Sqlite)?; - - Ok(()) - } - - #[instrument(skip_all)] - async fn add_melt_quote(&self, quote: wallet::MeltQuote) -> Result<(), Self::Err> { - Statement::new( - 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.pool.get().map_err(Error::Pool)?) - .map_err(Error::Sqlite)?; - - Ok(()) - } - - #[instrument(skip(self))] - async fn get_melt_quote(&self, quote_id: &str) -> Result, Self::Err> { - Ok(Statement::new( - 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.pool.get().map_err(Error::Pool)?) - .map_err(Error::Sqlite)? - .map(sqlite_row_to_melt_quote) - .transpose()?) - } - - #[instrument(skip(self))] - async fn get_melt_quotes(&self) -> Result, Self::Err> { - Ok(Statement::new( - r#" - SELECT - id, - unit, - amount, - request, - fee_reserve, - state, - expiry, - payment_preimage - FROM - melt_quote - "#, - ) - .fetch_all(&self.pool.get().map_err(Error::Pool)?) - .map_err(Error::Sqlite)? - .into_iter() - .map(sqlite_row_to_melt_quote) - .collect::>()?) - } - - #[instrument(skip(self))] - async fn remove_melt_quote(&self, quote_id: &str) -> Result<(), Self::Err> { - Statement::new(r#"DELETE FROM melt_quote WHERE id=:id"#) - .bind(":id", quote_id.to_owned()) - .execute(&self.pool.get().map_err(Error::Pool)?) - .map_err(Error::Sqlite)?; - - Ok(()) - } - - #[instrument(skip_all)] - async fn add_keys(&self, keyset: KeySet) -> Result<(), Self::Err> { - // Recompute ID for verification - keyset.verify_id()?; - - Statement::new( - 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.pool.get().map_err(Error::Pool)?) - .map_err(Error::Sqlite)?; - - Ok(()) - } - - #[instrument(skip(self), fields(keyset_id = %keyset_id))] - async fn get_keys(&self, keyset_id: &Id) -> Result, Self::Err> { - Ok(Statement::new( - r#" - SELECT - keys - FROM key - WHERE id = :id - "#, - ) - .bind(":id", keyset_id.to_string()) - .plunk(&self.pool.get().map_err(Error::Pool)?) - .map_err(Error::Sqlite)? - .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> { - Statement::new(r#"DELETE FROM key WHERE id = :id"#) - .bind(":id", id.to_string()) - .plunk(&self.pool.get().map_err(Error::Pool)?) - .map_err(Error::Sqlite)?; - - Ok(()) - } - - async fn update_proofs( - &self, - added: Vec, - removed_ys: Vec, - ) -> Result<(), Self::Err> { - // TODO: Use a transaction for all these operations - for proof in added { - Statement::new( - 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.pool.get().map_err(Error::Pool)?) - .map_err(Error::Sqlite)?; - } - - Statement::new(r#"DELETE FROM proof WHERE y IN (:ys)"#) - .bind_vec( - ":ys", - removed_ys.iter().map(|y| y.to_bytes().to_vec()).collect(), - ) - .execute(&self.pool.get().map_err(Error::Pool)?) - .map_err(Error::Sqlite)?; - - Ok(()) - } - - #[instrument(skip(self, state, spending_conditions))] - async fn get_proofs( - &self, - mint_url: Option, - unit: Option, - state: Option>, - spending_conditions: Option>, - ) -> Result, Self::Err> { - Ok(Statement::new( - 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.pool.get().map_err(Error::Pool)?) - .map_err(Error::Sqlite)? - .into_iter() - .filter_map(|row| { - let row = sqlite_row_to_proof_info(row).ok()?; - - if row.matches_conditions(&mint_url, &unit, &state, &spending_conditions) { - Some(row) + if let SqlPart::Raw(sql) = part { + sql } else { - None + unreachable!() } - }) - .collect::>()) - } + }; - async fn update_proofs_state(&self, ys: Vec, state: State) -> Result<(), Self::Err> { - Statement::new("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.pool.get().map_err(Error::Pool)?) - .map_err(Error::Sqlite)?; - - Ok(()) - } - - #[instrument(skip(self), fields(keyset_id = %keyset_id))] - async fn increment_keyset_counter(&self, keyset_id: &Id, count: u32) -> Result<(), Self::Err> { - Statement::new( - r#" - UPDATE keyset - SET counter=counter+:count - WHERE id=:id - "#, - ) - .bind(":count", count) - .bind(":id", keyset_id.to_string()) - .execute(&self.pool.get().map_err(Error::Pool)?) - .map_err(Error::Sqlite)?; - - Ok(()) - } - - #[instrument(skip(self), fields(keyset_id = %keyset_id))] - async fn get_keyset_counter(&self, keyset_id: &Id) -> Result, Self::Err> { - Ok(Statement::new( - r#" - SELECT - counter - FROM - keyset - WHERE - id=:id - "#, - ) - .bind(":id", keyset_id.to_string()) - .plunk(&self.pool.get().map_err(Error::Pool)?) - .map_err(Error::Sqlite)? - .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::>(); - - Statement::new( - 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.pool.get().map_err(Error::Pool)?) - .map_err(Error::Sqlite)?; - - Ok(()) - } - - #[instrument(skip(self))] - async fn get_transaction( - &self, - transaction_id: TransactionId, - ) -> Result, Self::Err> { - Ok(Statement::new( - 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.pool.get().map_err(Error::Pool)?) - .map_err(Error::Sqlite)? - .map(sqlite_row_to_transaction) - .transpose()?) - } - - #[instrument(skip(self))] - async fn list_transactions( - &self, - mint_url: Option, - direction: Option, - unit: Option, - ) -> Result, Self::Err> { - Ok(Statement::new( - r#" - SELECT - mint_url, - direction, - unit, - amount, - fee, - ys, - timestamp, - memo, - metadata - FROM - transactions - "#, - ) - .fetch_all(&self.pool.get().map_err(Error::Pool)?) - .map_err(Error::Sqlite)? - .into_iter() - .filter_map(|row| { - // TODO: Avoid a table scan by passing the heavy lifting of checking to the DB engine - let transaction = sqlite_row_to_transaction(row).ok()?; - if transaction.matches_conditions(&mint_url, &direction, &unit) { - Some(transaction) - } else { - None - } - }) - .collect::>()) - } - - #[instrument(skip(self))] - async fn remove_transaction(&self, transaction_id: TransactionId) -> Result<(), Self::Err> { - Statement::new(r#"DELETE FROM transactions WHERE id=:id"#) - .bind(":id", transaction_id.as_slice().to_vec()) - .execute(&self.pool.get().map_err(Error::Pool)?) - .map_err(Error::Sqlite)?; - - Ok(()) + conn.execute_batch(&sql) + .map_err(|e| Error::Database(Box::new(e))) } } -fn sqlite_row_to_mint_info(row: Vec) -> Result { - 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), - }) +impl From for SimpleAsyncRusqlite { + fn from(value: PathBuf) -> Self { + SimpleAsyncRusqlite(create_sqlite_pool(value.to_str().unwrap_or_default(), None)) + } } -#[instrument(skip_all)] -fn sqlite_row_to_keyset(row: Vec) -> Result { - 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), - }) +impl From<&str> for SimpleAsyncRusqlite { + fn from(value: &str) -> Self { + SimpleAsyncRusqlite(create_sqlite_pool(value, None)) + } } -fn sqlite_row_to_mint_quote(row: Vec) -> Result { - 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 = 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(), - }) +impl From<(&str, &str)> for SimpleAsyncRusqlite { + fn from((value, pass): (&str, &str)) -> Self { + SimpleAsyncRusqlite(create_sqlite_pool(value, Some(pass.to_owned()))) + } } -fn sqlite_row_to_melt_quote(row: Vec) -> Result { - 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), - }) +impl From<(PathBuf, &str)> for SimpleAsyncRusqlite { + fn from((value, pass): (PathBuf, &str)) -> Self { + SimpleAsyncRusqlite(create_sqlite_pool( + value.to_str().unwrap_or_default(), + Some(pass.to_owned()), + )) + } } -fn sqlite_row_to_proof_info(row: Vec) -> Result { - 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), - }) +impl From<(&str, String)> for SimpleAsyncRusqlite { + fn from((value, pass): (&str, String)) -> Self { + SimpleAsyncRusqlite(create_sqlite_pool(value, Some(pass))) + } } -fn sqlite_row_to_transaction(row: Vec) -> Result { - 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::, _>>()?, - 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(), - }) +impl From<(PathBuf, String)> for SimpleAsyncRusqlite { + fn from((value, pass): (PathBuf, String)) -> Self { + SimpleAsyncRusqlite(create_sqlite_pool( + value.to_str().unwrap_or_default(), + Some(pass), + )) + } } +impl From<&PathBuf> for SimpleAsyncRusqlite { + fn from(value: &PathBuf) -> Self { + SimpleAsyncRusqlite(create_sqlite_pool(value.to_str().unwrap_or_default(), None)) + } +} + +/// Mint SQLite implementation with rusqlite +pub type WalletSqliteDatabase = SQLWalletDatabase; + #[cfg(test)] mod tests { + use std::str::FromStr; + use cdk_common::database::WalletDatabase; use cdk_common::nuts::{ProofDleq, State}; use cdk_common::secret::Secret; @@ -1158,7 +211,7 @@ mod tests { let path = std::env::temp_dir() .to_path_buf() .join(format!("cdk-test-{}.sqlite", uuid::Uuid::new_v4())); - let db = WalletSqliteDatabase::new(path, "password".to_string()) + let db = WalletSqliteDatabase::new((path, "password".to_string())) .await .unwrap(); @@ -1176,8 +229,6 @@ mod tests { #[tokio::test] async fn test_proof_with_dleq() { - use std::str::FromStr; - use cdk_common::common::ProofInfo; use cdk_common::mint_url::MintUrl; use cdk_common::nuts::{CurrencyUnit, Id, Proof, PublicKey, SecretKey}; @@ -1189,7 +240,7 @@ mod tests { .join(format!("cdk-test-dleq-{}.sqlite", uuid::Uuid::new_v4())); #[cfg(feature = "sqlcipher")] - let db = WalletSqliteDatabase::new(path, "password".to_string()) + let db = WalletSqliteDatabase::new((path, "password".to_string())) .await .unwrap();