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.
This commit is contained in:
tsk
2025-10-13 09:21:12 +01:00
committed by GitHub
parent 69650c2ef9
commit f173b2da47
2 changed files with 48 additions and 2 deletions

View File

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

View File

@@ -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"#)?