Преглед на файлове

feat(cdk): add amount_mintable method and improve mint quote validation (#1075)

* feat(cdk): add amount_mintable method and improve mint quote validation

- Add MintQuote::amount_mintable() method to calculate available mint amount
- Update mint issue logic to use centralized amount calculation
- Add validation for Bolt11 payment amounts matching quote amounts
- Improve error handling and logging for quote amount mismatches
thesimplekid преди 1 месец
родител
ревизия
049ce64462
променени са 4 файла, в които са добавени 71 реда и са изтрити 50 реда
  1. 10 0
      crates/cdk-common/src/mint.rs
  2. 40 46
      crates/cdk-fake-wallet/src/lib.rs
  3. 14 4
      crates/cdk/src/mint/issue/mod.rs
  4. 7 0
      crates/cdk/src/mint/mod.rs

+ 10 - 0
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

+ 40 - 46
crates/cdk-fake-wallet/src/lib.rs

@@ -55,19 +55,22 @@ const DEFAULT_REPAY_QUEUE_MAX_SIZE: usize = 100;
 struct SecondaryRepaymentQueue {
     queue: Arc<Mutex<VecDeque<PaymentIdentifier>>>,
     max_size: usize,
-    sender: tokio::sync::mpsc::Sender<(PaymentIdentifier, Amount, String)>,
+    sender: tokio::sync::mpsc::Sender<WaitPaymentResponse>,
+    unit: CurrencyUnit,
 }
 
 impl SecondaryRepaymentQueue {
     fn new(
         max_size: usize,
-        sender: tokio::sync::mpsc::Sender<(PaymentIdentifier, Amount, String)>,
+        sender: tokio::sync::mpsc::Sender<WaitPaymentResponse>,
+        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<Mutex<Option<tokio::sync::mpsc::Receiver<(PaymentIdentifier, Amount, String)>>>>,
+    sender: tokio::sync::mpsc::Sender<WaitPaymentResponse>,
+    receiver: Arc<Mutex<Option<tokio::sync::mpsc::Receiver<WaitPaymentResponse>>>>,
     payment_states: Arc<Mutex<HashMap<String, MeltQuoteState>>>,
     failed_payment_check: Arc<Mutex<HashSet<String>>>,
     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))
+                let amount = bolt11_options.amount;
+                let expiry = bolt11_options.unix_expiry;
+
+                // For fake invoices, always use msats regardless of unit
+                let amount_msat = if unit == &CurrencyUnit::Sat {
+                    u64::from(amount) * 1000
                 } else {
-                    bolt11_options.amount
+                    // If unit is Msat, use as-is
+                    u64::from(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());
+                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);
             }
         });

+ 14 - 4
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),
         };

+ 7 - 0
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,