Forráskód Böngészése

Fix restore pending (#1546)

* feat: restore split by state

* fix: explict error if wallet cannot create split

* fix: tests with split amount check
tsk 6 napja
szülő
commit
6bea7fc496

+ 95 - 19
crates/cashu/src/amount.rs

@@ -38,6 +38,9 @@ pub enum Error {
     /// Utf8 parse error
     #[error(transparent)]
     Utf8ParseError(#[from] std::string::FromUtf8Error),
+    /// Cannot represent amount with available denominations
+    #[error("Cannot represent amount {0} with available denominations (got {1})")]
+    CannotSplitAmount(u64, u64),
 }
 
 /// Amount can be any unit
@@ -193,8 +196,11 @@ impl Amount<()> {
     }
 
     /// Split into parts that are powers of two
-    pub fn split(&self, fee_and_amounts: &FeeAndAmounts) -> Vec<Self> {
-        fee_and_amounts
+    ///
+    /// Returns an error if the amount cannot be fully represented
+    /// with the available denominations.
+    pub fn split(&self, fee_and_amounts: &FeeAndAmounts) -> Result<Vec<Self>, Error> {
+        let parts: Vec<Self> = fee_and_amounts
             .amounts
             .iter()
             .rev()
@@ -204,7 +210,14 @@ impl Amount<()> {
                 }
                 (acc, total % amount)
             })
-            .0
+            .0;
+
+        let sum: u64 = parts.iter().map(|a| a.value).sum();
+        if sum != self.value {
+            return Err(Error::CannotSplitAmount(self.value, sum));
+        }
+
+        Ok(parts)
     }
 
     /// Split into parts that are powers of two by target
@@ -214,17 +227,17 @@ impl Amount<()> {
         fee_and_amounts: &FeeAndAmounts,
     ) -> Result<Vec<Self>, Error> {
         let mut parts = match target {
-            SplitTarget::None => self.split(fee_and_amounts),
+            SplitTarget::None => self.split(fee_and_amounts)?,
             SplitTarget::Value(amount) => {
                 if self.le(amount) {
-                    return Ok(self.split(fee_and_amounts));
+                    return 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(fee_and_amounts);
+                let parts_of_value = amount.split(fee_and_amounts)?;
 
                 while parts_total.lt(self) {
                     for part in parts_of_value.iter().copied() {
@@ -233,7 +246,7 @@ impl Amount<()> {
                         } else {
                             let amount_left =
                                 self.checked_sub(parts_total).ok_or(Error::AmountOverflow)?;
-                            parts.extend(amount_left.split(fee_and_amounts));
+                            parts.extend(amount_left.split(fee_and_amounts)?);
                         }
 
                         parts_total = Amount::try_sum(parts.clone().iter().copied())?;
@@ -258,7 +271,7 @@ impl Amount<()> {
                         let extra = self
                             .checked_sub(values_total)
                             .ok_or(Error::AmountOverflow)?;
-                        let mut extra_amount = extra.split(fee_and_amounts);
+                        let mut extra_amount = extra.split(fee_and_amounts)?;
                         let mut values = values.clone();
 
                         values.append(&mut extra_amount);
@@ -274,7 +287,7 @@ impl Amount<()> {
 
     /// Splits amount into powers of two while accounting for the swap fee
     pub fn split_with_fee(&self, fee_and_amounts: &FeeAndAmounts) -> Result<Vec<Self>, Error> {
-        let without_fee_amounts = self.split(fee_and_amounts);
+        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)
@@ -282,7 +295,7 @@ impl Amount<()> {
         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(fee_and_amounts);
+        let split = new_amount.split(fee_and_amounts)?;
         let split_fee_ppk = (split.len() as u64)
             .checked_mul(fee_and_amounts.fee)
             .ok_or(Error::AmountOverflow)?;
@@ -681,24 +694,24 @@ mod tests {
         let fee_and_amounts = (0, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()).into();
 
         assert_eq!(
-            Amount::from(1).split(&fee_and_amounts),
+            Amount::from(1).split(&fee_and_amounts).unwrap(),
             vec![Amount::from(1)]
         );
         assert_eq!(
-            Amount::from(2).split(&fee_and_amounts),
+            Amount::from(2).split(&fee_and_amounts).unwrap(),
             vec![Amount::from(2)]
         );
         assert_eq!(
-            Amount::from(3).split(&fee_and_amounts),
+            Amount::from(3).split(&fee_and_amounts).unwrap(),
             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(&fee_and_amounts), amounts);
+        assert_eq!(Amount::from(11).split(&fee_and_amounts).unwrap(), 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(&fee_and_amounts), amounts);
+        assert_eq!(Amount::from(255).split(&fee_and_amounts).unwrap(), amounts);
     }
 
     #[test]
@@ -1159,17 +1172,17 @@ mod tests {
         let fee_and_amounts = (0, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()).into();
 
         let amount = Amount::from(11);
-        let result = amount.split(&fee_and_amounts);
+        let result = amount.split(&fee_and_amounts).unwrap();
         assert!(!result.is_empty());
         assert_eq!(Amount::try_sum(result.iter().copied()).unwrap(), amount);
 
         let amount = Amount::from(255);
-        let result = amount.split(&fee_and_amounts);
+        let result = amount.split(&fee_and_amounts).unwrap();
         assert!(!result.is_empty());
         assert_eq!(Amount::try_sum(result.iter().copied()).unwrap(), amount);
 
         let amount = Amount::from(7);
-        let result = amount.split(&fee_and_amounts);
+        let result = amount.split(&fee_and_amounts).unwrap();
         assert_eq!(
             result,
             vec![Amount::from(4), Amount::from(2), Amount::from(1)]
@@ -1191,7 +1204,7 @@ mod tests {
         let fee_and_amounts = (0, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()).into();
 
         let amount = Amount::from(15);
-        let result = amount.split(&fee_and_amounts);
+        let result = amount.split(&fee_and_amounts).unwrap();
 
         assert_eq!(
             result,
@@ -1207,6 +1220,69 @@ mod tests {
         assert_eq!(total, amount);
     }
 
+    /// Tests that split returns an error when the amount cannot be represented
+    /// with the available denominations.
+    #[test]
+    fn test_split_cannot_represent_amount() {
+        // Only denomination 32 available - the split algorithm can only use each denomination once
+        let fee_and_amounts: FeeAndAmounts = (0, vec![32]).into();
+
+        // 100 cannot be exactly represented: 100 >= 32, push(32), 100 % 32 = 4, result = [32]
+        let amount = Amount::from(100);
+        let result = amount.split(&fee_and_amounts);
+        assert!(result.is_err());
+        match result {
+            Err(Error::CannotSplitAmount(requested, got)) => {
+                assert_eq!(requested, 100);
+                assert_eq!(got, 32); // Only one 32 can be taken
+            }
+            _ => panic!("Expected CannotSplitAmount error"),
+        }
+
+        // 32 can be exactly represented
+        let amount = Amount::from(32);
+        let result = amount.split(&fee_and_amounts);
+        assert!(result.is_ok());
+        assert_eq!(result.unwrap(), vec![Amount::from(32)]);
+
+        // Missing denominations: only have 32 and 64, trying to split 100
+        // 100 >= 64, push(64), 100 % 64 = 36
+        // 36 >= 32, push(32), 36 % 32 = 4
+        // Result: [64, 32] = 96, missing 4
+        let fee_and_amounts: FeeAndAmounts = (0, vec![32, 64]).into();
+        let amount = Amount::from(100);
+        let result = amount.split(&fee_and_amounts);
+        assert!(result.is_err());
+        match result {
+            Err(Error::CannotSplitAmount(requested, got)) => {
+                assert_eq!(requested, 100);
+                assert_eq!(got, 96);
+            }
+            _ => panic!("Expected CannotSplitAmount error"),
+        }
+    }
+
+    #[test]
+    fn test_split_amount_exceeds_keyset_capacity() {
+        // Keyset with denominations 2^0 to 2^31
+        let fee_and_amounts = (0, (0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>()).into();
+
+        // Attempt to split 2^63 (way larger than sum of keyset)
+        let amount = Amount::from(2u64.pow(63));
+        let result = amount.split(&fee_and_amounts);
+
+        assert!(result.is_err());
+        match result {
+            Err(Error::CannotSplitAmount(requested, got)) => {
+                assert_eq!(requested, 2u64.pow(63));
+                // The algorithm greedily takes 2^31, and since 2^63 % 2^31 == 0, it stops there.
+                // So "got" should be 2^31.
+                assert_eq!(got, 2u64.pow(31));
+            }
+            _ => panic!("Expected CannotSplitAmount error, got {:?}", result),
+        }
+    }
+
     /// Tests that From<u64> correctly converts values to Amount.
     ///
     /// This conversion is used throughout the codebase including in loops and split operations.

+ 4 - 2
crates/cdk-cli/src/sub_commands/restore.rs

@@ -27,9 +27,11 @@ pub async fn restore(
         }
     };
 
-    let amount = wallet.restore().await?;
+    let restored = wallet.restore().await?;
 
-    println!("Restored {amount}");
+    println!("Restored: {}", restored.unspent);
+    println!("Spent: {}", restored.spent);
+    println!("Pending: {}", restored.pending);
 
     Ok(())
 }

+ 3 - 3
crates/cdk-ffi/src/multi_mint_wallet.rs

@@ -286,10 +286,10 @@ impl MultiMintWallet {
     }
 
     /// Restore wallets for a specific mint
-    pub async fn restore(&self, mint_url: MintUrl) -> Result<Amount, FfiError> {
+    pub async fn restore(&self, mint_url: MintUrl) -> Result<Restored, FfiError> {
         let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
-        let amount = self.inner.restore(&cdk_mint_url).await?;
-        Ok(amount.into())
+        let restored = self.inner.restore(&cdk_mint_url).await?;
+        Ok(restored.into())
     }
 
     /// Prepare a send operation from a specific mint

+ 18 - 0
crates/cdk-ffi/src/types/wallet.rs

@@ -462,3 +462,21 @@ impl From<cdk::nuts::MeltOptions> for MeltOptions {
         }
     }
 }
+
+/// Restored Data
+#[derive(Debug, Clone, uniffi::Record)]
+pub struct Restored {
+    pub spent: Amount,
+    pub unspent: Amount,
+    pub pending: Amount,
+}
+
+impl From<cdk::wallet::Restored> for Restored {
+    fn from(restored: cdk::wallet::Restored) -> Self {
+        Self {
+            spent: restored.spent.into(),
+            unspent: restored.unspent.into(),
+            pending: restored.pending.into(),
+        }
+    }
+}

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

@@ -138,9 +138,9 @@ impl Wallet {
     }
 
     /// Restore wallet from seed
-    pub async fn restore(&self) -> Result<Amount, FfiError> {
-        let amount = self.inner.restore().await?;
-        Ok(amount.into())
+    pub async fn restore(&self) -> Result<Restored, FfiError> {
+        let restored = self.inner.restore().await?;
+        Ok(restored.into())
     }
 
     /// Verify token DLEQ proofs

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

@@ -336,7 +336,7 @@ async fn test_restore() {
         .await
         .unwrap();
 
-    assert_eq!(restored, 100.into());
+    assert_eq!(restored.unspent, 100.into());
 
     // Since we have to do a swap we expect to restore amount - fee
     assert_eq!(
@@ -428,7 +428,7 @@ async fn test_restore_large_proof_count() {
     let proofs = wallet_2.get_unspent_proofs().await.unwrap();
 
     assert_eq!(proofs.len(), mint_amount as usize);
-    assert_eq!(restored, mint_amount.into());
+    assert_eq!(restored.unspent, mint_amount.into());
 
     // Swap in batches to avoid exceeding the 1000 input limit per request
     let mut total_fee = Amount::ZERO;
@@ -551,7 +551,7 @@ async fn test_restore_with_counter_gap() {
 
     // Restore the wallet - this should find proofs at non-sequential counter positions
     let restored = wallet_restored.restore().await.unwrap();
-    assert_eq!(restored, 200.into());
+    assert_eq!(restored.unspent, 200.into());
 
     let proofs = wallet_restored.get_unspent_proofs().await.unwrap();
     assert!(!proofs.is_empty());

+ 9 - 13
crates/cdk-integration-tests/tests/integration_tests_pure.rs

@@ -317,28 +317,24 @@ 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(
+    let pre_mint_amount = PreMintSecrets::from_secrets(
         keyset_id,
-        amount.into(),
-        &SplitTarget::default(),
-        &fee_and_amounts,
+        vec![amount.into()],
+        vec![cashu::secret::Secret::generate()],
     )
     .unwrap();
-    let pre_mint_amount_two = PreMintSecrets::random(
+    let pre_mint_amount_two = PreMintSecrets::from_secrets(
         keyset_id,
-        amount.into(),
-        &SplitTarget::default(),
-        &fee_and_amounts,
+        vec![amount.into()],
+        vec![cashu::secret::Secret::generate()],
     )
     .unwrap();
 
-    let mut pre_mint = PreMintSecrets::random(
+    let mut pre_mint = PreMintSecrets::from_secrets(
         keyset_id,
-        1.into(),
-        &SplitTarget::default(),
-        &fee_and_amounts,
+        vec![1.into()],
+        vec![cashu::secret::Secret::generate()],
     )
     .unwrap();
 

+ 1 - 1
crates/cdk-integration-tests/tests/multi_mint_wallet.rs

@@ -576,7 +576,7 @@ async fn test_multi_mint_wallet_restore() {
 
     // Restore from mint
     let restored = wallet2.restore(&mint_url).await.unwrap();
-    assert_eq!(restored, 100.into(), "Should restore 100 sats");
+    assert_eq!(restored.unspent, 100.into(), "Should restore 100 sats");
 }
 
 /// Test melt_with_mint() with explicit mint selection

+ 9 - 13
crates/cdk-integration-tests/tests/test_swap_flow.rs

@@ -1248,33 +1248,29 @@ async fn test_swap_amount_overflow_protection() {
         .expect("Could not get proofs");
 
     let keyset_id = get_keyset_id(&mint).await;
-    let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
 
     // Try to create outputs that would overflow
     // 2^63 + 2^63 + small amount would overflow u64
     let large_amount = 2_u64.pow(63);
 
-    let pre_mint1 = PreMintSecrets::random(
+    let pre_mint1 = PreMintSecrets::from_secrets(
         keyset_id,
-        large_amount.into(),
-        &SplitTarget::default(),
-        &fee_and_amounts,
+        vec![large_amount.into()],
+        vec![cashu::secret::Secret::generate()],
     )
     .expect("Failed to create pre_mint1");
 
-    let pre_mint2 = PreMintSecrets::random(
+    let pre_mint2 = PreMintSecrets::from_secrets(
         keyset_id,
-        large_amount.into(),
-        &SplitTarget::default(),
-        &fee_and_amounts,
+        vec![large_amount.into()],
+        vec![cashu::secret::Secret::generate()],
     )
     .expect("Failed to create pre_mint2");
 
-    let mut combined_pre_mint = PreMintSecrets::random(
+    let mut combined_pre_mint = PreMintSecrets::from_secrets(
         keyset_id,
-        1.into(),
-        &SplitTarget::default(),
-        &fee_and_amounts,
+        vec![1.into()],
+        vec![cashu::secret::Secret::generate()],
     )
     .expect("Failed to create combined_pre_mint");
 

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

@@ -233,7 +233,7 @@ pub async fn process_melt_change(
     let fee_and_amounts = get_keyset_fee_and_amounts(&mint.keysets, &change_outputs);
 
     // Split change into denominations
-    let mut amounts: Vec<Amount> = change_target.split(&fee_and_amounts);
+    let mut amounts: Vec<Amount> = change_target.split(&fee_and_amounts)?;
 
     if change_outputs.len() < amounts.len() {
         tracing::debug!(

+ 2 - 2
crates/cdk/src/wallet/melt/bolt11.rs

@@ -425,7 +425,7 @@ impl Wallet {
 
         // Calculate optimal denomination split and the fee for those proofs
         // First estimate based on inputs_needed_amount to get target_fee
-        let initial_split = inputs_needed_amount.split(&fee_and_amounts);
+        let initial_split = inputs_needed_amount.split(&fee_and_amounts)?;
         let target_fee = self
             .get_proofs_fee_by_count(
                 vec![(active_keyset_id, initial_split.len() as u64)]
@@ -440,7 +440,7 @@ impl Wallet {
         let inputs_total_needed = inputs_needed_amount + target_fee;
 
         // Recalculate target amounts based on the actual total we need (including fee)
-        let target_amounts = inputs_total_needed.split(&fee_and_amounts);
+        let target_amounts = inputs_total_needed.split(&fee_and_amounts)?;
         let input_proofs = Wallet::select_proofs(
             inputs_total_needed,
             available_proofs,

+ 1 - 0
crates/cdk/src/wallet/mint_connector/transport.rs

@@ -209,6 +209,7 @@ impl Transport for Async {
 
         serde_json::from_str::<R>(&response).map_err(|err| {
             tracing::warn!("Http Response error: {}", err);
+            tracing::debug!("{:?}", response);
             match ErrorResponse::from_json(&response) {
                 Ok(ok) => <ErrorResponse as Into<Error>>::into(ok),
                 Err(err) => err.into(),

+ 44 - 23
crates/cdk/src/wallet/mod.rs

@@ -26,7 +26,7 @@ use crate::mint_url::MintUrl;
 use crate::nuts::nut00::token::Token;
 use crate::nuts::nut17::Kind;
 use crate::nuts::{
-    nut10, CurrencyUnit, Id, Keys, MintInfo, MintQuoteState, PreMintSecrets, Proof, Proofs,
+    nut10, CurrencyUnit, Id, Keys, MintInfo, MintQuoteState, PreMintSecrets, Proofs,
     RestoreRequest, SpendingConditions, State,
 };
 use crate::types::ProofInfo;
@@ -171,6 +171,17 @@ impl From<WalletSubscription> for WalletParams {
     }
 }
 
+/// Amount that are recovered during restore operation
+#[derive(Debug, Hash, PartialEq, Eq, Default)]
+pub struct Restored {
+    /// Amount in the restore that has already been spent
+    pub spent: Amount,
+    /// Amount restored that is unspent
+    pub unspent: Amount,
+    /// Amount restored that is pending
+    pub pending: Amount,
+}
+
 impl Wallet {
     /// Create new [`Wallet`] using the builder pattern
     /// # Synopsis
@@ -447,7 +458,7 @@ impl Wallet {
 
     /// Restore
     #[instrument(skip(self))]
-    pub async fn restore(&self) -> Result<Amount, Error> {
+    pub async fn restore(&self) -> Result<Restored, Error> {
         // Check that mint is in store of mints
         if self
             .localstore
@@ -460,7 +471,7 @@ impl Wallet {
 
         let keysets = self.load_mint_keysets().await?;
 
-        let mut restored_value = Amount::ZERO;
+        let mut restored_result = Restored::default();
 
         for keyset in keysets {
             let keys = self.load_keyset_keys(keyset.id).await?;
@@ -558,27 +569,37 @@ impl Wallet {
 
                 let states = self.check_proofs_spent(proofs.clone()).await?;
 
-                let unspent_proofs: Vec<Proof> = proofs
-                    .iter()
-                    .zip(states)
-                    .filter(|(_, state)| !state.state.eq(&State::Spent))
-                    .map(|(p, _)| p)
-                    .cloned()
-                    .collect();
-
-                restored_value += unspent_proofs.total_amount()?;
-
-                let unspent_proofs = unspent_proofs
+                let (unspent_proofs, updated_restored) = proofs
                     .into_iter()
-                    .map(|proof| {
-                        ProofInfo::new(
-                            proof,
-                            self.mint_url.clone(),
-                            State::Unspent,
-                            keyset.unit.clone(),
-                        )
+                    .zip(states)
+                    .filter_map(|(p, state)| {
+                        ProofInfo::new(p, self.mint_url.clone(), state.state, keyset.unit.clone())
+                            .ok()
                     })
-                    .collect::<Result<Vec<ProofInfo>, _>>()?;
+                    .try_fold(
+                        (Vec::new(), restored_result),
+                        |(mut proofs, mut restored_result), proof_info| {
+                            match proof_info.state {
+                                State::Spent => {
+                                    restored_result.spent += proof_info.proof.amount;
+                                }
+                                State::Unspent =>  {
+                                    restored_result.unspent += proof_info.proof.amount;
+                                    proofs.push(proof_info);
+                                }
+                                State::Pending => {
+                                    restored_result.pending += proof_info.proof.amount;
+                                    proofs.push(proof_info);
+                                }
+                                _ => {
+                                    unreachable!("These states are unknown to the mint and cannot be returned")
+                                }
+                            }
+                            Ok::<(Vec<ProofInfo>, Restored), Error>((proofs, restored_result))
+                        },
+                    )?;
+
+                restored_result = updated_restored;
 
                 self.localstore
                     .update_proofs(unspent_proofs, vec![])
@@ -601,7 +622,7 @@ impl Wallet {
                 );
             }
         }
-        Ok(restored_value)
+        Ok(restored_result)
     }
 
     /// Verify all proofs in token have meet the required spend

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

@@ -20,7 +20,7 @@ use zeroize::Zeroize;
 use super::builder::WalletBuilder;
 use super::receive::ReceiveOptions;
 use super::send::{PreparedSend, SendOptions};
-use super::Error;
+use super::{Error, Restored};
 use crate::amount::SplitTarget;
 use crate::mint_url::MintUrl;
 use crate::nuts::nut00::ProofsMethods;
@@ -1627,7 +1627,7 @@ impl MultiMintWallet {
 
     /// Restore
     #[instrument(skip(self))]
-    pub async fn restore(&self, mint_url: &MintUrl) -> Result<Amount, Error> {
+    pub async fn restore(&self, mint_url: &MintUrl) -> Result<Restored, Error> {
         let wallets = self.wallets.read().await;
         let wallet = wallets.get(mint_url).ok_or(Error::UnknownMint {
             mint_url: mint_url.to_string(),

+ 1 - 1
crates/cdk/src/wallet/proofs.rs

@@ -307,7 +307,7 @@ impl Wallet {
 
         // Select proofs with the optimal amounts (only split once, not per keyset)
         if let Some(fee_and_amounts) = fee_and_amounts {
-            for optimal_amount in amount.split(fee_and_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);

+ 1 - 1
crates/cdk/src/wallet/send.rs

@@ -173,7 +173,7 @@ impl Wallet {
                 .await?;
             (send_split, send_fee)
         } else {
-            let send_split = amount.split(&fee_and_amounts);
+            let send_split = amount.split(&fee_and_amounts)?;
             let send_fee = crate::fees::ProofsFeeBreakdown {
                 total: Amount::ZERO,
                 per_keyset: std::collections::HashMap::new(),