cdk-common 0.16.0-rc.0

CDK common types and traits
Documentation
//! State transition rules

use cashu::{MeltQuoteState, State};

/// State transition Error
#[derive(thiserror::Error, Debug)]
pub enum Error {
    /// Pending Token
    #[error("Token already pending for another update")]
    Pending,
    /// Already spent
    #[error("Token already spent")]
    AlreadySpent,
    /// 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]
/// Check if the state transition is allowed
pub fn check_state_transition(current_state: State, new_state: State) -> Result<(), Error> {
    let is_valid_transition = match current_state {
        State::Unspent => matches!(new_state, State::Pending | State::Spent),
        State::Pending => matches!(new_state, State::Unspent | State::Spent),
        // Any other state shouldn't be updated by the mint, and the wallet does not use this
        // function
        _ => false,
    };

    if !is_valid_transition {
        Err(match current_state {
            State::Pending => Error::Pending,
            State::Spent => Error::AlreadySpent,
            _ => Error::InvalidTransition(current_state, new_state),
        })
    } else {
        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(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    mod proof_state_transitions {
        use super::*;

        #[test]
        fn unspent_to_pending_is_valid() {
            assert!(check_state_transition(State::Unspent, State::Pending).is_ok());
        }

        #[test]
        fn unspent_to_spent_is_valid() {
            assert!(check_state_transition(State::Unspent, State::Spent).is_ok());
        }

        #[test]
        fn pending_to_unspent_is_valid() {
            assert!(check_state_transition(State::Pending, State::Unspent).is_ok());
        }

        #[test]
        fn pending_to_spent_is_valid() {
            assert!(check_state_transition(State::Pending, State::Spent).is_ok());
        }

        #[test]
        fn unspent_to_unspent_is_invalid() {
            let result = check_state_transition(State::Unspent, State::Unspent);
            assert!(matches!(result, Err(Error::InvalidTransition(_, _))));
        }

        #[test]
        fn pending_to_pending_returns_pending_error() {
            let result = check_state_transition(State::Pending, State::Pending);
            assert!(matches!(result, Err(Error::Pending)));
        }

        #[test]
        fn spent_to_any_returns_already_spent() {
            assert!(matches!(
                check_state_transition(State::Spent, State::Unspent),
                Err(Error::AlreadySpent)
            ));
            assert!(matches!(
                check_state_transition(State::Spent, State::Pending),
                Err(Error::AlreadySpent)
            ));
            assert!(matches!(
                check_state_transition(State::Spent, State::Spent),
                Err(Error::AlreadySpent)
            ));
        }

        #[test]
        fn reserved_state_is_invalid_source() {
            let result = check_state_transition(State::Reserved, State::Unspent);
            assert!(matches!(result, Err(Error::InvalidTransition(_, _))));
        }
    }

    mod melt_quote_state_transitions {
        use super::*;

        #[test]
        fn unpaid_to_pending_is_valid() {
            assert!(check_melt_quote_state_transition(
                MeltQuoteState::Unpaid,
                MeltQuoteState::Pending
            )
            .is_ok());
        }

        #[test]
        fn unpaid_to_failed_is_valid() {
            assert!(check_melt_quote_state_transition(
                MeltQuoteState::Unpaid,
                MeltQuoteState::Failed
            )
            .is_ok());
        }

        #[test]
        fn pending_to_unpaid_is_valid() {
            assert!(check_melt_quote_state_transition(
                MeltQuoteState::Pending,
                MeltQuoteState::Unpaid
            )
            .is_ok());
        }

        #[test]
        fn pending_to_paid_is_valid() {
            assert!(check_melt_quote_state_transition(
                MeltQuoteState::Pending,
                MeltQuoteState::Paid
            )
            .is_ok());
        }

        #[test]
        fn pending_to_failed_is_valid() {
            assert!(check_melt_quote_state_transition(
                MeltQuoteState::Pending,
                MeltQuoteState::Failed
            )
            .is_ok());
        }

        #[test]
        fn failed_to_pending_is_valid() {
            assert!(check_melt_quote_state_transition(
                MeltQuoteState::Failed,
                MeltQuoteState::Pending
            )
            .is_ok());
        }

        #[test]
        fn failed_to_unpaid_is_valid() {
            assert!(check_melt_quote_state_transition(
                MeltQuoteState::Failed,
                MeltQuoteState::Unpaid
            )
            .is_ok());
        }

        #[test]
        fn unknown_to_any_is_valid() {
            assert!(check_melt_quote_state_transition(
                MeltQuoteState::Unknown,
                MeltQuoteState::Unpaid
            )
            .is_ok());
            assert!(check_melt_quote_state_transition(
                MeltQuoteState::Unknown,
                MeltQuoteState::Pending
            )
            .is_ok());
            assert!(check_melt_quote_state_transition(
                MeltQuoteState::Unknown,
                MeltQuoteState::Paid
            )
            .is_ok());
            assert!(check_melt_quote_state_transition(
                MeltQuoteState::Unknown,
                MeltQuoteState::Failed
            )
            .is_ok());
        }

        #[test]
        fn unpaid_to_paid_is_invalid() {
            let result =
                check_melt_quote_state_transition(MeltQuoteState::Unpaid, MeltQuoteState::Paid);
            assert!(matches!(
                result,
                Err(Error::InvalidMeltQuoteTransition(_, _))
            ));
        }

        #[test]
        fn unpaid_to_unpaid_is_invalid() {
            let result =
                check_melt_quote_state_transition(MeltQuoteState::Unpaid, MeltQuoteState::Unpaid);
            assert!(matches!(
                result,
                Err(Error::InvalidMeltQuoteTransition(_, _))
            ));
        }

        #[test]
        fn pending_to_pending_returns_pending_error() {
            let result =
                check_melt_quote_state_transition(MeltQuoteState::Pending, MeltQuoteState::Pending);
            assert!(matches!(result, Err(Error::Pending)));
        }

        #[test]
        fn paid_to_any_returns_already_paid() {
            assert!(matches!(
                check_melt_quote_state_transition(MeltQuoteState::Paid, MeltQuoteState::Unpaid),
                Err(Error::AlreadyPaid)
            ));
            assert!(matches!(
                check_melt_quote_state_transition(MeltQuoteState::Paid, MeltQuoteState::Pending),
                Err(Error::AlreadyPaid)
            ));
            assert!(matches!(
                check_melt_quote_state_transition(MeltQuoteState::Paid, MeltQuoteState::Paid),
                Err(Error::AlreadyPaid)
            ));
            assert!(matches!(
                check_melt_quote_state_transition(MeltQuoteState::Paid, MeltQuoteState::Failed),
                Err(Error::AlreadyPaid)
            ));
        }

        #[test]
        fn failed_to_paid_is_invalid() {
            let result =
                check_melt_quote_state_transition(MeltQuoteState::Failed, MeltQuoteState::Paid);
            assert!(matches!(
                result,
                Err(Error::InvalidMeltQuoteTransition(_, _))
            ));
        }

        #[test]
        fn failed_to_failed_is_invalid() {
            let result =
                check_melt_quote_state_transition(MeltQuoteState::Failed, MeltQuoteState::Failed);
            assert!(matches!(
                result,
                Err(Error::InvalidMeltQuoteTransition(_, _))
            ));
        }
    }
}