diff --git a/crates/cdk-common/src/mint.rs b/crates/cdk-common/src/mint.rs index a6d3d91e..f75cf120 100644 --- a/crates/cdk-common/src/mint.rs +++ b/crates/cdk-common/src/mint.rs @@ -139,6 +139,16 @@ impl MintQuote { self.payments.iter().map(|a| &a.payment_id).collect() } + /// Amount mintable + /// Returns the amount that is still available for minting. + /// + /// The value is computed as the difference between the total amount that + /// has been paid for this issuance (`self.amount_paid`) and the amount + /// that has already been issued (`self.amount_issued`). In other words, + pub fn amount_mintable(&self) -> Amount { + self.amount_paid - self.amount_issued + } + /// Add a payment ID to the list of payment IDs /// /// Returns an error if the payment ID is already in the list diff --git a/crates/cdk-fake-wallet/src/lib.rs b/crates/cdk-fake-wallet/src/lib.rs index a5b0f344..58630d14 100644 --- a/crates/cdk-fake-wallet/src/lib.rs +++ b/crates/cdk-fake-wallet/src/lib.rs @@ -55,19 +55,22 @@ const DEFAULT_REPAY_QUEUE_MAX_SIZE: usize = 100; struct SecondaryRepaymentQueue { queue: Arc>>, max_size: usize, - sender: tokio::sync::mpsc::Sender<(PaymentIdentifier, Amount, String)>, + sender: tokio::sync::mpsc::Sender, + unit: CurrencyUnit, } impl SecondaryRepaymentQueue { fn new( max_size: usize, - sender: tokio::sync::mpsc::Sender<(PaymentIdentifier, Amount, String)>, + sender: tokio::sync::mpsc::Sender, + unit: CurrencyUnit, ) -> Self { let queue = Arc::new(Mutex::new(VecDeque::new())); let repayment_queue = Self { queue: queue.clone(), max_size, sender, + unit, }; // Start the background secondary repayment processor @@ -101,6 +104,7 @@ impl SecondaryRepaymentQueue { fn start_secondary_repayment_processor(&self) { let queue = self.queue.clone(); let sender = self.sender.clone(); + let unit = self.unit.clone(); tokio::spawn(async move { use bitcoin::secp256k1::rand::rngs::OsRng; @@ -127,7 +131,13 @@ impl SecondaryRepaymentQueue { if let Some(payment) = payment_to_process { // Generate a random amount for this secondary payment (same range as initial payment: 1-1000) let random_amount: u64 = rng.gen_range(1..=1000); - let secondary_amount = Amount::from(random_amount); + + // Create amount based on unit, ensuring minimum of 1 sat worth + let secondary_amount = match &unit { + CurrencyUnit::Sat => Amount::from(random_amount), + CurrencyUnit::Msat => Amount::from(u64::max(random_amount * 1000, 1000)), + _ => Amount::from(u64::max(random_amount, 1)), // fallback + }; // Generate a unique payment identifier for this secondary payment // We'll create a new payment hash by appending a timestamp and random bytes @@ -157,14 +167,14 @@ impl SecondaryRepaymentQueue { // Send the payment notification using the original payment identifier // The mint will process this through the normal payment stream - if let Err(e) = sender - .send(( - payment.clone(), - secondary_amount, - unique_payment_id.to_string(), - )) - .await - { + let secondary_response = WaitPaymentResponse { + payment_identifier: payment.clone(), + payment_amount: secondary_amount, + unit: unit.clone(), + payment_id: unique_payment_id.to_string(), + }; + + if let Err(e) = sender.send(secondary_response).await { tracing::error!( "Failed to send secondary repayment notification for {:?}: {}", unique_payment_id, @@ -181,10 +191,8 @@ impl SecondaryRepaymentQueue { #[derive(Clone)] pub struct FakeWallet { fee_reserve: FeeReserve, - #[allow(clippy::type_complexity)] - sender: tokio::sync::mpsc::Sender<(PaymentIdentifier, Amount, String)>, - #[allow(clippy::type_complexity)] - receiver: Arc>>>, + sender: tokio::sync::mpsc::Sender, + receiver: Arc>>>, payment_states: Arc>>, failed_payment_check: Arc>>, payment_delay: u64, @@ -227,7 +235,7 @@ impl FakeWallet { let incoming_payments = Arc::new(RwLock::new(HashMap::new())); let secondary_repayment_queue = - SecondaryRepaymentQueue::new(repay_queue_max_size, sender.clone()); + SecondaryRepaymentQueue::new(repay_queue_max_size, sender.clone(), unit.clone()); Self { fee_reserve, @@ -306,19 +314,10 @@ impl MintPayment for FakeWallet { .take() .ok_or(Error::NoReceiver) .unwrap(); - let unit = self.unit.clone(); let receiver_stream = ReceiverStream::new(receiver); - Ok(Box::pin(receiver_stream.map( - move |(request_lookup_id, payment_amount, payment_id)| { - let wait_response = WaitPaymentResponse { - payment_identifier: request_lookup_id.clone(), - payment_amount, - unit: unit.clone(), - payment_id, - }; - Event::PaymentReceived(wait_response) - }, - ))) + Ok(Box::pin(receiver_stream.map(move |wait_response| { + Event::PaymentReceived(wait_response) + }))) } #[instrument(skip_all)] @@ -517,16 +516,18 @@ impl MintPayment for FakeWallet { } IncomingPaymentOptions::Bolt11(bolt11_options) => { let description = bolt11_options.description.unwrap_or_default(); - let amount = if unit == &CurrencyUnit::Sat { - to_unit(bolt11_options.amount, unit, &CurrencyUnit::Msat) - .unwrap_or(bolt11_options.amount * Amount::from(1000)) - } else { - bolt11_options.amount - }; + let amount = bolt11_options.amount; let expiry = bolt11_options.unix_expiry; - // Since this is fake we just use the amount no matter the unit to create an invoice - let invoice = create_fake_invoice(amount.into(), description.clone()); + // For fake invoices, always use msats regardless of unit + let amount_msat = if unit == &CurrencyUnit::Sat { + u64::from(amount) * 1000 + } else { + // If unit is Msat, use as-is + u64::from(amount) + }; + + let invoice = create_fake_invoice(amount_msat, description.clone()); let payment_hash = invoice.payment_hash(); ( @@ -550,8 +551,9 @@ impl MintPayment for FakeWallet { use bitcoin::secp256k1::rand::rngs::OsRng; use bitcoin::secp256k1::rand::Rng; let mut rng = OsRng; - let random_amount: u64 = rng.gen_range(1..=1000); - random_amount.into() + let random_amount: u64 = rng.gen_range(1000..=10000); + // Use the same unit as the wallet for any-amount invoices + Amount::from(random_amount) } else { amount }; @@ -574,15 +576,7 @@ impl MintPayment for FakeWallet { .push(response.clone()); // Send the message after waiting for the specified duration - if sender - .send(( - payment_hash_clone.clone(), - final_amount, - payment_hash_clone.to_string(), - )) - .await - .is_err() - { + if sender.send(response.clone()).await.is_err() { tracing::error!("Failed to send label: {:?}", payment_hash_clone); } }); diff --git a/crates/cdk/src/mint/issue/mod.rs b/crates/cdk/src/mint/issue/mod.rs index b4c2689a..14e8ce9f 100644 --- a/crates/cdk/src/mint/issue/mod.rs +++ b/crates/cdk/src/mint/issue/mod.rs @@ -624,17 +624,27 @@ impl Mint { } let mint_amount = match mint_quote.payment_method { - PaymentMethod::Bolt11 => mint_quote.amount.ok_or(Error::AmountUndefined)?, + PaymentMethod::Bolt11 => { + let quote_amount = mint_quote.amount.ok_or(Error::AmountUndefined)?; + + if quote_amount != mint_quote.amount_mintable() { + tracing::error!("The quote amount {} does not equal the amount paid {}.", quote_amount, mint_quote.amount_mintable()); + return Err(Error::IncorrectQuoteAmount); + } + + quote_amount + }, PaymentMethod::Bolt12 => { - if mint_quote.amount_issued() > mint_quote.amount_paid() { + if mint_quote.amount_mintable() == Amount::ZERO{ tracing::error!( - "Quote state should not be issued if issued {} is > paid {}.", + "Quote state should not be issued if issued {} is => paid {}.", mint_quote.amount_issued(), mint_quote.amount_paid() ); return Err(Error::UnpaidQuote); } - mint_quote.amount_paid() - mint_quote.amount_issued() + + mint_quote.amount_mintable() } _ => return Err(Error::UnsupportedPaymentMethod), }; diff --git a/crates/cdk/src/mint/mod.rs b/crates/cdk/src/mint/mod.rs index df6c52c1..4b429ca1 100644 --- a/crates/cdk/src/mint/mod.rs +++ b/crates/cdk/src/mint/mod.rs @@ -627,6 +627,13 @@ impl Mint { return Err(Error::AmountUndefined); } + if mint_quote.payment_method == PaymentMethod::Bolt11 + && mint_quote.amount != Some(payment_amount_quote_unit) + { + tracing::error!("Bolt11 incoming payment should equal mint quote."); + return Err(Error::IncorrectQuoteAmount); + } + tracing::debug!( "Payment received amount in quote unit {} {}", mint_quote.unit,