|
@@ -5,8 +5,7 @@
|
|
|
|
|
|
|
|
use std::str::FromStr;
|
|
use std::str::FromStr;
|
|
|
|
|
|
|
|
-use cdk_common::database::Error as DatabaseError;
|
|
|
|
|
-use cdk_common::mint::OperationKind;
|
|
|
|
|
|
|
+use cdk_common::mint::{OperationKind, Saga};
|
|
|
use cdk_common::QuoteId;
|
|
use cdk_common::QuoteId;
|
|
|
|
|
|
|
|
use super::{Error, Mint};
|
|
use super::{Error, Mint};
|
|
@@ -15,6 +14,23 @@ use crate::mint::{MeltQuote, MeltQuoteState};
|
|
|
use crate::types::PaymentProcessorKey;
|
|
use crate::types::PaymentProcessorKey;
|
|
|
|
|
|
|
|
impl Mint {
|
|
impl Mint {
|
|
|
|
|
+ /// Get incomplete melt saga by quote_id
|
|
|
|
|
+ async fn get_melt_saga_by_quote_id(&self, quote_id: &str) -> Result<Option<Saga>, Error> {
|
|
|
|
|
+ let incomplete_sagas = self
|
|
|
|
|
+ .localstore
|
|
|
|
|
+ .get_incomplete_sagas(OperationKind::Melt)
|
|
|
|
|
+ .await?;
|
|
|
|
|
+
|
|
|
|
|
+ for saga in incomplete_sagas {
|
|
|
|
|
+ if let Some(ref qid) = saga.quote_id {
|
|
|
|
|
+ if qid == quote_id {
|
|
|
|
|
+ return Ok(Some(saga));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ Ok(None)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
/// Checks the payment status of a melt quote with the LN backend
|
|
/// Checks the payment status of a melt quote with the LN backend
|
|
|
///
|
|
///
|
|
|
/// This is a helper function used by saga recovery to determine whether to
|
|
/// This is a helper function used by saga recovery to determine whether to
|
|
@@ -145,7 +161,10 @@ impl Mint {
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
// Execute compensation (includes saga deletion)
|
|
// Execute compensation (includes saga deletion)
|
|
|
- if let Err(e) = compensation.execute(&self.localstore).await {
|
|
|
|
|
|
|
+ if let Err(e) = compensation
|
|
|
|
|
+ .execute(&self.localstore, &self.pubsub_manager)
|
|
|
|
|
+ .await
|
|
|
|
|
+ {
|
|
|
tracing::error!(
|
|
tracing::error!(
|
|
|
"Failed to compensate saga {}: {}. Continuing...",
|
|
"Failed to compensate saga {}: {}. Continuing...",
|
|
|
saga.operation_id,
|
|
saga.operation_id,
|
|
@@ -296,7 +315,7 @@ impl Mint {
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
- let quote = match self.localstore.get_melt_quote("e_id_parsed).await {
|
|
|
|
|
|
|
+ let mut quote = match self.localstore.get_melt_quote("e_id_parsed).await {
|
|
|
Ok(Some(q)) => q,
|
|
Ok(Some(q)) => q,
|
|
|
Ok(None) => {
|
|
Ok(None) => {
|
|
|
tracing::warn!(
|
|
tracing::warn!(
|
|
@@ -457,68 +476,54 @@ impl Mint {
|
|
|
Ok(payment_response) => {
|
|
Ok(payment_response) => {
|
|
|
match payment_response.status {
|
|
match payment_response.status {
|
|
|
MeltQuoteState::Paid => {
|
|
MeltQuoteState::Paid => {
|
|
|
- // Payment succeeded - finalize instead of compensating
|
|
|
|
|
- tracing::info!(
|
|
|
|
|
- "Saga {} for quote {} - payment PAID on LN backend, will finalize",
|
|
|
|
|
- saga.operation_id,
|
|
|
|
|
- quote_id
|
|
|
|
|
- );
|
|
|
|
|
-
|
|
|
|
|
- if let Err(err) = self
|
|
|
|
|
- .finalize_paid_melt_quote(
|
|
|
|
|
- "e,
|
|
|
|
|
- payment_response.total_spent,
|
|
|
|
|
- payment_response.payment_proof,
|
|
|
|
|
- &payment_response.payment_lookup_id,
|
|
|
|
|
- )
|
|
|
|
|
- .await
|
|
|
|
|
|
|
+ if let Err(err) = super::saga_recovery::process_melt_saga_outcome(
|
|
|
|
|
+ &saga,
|
|
|
|
|
+ &mut quote,
|
|
|
|
|
+ &payment_response,
|
|
|
|
|
+ &self.localstore,
|
|
|
|
|
+ &self.pubsub_manager,
|
|
|
|
|
+ self,
|
|
|
|
|
+ )
|
|
|
|
|
+ .await
|
|
|
{
|
|
{
|
|
|
tracing::error!(
|
|
tracing::error!(
|
|
|
- "Failed to finalize paid melt saga {}: {}. Will retry on next recovery cycle.",
|
|
|
|
|
|
|
+ "Failed to process paid melt saga {}: {}. Will retry on next recovery cycle.",
|
|
|
saga.operation_id,
|
|
saga.operation_id,
|
|
|
err
|
|
err
|
|
|
);
|
|
);
|
|
|
continue;
|
|
continue;
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- // Delete saga after successful finalization
|
|
|
|
|
- let mut tx = self.localstore.begin_transaction().await?;
|
|
|
|
|
- if let Err(e) = tx.delete_saga(&saga.operation_id).await {
|
|
|
|
|
|
|
+ continue; // Saga handled
|
|
|
|
|
+ }
|
|
|
|
|
+ MeltQuoteState::Unpaid | MeltQuoteState::Failed => {
|
|
|
|
|
+ if let Err(err) = super::saga_recovery::process_melt_saga_outcome(
|
|
|
|
|
+ &saga,
|
|
|
|
|
+ &mut quote,
|
|
|
|
|
+ &payment_response,
|
|
|
|
|
+ &self.localstore,
|
|
|
|
|
+ &self.pubsub_manager,
|
|
|
|
|
+ self,
|
|
|
|
|
+ )
|
|
|
|
|
+ .await
|
|
|
|
|
+ {
|
|
|
tracing::error!(
|
|
tracing::error!(
|
|
|
- "Failed to delete saga {}: {}. Will retry on next recovery cycle.",
|
|
|
|
|
|
|
+ "Failed to process failed melt saga {}: {}. Will retry on next recovery cycle.",
|
|
|
saga.operation_id,
|
|
saga.operation_id,
|
|
|
- e
|
|
|
|
|
|
|
+ err
|
|
|
);
|
|
);
|
|
|
- tx.rollback().await?;
|
|
|
|
|
continue;
|
|
continue;
|
|
|
}
|
|
}
|
|
|
- tx.commit().await?;
|
|
|
|
|
- tracing::info!(
|
|
|
|
|
- "Successfully recovered and finalized melt saga {}",
|
|
|
|
|
- saga.operation_id
|
|
|
|
|
- );
|
|
|
|
|
-
|
|
|
|
|
- continue; // Skip compensation, saga handled
|
|
|
|
|
- }
|
|
|
|
|
- MeltQuoteState::Unpaid | MeltQuoteState::Failed => {
|
|
|
|
|
- // Payment failed - compensate
|
|
|
|
|
- tracing::info!(
|
|
|
|
|
- "Saga {} for quote {} - payment {} on LN backend, will compensate",
|
|
|
|
|
- saga.operation_id,
|
|
|
|
|
- quote_id,
|
|
|
|
|
- payment_response.status
|
|
|
|
|
- );
|
|
|
|
|
- true
|
|
|
|
|
|
|
+ continue; // Saga handled
|
|
|
}
|
|
}
|
|
|
MeltQuoteState::Pending | MeltQuoteState::Unknown => {
|
|
MeltQuoteState::Pending | MeltQuoteState::Unknown => {
|
|
|
// Payment still pending - skip for check_pending_melt_quotes
|
|
// Payment still pending - skip for check_pending_melt_quotes
|
|
|
tracing::info!(
|
|
tracing::info!(
|
|
|
- "Saga {} for quote {} - payment {} on LN backend, skipping (will be handled by check_pending_melt_quotes)",
|
|
|
|
|
|
|
+ "Saga {} for quote {} - payment {} on LN backend, skipping",
|
|
|
saga.operation_id,
|
|
saga.operation_id,
|
|
|
quote_id,
|
|
quote_id,
|
|
|
payment_response.status
|
|
payment_response.status
|
|
|
);
|
|
);
|
|
|
- continue; // Skip this saga, don't compensate or finalize
|
|
|
|
|
|
|
+ continue; // Skip this saga
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -544,112 +549,23 @@ impl Mint {
|
|
|
blinded_secrets.len()
|
|
blinded_secrets.len()
|
|
|
);
|
|
);
|
|
|
|
|
|
|
|
- let mut tx = self.localstore.begin_transaction().await?;
|
|
|
|
|
-
|
|
|
|
|
- // Remove blinded messages (change outputs)
|
|
|
|
|
- if !blinded_secrets.is_empty() {
|
|
|
|
|
- if let Err(e) = tx.delete_blinded_messages(&blinded_secrets).await {
|
|
|
|
|
- tracing::error!(
|
|
|
|
|
- "Failed to delete blinded messages for saga {}: {}",
|
|
|
|
|
- saga.operation_id,
|
|
|
|
|
- e
|
|
|
|
|
- );
|
|
|
|
|
- tx.rollback().await?;
|
|
|
|
|
- continue;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Remove proofs (inputs)
|
|
|
|
|
- if !input_ys.is_empty() {
|
|
|
|
|
- match tx.remove_proofs(&input_ys, None).await {
|
|
|
|
|
- Ok(()) => {}
|
|
|
|
|
- Err(DatabaseError::AttemptRemoveSpentProof) => {
|
|
|
|
|
- // Proofs are already spent or missing - this is okay for compensation.
|
|
|
|
|
- // The goal is to make proofs unusable, and they already are.
|
|
|
|
|
- // Continue with saga deletion to avoid infinite recovery loop.
|
|
|
|
|
- tracing::warn!(
|
|
|
|
|
- "Saga {} compensation: proofs already spent or missing, proceeding with saga cleanup",
|
|
|
|
|
- saga.operation_id
|
|
|
|
|
- );
|
|
|
|
|
- }
|
|
|
|
|
- Err(e) => {
|
|
|
|
|
- tracing::error!(
|
|
|
|
|
- "Failed to remove proofs for saga {}: {}",
|
|
|
|
|
- saga.operation_id,
|
|
|
|
|
- e
|
|
|
|
|
- );
|
|
|
|
|
- tx.rollback().await?;
|
|
|
|
|
- continue;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Reset quote state to Unpaid (melt-specific, unlike swap)
|
|
|
|
|
- // Acquire lock on the quote first
|
|
|
|
|
- let mut locked_quote = match tx.get_melt_quote("e_id_parsed).await {
|
|
|
|
|
- Ok(Some(q)) => q,
|
|
|
|
|
- Ok(None) => {
|
|
|
|
|
- tracing::warn!(
|
|
|
|
|
- "Melt quote {} not found for saga {} - may have been cleaned up",
|
|
|
|
|
- quote_id_parsed,
|
|
|
|
|
- saga.operation_id
|
|
|
|
|
- );
|
|
|
|
|
- // Continue with saga deletion even if quote is gone
|
|
|
|
|
- if let Err(e) = tx.delete_saga(&saga.operation_id).await {
|
|
|
|
|
- tracing::error!("Failed to delete saga {}: {}", saga.operation_id, e);
|
|
|
|
|
- tx.rollback().await?;
|
|
|
|
|
- continue;
|
|
|
|
|
- }
|
|
|
|
|
- tx.commit().await?;
|
|
|
|
|
- continue;
|
|
|
|
|
- }
|
|
|
|
|
- Err(e) => {
|
|
|
|
|
- tracing::error!(
|
|
|
|
|
- "Failed to get quote for saga {}: {}",
|
|
|
|
|
- saga.operation_id,
|
|
|
|
|
- e
|
|
|
|
|
- );
|
|
|
|
|
- tx.rollback().await?;
|
|
|
|
|
- continue;
|
|
|
|
|
- }
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- if let Err(e) = tx
|
|
|
|
|
- .update_melt_quote_state(&mut locked_quote, MeltQuoteState::Unpaid, None)
|
|
|
|
|
- .await
|
|
|
|
|
|
|
+ if let Err(err) = super::melt::shared::rollback_melt_quote(
|
|
|
|
|
+ &self.localstore,
|
|
|
|
|
+ &self.pubsub_manager,
|
|
|
|
|
+ "e_id_parsed,
|
|
|
|
|
+ &input_ys,
|
|
|
|
|
+ &blinded_secrets,
|
|
|
|
|
+ &saga.operation_id,
|
|
|
|
|
+ )
|
|
|
|
|
+ .await
|
|
|
{
|
|
{
|
|
|
tracing::error!(
|
|
tracing::error!(
|
|
|
- "Failed to reset quote state for saga {}: {}",
|
|
|
|
|
- saga.operation_id,
|
|
|
|
|
- e
|
|
|
|
|
- );
|
|
|
|
|
- tx.rollback().await?;
|
|
|
|
|
- continue;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Delete melt request tracking record
|
|
|
|
|
- if let Err(e) = tx.delete_melt_request("e_id_parsed).await {
|
|
|
|
|
- tracing::error!(
|
|
|
|
|
- "Failed to delete melt request for saga {}: {}",
|
|
|
|
|
|
|
+ "Failed to rollback melt quote {} for saga {}: {}",
|
|
|
|
|
+ quote_id_parsed,
|
|
|
saga.operation_id,
|
|
saga.operation_id,
|
|
|
- e
|
|
|
|
|
|
|
+ err
|
|
|
);
|
|
);
|
|
|
- // Don't fail if melt request doesn't exist - it might not have been created yet
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Delete saga after successful compensation
|
|
|
|
|
- if let Err(e) = tx.delete_saga(&saga.operation_id).await {
|
|
|
|
|
- tracing::error!("Failed to delete saga for {}: {}", saga.operation_id, e);
|
|
|
|
|
- tx.rollback().await?;
|
|
|
|
|
- continue;
|
|
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- tx.commit().await?;
|
|
|
|
|
-
|
|
|
|
|
- tracing::info!(
|
|
|
|
|
- "Successfully recovered and compensated melt saga {}",
|
|
|
|
|
- saga.operation_id
|
|
|
|
|
- );
|
|
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -660,4 +576,42 @@ impl Mint {
|
|
|
|
|
|
|
|
Ok(())
|
|
Ok(())
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ /// Handle pending melt quote by resuming the saga
|
|
|
|
|
+ pub(crate) async fn handle_pending_melt_quote(
|
|
|
|
|
+ &self,
|
|
|
|
|
+ quote: &mut MeltQuote,
|
|
|
|
|
+ ) -> Result<(), Error> {
|
|
|
|
|
+ if quote.state != MeltQuoteState::Pending {
|
|
|
|
|
+ return Ok(());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ let saga = match self
|
|
|
|
|
+ .get_melt_saga_by_quote_id("e.id.to_string())
|
|
|
|
|
+ .await?
|
|
|
|
|
+ {
|
|
|
|
|
+ Some(saga) => saga,
|
|
|
|
|
+ None => {
|
|
|
|
|
+ tracing::warn!(
|
|
|
|
|
+ "No saga found for pending melt quote {}, cannot resume",
|
|
|
|
|
+ quote.id
|
|
|
|
|
+ );
|
|
|
|
|
+ return Ok(());
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ let payment_response = self.check_melt_payment_status(quote).await?;
|
|
|
|
|
+
|
|
|
|
|
+ super::saga_recovery::process_melt_saga_outcome(
|
|
|
|
|
+ &saga,
|
|
|
|
|
+ quote,
|
|
|
|
|
+ &payment_response,
|
|
|
|
|
+ &self.localstore,
|
|
|
|
|
+ &self.pubsub_manager,
|
|
|
|
|
+ self,
|
|
|
|
|
+ )
|
|
|
|
|
+ .await?;
|
|
|
|
|
+
|
|
|
|
|
+ Ok(())
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|