diff --git a/bindings/cashu-ffi/src/types/keyset_info.rs b/bindings/cashu-ffi/src/types/keyset_info.rs index fbcce8f5..04e0b080 100644 --- a/bindings/cashu-ffi/src/types/keyset_info.rs +++ b/bindings/cashu-ffi/src/types/keyset_info.rs @@ -24,11 +24,12 @@ impl From for KeySetInfo { } impl KeySetInfo { - pub fn new(id: Arc, unit: String) -> Self { + pub fn new(id: Arc, unit: String, active: bool) -> Self { Self { inner: KeySetInfoSdk { id: *id.as_ref().deref(), unit: CurrencyUnit::from_str(&unit).unwrap(), + active, }, } } diff --git a/crates/cashu-sdk/src/mint.rs b/crates/cashu-sdk/src/mint.rs index db9c04e6..52c10150 100644 --- a/crates/cashu-sdk/src/mint.rs +++ b/crates/cashu-sdk/src/mint.rs @@ -357,6 +357,7 @@ impl From for KeySetInfo { Self { id: keyset_info.id, unit: keyset_info.unit, + active: keyset_info.active, } } } diff --git a/crates/cashu-sdk/src/wallet.rs b/crates/cashu-sdk/src/wallet.rs index 2f0592ca..40922b18 100644 --- a/crates/cashu-sdk/src/wallet.rs +++ b/crates/cashu-sdk/src/wallet.rs @@ -1,5 +1,5 @@ //! Cashu Wallet -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::str::FromStr; use bip39::Mnemonic; @@ -7,8 +7,8 @@ use cashu::dhke::{construct_proofs, unblind_message}; #[cfg(feature = "nut07")] use cashu::nuts::nut00::mint; use cashu::nuts::{ - BlindedSignature, CurrencyUnit, Id, Keys, PreMintSecrets, PreSwap, Proof, Proofs, SwapRequest, - Token, + BlindedSignature, CurrencyUnit, Id, KeySetInfo, Keys, MintInfo, PreMintSecrets, PreSwap, Proof, + Proofs, SwapRequest, Token, }; #[cfg(feature = "nut07")] use cashu::types::ProofsStatus; @@ -52,26 +52,29 @@ pub struct BackupInfo { pub struct Wallet { backup_info: Option, pub client: C, - pub mint_url: UncheckedUrl, + pub mints: HashMap, + pub mint_keysets: HashMap>, pub mint_quotes: HashMap, pub melt_quotes: HashMap, - pub mint_keys: Keys, + pub mint_keys: HashMap, pub balance: Amount, } impl Wallet { pub fn new( client: C, - mint_url: UncheckedUrl, + mints: HashMap, + mint_keysets: HashMap>, mint_quotes: Vec, melt_quotes: Vec, backup_info: Option, - mint_keys: Keys, + mint_keys: HashMap, ) -> Self { Self { backup_info, client, - mint_url, + mints, + mint_keysets, mint_keys, mint_quotes: mint_quotes.into_iter().map(|q| (q.id.clone(), q)).collect(), melt_quotes: melt_quotes.into_iter().map(|q| (q.id.clone(), q)).collect(), @@ -81,11 +84,15 @@ impl Wallet { /// Check if a proof is spent #[cfg(feature = "nut07")] - pub async fn check_proofs_spent(&self, proofs: Proofs) -> Result { + pub async fn check_proofs_spent( + &self, + mint_url: UncheckedUrl, + proofs: Proofs, + ) -> Result { let spendable = self .client .post_check_spendable( - self.mint_url.clone().try_into()?, + mint_url.try_into()?, proofs .clone() .into_iter() @@ -110,33 +117,36 @@ impl Wallet { // Mint a token pub async fn mint_token( &mut self, + mint_url: UncheckedUrl, amount: Amount, memo: Option, unit: Option, ) -> Result { let quote = self .mint_quote( + mint_url.clone(), amount, unit.clone() .ok_or(Error::Custom("Unit required".to_string()))?, ) .await?; - let proofs = self.mint("e.id).await?; + let proofs = self.mint(mint_url.clone(), "e.id).await?; - let token = Token::new(self.mint_url.clone(), proofs, memo, unit); + let token = Token::new(mint_url.clone(), proofs, memo, unit); Ok(token?) } /// Mint Quote pub async fn mint_quote( &mut self, + mint_url: UncheckedUrl, amount: Amount, unit: CurrencyUnit, ) -> Result { let quote_res = self .client - .post_mint_quote(self.mint_url.clone().try_into()?, amount, unit.clone()) + .post_mint_quote(mint_url.try_into()?, amount, unit.clone()) .await?; let quote = MintQuote { @@ -154,8 +164,27 @@ impl Wallet { Ok(quote) } + fn active_mint_keyset(&self, mint_url: &UncheckedUrl, unit: &CurrencyUnit) -> Option { + if let Some(keysets) = self.mint_keysets.get(mint_url) { + for keyset in keysets { + if keyset.unit.eq(unit) && keyset.active { + return Some(keyset.id); + } + } + } + + return None; + } + + fn active_keys(&self, mint_url: &UncheckedUrl, unit: &CurrencyUnit) -> Option { + self.active_mint_keyset(mint_url, unit) + .map(|id| self.mint_keys.get(&id)) + .flatten() + .cloned() + } + /// Mint - pub async fn mint(&mut self, quote_id: &str) -> Result { + pub async fn mint(&mut self, mint_url: UncheckedUrl, quote_id: &str) -> Result { let quote_info = self.mint_quotes.get(quote_id); let quote_info = if let Some(quote) = quote_info { @@ -168,33 +197,36 @@ impl Wallet { return Err(Error::QuoteUnknown); }; + let active_keyset_id = self + .active_mint_keyset(&mint_url, "e_info.unit) + .unwrap(); + let premint_secrets = match &self.backup_info { Some(backup_info) => PreMintSecrets::from_seed( - Id::from(&self.mint_keys), - *backup_info - .counter - .get(&Id::from(&self.mint_keys)) - .unwrap_or(&0), + active_keyset_id, + *backup_info.counter.get(&active_keyset_id).unwrap_or(&0), &backup_info.mnemonic, quote_info.amount, )?, - None => PreMintSecrets::random((&self.mint_keys).into(), quote_info.amount)?, + None => PreMintSecrets::random(active_keyset_id, quote_info.amount)?, }; let mint_res = self .client .post_mint( - self.mint_url.clone().try_into()?, + mint_url.clone().try_into()?, quote_id, premint_secrets.clone(), ) .await?; + let keys = self.mint_keys.get(&active_keyset_id).unwrap(); + let proofs = construct_proofs( mint_res.signatures, premint_secrets.rs(), premint_secrets.secrets(), - &self.mint_keys, + keys, )?; self.mint_quotes.remove("e_info.id); @@ -206,26 +238,35 @@ impl Wallet { pub async fn receive(&self, encoded_token: &str) -> Result { let token_data = Token::from_str(encoded_token)?; + let unit = token_data.unit.unwrap_or_default(); + let mut proofs: Vec = vec![vec![]]; for token in token_data.token { if token.proofs.is_empty() { continue; } + /* + let keys = if token.mint.to_string().eq(&self.mint_url.to_string()) { + self.mint_keys.clone() + } else { + self.client.get_mint_keys(token.mint.try_into()?).await? + }; + */ - let keys = if token.mint.to_string().eq(&self.mint_url.to_string()) { - self.mint_keys.clone() - } else { - self.client.get_mint_keys(token.mint.try_into()?).await? - }; + let active_keyset_id = self.active_mint_keyset(&token.mint, &unit); + + // TODO: if none fetch keyset for mint + + let keys = self.mint_keys.get(&active_keyset_id.unwrap()); // Sum amount of all proofs - let _amount: Amount = token.proofs.iter().map(|p| p.amount).sum(); + let amount: Amount = token.proofs.iter().map(|p| p.amount).sum(); - let pre_swap = self.create_split(None, token.proofs)?; + let pre_swap = self.create_split(&token.mint, &unit, Some(amount), token.proofs)?; let swap_response = self .client - .post_split(self.mint_url.clone().try_into()?, pre_swap.split_request) + .post_split(token.mint.clone().try_into()?, pre_swap.split_request) .await?; // Proof to keep @@ -233,7 +274,7 @@ impl Wallet { swap_response.signatures, pre_swap.pre_mint_secrets.rs(), pre_swap.pre_mint_secrets.secrets(), - &keys, + &keys.unwrap(), )?; proofs.push(p); } @@ -241,16 +282,24 @@ impl Wallet { } /// Create Split Payload - fn create_split(&self, amount: Option, proofs: Proofs) -> Result { + fn create_split( + &self, + mint_url: &UncheckedUrl, + unit: &CurrencyUnit, + amount: Option, + proofs: Proofs, + ) -> Result { // Since split is used to get the needed combination of tokens for a specific // amount first blinded messages are created for the amount + let active_keyset_id = self.active_mint_keyset(mint_url, unit).unwrap(); + let pre_mint_secrets = if let Some(amount) = amount { - let mut desired_messages = PreMintSecrets::random((&self.mint_keys).into(), amount)?; + let mut desired_messages = PreMintSecrets::random(active_keyset_id, amount)?; let change_amount = proofs.iter().map(|p| p.amount).sum::() - amount; - let change_messages = PreMintSecrets::random((&self.mint_keys).into(), change_amount)?; + let change_messages = 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 @@ -259,7 +308,7 @@ impl Wallet { } else { let value = proofs.iter().map(|p| p.amount).sum(); - PreMintSecrets::random((&self.mint_keys).into(), value)? + PreMintSecrets::random(active_keyset_id, value)? }; let split_request = SwapRequest::new(proofs, pre_mint_secrets.blinded_messages()); @@ -280,6 +329,8 @@ impl Wallet { for (promise, premint) in promises.iter().zip(blinded_messages) { let a = self .mint_keys + .get(&promise.keyset_id) + .unwrap() .amount_key(promise.amount) .unwrap() .to_owned(); @@ -301,7 +352,13 @@ impl Wallet { } /// Send - pub async fn send(&self, amount: Amount, proofs: Proofs) -> Result { + pub async fn send( + &self, + mint_url: &UncheckedUrl, + unit: &CurrencyUnit, + amount: Amount, + proofs: Proofs, + ) -> Result { let amount_available: Amount = proofs.iter().map(|p| p.amount).sum(); if amount_available.lt(&amount) { @@ -309,11 +366,11 @@ impl Wallet { return Err(Error::InsufficientFunds); } - let pre_swap = self.create_split(Some(amount), proofs)?; + let pre_swap = self.create_split(mint_url, unit, Some(amount), proofs)?; let swap_response = self .client - .post_split(self.mint_url.clone().try_into()?, pre_swap.split_request) + .post_split(mint_url.clone().try_into()?, pre_swap.split_request) .await?; let mut keep_proofs = Proofs::new(); @@ -323,7 +380,7 @@ impl Wallet { swap_response.signatures, pre_swap.pre_mint_secrets.rs(), pre_swap.pre_mint_secrets.secrets(), - &self.mint_keys, + &self.active_keys(mint_url, unit).unwrap(), )?; proofs.reverse(); @@ -357,16 +414,13 @@ impl Wallet { /// Melt Quote pub async fn melt_quote( &mut self, + mint_url: UncheckedUrl, unit: CurrencyUnit, request: Bolt11Invoice, ) -> Result { let quote_res = self .client - .post_melt_quote( - self.mint_url.clone().try_into()?, - unit.clone(), - request.clone(), - ) + .post_melt_quote(mint_url.clone().try_into()?, unit.clone(), request.clone()) .await?; let quote = MeltQuote { @@ -385,7 +439,12 @@ impl Wallet { } /// Melt - pub async fn melt(&self, quote_id: &str, proofs: Proofs) -> Result { + pub async fn melt( + &self, + mint_url: &UncheckedUrl, + quote_id: &str, + proofs: Proofs, + ) -> Result { let quote_info = self.melt_quotes.get(quote_id); let quote_info = if let Some(quote) = quote_info { @@ -398,12 +457,15 @@ impl Wallet { return Err(Error::QuoteUnknown); }; - let blinded = PreMintSecrets::blank((&self.mint_keys).into(), quote_info.fee_reserve)?; + let blinded = PreMintSecrets::blank( + self.active_mint_keyset(mint_url, "e_info.unit).unwrap(), + quote_info.fee_reserve, + )?; let melt_response = self .client .post_melt( - self.mint_url.clone().try_into()?, + mint_url.clone().try_into()?, quote_id.to_string(), proofs, Some(blinded.blinded_messages()), @@ -415,7 +477,7 @@ impl Wallet { change, blinded.rs(), blinded.secrets(), - &self.mint_keys, + &self.active_keys(mint_url, "e_info.unit).unwrap(), )?), None => None, }; @@ -431,11 +493,12 @@ impl Wallet { pub fn proofs_to_token( &self, + mint_url: UncheckedUrl, proofs: Proofs, memo: Option, unit: Option, ) -> Result { - Ok(Token::new(self.mint_url.clone(), proofs, memo, unit)?.to_string()) + Ok(Token::new(mint_url, proofs, memo, unit)?.to_string()) } } diff --git a/crates/cashu/src/nuts/nut02.rs b/crates/cashu/src/nuts/nut02.rs index 95819a19..9fca212e 100644 --- a/crates/cashu/src/nuts/nut02.rs +++ b/crates/cashu/src/nuts/nut02.rs @@ -192,6 +192,7 @@ impl From for KeySet { pub struct KeySetInfo { pub id: Id, pub unit: CurrencyUnit, + pub active: bool, } impl From for KeySetInfo { @@ -199,6 +200,7 @@ impl From for KeySetInfo { Self { id: keyset.id, unit: keyset.unit, + active: false, } } }