Prechádzať zdrojové kódy

Prepared Send (#596)


Co-authored-by: thesimplekid <tsk@thesimplekid.com>
Co-authored-by: ok300 <106775972+ok300@users.noreply.github.com>
David Caseria 1 mesiac pred
rodič
commit
db1db86509

+ 55 - 0
crates/cashu/src/amount.rs

@@ -49,6 +49,9 @@ impl Amount {
     /// Amount zero
     pub const ZERO: Amount = Amount(0);
 
+    // Amount one
+    pub const ONE: Amount = Amount(1);
+
     /// Split into parts that are powers of two
     pub fn split(&self) -> Vec<Self> {
         let sats = self.0;
@@ -119,6 +122,27 @@ impl Amount {
         Ok(parts)
     }
 
+    /// Splits amount into powers of two while accounting for the swap fee
+    pub fn split_with_fee(&self, fee_ppk: u64) -> Result<Vec<Self>, Error> {
+        let without_fee_amounts = self.split();
+        let fee_ppk = fee_ppk * without_fee_amounts.len() as u64;
+        let fee = Amount::from((fee_ppk + 999) / 1000);
+        let new_amount = self.checked_add(fee).ok_or(Error::AmountOverflow)?;
+
+        let split = new_amount.split();
+        let split_fee_ppk = split.len() as u64 * fee_ppk;
+        let split_fee = Amount::from((split_fee_ppk + 999) / 1000);
+
+        if let Some(net_amount) = new_amount.checked_sub(split_fee) {
+            if net_amount >= *self {
+                return Ok(split);
+            }
+        }
+        self.checked_add(Amount::ONE)
+            .ok_or(Error::AmountOverflow)?
+            .split_with_fee(fee_ppk)
+    }
+
     /// Checked addition for Amount. Returns None if overflow occurs.
     pub fn checked_add(self, other: Amount) -> Option<Amount> {
         self.0.checked_add(other.0).map(Amount)
@@ -129,6 +153,16 @@ impl Amount {
         self.0.checked_sub(other.0).map(Amount)
     }
 
+    /// Checked multiplication for Amount. Returns None if overflow occurs.
+    pub fn checked_mul(self, other: Amount) -> Option<Amount> {
+        self.0.checked_mul(other.0).map(Amount)
+    }
+
+    /// Checked division for Amount. Returns None if overflow occurs.
+    pub fn checked_div(self, other: Amount) -> Option<Amount> {
+        self.0.checked_div(other.0).map(Amount)
+    }
+
     /// Try sum to check for overflow
     pub fn try_sum<I>(iter: I) -> Result<Self, Error>
     where
@@ -335,6 +369,27 @@ mod tests {
     }
 
     #[test]
+    fn test_split_with_fee() {
+        let amount = Amount(2);
+        let fee_ppk = 1;
+
+        let split = amount.split_with_fee(fee_ppk).unwrap();
+        assert_eq!(split, vec![Amount(2), Amount(1)]);
+
+        let amount = Amount(3);
+        let fee_ppk = 1;
+
+        let split = amount.split_with_fee(fee_ppk).unwrap();
+        assert_eq!(split, vec![Amount(4)]);
+
+        let amount = Amount(3);
+        let fee_ppk = 1000;
+
+        let split = amount.split_with_fee(fee_ppk).unwrap();
+        assert_eq!(split, vec![Amount(32)]);
+    }
+
+    #[test]
     fn test_split_values() {
         let amount = Amount(10);
 

+ 64 - 4
crates/cashu/src/nuts/nut00/mod.rs

@@ -3,6 +3,7 @@
 //! <https://github.com/cashubtc/nuts/blob/main/00.md>
 
 use std::cmp::Ordering;
+use std::collections::{HashMap, HashSet};
 use std::fmt;
 use std::hash::{Hash, Hasher};
 use std::str::FromStr;
@@ -38,6 +39,12 @@ pub type Proofs = Vec<Proof>;
 
 /// Utility methods for [Proofs]
 pub trait ProofsMethods {
+    /// Count proofs by keyset
+    fn count_by_keyset(&self) -> HashMap<Id, u64>;
+
+    /// Sum proofs by keyset
+    fn sum_by_keyset(&self) -> HashMap<Id, Amount>;
+
     /// Try to sum up the amounts of all [Proof]s
     fn total_amount(&self) -> Result<Amount, Error>;
 
@@ -46,17 +53,65 @@ pub trait ProofsMethods {
 }
 
 impl ProofsMethods for Proofs {
+    fn count_by_keyset(&self) -> HashMap<Id, u64> {
+        count_by_keyset(self.iter())
+    }
+
+    fn sum_by_keyset(&self) -> HashMap<Id, Amount> {
+        sum_by_keyset(self.iter())
+    }
+
     fn total_amount(&self) -> Result<Amount, Error> {
-        Amount::try_sum(self.iter().map(|p| p.amount)).map_err(Into::into)
+        total_amount(self.iter())
     }
 
     fn ys(&self) -> Result<Vec<PublicKey>, Error> {
-        self.iter()
-            .map(|p| p.y())
-            .collect::<Result<Vec<PublicKey>, _>>()
+        ys(self.iter())
     }
 }
 
+impl ProofsMethods for HashSet<Proof> {
+    fn count_by_keyset(&self) -> HashMap<Id, u64> {
+        count_by_keyset(self.iter())
+    }
+
+    fn sum_by_keyset(&self) -> HashMap<Id, Amount> {
+        sum_by_keyset(self.iter())
+    }
+
+    fn total_amount(&self) -> Result<Amount, Error> {
+        total_amount(self.iter())
+    }
+
+    fn ys(&self) -> Result<Vec<PublicKey>, Error> {
+        ys(self.iter())
+    }
+}
+
+fn count_by_keyset<'a, I: Iterator<Item = &'a Proof>>(proofs: I) -> HashMap<Id, u64> {
+    let mut counts = HashMap::new();
+    for proof in proofs {
+        *counts.entry(proof.keyset_id).or_insert(0) += 1;
+    }
+    counts
+}
+
+fn sum_by_keyset<'a, I: Iterator<Item = &'a Proof>>(proofs: I) -> HashMap<Id, Amount> {
+    let mut sums = HashMap::new();
+    for proof in proofs {
+        *sums.entry(proof.keyset_id).or_insert(Amount::ZERO) += proof.amount;
+    }
+    sums
+}
+
+fn total_amount<'a, I: Iterator<Item = &'a Proof>>(proofs: I) -> Result<Amount, Error> {
+    Amount::try_sum(proofs.map(|p| p.amount)).map_err(Into::into)
+}
+
+fn ys<'a, I: Iterator<Item = &'a Proof>>(proofs: I) -> Result<Vec<PublicKey>, Error> {
+    proofs.map(|p| p.y()).collect::<Result<Vec<PublicKey>, _>>()
+}
+
 /// NUT00 Error
 #[derive(Debug, Error)]
 pub enum Error {
@@ -272,6 +327,11 @@ impl Proof {
         }
     }
 
+    /// Check if proof is in active keyset `Id`s
+    pub fn is_active(&self, active_keyset_ids: &[Id]) -> bool {
+        active_keyset_ids.contains(&self.keyset_id)
+    }
+
     /// Get y from proof
     ///
     /// Where y is `hash_to_curve(secret)`

+ 7 - 1
crates/cashu/src/nuts/nut01/public_key.rs

@@ -12,13 +12,19 @@ use super::Error;
 use crate::SECP256K1;
 
 /// PublicKey
-#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
 #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
 pub struct PublicKey {
     #[cfg_attr(feature = "swagger", schema(value_type = String))]
     inner: secp256k1::PublicKey,
 }
 
+impl fmt::Debug for PublicKey {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "PublicKey({})", self.to_hex())
+    }
+}
+
 impl Deref for PublicKey {
     type Target = secp256k1::PublicKey;
 

+ 6 - 2
crates/cashu/src/nuts/nut07.rs

@@ -31,10 +31,12 @@ pub enum State {
     ///
     /// Currently being used in a transaction i.e. melt in progress
     Pending,
-    /// Proof is reserved
+    /// Reserved
     ///
-    /// i.e. used to create a token
+    /// Proof is reserved for future token creation
     Reserved,
+    /// Pending spent (i.e., spent but not yet swapped by receiver)
+    PendingSpent,
 }
 
 impl fmt::Display for State {
@@ -44,6 +46,7 @@ impl fmt::Display for State {
             Self::Unspent => "UNSPENT",
             Self::Pending => "PENDING",
             Self::Reserved => "RESERVED",
+            Self::PendingSpent => "PENDING_SPENT",
         };
 
         write!(f, "{}", s)
@@ -59,6 +62,7 @@ impl FromStr for State {
             "UNSPENT" => Ok(Self::Unspent),
             "PENDING" => Ok(Self::Pending),
             "RESERVED" => Ok(Self::Reserved),
+            "PENDING_SPENT" => Ok(Self::PendingSpent),
             _ => Err(Error::UnknownState),
         }
     }

+ 22 - 0
crates/cashu/src/wallet.rs

@@ -85,3 +85,25 @@ pub enum SendKind {
     /// Wallet must remain offline but can over pay if below tolerance
     OfflineTolerance(Amount),
 }
+
+impl SendKind {
+    /// Check if send kind is online
+    pub fn is_online(&self) -> bool {
+        matches!(self, Self::OnlineExact | Self::OnlineTolerance(_))
+    }
+
+    /// Check if send kind is offline
+    pub fn is_offline(&self) -> bool {
+        matches!(self, Self::OfflineExact | Self::OfflineTolerance(_))
+    }
+
+    /// Check if send kind is exact
+    pub fn is_exact(&self) -> bool {
+        matches!(self, Self::OnlineExact | Self::OfflineExact)
+    }
+
+    /// Check if send kind has tolerance
+    pub fn has_tolerance(&self) -> bool {
+        matches!(self, Self::OnlineTolerance(_) | Self::OfflineTolerance(_))
+    }
+}

+ 9 - 11
crates/cdk-cli/src/sub_commands/pay_request.rs

@@ -1,10 +1,9 @@
 use std::io::{self, Write};
 
 use anyhow::{anyhow, Result};
-use cdk::amount::SplitTarget;
 use cdk::nuts::nut18::TransportType;
 use cdk::nuts::{PaymentRequest, PaymentRequestPayload};
-use cdk::wallet::{MultiMintWallet, SendKind};
+use cdk::wallet::{MultiMintWallet, SendOptions};
 use clap::Args;
 use nostr_sdk::nips::nip19::Nip19Profile;
 use nostr_sdk::{Client as NostrClient, EventBuilder, FromBech32, Keys};
@@ -81,17 +80,16 @@ pub async fn pay_request(
         })
         .ok_or(anyhow!("No supported transport method found"))?;
 
-    let proofs = matching_wallet
-        .send(
+    let prepared_send = matching_wallet
+        .prepare_send(
             amount,
-            None,
-            None,
-            &SplitTarget::default(),
-            &SendKind::default(),
-            true,
+            SendOptions {
+                include_fee: true,
+                ..Default::default()
+            },
         )
-        .await?
-        .proofs();
+        .await?;
+    let proofs = matching_wallet.send(prepared_send, None).await?.proofs();
 
     let payload = PaymentRequestPayload {
         id: payment_request.payment_id.clone(),

+ 14 - 9
crates/cdk-cli/src/sub_commands/send.rs

@@ -3,10 +3,9 @@ use std::io::Write;
 use std::str::FromStr;
 
 use anyhow::{bail, Result};
-use cdk::amount::SplitTarget;
 use cdk::nuts::{Conditions, CurrencyUnit, PublicKey, SpendingConditions};
 use cdk::wallet::types::{SendKind, WalletKey};
-use cdk::wallet::MultiMintWallet;
+use cdk::wallet::{MultiMintWallet, SendMemo, SendOptions};
 use cdk::Amount;
 use clap::Args;
 
@@ -170,16 +169,22 @@ pub async fn send(
         (false, None) => SendKind::OnlineExact,
     };
 
-    let token = wallet
-        .send(
+    let prepared_send = wallet
+        .prepare_send(
             token_amount,
-            sub_command_args.memo.clone(),
-            conditions,
-            &SplitTarget::default(),
-            &send_kind,
-            sub_command_args.include_fee,
+            SendOptions {
+                memo: sub_command_args.memo.clone().map(|memo| SendMemo {
+                    memo,
+                    include_memo: true,
+                }),
+                send_kind,
+                include_fee: sub_command_args.include_fee,
+                conditions,
+                ..Default::default()
+            },
         )
         .await?;
+    let token = wallet.send(prepared_send, None).await?;
 
     match sub_command_args.v3 {
         true => {

+ 2 - 8
crates/cdk-common/src/database/wallet.rs

@@ -84,14 +84,6 @@ pub trait Database: Debug {
         added: Vec<ProofInfo>,
         removed_ys: Vec<PublicKey>,
     ) -> Result<(), Self::Err>;
-    /// Set proofs as pending in storage. Proofs are identified by their Y
-    /// value.
-    async fn set_pending_proofs(&self, ys: Vec<PublicKey>) -> Result<(), Self::Err>;
-    /// Reserve proofs in storage. Proofs are identified by their Y value.
-    async fn reserve_proofs(&self, ys: Vec<PublicKey>) -> Result<(), Self::Err>;
-    /// Set proofs as unspent in storage. Proofs are identified by their Y
-    /// value.
-    async fn set_unspent_proofs(&self, ys: Vec<PublicKey>) -> Result<(), Self::Err>;
     /// Get proofs from storage
     async fn get_proofs(
         &self,
@@ -100,6 +92,8 @@ pub trait Database: Debug {
         state: Option<Vec<State>>,
         spending_conditions: Option<Vec<SpendingConditions>>,
     ) -> Result<Vec<ProofInfo>, Self::Err>;
+    /// Update proofs state in storage
+    async fn update_proofs_state(&self, ys: Vec<PublicKey>, state: State) -> Result<(), Self::Err>;
 
     /// Increment Keyset counter
     async fn increment_keyset_counter(&self, keyset_id: &Id, count: u32) -> Result<(), Self::Err>;

+ 3 - 0
crates/cdk-common/src/error.rs

@@ -169,6 +169,9 @@ pub enum Error {
     /// Insufficient Funds
     #[error("Insufficient funds")]
     InsufficientFunds,
+    /// Unexpected proof state
+    #[error("Unexpected proof state")]
+    UnexpectedProofState,
     /// No active keyset
     #[error("No active keyset")]
     NoActiveKeyset,

+ 14 - 10
crates/cdk-integration-tests/tests/integration_tests_pure.rs

@@ -1,8 +1,10 @@
 use std::assert_eq;
+use std::collections::HashSet;
+use std::hash::RandomState;
 
 use cdk::amount::SplitTarget;
 use cdk::nuts::nut00::ProofsMethods;
-use cdk::wallet::SendKind;
+use cdk::wallet::SendOptions;
 use cdk::Amount;
 use cdk_integration_tests::init_pure_tests::*;
 
@@ -18,18 +20,20 @@ async fn test_swap_to_send() -> anyhow::Result<()> {
     assert_eq!(Amount::from(64), balance_alice);
 
     // Alice wants to send 40 sats, which internally swaps
-    let token = wallet_alice
-        .send(
-            Amount::from(40),
-            None,
-            None,
-            &SplitTarget::None,
-            &SendKind::OnlineExact,
-            false,
-        )
+    let prepared_send = wallet_alice
+        .prepare_send(Amount::from(40), SendOptions::default())
         .await?;
+    assert_eq!(
+        HashSet::<_, RandomState>::from_iter(prepared_send.proofs().ys()?),
+        HashSet::from_iter(wallet_alice.get_reserved_proofs().await?.ys()?)
+    );
+    let token = wallet_alice.send(prepared_send, None).await?;
     assert_eq!(Amount::from(40), token.proofs().total_amount()?);
     assert_eq!(Amount::from(24), wallet_alice.total_balance().await?);
+    assert_eq!(
+        HashSet::<_, RandomState>::from_iter(token.proofs().ys()?),
+        HashSet::from_iter(wallet_alice.get_pending_spent_proofs().await?.ys()?)
+    );
 
     // Alice sends cashu, Carol receives
     let wallet_carol = create_test_wallet_arc_for_mint(mint_bob.clone()).await?;

+ 40 - 55
crates/cdk-redb/src/wallet/mod.rs

@@ -145,46 +145,6 @@ impl WalletRedbDatabase {
 
         Ok(Self { db: Arc::new(db) })
     }
-
-    async fn update_proof_states(
-        &self,
-        ys: Vec<PublicKey>,
-        state: State,
-    ) -> Result<(), database::Error> {
-        let read_txn = self.db.begin_read().map_err(Error::from)?;
-        let table = read_txn.open_table(PROOFS_TABLE).map_err(Error::from)?;
-
-        let write_txn = self.db.begin_write().map_err(Error::from)?;
-
-        for y in ys {
-            let y_slice = y.to_bytes();
-            let proof = table
-                .get(y_slice.as_slice())
-                .map_err(Error::from)?
-                .ok_or(Error::UnknownY)?;
-
-            let mut proof_info =
-                serde_json::from_str::<ProofInfo>(proof.value()).map_err(Error::from)?;
-
-            proof_info.state = state;
-
-            {
-                let mut table = write_txn.open_table(PROOFS_TABLE).map_err(Error::from)?;
-                table
-                    .insert(
-                        y_slice.as_slice(),
-                        serde_json::to_string(&proof_info)
-                            .map_err(Error::from)?
-                            .as_str(),
-                    )
-                    .map_err(Error::from)?;
-            }
-        }
-
-        write_txn.commit().map_err(Error::from)?;
-
-        Ok(())
-    }
 }
 
 #[async_trait]
@@ -611,21 +571,6 @@ impl WalletDatabase for WalletRedbDatabase {
         Ok(())
     }
 
-    #[instrument(skip(self, ys))]
-    async fn set_pending_proofs(&self, ys: Vec<PublicKey>) -> Result<(), Self::Err> {
-        self.update_proof_states(ys, State::Pending).await
-    }
-
-    #[instrument(skip(self, ys))]
-    async fn reserve_proofs(&self, ys: Vec<PublicKey>) -> Result<(), Self::Err> {
-        self.update_proof_states(ys, State::Reserved).await
-    }
-
-    #[instrument(skip(self, ys))]
-    async fn set_unspent_proofs(&self, ys: Vec<PublicKey>) -> Result<(), Self::Err> {
-        self.update_proof_states(ys, State::Unspent).await
-    }
-
     #[instrument(skip_all)]
     async fn get_proofs(
         &self,
@@ -659,6 +604,46 @@ impl WalletDatabase for WalletRedbDatabase {
         Ok(proofs)
     }
 
+    async fn update_proofs_state(
+        &self,
+        ys: Vec<PublicKey>,
+        state: State,
+    ) -> Result<(), database::Error> {
+        let read_txn = self.db.begin_read().map_err(Error::from)?;
+        let table = read_txn.open_table(PROOFS_TABLE).map_err(Error::from)?;
+
+        let write_txn = self.db.begin_write().map_err(Error::from)?;
+
+        for y in ys {
+            let y_slice = y.to_bytes();
+            let proof = table
+                .get(y_slice.as_slice())
+                .map_err(Error::from)?
+                .ok_or(Error::UnknownY)?;
+
+            let mut proof_info =
+                serde_json::from_str::<ProofInfo>(proof.value()).map_err(Error::from)?;
+
+            proof_info.state = state;
+
+            {
+                let mut table = write_txn.open_table(PROOFS_TABLE).map_err(Error::from)?;
+                table
+                    .insert(
+                        y_slice.as_slice(),
+                        serde_json::to_string(&proof_info)
+                            .map_err(Error::from)?
+                            .as_str(),
+                    )
+                    .map_err(Error::from)?;
+            }
+        }
+
+        write_txn.commit().map_err(Error::from)?;
+
+        Ok(())
+    }
+
     #[instrument(skip(self), fields(keyset_id = %keyset_id))]
     async fn increment_keyset_counter(&self, keyset_id: &Id, count: u32) -> Result<(), Self::Err> {
         let write_txn = self.db.begin_write().map_err(Error::from)?;

+ 31 - 0
crates/cdk-sqlite/src/wallet/migrations/20250314082116_allow_pending_spent.sql

@@ -0,0 +1,31 @@
+-- Create a new table with the updated CHECK constraint
+CREATE TABLE IF NOT EXISTS proof_new (
+y BLOB PRIMARY KEY,
+mint_url TEXT NOT NULL,
+state TEXT CHECK ( state IN ('SPENT', 'UNSPENT', 'PENDING', 'RESERVED', 'PENDING_SPENT' ) ) NOT NULL,
+spending_condition TEXT,
+unit TEXT NOT NULL,
+amount INTEGER NOT NULL,
+keyset_id TEXT NOT NULL,
+secret TEXT NOT NULL,
+c BLOB NOT NULL,
+witness TEXT
+);
+
+CREATE INDEX IF NOT EXISTS secret_index ON proof_new(secret);
+CREATE INDEX IF NOT EXISTS state_index ON proof_new(state);
+CREATE INDEX IF NOT EXISTS spending_condition_index ON proof_new(spending_condition);
+CREATE INDEX IF NOT EXISTS unit_index ON proof_new(unit);
+CREATE INDEX IF NOT EXISTS amount_index ON proof_new(amount);
+CREATE INDEX IF NOT EXISTS mint_url_index ON proof_new(mint_url);
+
+-- Copy data from old proof table to new proof table
+INSERT INTO proof_new (y, mint_url, state, spending_condition, unit, amount, keyset_id, secret, c, witness)
+SELECT y, mint_url, state, spending_condition, unit, amount, keyset_id, secret, c, witness
+FROM proof;
+
+-- Drop the old proof table
+DROP TABLE proof;
+
+-- Rename the new proof table to proof
+ALTER TABLE proof_new RENAME TO proof;

+ 25 - 41
crates/cdk-sqlite/src/wallet/mod.rs

@@ -59,23 +59,6 @@ impl WalletSqliteDatabase {
             .await
             .expect("Could not run migrations");
     }
-
-    async fn set_proof_state(&self, y: PublicKey, state: State) -> Result<(), database::Error> {
-        sqlx::query(
-            r#"
-    UPDATE proof
-    SET state=?
-    WHERE y IS ?;
-            "#,
-        )
-        .bind(state.to_string())
-        .bind(y.to_bytes().to_vec())
-        .execute(&self.pool)
-        .await
-        .map_err(Error::from)?;
-
-        Ok(())
-    }
 }
 
 #[async_trait]
@@ -658,30 +641,6 @@ WHERE id=?
         Ok(())
     }
 
-    async fn set_pending_proofs(&self, ys: Vec<PublicKey>) -> Result<(), Self::Err> {
-        for y in ys {
-            self.set_proof_state(y, State::Pending).await?;
-        }
-
-        Ok(())
-    }
-
-    async fn reserve_proofs(&self, ys: Vec<PublicKey>) -> Result<(), Self::Err> {
-        for y in ys {
-            self.set_proof_state(y, State::Reserved).await?;
-        }
-
-        Ok(())
-    }
-
-    async fn set_unspent_proofs(&self, ys: Vec<PublicKey>) -> Result<(), Self::Err> {
-        for y in ys {
-            self.set_proof_state(y, State::Unspent).await?;
-        }
-
-        Ok(())
-    }
-
     #[instrument(skip(self, state, spending_conditions))]
     async fn get_proofs(
         &self,
@@ -734,6 +693,31 @@ FROM proof;
         }
     }
 
+    async fn update_proofs_state(&self, ys: Vec<PublicKey>, state: State) -> Result<(), Self::Err> {
+        let mut transaction = self.pool.begin().await.map_err(Error::from)?;
+
+        let update_sql = format!(
+            "UPDATE proof SET state = ? WHERE y IN ({})",
+            "?,".repeat(ys.len()).trim_end_matches(',')
+        );
+
+        ys.iter()
+            .fold(
+                sqlx::query(&update_sql).bind(state.to_string()),
+                |query, y| query.bind(y.to_bytes().to_vec()),
+            )
+            .execute(&mut *transaction)
+            .await
+            .map_err(|err| {
+                tracing::error!("SQLite could not update proof state: {err:?}");
+                Error::SQLX(err)
+            })?;
+
+        transaction.commit().await.map_err(Error::from)?;
+
+        Ok(())
+    }
+
     #[instrument(skip(self), fields(keyset_id = %keyset_id))]
     async fn increment_keyset_counter(&self, keyset_id: &Id, count: u32) -> Result<(), Self::Err> {
         let mut transaction = self.pool.begin().await.map_err(Error::from)?;

+ 13 - 12
crates/cdk/examples/mint-token.rs

@@ -4,14 +4,23 @@ use cdk::amount::SplitTarget;
 use cdk::error::Error;
 use cdk::nuts::nut00::ProofsMethods;
 use cdk::nuts::{CurrencyUnit, MintQuoteState, NotificationPayload};
-use cdk::wallet::types::SendKind;
-use cdk::wallet::{Wallet, WalletSubscription};
+use cdk::wallet::{SendOptions, Wallet, WalletSubscription};
 use cdk::Amount;
 use cdk_sqlite::wallet::memory;
 use rand::Rng;
+use tracing_subscriber::EnvFilter;
 
 #[tokio::main]
 async fn main() -> Result<(), Error> {
+    let default_filter = "debug";
+
+    let sqlx_filter = "sqlx=warn,hyper_util=warn,reqwest=warn,rustls=warn";
+
+    let env_filter = EnvFilter::new(format!("{},{}", default_filter, sqlx_filter));
+
+    // Parse input
+    tracing_subscriber::fmt().with_env_filter(env_filter).init();
+
     // Initialize the memory store for the wallet
     let localstore = memory::empty().await?;
 
@@ -52,16 +61,8 @@ async fn main() -> Result<(), Error> {
     println!("Received {} from mint {}", receive_amount, mint_url);
 
     // Send a token with the specified amount
-    let token = wallet
-        .send(
-            amount,
-            None,
-            None,
-            &SplitTarget::default(),
-            &SendKind::OnlineExact,
-            false,
-        )
-        .await?;
+    let prepared_send = wallet.prepare_send(amount, SendOptions::default()).await?;
+    let token = wallet.send(prepared_send, None).await?;
     println!("Token:");
     println!("{}", token);
 

+ 21 - 13
crates/cdk/examples/p2pk.rs

@@ -3,8 +3,7 @@ use std::sync::Arc;
 use cdk::amount::SplitTarget;
 use cdk::error::Error;
 use cdk::nuts::{CurrencyUnit, MintQuoteState, NotificationPayload, SecretKey, SpendingConditions};
-use cdk::wallet::types::SendKind;
-use cdk::wallet::{Wallet, WalletSubscription};
+use cdk::wallet::{SendOptions, Wallet, WalletSubscription};
 use cdk::Amount;
 use cdk_sqlite::wallet::memory;
 use rand::Rng;
@@ -30,10 +29,10 @@ async fn main() -> Result<(), Error> {
     // Define the mint URL and currency unit
     let mint_url = "https://testnut.cashu.space";
     let unit = CurrencyUnit::Sat;
-    let amount = Amount::from(50);
+    let amount = Amount::from(100);
 
     // Create a new wallet
-    let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), &seed, None)?;
+    let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), &seed, Some(1))?;
 
     // Request a mint quote from the wallet
     let quote = wallet.mint_quote(amount, None).await?;
@@ -57,7 +56,14 @@ async fn main() -> Result<(), Error> {
     }
 
     // Mint the received amount
-    let _receive_amount = wallet.mint(&quote.id, SplitTarget::default(), None).await?;
+    let received_proofs = wallet.mint(&quote.id, SplitTarget::default(), None).await?;
+    println!(
+        "Minted nuts: {:?}",
+        received_proofs
+            .into_iter()
+            .map(|p| p.amount)
+            .collect::<Vec<_>>()
+    );
 
     // Generate a secret key for spending conditions
     let secret = SecretKey::generate();
@@ -67,19 +73,21 @@ async fn main() -> Result<(), Error> {
 
     // Get the total balance of the wallet
     let bal = wallet.total_balance().await?;
-    println!("{}", bal);
+    println!("Total balance: {}", bal);
 
     // Send a token with the specified amount and spending conditions
-    let token = wallet
-        .send(
+    let prepared_send = wallet
+        .prepare_send(
             10.into(),
-            None,
-            Some(spending_conditions),
-            &SplitTarget::default(),
-            &SendKind::default(),
-            false,
+            SendOptions {
+                conditions: Some(spending_conditions),
+                include_fee: true,
+                ..Default::default()
+            },
         )
         .await?;
+    println!("Fee: {}", prepared_send.fee());
+    let token = wallet.send(prepared_send, None).await?;
 
     println!("Created token locked to pubkey: {}", secret.public_key());
     println!("{}", token);

+ 10 - 3
crates/cdk/examples/proof-selection.rs

@@ -1,5 +1,6 @@
 //! Wallet example with memory store
 
+use std::collections::HashMap;
 use std::sync::Arc;
 
 use cdk::amount::SplitTarget;
@@ -59,9 +60,15 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
     let proofs = wallet.get_unspent_proofs().await?;
 
     // Select proofs to send
-    let selected = wallet
-        .select_proofs_to_send(Amount::from(64), proofs, false)
-        .await?;
+    let amount = Amount::from(64);
+    let active_keyset_ids = wallet
+        .get_active_mint_keysets()
+        .await?
+        .into_iter()
+        .map(|keyset| keyset.id)
+        .collect();
+    let selected =
+        Wallet::select_proofs(amount, proofs, &active_keyset_ids, &HashMap::new(), false)?;
     for (i, proof) in selected.iter().enumerate() {
         println!("{}: {}", i, proof.amount);
     }

+ 3 - 12
crates/cdk/examples/wallet.rs

@@ -4,8 +4,7 @@ use std::time::Duration;
 use cdk::amount::SplitTarget;
 use cdk::nuts::nut00::ProofsMethods;
 use cdk::nuts::{CurrencyUnit, MintQuoteState};
-use cdk::wallet::types::SendKind;
-use cdk::wallet::Wallet;
+use cdk::wallet::{SendOptions, Wallet};
 use cdk::Amount;
 use cdk_sqlite::wallet::memory;
 use rand::Rng;
@@ -59,16 +58,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
     println!("Minted {}", receive_amount);
 
     // Send the token
-    let token = wallet
-        .send(
-            amount,
-            None,
-            None,
-            &SplitTarget::None,
-            &SendKind::default(),
-            false,
-        )
-        .await?;
+    let prepared_send = wallet.prepare_send(amount, SendOptions::default()).await?;
+    let token = wallet.send(prepared_send, None).await?;
 
     println!("{}", token);
 

+ 27 - 0
crates/cdk/src/wallet/keysets.rs

@@ -1,3 +1,5 @@
+use std::collections::HashMap;
+
 use tracing::instrument;
 
 use crate::nuts::{Id, KeySetInfo, Keys};
@@ -99,4 +101,29 @@ impl Wallet {
             .ok_or(Error::NoActiveKeyset)?;
         Ok(keyset_with_lowest_fee)
     }
+
+    /// Get keyset fees for mint
+    pub async fn get_keyset_fees(&self) -> Result<HashMap<Id, u64>, Error> {
+        let keysets = self
+            .localstore
+            .get_mint_keysets(self.mint_url.clone())
+            .await?
+            .ok_or(Error::UnknownKeySet)?;
+
+        let mut fees = HashMap::new();
+        for keyset in keysets {
+            fees.insert(keyset.id, keyset.input_fee_ppk);
+        }
+
+        Ok(fees)
+    }
+
+    /// Get keyset fees for mint by keyset id
+    pub async fn get_keyset_fees_by_id(&self, keyset_id: Id) -> Result<u64, Error> {
+        self.get_keyset_fees()
+            .await?
+            .get(&keyset_id)
+            .cloned()
+            .ok_or(Error::UnknownKeySet)
+    }
 }

+ 17 - 4
crates/cdk/src/wallet/melt.rs

@@ -130,7 +130,9 @@ impl Wallet {
         }
 
         let ys = proofs.ys()?;
-        self.localstore.set_pending_proofs(ys).await?;
+        self.localstore
+            .update_proofs_state(ys, State::Pending)
+            .await?;
 
         let active_keyset_id = self.get_active_mint_keyset().await?.id;
 
@@ -287,9 +289,20 @@ impl Wallet {
 
         let available_proofs = self.get_unspent_proofs().await?;
 
-        let input_proofs = self
-            .select_proofs_to_swap(inputs_needed_amount, available_proofs)
-            .await?;
+        let active_keyset_ids = self
+            .get_active_mint_keysets()
+            .await?
+            .into_iter()
+            .map(|k| k.id)
+            .collect();
+        let keyset_fees = self.get_keyset_fees().await?;
+        let input_proofs = Wallet::select_proofs(
+            inputs_needed_amount,
+            available_proofs,
+            &active_keyset_ids,
+            &keyset_fees,
+            true,
+        )?;
 
         self.melt_proofs(quote_id, input_proofs).await
     }

+ 17 - 17
crates/cdk/src/wallet/mod.rs

@@ -43,6 +43,7 @@ mod swap;
 pub mod util;
 
 pub use cdk_common::wallet as types;
+pub use send::{PreparedSend, SendMemo, SendOptions};
 
 use crate::nuts::nut00::ProofsMethods;
 
@@ -173,25 +174,24 @@ impl Wallet {
     /// Fee required for proof set
     #[instrument(skip_all)]
     pub async fn get_proofs_fee(&self, proofs: &Proofs) -> Result<Amount, Error> {
-        let mut proofs_per_keyset = HashMap::new();
-        let mut fee_per_keyset = HashMap::new();
+        let proofs_per_keyset = proofs.count_by_keyset();
+        self.get_proofs_fee_by_count(proofs_per_keyset).await
+    }
 
-        for proof in proofs {
-            if let std::collections::hash_map::Entry::Vacant(e) =
-                fee_per_keyset.entry(proof.keyset_id)
-            {
-                let mint_keyset_info = self
-                    .localstore
-                    .get_keyset_by_id(&proof.keyset_id)
-                    .await?
-                    .ok_or(Error::UnknownKeySet)?;
-                e.insert(mint_keyset_info.input_fee_ppk);
-            }
+    /// Fee required for proof set by count
+    pub async fn get_proofs_fee_by_count(
+        &self,
+        proofs_per_keyset: HashMap<Id, u64>,
+    ) -> Result<Amount, Error> {
+        let mut fee_per_keyset = HashMap::new();
 
-            proofs_per_keyset
-                .entry(proof.keyset_id)
-                .and_modify(|count| *count += 1)
-                .or_insert(1);
+        for keyset_id in proofs_per_keyset.keys() {
+            let mint_keyset_info = self
+                .localstore
+                .get_keyset_by_id(keyset_id)
+                .await?
+                .ok_or(Error::UnknownKeySet)?;
+            fee_per_keyset.insert(*keyset_id, mint_keyset_info.input_fee_ppk);
         }
 
         let fee = calculate_fee(&proofs_per_keyset, &fee_per_keyset)?;

+ 20 - 16
crates/cdk/src/wallet/multi_mint_wallet.rs

@@ -11,7 +11,7 @@ use cdk_common::wallet::WalletKey;
 use tokio::sync::Mutex;
 use tracing::instrument;
 
-use super::types::SendKind;
+use super::send::{PreparedSend, SendMemo, SendOptions};
 use super::Error;
 use crate::amount::SplitTarget;
 use crate::mint_url::MintUrl;
@@ -110,32 +110,36 @@ impl MultiMintWallet {
         Ok(mint_proofs)
     }
 
+    /// Prepare to send
+    #[instrument(skip(self))]
+    pub async fn prepare_send(
+        &self,
+        wallet_key: &WalletKey,
+        amount: Amount,
+        opts: SendOptions,
+    ) -> Result<PreparedSend, Error> {
+        let wallet = self
+            .get_wallet(wallet_key)
+            .await
+            .ok_or(Error::UnknownWallet(wallet_key.clone()))?;
+
+        wallet.prepare_send(amount, opts).await
+    }
+
     /// Create cashu token
     #[instrument(skip(self))]
     pub async fn send(
         &self,
         wallet_key: &WalletKey,
-        amount: Amount,
-        memo: Option<String>,
-        conditions: Option<SpendingConditions>,
-        send_kind: SendKind,
-        include_fees: bool,
+        send: PreparedSend,
+        memo: Option<SendMemo>,
     ) -> Result<Token, Error> {
         let wallet = self
             .get_wallet(wallet_key)
             .await
             .ok_or(Error::UnknownWallet(wallet_key.clone()))?;
 
-        wallet
-            .send(
-                amount,
-                memo,
-                conditions,
-                &SplitTarget::default(),
-                &send_kind,
-                include_fees,
-            )
-            .await
+        wallet.send(send, memo).await
     }
 
     /// Mint quote for wallet

+ 342 - 78
crates/cdk/src/wallet/proofs.rs

@@ -1,8 +1,10 @@
-use std::collections::HashSet;
+use std::collections::{HashMap, HashSet};
 
+use cdk_common::Id;
 use tracing::instrument;
 
 use crate::amount::SplitTarget;
+use crate::fees::calculate_fee;
 use crate::nuts::nut00::ProofsMethods;
 use crate::nuts::{
     CheckStateRequest, Proof, ProofState, Proofs, PublicKey, SpendingConditions, State,
@@ -30,6 +32,13 @@ impl Wallet {
             .await
     }
 
+    /// Get pending spent [`Proofs`]
+    #[instrument(skip(self))]
+    pub async fn get_pending_spent_proofs(&self) -> Result<Proofs, Error> {
+        self.get_proofs_with(Some(vec![State::PendingSpent]), None)
+            .await
+    }
+
     /// Get this wallet's [Proofs] that match the args
     pub async fn get_proofs_with(
         &self,
@@ -53,7 +62,10 @@ impl Wallet {
     /// Return proofs to unspent allowing them to be selected and spent
     #[instrument(skip(self))]
     pub async fn unreserve_proofs(&self, ys: Vec<PublicKey>) -> Result<(), Error> {
-        Ok(self.localstore.set_unspent_proofs(ys).await?)
+        Ok(self
+            .localstore
+            .update_proofs_state(ys, State::Unspent)
+            .await?)
     }
 
     /// Reclaim unspent proofs
@@ -112,7 +124,7 @@ impl Wallet {
             .get_proofs(
                 Some(self.mint_url.clone()),
                 Some(self.unit.clone()),
-                Some(vec![State::Pending, State::Reserved]),
+                Some(vec![State::Pending, State::Reserved, State::PendingSpent]),
                 None,
             )
             .await?;
@@ -153,123 +165,375 @@ impl Wallet {
         Ok(balance)
     }
 
-    /// Select proofs to send
+    /// Select proofs
     #[instrument(skip_all)]
-    pub async fn select_proofs_to_send(
-        &self,
+    pub fn select_proofs(
         amount: Amount,
         proofs: Proofs,
+        active_keyset_ids: &Vec<Id>,
+        keyset_fees: &HashMap<Id, u64>,
         include_fees: bool,
     ) -> Result<Proofs, Error> {
         tracing::debug!(
-            "Selecting proofs to send {} from {}",
+            "amount={}, proofs={:?}",
             amount,
-            proofs.total_amount()?
+            proofs.iter().map(|p| p.amount.into()).collect::<Vec<u64>>()
         );
+        if amount == Amount::ZERO {
+            return Ok(vec![]);
+        }
         ensure_cdk!(proofs.total_amount()? >= amount, Error::InsufficientFunds);
 
-        let (mut proofs_larger, mut proofs_smaller): (Proofs, Proofs) =
-            proofs.into_iter().partition(|p| p.amount > amount);
-
-        let next_bigger_proof = proofs_larger.first().cloned();
+        // Sort proofs in descending order
+        let mut proofs = proofs;
+        proofs.sort_by(|a, b| a.cmp(b).reverse());
 
-        let mut selected_proofs: Proofs = Vec::new();
-        let mut remaining_amount = amount;
+        // Split the amount into optimal amounts
+        let optimal_amounts = amount.split();
 
-        while remaining_amount > Amount::ZERO {
-            proofs_larger.sort();
-            // Sort smaller proofs in descending order
-            proofs_smaller.sort_by(|a: &Proof, b: &Proof| b.cmp(a));
-
-            let selected_proof = if let Some(next_small) = proofs_smaller.clone().first() {
-                next_small.clone()
-            } else if let Some(next_bigger) = proofs_larger.first() {
-                next_bigger.clone()
+        // Track selected proofs and remaining amounts (include all inactive proofs first)
+        let mut selected_proofs: HashSet<Proof> = proofs
+            .iter()
+            .filter(|p| !p.is_active(active_keyset_ids))
+            .cloned()
+            .collect();
+        if selected_proofs.total_amount()? >= amount {
+            tracing::debug!("All inactive proofs are sufficient");
+            return Ok(selected_proofs.into_iter().collect());
+        }
+        let mut remaining_amounts: Vec<Amount> = Vec::new();
+
+        // Select proof with the exact amount and not already selected
+        let mut select_proof = |proofs: &Proofs, amount: Amount, exact: bool| -> bool {
+            let mut last_proof = None;
+            for proof in proofs.iter() {
+                if !selected_proofs.contains(proof) {
+                    if proof.amount == amount {
+                        selected_proofs.insert(proof.clone());
+                        return true;
+                    } else if !exact && proof.amount > amount {
+                        last_proof = Some(proof.clone());
+                    } else if proof.amount < amount {
+                        break;
+                    }
+                }
+            }
+            if let Some(proof) = last_proof {
+                selected_proofs.insert(proof);
+                true
             } else {
-                break;
-            };
+                false
+            }
+        };
 
-            let proof_amount = selected_proof.amount;
+        // Select proofs with the optimal amounts
+        for optimal_amount in optimal_amounts {
+            if !select_proof(&proofs, optimal_amount, true) {
+                // Add the remaining amount to the remaining amounts because proof with the optimal amount was not found
+                remaining_amounts.push(optimal_amount);
+            }
+        }
 
-            selected_proofs.push(selected_proof);
+        // If all the optimal amounts are selected, return the selected proofs
+        if remaining_amounts.is_empty() {
+            tracing::debug!("All optimal amounts are selected");
+            if include_fees {
+                return Self::include_fees(
+                    amount,
+                    proofs,
+                    selected_proofs.into_iter().collect(),
+                    active_keyset_ids,
+                    keyset_fees,
+                );
+            } else {
+                return Ok(selected_proofs.into_iter().collect());
+            }
+        }
 
-            let fees = match include_fees {
-                true => self.get_proofs_fee(&selected_proofs).await?,
-                false => Amount::ZERO,
-            };
+        // Select proofs with the remaining amounts by checking for 2 of the half amount, 4 of the quarter amount, etc.
+        tracing::debug!("Selecting proofs with the remaining amounts");
+        for remaining_amount in remaining_amounts {
+            // Number of proofs to select
+            let mut n = 2;
+
+            let mut target_amount = remaining_amount;
+            let mut found = false;
+            while let Some(curr_amount) = target_amount.checked_div(Amount::from(2)) {
+                if curr_amount == Amount::ZERO {
+                    break;
+                }
+
+                // Select proofs with the current amount
+                let mut count = 0;
+                for _ in 0..n {
+                    if select_proof(&proofs, curr_amount, true) {
+                        count += 1;
+                    } else {
+                        break;
+                    }
+                }
+                n -= count;
+
+                // All proofs with the current amount are selected
+                if n == 0 {
+                    found = true;
+                    break;
+                }
+
+                // Try to find double the number of the next amount
+                n *= 2;
+                target_amount = curr_amount;
+            }
 
-            if proof_amount >= remaining_amount + fees {
-                remaining_amount = Amount::ZERO;
-                break;
+            // Find closest amount over the remaining amount
+            if !found {
+                select_proof(&proofs, remaining_amount, false);
             }
+        }
 
-            remaining_amount = amount.checked_add(fees).ok_or(Error::AmountOverflow)?
-                - selected_proofs.total_amount()?;
-            (proofs_larger, proofs_smaller) = proofs_smaller
-                .into_iter()
-                .skip(1)
-                .partition(|p| p.amount > remaining_amount);
+        // Check if the selected proofs total amount is equal to the amount else filter out unnecessary proofs
+        let mut selected_proofs = selected_proofs.into_iter().collect::<Vec<_>>();
+        let total_amount = selected_proofs.total_amount()?;
+        if total_amount != amount && selected_proofs.len() > 1 {
+            selected_proofs.sort_by(|a, b| a.cmp(b).reverse());
+            selected_proofs = Self::select_least_amount_over(selected_proofs, amount)?;
         }
 
-        if remaining_amount > Amount::ZERO {
-            if let Some(next_bigger) = next_bigger_proof {
-                return Ok(vec![next_bigger.clone()]);
-            }
+        if include_fees {
+            return Self::include_fees(
+                amount,
+                proofs,
+                selected_proofs,
+                active_keyset_ids,
+                keyset_fees,
+            );
+        }
+
+        Ok(selected_proofs)
+    }
 
+    fn select_least_amount_over(proofs: Proofs, amount: Amount) -> Result<Vec<Proof>, Error> {
+        let total_amount = proofs.total_amount()?;
+        if total_amount < amount {
             return Err(Error::InsufficientFunds);
         }
+        if proofs.len() == 1 {
+            return Ok(proofs);
+        }
 
-        Ok(selected_proofs)
+        for i in 1..proofs.len() {
+            let (left, right) = proofs.split_at(i);
+            let left = left.to_vec();
+            let right = right.to_vec();
+            let left_amount = left.total_amount()?;
+            let right_amount = right.total_amount()?;
+
+            if left_amount >= amount && right_amount >= amount {
+                match (
+                    Self::select_least_amount_over(left, amount),
+                    Self::select_least_amount_over(right, amount),
+                ) {
+                    (Ok(left_proofs), Ok(right_proofs)) => {
+                        let left_total_amount = left_proofs.total_amount()?;
+                        let right_total_amount = right_proofs.total_amount()?;
+                        if left_total_amount < right_total_amount {
+                            return Ok(left_proofs);
+                        } else {
+                            return Ok(right_proofs);
+                        }
+                    }
+                    (Ok(left_proofs), Err(_)) => return Ok(left_proofs),
+                    (Err(_), Ok(right_proofs)) => return Ok(right_proofs),
+                    (Err(_), Err(_)) => return Err(Error::InsufficientFunds),
+                }
+            } else if left_amount >= amount {
+                return Self::select_least_amount_over(left, amount);
+            } else if right_amount >= amount {
+                return Self::select_least_amount_over(right, amount);
+            }
+        }
+
+        Ok(proofs)
     }
 
-    /// Select proofs to send
-    #[instrument(skip_all)]
-    pub async fn select_proofs_to_swap(
-        &self,
+    fn include_fees(
         amount: Amount,
         proofs: Proofs,
+        mut selected_proofs: Proofs,
+        active_keyset_ids: &Vec<Id>,
+        keyset_fees: &HashMap<Id, u64>,
     ) -> Result<Proofs, Error> {
+        tracing::debug!("Including fees");
+        let fee =
+            calculate_fee(&selected_proofs.count_by_keyset(), keyset_fees).unwrap_or_default();
+        let net_amount = selected_proofs.total_amount()? - fee;
         tracing::debug!(
-            "Selecting proofs to swap {} from {}",
-            amount,
-            proofs.total_amount()?
+            "Net amount={}, fee={}, total amount={}",
+            net_amount,
+            fee,
+            selected_proofs.total_amount()?
         );
-        let active_keyset_id = self.get_active_mint_keyset().await?.id;
+        if net_amount >= amount {
+            tracing::debug!(
+                "Selected proofs: {:?}",
+                selected_proofs
+                    .iter()
+                    .map(|p| p.amount.into())
+                    .collect::<Vec<u64>>(),
+            );
+            return Ok(selected_proofs);
+        }
 
-        let (mut active_proofs, mut inactive_proofs): (Proofs, Proofs) = proofs
+        tracing::debug!("Net amount is less than the required amount");
+        let remaining_amount = amount - net_amount;
+        let remaining_proofs = proofs
             .into_iter()
-            .partition(|p| p.keyset_id == active_keyset_id);
+            .filter(|p| !selected_proofs.contains(p))
+            .collect::<Proofs>();
+        selected_proofs.extend(Wallet::select_proofs(
+            remaining_amount,
+            remaining_proofs,
+            active_keyset_ids,
+            &HashMap::new(), // Fees are already calculated
+            false,
+        )?);
+        tracing::debug!(
+            "Selected proofs: {:?}",
+            selected_proofs
+                .iter()
+                .map(|p| p.amount.into())
+                .collect::<Vec<u64>>(),
+        );
+        Ok(selected_proofs)
+    }
+}
 
-        let mut selected_proofs: Proofs = Vec::new();
-        inactive_proofs.sort_by(|a: &Proof, b: &Proof| b.cmp(a));
+#[cfg(test)]
+mod tests {
+    use std::collections::HashMap;
 
-        for inactive_proof in inactive_proofs {
-            selected_proofs.push(inactive_proof);
-            let selected_total = selected_proofs.total_amount()?;
-            let fees = self.get_proofs_fee(&selected_proofs).await?;
+    use cdk_common::secret::Secret;
+    use cdk_common::{Amount, Id, Proof, PublicKey};
 
-            if selected_total >= amount + fees {
-                return Ok(selected_proofs);
-            }
-        }
+    use crate::Wallet;
 
-        active_proofs.sort_by(|a: &Proof, b: &Proof| b.cmp(a));
+    fn id() -> Id {
+        Id::from_bytes(&[0; 8]).unwrap()
+    }
 
-        for active_proof in active_proofs {
-            selected_proofs.push(active_proof);
-            let selected_total = selected_proofs.total_amount()?;
-            let fees = self.get_proofs_fee(&selected_proofs).await?;
+    fn proof(amount: u64) -> Proof {
+        Proof::new(
+            Amount::from(amount),
+            id(),
+            Secret::generate(),
+            PublicKey::from_hex(
+                "03deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
+            )
+            .unwrap(),
+        )
+    }
 
-            if selected_total >= amount + fees {
-                return Ok(selected_proofs);
-            }
+    #[test]
+    fn test_select_proofs_empty() {
+        let proofs = vec![];
+        let selected_proofs =
+            Wallet::select_proofs(0.into(), proofs, &vec![id()], &HashMap::new(), false).unwrap();
+        assert_eq!(selected_proofs.len(), 0);
+    }
+
+    #[test]
+    fn test_select_proofs_insufficient() {
+        let proofs = vec![proof(1), proof(2), proof(4)];
+        let selected_proofs =
+            Wallet::select_proofs(8.into(), proofs, &vec![id()], &HashMap::new(), false);
+        assert!(selected_proofs.is_err());
+    }
+
+    #[test]
+    fn test_select_proofs_exact() {
+        let proofs = vec![
+            proof(1),
+            proof(2),
+            proof(4),
+            proof(8),
+            proof(16),
+            proof(32),
+            proof(64),
+        ];
+        let mut selected_proofs =
+            Wallet::select_proofs(77.into(), proofs, &vec![id()], &HashMap::new(), false).unwrap();
+        selected_proofs.sort();
+        assert_eq!(selected_proofs.len(), 4);
+        assert_eq!(selected_proofs[0].amount, 1.into());
+        assert_eq!(selected_proofs[1].amount, 4.into());
+        assert_eq!(selected_proofs[2].amount, 8.into());
+        assert_eq!(selected_proofs[3].amount, 64.into());
+    }
+
+    #[test]
+    fn test_select_proofs_over() {
+        let proofs = vec![proof(1), proof(2), proof(4), proof(8), proof(32), proof(64)];
+        let selected_proofs =
+            Wallet::select_proofs(31.into(), proofs, &vec![id()], &HashMap::new(), false).unwrap();
+        assert_eq!(selected_proofs.len(), 1);
+        assert_eq!(selected_proofs[0].amount, 32.into());
+    }
+
+    #[test]
+    fn test_select_proofs_smaller_over() {
+        let proofs = vec![proof(8), proof(16), proof(32)];
+        let selected_proofs =
+            Wallet::select_proofs(23.into(), proofs, &vec![id()], &HashMap::new(), false).unwrap();
+        assert_eq!(selected_proofs.len(), 2);
+        assert_eq!(selected_proofs[0].amount, 16.into());
+        assert_eq!(selected_proofs[1].amount, 8.into());
+    }
+
+    #[test]
+    fn test_select_proofs_many_ones() {
+        let proofs = (0..1024).into_iter().map(|_| proof(1)).collect::<Vec<_>>();
+        let selected_proofs =
+            Wallet::select_proofs(1024.into(), proofs, &vec![id()], &HashMap::new(), false)
+                .unwrap();
+        assert_eq!(selected_proofs.len(), 1024);
+        for i in 0..1024 {
+            assert_eq!(selected_proofs[i].amount, 1.into());
         }
+    }
 
-        tracing::debug!(
-            "Could not select proofs to swap: total selected: {}",
-            selected_proofs.total_amount()?
-        );
+    #[test]
+    fn test_select_proofs_huge_proofs() {
+        let proofs = (0..32)
+            .flat_map(|i| {
+                (0..5)
+                    .into_iter()
+                    .map(|_| proof(1 << i))
+                    .collect::<Vec<_>>()
+            })
+            .collect::<Vec<_>>();
+        let mut selected_proofs = Wallet::select_proofs(
+            ((1u64 << 32) - 1).into(),
+            proofs,
+            &vec![id()],
+            &HashMap::new(),
+            false,
+        )
+        .unwrap();
+        selected_proofs.sort();
+        assert_eq!(selected_proofs.len(), 32);
+        for i in 0..32 {
+            assert_eq!(selected_proofs[i].amount, (1 << i).into());
+        }
+    }
 
-        Err(Error::InsufficientFunds)
+    #[test]
+    fn test_select_proofs_with_fees() {
+        let proofs = vec![proof(64), proof(4), proof(32)];
+        let mut keyset_fees = HashMap::new();
+        keyset_fees.insert(id(), 100);
+        let selected_proofs =
+            Wallet::select_proofs(10.into(), proofs, &vec![id()], &keyset_fees, false).unwrap();
+        assert_eq!(selected_proofs.len(), 1);
+        assert_eq!(selected_proofs[0].amount, 32.into());
     }
 }

+ 365 - 131
crates/cdk/src/wallet/send.rs

@@ -1,3 +1,5 @@
+use std::fmt::Debug;
+
 use tracing::instrument;
 
 use super::SendKind;
@@ -7,36 +9,26 @@ use crate::nuts::{Proofs, SpendingConditions, State, Token};
 use crate::{Amount, Error, Wallet};
 
 impl Wallet {
-    /// Send specific proofs
-    #[instrument(skip(self))]
-    pub async fn send_proofs(&self, memo: Option<String>, proofs: Proofs) -> Result<Token, Error> {
-        let ys = proofs.ys()?;
-        self.localstore.reserve_proofs(ys).await?;
-
-        Ok(Token::new(
-            self.mint_url.clone(),
-            proofs,
-            memo,
-            self.unit.clone(),
-        ))
-    }
-
-    /// Send
-    #[instrument(skip(self))]
-    pub async fn send(
+    /// Prepare A Send Transaction
+    ///
+    /// This function prepares a send transaction by selecting proofs to send and proofs to swap.
+    /// By doing so, it ensures that the wallet user is able to view the fees associated with the send transaction.
+    ///
+    /// ```no_compile
+    /// let send = wallet.prepare_send(Amount::from(10), SendOptions::default()).await?;
+    /// assert!(send.fee() <= Amount::from(1));
+    /// let token = wallet.send(send, None).await?;
+    /// ```
+    #[instrument(skip(self), err)]
+    pub async fn prepare_send(
         &self,
         amount: Amount,
-        memo: Option<String>,
-        conditions: Option<SpendingConditions>,
-        amount_split_target: &SplitTarget,
-        send_kind: &SendKind,
-        include_fees: bool,
-    ) -> Result<Token, Error> {
+        opts: SendOptions,
+    ) -> Result<PreparedSend, Error> {
+        tracing::info!("Preparing send");
+
         // If online send check mint for current keysets fees
-        if matches!(
-            send_kind,
-            SendKind::OnlineExact | SendKind::OnlineTolerance(_)
-        ) {
+        if opts.send_kind.is_online() {
             if let Err(e) = self.get_active_mint_keyset().await {
                 tracing::error!(
                     "Error fetching active mint keyset: {:?}. Using stored keysets",
@@ -45,135 +37,377 @@ impl Wallet {
             }
         }
 
-        let available_proofs = self
+        // Get keyset fees from localstore
+        let keyset_fees = self.get_keyset_fees().await?;
+
+        // Get available proofs matching conditions
+        let mut available_proofs = self
             .get_proofs_with(
                 Some(vec![State::Unspent]),
-                conditions.clone().map(|c| vec![c]),
+                opts.conditions.clone().map(|c| vec![c]),
             )
             .await?;
 
-        let proofs_sum = available_proofs.total_amount()?;
-
-        let available_proofs = if proofs_sum < amount {
-            match &conditions {
-                Some(conditions) => {
-                    tracing::debug!("Insufficient prrofs matching conditions attempting swap");
-                    let unspent_proofs = self.get_unspent_proofs().await?;
-                    let proofs_to_swap = self.select_proofs_to_swap(amount, unspent_proofs).await?;
-
-                    let proofs_with_conditions = self
-                        .swap(
-                            Some(amount),
-                            SplitTarget::default(),
-                            proofs_to_swap,
-                            Some(conditions.clone()),
-                            include_fees,
-                        )
-                        .await?;
-                    proofs_with_conditions.ok_or(Error::InsufficientFunds)
-                }
-                None => Err(Error::InsufficientFunds),
-            }?
+        // Check if sufficient proofs are available
+        let mut force_swap = false;
+        let available_sum = available_proofs.total_amount()?;
+        if available_sum < amount {
+            if opts.conditions.is_none() || opts.send_kind.is_offline() {
+                return Err(Error::InsufficientFunds);
+            } else {
+                // Swap is required for send
+                tracing::debug!("Insufficient proofs matching conditions");
+                force_swap = true;
+                available_proofs = self
+                    .localstore
+                    .get_proofs(
+                        Some(self.mint_url.clone()),
+                        Some(self.unit.clone()),
+                        Some(vec![State::Unspent]),
+                        Some(vec![]),
+                    )
+                    .await?
+                    .into_iter()
+                    .map(|p| p.proof)
+                    .collect();
+            }
+        }
+
+        // Select proofs
+        let active_keyset_ids = self
+            .get_active_mint_keysets()
+            .await?
+            .into_iter()
+            .map(|k| k.id)
+            .collect();
+        let selected_proofs = Wallet::select_proofs(
+            amount,
+            available_proofs,
+            &active_keyset_ids,
+            &keyset_fees,
+            opts.include_fee,
+        )?;
+        let selected_total = selected_proofs.total_amount()?;
+
+        // Check if selected proofs are exact
+        let send_fee = if opts.include_fee {
+            self.get_proofs_fee(&selected_proofs).await?
         } else {
-            available_proofs
+            Amount::ZERO
         };
+        if selected_total == amount + send_fee {
+            return self
+                .internal_prepare_send(amount, opts, selected_proofs, force_swap)
+                .await;
+        } else if opts.send_kind == SendKind::OfflineExact {
+            return Err(Error::InsufficientFunds);
+        }
 
-        let selected = self
-            .select_proofs_to_send(amount, available_proofs, include_fees)
-            .await;
+        // Check if selected proofs are sufficient for tolerance
+        let tolerance = match opts.send_kind {
+            SendKind::OfflineTolerance(tolerance) => Some(tolerance),
+            SendKind::OnlineTolerance(tolerance) => Some(tolerance),
+            _ => None,
+        };
+        if let Some(tolerance) = tolerance {
+            if selected_total - amount > tolerance && opts.send_kind.is_offline() {
+                return Err(Error::InsufficientFunds);
+            }
+        }
 
-        let send_proofs: Proofs = match (send_kind, selected, conditions.clone()) {
-            // Handle exact matches offline
-            (SendKind::OfflineExact, Ok(selected_proofs), _) => {
-                let selected_proofs_amount = selected_proofs.total_amount()?;
+        self.internal_prepare_send(amount, opts, selected_proofs, force_swap)
+            .await
+    }
 
-                let amount_to_send = match include_fees {
-                    true => amount + self.get_proofs_fee(&selected_proofs).await?,
-                    false => amount,
-                };
+    async fn internal_prepare_send(
+        &self,
+        amount: Amount,
+        opts: SendOptions,
+        proofs: Proofs,
+        force_swap: bool,
+    ) -> Result<PreparedSend, Error> {
+        // Split amount with fee if necessary
+        let (send_amounts, send_fee) = if opts.include_fee {
+            let active_keyset_id = self.get_active_mint_keyset().await?.id;
+            let keyset_fee_ppk = self.get_keyset_fees_by_id(active_keyset_id).await?;
+            tracing::debug!("Keyset fee per proof: {:?}", keyset_fee_ppk);
+            let send_split = amount.split_with_fee(keyset_fee_ppk)?;
+            let send_fee = self
+                .get_proofs_fee_by_count(
+                    vec![(active_keyset_id, send_split.len() as u64)]
+                        .into_iter()
+                        .collect(),
+                )
+                .await?;
+            (send_split, send_fee)
+        } else {
+            let send_split = amount.split();
+            let send_fee = Amount::ZERO;
+            (send_split, send_fee)
+        };
+        tracing::debug!("Send amounts: {:?}", send_amounts);
+        tracing::debug!("Send fee: {:?}", send_fee);
+
+        // Reserve proofs
+        self.localstore
+            .update_proofs_state(proofs.ys()?, State::Reserved)
+            .await?;
 
-                if selected_proofs_amount == amount_to_send {
-                    selected_proofs
+        // Check if proofs are exact send amount
+        let proofs_exact_amount = proofs.total_amount()? == amount + send_fee;
+
+        // Split proofs to swap and send
+        let mut proofs_to_swap = Proofs::new();
+        let mut proofs_to_send = Proofs::new();
+        if force_swap {
+            proofs_to_swap = proofs;
+        } else if proofs_exact_amount
+            || opts.send_kind.is_offline()
+            || opts.send_kind.has_tolerance()
+        {
+            proofs_to_send = proofs;
+        } else {
+            let mut remaining_send_amounts = send_amounts.clone();
+            for proof in proofs {
+                if let Some(idx) = remaining_send_amounts
+                    .iter()
+                    .position(|a| a == &proof.amount)
+                {
+                    proofs_to_send.push(proof);
+                    remaining_send_amounts.remove(idx);
                 } else {
-                    return Err(Error::InsufficientFunds);
+                    proofs_to_swap.push(proof);
                 }
             }
+        }
 
-            // Handle exact matches
-            (SendKind::OnlineExact, Ok(selected_proofs), _) => {
-                let selected_proofs_amount = selected_proofs.total_amount()?;
+        // Calculate swap fee
+        let swap_fee = self.get_proofs_fee(&proofs_to_swap).await?;
 
-                let amount_to_send = match include_fees {
-                    true => amount + self.get_proofs_fee(&selected_proofs).await?,
-                    false => amount,
-                };
+        // Return prepared send
+        Ok(PreparedSend {
+            amount,
+            options: opts,
+            proofs_to_swap,
+            swap_fee,
+            proofs_to_send,
+            send_fee,
+        })
+    }
 
-                if selected_proofs_amount == amount_to_send {
-                    selected_proofs
-                } else {
-                    tracing::info!("Could not select proofs exact while offline.");
-                    tracing::info!("Attempting to select proofs and swapping");
+    /// Finalize A Send Transaction
+    ///
+    /// This function finalizes a send transaction by constructing a token the [`PreparedSend`].
+    /// See [`Wallet::prepare_send`] for more information.
+    #[instrument(skip(self), err)]
+    pub async fn send(&self, send: PreparedSend, memo: Option<SendMemo>) -> Result<Token, Error> {
+        tracing::info!("Sending prepared send");
+        let mut proofs_to_send = send.proofs_to_send;
 
-                    self.swap_from_unspent(amount, conditions, include_fees)
-                        .await?
-                }
-            }
+        // Get active keyset ID
+        let active_keyset_id = self.get_active_mint_keyset().await?.id;
+        tracing::debug!("Active keyset ID: {:?}", active_keyset_id);
 
-            // Handle offline tolerance
-            (SendKind::OfflineTolerance(tolerance), Ok(selected_proofs), _) => {
-                let selected_proofs_amount = selected_proofs.total_amount()?;
+        // Get keyset fees
+        let keyset_fee_ppk = self.get_keyset_fees_by_id(active_keyset_id).await?;
+        tracing::debug!("Keyset fees: {:?}", keyset_fee_ppk);
 
-                let amount_to_send = match include_fees {
-                    true => amount + self.get_proofs_fee(&selected_proofs).await?,
-                    false => amount,
-                };
-                if selected_proofs_amount - amount_to_send <= *tolerance {
-                    selected_proofs
-                } else {
-                    tracing::info!("Selected proofs greater than tolerance. Must swap online");
-                    return Err(Error::InsufficientFunds);
-                }
+        // Calculate total send amount
+        let total_send_amount = send.amount + send.send_fee;
+        tracing::debug!("Total send amount: {}", total_send_amount);
+
+        // Swap proofs if necessary
+        if !send.proofs_to_swap.is_empty() {
+            let swap_amount = total_send_amount - proofs_to_send.total_amount()?;
+            tracing::debug!("Swapping proofs; swap_amount={:?}", swap_amount);
+            if let Some(proofs) = self
+                .swap(
+                    Some(swap_amount),
+                    SplitTarget::None,
+                    send.proofs_to_swap,
+                    send.options.conditions.clone(),
+                    false, // already included in swap_amount
+                )
+                .await?
+            {
+                proofs_to_send.extend(proofs);
             }
+        }
+        tracing::debug!(
+            "Proofs to send: {:?}",
+            proofs_to_send.iter().map(|p| p.amount).collect::<Vec<_>>()
+        );
 
-            // Handle online tolerance when selection fails and conditions are present
-            (SendKind::OnlineTolerance(_), Err(_), Some(_)) => {
-                tracing::info!("Could not select proofs with conditions while offline.");
-                tracing::info!("Attempting to select proofs without conditions and swapping");
+        // Check if sufficient proofs are available
+        if send.amount > proofs_to_send.total_amount()? {
+            return Err(Error::InsufficientFunds);
+        }
 
-                self.swap_from_unspent(amount, conditions, include_fees)
-                    .await?
-            }
+        // Check if proofs are reserved or unspent
+        let sendable_proof_ys = self
+            .get_proofs_with(
+                Some(vec![State::Reserved, State::Unspent]),
+                send.options.conditions.clone().map(|c| vec![c]),
+            )
+            .await?
+            .ys()?;
+        if proofs_to_send
+            .ys()?
+            .iter()
+            .any(|y| !sendable_proof_ys.contains(y))
+        {
+            tracing::warn!("Proofs to send are not reserved or unspent");
+            return Err(Error::UnexpectedProofState);
+        }
 
-            // Handle online tolerance with successful selection
-            (SendKind::OnlineTolerance(tolerance), Ok(selected_proofs), _) => {
-                let selected_proofs_amount = selected_proofs.total_amount()?;
-                let amount_to_send = match include_fees {
-                    true => amount + self.get_proofs_fee(&selected_proofs).await?,
-                    false => amount,
-                };
-                if selected_proofs_amount - amount_to_send <= *tolerance {
-                    selected_proofs
-                } else {
-                    tracing::info!("Could not select proofs while offline. Attempting swap");
-                    self.swap_from_unspent(amount, conditions, include_fees)
-                        .await?
-                }
-            }
+        // Update proofs state to pending spent
+        tracing::debug!(
+            "Updating proofs state to pending spent: {:?}",
+            proofs_to_send.ys()?
+        );
+        self.localstore
+            .update_proofs_state(proofs_to_send.ys()?, State::PendingSpent)
+            .await?;
 
-            // Handle all other cases where selection fails
-            (
-                SendKind::OfflineExact
-                | SendKind::OnlineExact
-                | SendKind::OfflineTolerance(_)
-                | SendKind::OnlineTolerance(_),
-                Err(_),
-                _,
-            ) => {
-                tracing::debug!("Could not select proofs");
-                return Err(Error::InsufficientFunds);
-            }
-        };
+        // Include token memo
+        let send_memo = send.options.memo.or(memo);
+        let memo = send_memo.and_then(|m| if m.include_memo { Some(m.memo) } else { None });
+
+        // Create and return token
+        Ok(Token::new(
+            self.mint_url.clone(),
+            proofs_to_send,
+            memo,
+            self.unit.clone(),
+        ))
+    }
+
+    /// Cancel prepared send
+    pub async fn cancel_send(&self, send: PreparedSend) -> Result<(), Error> {
+        tracing::info!("Cancelling prepared send");
+
+        // Double-check proofs state
+        let reserved_proofs = self.get_reserved_proofs().await?.ys()?;
+        if !send
+            .proofs()
+            .ys()?
+            .iter()
+            .all(|y| reserved_proofs.contains(y))
+        {
+            return Err(Error::UnexpectedProofState);
+        }
+
+        self.localstore
+            .update_proofs_state(send.proofs().ys()?, State::Unspent)
+            .await?;
+
+        Ok(())
+    }
+}
+
+/// Prepared send
+pub struct PreparedSend {
+    amount: Amount,
+    options: SendOptions,
+    proofs_to_swap: Proofs,
+    swap_fee: Amount,
+    proofs_to_send: Proofs,
+    send_fee: Amount,
+}
 
-        self.send_proofs(memo, send_proofs).await
+impl PreparedSend {
+    /// Amount
+    pub fn amount(&self) -> Amount {
+        self.amount
     }
+
+    /// Send options
+    pub fn options(&self) -> &SendOptions {
+        &self.options
+    }
+
+    /// Proofs to swap (i.e., proofs that need to be swapped before constructing the token)
+    pub fn proofs_to_swap(&self) -> &Proofs {
+        &self.proofs_to_swap
+    }
+
+    /// Swap fee
+    pub fn swap_fee(&self) -> Amount {
+        self.swap_fee
+    }
+
+    /// Proofs to send (i.e., proofs that will be included in the token)
+    pub fn proofs_to_send(&self) -> &Proofs {
+        &self.proofs_to_send
+    }
+
+    /// Send fee
+    pub fn send_fee(&self) -> Amount {
+        self.send_fee
+    }
+
+    /// All proofs
+    pub fn proofs(&self) -> Proofs {
+        let mut proofs = self.proofs_to_swap.clone();
+        proofs.extend(self.proofs_to_send.clone());
+        proofs
+    }
+
+    /// Total fee
+    pub fn fee(&self) -> Amount {
+        self.swap_fee + self.send_fee
+    }
+}
+
+impl Debug for PreparedSend {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("PreparedSend")
+            .field("amount", &self.amount)
+            .field("options", &self.options)
+            .field(
+                "proofs_to_swap",
+                &self
+                    .proofs_to_swap
+                    .iter()
+                    .map(|p| p.amount)
+                    .collect::<Vec<_>>(),
+            )
+            .field("swap_fee", &self.swap_fee)
+            .field(
+                "proofs_to_send",
+                &self
+                    .proofs_to_send
+                    .iter()
+                    .map(|p| p.amount)
+                    .collect::<Vec<_>>(),
+            )
+            .field("send_fee", &self.send_fee)
+            .finish()
+    }
+}
+
+/// Send options
+#[derive(Debug, Clone, Default)]
+pub struct SendOptions {
+    /// Memo
+    pub memo: Option<SendMemo>,
+    /// Spending conditions
+    pub conditions: Option<SpendingConditions>,
+    /// Amount split target
+    pub amount_split_target: SplitTarget,
+    /// Send kind
+    pub send_kind: SendKind,
+    /// Include fee
+    ///
+    /// When this is true the token created will include the amount of fees needed to redeem the token (amount + fee_to_redeem)
+    pub include_fee: bool,
+}
+
+/// Send memo
+#[derive(Debug, Clone)]
+pub struct SendMemo {
+    /// Memo
+    pub memo: String,
+    /// Include memo in token
+    pub include_memo: bool,
 }

+ 26 - 7
crates/cdk/src/wallet/swap.rs

@@ -20,13 +20,14 @@ impl Wallet {
         spending_conditions: Option<SpendingConditions>,
         include_fees: bool,
     ) -> Result<Option<Proofs>, Error> {
+        tracing::info!("Swapping");
         let mint_url = &self.mint_url;
         let unit = &self.unit;
 
         let pre_swap = self
             .create_swap(
                 amount,
-                amount_split_target,
+                amount_split_target.clone(),
                 input_proofs.clone(),
                 spending_conditions.clone(),
                 include_fees,
@@ -72,13 +73,15 @@ impl Wallet {
                         let mut all_proofs = proofs_without_condition;
                         all_proofs.reverse();
 
-                        let mut proofs_to_send: Proofs = Vec::new();
-                        let mut proofs_to_keep = Vec::new();
+                        let mut proofs_to_send = Proofs::new();
+                        let mut proofs_to_keep = Proofs::new();
+                        let mut amount_split = amount.split_targeted(&amount_split_target)?;
 
                         for proof in all_proofs {
-                            let proofs_to_send_amount = proofs_to_send.total_amount()?;
-                            if proof.amount + proofs_to_send_amount <= amount + pre_swap.fee {
+                            if let Some(idx) = amount_split.iter().position(|&a| a == proof.amount)
+                            {
                                 proofs_to_send.push(proof);
+                                amount_split.remove(idx);
                             } else {
                                 proofs_to_keep.push(proof);
                             }
@@ -163,7 +166,20 @@ impl Wallet {
 
         ensure_cdk!(proofs_sum >= amount, Error::InsufficientFunds);
 
-        let proofs = self.select_proofs_to_swap(amount, available_proofs).await?;
+        let active_keyset_ids = self
+            .get_active_mint_keysets()
+            .await?
+            .into_iter()
+            .map(|k| k.id)
+            .collect();
+        let keyset_fees = self.get_keyset_fees().await?;
+        let proofs = Wallet::select_proofs(
+            amount,
+            available_proofs,
+            &active_keyset_ids,
+            &keyset_fees,
+            true,
+        )?;
 
         self.swap(
             Some(amount),
@@ -186,13 +202,16 @@ impl Wallet {
         spending_conditions: Option<SpendingConditions>,
         include_fees: bool,
     ) -> Result<PreSwap, Error> {
+        tracing::info!("Creating swap");
         let active_keyset_id = self.get_active_mint_keyset().await?.id;
 
         // Desired amount is either amount passed or value of all proof
         let proofs_total = proofs.total_amount()?;
 
         let ys: Vec<PublicKey> = proofs.ys()?;
-        self.localstore.set_pending_proofs(ys).await?;
+        self.localstore
+            .update_proofs_state(ys, State::Reserved)
+            .await?;
 
         let fee = self.get_proofs_fee(&proofs).await?;