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
This commit is contained in:
thesimplekid
2025-09-16 17:26:19 +01:00
committed by GitHub
parent 9d25d963ee
commit 049ce64462
4 changed files with 72 additions and 51 deletions

View File

@@ -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

View File

@@ -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))
} 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);
}
});

View File

@@ -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),
};

View File

@@ -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,