mirror of
https://github.com/aljazceru/cdk.git
synced 2026-02-02 11:45:51 +01:00
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:
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user