Ver Fonte

Include supported amounts instead of assuming the power of 2

The mint's signatory defines the amounts and the wallet, and the mint, when
paying, should use them instead of assuming the supported amounts are
2^(0..32), which is not part of the spec.
Cesar Rodas há 1 mês atrás
pai
commit
d9d76a6511

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

@@ -60,31 +60,37 @@ impl Amount {
     pub const ONE: Amount = Amount(1);
 
     /// Split into parts that are powers of two
-    pub fn split(&self) -> Vec<Self> {
-        let sats = self.0;
-        (0_u64..64)
+    pub fn split(&self, amounts_ppk: &[u64]) -> Vec<Self> {
+        amounts_ppk
+            .iter()
             .rev()
-            .filter_map(|bit| {
-                let part = 1 << bit;
-                ((sats & part) == part).then_some(Self::from(part))
+            .fold((Vec::new(), self.0), |(mut acc, total), &amount| {
+                if total >= amount {
+                    acc.push(Self::from(amount));
+                }
+                (acc, total % amount)
             })
-            .collect()
+            .0
     }
 
     /// Split into parts that are powers of two by target
-    pub fn split_targeted(&self, target: &SplitTarget) -> Result<Vec<Self>, Error> {
+    pub fn split_targeted(
+        &self,
+        target: &SplitTarget,
+        amounts_ppk: &[u64],
+    ) -> Result<Vec<Self>, Error> {
         let mut parts = match target {
-            SplitTarget::None => self.split(),
+            SplitTarget::None => self.split(amounts_ppk),
             SplitTarget::Value(amount) => {
                 if self.le(amount) {
-                    return Ok(self.split());
+                    return Ok(self.split(amounts_ppk));
                 }
 
                 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();
+                let parts_of_value = amount.split(amounts_ppk);
 
                 while parts_total.lt(self) {
                     for part in parts_of_value.iter().copied() {
@@ -92,7 +98,7 @@ impl Amount {
                             parts.push(part);
                         } else {
                             let amount_left = *self - parts_total;
-                            parts.extend(amount_left.split());
+                            parts.extend(amount_left.split(amounts_ppk));
                         }
 
                         parts_total = Amount::try_sum(parts.clone().iter().copied())?;
@@ -115,7 +121,7 @@ impl Amount {
                     }
                     Ordering::Greater => {
                         let extra = *self - values_total;
-                        let mut extra_amount = extra.split();
+                        let mut extra_amount = extra.split(amounts_ppk);
                         let mut values = values.clone();
 
                         values.append(&mut extra_amount);
@@ -130,15 +136,15 @@ impl Amount {
     }
 
     /// 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();
+    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
             .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();
+        let split = new_amount.split(amounts_ppk);
         let split_fee_ppk = (split.len() as u64)
             .checked_mul(fee_ppk)
             .ok_or(Error::AmountOverflow)?;
@@ -151,7 +157,7 @@ impl Amount {
         }
         self.checked_add(Amount::ONE)
             .ok_or(Error::AmountOverflow)?
-            .split_with_fee(fee_ppk)
+            .split_with_fee(fee_ppk, amounts_ppk)
     }
 
     /// Checked addition for Amount. Returns None if overflow occurs.
@@ -192,6 +198,11 @@ impl Amount {
     ) -> Result<Amount, Error> {
         to_unit(self.0, current_unit, target_unit)
     }
+    ///
+    /// Convert to u64
+    pub fn to_u64(self) -> u64 {
+        self.0
+    }
 
     /// Convert to i64
     pub fn to_i64(self) -> Option<i64> {
@@ -376,34 +387,37 @@ mod tests {
 
     #[test]
     fn test_split_amount() {
-        assert_eq!(Amount::from(1).split(), vec![Amount::from(1)]);
-        assert_eq!(Amount::from(2).split(), vec![Amount::from(2)]);
+        let amounts_ppk = (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>();
+
+        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(),
+            Amount::from(3).split(&amounts_ppk),
             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);
+        assert_eq!(Amount::from(11).split(&amounts_ppk), 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);
+        assert_eq!(Amount::from(255).split(&amounts_ppk), amounts);
     }
 
     #[test]
     fn test_split_target_amount() {
+        let amounts_ppk = (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>();
         let amount = Amount(65);
 
         let split = amount
-            .split_targeted(&SplitTarget::Value(Amount(32)))
+            .split_targeted(&SplitTarget::Value(Amount(32)), &amounts_ppk)
             .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)))
+            .split_targeted(&SplitTarget::Value(Amount::from(50)), &amounts_ppk)
             .unwrap();
         assert_eq!(
             vec![
@@ -423,7 +437,7 @@ mod tests {
         let amount = Amount::from(63);
 
         let split = amount
-            .split_targeted(&SplitTarget::Value(Amount::from(32)))
+            .split_targeted(&SplitTarget::Value(Amount::from(32)), &amounts_ppk)
             .unwrap();
         assert_eq!(
             vec![
@@ -440,22 +454,23 @@ mod tests {
 
     #[test]
     fn test_split_with_fee() {
+        let amounts_ppk = (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>();
         let amount = Amount(2);
         let fee_ppk = 1;
 
-        let split = amount.split_with_fee(fee_ppk).unwrap();
+        let split = amount.split_with_fee(fee_ppk, &amounts_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();
+        let split = amount.split_with_fee(fee_ppk, &amounts_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();
+        let split = amount.split_with_fee(fee_ppk, &amounts_ppk).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)]);
@@ -463,11 +478,12 @@ mod tests {
 
     #[test]
     fn test_split_with_fee_reported_issue() {
+        let amounts_ppk = (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>();
         // 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).unwrap();
+        let split = amount.split_with_fee(fee_ppk, &amounts_ppk).unwrap();
 
         // Calculate the total fee for the split
         let total_fee_ppk = (split.len() as u64) * fee_ppk;
@@ -486,6 +502,7 @@ 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),
@@ -502,7 +519,7 @@ mod tests {
         ];
 
         for (amount, fee_ppk) in test_cases {
-            let result = amount.split_with_fee(fee_ppk);
+            let result = amount.split_with_fee(fee_ppk, &amounts_ppk);
             assert!(
                 result.is_ok(),
                 "split_with_fee failed for amount {} with fee_ppk {}: {:?}",
@@ -539,6 +556,7 @@ 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
@@ -550,7 +568,7 @@ mod tests {
         ];
 
         for (amount, fee_ppk) in test_cases {
-            let result = amount.split_with_fee(fee_ppk);
+            let result = amount.split_with_fee(fee_ppk, &amounts_ppk);
             assert!(
                 result.is_ok(),
                 "split_with_fee failed for amount {} with fee_ppk {}: {:?}",
@@ -575,12 +593,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 result = amount.split_with_fee(fee_ppk);
+        let result = amount.split_with_fee(fee_ppk, &amounts_ppk);
         assert!(
             result.is_ok(),
             "split_with_fee should handle extreme fees without infinite recursion"
@@ -589,13 +608,14 @@ mod tests {
 
     #[test]
     fn test_split_values() {
+        let amounts_ppk = (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>();
         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).unwrap();
+        let values = amount.split_targeted(&split_target, &amounts_ppk).unwrap();
 
         assert_eq!(target, values);
 
@@ -603,13 +623,13 @@ mod tests {
 
         let split_target = SplitTarget::Values(vec![Amount(2), Amount(4)]);
 
-        let values = amount.split_targeted(&split_target).unwrap();
+        let values = amount.split_targeted(&split_target, &amounts_ppk).unwrap();
 
         assert_eq!(target, values);
 
         let split_target = SplitTarget::Values(vec![Amount(2), Amount(10)]);
 
-        let values = amount.split_targeted(&split_target);
+        let values = amount.split_targeted(&split_target, &amounts_ppk);
 
         assert!(values.is_err())
     }

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

@@ -746,8 +746,9 @@ impl PreMintSecrets {
         keyset_id: Id,
         amount: Amount,
         amount_split_target: &SplitTarget,
+        amounts_ppk: &[u64],
     ) -> Result<Self, Error> {
-        let amount_split = amount.split_targeted(amount_split_target)?;
+        let amount_split = amount.split_targeted(amount_split_target, amounts_ppk)?;
 
         let mut output = Vec::with_capacity(amount_split.len());
 
@@ -830,8 +831,9 @@ impl PreMintSecrets {
         amount: Amount,
         amount_split_target: &SplitTarget,
         conditions: &SpendingConditions,
+        amounts_ppk: &[u64],
     ) -> Result<Self, Error> {
-        let amount_split = amount.split_targeted(amount_split_target)?;
+        let amount_split = amount.split_targeted(amount_split_target, amounts_ppk)?;
 
         let mut output = Vec::with_capacity(amount_split.len());
 

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

@@ -127,12 +127,13 @@ impl PreMintSecrets {
         seed: &[u8; 64],
         amount: Amount,
         amount_split_target: &SplitTarget,
+        amounts_ppk: &[u64],
     ) -> 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)? {
+        for amount in amount.split_targeted(amount_split_target, amounts_ppk)? {
             let secret = Secret::from_seed(seed, keyset_id, counter)?;
             let blinding_factor = SecretKey::from_seed(seed, keyset_id, counter)?;
 
@@ -486,10 +487,12 @@ 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<_>>();
 
         // Test PreMintSecrets generation with v2 keyset
         let pre_mint_secrets =
-            PreMintSecrets::from_seed(keyset_id, 0, &seed, amount, &split_target).unwrap();
+            PreMintSecrets::from_seed(keyset_id, 0, &seed, amount, &split_target, &amounts_ppk)
+                .unwrap();
 
         // Verify all secrets in the pre_mint use the new v2 derivation
         for (i, pre_mint) in pre_mint_secrets.secrets.iter().enumerate() {

+ 2 - 2
crates/cdk-ffi/src/wallet.rs

@@ -372,7 +372,7 @@ 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_by_id(id).await?;
+        let (fees, _) = self.inner.get_keyset_fees_and_amounts_by_id(id).await?;
         Ok(fees)
     }
 
@@ -397,7 +397,7 @@ 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_by_id(id).await?;
+        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
         Ok(Amount::new(total_fee))
     }

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

@@ -352,7 +352,12 @@ 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 pre_mint = PreMintSecrets::random(active_keyset_id, 500.into(), &SplitTarget::None)?;
+    let pre_mint = PreMintSecrets::random(
+        active_keyset_id,
+        500.into(),
+        &SplitTarget::None,
+        &((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+    )?;
 
     let quote_info = wallet
         .localstore

+ 109 - 30
crates/cdk-integration-tests/tests/fake_wallet.rs

@@ -393,8 +393,13 @@ async fn test_fake_melt_change_in_quote() {
 
     let keyset = wallet.fetch_active_keyset().await.unwrap();
 
-    let premint_secrets =
-        PreMintSecrets::random(keyset.id, 100.into(), &SplitTarget::default()).unwrap();
+    let premint_secrets = PreMintSecrets::random(
+        keyset.id,
+        100.into(),
+        &SplitTarget::default(),
+        &((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+    )
+    .unwrap();
 
     let client = HttpClient::new(MINT_URL.parse().unwrap(), None);
 
@@ -470,8 +475,13 @@ async fn test_fake_mint_without_witness() {
 
     let active_keyset_id = wallet.fetch_active_keyset().await.unwrap().id;
 
-    let premint_secrets =
-        PreMintSecrets::random(active_keyset_id, 100.into(), &SplitTarget::default()).unwrap();
+    let premint_secrets = PreMintSecrets::random(
+        active_keyset_id,
+        100.into(),
+        &SplitTarget::default(),
+        &((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+    )
+    .unwrap();
 
     let request = MintRequest {
         quote: mint_quote.id,
@@ -514,8 +524,13 @@ async fn test_fake_mint_with_wrong_witness() {
 
     let active_keyset_id = wallet.fetch_active_keyset().await.unwrap().id;
 
-    let premint_secrets =
-        PreMintSecrets::random(active_keyset_id, 100.into(), &SplitTarget::default()).unwrap();
+    let premint_secrets = PreMintSecrets::random(
+        active_keyset_id,
+        100.into(),
+        &SplitTarget::default(),
+        &((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+    )
+    .unwrap();
 
     let mut request = MintRequest {
         quote: mint_quote.id,
@@ -562,8 +577,13 @@ async fn test_fake_mint_inflated() {
 
     let active_keyset_id = wallet.fetch_active_keyset().await.unwrap().id;
 
-    let pre_mint =
-        PreMintSecrets::random(active_keyset_id, 500.into(), &SplitTarget::None).unwrap();
+    let pre_mint = PreMintSecrets::random(
+        active_keyset_id,
+        500.into(),
+        &SplitTarget::None,
+        &((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+    )
+    .unwrap();
 
     let quote_info = wallet
         .localstore
@@ -624,7 +644,13 @@ async fn test_fake_mint_multiple_units() {
 
     let active_keyset_id = wallet.fetch_active_keyset().await.unwrap().id;
 
-    let pre_mint = PreMintSecrets::random(active_keyset_id, 50.into(), &SplitTarget::None).unwrap();
+    let pre_mint = PreMintSecrets::random(
+        active_keyset_id,
+        50.into(),
+        &SplitTarget::None,
+        &((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+    )
+    .unwrap();
 
     let wallet_usd = Wallet::new(
         MINT_URL,
@@ -637,8 +663,13 @@ async fn test_fake_mint_multiple_units() {
 
     let active_keyset_id = wallet_usd.fetch_active_keyset().await.unwrap().id;
 
-    let usd_pre_mint =
-        PreMintSecrets::random(active_keyset_id, 50.into(), &SplitTarget::None).unwrap();
+    let usd_pre_mint = PreMintSecrets::random(
+        active_keyset_id,
+        50.into(),
+        &SplitTarget::None,
+        &((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+    )
+    .unwrap();
 
     let quote_info = wallet
         .localstore
@@ -738,6 +769,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<_>>()),
         )
         .unwrap();
 
@@ -766,11 +798,20 @@ async fn test_fake_mint_multiple_unit_swap() {
         let total_inputs = inputs.total_amount().unwrap();
 
         let half = total_inputs / 2.into();
-        let usd_pre_mint =
-            PreMintSecrets::random(usd_active_keyset_id, half, &SplitTarget::None).unwrap();
-        let pre_mint =
-            PreMintSecrets::random(active_keyset_id, total_inputs - half, &SplitTarget::None)
-                .unwrap();
+        let usd_pre_mint = PreMintSecrets::random(
+            usd_active_keyset_id,
+            half,
+            &SplitTarget::None,
+            &((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+        )
+        .unwrap();
+        let pre_mint = PreMintSecrets::random(
+            active_keyset_id,
+            total_inputs - half,
+            &SplitTarget::None,
+            &((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+        )
+        .unwrap();
 
         let mut usd_outputs = usd_pre_mint.blinded_messages();
         let mut sat_outputs = pre_mint.blinded_messages();
@@ -882,10 +923,16 @@ 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<_>>()),
+        )
+        .unwrap();
+        let pre_mint = PreMintSecrets::random(
+            active_keyset_id,
+            100.into(),
+            &SplitTarget::None,
+            &((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
         )
         .unwrap();
-        let pre_mint =
-            PreMintSecrets::random(active_keyset_id, 100.into(), &SplitTarget::None).unwrap();
 
         let mut usd_outputs = usd_pre_mint.blinded_messages();
         let mut sat_outputs = pre_mint.blinded_messages();
@@ -951,6 +998,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<_>>()),
     )
     .unwrap();
 
@@ -993,8 +1041,13 @@ async fn test_fake_mint_swap_inflated() {
         .expect("no error");
 
     let active_keyset_id = wallet.fetch_active_keyset().await.unwrap().id;
-    let pre_mint =
-        PreMintSecrets::random(active_keyset_id, 101.into(), &SplitTarget::None).unwrap();
+    let pre_mint = PreMintSecrets::random(
+        active_keyset_id,
+        101.into(),
+        &SplitTarget::None,
+        &((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+    )
+    .unwrap();
 
     let swap_request = SwapRequest::new(proofs, pre_mint.blinded_messages());
 
@@ -1038,8 +1091,13 @@ async fn test_fake_mint_swap_spend_after_fail() {
 
     let active_keyset_id = wallet.fetch_active_keyset().await.unwrap().id;
 
-    let pre_mint =
-        PreMintSecrets::random(active_keyset_id, 100.into(), &SplitTarget::None).unwrap();
+    let pre_mint = PreMintSecrets::random(
+        active_keyset_id,
+        100.into(),
+        &SplitTarget::None,
+        &((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+    )
+    .unwrap();
 
     let swap_request = SwapRequest::new(proofs.clone(), pre_mint.blinded_messages());
 
@@ -1048,8 +1106,13 @@ async fn test_fake_mint_swap_spend_after_fail() {
 
     assert!(response.is_ok());
 
-    let pre_mint =
-        PreMintSecrets::random(active_keyset_id, 101.into(), &SplitTarget::None).unwrap();
+    let pre_mint = PreMintSecrets::random(
+        active_keyset_id,
+        101.into(),
+        &SplitTarget::None,
+        &((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+    )
+    .unwrap();
 
     let swap_request = SwapRequest::new(proofs.clone(), pre_mint.blinded_messages());
 
@@ -1064,8 +1127,13 @@ async fn test_fake_mint_swap_spend_after_fail() {
         Ok(_) => panic!("Should not have allowed swap with unbalanced"),
     }
 
-    let pre_mint =
-        PreMintSecrets::random(active_keyset_id, 100.into(), &SplitTarget::None).unwrap();
+    let pre_mint = PreMintSecrets::random(
+        active_keyset_id,
+        100.into(),
+        &SplitTarget::None,
+        &((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+    )
+    .unwrap();
 
     let swap_request = SwapRequest::new(proofs, pre_mint.blinded_messages());
 
@@ -1109,8 +1177,13 @@ async fn test_fake_mint_melt_spend_after_fail() {
 
     let active_keyset_id = wallet.fetch_active_keyset().await.unwrap().id;
 
-    let pre_mint =
-        PreMintSecrets::random(active_keyset_id, 100.into(), &SplitTarget::None).unwrap();
+    let pre_mint = PreMintSecrets::random(
+        active_keyset_id,
+        100.into(),
+        &SplitTarget::None,
+        &((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+    )
+    .unwrap();
 
     let swap_request = SwapRequest::new(proofs.clone(), pre_mint.blinded_messages());
 
@@ -1119,8 +1192,13 @@ async fn test_fake_mint_melt_spend_after_fail() {
 
     assert!(response.is_ok());
 
-    let pre_mint =
-        PreMintSecrets::random(active_keyset_id, 101.into(), &SplitTarget::None).unwrap();
+    let pre_mint = PreMintSecrets::random(
+        active_keyset_id,
+        101.into(),
+        &SplitTarget::None,
+        &((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+    )
+    .unwrap();
 
     let swap_request = SwapRequest::new(proofs.clone(), pre_mint.blinded_messages());
 
@@ -1187,6 +1265,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<_>>()),
     )
     .unwrap();
 

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

@@ -344,6 +344,7 @@ 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,
@@ -378,7 +379,8 @@ async fn test_fake_melt_change_in_quote() {
     let keyset = wallet.fetch_active_keyset().await.unwrap();
 
     let premint_secrets =
-        PreMintSecrets::random(keyset.id, 100.into(), &SplitTarget::default()).unwrap();
+        PreMintSecrets::random(keyset.id, 100.into(), &SplitTarget::default(), &amounts_ppk)
+            .unwrap();
 
     let client = HttpClient::new(get_mint_url_from_env().parse().unwrap(), None);
 

+ 108 - 23
crates/cdk-integration-tests/tests/integration_tests_pure.rs

@@ -248,6 +248,7 @@ async fn test_mint_double_spend() {
         keyset_id,
         proofs.total_amount().unwrap(),
         &SplitTarget::default(),
+        &((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
     )
     .unwrap();
 
@@ -260,6 +261,7 @@ async fn test_mint_double_spend() {
         keyset_id,
         proofs.total_amount().unwrap(),
         &SplitTarget::default(),
+        &((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
     )
     .unwrap();
 
@@ -301,13 +303,28 @@ async fn test_attempt_to_swap_by_overflowing() {
     let keys = mint_bob.pubkeys().keysets.first().unwrap().clone();
     let keyset_id = keys.id;
 
-    let pre_mint_amount =
-        PreMintSecrets::random(keyset_id, amount.into(), &SplitTarget::default()).unwrap();
-    let pre_mint_amount_two =
-        PreMintSecrets::random(keyset_id, amount.into(), &SplitTarget::default()).unwrap();
+    let pre_mint_amount = PreMintSecrets::random(
+        keyset_id,
+        amount.into(),
+        &SplitTarget::default(),
+        &((0..64).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+    )
+    .unwrap();
+    let pre_mint_amount_two = PreMintSecrets::random(
+        keyset_id,
+        amount.into(),
+        &SplitTarget::default(),
+        &((0..64).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+    )
+    .unwrap();
 
-    let mut pre_mint =
-        PreMintSecrets::random(keyset_id, 1.into(), &SplitTarget::default()).unwrap();
+    let mut pre_mint = PreMintSecrets::random(
+        keyset_id,
+        1.into(),
+        &SplitTarget::default(),
+        &((0..64).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+    )
+    .unwrap();
 
     pre_mint.combine(pre_mint_amount);
     pre_mint.combine(pre_mint_amount_two);
@@ -354,8 +371,13 @@ async fn test_swap_unbalanced() {
     let keyset_id = get_keyset_id(&mint_bob).await;
 
     // Try to swap for less than the input amount (95 < 100)
-    let preswap = PreMintSecrets::random(keyset_id, 95.into(), &SplitTarget::default())
-        .expect("Failed to create preswap");
+    let preswap = PreMintSecrets::random(
+        keyset_id,
+        95.into(),
+        &SplitTarget::default(),
+        &((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+    )
+    .expect("Failed to create preswap");
 
     let swap_request = SwapRequest::new(proofs.clone(), preswap.blinded_messages());
 
@@ -368,8 +390,13 @@ async fn test_swap_unbalanced() {
     }
 
     // Try to swap for more than the input amount (101 > 100)
-    let preswap = PreMintSecrets::random(keyset_id, 101.into(), &SplitTarget::default())
-        .expect("Failed to create preswap");
+    let preswap = PreMintSecrets::random(
+        keyset_id,
+        101.into(),
+        &SplitTarget::default(),
+        &((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+    )
+    .expect("Failed to create preswap");
 
     let swap_request = SwapRequest::new(proofs.clone(), preswap.blinded_messages());
 
@@ -413,6 +440,7 @@ pub async fn test_p2pk_swap() {
         100.into(),
         &SplitTarget::default(),
         &spending_conditions,
+        &((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
     )
     .unwrap();
 
@@ -430,7 +458,13 @@ pub async fn test_p2pk_swap() {
     )
     .unwrap();
 
-    let pre_swap = PreMintSecrets::random(keyset_id, 100.into(), &SplitTarget::default()).unwrap();
+    let pre_swap = PreMintSecrets::random(
+        keyset_id,
+        100.into(),
+        &SplitTarget::default(),
+        &((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+    )
+    .unwrap();
 
     let swap_request = SwapRequest::new(proofs.clone(), pre_swap.blinded_messages());
 
@@ -537,7 +571,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 preswap = PreMintSecrets::random(keyset_id, 9998.into(), &SplitTarget::default()).unwrap();
+    let preswap = PreMintSecrets::random(
+        keyset_id,
+        9998.into(),
+        &SplitTarget::default(),
+        &((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+    )
+    .unwrap();
 
     let swap_request = SwapRequest::new(proofs.clone(), preswap.blinded_messages());
 
@@ -553,7 +593,13 @@ async fn test_swap_overpay_underpay_fee() {
         },
     }
 
-    let preswap = PreMintSecrets::random(keyset_id, 1000.into(), &SplitTarget::default()).unwrap();
+    let preswap = PreMintSecrets::random(
+        keyset_id,
+        1000.into(),
+        &SplitTarget::default(),
+        &((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+    )
+    .unwrap();
 
     let swap_request = SwapRequest::new(proofs.clone(), preswap.blinded_messages());
 
@@ -605,7 +651,13 @@ async fn test_mint_enforce_fee() {
 
     let five_proofs: Vec<_> = proofs.drain(..5).collect();
 
-    let preswap = PreMintSecrets::random(keyset_id, 5.into(), &SplitTarget::default()).unwrap();
+    let preswap = PreMintSecrets::random(
+        keyset_id,
+        5.into(),
+        &SplitTarget::default(),
+        &((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+    )
+    .unwrap();
 
     let swap_request = SwapRequest::new(five_proofs.clone(), preswap.blinded_messages());
 
@@ -621,7 +673,13 @@ async fn test_mint_enforce_fee() {
         },
     }
 
-    let preswap = PreMintSecrets::random(keyset_id, 4.into(), &SplitTarget::default()).unwrap();
+    let preswap = PreMintSecrets::random(
+        keyset_id,
+        4.into(),
+        &SplitTarget::default(),
+        &((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+    )
+    .unwrap();
 
     let swap_request = SwapRequest::new(five_proofs.clone(), preswap.blinded_messages());
 
@@ -631,7 +689,13 @@ async fn test_mint_enforce_fee() {
 
     let thousnad_proofs: Vec<_> = proofs.drain(..1001).collect();
 
-    let preswap = PreMintSecrets::random(keyset_id, 1000.into(), &SplitTarget::default()).unwrap();
+    let preswap = PreMintSecrets::random(
+        keyset_id,
+        1000.into(),
+        &SplitTarget::default(),
+        &((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+    )
+    .unwrap();
 
     let swap_request = SwapRequest::new(thousnad_proofs.clone(), preswap.blinded_messages());
 
@@ -647,7 +711,13 @@ async fn test_mint_enforce_fee() {
         },
     }
 
-    let preswap = PreMintSecrets::random(keyset_id, 999.into(), &SplitTarget::default()).unwrap();
+    let preswap = PreMintSecrets::random(
+        keyset_id,
+        999.into(),
+        &SplitTarget::default(),
+        &((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+    )
+    .unwrap();
 
     let swap_request = SwapRequest::new(thousnad_proofs.clone(), preswap.blinded_messages());
 
@@ -723,16 +793,31 @@ async fn test_concurrent_double_spend_swap() {
     let keyset_id = get_keyset_id(&mint_bob).await;
 
     // Create 3 identical swap requests with the same proofs
-    let preswap1 = PreMintSecrets::random(keyset_id, 100.into(), &SplitTarget::default())
-        .expect("Failed to create preswap");
+    let preswap1 = PreMintSecrets::random(
+        keyset_id,
+        100.into(),
+        &SplitTarget::default(),
+        &((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+    )
+    .expect("Failed to create preswap");
     let swap_request1 = SwapRequest::new(proofs.clone(), preswap1.blinded_messages());
 
-    let preswap2 = PreMintSecrets::random(keyset_id, 100.into(), &SplitTarget::default())
-        .expect("Failed to create preswap");
+    let preswap2 = PreMintSecrets::random(
+        keyset_id,
+        100.into(),
+        &SplitTarget::default(),
+        &((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+    )
+    .expect("Failed to create preswap");
     let swap_request2 = SwapRequest::new(proofs.clone(), preswap2.blinded_messages());
 
-    let preswap3 = PreMintSecrets::random(keyset_id, 100.into(), &SplitTarget::default())
-        .expect("Failed to create preswap");
+    let preswap3 = PreMintSecrets::random(
+        keyset_id,
+        100.into(),
+        &SplitTarget::default(),
+        &((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+    )
+    .expect("Failed to create preswap");
     let swap_request3 = SwapRequest::new(proofs.clone(), preswap3.blinded_messages());
 
     // Spawn 3 concurrent tasks to process the swap requests

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

@@ -316,8 +316,13 @@ async fn test_cached_mint() {
 
     let active_keyset_id = wallet.fetch_active_keyset().await.unwrap().id;
     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()).unwrap();
+    let premint_secrets = PreMintSecrets::random(
+        active_keyset_id,
+        100.into(),
+        &SplitTarget::default().to_owned(),
+        &((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+    )
+    .unwrap();
 
     let mut request = MintRequest {
         quote: quote.id,

+ 3 - 0
crates/cdk-signatory/src/proto/convert.rs

@@ -60,6 +60,7 @@ impl TryInto<crate::signatory::SignatoryKeySet> for KeySet {
                     .map(|(amount, pk)| PublicKey::from_slice(&pk).map(|pk| (amount.into(), pk)))
                     .collect::<Result<BTreeMap<Amount, _>, _>>()?,
             ),
+            amounts: self.amounts,
             final_expiry: self.final_expiry,
         })
     }
@@ -80,6 +81,7 @@ impl From<crate::signatory::SignatoryKeySet> for KeySet {
                     .collect(),
             }),
             final_expiry: keyset.final_expiry,
+            amounts: keyset.amounts,
             version: Default::default(),
         }
     }
@@ -361,6 +363,7 @@ impl From<cdk_common::KeySetInfo> for KeySet {
             input_fee_ppk: value.input_fee_ppk,
             keys: Default::default(),
             final_expiry: value.final_expiry,
+            amounts: vec![],
             version: Default::default(),
         }
     }

+ 1 - 0
crates/cdk-signatory/src/proto/signatory.proto

@@ -64,6 +64,7 @@ message KeySet {
   Keys keys = 5;
   optional uint64 final_expiry = 6;
   uint64 version = 7;
+  repeated uint64 amounts = 8;
 }
 
 message Keys {

+ 4 - 1
crates/cdk-signatory/src/signatory.rs

@@ -71,6 +71,8 @@ pub struct SignatoryKeySet {
     pub active: bool,
     /// The list of public keys
     pub keys: Keys,
+    /// Amounts supported by the keyset
+    pub amounts: Vec<u64>,
     /// Information about the fee per public key
     pub input_fee_ppk: u64,
     /// Final expiry of the keyset (unix timestamp in the future)
@@ -110,7 +112,7 @@ impl From<SignatoryKeySet> for MintKeySetInfo {
             derivation_path: Default::default(),
             derivation_path_index: Default::default(),
             max_order: 0,
-            amounts: vec![],
+            amounts: val.amounts,
             final_expiry: val.final_expiry,
             valid_from: 0,
         }
@@ -124,6 +126,7 @@ impl From<&(MintKeySetInfo, MintKeySet)> for SignatoryKeySet {
             unit: key.unit.clone(),
             active: info.active,
             input_fee_ppk: info.input_fee_ppk,
+            amounts: info.amounts.clone(),
             keys: key.keys.clone().into(),
             final_expiry: key.final_expiry,
         }

+ 16 - 1
crates/cdk/src/mint/melt.rs

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

+ 13 - 3
crates/cdk/src/wallet/auth/auth_wallet.rs

@@ -394,9 +394,19 @@ impl AuthWallet {
         }
 
         let active_keyset_id = self.fetch_active_keyset().await?.id;
-
-        let premint_secrets =
-            PreMintSecrets::random(active_keyset_id, amount, &SplitTarget::Value(1.into()))?;
+        let amounts_ppk = self
+            .load_keyset_keys(active_keyset_id)
+            .await?
+            .iter()
+            .map(|(amount, _)| amount.to_u64())
+            .collect::<Vec<_>>();
+
+        let premint_secrets = PreMintSecrets::random(
+            active_keyset_id,
+            amount,
+            &SplitTarget::Value(1.into()),
+            &amounts_ppk,
+        )?;
 
         let request = MintAuthRequest {
             outputs: premint_secrets.blinded_messages(),

+ 7 - 1
crates/cdk/src/wallet/issue/issue_bolt11.rs

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

+ 6 - 1
crates/cdk/src/wallet/issue/issue_bolt12.rs

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

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

@@ -139,12 +139,12 @@ impl Wallet {
         }
     }
 
-    /// Get keyset fees for mint from local database only - offline operation
+    /// Get keyset fees and amounts for mint from local database only - offline operation
     ///
     /// 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(&self) -> Result<HashMap<Id, u64>, Error> {
+    pub async fn get_keyset_fees_and_amounts(&self) -> Result<HashMap<Id, (u64, Vec<u64>)>, Error> {
         let keysets = self
             .localstore
             .get_mint_keysets(self.mint_url.clone())
@@ -153,19 +153,32 @@ impl Wallet {
 
         let mut fees = HashMap::new();
         for keyset in keysets {
-            fees.insert(keyset.id, keyset.input_fee_ppk);
+            fees.insert(
+                keyset.id,
+                (
+                    keyset.input_fee_ppk,
+                    self.load_keyset_keys(keyset.id)
+                        .await?
+                        .iter()
+                        .map(|(amount, _)| amount.to_u64())
+                        .collect(),
+                ),
+            );
         }
 
         Ok(fees)
     }
 
-    /// Get keyset fees for mint by keyset id from local database only - offline operation
+    /// Get keyset fees and amounts for mint by keyset id from local database only - offline operation
     ///
     /// Returns the input fee rate (per-proof-per-thousand) for a specific keyset ID from
     /// cached keysets in the local database. This is an offline operation that does not
     /// contact the mint. If the keyset is not found locally, returns an error.
-    pub async fn get_keyset_fees_by_id(&self, keyset_id: Id) -> Result<u64, Error> {
-        self.get_keyset_fees()
+    pub async fn get_keyset_fees_and_amounts_by_id(
+        &self,
+        keyset_id: Id,
+    ) -> Result<(u64, Vec<u64>), Error> {
+        self.get_keyset_fees_and_amounts()
             .await?
             .get(&keyset_id)
             .cloned()

+ 1 - 1
crates/cdk/src/wallet/melt/melt_bolt11.rs

@@ -341,7 +341,7 @@ impl Wallet {
             .into_iter()
             .map(|k| k.id)
             .collect();
-        let keyset_fees = self.get_keyset_fees().await?;
+        let keyset_fees = self.get_keyset_fees_and_amounts().await?;
         let (mut input_proofs, mut exchange) = Wallet::select_exact_proofs(
             inputs_needed_amount,
             available_proofs,

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

@@ -326,34 +326,32 @@ impl Wallet {
 
     /// Get amounts needed to refill proof state
     #[instrument(skip(self))]
-    pub async fn amounts_needed_for_state_target(&self) -> Result<Vec<Amount>, Error> {
+    pub async fn amounts_needed_for_state_target(
+        &self,
+        amounts_ppk: &[u64],
+    ) -> Result<Vec<Amount>, Error> {
         let unspent_proofs = self.get_unspent_proofs().await?;
 
-        let amounts_count: HashMap<usize, usize> =
+        let amounts_count: HashMap<u64, u64> =
             unspent_proofs
                 .iter()
                 .fold(HashMap::new(), |mut acc, proof| {
                     let amount = proof.amount;
-                    let counter = acc.entry(u64::from(amount) as usize).or_insert(0);
+                    let counter = acc.entry(u64::from(amount)).or_insert(0);
                     *counter += 1;
                     acc
                 });
 
-        let all_possible_amounts: Vec<usize> = (0..32).map(|i| 2usize.pow(i as u32)).collect();
-
-        let needed_amounts = all_possible_amounts
-            .iter()
-            .fold(Vec::new(), |mut acc, amount| {
-                let count_needed: usize = self
-                    .target_proof_count
-                    .saturating_sub(*amounts_count.get(amount).unwrap_or(&0));
+        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));
 
-                for _i in 0..count_needed {
-                    acc.push(Amount::from(*amount as u64));
-                }
+            for _i in 0..count_needed {
+                acc.push(Amount::from(*amount));
+            }
 
-                acc
-            });
+            acc
+        });
         Ok(needed_amounts)
     }
 
@@ -362,8 +360,9 @@ impl Wallet {
     async fn determine_split_target_values(
         &self,
         change_amount: Amount,
+        amounts_ppk: &[u64],
     ) -> Result<SplitTarget, Error> {
-        let mut amounts_needed_refill = self.amounts_needed_for_state_target().await?;
+        let mut amounts_needed_refill = self.amounts_needed_for_state_target(amounts_ppk).await?;
 
         amounts_needed_refill.sort();
 

+ 144 - 37
crates/cdk/src/wallet/proofs.rs

@@ -13,6 +13,12 @@ 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))]
@@ -188,11 +194,16 @@ impl Wallet {
         amount: Amount,
         proofs: Proofs,
         active_keyset_ids: &Vec<Id>,
-        keyset_fees: &HashMap<Id, u64>,
+        fees_and_amounts_ppk: &KeysetFeeAndAmount,
         include_fees: bool,
     ) -> Result<(Proofs, Option<(Proof, Amount)>), Error> {
-        let mut input_proofs =
-            Self::select_proofs(amount, proofs, active_keyset_ids, keyset_fees, include_fees)?;
+        let mut input_proofs = Self::select_proofs(
+            amount,
+            proofs,
+            active_keyset_ids,
+            fees_and_amounts_ppk,
+            include_fees,
+        )?;
         let mut exchange = None;
 
         // How much amounts do we have selected in our proof sets?
@@ -211,9 +222,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 = keyset_fees
+                let fee_ppk = fees_and_amounts_ppk
                     .get(&proof_to_exchange.keyset_id)
-                    .cloned()
+                    .map(|(fee, _)| *fee)
                     .unwrap_or_default()
                     .into();
 
@@ -239,7 +250,7 @@ impl Wallet {
         amount: Amount,
         proofs: Proofs,
         active_keyset_ids: &Vec<Id>,
-        keyset_fees: &HashMap<Id, u64>,
+        fees_and_amounts_ppk: &KeysetFeeAndAmount,
         include_fees: bool,
     ) -> Result<Proofs, Error> {
         tracing::debug!(
@@ -256,9 +267,6 @@ impl Wallet {
         let mut proofs = proofs;
         proofs.sort_by(|a, b| a.cmp(b).reverse());
 
-        // Split the amount into optimal amounts
-        let optimal_amounts = amount.split();
-
         // Track selected proofs and remaining amounts (include all inactive proofs first)
         let mut selected_proofs: HashSet<Proof> = proofs
             .iter()
@@ -295,10 +303,13 @@ impl Wallet {
         };
 
         // 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);
+        for (_, (_, amounts)) in fees_and_amounts_ppk.iter() {
+            // Split the amount into optimal amounts
+            for optimal_amount in amount.split(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);
+                }
             }
         }
 
@@ -311,7 +322,7 @@ impl Wallet {
                     proofs,
                     selected_proofs.into_iter().collect(),
                     active_keyset_ids,
-                    keyset_fees,
+                    fees_and_amounts_ppk,
                 );
             } else {
                 return Ok(selected_proofs.into_iter().collect());
@@ -373,7 +384,7 @@ impl Wallet {
                 proofs,
                 selected_proofs,
                 active_keyset_ids,
-                keyset_fees,
+                fees_and_amounts_ppk,
             );
         }
 
@@ -429,11 +440,17 @@ impl Wallet {
         proofs: Proofs,
         mut selected_proofs: Proofs,
         active_keyset_ids: &Vec<Id>,
-        keyset_fees: &HashMap<Id, u64>,
+        fees_and_amounts_ppk: &KeysetFeeAndAmount,
     ) -> Result<Proofs, Error> {
         tracing::debug!("Including fees");
-        let fee =
-            calculate_fee(&selected_proofs.count_by_keyset(), keyset_fees).unwrap_or_default();
+        let fee = calculate_fee(
+            &selected_proofs.count_by_keyset(),
+            &fees_and_amounts_ppk
+                .iter()
+                .map(|(key, values)| (*key, values.0))
+                .collect(),
+        )
+        .unwrap_or_default();
         let net_amount = selected_proofs.total_amount()? - fee;
         tracing::debug!(
             "Net amount={}, fee={}, total amount={}",
@@ -503,17 +520,40 @@ 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(
+            active_id,
+            (0, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+        );
         let proofs = vec![];
-        let selected_proofs =
-            Wallet::select_proofs(0.into(), proofs, &vec![id()], &HashMap::new(), false).unwrap();
+        let selected_proofs = Wallet::select_proofs(
+            0.into(),
+            proofs,
+            &vec![active_id],
+            &fee_and_amounts_ppk,
+            false,
+        )
+        .unwrap();
         assert_eq!(selected_proofs.len(), 0);
     }
 
     #[test]
     fn test_select_proofs_insufficient() {
+        let active_id = id();
+        let mut fee_and_amounts_ppk = HashMap::new();
+        fee_and_amounts_ppk.insert(
+            active_id,
+            (0, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+        );
         let proofs = vec![proof(1), proof(2), proof(4)];
-        let selected_proofs =
-            Wallet::select_proofs(8.into(), proofs, &vec![id()], &HashMap::new(), false);
+        let selected_proofs = Wallet::select_proofs(
+            8.into(),
+            proofs,
+            &vec![active_id],
+            &fee_and_amounts_ppk,
+            false,
+        );
         assert!(selected_proofs.is_err());
     }
 
@@ -528,8 +568,22 @@ mod tests {
             proof(32),
             proof(64),
         ];
-        let mut selected_proofs =
-            Wallet::select_proofs(77.into(), proofs, &vec![id()], &HashMap::new(), false).unwrap();
+
+        let active_id = id();
+        let mut fee_and_amounts_ppk = HashMap::new();
+        fee_and_amounts_ppk.insert(
+            active_id,
+            (0, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+        );
+
+        let mut selected_proofs = Wallet::select_proofs(
+            77.into(),
+            proofs,
+            &vec![active_id],
+            &fee_and_amounts_ppk,
+            false,
+        )
+        .unwrap();
         selected_proofs.sort();
         assert_eq!(selected_proofs.len(), 4);
         assert_eq!(selected_proofs[0].amount, 1.into());
@@ -540,9 +594,21 @@ 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(
+            active_id,
+            (0, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+        );
         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();
+        let selected_proofs = Wallet::select_proofs(
+            31.into(),
+            proofs,
+            &vec![active_id],
+            &fee_and_amounts_ppk,
+            false,
+        )
+        .unwrap();
         assert_eq!(selected_proofs.len(), 1);
         assert_eq!(selected_proofs[0].amount, 32.into());
     }
@@ -550,8 +616,21 @@ mod tests {
     #[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();
+        let active_id = id();
+        let mut fee_and_amounts_ppk = HashMap::new();
+        fee_and_amounts_ppk.insert(
+            active_id,
+            (0, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+        );
+
+        let selected_proofs = Wallet::select_proofs(
+            23.into(),
+            proofs,
+            &vec![active_id],
+            &fee_and_amounts_ppk,
+            false,
+        )
+        .unwrap();
         assert_eq!(selected_proofs.len(), 2);
         assert_eq!(selected_proofs[0].amount, 16.into());
         assert_eq!(selected_proofs[1].amount, 8.into());
@@ -559,10 +638,21 @@ 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(
+            active_id,
+            (0, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+        );
         let proofs = (0..1024).map(|_| proof(1)).collect::<Vec<_>>();
-        let selected_proofs =
-            Wallet::select_proofs(1024.into(), proofs, &vec![id()], &HashMap::new(), false)
-                .unwrap();
+        let selected_proofs = Wallet::select_proofs(
+            1024.into(),
+            proofs,
+            &vec![active_id],
+            &fee_and_amounts_ppk,
+            false,
+        )
+        .unwrap();
         assert_eq!(selected_proofs.len(), 1024);
         selected_proofs
             .iter()
@@ -571,10 +661,21 @@ 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(
+            active_id,
+            (0, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+        );
         let proofs = vec![proof(64), proof(4), proof(32)];
-        let (selected_proofs, exchange) =
-            Wallet::select_exact_proofs(97.into(), proofs, &vec![id()], &HashMap::new(), false)
-                .unwrap();
+        let (selected_proofs, exchange) = Wallet::select_exact_proofs(
+            97.into(),
+            proofs,
+            &vec![active_id],
+            &fee_and_amounts_ppk,
+            false,
+        )
+        .unwrap();
         assert!(exchange.is_some());
         let (proof_to_exchange, amount) = exchange.unwrap();
 
@@ -585,14 +686,20 @@ 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(
+            active_id,
+            (0, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()),
+        );
         let proofs = (0..32)
             .flat_map(|i| (0..5).map(|_| proof(1 << i)).collect::<Vec<_>>())
             .collect::<Vec<_>>();
         let mut selected_proofs = Wallet::select_proofs(
             ((1u64 << 32) - 1).into(),
             proofs,
-            &vec![id()],
-            &HashMap::new(),
+            &vec![active_id],
+            &fee_and_amounts_ppk,
             false,
         )
         .unwrap();
@@ -609,7 +716,7 @@ mod tests {
     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);
+        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();
         assert_eq!(selected_proofs.len(), 1);

+ 14 - 6
crates/cdk/src/wallet/send.rs

@@ -39,7 +39,7 @@ impl Wallet {
         }
 
         // Get keyset fees from localstore
-        let keyset_fees = self.get_keyset_fees().await?;
+        let keyset_fees = self.get_keyset_fees_and_amounts().await?;
 
         // Get available proofs matching conditions
         let mut available_proofs = self
@@ -129,11 +129,16 @@ impl Wallet {
         force_swap: bool,
     ) -> Result<PreparedSend, Error> {
         // Split amount with fee if necessary
+        let active_keyset_id = self.get_active_keyset().await?.id;
+        let (_, amounts_ppk) = self
+            .get_keyset_fees_and_amounts_by_id(active_keyset_id)
+            .await?;
         let (send_amounts, send_fee) = if opts.include_fee {
-            let active_keyset_id = self.get_active_keyset().await?.id;
-            let keyset_fee_ppk = self.get_keyset_fees_by_id(active_keyset_id).await?;
+            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)?;
+            let send_split = amount.split_with_fee(keyset_fee_ppk, &amounts_ppk)?;
             let send_fee = self
                 .get_proofs_fee_by_count(
                     vec![(active_keyset_id, send_split.len() as u64)]
@@ -143,7 +148,7 @@ impl Wallet {
                 .await?;
             (send_split, send_fee)
         } else {
-            let send_split = amount.split();
+            let send_split = amount.split(&amounts_ppk);
             let send_fee = Amount::ZERO;
             (send_split, send_fee)
         };
@@ -265,7 +270,10 @@ impl PreparedSend {
         tracing::debug!("Active keyset ID: {:?}", active_keyset_id);
 
         // Get keyset fees
-        let keyset_fee_ppk = self.wallet.get_keyset_fees_by_id(active_keyset_id).await?;
+        let keyset_fee_ppk = self
+            .wallet
+            .get_keyset_fees_and_amounts_by_id(active_keyset_id)
+            .await?;
         tracing::debug!("Keyset fees: {:?}", keyset_fee_ppk);
 
         // Calculate total send amount

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

@@ -40,6 +40,9 @@ 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
+            .get_keyset_fees_and_amounts_by_id(active_keyset_id)
+            .await?;
 
         let active_keys = self
             .localstore
@@ -74,7 +77,8 @@ 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)?;
+                        let mut amount_split =
+                            amount.split_targeted(&amount_split_target, &amounts_ppk)?;
 
                         for proof in all_proofs {
                             if let Some(idx) = amount_split.iter().position(|&a| a == proof.amount)
@@ -172,7 +176,7 @@ impl Wallet {
             .map(|k| k.id)
             .collect();
 
-        let keyset_fees = self.get_keyset_fees().await?;
+        let keyset_fees = self.get_keyset_fees_and_amounts().await?;
         let proofs = Wallet::select_proofs(
             amount,
             available_proofs,
@@ -224,11 +228,15 @@ impl Wallet {
             .checked_sub(total_to_subtract)
             .ok_or(Error::InsufficientFunds)?;
 
+        let (_, amounts_ppk) = self
+            .get_keyset_fees_and_amounts_by_id(active_keyset_id)
+            .await?;
+
         let (send_amount, change_amount) = match include_fees {
             true => {
                 let split_count = amount
                     .unwrap_or(Amount::ZERO)
-                    .split_targeted(&SplitTarget::default())
+                    .split_targeted(&SplitTarget::default(), &amounts_ppk)
                     .unwrap()
                     .len();
 
@@ -251,7 +259,10 @@ impl Wallet {
         // If a non None split target is passed use that
         // else use state refill
         let change_split_target = match amount_split_target {
-            SplitTarget::None => self.determine_split_target_values(change_amount).await?,
+            SplitTarget::None => {
+                self.determine_split_target_values(change_amount, &amounts_ppk)
+                    .await?
+            }
             s => s,
         };
 
@@ -261,15 +272,19 @@ impl Wallet {
         let total_secrets_needed = match spending_conditions {
             Some(_) => {
                 // For spending conditions, we only need to count change secrets
-                change_amount.split_targeted(&change_split_target)?.len() as u32
+                change_amount
+                    .split_targeted(&change_split_target, &amounts_ppk)?
+                    .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())?
+                    .split_targeted(&SplitTarget::default(), &amounts_ppk)?
+                    .len() as u32;
+                let change_count = change_amount
+                    .split_targeted(&change_split_target, &amounts_ppk)?
                     .len() as u32;
-                let change_count = change_amount.split_targeted(&change_split_target)?.len() as u32;
                 send_count + change_count
             }
         };
@@ -302,6 +317,7 @@ impl Wallet {
                     &self.seed,
                     change_amount,
                     &change_split_target,
+                    &amounts_ppk,
                 )?;
 
                 derived_secret_count = change_premint_secrets.len();
@@ -312,6 +328,7 @@ impl Wallet {
                         send_amount.unwrap_or(Amount::ZERO),
                         &SplitTarget::default(),
                         &conditions,
+                        &amounts_ppk,
                     )?,
                     change_premint_secrets,
                 )
@@ -323,6 +340,7 @@ impl Wallet {
                     &self.seed,
                     send_amount.unwrap_or(Amount::ZERO),
                     &SplitTarget::default(),
+                    &amounts_ppk,
                 )?;
 
                 count += premint_secrets.len() as u32;
@@ -333,6 +351,7 @@ impl Wallet {
                     &self.seed,
                     change_amount,
                     &change_split_target,
+                    &amounts_ppk,
                 )?;
 
                 derived_secret_count = change_premint_secrets.len() + premint_secrets.len();