diff --git a/crates/cashu-sdk/Cargo.toml b/crates/cashu-sdk/Cargo.toml index be15c2d8..90486dd0 100644 --- a/crates/cashu-sdk/Cargo.toml +++ b/crates/cashu-sdk/Cargo.toml @@ -14,11 +14,12 @@ default = ["mint", "wallet", "all-nuts", "redb"] mint = ["cashu/mint"] wallet = ["cashu/wallet", "dep:minreq", "dep:once_cell"] gloo = ["dep:gloo"] -all-nuts = ["nut07", "nut08", "nut10", "nut11"] +all-nuts = ["nut07", "nut08", "nut10", "nut11", "nut13"] nut07 = ["cashu/nut07"] nut08 = ["cashu/nut08"] nut10 = ["cashu/nut10"] nut11 = ["cashu/nut11"] +nut13 = ["cashu/nut13"] redb = ["dep:redb"] diff --git a/crates/cashu-sdk/src/wallet/localstore/memory.rs b/crates/cashu-sdk/src/wallet/localstore/memory.rs index 3e7d2999..1ffec618 100644 --- a/crates/cashu-sdk/src/wallet/localstore/memory.rs +++ b/crates/cashu-sdk/src/wallet/localstore/memory.rs @@ -18,6 +18,8 @@ pub struct MemoryLocalStore { mint_keys: Arc>>, proofs: Arc>>>, pending_proofs: Arc>>>, + #[cfg(feature = "nut13")] + keyset_counter: Arc>>, } impl MemoryLocalStore { @@ -25,6 +27,7 @@ impl MemoryLocalStore { mint_quotes: Vec, melt_quotes: Vec, mint_keys: Vec, + keyset_counter: HashMap, ) -> Self { Self { mints: Arc::new(Mutex::new(HashMap::new())), @@ -40,6 +43,8 @@ impl MemoryLocalStore { )), proofs: Arc::new(Mutex::new(HashMap::new())), pending_proofs: Arc::new(Mutex::new(HashMap::new())), + #[cfg(feature = "nut13")] + keyset_counter: Arc::new(Mutex::new(keyset_counter)), } } } @@ -205,4 +210,16 @@ impl LocalStore for MemoryLocalStore { Ok(()) } + + async fn add_keyset_counter(&self, keyset_id: &Id, count: u64) -> Result<(), Error> { + self.keyset_counter + .lock() + .await + .insert(keyset_id.clone(), count); + Ok(()) + } + + async fn get_keyset_counter(&self, id: &Id) -> Result, Error> { + Ok(self.keyset_counter.lock().await.get(id).cloned()) + } } diff --git a/crates/cashu-sdk/src/wallet/localstore/mod.rs b/crates/cashu-sdk/src/wallet/localstore/mod.rs index 84a20d13..afe5b95e 100644 --- a/crates/cashu-sdk/src/wallet/localstore/mod.rs +++ b/crates/cashu-sdk/src/wallet/localstore/mod.rs @@ -82,4 +82,9 @@ pub trait LocalStore { mint_url: UncheckedUrl, proofs: &Proofs, ) -> Result<(), Error>; + + #[cfg(feature = "nut13")] + async fn add_keyset_counter(&self, keyset_id: &Id, count: u64) -> Result<(), Error>; + #[cfg(feature = "nut13")] + async fn get_keyset_counter(&self, keyset_id: &Id) -> Result, Error>; } diff --git a/crates/cashu-sdk/src/wallet/localstore/redb_store.rs b/crates/cashu-sdk/src/wallet/localstore/redb_store.rs index f8b6bfec..73c15413 100644 --- a/crates/cashu-sdk/src/wallet/localstore/redb_store.rs +++ b/crates/cashu-sdk/src/wallet/localstore/redb_store.rs @@ -22,6 +22,8 @@ const MINT_KEYS_TABLE: TableDefinition<&str, &str> = TableDefinition::new("mint_ const PROOFS_TABLE: MultimapTableDefinition<&str, &str> = MultimapTableDefinition::new("proofs"); const PENDING_PROOFS_TABLE: MultimapTableDefinition<&str, &str> = MultimapTableDefinition::new("pending_proofs"); +#[cfg(feature = "nut13")] +const KEYSET_COUNTER: TableDefinition<&str, u64> = TableDefinition::new("keyset_counter"); #[derive(Debug, Clone)] pub struct RedbLocalStore { @@ -40,6 +42,8 @@ impl RedbLocalStore { let _ = write_txn.open_table(MELT_QUOTES_TABLE)?; let _ = write_txn.open_table(MINT_KEYS_TABLE)?; let _ = write_txn.open_multimap_table(PROOFS_TABLE)?; + #[cfg(feature = "nut13")] + let _ = write_txn.open_table(KEYSET_COUNTER)?; } write_txn.commit()?; @@ -383,4 +387,31 @@ impl LocalStore for RedbLocalStore { Ok(()) } + + #[cfg(feature = "nut13")] + async fn add_keyset_counter(&self, keyset_id: &Id, count: u64) -> Result<(), Error> { + let db = self.db.lock().await; + + let write_txn = db.begin_write()?; + + { + let mut table = write_txn.open_table(KEYSET_COUNTER)?; + + table.insert(keyset_id.to_string().as_str(), count)?; + } + write_txn.commit()?; + + Ok(()) + } + + #[cfg(feature = "nut13")] + async fn get_keyset_counter(&self, keyset_id: &Id) -> Result, Error> { + let db = self.db.lock().await; + let read_txn = db.begin_read()?; + let table = read_txn.open_table(KEYSET_COUNTER)?; + + let counter = table.get(keyset_id.to_string().as_str())?; + + Ok(counter.map(|c| c.value())) + } } diff --git a/crates/cashu-sdk/src/wallet/mod.rs b/crates/cashu-sdk/src/wallet/mod.rs index cb179cc3..5f029a0c 100644 --- a/crates/cashu-sdk/src/wallet/mod.rs +++ b/crates/cashu-sdk/src/wallet/mod.rs @@ -51,23 +51,17 @@ pub enum Error { Custom(String), } -#[derive(Clone, Debug)] -pub struct BackupInfo { - mnemonic: Mnemonic, - counter: HashMap, -} - #[derive(Clone, Debug)] pub struct Wallet { pub client: C, localstore: L, - backup_info: Option, + mnemonic: Option, } impl Wallet { - pub async fn new(client: C, localstore: L, backup_info: Option) -> Self { + pub async fn new(client: C, localstore: L, mnemonic: Option) -> Self { Self { - backup_info, + mnemonic, client, localstore, } @@ -75,12 +69,7 @@ impl Wallet { /// Back up seed pub fn mnemonic(&self) -> Option { - self.backup_info.clone().map(|b| b.mnemonic) - } - - /// Back up keyset counters - pub fn keyset_counters(&self) -> Option> { - self.backup_info.clone().map(|b| b.counter) + self.mnemonic.clone().map(|b| b) } pub async fn mint_balances(&self) -> Result, Error> { @@ -303,13 +292,25 @@ impl Wallet { let active_keyset_id = self.active_mint_keyset(&mint_url, "e_info.unit).await?; - let premint_secrets = match &self.backup_info { - Some(backup_info) => PreMintSecrets::from_seed( - active_keyset_id, - *backup_info.counter.get(&active_keyset_id).unwrap_or(&0), - &backup_info.mnemonic, - quote_info.amount, - )?, + let mut counter = None; + + let premint_secrets = match &self.mnemonic { + Some(mnemonic) => { + let count = self + .localstore + .get_keyset_counter(&active_keyset_id) + .await? + .unwrap_or(0); + + counter = Some(count); + PreMintSecrets::from_seed( + active_keyset_id, + count, + &mnemonic, + quote_info.amount, + false, + )? + } None => PreMintSecrets::random(active_keyset_id, quote_info.amount)?, }; @@ -336,6 +337,14 @@ impl Wallet { // Remove filled quote from store self.localstore.remove_mint_quote("e_info.id).await?; + // Update counter for keyset + if let Some(counter) = counter { + let count = counter + proofs.len() as u64; + self.localstore + .add_keyset_counter(&active_keyset_id, count) + .await?; + } + // Add new proofs to store self.localstore.add_proofs(mint_url, proofs).await?; @@ -407,27 +416,52 @@ impl Wallet { ) -> Result { let active_keyset_id = self.active_mint_keyset(mint_url, unit).await?; - let pre_mint_secrets = if let Some(amount) = amount { - let mut desired_messages = PreMintSecrets::random(active_keyset_id, amount)?; + // Desired amount is either amount passwed or value of all proof + let proofs_total = proofs.iter().map(|p| p.amount).sum(); - let change_amount = proofs.iter().map(|p| p.amount).sum::() - amount; + let desired_amount = amount.unwrap_or(proofs_total); - let change_messages = PreMintSecrets::random(active_keyset_id, change_amount)?; + let mut counter = None; + + let mut desired_messages = if let Some(mnemonic) = &self.mnemonic { + let count = self + .localstore + .get_keyset_counter(&active_keyset_id) + .await? + .unwrap_or(0); + let premint_secrets = PreMintSecrets::from_seed( + active_keyset_id, + count, + mnemonic, + desired_amount, + false, + )?; + + counter = Some(count + premint_secrets.len() as u64); + + premint_secrets + } else { + PreMintSecrets::random(active_keyset_id, desired_amount)? + }; + + if let (Some(amt), Some(mnemonic)) = (amount, &self.mnemonic) { + let change_amount = proofs_total - amt; + + let change_messages = if let Some(count) = counter { + PreMintSecrets::from_seed(active_keyset_id, count, mnemonic, desired_amount, false)? + } else { + PreMintSecrets::random(active_keyset_id, change_amount)? + }; // Combine the BlindedMessages totoalling the desired amount with change desired_messages.combine(change_messages); // Sort the premint secrets to avoid finger printing desired_messages.sort_secrets(); - desired_messages - } else { - let amount = proofs.iter().map(|p| p.amount).sum(); - - PreMintSecrets::random(active_keyset_id, amount)? }; - let swap_request = SwapRequest::new(proofs, pre_mint_secrets.blinded_messages()); + let swap_request = SwapRequest::new(proofs, desired_messages.blinded_messages()); Ok(PreSwap { - pre_mint_secrets, + pre_mint_secrets: desired_messages, swap_request, }) } @@ -439,6 +473,8 @@ impl Wallet { ) -> Result { let mut proofs = vec![]; + let mut proof_count: HashMap = HashMap::new(); + for (promise, premint) in promises.iter().zip(blinded_messages) { let a = self .localstore @@ -452,6 +488,10 @@ impl Wallet { let blinded_c = promise.c.clone(); let unblinded_sig = unblind_message(blinded_c, premint.r.into(), a).unwrap(); + + let count = proof_count.get(&promise.keyset_id).unwrap_or(&0); + proof_count.insert(promise.keyset_id, count + 1); + let proof = Proof::new( promise.amount, promise.keyset_id, @@ -462,6 +502,20 @@ impl Wallet { proofs.push(proof); } + if self.mnemonic.is_some() { + for (keyset_id, count) in proof_count { + let counter = self + .localstore + .get_keyset_counter(&keyset_id) + .await? + .unwrap_or(0); + + self.localstore + .add_keyset_counter(&keyset_id, counter + count) + .await?; + } + } + Ok(proofs) } @@ -637,10 +691,23 @@ impl Wallet { let proofs_amount = proofs.iter().map(|p| p.amount).sum(); - let blinded = PreMintSecrets::blank( - self.active_mint_keyset(mint_url, "e_info.unit).await?, - proofs_amount, - )?; + let mut counter = None; + + let active_keyset_id = self.active_mint_keyset(mint_url, "e_info.unit).await?; + + let premint_secrets = match &self.mnemonic { + Some(mnemonic) => { + let count = self + .localstore + .get_keyset_counter(&active_keyset_id) + .await? + .unwrap_or(0); + + counter = Some(count); + PreMintSecrets::from_seed(active_keyset_id, count, &mnemonic, proofs_amount, true)? + } + None => PreMintSecrets::blank(active_keyset_id, proofs_amount)?, + }; let melt_response = self .client @@ -648,15 +715,15 @@ impl Wallet { mint_url.clone().try_into()?, quote_id.to_string(), proofs.clone(), - Some(blinded.blinded_messages()), + Some(premint_secrets.blinded_messages()), ) .await?; let change_proofs = match melt_response.change { Some(change) => Some(construct_proofs( change, - blinded.rs(), - blinded.secrets(), + premint_secrets.rs(), + premint_secrets.secrets(), &self.active_keys(mint_url, "e_info.unit).await?.unwrap(), )?), None => None, @@ -669,6 +736,14 @@ impl Wallet { }; if let Some(change_proofs) = change_proofs { + // Update counter for keyset + if let Some(counter) = counter { + let count = counter + change_proofs.len() as u64; + self.localstore + .add_keyset_counter(&active_keyset_id, count) + .await?; + } + self.localstore .add_proofs(mint_url.clone(), change_proofs) .await?; diff --git a/crates/cashu/src/nuts/nut13.rs b/crates/cashu/src/nuts/nut13.rs index f3ebf939..5097e4f6 100644 --- a/crates/cashu/src/nuts/nut13.rs +++ b/crates/cashu/src/nuts/nut13.rs @@ -55,6 +55,7 @@ mod wallet { counter: u64, mnemonic: &Mnemonic, amount: Amount, + zero_amount: bool, ) -> Result { let mut pre_mint_secrets = PreMintSecrets::default(); @@ -66,13 +67,15 @@ mod wallet { let (blinded, r) = blind_message(&secret.to_bytes(), Some(blinding_factor.into()))?; + let amount = if zero_amount { Amount::ZERO } else { amount }; + let blinded_message = BlindedMessage::new(amount, keyset_id, blinded); let pre_mint = PreMint { blinded_message, secret: secret.clone(), r: r.into(), - amount: Amount::ZERO, + amount, }; pre_mint_secrets.secrets.push(pre_mint);