Bläddra i källkod

feat(cdk): add melt quote state transition validation (#1188)

Add state machine validation for melt quote transitions to prevent
invalid state changes. Includes new error types and validation logic
for Unpaid, Pending, Paid, and Failed states.
tsk 10 månader sedan
förälder
incheckning
f173b2da47
2 ändrade filer med 48 tillägg och 2 borttagningar
  1. 45 1
      crates/cdk-common/src/state.rs
  2. 3 1
      crates/cdk-sql-common/src/mint/mod.rs

+ 45 - 1
crates/cdk-common/src/state.rs

@@ -1,6 +1,6 @@
 //! State transition rules
 
-use cashu::State;
+use cashu::{MeltQuoteState, State};
 
 /// State transition Error
 #[derive(thiserror::Error, Debug)]
@@ -14,6 +14,12 @@ pub enum Error {
     /// Invalid transition
     #[error("Invalid transition: From {0} to {1}")]
     InvalidTransition(State, State),
+    /// Already paid
+    #[error("Quote already paid")]
+    AlreadyPaid,
+    /// Invalid transition
+    #[error("Invalid melt quote state transition: From {0} to {1}")]
+    InvalidMeltQuoteTransition(MeltQuoteState, MeltQuoteState),
 }
 
 #[inline]
@@ -37,3 +43,41 @@ pub fn check_state_transition(current_state: State, new_state: State) -> Result<
         Ok(())
     }
 }
+
+#[inline]
+/// Check if the melt quote state transition is allowed
+///
+/// Valid transitions:
+/// - Unpaid -> Pending, Failed
+/// - Pending -> Unpaid, Paid, Failed
+/// - Paid -> (no transitions allowed)
+/// - Failed -> Pending
+pub fn check_melt_quote_state_transition(
+    current_state: MeltQuoteState,
+    new_state: MeltQuoteState,
+) -> Result<(), Error> {
+    let is_valid_transition = match current_state {
+        MeltQuoteState::Unpaid => {
+            matches!(new_state, MeltQuoteState::Pending | MeltQuoteState::Failed)
+        }
+        MeltQuoteState::Pending => matches!(
+            new_state,
+            MeltQuoteState::Unpaid | MeltQuoteState::Paid | MeltQuoteState::Failed
+        ),
+        MeltQuoteState::Failed => {
+            matches!(new_state, MeltQuoteState::Pending | MeltQuoteState::Unpaid)
+        }
+        MeltQuoteState::Paid => false,
+        MeltQuoteState::Unknown => true,
+    };
+
+    if !is_valid_transition {
+        Err(match current_state {
+            MeltQuoteState::Pending => Error::Pending,
+            MeltQuoteState::Paid => Error::AlreadyPaid,
+            _ => Error::InvalidMeltQuoteTransition(current_state, new_state),
+        })
+    } else {
+        Ok(())
+    }
+}

+ 3 - 1
crates/cdk-sql-common/src/mint/mod.rs

@@ -28,7 +28,7 @@ use cdk_common::nut00::ProofsMethods;
 use cdk_common::payment::PaymentIdentifier;
 use cdk_common::quote_id::QuoteId;
 use cdk_common::secret::Secret;
-use cdk_common::state::check_state_transition;
+use cdk_common::state::{check_melt_quote_state_transition, check_state_transition};
 use cdk_common::util::unix_time;
 use cdk_common::{
     Amount, BlindSignature, BlindSignatureDleq, BlindedMessage, CurrencyUnit, Id, MeltQuoteState,
@@ -1043,6 +1043,8 @@ VALUES (:quote_id, :amount, :timestamp);
         .transpose()?
         .ok_or(Error::QuoteNotFound)?;
 
+        check_melt_quote_state_transition(quote.state, state)?;
+
         let rec = if state == MeltQuoteState::Paid {
             let current_time = unix_time();
             query(r#"UPDATE melt_quote SET state = :state, paid_time = :paid_time, payment_preimage = :payment_preimage WHERE id = :id"#)?