diff --git a/bindings/cdk-js/src/wallet.rs b/bindings/cdk-js/src/wallet.rs index 1cd710a3..1799c1cf 100644 --- a/bindings/cdk-js/src/wallet.rs +++ b/bindings/cdk-js/src/wallet.rs @@ -277,7 +277,7 @@ impl JsWallet { self.inner .send( &mint_url, - &unit.into(), + unit.into(), memo, Amount::from(amount), &target, diff --git a/crates/cdk-redb/src/wallet.rs b/crates/cdk-redb/src/wallet.rs index e2592f69..dfd9c745 100644 --- a/crates/cdk-redb/src/wallet.rs +++ b/crates/cdk-redb/src/wallet.rs @@ -5,7 +5,9 @@ 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, PublicKey, SpendingConditions, State}; +use cdk::nuts::{ + CurrencyUnit, Id, KeySetInfo, Keys, MintInfo, Proofs, PublicKey, SpendingConditions, State, +}; use cdk::types::{MeltQuote, MintQuote, ProofInfo}; use cdk::url::UncheckedUrl; use redb::{Database, MultimapTableDefinition, ReadableTable, TableDefinition}; @@ -399,6 +401,7 @@ impl WalletDatabase for RedbWalletDatabase { async fn get_proofs( &self, mint_url: Option, + unit: Option, state: Option>, spending_conditions: Option>, ) -> Result>, Self::Err> { @@ -415,10 +418,14 @@ impl WalletDatabase for RedbWalletDatabase { let mut proof = None; if let Ok(proof_info) = serde_json::from_str::(v.value()) { - match proof_info.matches_conditions(&mint_url, &state, &spending_conditions) { - Ok(true) => proof = Some(proof_info), - Ok(false) => (), - Err(_) => (), + match proof_info.matches_conditions( + &mint_url, + &unit, + &state, + &spending_conditions, + ) { + true => proof = Some(proof_info), + false => (), } } diff --git a/crates/cdk-rexie/src/wallet.rs b/crates/cdk-rexie/src/wallet.rs index 5b1af589..589e2e57 100644 --- a/crates/cdk-rexie/src/wallet.rs +++ b/crates/cdk-rexie/src/wallet.rs @@ -4,7 +4,9 @@ use std::result::Result; use async_trait::async_trait; use cdk::cdk_database::WalletDatabase; -use cdk::nuts::{Id, KeySetInfo, Keys, MintInfo, Proofs, PublicKey, SpendingConditions, State}; +use cdk::nuts::{ + CurrencyUnit, Id, KeySetInfo, Keys, MintInfo, Proofs, PublicKey, SpendingConditions, State, +}; use cdk::types::{MeltQuote, MintQuote, ProofInfo}; use cdk::url::UncheckedUrl; use rexie::*; @@ -448,6 +450,7 @@ impl WalletDatabase for RexieWalletDatabase { async fn get_proofs( &self, mint_url: Option, + unit: Option, state: Option>, spending_conditions: Option>, ) -> Result>, Self::Err> { @@ -472,12 +475,12 @@ impl WalletDatabase for RexieWalletDatabase { if let Ok(proof_info) = serde_wasm_bindgen::from_value::(v) { proof = match proof_info.matches_conditions( &mint_url, + &unit, &state, &spending_conditions, ) { - Ok(true) => Some(proof_info), - Ok(false) => None, - Err(_) => None, + true => Some(proof_info), + false => None, }; } diff --git a/crates/cdk/src/cdk_database/mod.rs b/crates/cdk/src/cdk_database/mod.rs index bf3c0060..411bd50e 100644 --- a/crates/cdk/src/cdk_database/mod.rs +++ b/crates/cdk/src/cdk_database/mod.rs @@ -12,9 +12,9 @@ use crate::mint::MintKeySetInfo; #[cfg(feature = "wallet")] use crate::nuts::State; #[cfg(feature = "mint")] -use crate::nuts::{BlindSignature, CurrencyUnit, Proof}; +use crate::nuts::{BlindSignature, Proof}; #[cfg(any(feature = "wallet", feature = "mint"))] -use crate::nuts::{Id, MintInfo, PublicKey}; +use crate::nuts::{CurrencyUnit, Id, MintInfo, PublicKey}; #[cfg(feature = "wallet")] use crate::nuts::{KeySetInfo, Keys, Proofs, SpendingConditions}; #[cfg(feature = "mint")] @@ -81,6 +81,7 @@ pub trait WalletDatabase { async fn get_proofs( &self, mint_url: Option, + unit: Option, state: Option>, spending_conditions: Option>, ) -> Result>, Self::Err>; diff --git a/crates/cdk/src/cdk_database/wallet_memory.rs b/crates/cdk/src/cdk_database/wallet_memory.rs index 4659e684..6324eff2 100644 --- a/crates/cdk/src/cdk_database/wallet_memory.rs +++ b/crates/cdk/src/cdk_database/wallet_memory.rs @@ -8,7 +8,9 @@ use tokio::sync::RwLock; use super::WalletDatabase; use crate::cdk_database::Error; -use crate::nuts::{Id, KeySetInfo, Keys, MintInfo, Proofs, PublicKey, SpendingConditions, State}; +use crate::nuts::{ + CurrencyUnit, Id, KeySetInfo, Keys, MintInfo, Proofs, PublicKey, SpendingConditions, State, +}; use crate::types::{MeltQuote, MintQuote, ProofInfo}; use crate::url::UncheckedUrl; @@ -169,6 +171,7 @@ impl WalletDatabase for WalletMemoryDatabase { async fn get_proofs( &self, mint_url: Option, + unit: Option, state: Option>, spending_conditions: Option>, ) -> Result>, Error> { @@ -178,10 +181,10 @@ impl WalletDatabase for WalletMemoryDatabase { .clone() .into_values() .filter_map(|proof_info| { - match proof_info.matches_conditions(&mint_url, &state, &spending_conditions) { - Ok(true) => Some(proof_info), - Ok(false) => None, - Err(_) => None, + match proof_info.matches_conditions(&mint_url, &unit, &state, &spending_conditions) + { + true => Some(proof_info), + false => None, } }) .collect(); diff --git a/crates/cdk/src/types.rs b/crates/cdk/src/types.rs index e00a5976..67a52223 100644 --- a/crates/cdk/src/types.rs +++ b/crates/cdk/src/types.rs @@ -129,32 +129,39 @@ impl ProofInfo { pub fn matches_conditions( &self, mint_url: &Option, + unit: &Option, state: &Option>, spending_conditions: &Option>, - ) -> Result { + ) -> bool { if let Some(mint_url) = mint_url { if mint_url.ne(&self.mint_url) { - return Ok(false); + return false; + } + } + + if let Some(unit) = unit { + if unit.ne(&self.unit) { + return false; } } if let Some(state) = state { if !state.contains(&self.state) { - return Ok(false); + return false; } } if let Some(spending_conditions) = spending_conditions { match &self.spending_condition { - None => return Ok(false), + None => return false, Some(s) => { if !spending_conditions.contains(s) { - return Ok(false); + return false; } } } } - Ok(true) + true } } diff --git a/crates/cdk/src/wallet.rs b/crates/cdk/src/wallet.rs index 51b7b9d1..e0ecbd13 100644 --- a/crates/cdk/src/wallet.rs +++ b/crates/cdk/src/wallet.rs @@ -143,7 +143,7 @@ impl Wallet { if let Some(proofs) = self .localstore - .get_proofs(None, Some(vec![State::Unspent]), None) + .get_proofs(None, None, Some(vec![State::Unspent]), None) .await? { for proof in proofs { @@ -164,7 +164,7 @@ impl Wallet { if let Some(proofs) = self .localstore - .get_proofs(None, Some(vec![State::Pending]), None) + .get_proofs(None, None, Some(vec![State::Pending]), None) .await? { let amount = proofs.iter().map(|p| p.proof.amount).sum(); @@ -186,7 +186,7 @@ impl Wallet { for (mint, _) in mints { if let Some(proofs) = self .localstore - .get_proofs(Some(mint.clone()), None, None) + .get_proofs(Some(mint.clone()), None, None, None) .await? { let mut balances = HashMap::new(); @@ -211,7 +211,7 @@ impl Wallet { pub async fn get_proofs(&self, mint_url: UncheckedUrl) -> Result, Error> { Ok(self .localstore - .get_proofs(Some(mint_url), Some(vec![State::Unspent]), None) + .get_proofs(Some(mint_url), None, Some(vec![State::Unspent]), None) .await? .map(|p| p.into_iter().map(|p| p.proof).collect())) } @@ -373,8 +373,9 @@ impl Wallet { .localstore .get_proofs( Some(mint.clone()), - Some(vec![State::Unspent, State::Pending]), None, + Some(vec![State::Unspent, State::Pending]), + Some(vec![]), ) .await? { @@ -858,33 +859,62 @@ impl Wallet { pub async fn send( &mut self, mint_url: &UncheckedUrl, - unit: &CurrencyUnit, + unit: CurrencyUnit, memo: Option, amount: Amount, amount_split_target: &SplitTarget, conditions: Option, ) -> Result { - let input_proofs = self.select_proofs(mint_url.clone(), unit, amount).await?; + let (condition_input_proofs, input_proofs) = self + .select_proofs(mint_url.clone(), unit.clone(), amount, conditions.clone()) + .await?; - let send_proofs = match ( - input_proofs - .iter() - .map(|p| p.amount) - .sum::() - .eq(&amount), - &conditions, - ) { - (true, None) => Some(input_proofs), - _ => { - self.swap( - mint_url, - unit, - Some(amount), - amount_split_target, - input_proofs, - conditions, + let send_proofs = match conditions { + Some(_) => { + let needed_amount = condition_input_proofs + .iter() + .map(|p| p.amount) + .sum::(); + + let top_up_proofs = self + .swap( + mint_url, + &unit, + Some(needed_amount), + amount_split_target, + input_proofs, + conditions, + ) + .await?; + + Some( + [ + condition_input_proofs, + top_up_proofs.ok_or(Error::InsufficientFunds)?, + ] + .concat(), ) - .await? + } + None => { + match input_proofs + .iter() + .map(|p| p.amount) + .sum::() + .eq(&amount) + { + true => Some(input_proofs), + false => { + self.swap( + mint_url, + &unit, + Some(amount), + amount_split_target, + input_proofs, + conditions, + ) + .await? + } + } } }; @@ -962,32 +992,83 @@ impl Wallet { pub async fn select_proofs( &self, mint_url: UncheckedUrl, - unit: &CurrencyUnit, + unit: CurrencyUnit, amount: Amount, - ) -> Result { - let mint_proofs: Proofs = self - .localstore - .get_proofs(Some(mint_url.clone()), Some(vec![State::Unspent]), None) - .await? - .ok_or(Error::InsufficientFunds)? - .into_iter() - .map(|p| p.proof) - .collect(); + conditions: Option, + ) -> Result<(Proofs, Proofs), Error> { + let mut condition_mint_proofs = Vec::new(); + + if conditions.is_some() { + condition_mint_proofs = self + .localstore + .get_proofs( + Some(mint_url.clone()), + Some(unit.clone()), + Some(vec![State::Unspent]), + None, + ) + .await? + .unwrap_or_default() + .into_iter() + .map(|p| p.proof) + .collect(); + } let mint_keysets = self .localstore - .get_mint_keysets(mint_url) + .get_mint_keysets(mint_url.clone()) .await? .ok_or(Error::UnknownKey)?; let (active, inactive): (HashSet, HashSet) = mint_keysets .into_iter() - .filter(|p| p.unit.eq(unit)) + .filter(|p| p.unit.eq(&unit.clone())) .partition(|x| x.active); let active: HashSet = active.iter().map(|k| k.id).collect(); let inactive: HashSet = inactive.iter().map(|k| k.id).collect(); + let (mut condition_active_proofs, mut condition_inactive_proofs): (Proofs, Proofs) = + condition_mint_proofs + .into_iter() + .partition(|p| active.contains(&p.keyset_id)); + + condition_active_proofs.reverse(); + condition_inactive_proofs.reverse(); + + let condition_proofs = [condition_inactive_proofs, condition_active_proofs].concat(); + + let mut condition_selected_proofs: Proofs = Vec::new(); + + for proof in condition_proofs { + if condition_selected_proofs + .iter() + .map(|p| p.amount) + .sum::() + < amount + { + condition_selected_proofs.push(proof); + } else { + return Ok((condition_selected_proofs, vec![])); + } + } + + let condition_proof_total = condition_selected_proofs.iter().map(|p| p.amount).sum(); + + let mint_proofs: Proofs = self + .localstore + .get_proofs( + Some(mint_url.clone()), + Some(unit.clone()), + Some(vec![State::Unspent]), + None, + ) + .await? + .ok_or(Error::InsufficientFunds)? + .into_iter() + .map(|p| p.proof) + .collect(); + let mut active_proofs: Proofs = Vec::new(); let mut inactive_proofs: Proofs = Vec::new(); @@ -1002,15 +1083,15 @@ impl Wallet { active_proofs.reverse(); inactive_proofs.reverse(); - inactive_proofs.append(&mut active_proofs); - - let proofs = inactive_proofs; - let mut selected_proofs: Proofs = Vec::new(); - for proof in proofs { - if selected_proofs.iter().map(|p| p.amount).sum::() < amount { + for proof in [inactive_proofs, active_proofs].concat() { + if selected_proofs.iter().map(|p| p.amount).sum::() + condition_proof_total + < amount + { selected_proofs.push(proof); + } else { + break; } } @@ -1018,7 +1099,7 @@ impl Wallet { return Err(Error::InsufficientFunds); } - Ok(selected_proofs) + Ok((condition_selected_proofs, selected_proofs)) } /// Melt @@ -1042,8 +1123,14 @@ impl Wallet { }; let proofs = self - .select_proofs(mint_url.clone(), "e_info.unit, quote_info.amount) - .await?; + .select_proofs( + mint_url.clone(), + quote_info.unit.clone(), + quote_info.amount, + None, + ) + .await? + .1; let proofs_amount = proofs.iter().map(|p| p.amount).sum();