diff --git a/core/archipelago/src/wallet/ecash.rs b/core/archipelago/src/wallet/ecash.rs index 49b14d45..c374ee3f 100644 --- a/core/archipelago/src/wallet/ecash.rs +++ b/core/archipelago/src/wallet/ecash.rs @@ -632,11 +632,30 @@ pub async fn send_token_at(data_dir: &Path, mint_url: &str, amount_sats: u64) -> // Mark original proofs as spent wallet.mark_spent(&indices); - // Separate send proofs from change proofs - let (send, change): (Vec<_>, Vec<_>) = swap_result - .new_proofs - .into_iter() - .partition(|p| send_denoms.contains(&p.amount)); + // Separate send proofs from change proofs BY COUNT, not membership: + // partition(contains) put EVERY proof whose denomination appeared in + // send_denoms into the token — when change shared a denomination with + // the send (worst case: spending from a proof worth exactly 2× the + // amount, send [n] + change [n]), the change proofs rode along and the + // receiver was credited double. Consume exactly one proof per needed + // send denomination; everything else is change. + let mut send_needed = send_denoms.clone(); + let mut send: Vec = Vec::new(); + let mut change: Vec = Vec::new(); + for p in swap_result.new_proofs { + if let Some(pos) = send_needed.iter().position(|&d| d == p.amount) { + send_needed.swap_remove(pos); + send.push(p); + } else { + change.push(p); + } + } + if !send_needed.is_empty() { + anyhow::bail!( + "Mint swap returned incomplete send denominations (missing {:?})", + send_needed + ); + } // Add change proofs back to wallet if !change.is_empty() {