diff --git a/Cargo.toml b/Cargo.toml index 23536958..da4a9dc0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,9 +1,7 @@ [workspace] members = [ "bindings/cdk-js", - "crates/cdk", - "crates/cdk-redb", - "crates/cdk-rexie", + "crates/*", ] resolver = "2" @@ -35,6 +33,11 @@ serde_json = "1" serde-wasm-bindgen = { version = "0.6.5", default-features = false } web-sys = { version = "0.3.68", default-features = false, features = ["console"] } uniffi = { version = "0.27.1", default-features = false } +bitcoin = { version = "0.30", features = [ + "serde", + "rand", + "rand-std", +] } # lightning-invoice uses v0.30 [profile] diff --git a/crates/cdk-sqlite/Cargo.toml b/crates/cdk-sqlite/Cargo.toml new file mode 100644 index 00000000..b71747f9 --- /dev/null +++ b/crates/cdk-sqlite/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "cdk-sqlite" +version = "0.1.0" +edition = "2021" +license.workspace = true +homepage.workspace = true +repository.workspace = true +rust-version.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] +default = ["mint", "wallet"] +mint = ["cdk/mint"] +wallet = ["cdk/wallet"] +nostr = ["cdk/nostr"] + +[dependencies] +bitcoin.workspace = true +const_format = "0.2.32" +sqlx = { version = "0.6.3", default-features = false, features = ["runtime-tokio-rustls", "chrono", "sqlite"] } +cdk = { workspace = true, default-features = false } +thiserror.workspace = true +tokio = { workspace = true, features = [ + "time", + "macros", + "sync", +] } +tracing.workspace = true +async-trait.workspace = true +serde_json.workspace = true diff --git a/crates/cdk-sqlite/src/lib.rs b/crates/cdk-sqlite/src/lib.rs new file mode 100644 index 00000000..95e813f3 --- /dev/null +++ b/crates/cdk-sqlite/src/lib.rs @@ -0,0 +1,7 @@ +#[cfg(feature = "mint")] +pub mod mint; +#[cfg(feature = "wallet")] +pub mod wallet; + +#[cfg(feature = "mint")] +pub use mint::MintSqliteDatabase; diff --git a/crates/cdk-sqlite/src/mint/error.rs b/crates/cdk-sqlite/src/mint/error.rs new file mode 100644 index 00000000..0dba4855 --- /dev/null +++ b/crates/cdk-sqlite/src/mint/error.rs @@ -0,0 +1,29 @@ +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum Error { + /// SQLX Error + #[error(transparent)] + SQLX(#[from] sqlx::Error), + /// NUT02 Error + #[error(transparent)] + CDKNUT02(#[from] cdk::nuts::nut02::Error), + /// NUT01 Error + #[error(transparent)] + CDKNUT01(#[from] cdk::nuts::nut01::Error), + /// Secret Error + #[error(transparent)] + CDKSECRET(#[from] cdk::secret::Error), + /// BIP32 Error + #[error(transparent)] + BIP32(#[from] bitcoin::bip32::Error), + /// Could Not Initialize Db + #[error("Could not initialize Db")] + CouldNotInitialize, +} + +impl From for cdk::cdk_database::Error { + fn from(e: Error) -> Self { + Self::Database(Box::new(e)) + } +} diff --git a/crates/cdk-sqlite/src/mint/migration.rs b/crates/cdk-sqlite/src/mint/migration.rs new file mode 100644 index 00000000..eb595968 --- /dev/null +++ b/crates/cdk-sqlite/src/mint/migration.rs @@ -0,0 +1,106 @@ +use const_format::formatcp; +use sqlx::{Executor, Pool, Sqlite}; + +use super::error::Error; + +/// Latest database version +pub const DB_VERSION: usize = 0; + +/// Schema definition +const INIT_SQL: &str = formatcp!( + r#" +-- Database settings +PRAGMA encoding = "UTF-8"; +PRAGMA journal_mode = WAL; +PRAGMA auto_vacuum = FULL; +PRAGMA main.synchronous=NORMAL; +PRAGMA foreign_keys = ON; +PRAGMA user_version = {}; + +-- Proof Table +CREATE TABLE IF NOT EXISTS proof ( +y BLOB PRIMARY KEY, +amount INTEGER NOT NULL, +keyset_id TEXT NOT NULL, +secret TEXT NOT NULL, +c BLOB NOT NULL, +witness TEXT, +state TEXT CHECK ( state IN ('SPENT', 'PENDING' ) ) NOT NULL +); + +CREATE INDEX IF NOT EXISTS state_index ON proof(state); +CREATE INDEX IF NOT EXISTS secret_index ON proof(secret); + +-- Keysets Table + +CREATE TABLE IF NOT EXISTS 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 +); + +CREATE INDEX IF NOT EXISTS unit_index ON keyset(unit); +CREATE INDEX IF NOT EXISTS active_index ON keyset(active); + + +CREATE TABLE IF NOT EXISTS mint_quote ( + id TEXT PRIMARY KEY, + mint_url TEXT NOT NULL, + amount INTEGER NOT NULL, + unit TEXT NOT NULL, + request TEXT NOT NULL, + paid BOOL NOT NULL DEFAULT FALSE, + expiry INTEGER NOT NULL +); + + +CREATE INDEX IF NOT EXISTS paid_index ON mint_quote(paid); +CREATE INDEX IF NOT EXISTS request_index ON mint_quote(request); + +CREATE TABLE IF NOT EXISTS melt_quote ( + id TEXT PRIMARY KEY, + unit TEXT NOT NULL, + amount INTEGER NOT NULL, + request TEXT NOT NULL, + fee_reserve INTEGER NOT NULL, + paid BOOL NOT NULL DEFAULT FALSE, + expiry INTEGER NOT NULL +); + +CREATE INDEX IF NOT EXISTS paid_index ON melt_quote(paid); +CREATE INDEX IF NOT EXISTS request_index ON melt_quote(request); + +CREATE TABLE IF NOT EXISTS blind_signature ( + y BLOB PRIMARY KEY, + amount INTEGER NOT NULL, + keyset_id TEXT NOT NULL, + c BLOB NOT NULL +); + +CREATE INDEX IF NOT EXISTS keyset_id_index ON blind_signature(keyset_id); + + "#, + DB_VERSION +); + +pub(crate) async fn init_migration(pool: &Pool) -> Result { + let mut conn = pool.acquire().await?; + + match conn.execute(INIT_SQL).await { + Ok(_) => { + tracing::info!( + "database pragma/schema initialized to v{}, and ready", + DB_VERSION + ); + } + Err(err) => { + tracing::error!("update (init) failed: {}", err); + return Err(Error::CouldNotInitialize); + } + } + Ok(DB_VERSION) +} diff --git a/crates/cdk-sqlite/src/mint/mod.rs b/crates/cdk-sqlite/src/mint/mod.rs new file mode 100644 index 00000000..dca5447e --- /dev/null +++ b/crates/cdk-sqlite/src/mint/mod.rs @@ -0,0 +1,641 @@ +//! SQLite + +use std::collections::HashMap; +use std::str::FromStr; + +use async_trait::async_trait; +use bitcoin::bip32::DerivationPath; +use cdk::cdk_database::{self, MintDatabase}; +use cdk::mint::MintKeySetInfo; +use cdk::nuts::{BlindSignature, CurrencyUnit, Id, Proof, PublicKey}; +use cdk::secret::Secret; +use cdk::types::{MeltQuote, MintQuote}; +use cdk::Amount; +use error::Error; +use migration::init_migration; +use sqlx::sqlite::{SqliteConnectOptions, SqlitePool, SqliteRow}; +use sqlx::{ConnectOptions, Row}; + +pub mod error; +mod migration; + +#[derive(Debug, Clone)] +pub struct MintSqliteDatabase { + pool: SqlitePool, +} + +impl MintSqliteDatabase { + pub async fn new(path: &str) -> Result { + let _conn = SqliteConnectOptions::from_str(path)? + .journal_mode(sqlx::sqlite::SqliteJournalMode::Wal) + .read_only(false) + .create_if_missing(true) + .auto_vacuum(sqlx::sqlite::SqliteAutoVacuum::Full) + .connect() + .await?; + + let pool = SqlitePool::connect(path).await?; + + init_migration(&pool).await?; + + Ok(Self { pool }) + } +} + +#[async_trait] +impl MintDatabase for MintSqliteDatabase { + type Err = cdk_database::Error; + + async fn add_active_keyset(&self, unit: CurrencyUnit, id: Id) -> Result<(), Self::Err> { + sqlx::query( + r#" +UPDATE keyset +SET active=TRUE +WHERE unit IS ? +AND id IS ?; + "#, + ) + .bind(unit.to_string()) + .bind(id.to_string()) + .execute(&self.pool) + .await + // TODO: should check if error is not found and return none + .map_err(Error::from)?; + + Ok(()) + } + async fn get_active_keyset_id(&self, unit: &CurrencyUnit) -> Result, Self::Err> { + let rec = sqlx::query( + r#" +SELECT id +FROM keyset +WHERE active = 1 +AND unit IS ? + "#, + ) + .bind(unit.to_string()) + .fetch_one(&self.pool) + .await; + + let rec = match rec { + Ok(rec) => rec, + Err(err) => match err { + sqlx::Error::RowNotFound => return Ok(None), + _ => return Err(Error::SQLX(err).into()), + }, + }; + + Ok(Some( + Id::from_str(rec.try_get("id").map_err(Error::from)?).map_err(Error::from)?, + )) + } + async fn get_active_keysets(&self) -> Result, Self::Err> { + let recs = sqlx::query( + r#" +SELECT id, unit +FROM keyset +WHERE active = 1 + "#, + ) + .fetch_all(&self.pool) + .await + // TODO: should check if error is not found and return none + .map_err(Error::from)?; + + let keysets = recs + .iter() + .filter_map(|r| match Id::from_str(r.get("id")) { + Ok(id) => Some((CurrencyUnit::from(r.get::<'_, &str, &str>("unit")), id)), + Err(_) => None, + }) + .collect(); + + Ok(keysets) + } + + async fn add_mint_quote(&self, quote: MintQuote) -> Result<(), Self::Err> { + sqlx::query( + r#" +INSERT OR REPLACE INTO mint_quote +(id, mint_url, amount, unit, request, paid, expiry) +VALUES (?, ?, ?, ?, ?, ?, ?); + "#, + ) + .bind(quote.id.to_string()) + .bind(quote.mint_url.to_string()) + .bind(u64::from(quote.amount) as i64) + .bind(quote.unit.to_string()) + .bind(quote.request) + .bind(quote.paid) + .bind(quote.expiry as i64) + .execute(&self.pool) + .await + // TODO: should check if error is not found and return none + .map_err(Error::from)?; + + Ok(()) + } + async fn get_mint_quote(&self, quote_id: &str) -> Result, Self::Err> { + let rec = sqlx::query( + r#" +SELECT * +FROM mint_quote +WHERE id=?; + "#, + ) + .bind(quote_id) + .fetch_one(&self.pool) + .await; + + let rec = match rec { + Ok(rec) => rec, + Err(err) => match err { + sqlx::Error::RowNotFound => return Ok(None), + _ => return Err(Error::SQLX(err).into()), + }, + }; + + Ok(Some(sqlite_row_to_mint_quote(rec)?)) + } + async fn get_mint_quotes(&self) -> Result, Self::Err> { + let rec = sqlx::query( + r#" +SELECT * +FROM mint_quote + "#, + ) + .fetch_all(&self.pool) + .await + .map_err(Error::from)?; + + let mint_quotes = rec.into_iter().flat_map(sqlite_row_to_mint_quote).collect(); + + Ok(mint_quotes) + } + async fn remove_mint_quote(&self, quote_id: &str) -> Result<(), Self::Err> { + sqlx::query( + r#" +DELETE FROM mint_quote +WHERE id=? + "#, + ) + .bind(quote_id) + .execute(&self.pool) + .await + .map_err(Error::from)?; + + Ok(()) + } + + async fn add_melt_quote(&self, quote: MeltQuote) -> Result<(), Self::Err> { + sqlx::query( + r#" +INSERT OR REPLACE INTO melt_quote +(id, unit, amount, request, fee_reserve, paid, expiry) +VALUES (?, ?, ?, ?, ?, ?, ?); + "#, + ) + .bind(quote.id.to_string()) + .bind(quote.unit.to_string()) + .bind(u64::from(quote.amount) as i64) + .bind(quote.request) + .bind(u64::from(quote.fee_reserve) as i64) + .bind(quote.paid) + .bind(quote.expiry as i64) + .execute(&self.pool) + .await + .map_err(Error::from)?; + + Ok(()) + } + async fn get_melt_quote(&self, quote_id: &str) -> Result, Self::Err> { + let rec = sqlx::query( + r#" +SELECT * +FROM melt_quote +WHERE id=?; + "#, + ) + .bind(quote_id) + .fetch_one(&self.pool) + .await; + + let rec = match rec { + Ok(rec) => rec, + Err(err) => match err { + sqlx::Error::RowNotFound => return Ok(None), + _ => return Err(Error::SQLX(err).into()), + }, + }; + + Ok(Some(sqlite_row_to_melt_quote(rec)?)) + } + async fn get_melt_quotes(&self) -> Result, Self::Err> { + let rec = sqlx::query( + r#" +SELECT * +FROM melt_quote + "#, + ) + .fetch_all(&self.pool) + .await + .map_err(Error::from)?; + + let melt_quotes = rec.into_iter().flat_map(sqlite_row_to_melt_quote).collect(); + + Ok(melt_quotes) + } + async fn remove_melt_quote(&self, quote_id: &str) -> Result<(), Self::Err> { + sqlx::query( + r#" +DELETE FROM melt_quote +WHERE id=? + "#, + ) + .bind(quote_id) + .execute(&self.pool) + .await + .map_err(Error::from)?; + + Ok(()) + } + + async fn add_keyset_info(&self, keyset: MintKeySetInfo) -> Result<(), Self::Err> { + sqlx::query( + r#" +INSERT INTO keyset +(id, unit, active, valid_from, valid_to, derivation_path, max_order) +VALUES (?, ?, ?, ?, ?, ?, ?); + "#, + ) + .bind(keyset.id.to_string()) + .bind(keyset.unit.to_string()) + .bind(keyset.active) + .bind(keyset.valid_from as i64) + .bind(keyset.valid_to.map(|v| v as i64)) + .bind(keyset.derivation_path.to_string()) + .bind(keyset.max_order) + .execute(&self.pool) + .await + .map_err(Error::from)?; + + Ok(()) + } + async fn get_keyset_info(&self, id: &Id) -> Result, Self::Err> { + let rec = sqlx::query( + r#" +SELECT * +FROM keyset +WHERE id=?; + "#, + ) + .bind(id.to_string()) + .fetch_one(&self.pool) + .await; + + let rec = match rec { + Ok(rec) => rec, + Err(err) => match err { + sqlx::Error::RowNotFound => return Ok(None), + _ => return Err(Error::SQLX(err).into()), + }, + }; + + Ok(Some(sqlite_row_to_keyset_info(rec)?)) + } + async fn get_keyset_infos(&self) -> Result, Self::Err> { + let recs = sqlx::query( + r#" +SELECT * +FROM keyset; + "#, + ) + .fetch_all(&self.pool) + .await + .map_err(Error::from)?; + + Ok(recs + .into_iter() + .flat_map(sqlite_row_to_keyset_info) + .collect()) + } + + async fn add_spent_proof(&self, proof: Proof) -> Result<(), Self::Err> { + sqlx::query( + r#" +INSERT OR REPLACE INTO proof +(y, amount, keyset_id, secret, c, witness, state) +VALUES (?, ?, ?, ?, ?, ?, ?); + "#, + ) + .bind(proof.y()?.to_bytes().to_vec()) + .bind(u64::from(proof.amount) as i64) + .bind(proof.keyset_id.to_string()) + .bind(proof.secret.to_string()) + .bind(proof.c.to_bytes().to_vec()) + .bind(proof.witness.map(|w| serde_json::to_string(&w).unwrap())) + .bind("SPENT") + .execute(&self.pool) + .await + .map_err(Error::from)?; + + Ok(()) + } + async fn get_spent_proof_by_secret(&self, secret: &Secret) -> Result, Self::Err> { + let rec = sqlx::query( + r#" +SELECT * +FROM proof +WHERE secret=? +AND state="SPENT"; + "#, + ) + .bind(secret.to_string()) + .fetch_one(&self.pool) + .await; + + let rec = match rec { + Ok(rec) => rec, + Err(err) => match err { + sqlx::Error::RowNotFound => return Ok(None), + _ => return Err(Error::SQLX(err).into()), + }, + }; + + Ok(Some(sqlite_row_to_proof(rec)?)) + } + async fn get_spent_proof_by_y(&self, y: &PublicKey) -> Result, Self::Err> { + let rec = sqlx::query( + r#" +SELECT * +FROM proof +WHERE y=? +AND state="SPENT"; + "#, + ) + .bind(y.to_bytes().to_vec()) + .fetch_one(&self.pool) + .await; + + let rec = match rec { + Ok(rec) => rec, + Err(err) => match err { + sqlx::Error::RowNotFound => return Ok(None), + _ => return Err(Error::SQLX(err).into()), + }, + }; + + Ok(Some(sqlite_row_to_proof(rec)?)) + } + + async fn add_pending_proof(&self, proof: Proof) -> Result<(), Self::Err> { + sqlx::query( + r#" +INSERT OR REPLACE INTO proof +(y, amount, keyset_id, secret, c, witness, spent, pending) +VALUES (?, ?, ?, ?, ?, ?, ?); + "#, + ) + .bind(proof.y()?.to_bytes().to_vec()) + .bind(u64::from(proof.amount) as i64) + .bind(proof.keyset_id.to_string()) + .bind(proof.secret.to_string()) + .bind(proof.c.to_bytes().to_vec()) + .bind(proof.witness.map(|w| serde_json::to_string(&w).unwrap())) + .bind("PENDING") + .execute(&self.pool) + .await + .map_err(Error::from)?; + + Ok(()) + } + async fn get_pending_proof_by_secret( + &self, + secret: &Secret, + ) -> Result, Self::Err> { + let rec = sqlx::query( + r#" +SELECT * +FROM proof +WHERE secret=? +AND state="PENDING"; + "#, + ) + .bind(secret.to_string()) + .fetch_one(&self.pool) + .await; + + let rec = match rec { + Ok(rec) => rec, + Err(err) => match err { + sqlx::Error::RowNotFound => return Ok(None), + _ => return Err(Error::SQLX(err).into()), + }, + }; + + Ok(Some(sqlite_row_to_proof(rec)?)) + } + async fn get_pending_proof_by_y(&self, y: &PublicKey) -> Result, Self::Err> { + let rec = sqlx::query( + r#" +SELECT * +FROM proof +WHERE y=? +AND state="PENDING"; + "#, + ) + .bind(y.to_bytes().to_vec()) + .fetch_one(&self.pool) + .await; + + let rec = match rec { + Ok(rec) => rec, + Err(err) => match err { + sqlx::Error::RowNotFound => return Ok(None), + _ => return Err(Error::SQLX(err).into()), + }, + }; + Ok(Some(sqlite_row_to_proof(rec)?)) + } + async fn remove_pending_proof(&self, secret: &Secret) -> Result<(), Self::Err> { + sqlx::query( + r#" +DELETE FROM proof +WHERE secret=? +AND state="PENDING"; + "#, + ) + .bind(secret.to_string()) + .execute(&self.pool) + .await + .map_err(Error::from)?; + + Ok(()) + } + + async fn add_blinded_signature( + &self, + blinded_message: PublicKey, + blinded_signature: BlindSignature, + ) -> Result<(), Self::Err> { + sqlx::query( + r#" +INSERT INTO blind_signature +(y, amount, keyset_id, c) +VALUES (?, ?, ?, ?); + "#, + ) + .bind(blinded_message.to_bytes().to_vec()) + .bind(u64::from(blinded_signature.amount) as i64) + .bind(blinded_signature.keyset_id.to_string()) + .bind(blinded_signature.c.to_bytes().to_vec()) + .execute(&self.pool) + .await + .map_err(Error::from)?; + + Ok(()) + } + async fn get_blinded_signature( + &self, + blinded_message: &PublicKey, + ) -> Result, Self::Err> { + let rec = sqlx::query( + r#" +SELECT * +FROM blind_signature +WHERE y=?; + "#, + ) + .bind(blinded_message.to_bytes().to_vec()) + .fetch_one(&self.pool) + .await; + + let rec = match rec { + Ok(rec) => rec, + Err(err) => match err { + sqlx::Error::RowNotFound => return Ok(None), + _ => return Err(Error::SQLX(err).into()), + }, + }; + + Ok(Some(sqlite_row_to_blind_signature(rec)?)) + } + async fn get_blinded_signatures( + &self, + blinded_messages: Vec, + ) -> Result>, Self::Err> { + let mut signatures = Vec::with_capacity(blinded_messages.len()); + for message in blinded_messages { + let rec = sqlx::query( + r#" +SELECT * +FROM blind_signature +WHERE y=?; + "#, + ) + .bind(message.to_bytes().to_vec()) + .fetch_one(&self.pool) + .await; + + if let Ok(row) = rec { + let blinded = sqlite_row_to_blind_signature(row)?; + + signatures.push(Some(blinded)); + } else { + signatures.push(None); + } + } + + Ok(signatures) + } +} + +fn sqlite_row_to_keyset_info(row: SqliteRow) -> Result { + let row_id: String = row.try_get("id").map_err(Error::from)?; + let row_unit: String = row.try_get("unit").map_err(Error::from)?; + let row_active: bool = row.try_get("active").map_err(Error::from)?; + let row_valid_from: i64 = row.try_get("valid_from").map_err(Error::from)?; + let row_valid_to: Option = row.try_get("valid_to").map_err(Error::from)?; + let row_derivation_path: String = row.try_get("derivation_path").map_err(Error::from)?; + let row_max_order: u8 = row.try_get("max_order").map_err(Error::from)?; + + Ok(MintKeySetInfo { + id: Id::from_str(&row_id).map_err(Error::from)?, + unit: CurrencyUnit::from(&row_unit), + active: row_active, + valid_from: row_valid_from as u64, + valid_to: row_valid_to.map(|v| v as u64), + derivation_path: DerivationPath::from_str(&row_derivation_path).map_err(Error::from)?, + max_order: row_max_order, + }) +} + +fn sqlite_row_to_mint_quote(row: SqliteRow) -> Result { + let row_id: String = row.try_get("id").map_err(Error::from)?; + let row_mint_url: String = row.try_get("mint_url").map_err(Error::from)?; + let row_amount: i64 = row.try_get("amount").map_err(Error::from)?; + let row_unit: String = row.try_get("unit").map_err(Error::from)?; + let row_request: String = row.try_get("request").map_err(Error::from)?; + let row_paid: bool = row.try_get("paid").map_err(Error::from)?; + let row_expiry: i64 = row.try_get("expiry").map_err(Error::from)?; + + Ok(MintQuote { + id: row_id, + mint_url: row_mint_url.into(), + amount: Amount::from(row_amount as u64), + unit: CurrencyUnit::from(row_unit), + request: row_request, + paid: row_paid, + expiry: row_expiry as u64, + }) +} + +fn sqlite_row_to_melt_quote(row: SqliteRow) -> Result { + let row_id: String = row.try_get("id").map_err(Error::from)?; + let row_unit: String = row.try_get("unit").map_err(Error::from)?; + let row_amount: i64 = row.try_get("amount").map_err(Error::from)?; + let row_request: String = row.try_get("request").map_err(Error::from)?; + let row_fee_reserve: i64 = row.try_get("fee_reserve").map_err(Error::from)?; + let row_paid: bool = row.try_get("paid").map_err(Error::from)?; + let row_expiry: i64 = row.try_get("expiry").map_err(Error::from)?; + + Ok(MeltQuote { + id: row_id, + amount: Amount::from(row_amount as u64), + unit: CurrencyUnit::from(row_unit), + request: row_request, + fee_reserve: Amount::from(row_fee_reserve as u64), + paid: row_paid, + expiry: row_expiry as u64, + }) +} + +fn sqlite_row_to_proof(row: SqliteRow) -> Result { + let row_amount: i64 = row.try_get("amount").map_err(Error::from)?; + let keyset_id: String = row.try_get("keyset_id").map_err(Error::from)?; + let row_secret: String = row.try_get("secret").map_err(Error::from)?; + let row_c: Vec = row.try_get("c").map_err(Error::from)?; + let row_witness: Option = row.try_get("witness").map_err(Error::from)?; + + Ok(Proof { + amount: Amount::from(row_amount as u64), + keyset_id: Id::from_str(&keyset_id)?, + secret: Secret::from_str(&row_secret)?, + c: PublicKey::from_slice(&row_c)?, + witness: row_witness.and_then(|w| serde_json::from_str(&w).ok()), + dleq: None, + }) +} + +fn sqlite_row_to_blind_signature(row: SqliteRow) -> Result { + let row_amount: i64 = row.try_get("amount").map_err(Error::from)?; + let keyset_id: String = row.try_get("keyset_id").map_err(Error::from)?; + let row_c: Vec = row.try_get("c").map_err(Error::from)?; + + Ok(BlindSignature { + amount: Amount::from(row_amount as u64), + keyset_id: Id::from_str(&keyset_id)?, + c: PublicKey::from_slice(&row_c)?, + dleq: None, + }) +} diff --git a/crates/cdk-sqlite/src/wallet/mod.rs b/crates/cdk-sqlite/src/wallet/mod.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/crates/cdk-sqlite/src/wallet/mod.rs @@ -0,0 +1 @@ + diff --git a/crates/cdk/Cargo.toml b/crates/cdk/Cargo.toml index 654e4670..441bc8b9 100644 --- a/crates/cdk/Cargo.toml +++ b/crates/cdk/Cargo.toml @@ -17,15 +17,15 @@ nostr = ["dep:nostr-sdk"] [dependencies] -async-trait = "0.1" +async-trait.workspace = true base64 = "0.22" # bitcoin uses v0.13 (optional dep) bip39 = "2.0" -bitcoin = { version = "0.30", features = [ +http = "1.0" +bitcoin = { workspace = true, features = [ "serde", "rand", "rand-std", -] } # lightning-invoice uses v0.30 -http = "1.0" +] } lightning-invoice = { version = "0.31", features = ["serde"] } once_cell = "1.19" reqwest = { version = "0.12", default-features = false, features = [ diff --git a/flake.lock b/flake.lock index b61a880c..3023b5fe 100644 --- a/flake.lock +++ b/flake.lock @@ -175,11 +175,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1716633019, - "narHash": "sha256-xim1b5/HZYbWaZKyI7cn9TJCM6ewNVZnesRr00mXeS4=", + "lastModified": 1717159533, + "narHash": "sha256-oamiKNfr2MS6yH64rUn99mIZjc45nGJlj9eGth/3Xuw=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "9d29cd266cebf80234c98dd0b87256b6be0af44e", + "rev": "a62e6edd6d5e1fa0329b8653c801147986f8d446", "type": "github" }, "original": { diff --git a/misc/scripts/check-crates.sh b/misc/scripts/check-crates.sh index 94455a82..8cfdbf7c 100755 --- a/misc/scripts/check-crates.sh +++ b/misc/scripts/check-crates.sh @@ -32,6 +32,8 @@ buildargs=( "-p cdk-redb --no-default-features --features wallet" "-p cdk-redb --no-default-features --features wallet --features nostr" "-p cdk-redb --no-default-features --features mint" + "-p cdk-sqlite --no-default-features --features mint" + "-p cdk-sqlite --no-default-features --features wallet" "--examples" )