ソースを参照

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 3 週間 前
コミット
f173b2da47
2 ファイル変更48 行追加2 行削除
  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"#)?