Kaynağa Gözat

Introduce FeeAndAmount struct

Cesar Rodas 1 ay önce
ebeveyn
işleme
5a9435ce18

+ 93 - 46
crates/cashu/src/amount.rs

@@ -3,6 +3,7 @@
 //! Is any unit and will be treated as the unit of the wallet
 
 use std::cmp::Ordering;
+use std::collections::HashMap;
 use std::fmt;
 use std::str::FromStr;
 
@@ -11,6 +12,7 @@ use serde::{Deserialize, Serialize};
 use thiserror::Error;
 
 use crate::nuts::CurrencyUnit;
+use crate::Id;
 
 /// Amount Error
 #[derive(Debug, Error)]
@@ -41,6 +43,40 @@ pub enum Error {
 #[serde(transparent)]
 pub struct Amount(u64);
 
+/// Fees and and amount type, it can be casted just as a reference to the inner amounts, or a single
+/// u64 which is the fee
+#[derive(Debug, Clone)]
+pub struct FeeAndAmounts {
+    fee: u64,
+    amounts: Vec<u64>,
+}
+
+impl From<(u64, Vec<u64>)> for FeeAndAmounts {
+    fn from(value: (u64, Vec<u64>)) -> Self {
+        Self {
+            fee: value.0,
+            amounts: value.1,
+        }
+    }
+}
+
+impl FeeAndAmounts {
+    /// Fees
+    #[inline(always)]
+    pub fn fee(&self) -> u64 {
+        self.fee
+    }
+
+    /// Amounts
+    #[inline(always)]
+    pub fn amounts(&self) -> &[u64] {
+        &self.amounts
+    }
+}
+
+/// Fees and Amounts for each Keyset
+pub type KeysetFeeAndAmounts = HashMap<Id, FeeAndAmounts>;
+
 impl FromStr for Amount {
     type Err = Error;
 
@@ -60,8 +96,9 @@ impl Amount {
     pub const ONE: Amount = Amount(1);
 
     /// Split into parts that are powers of two
-    pub fn split(&self, amounts_ppk: &[u64]) -> Vec<Self> {
-        amounts_ppk
+    pub fn split(&self, fee_and_amounts: &FeeAndAmounts) -> Vec<Self> {
+        fee_and_amounts
+            .amounts
             .iter()
             .rev()
             .fold((Vec::new(), self.0), |(mut acc, total), &amount| {
@@ -77,20 +114,20 @@ impl Amount {
     pub fn split_targeted(
         &self,
         target: &SplitTarget,
-        amounts_ppk: &[u64],
+        fee_and_amounts: &FeeAndAmounts,
     ) -> Result<Vec<Self>, Error> {
         let mut parts = match target {
-            SplitTarget::None => self.split(amounts_ppk),
+            SplitTarget::None => self.split(fee_and_amounts),
             SplitTarget::Value(amount) => {
                 if self.le(amount) {
-                    return Ok(self.split(amounts_ppk));
+                    return Ok(self.split(fee_and_amounts));
                 }
 
                 let mut parts_total = Amount::ZERO;
                 let mut parts = Vec::new();
 
                 // The powers of two that are need to create target value
-                let parts_of_value = amount.split(amounts_ppk);
+                let parts_of_value = amount.split(fee_and_amounts);
 
                 while parts_total.lt(self) {
                     for part in parts_of_value.iter().copied() {
@@ -98,7 +135,7 @@ impl Amount {
                             parts.push(part);
                         } else {
                             let amount_left = *self - parts_total;
-                            parts.extend(amount_left.split(amounts_ppk));
+                            parts.extend(amount_left.split(fee_and_amounts));
                         }
 
                         parts_total = Amount::try_sum(parts.clone().iter().copied())?;
@@ -121,7 +158,7 @@ impl Amount {
                     }
                     Ordering::Greater => {
                         let extra = *self - values_total;
-                        let mut extra_amount = extra.split(amounts_ppk);
+                        let mut extra_amount = extra.split(fee_and_amounts);
                         let mut values = values.clone();
 
                         values.append(&mut extra_amount);
@@ -136,17 +173,18 @@ impl Amount {
     }
 
     /// Splits amount into powers of two while accounting for the swap fee
-    pub fn split_with_fee(&self, fee_ppk: u64, amounts_ppk: &[u64]) -> Result<Vec<Self>, Error> {
-        let without_fee_amounts = self.split(amounts_ppk);
-        let total_fee_ppk = fee_ppk
+    pub fn split_with_fee(&self, fee_and_amounts: &FeeAndAmounts) -> Result<Vec<Self>, Error> {
+        let without_fee_amounts = self.split(fee_and_amounts);
+        let total_fee_ppk = fee_and_amounts
+            .fee
             .checked_mul(without_fee_amounts.len() as u64)
             .ok_or(Error::AmountOverflow)?;
         let fee = Amount::from(total_fee_ppk.div_ceil(1000));
         let new_amount = self.checked_add(fee).ok_or(Error::AmountOverflow)?;
 
-        let split = new_amount.split(amounts_ppk);
+        let split = new_amount.split(fee_and_amounts);
         let split_fee_ppk = (split.len() as u64)
-            .checked_mul(fee_ppk)
+            .checked_mul(fee_and_amounts.fee)
             .ok_or(Error::AmountOverflow)?;
         let split_fee = Amount::from(split_fee_ppk.div_ceil(1000));
 
@@ -157,7 +195,7 @@ impl Amount {
         }
         self.checked_add(Amount::ONE)
             .ok_or(Error::AmountOverflow)?
-            .split_with_fee(fee_ppk, amounts_ppk)
+            .split_with_fee(fee_and_amounts)
     }
 
     /// Checked addition for Amount. Returns None if overflow occurs.
@@ -387,37 +425,43 @@ mod tests {
 
     #[test]
     fn test_split_amount() {
-        let amounts_ppk = (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>();
+        let fee_and_amounts = (0, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()).into();
 
-        assert_eq!(Amount::from(1).split(&amounts_ppk), vec![Amount::from(1)]);
-        assert_eq!(Amount::from(2).split(&amounts_ppk), vec![Amount::from(2)]);
         assert_eq!(
-            Amount::from(3).split(&amounts_ppk),
+            Amount::from(1).split(&fee_and_amounts),
+            vec![Amount::from(1)]
+        );
+        assert_eq!(
+            Amount::from(2).split(&fee_and_amounts),
+            vec![Amount::from(2)]
+        );
+        assert_eq!(
+            Amount::from(3).split(&fee_and_amounts),
             vec![Amount::from(2), Amount::from(1)]
         );
         let amounts: Vec<Amount> = [8, 2, 1].iter().map(|a| Amount::from(*a)).collect();
-        assert_eq!(Amount::from(11).split(&amounts_ppk), amounts);
+        assert_eq!(Amount::from(11).split(&fee_and_amounts), amounts);
         let amounts: Vec<Amount> = [128, 64, 32, 16, 8, 4, 2, 1]
             .iter()
             .map(|a| Amount::from(*a))
             .collect();
-        assert_eq!(Amount::from(255).split(&amounts_ppk), amounts);
+        assert_eq!(Amount::from(255).split(&fee_and_amounts), amounts);
     }
 
     #[test]
     fn test_split_target_amount() {
-        let amounts_ppk = (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>();
+        let fee_and_amounts = (0, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()).into();
         let amount = Amount(65);
 
         let split = amount
-            .split_targeted(&SplitTarget::Value(Amount(32)), &amounts_ppk)
+            .split_targeted(&SplitTarget::Value(Amount(32)), &fee_and_amounts)
             .unwrap();
         assert_eq!(vec![Amount(1), Amount(32), Amount(32)], split);
 
         let amount = Amount(150);
 
         let split = amount
-            .split_targeted(&SplitTarget::Value(Amount::from(50)), &amounts_ppk)
+            .split_targeted(&SplitTarget::Value(Amount::from(50)), &fee_and_amounts)
             .unwrap();
         assert_eq!(
             vec![
@@ -437,7 +481,7 @@ mod tests {
         let amount = Amount::from(63);
 
         let split = amount
-            .split_targeted(&SplitTarget::Value(Amount::from(32)), &amounts_ppk)
+            .split_targeted(&SplitTarget::Value(Amount::from(32)), &fee_and_amounts)
             .unwrap();
         assert_eq!(
             vec![
@@ -454,23 +498,21 @@ mod tests {
 
     #[test]
     fn test_split_with_fee() {
-        let amounts_ppk = (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>();
+        let fee_and_amounts = (1, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()).into();
         let amount = Amount(2);
-        let fee_ppk = 1;
 
-        let split = amount.split_with_fee(fee_ppk, &amounts_ppk).unwrap();
+        let split = amount.split_with_fee(&fee_and_amounts).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, &amounts_ppk).unwrap();
+        let split = amount.split_with_fee(&fee_and_amounts).unwrap();
         assert_eq!(split, vec![Amount(4)]);
 
         let amount = Amount(3);
-        let fee_ppk = 1000;
+        let fee_and_amounts = (1000, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()).into();
 
-        let split = amount.split_with_fee(fee_ppk, &amounts_ppk).unwrap();
+        let split = amount.split_with_fee(&fee_and_amounts).unwrap();
         // With fee_ppk=1000 (100%), amount 3 requires proofs totaling at least 5
         // to cover both the amount (3) and fees (~2 for 2 proofs)
         assert_eq!(split, vec![Amount(4), Amount(1)]);
@@ -478,15 +520,14 @@ mod tests {
 
     #[test]
     fn test_split_with_fee_reported_issue() {
-        let amounts_ppk = (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>();
+        let fee_and_amounts = (100, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()).into();
         // Test the reported issue: mint 600, send 300 with fee_ppk=100
         let amount = Amount(300);
-        let fee_ppk = 100;
 
-        let split = amount.split_with_fee(fee_ppk, &amounts_ppk).unwrap();
+        let split = amount.split_with_fee(&fee_and_amounts).unwrap();
 
         // Calculate the total fee for the split
-        let total_fee_ppk = (split.len() as u64) * fee_ppk;
+        let total_fee_ppk = (split.len() as u64) * fee_and_amounts.fee;
         let total_fee = Amount::from(total_fee_ppk.div_ceil(1000));
 
         // The split should cover the amount plus fees
@@ -502,7 +543,6 @@ mod tests {
 
     #[test]
     fn test_split_with_fee_edge_cases() {
-        let amounts_ppk = (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>();
         // Test various amounts with fee_ppk=100
         let test_cases = vec![
             (Amount(1), 100),
@@ -519,7 +559,9 @@ mod tests {
         ];
 
         for (amount, fee_ppk) in test_cases {
-            let result = amount.split_with_fee(fee_ppk, &amounts_ppk);
+            let fee_and_amounts =
+                (fee_ppk, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()).into();
+            let result = amount.split_with_fee(&fee_and_amounts);
             assert!(
                 result.is_ok(),
                 "split_with_fee failed for amount {} with fee_ppk {}: {:?}",
@@ -556,7 +598,6 @@ mod tests {
 
     #[test]
     fn test_split_with_fee_high_fees() {
-        let amounts_ppk = (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>();
         // Test with very high fees
         let test_cases = vec![
             (Amount(10), 500),  // 50% fee
@@ -568,7 +609,9 @@ mod tests {
         ];
 
         for (amount, fee_ppk) in test_cases {
-            let result = amount.split_with_fee(fee_ppk, &amounts_ppk);
+            let fee_and_amounts =
+                (fee_ppk, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()).into();
+            let result = amount.split_with_fee(&fee_and_amounts);
             assert!(
                 result.is_ok(),
                 "split_with_fee failed for amount {} with fee_ppk {}: {:?}",
@@ -593,13 +636,13 @@ mod tests {
 
     #[test]
     fn test_split_with_fee_recursion_limit() {
-        let amounts_ppk = (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>();
         // Test that the recursion doesn't go infinite
         // This tests the edge case where the method keeps adding Amount::ONE
         let amount = Amount(1);
-        let fee_ppk = 10000; // Very high fee that might cause recursion
+        let fee_ppk = 10000;
+        let fee_and_amounts = (fee_ppk, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()).into();
 
-        let result = amount.split_with_fee(fee_ppk, &amounts_ppk);
+        let result = amount.split_with_fee(&fee_and_amounts);
         assert!(
             result.is_ok(),
             "split_with_fee should handle extreme fees without infinite recursion"
@@ -608,14 +651,16 @@ mod tests {
 
     #[test]
     fn test_split_values() {
-        let amounts_ppk = (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>();
+        let fee_and_amounts = (0, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()).into();
         let amount = Amount(10);
 
         let target = vec![Amount(2), Amount(4), Amount(4)];
 
         let split_target = SplitTarget::Values(target.clone());
 
-        let values = amount.split_targeted(&split_target, &amounts_ppk).unwrap();
+        let values = amount
+            .split_targeted(&split_target, &fee_and_amounts)
+            .unwrap();
 
         assert_eq!(target, values);
 
@@ -623,13 +668,15 @@ mod tests {
 
         let split_target = SplitTarget::Values(vec![Amount(2), Amount(4)]);
 
-        let values = amount.split_targeted(&split_target, &amounts_ppk).unwrap();
+        let values = amount
+            .split_targeted(&split_target, &fee_and_amounts)
+            .unwrap();
 
         assert_eq!(target, values);
 
         let split_target = SplitTarget::Values(vec![Amount(2), Amount(10)]);
 
-        let values = amount.split_targeted(&split_target, &amounts_ppk);
+        let values = amount.split_targeted(&split_target, &fee_and_amounts);
 
         assert!(values.is_err())
     }

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

@@ -17,6 +17,7 @@ use super::nut02::ShortKeysetId;
 use super::nut10;
 #[cfg(feature = "wallet")]
 use super::nut11::SpendingConditions;
+use crate::amount::FeeAndAmounts;
 #[cfg(feature = "wallet")]
 use crate::amount::SplitTarget;
 #[cfg(feature = "wallet")]
@@ -746,9 +747,9 @@ impl PreMintSecrets {
         keyset_id: Id,
         amount: Amount,
         amount_split_target: &SplitTarget,
-        amounts_ppk: &[u64],
+        fee_and_amounts: &FeeAndAmounts,
     ) -> Result<Self, Error> {
-        let amount_split = amount.split_targeted(amount_split_target, amounts_ppk)?;
+        let amount_split = amount.split_targeted(amount_split_target, fee_and_amounts)?;
 
         let mut output = Vec::with_capacity(amount_split.len());
 
@@ -831,9 +832,9 @@ impl PreMintSecrets {
         amount: Amount,
         amount_split_target: &SplitTarget,
         conditions: &SpendingConditions,
-        amounts_ppk: &[u64],
+        fee_and_amounts: &FeeAndAmounts,
     ) -> Result<Self, Error> {
-        let amount_split = amount.split_targeted(amount_split_target, amounts_ppk)?;
+        let amount_split = amount.split_targeted(amount_split_target, fee_and_amounts)?;
 
         let mut output = Vec::with_capacity(amount_split.len());
 

+ 5 - 5
crates/cashu/src/nuts/nut13.rs

@@ -11,7 +11,7 @@ use tracing::instrument;
 use super::nut00::{BlindedMessage, PreMint, PreMintSecrets};
 use super::nut01::SecretKey;
 use super::nut02::Id;
-use crate::amount::SplitTarget;
+use crate::amount::{FeeAndAmounts, SplitTarget};
 use crate::dhke::blind_message;
 use crate::secret::Secret;
 use crate::util::hex;
@@ -127,13 +127,13 @@ impl PreMintSecrets {
         seed: &[u8; 64],
         amount: Amount,
         amount_split_target: &SplitTarget,
-        amounts_ppk: &[u64],
+        fee_and_amounts: &FeeAndAmounts,
     ) -> Result<Self, Error> {
         let mut pre_mint_secrets = PreMintSecrets::new(keyset_id);
 
         let mut counter = counter;
 
-        for amount in amount.split_targeted(amount_split_target, amounts_ppk)? {
+        for amount in amount.split_targeted(amount_split_target, fee_and_amounts)? {
             let secret = Secret::from_seed(seed, keyset_id, counter)?;
             let blinding_factor = SecretKey::from_seed(seed, keyset_id, counter)?;
 
@@ -487,11 +487,11 @@ mod tests {
                 .unwrap();
         let amount = Amount::from(1000u64);
         let split_target = SplitTarget::default();
-        let amounts_ppk = (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>();
+        let fee_and_amounts = (0, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()).into();
 
         // Test PreMintSecrets generation with v2 keyset
         let pre_mint_secrets =
-            PreMintSecrets::from_seed(keyset_id, 0, &seed, amount, &split_target, &amounts_ppk)
+            PreMintSecrets::from_seed(keyset_id, 0, &seed, amount, &split_target, &fee_and_amounts)
                 .unwrap();
 
         // Verify all secrets in the pre_mint use the new v2 derivation

+ 3 - 5
crates/cdk-common/src/database/mint/mod.rs

@@ -34,8 +34,7 @@ pub const KVSTORE_NAMESPACE_KEY_MAX_LEN: usize = 120;
 pub fn validate_kvstore_string(s: &str) -> Result<(), Error> {
     if s.len() > KVSTORE_NAMESPACE_KEY_MAX_LEN {
         return Err(Error::KVStoreInvalidKey(format!(
-            "{} exceeds maximum length of key characters",
-            KVSTORE_NAMESPACE_KEY_MAX_LEN
+            "{KVSTORE_NAMESPACE_KEY_MAX_LEN} exceeds maximum length of key characters"
         )));
     }
 
@@ -72,11 +71,10 @@ pub fn validate_kvstore_params(
     }
 
     // Check for potential collisions between keys and namespaces in the same namespace
-    let namespace_key = format!("{}/{}", primary_namespace, secondary_namespace);
+    let namespace_key = format!("{primary_namespace}/{secondary_namespace}");
     if key == primary_namespace || key == secondary_namespace || key == namespace_key {
         return Err(Error::KVStoreInvalidKey(format!(
-            "Key '{}' conflicts with namespace names",
-            key
+            "Key '{key}' conflicts with namespace names"
         )));
     }
 

+ 7 - 4
crates/cdk-ffi/src/wallet.rs

@@ -372,8 +372,11 @@ impl Wallet {
     pub async fn get_keyset_fees_by_id(&self, keyset_id: String) -> Result<u64, FfiError> {
         let id = cdk::nuts::Id::from_str(&keyset_id)
             .map_err(|e| FfiError::Generic { msg: e.to_string() })?;
-        let (fees, _) = self.inner.get_keyset_fees_and_amounts_by_id(id).await?;
-        Ok(fees)
+        Ok(self
+            .inner
+            .get_keyset_fees_and_amounts_by_id(id)
+            .await?
+            .fee())
     }
 
     /// Reclaim unspent proofs (mark them as unspent in the database)
@@ -397,8 +400,8 @@ impl Wallet {
     ) -> Result<Amount, FfiError> {
         let id = cdk::nuts::Id::from_str(&keyset_id)
             .map_err(|e| FfiError::Generic { msg: e.to_string() })?;
-        let (fee_ppk, _) = self.inner.get_keyset_fees_and_amounts_by_id(id).await?;
-        let total_fee = (proof_count as u64 * fee_ppk) / 1000; // fee is per thousand
+        let fee_and_amounts = self.inner.get_keyset_fees_and_amounts_by_id(id).await?;
+        let total_fee = (proof_count as u64 * fee_and_amounts.fee()) / 1000; // fee is per thousand
         Ok(Amount::new(total_fee))
     }
 }

+ 3 - 1
crates/cdk-integration-tests/tests/bolt12.rs

@@ -352,11 +352,13 @@ async fn test_regtest_bolt12_mint_extra() -> Result<()> {
     assert_eq!(state.amount_paid, (pay_amount_msats / 1_000).into());
     assert_eq!(state.amount_issued, Amount::ZERO);
 
+    let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
+
     let pre_mint = PreMintSecrets::random(
         active_keyset_id,
         500.into(),
         &SplitTarget::None,
-        &((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+        &fee_and_amounts,
     )?;
 
     let quote_info = wallet

+ 32 - 19
crates/cdk-integration-tests/tests/fake_wallet.rs

@@ -392,12 +392,13 @@ async fn test_fake_melt_change_in_quote() {
     let melt_quote = wallet.melt_quote(invoice.to_string(), None).await.unwrap();
 
     let keyset = wallet.fetch_active_keyset().await.unwrap();
+    let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
 
     let premint_secrets = PreMintSecrets::random(
         keyset.id,
         100.into(),
         &SplitTarget::default(),
-        &((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+        &fee_and_amounts,
     )
     .unwrap();
 
@@ -474,12 +475,13 @@ async fn test_fake_mint_without_witness() {
     let http_client = HttpClient::new(MINT_URL.parse().unwrap(), None);
 
     let active_keyset_id = wallet.fetch_active_keyset().await.unwrap().id;
+    let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
 
     let premint_secrets = PreMintSecrets::random(
         active_keyset_id,
         100.into(),
         &SplitTarget::default(),
-        &((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+        &fee_and_amounts,
     )
     .unwrap();
 
@@ -523,12 +525,13 @@ async fn test_fake_mint_with_wrong_witness() {
     let http_client = HttpClient::new(MINT_URL.parse().unwrap(), None);
 
     let active_keyset_id = wallet.fetch_active_keyset().await.unwrap().id;
+    let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
 
     let premint_secrets = PreMintSecrets::random(
         active_keyset_id,
         100.into(),
         &SplitTarget::default(),
-        &((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+        &fee_and_amounts,
     )
     .unwrap();
 
@@ -576,12 +579,13 @@ async fn test_fake_mint_inflated() {
         .expect("no error");
 
     let active_keyset_id = wallet.fetch_active_keyset().await.unwrap().id;
+    let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
 
     let pre_mint = PreMintSecrets::random(
         active_keyset_id,
         500.into(),
         &SplitTarget::None,
-        &((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+        &fee_and_amounts,
     )
     .unwrap();
 
@@ -643,12 +647,13 @@ async fn test_fake_mint_multiple_units() {
         .expect("no error");
 
     let active_keyset_id = wallet.fetch_active_keyset().await.unwrap().id;
+    let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
 
     let pre_mint = PreMintSecrets::random(
         active_keyset_id,
         50.into(),
         &SplitTarget::None,
-        &((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+        &fee_and_amounts,
     )
     .unwrap();
 
@@ -667,7 +672,7 @@ async fn test_fake_mint_multiple_units() {
         active_keyset_id,
         50.into(),
         &SplitTarget::None,
-        &((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+        &fee_and_amounts,
     )
     .unwrap();
 
@@ -758,6 +763,7 @@ async fn test_fake_mint_multiple_unit_swap() {
         .expect("no error");
 
     let active_keyset_id = wallet.fetch_active_keyset().await.unwrap().id;
+    let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
 
     {
         let inputs: Proofs = vec![
@@ -769,7 +775,7 @@ async fn test_fake_mint_multiple_unit_swap() {
             active_keyset_id,
             inputs.total_amount().unwrap(),
             &SplitTarget::None,
-            &((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+            &fee_and_amounts,
         )
         .unwrap();
 
@@ -796,20 +802,21 @@ async fn test_fake_mint_multiple_unit_swap() {
         let inputs: Proofs = proofs.into_iter().take(2).collect();
 
         let total_inputs = inputs.total_amount().unwrap();
+        let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
 
         let half = total_inputs / 2.into();
         let usd_pre_mint = PreMintSecrets::random(
             usd_active_keyset_id,
             half,
             &SplitTarget::None,
-            &((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+            &fee_and_amounts,
         )
         .unwrap();
         let pre_mint = PreMintSecrets::random(
             active_keyset_id,
             total_inputs - half,
             &SplitTarget::None,
-            &((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+            &fee_and_amounts,
         )
         .unwrap();
 
@@ -911,6 +918,7 @@ async fn test_fake_mint_multiple_unit_melt() {
     }
 
     {
+        let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
         let inputs: Proofs = vec![proofs.first().expect("There is a proof").clone()];
 
         let input_amount: u64 = inputs.total_amount().unwrap().into();
@@ -923,14 +931,14 @@ async fn test_fake_mint_multiple_unit_melt() {
             usd_active_keyset_id,
             inputs.total_amount().unwrap() + 100.into(),
             &SplitTarget::None,
-            &((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+            &fee_and_amounts,
         )
         .unwrap();
         let pre_mint = PreMintSecrets::random(
             active_keyset_id,
             100.into(),
             &SplitTarget::None,
-            &((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+            &fee_and_amounts,
         )
         .unwrap();
 
@@ -991,6 +999,7 @@ async fn test_fake_mint_input_output_mismatch() {
     )
     .expect("failed to create new  usd wallet");
     let usd_active_keyset_id = wallet_usd.fetch_active_keyset().await.unwrap().id;
+    let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
 
     let inputs = proofs;
 
@@ -998,7 +1007,7 @@ async fn test_fake_mint_input_output_mismatch() {
         usd_active_keyset_id,
         inputs.total_amount().unwrap(),
         &SplitTarget::None,
-        &((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+        &fee_and_amounts,
     )
     .unwrap();
 
@@ -1033,6 +1042,7 @@ async fn test_fake_mint_swap_inflated() {
     let mint_quote = wallet.mint_quote(100.into(), None).await.unwrap();
 
     let mut proof_streams = wallet.proof_stream(mint_quote.clone(), SplitTarget::default(), None);
+    let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
 
     let proofs = proof_streams
         .next()
@@ -1045,7 +1055,7 @@ async fn test_fake_mint_swap_inflated() {
         active_keyset_id,
         101.into(),
         &SplitTarget::None,
-        &((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+        &fee_and_amounts,
     )
     .unwrap();
 
@@ -1090,12 +1100,13 @@ async fn test_fake_mint_swap_spend_after_fail() {
         .expect("no error");
 
     let active_keyset_id = wallet.fetch_active_keyset().await.unwrap().id;
+    let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
 
     let pre_mint = PreMintSecrets::random(
         active_keyset_id,
         100.into(),
         &SplitTarget::None,
-        &((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+        &fee_and_amounts,
     )
     .unwrap();
 
@@ -1110,7 +1121,7 @@ async fn test_fake_mint_swap_spend_after_fail() {
         active_keyset_id,
         101.into(),
         &SplitTarget::None,
-        &((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+        &fee_and_amounts,
     )
     .unwrap();
 
@@ -1131,7 +1142,7 @@ async fn test_fake_mint_swap_spend_after_fail() {
         active_keyset_id,
         100.into(),
         &SplitTarget::None,
-        &((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+        &fee_and_amounts,
     )
     .unwrap();
 
@@ -1176,12 +1187,13 @@ async fn test_fake_mint_melt_spend_after_fail() {
         .expect("no error");
 
     let active_keyset_id = wallet.fetch_active_keyset().await.unwrap().id;
+    let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
 
     let pre_mint = PreMintSecrets::random(
         active_keyset_id,
         100.into(),
         &SplitTarget::None,
-        &((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+        &fee_and_amounts,
     )
     .unwrap();
 
@@ -1196,7 +1208,7 @@ async fn test_fake_mint_melt_spend_after_fail() {
         active_keyset_id,
         101.into(),
         &SplitTarget::None,
-        &((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+        &fee_and_amounts,
     )
     .unwrap();
 
@@ -1258,6 +1270,7 @@ async fn test_fake_mint_duplicate_proofs_swap() {
         .expect("no error");
 
     let active_keyset_id = wallet.fetch_active_keyset().await.unwrap().id;
+    let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
 
     let inputs = vec![proofs[0].clone(), proofs[0].clone()];
 
@@ -1265,7 +1278,7 @@ async fn test_fake_mint_duplicate_proofs_swap() {
         active_keyset_id,
         inputs.total_amount().unwrap(),
         &SplitTarget::None,
-        &((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+        &fee_and_amounts,
     )
     .unwrap();
 

+ 8 - 4
crates/cdk-integration-tests/tests/happy_path_mint_wallet.rs

@@ -344,7 +344,6 @@ async fn test_restore() {
 /// and that the wallet can properly verify the change amounts match expectations.
 #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
 async fn test_fake_melt_change_in_quote() {
-    let amounts_ppk = (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>();
     let wallet = Wallet::new(
         &get_mint_url_from_env(),
         CurrencyUnit::Sat,
@@ -377,10 +376,15 @@ async fn test_fake_melt_change_in_quote() {
     let melt_quote = wallet.melt_quote(invoice.to_string(), None).await.unwrap();
 
     let keyset = wallet.fetch_active_keyset().await.unwrap();
+    let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
 
-    let premint_secrets =
-        PreMintSecrets::random(keyset.id, 100.into(), &SplitTarget::default(), &amounts_ppk)
-            .unwrap();
+    let premint_secrets = PreMintSecrets::random(
+        keyset.id,
+        100.into(),
+        &SplitTarget::default(),
+        &fee_and_amounts,
+    )
+    .unwrap();
 
     let client = HttpClient::new(get_mint_url_from_env().parse().unwrap(), None);
 

+ 26 - 18
crates/cdk-integration-tests/tests/integration_tests_pure.rs

@@ -243,12 +243,13 @@ async fn test_mint_double_spend() {
 
     let keys = mint_bob.pubkeys().keysets.first().unwrap().clone();
     let keyset_id = keys.id;
+    let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
 
     let preswap = PreMintSecrets::random(
         keyset_id,
         proofs.total_amount().unwrap(),
         &SplitTarget::default(),
-        &((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+        &fee_and_amounts,
     )
     .unwrap();
 
@@ -261,7 +262,7 @@ async fn test_mint_double_spend() {
         keyset_id,
         proofs.total_amount().unwrap(),
         &SplitTarget::default(),
-        &((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+        &fee_and_amounts,
     )
     .unwrap();
 
@@ -302,19 +303,20 @@ async fn test_attempt_to_swap_by_overflowing() {
 
     let keys = mint_bob.pubkeys().keysets.first().unwrap().clone();
     let keyset_id = keys.id;
+    let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
 
     let pre_mint_amount = PreMintSecrets::random(
         keyset_id,
         amount.into(),
         &SplitTarget::default(),
-        &((0..64).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+        &fee_and_amounts,
     )
     .unwrap();
     let pre_mint_amount_two = PreMintSecrets::random(
         keyset_id,
         amount.into(),
         &SplitTarget::default(),
-        &((0..64).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+        &fee_and_amounts,
     )
     .unwrap();
 
@@ -322,7 +324,7 @@ async fn test_attempt_to_swap_by_overflowing() {
         keyset_id,
         1.into(),
         &SplitTarget::default(),
-        &((0..64).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+        &fee_and_amounts,
     )
     .unwrap();
 
@@ -370,12 +372,14 @@ async fn test_swap_unbalanced() {
 
     let keyset_id = get_keyset_id(&mint_bob).await;
 
+    let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
+
     // Try to swap for less than the input amount (95 < 100)
     let preswap = PreMintSecrets::random(
         keyset_id,
         95.into(),
         &SplitTarget::default(),
-        &((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+        &fee_and_amounts,
     )
     .expect("Failed to create preswap");
 
@@ -394,7 +398,7 @@ async fn test_swap_unbalanced() {
         keyset_id,
         101.into(),
         &SplitTarget::default(),
-        &((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+        &fee_and_amounts,
     )
     .expect("Failed to create preswap");
 
@@ -434,13 +438,14 @@ pub async fn test_p2pk_swap() {
     let secret = SecretKey::generate();
 
     let spending_conditions = SpendingConditions::new_p2pk(secret.public_key(), None);
+    let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
 
     let pre_swap = PreMintSecrets::with_conditions(
         keyset_id,
         100.into(),
         &SplitTarget::default(),
         &spending_conditions,
-        &((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+        &fee_and_amounts,
     )
     .unwrap();
 
@@ -462,7 +467,7 @@ pub async fn test_p2pk_swap() {
         keyset_id,
         100.into(),
         &SplitTarget::default(),
-        &((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+        &fee_and_amounts,
     )
     .unwrap();
 
@@ -570,12 +575,13 @@ async fn test_swap_overpay_underpay_fee() {
 
     let keys = mint_bob.pubkeys().keysets.first().unwrap().clone().keys;
     let keyset_id = Id::v1_from_keys(&keys);
+    let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
 
     let preswap = PreMintSecrets::random(
         keyset_id,
         9998.into(),
         &SplitTarget::default(),
-        &((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+        &fee_and_amounts,
     )
     .unwrap();
 
@@ -597,7 +603,7 @@ async fn test_swap_overpay_underpay_fee() {
         keyset_id,
         1000.into(),
         &SplitTarget::default(),
-        &((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+        &fee_and_amounts,
     )
     .unwrap();
 
@@ -648,6 +654,7 @@ async fn test_mint_enforce_fee() {
 
     let keys = mint_bob.pubkeys().keysets.first().unwrap().clone();
     let keyset_id = keys.id;
+    let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
 
     let five_proofs: Vec<_> = proofs.drain(..5).collect();
 
@@ -655,7 +662,7 @@ async fn test_mint_enforce_fee() {
         keyset_id,
         5.into(),
         &SplitTarget::default(),
-        &((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+        &fee_and_amounts,
     )
     .unwrap();
 
@@ -677,7 +684,7 @@ async fn test_mint_enforce_fee() {
         keyset_id,
         4.into(),
         &SplitTarget::default(),
-        &((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+        &fee_and_amounts,
     )
     .unwrap();
 
@@ -693,7 +700,7 @@ async fn test_mint_enforce_fee() {
         keyset_id,
         1000.into(),
         &SplitTarget::default(),
-        &((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+        &fee_and_amounts,
     )
     .unwrap();
 
@@ -715,7 +722,7 @@ async fn test_mint_enforce_fee() {
         keyset_id,
         999.into(),
         &SplitTarget::default(),
-        &((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+        &fee_and_amounts,
     )
     .unwrap();
 
@@ -791,13 +798,14 @@ async fn test_concurrent_double_spend_swap() {
         .expect("Could not get proofs");
 
     let keyset_id = get_keyset_id(&mint_bob).await;
+    let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
 
     // Create 3 identical swap requests with the same proofs
     let preswap1 = PreMintSecrets::random(
         keyset_id,
         100.into(),
         &SplitTarget::default(),
-        &((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+        &fee_and_amounts,
     )
     .expect("Failed to create preswap");
     let swap_request1 = SwapRequest::new(proofs.clone(), preswap1.blinded_messages());
@@ -806,7 +814,7 @@ async fn test_concurrent_double_spend_swap() {
         keyset_id,
         100.into(),
         &SplitTarget::default(),
-        &((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+        &fee_and_amounts,
     )
     .expect("Failed to create preswap");
     let swap_request2 = SwapRequest::new(proofs.clone(), preswap2.blinded_messages());
@@ -815,7 +823,7 @@ async fn test_concurrent_double_spend_swap() {
         keyset_id,
         100.into(),
         &SplitTarget::default(),
-        &((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+        &fee_and_amounts,
     )
     .expect("Failed to create preswap");
     let swap_request3 = SwapRequest::new(proofs.clone(), preswap3.blinded_messages());

+ 2 - 1
crates/cdk-integration-tests/tests/regtest.rs

@@ -315,12 +315,13 @@ async fn test_cached_mint() {
         .expect("payment");
 
     let active_keyset_id = wallet.fetch_active_keyset().await.unwrap().id;
+    let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
     let http_client = HttpClient::new(get_mint_url_from_env().parse().unwrap(), None);
     let premint_secrets = PreMintSecrets::random(
         active_keyset_id,
         100.into(),
         &SplitTarget::default().to_owned(),
-        &((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+        &fee_and_amounts,
     )
     .unwrap();
 

+ 6 - 4
crates/cdk/src/mint/melt.rs

@@ -930,22 +930,24 @@ impl Mint {
 
                 let change_target = inputs_amount - total_spent - inputs_fee;
 
-                let amounts_ppk = self
+                let fee_and_amounts = self
                     .keysets
                     .load()
                     .iter()
                     .filter_map(|keyset| {
                         if keyset.active && Some(keyset.id) == outputs.first().map(|x| x.keyset_id)
                         {
-                            Some(keyset.amounts.clone())
+                            Some((keyset.input_fee_ppk, keyset.amounts.clone()).into())
                         } else {
                             None
                         }
                     })
                     .next()
-                    .unwrap_or_else(|| (0..32).map(|x| 2u64.pow(x)).collect());
+                    .unwrap_or_else(|| {
+                        (0, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()).into()
+                    });
 
-                let mut amounts = change_target.split(&amounts_ppk);
+                let mut amounts = change_target.split(&fee_and_amounts);
 
                 if outputs.len().lt(&amounts.len()) {
                     tracing::debug!(

+ 20 - 7
crates/cdk/src/wallet/auth/auth_wallet.rs

@@ -393,19 +393,32 @@ impl AuthWallet {
             }
         }
 
-        let active_keyset_id = self.fetch_active_keyset().await?.id;
-        let amounts_ppk = self
-            .load_keyset_keys(active_keyset_id)
+        let keysets = self
+            .load_mint_keysets()
             .await?
-            .iter()
-            .map(|(amount, _)| amount.to_u64())
-            .collect::<Vec<_>>();
+            .into_iter()
+            .map(|x| (x.id, x))
+            .collect::<HashMap<_, _>>();
+
+        let active_keyset_id = self.fetch_active_keyset().await?.id;
+        let fee_and_amounts = (
+            keysets
+                .get(&active_keyset_id)
+                .map(|x| x.input_fee_ppk)
+                .unwrap_or_default(),
+            self.load_keyset_keys(active_keyset_id)
+                .await?
+                .iter()
+                .map(|(amount, _)| amount.to_u64())
+                .collect::<Vec<_>>(),
+        )
+            .into();
 
         let premint_secrets = PreMintSecrets::random(
             active_keyset_id,
             amount,
             &SplitTarget::Value(1.into()),
-            &amounts_ppk,
+            &fee_and_amounts,
         )?;
 
         let request = MintAuthRequest {

+ 4 - 4
crates/cdk/src/wallet/issue/issue_bolt11.rs

@@ -222,7 +222,7 @@ impl Wallet {
         }
 
         let active_keyset_id = self.fetch_active_keyset().await?.id;
-        let (_, amounts_ppk) = self
+        let fee_and_amounts = self
             .get_keyset_fees_and_amounts_by_id(active_keyset_id)
             .await?;
 
@@ -232,12 +232,12 @@ impl Wallet {
                 amount_mintable,
                 &amount_split_target,
                 spending_conditions,
-                &amounts_ppk,
+                &fee_and_amounts,
             )?,
             None => {
                 // Calculate how many secrets we'll need
                 let amount_split =
-                    amount_mintable.split_targeted(&amount_split_target, &amounts_ppk)?;
+                    amount_mintable.split_targeted(&amount_split_target, &fee_and_amounts)?;
                 let num_secrets = amount_split.len() as u32;
 
                 tracing::debug!(
@@ -260,7 +260,7 @@ impl Wallet {
                     &self.seed,
                     amount_mintable,
                     &amount_split_target,
-                    &amounts_ppk,
+                    &fee_and_amounts,
                 )?
             }
         };

+ 4 - 4
crates/cdk/src/wallet/issue/issue_bolt12.rs

@@ -100,7 +100,7 @@ impl Wallet {
         };
 
         let active_keyset_id = self.fetch_active_keyset().await?.id;
-        let (_, amounts_ppk) = self
+        let fee_and_amounts = self
             .get_keyset_fees_and_amounts_by_id(active_keyset_id)
             .await?;
 
@@ -126,11 +126,11 @@ impl Wallet {
                 amount,
                 &amount_split_target,
                 spending_conditions,
-                &amounts_ppk,
+                &fee_and_amounts,
             )?,
             None => {
                 // Calculate how many secrets we'll need without generating them
-                let amount_split = amount.split_targeted(&amount_split_target, &amounts_ppk)?;
+                let amount_split = amount.split_targeted(&amount_split_target, &fee_and_amounts)?;
                 let num_secrets = amount_split.len() as u32;
 
                 tracing::debug!(
@@ -153,7 +153,7 @@ impl Wallet {
                     &self.seed,
                     amount,
                     &amount_split_target,
-                    &amounts_ppk,
+                    &fee_and_amounts,
                 )?
             }
         };

+ 6 - 4
crates/cdk/src/wallet/keysets.rs

@@ -1,5 +1,6 @@
 use std::collections::HashMap;
 
+use cdk_common::amount::{FeeAndAmounts, KeysetFeeAndAmounts};
 use cdk_common::nut02::{KeySetInfos, KeySetInfosMethods};
 use tracing::instrument;
 
@@ -144,7 +145,7 @@ impl Wallet {
     /// Returns a HashMap of keyset IDs to their input fee rates (per-proof-per-thousand)
     /// from cached keysets in the local database. This is an offline operation that does
     /// not contact the mint. If no keysets are found locally, returns an error.
-    pub async fn get_keyset_fees_and_amounts(&self) -> Result<HashMap<Id, (u64, Vec<u64>)>, Error> {
+    pub async fn get_keyset_fees_and_amounts(&self) -> Result<KeysetFeeAndAmounts, Error> {
         let keysets = self
             .localstore
             .get_mint_keysets(self.mint_url.clone())
@@ -161,8 +162,9 @@ impl Wallet {
                         .await?
                         .iter()
                         .map(|(amount, _)| amount.to_u64())
-                        .collect(),
-                ),
+                        .collect::<Vec<_>>(),
+                )
+                    .into(),
             );
         }
 
@@ -177,7 +179,7 @@ impl Wallet {
     pub async fn get_keyset_fees_and_amounts_by_id(
         &self,
         keyset_id: Id,
-    ) -> Result<(u64, Vec<u64>), Error> {
+    ) -> Result<FeeAndAmounts, Error> {
         self.get_keyset_fees_and_amounts()
             .await?
             .get(&keyset_id)

+ 18 - 11
crates/cdk/src/wallet/mod.rs

@@ -4,6 +4,7 @@ use std::collections::HashMap;
 use std::str::FromStr;
 use std::sync::Arc;
 
+use cdk_common::amount::FeeAndAmounts;
 use cdk_common::database::{self, WalletDatabase};
 use cdk_common::subscription::Params;
 use getrandom::getrandom;
@@ -328,7 +329,7 @@ impl Wallet {
     #[instrument(skip(self))]
     pub async fn amounts_needed_for_state_target(
         &self,
-        amounts_ppk: &[u64],
+        fee_and_amounts: &FeeAndAmounts,
     ) -> Result<Vec<Amount>, Error> {
         let unspent_proofs = self.get_unspent_proofs().await?;
 
@@ -342,16 +343,20 @@ impl Wallet {
                     acc
                 });
 
-        let needed_amounts = amounts_ppk.iter().fold(Vec::new(), |mut acc, amount| {
-            let count_needed = (self.target_proof_count as u64)
-                .saturating_sub(*amounts_count.get(amount).unwrap_or(&0));
+        let needed_amounts =
+            fee_and_amounts
+                .amounts()
+                .iter()
+                .fold(Vec::new(), |mut acc, amount| {
+                    let count_needed = (self.target_proof_count as u64)
+                        .saturating_sub(*amounts_count.get(amount).unwrap_or(&0));
 
-            for _i in 0..count_needed {
-                acc.push(Amount::from(*amount));
-            }
+                    for _i in 0..count_needed {
+                        acc.push(Amount::from(*amount));
+                    }
 
-            acc
-        });
+                    acc
+                });
         Ok(needed_amounts)
     }
 
@@ -360,9 +365,11 @@ impl Wallet {
     async fn determine_split_target_values(
         &self,
         change_amount: Amount,
-        amounts_ppk: &[u64],
+        fee_and_amounts: &FeeAndAmounts,
     ) -> Result<SplitTarget, Error> {
-        let mut amounts_needed_refill = self.amounts_needed_for_state_target(amounts_ppk).await?;
+        let mut amounts_needed_refill = self
+            .amounts_needed_for_state_target(fee_and_amounts)
+            .await?;
 
         amounts_needed_refill.sort();
 

+ 55 - 54
crates/cdk/src/wallet/proofs.rs

@@ -1,5 +1,6 @@
 use std::collections::{HashMap, HashSet};
 
+use cdk_common::amount::KeysetFeeAndAmounts;
 use cdk_common::wallet::TransactionId;
 use cdk_common::Id;
 use tracing::instrument;
@@ -13,12 +14,6 @@ use crate::nuts::{
 use crate::types::ProofInfo;
 use crate::{ensure_cdk, Amount, Error, Wallet};
 
-/// Fees and Amounts supported
-pub type FeesAndAmounts = (u64, Vec<u64>);
-
-/// Fees and Amounts for each Keyset
-pub type KeysetFeeAndAmount = HashMap<Id, FeesAndAmounts>;
-
 impl Wallet {
     /// Get unspent proofs for mint
     #[instrument(skip(self))]
@@ -194,14 +189,14 @@ impl Wallet {
         amount: Amount,
         proofs: Proofs,
         active_keyset_ids: &Vec<Id>,
-        fees_and_amounts_ppk: &KeysetFeeAndAmount,
+        fees_and_keyset_amounts: &KeysetFeeAndAmounts,
         include_fees: bool,
     ) -> Result<(Proofs, Option<(Proof, Amount)>), Error> {
         let mut input_proofs = Self::select_proofs(
             amount,
             proofs,
             active_keyset_ids,
-            fees_and_amounts_ppk,
+            fees_and_keyset_amounts,
             include_fees,
         )?;
         let mut exchange = None;
@@ -222,9 +217,9 @@ impl Wallet {
             input_proofs.sort_by(|a, b| a.amount.cmp(&b.amount));
 
             if let Some(proof_to_exchange) = input_proofs.pop() {
-                let fee_ppk = fees_and_amounts_ppk
+                let fee_ppk = fees_and_keyset_amounts
                     .get(&proof_to_exchange.keyset_id)
-                    .map(|(fee, _)| *fee)
+                    .map(|fee_and_amounts| fee_and_amounts.fee())
                     .unwrap_or_default()
                     .into();
 
@@ -250,7 +245,7 @@ impl Wallet {
         amount: Amount,
         proofs: Proofs,
         active_keyset_ids: &Vec<Id>,
-        fees_and_amounts_ppk: &KeysetFeeAndAmount,
+        fees_and_keyset_amounts: &KeysetFeeAndAmounts,
         include_fees: bool,
     ) -> Result<Proofs, Error> {
         tracing::debug!(
@@ -303,9 +298,9 @@ impl Wallet {
         };
 
         // Select proofs with the optimal amounts
-        for (_, (_, amounts)) in fees_and_amounts_ppk.iter() {
+        for (_, fee_and_amounts) in fees_and_keyset_amounts.iter() {
             // Split the amount into optimal amounts
-            for optimal_amount in amount.split(amounts) {
+            for optimal_amount in amount.split(fee_and_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);
@@ -322,7 +317,7 @@ impl Wallet {
                     proofs,
                     selected_proofs.into_iter().collect(),
                     active_keyset_ids,
-                    fees_and_amounts_ppk,
+                    fees_and_keyset_amounts,
                 );
             } else {
                 return Ok(selected_proofs.into_iter().collect());
@@ -384,7 +379,7 @@ impl Wallet {
                 proofs,
                 selected_proofs,
                 active_keyset_ids,
-                fees_and_amounts_ppk,
+                fees_and_keyset_amounts,
             );
         }
 
@@ -440,14 +435,14 @@ impl Wallet {
         proofs: Proofs,
         mut selected_proofs: Proofs,
         active_keyset_ids: &Vec<Id>,
-        fees_and_amounts_ppk: &KeysetFeeAndAmount,
+        fees_and_keyset_amounts: &KeysetFeeAndAmounts,
     ) -> Result<Proofs, Error> {
         tracing::debug!("Including fees");
         let fee = calculate_fee(
             &selected_proofs.count_by_keyset(),
-            &fees_and_amounts_ppk
+            &fees_and_keyset_amounts
                 .iter()
-                .map(|(key, values)| (*key, values.0))
+                .map(|(key, values)| (*key, values.fee()))
                 .collect(),
         )
         .unwrap_or_default();
@@ -521,17 +516,17 @@ mod tests {
     #[test]
     fn test_select_proofs_empty() {
         let active_id = id();
-        let mut fee_and_amounts_ppk = HashMap::new();
-        fee_and_amounts_ppk.insert(
+        let mut keyset_fee_and_amounts = HashMap::new();
+        keyset_fee_and_amounts.insert(
             active_id,
-            (0, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+            (0, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()).into(),
         );
         let proofs = vec![];
         let selected_proofs = Wallet::select_proofs(
             0.into(),
             proofs,
             &vec![active_id],
-            &fee_and_amounts_ppk,
+            &keyset_fee_and_amounts,
             false,
         )
         .unwrap();
@@ -541,17 +536,17 @@ mod tests {
     #[test]
     fn test_select_proofs_insufficient() {
         let active_id = id();
-        let mut fee_and_amounts_ppk = HashMap::new();
-        fee_and_amounts_ppk.insert(
+        let mut keyset_fee_and_amounts = HashMap::new();
+        keyset_fee_and_amounts.insert(
             active_id,
-            (0, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+            (0, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()).into(),
         );
         let proofs = vec![proof(1), proof(2), proof(4)];
         let selected_proofs = Wallet::select_proofs(
             8.into(),
             proofs,
             &vec![active_id],
-            &fee_and_amounts_ppk,
+            &keyset_fee_and_amounts,
             false,
         );
         assert!(selected_proofs.is_err());
@@ -570,17 +565,17 @@ mod tests {
         ];
 
         let active_id = id();
-        let mut fee_and_amounts_ppk = HashMap::new();
-        fee_and_amounts_ppk.insert(
+        let mut keyset_fee_and_amounts = HashMap::new();
+        keyset_fee_and_amounts.insert(
             active_id,
-            (0, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+            (0, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()).into(),
         );
 
         let mut selected_proofs = Wallet::select_proofs(
             77.into(),
             proofs,
             &vec![active_id],
-            &fee_and_amounts_ppk,
+            &keyset_fee_and_amounts,
             false,
         )
         .unwrap();
@@ -595,17 +590,17 @@ mod tests {
     #[test]
     fn test_select_proofs_over() {
         let active_id = id();
-        let mut fee_and_amounts_ppk = HashMap::new();
-        fee_and_amounts_ppk.insert(
+        let mut keyset_fee_and_amounts = HashMap::new();
+        keyset_fee_and_amounts.insert(
             active_id,
-            (0, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+            (0, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()).into(),
         );
         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![active_id],
-            &fee_and_amounts_ppk,
+            &keyset_fee_and_amounts,
             false,
         )
         .unwrap();
@@ -617,17 +612,17 @@ mod tests {
     fn test_select_proofs_smaller_over() {
         let proofs = vec![proof(8), proof(16), proof(32)];
         let active_id = id();
-        let mut fee_and_amounts_ppk = HashMap::new();
-        fee_and_amounts_ppk.insert(
+        let mut keyset_fee_and_amounts = HashMap::new();
+        keyset_fee_and_amounts.insert(
             active_id,
-            (0, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+            (0, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()).into(),
         );
 
         let selected_proofs = Wallet::select_proofs(
             23.into(),
             proofs,
             &vec![active_id],
-            &fee_and_amounts_ppk,
+            &keyset_fee_and_amounts,
             false,
         )
         .unwrap();
@@ -639,17 +634,17 @@ mod tests {
     #[test]
     fn test_select_proofs_many_ones() {
         let active_id = id();
-        let mut fee_and_amounts_ppk = HashMap::new();
-        fee_and_amounts_ppk.insert(
+        let mut fee_and_keyset_amounts = HashMap::new();
+        fee_and_keyset_amounts.insert(
             active_id,
-            (0, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+            (0, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()).into(),
         );
         let proofs = (0..1024).map(|_| proof(1)).collect::<Vec<_>>();
         let selected_proofs = Wallet::select_proofs(
             1024.into(),
             proofs,
             &vec![active_id],
-            &fee_and_amounts_ppk,
+            &fee_and_keyset_amounts,
             false,
         )
         .unwrap();
@@ -662,17 +657,17 @@ mod tests {
     #[test]
     fn test_select_proof_change() {
         let active_id = id();
-        let mut fee_and_amounts_ppk = HashMap::new();
-        fee_and_amounts_ppk.insert(
+        let mut keyset_fee_and_amounts = HashMap::new();
+        keyset_fee_and_amounts.insert(
             active_id,
-            (0, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+            (0, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()).into(),
         );
         let proofs = vec![proof(64), proof(4), proof(32)];
         let (selected_proofs, exchange) = Wallet::select_exact_proofs(
             97.into(),
             proofs,
             &vec![active_id],
-            &fee_and_amounts_ppk,
+            &keyset_fee_and_amounts,
             false,
         )
         .unwrap();
@@ -687,10 +682,10 @@ mod tests {
     #[test]
     fn test_select_proofs_huge_proofs() {
         let active_id = id();
-        let mut fee_and_amounts_ppk = HashMap::new();
-        fee_and_amounts_ppk.insert(
+        let mut keyset_fee_and_amounts = HashMap::new();
+        keyset_fee_and_amounts.insert(
             active_id,
-            (0, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+            (0, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()).into(),
         );
         let proofs = (0..32)
             .flat_map(|i| (0..5).map(|_| proof(1 << i)).collect::<Vec<_>>())
@@ -699,7 +694,7 @@ mod tests {
             ((1u64 << 32) - 1).into(),
             proofs,
             &vec![active_id],
-            &fee_and_amounts_ppk,
+            &keyset_fee_and_amounts,
             false,
         )
         .unwrap();
@@ -715,10 +710,16 @@ mod tests {
     #[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, (0..32).map(|x| 2u64.pow(x)).collect()));
-        let selected_proofs =
-            Wallet::select_proofs(10.into(), proofs, &vec![id()], &keyset_fees, false).unwrap();
+        let mut keyset_fee_and_amounts = HashMap::new();
+        keyset_fee_and_amounts.insert(id(), (100, (0..32).map(|x| 2u64.pow(x)).collect()).into());
+        let selected_proofs = Wallet::select_proofs(
+            10.into(),
+            proofs,
+            &vec![id()],
+            &keyset_fee_and_amounts,
+            false,
+        )
+        .unwrap();
         assert_eq!(selected_proofs.len(), 1);
         assert_eq!(selected_proofs[0].amount, 32.into());
     }

+ 4 - 7
crates/cdk/src/wallet/send.rs

@@ -130,15 +130,12 @@ impl Wallet {
     ) -> Result<PreparedSend, Error> {
         // Split amount with fee if necessary
         let active_keyset_id = self.get_active_keyset().await?.id;
-        let (_, amounts_ppk) = self
+        let fee_and_amounts = self
             .get_keyset_fees_and_amounts_by_id(active_keyset_id)
             .await?;
         let (send_amounts, send_fee) = if opts.include_fee {
-            let (keyset_fee_ppk, amounts_ppk) = self
-                .get_keyset_fees_and_amounts_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, &amounts_ppk)?;
+            tracing::debug!("Keyset fee per proof: {:?}", fee_and_amounts.fee());
+            let send_split = amount.split_with_fee(&fee_and_amounts)?;
             let send_fee = self
                 .get_proofs_fee_by_count(
                     vec![(active_keyset_id, send_split.len() as u64)]
@@ -148,7 +145,7 @@ impl Wallet {
                 .await?;
             (send_split, send_fee)
         } else {
-            let send_split = amount.split(&amounts_ppk);
+            let send_split = amount.split(&fee_and_amounts);
             let send_fee = Amount::ZERO;
             (send_split, send_fee)
         };

+ 12 - 12
crates/cdk/src/wallet/swap.rs

@@ -40,7 +40,7 @@ impl Wallet {
         let swap_response = self.client.post_swap(pre_swap.swap_request).await?;
 
         let active_keyset_id = pre_swap.pre_mint_secrets.keyset_id;
-        let (_, amounts_ppk) = self
+        let fee_and_amounts = self
             .get_keyset_fees_and_amounts_by_id(active_keyset_id)
             .await?;
 
@@ -78,7 +78,7 @@ impl Wallet {
                         let mut proofs_to_send = Proofs::new();
                         let mut proofs_to_keep = Proofs::new();
                         let mut amount_split =
-                            amount.split_targeted(&amount_split_target, &amounts_ppk)?;
+                            amount.split_targeted(&amount_split_target, &fee_and_amounts)?;
 
                         for proof in all_proofs {
                             if let Some(idx) = amount_split.iter().position(|&a| a == proof.amount)
@@ -228,7 +228,7 @@ impl Wallet {
             .checked_sub(total_to_subtract)
             .ok_or(Error::InsufficientFunds)?;
 
-        let (_, amounts_ppk) = self
+        let fee_and_amounts = self
             .get_keyset_fees_and_amounts_by_id(active_keyset_id)
             .await?;
 
@@ -236,7 +236,7 @@ impl Wallet {
             true => {
                 let split_count = amount
                     .unwrap_or(Amount::ZERO)
-                    .split_targeted(&SplitTarget::default(), &amounts_ppk)
+                    .split_targeted(&SplitTarget::default(), &fee_and_amounts)
                     .unwrap()
                     .len();
 
@@ -260,7 +260,7 @@ impl Wallet {
         // else use state refill
         let change_split_target = match amount_split_target {
             SplitTarget::None => {
-                self.determine_split_target_values(change_amount, &amounts_ppk)
+                self.determine_split_target_values(change_amount, &fee_and_amounts)
                     .await?
             }
             s => s,
@@ -273,17 +273,17 @@ impl Wallet {
             Some(_) => {
                 // For spending conditions, we only need to count change secrets
                 change_amount
-                    .split_targeted(&change_split_target, &amounts_ppk)?
+                    .split_targeted(&change_split_target, &fee_and_amounts)?
                     .len() as u32
             }
             None => {
                 // For no spending conditions, count both send and change secrets
                 let send_count = send_amount
                     .unwrap_or(Amount::ZERO)
-                    .split_targeted(&SplitTarget::default(), &amounts_ppk)?
+                    .split_targeted(&SplitTarget::default(), &fee_and_amounts)?
                     .len() as u32;
                 let change_count = change_amount
-                    .split_targeted(&change_split_target, &amounts_ppk)?
+                    .split_targeted(&change_split_target, &fee_and_amounts)?
                     .len() as u32;
                 send_count + change_count
             }
@@ -317,7 +317,7 @@ impl Wallet {
                     &self.seed,
                     change_amount,
                     &change_split_target,
-                    &amounts_ppk,
+                    &fee_and_amounts,
                 )?;
 
                 derived_secret_count = change_premint_secrets.len();
@@ -328,7 +328,7 @@ impl Wallet {
                         send_amount.unwrap_or(Amount::ZERO),
                         &SplitTarget::default(),
                         &conditions,
-                        &amounts_ppk,
+                        &fee_and_amounts,
                     )?,
                     change_premint_secrets,
                 )
@@ -340,7 +340,7 @@ impl Wallet {
                     &self.seed,
                     send_amount.unwrap_or(Amount::ZERO),
                     &SplitTarget::default(),
-                    &amounts_ppk,
+                    &fee_and_amounts,
                 )?;
 
                 count += premint_secrets.len() as u32;
@@ -351,7 +351,7 @@ impl Wallet {
                     &self.seed,
                     change_amount,
                     &change_split_target,
-                    &amounts_ppk,
+                    &fee_and_amounts,
                 )?;
 
                 derived_secret_count = change_premint_secrets.len() + premint_secrets.len();