diff --git a/Cargo.toml b/Cargo.toml index 7dbb65ad..61bfdc86 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ members = [ "crates/cdk", "crates/cdk-redb", + "crates/cdk-rexie", ] resolver = "2" diff --git a/crates/cdk-redb/src/mint.rs b/crates/cdk-redb/src/mint.rs index c39b343c..fce1f7b1 100644 --- a/crates/cdk-redb/src/mint.rs +++ b/crates/cdk-redb/src/mint.rs @@ -3,6 +3,7 @@ use std::str::FromStr; use std::sync::Arc; use async_trait::async_trait; +use cdk::cdk_database; use cdk::cdk_database::MintDatabase; use cdk::dhke::hash_to_curve; use cdk::mint::MintKeySetInfo; diff --git a/crates/cdk-redb/src/wallet.rs b/crates/cdk-redb/src/wallet.rs index 4e202357..07d1f421 100644 --- a/crates/cdk-redb/src/wallet.rs +++ b/crates/cdk-redb/src/wallet.rs @@ -3,6 +3,7 @@ use std::str::FromStr; use std::sync::Arc; use async_trait::async_trait; +use cdk::cdk_database; use cdk::cdk_database::WalletDatabase; use cdk::nuts::{Id, KeySetInfo, Keys, MintInfo, Proofs}; use cdk::types::{MeltQuote, MintQuote}; @@ -24,7 +25,7 @@ const PENDING_PROOFS_TABLE: MultimapTableDefinition<&str, &str> = const CONFIG_TABLE: TableDefinition<&str, &str> = TableDefinition::new("config"); const KEYSET_COUNTER: TableDefinition<&str, u64> = TableDefinition::new("keyset_counter"); -const DATABASE_VERSION: u64 = 0; +const DATABASE_VERSION: u32 = 0; #[derive(Debug, Clone)] pub struct RedbWalletDatabase { @@ -46,7 +47,7 @@ impl RedbWalletDatabase { match db_version { Some(db_version) => { - let current_file_version = u64::from_str(&db_version)?; + let current_file_version = u32::from_str(&db_version)?; if current_file_version.ne(&DATABASE_VERSION) { // Database needs to be upgraded todo!() @@ -120,12 +121,8 @@ impl WalletDatabase for RedbWalletDatabase { async fn get_mints(&self) -> Result>, Self::Err> { let db = self.db.lock().await; - let read_txn = db - .begin_read() - .map_err(Into::::into) - .map_err(Error::from)?; + let read_txn = db.begin_read().map_err(Error::from)?; let table = read_txn.open_table(MINTS_TABLE).map_err(Error::from)?; - let mints = table .iter() .map_err(Error::from)? diff --git a/crates/cdk-rexie/Cargo.toml b/crates/cdk-rexie/Cargo.toml new file mode 100644 index 00000000..58293268 --- /dev/null +++ b/crates/cdk-rexie/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "cdk-rexie" +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 = ["wallet"] +wallet = ["cdk/wallet"] + +[dependencies] +rexie = "0.5.0" +cdk = { workspace = true, default-features = false } +async-trait.workspace = true +tokio.workspace = true +serde.workspace = true +serde_json.workspace = true +thiserror.workspace = true +serde-wasm-bindgen = "0.6.5" diff --git a/crates/cdk-rexie/src/lib.rs b/crates/cdk-rexie/src/lib.rs new file mode 100644 index 00000000..8d001668 --- /dev/null +++ b/crates/cdk-rexie/src/lib.rs @@ -0,0 +1,5 @@ +#[cfg(all(feature = "wallet", target_arch = "wasm32"))] +pub mod wallet; + +#[cfg(all(feature = "wallet", target_arch = "wasm32"))] +pub use wallet::RexieWalletDatabase; diff --git a/crates/cdk-rexie/src/wallet.rs b/crates/cdk-rexie/src/wallet.rs new file mode 100644 index 00000000..d7308443 --- /dev/null +++ b/crates/cdk-rexie/src/wallet.rs @@ -0,0 +1,556 @@ +use std::collections::HashMap; +use std::result::Result; +use std::sync::Arc; + +use async_trait::async_trait; +use cdk::cdk_database::WalletDatabase; +use cdk::nuts::{Id, KeySetInfo, Keys, MintInfo, Proofs}; +use cdk::types::{MeltQuote, MintQuote}; +use cdk::url::UncheckedUrl; +use rexie::*; +use thiserror::Error; +use tokio::sync::Mutex; + +// Tables +const MINTS: &str = "mints"; +const MINT_KEYSETS: &str = "mint_keysets"; +const MINT_KEYS: &str = "mint_keys"; +const MINT_QUOTES: &str = "mint_quotes"; +const MELT_QUOTES: &str = "melt_quotes"; +const PROOFS: &str = "proofs"; +const PENDING_PROOFS: &str = "pending_proofs"; +const CONFIG: &str = "config"; +const KEYSET_COUNTER: &str = "keyset_counter"; + +const DATABASE_VERSION: u32 = 0; + +#[derive(Debug, Error)] +pub enum Error { + /// CDK Database Error + #[error(transparent)] + CDKDatabase(#[from] cdk::cdk_database::Error), + /// Rexie Error + #[error(transparent)] + Redb(#[from] rexie::Error), + /// Serde Wasm Error + #[error(transparent)] + SerdeBindgen(#[from] serde_wasm_bindgen::Error), +} + +impl From for cdk::cdk_database::Error { + fn from(e: Error) -> Self { + Self::Database(Box::new(e)) + } +} + +// These are okay because we never actually send across threads in the browser +unsafe impl Send for Error {} +unsafe impl Sync for Error {} + +#[derive(Debug, Clone)] +pub struct RexieWalletDatabase { + db: Arc>, +} + +// These are okay because we never actually send across threads in the browser +//unsafe impl Send for RexieWalletDatabase {} +//unsafe impl Sync for RexieWalletDatabase {} + +impl RexieWalletDatabase { + pub async fn new() -> Result { + let rexie = Rexie::builder("cdk") + // Set the version of the database to 1.0 + .version(DATABASE_VERSION) + // Add an object store named `employees` + .add_object_store( + ObjectStore::new(PROOFS) + // Set the key path to `id` + .key_path("y") + // Add an index named `email` with the key path `email` with unique enabled + .add_index(Index::new("y", "y").unique(true)), + ) + .add_object_store( + ObjectStore::new(MINTS) + // Set the key path to `id` + .key_path("mint_url") + // Add an index named `email` with the key path `email` with unique enabled + .add_index(Index::new("mint_url", "mint_url").unique(true)), + ) + .add_object_store( + ObjectStore::new(MINT_KEYSETS) + .key_path("keyset_id") + .add_index(Index::new("keyset_id", "keyset_id").unique(true)), + ) + .add_object_store( + ObjectStore::new(MINT_KEYS) + .key_path("keyset_id") + .add_index(Index::new("keyset_id", "keyset_id").unique(true)), + ) + .add_object_store( + ObjectStore::new(MINT_QUOTES) + .key_path("keyset_id") + .add_index(Index::new("keyset_id", "keyset_id").unique(true)), + ) + .add_object_store( + ObjectStore::new(MELT_QUOTES) + .key_path("keyset_id") + .add_index(Index::new("keyset_id", "keyset_id").unique(true)), + ) + .add_object_store( + ObjectStore::new(PENDING_PROOFS) + .key_path("keyset_id") + .add_index(Index::new("keyset_id", "keyset_id").unique(true)), + ) + .add_object_store( + ObjectStore::new(CONFIG) + .key_path("keyset_id") + .add_index(Index::new("keyset_id", "keyset_id").unique(true)), + ) + .add_object_store( + ObjectStore::new(KEYSET_COUNTER) + .key_path("keyset_id") + .add_index(Index::new("keyset_id", "keyset_id").unique(true)), + ) + // Build the database + .build() + .await + .unwrap(); + + Ok(Self { + db: Arc::new(Mutex::new(rexie)), + }) + } +} + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl WalletDatabase for RexieWalletDatabase { + type Err = Error; + async fn add_mint( + &self, + mint_url: UncheckedUrl, + mint_info: Option, + ) -> Result<(), Error> { + let rexie = self.db.lock().await; + + let transaction = rexie.transaction(&[MINTS], TransactionMode::ReadWrite)?; + + let mints_store = transaction.store(MINTS)?; + + let mint_url = serde_wasm_bindgen::to_value(&mint_url)?; + let mint_info = serde_wasm_bindgen::to_value(&mint_info)?; + + mints_store.add(&mint_info, Some(&mint_url)).await?; + + transaction.done().await?; + + Ok(()) + } + + async fn get_mint(&self, mint_url: UncheckedUrl) -> Result, Error> { + let rexie = self.db.lock().await; + + let transaction = rexie.transaction(&[MINTS], TransactionMode::ReadOnly)?; + + let mints_store = transaction.store(MINTS)?; + + let mint_url = serde_wasm_bindgen::to_value(&mint_url)?; + let mint_info = mints_store.get(&mint_url).await?; + + let mint_info: Option = serde_wasm_bindgen::from_value(mint_info)?; + + Ok(mint_info) + } + + async fn get_mints(&self) -> Result>, Error> { + let rexie = self.db.lock().await; + + let transaction = rexie.transaction(&[MINTS], TransactionMode::ReadOnly)?; + + let mints_store = transaction.store(MINTS)?; + + let mints = mints_store.get_all(None, None, None, None).await?; + + let mints: HashMap> = mints + .into_iter() + .map(|(url, info)| { + ( + serde_wasm_bindgen::from_value(url).unwrap(), + serde_wasm_bindgen::from_value(info).unwrap(), + ) + }) + .collect(); + + Ok(mints) + } + + async fn add_mint_keysets( + &self, + mint_url: UncheckedUrl, + keysets: Vec, + ) -> Result<(), Error> { + let rexie = self.db.lock().await; + + let transaction = rexie.transaction(&[MINT_KEYSETS], TransactionMode::ReadWrite)?; + + let keysets_store = transaction.store(MINT_KEYSETS)?; + + let mint_url = serde_wasm_bindgen::to_value(&mint_url)?; + let keysets = serde_wasm_bindgen::to_value(&keysets)?; + + keysets_store.add(&keysets, Some(&mint_url)).await?; + + transaction.done().await?; + + Ok(()) + } + + async fn get_mint_keysets( + &self, + mint_url: UncheckedUrl, + ) -> Result>, Error> { + let rexie = self.db.lock().await; + + let transaction = rexie.transaction(&[MINT_KEYSETS], TransactionMode::ReadOnly)?; + + let mints_store = transaction.store(MINT_KEYSETS)?; + + let mint_url = serde_wasm_bindgen::to_value(&mint_url)?; + let keysets = mints_store.get(&mint_url).await?; + + let keysets: Option> = serde_wasm_bindgen::from_value(keysets)?; + + Ok(keysets) + } + + async fn add_mint_quote(&self, quote: MintQuote) -> Result<(), Error> { + let rexie = self.db.lock().await; + + let transaction = rexie.transaction(&[MINT_QUOTES], TransactionMode::ReadWrite)?; + + let quotes_store = transaction.store(MINT_QUOTES)?; + + let quote_id = serde_wasm_bindgen::to_value("e.id)?; + let quote = serde_wasm_bindgen::to_value("e)?; + + quotes_store.add("e, Some("e_id)).await?; + + transaction.done().await?; + + Ok(()) + } + + async fn get_mint_quote(&self, quote_id: &str) -> Result, Error> { + let rexie = self.db.lock().await; + + let transaction = rexie.transaction(&[MINT_QUOTES], TransactionMode::ReadOnly)?; + + let quotes_store = transaction.store(MINT_QUOTES)?; + + let quote_id = serde_wasm_bindgen::to_value("e_id)?; + let keysets = quotes_store.get("e_id).await?; + + let quote: Option = serde_wasm_bindgen::from_value(keysets)?; + + Ok(quote) + } + + async fn remove_mint_quote(&self, quote_id: &str) -> Result<(), Error> { + let rexie = self.db.lock().await; + + let transaction = rexie.transaction(&[MINT_QUOTES], TransactionMode::ReadWrite)?; + + let quotes_store = transaction.store(MINT_QUOTES)?; + + let quote_id = serde_wasm_bindgen::to_value("e_id)?; + + quotes_store.delete("e_id).await?; + + transaction.done().await?; + + Ok(()) + } + + async fn add_melt_quote(&self, quote: MeltQuote) -> Result<(), Error> { + let rexie = self.db.lock().await; + + let transaction = rexie.transaction(&[MELT_QUOTES], TransactionMode::ReadWrite)?; + + let quotes_store = transaction.store(MELT_QUOTES)?; + + let quote_id = serde_wasm_bindgen::to_value("e.id)?; + let quote = serde_wasm_bindgen::to_value("e)?; + + quotes_store.add("e, Some("e_id)).await?; + + transaction.done().await?; + + Ok(()) + } + + async fn get_melt_quote(&self, quote_id: &str) -> Result, Error> { + let rexie = self.db.lock().await; + + let transaction = rexie.transaction(&[MELT_QUOTES], TransactionMode::ReadOnly)?; + + let quotes_store = transaction.store(MELT_QUOTES)?; + + let quote_id = serde_wasm_bindgen::to_value("e_id)?; + let keysets = quotes_store.get("e_id).await?; + + let quote: Option = serde_wasm_bindgen::from_value(keysets)?; + + Ok(quote) + } + + async fn remove_melt_quote(&self, quote_id: &str) -> Result<(), Error> { + let rexie = self.db.lock().await; + + let transaction = rexie.transaction(&[MELT_QUOTES], TransactionMode::ReadWrite)?; + + let quotes_store = transaction.store(MELT_QUOTES)?; + + let quote_id = serde_wasm_bindgen::to_value("e_id)?; + + quotes_store.delete("e_id).await?; + + transaction.done().await?; + + Ok(()) + } + + async fn add_keys(&self, keys: Keys) -> Result<(), Error> { + let rexie = self.db.lock().await; + + let transaction = rexie.transaction(&[MINT_KEYS], TransactionMode::ReadWrite)?; + + let keys_store = transaction.store(MINT_KEYS)?; + + let keyset_id = serde_wasm_bindgen::to_value(&Id::from(&keys))?; + let keys = serde_wasm_bindgen::to_value(&keys)?; + + keys_store.add(&keys, Some(&keyset_id)).await?; + + transaction.done().await?; + + Ok(()) + } + + async fn get_keys(&self, id: &Id) -> Result, Error> { + let rexie = self.db.lock().await; + + let transaction = rexie.transaction(&[MINT_KEYS], TransactionMode::ReadOnly)?; + + let keys_store = transaction.store(MINT_KEYS)?; + + let keyset_id = serde_wasm_bindgen::to_value(id)?; + let keys = keys_store.get(&keyset_id).await?; + + let keys: Option = serde_wasm_bindgen::from_value(keys)?; + + Ok(keys) + } + + async fn remove_keys(&self, id: &Id) -> Result<(), Error> { + let rexie = self.db.lock().await; + + let transaction = rexie.transaction(&[MINT_KEYS], TransactionMode::ReadWrite)?; + + let keys_store = transaction.store(MINT_KEYS)?; + + let keyset_id = serde_wasm_bindgen::to_value(id)?; + keys_store.delete(&keyset_id).await?; + + Ok(()) + } + + async fn add_proofs(&self, mint_url: UncheckedUrl, proofs: Proofs) -> Result<(), Self::Err> { + let rexie = self.db.lock().await; + + let transaction = rexie.transaction(&[PROOFS], TransactionMode::ReadWrite)?; + + let proofs_store = transaction.store(PROOFS)?; + + let mint_url = serde_wasm_bindgen::to_value(&mint_url)?; + + let current_proofs = proofs_store.get(&mint_url).await?; + + let current_proofs: Proofs = serde_wasm_bindgen::from_value(current_proofs)?; + + let all_proofs: Proofs = current_proofs + .into_iter() + .chain(proofs.into_iter()) + .collect(); + + let all_proofs = serde_wasm_bindgen::to_value(&all_proofs)?; + + proofs_store.add(&all_proofs, Some(&mint_url)).await?; + + transaction.done().await?; + + Ok(()) + } + + async fn get_proofs(&self, mint_url: UncheckedUrl) -> Result, Error> { + let rexie = self.db.lock().await; + + let transaction = rexie.transaction(&[PROOFS], TransactionMode::ReadOnly)?; + + let proofs_store = transaction.store(PROOFS)?; + + let mint_url = serde_wasm_bindgen::to_value(&mint_url)?; + let proofs = proofs_store.get(&mint_url).await?; + + transaction.done().await?; + + let proofs: Option = serde_wasm_bindgen::from_value(proofs)?; + + Ok(proofs) + } + + async fn remove_proofs(&self, mint_url: UncheckedUrl, proofs: &Proofs) -> Result<(), Error> { + let rexie = self.db.lock().await; + + let transaction = rexie.transaction(&[PROOFS], TransactionMode::ReadWrite)?; + + let proofs_store = transaction.store(PROOFS)?; + + let mint_url = serde_wasm_bindgen::to_value(&mint_url)?; + let current_proofs = proofs_store.get(&mint_url).await?; + + let current_proofs: Option = serde_wasm_bindgen::from_value(current_proofs)?; + + if let Some(current_proofs) = current_proofs { + let proofs: Proofs = current_proofs + .into_iter() + .filter(|p| !proofs.contains(p)) + .collect(); + + let proofs = serde_wasm_bindgen::to_value(&proofs)?; + + proofs_store.add(&proofs, Some(&mint_url)).await?; + } + + transaction.done().await?; + + Ok(()) + } + + async fn add_pending_proofs( + &self, + mint_url: UncheckedUrl, + proofs: Proofs, + ) -> Result<(), Self::Err> { + let rexie = self.db.lock().await; + + let transaction = rexie.transaction(&[PENDING_PROOFS], TransactionMode::ReadWrite)?; + + let proofs_store = transaction.store(PENDING_PROOFS)?; + + let mint_url = serde_wasm_bindgen::to_value(&mint_url)?; + + let current_proofs = proofs_store.get(&mint_url).await?; + + let current_proofs: Proofs = serde_wasm_bindgen::from_value(current_proofs)?; + + let all_proofs: Proofs = current_proofs + .into_iter() + .chain(proofs.into_iter()) + .collect(); + + let all_proofs = serde_wasm_bindgen::to_value(&all_proofs)?; + + proofs_store.add(&all_proofs, Some(&mint_url)).await?; + + transaction.done().await?; + + Ok(()) + } + + async fn get_pending_proofs(&self, mint_url: UncheckedUrl) -> Result, Error> { + let rexie = self.db.lock().await; + + let transaction = rexie.transaction(&[PENDING_PROOFS], TransactionMode::ReadOnly)?; + + let proofs_store = transaction.store(PENDING_PROOFS)?; + + let mint_url = serde_wasm_bindgen::to_value(&mint_url)?; + let proofs = proofs_store.get(&mint_url).await?; + + transaction.done().await?; + + let proofs: Option = serde_wasm_bindgen::from_value(proofs)?; + + Ok(proofs) + } + + async fn remove_pending_proofs( + &self, + mint_url: UncheckedUrl, + proofs: &Proofs, + ) -> Result<(), Error> { + let rexie = self.db.lock().await; + + let transaction = rexie.transaction(&[PENDING_PROOFS], TransactionMode::ReadWrite)?; + + let proofs_store = transaction.store(PENDING_PROOFS)?; + + let mint_url = serde_wasm_bindgen::to_value(&mint_url)?; + let current_proofs = proofs_store.get(&mint_url).await?; + + let current_proofs: Option = serde_wasm_bindgen::from_value(current_proofs)?; + + if let Some(current_proofs) = current_proofs { + let proofs: Proofs = current_proofs + .into_iter() + .filter(|p| !proofs.contains(p)) + .collect(); + + let proofs = serde_wasm_bindgen::to_value(&proofs)?; + + proofs_store.add(&proofs, Some(&mint_url)).await?; + } + + transaction.done().await?; + + Ok(()) + } + + async fn increment_keyset_counter(&self, keyset_id: &Id, count: u64) -> Result<(), Error> { + let rexie = self.db.lock().await; + + let transaction = rexie.transaction(&[KEYSET_COUNTER], TransactionMode::ReadWrite)?; + + let counter_store = transaction.store(KEYSET_COUNTER)?; + + let keyset_id = serde_wasm_bindgen::to_value(keyset_id)?; + + let current_count = counter_store.get(&keyset_id).await?; + let current_count: Option = serde_wasm_bindgen::from_value(current_count)?; + + let new_count = current_count.unwrap_or_default() + count; + + let new_count = serde_wasm_bindgen::to_value(&new_count)?; + + counter_store.add(&new_count, Some(&keyset_id)).await?; + + transaction.done().await?; + + Ok(()) + } + + async fn get_keyset_counter(&self, keyset_id: &Id) -> Result, Error> { + let rexie = self.db.lock().await; + + let transaction = rexie.transaction(&[KEYSET_COUNTER], TransactionMode::ReadWrite)?; + + let counter_store = transaction.store(KEYSET_COUNTER)?; + + let keyset_id = serde_wasm_bindgen::to_value(keyset_id)?; + + let current_count = counter_store.get(&keyset_id).await?; + let current_count: Option = serde_wasm_bindgen::from_value(current_count)?; + + Ok(current_count) + } +} diff --git a/crates/cdk/src/cdk_database/mod.rs b/crates/cdk/src/cdk_database/mod.rs index 9f78748b..21e7fb33 100644 --- a/crates/cdk/src/cdk_database/mod.rs +++ b/crates/cdk/src/cdk_database/mod.rs @@ -36,7 +36,8 @@ pub enum Error { } #[cfg(feature = "wallet")] -#[async_trait] +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] pub trait WalletDatabase { type Err: Into + From; diff --git a/crates/cdk/src/cdk_database/wallet_memory.rs b/crates/cdk/src/cdk_database/wallet_memory.rs index 011c70ea..298c79b5 100644 --- a/crates/cdk/src/cdk_database/wallet_memory.rs +++ b/crates/cdk/src/cdk_database/wallet_memory.rs @@ -50,7 +50,8 @@ impl WalletMemoryDatabase { } } -#[async_trait] +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] impl WalletDatabase for WalletMemoryDatabase { type Err = Error;