use cashu::{MeltQuoteState, State};
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("Token already pending for another update")]
Pending,
#[error("Token already spent")]
AlreadySpent,
#[error("Invalid transition: From {0} to {1}")]
InvalidTransition(State, State),
#[error("Quote already paid")]
AlreadyPaid,
#[error("Invalid melt quote state transition: From {0} to {1}")]
InvalidMeltQuoteTransition(MeltQuoteState, MeltQuoteState),
}
#[inline]
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),
_ => 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]
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(_, _))
));
}
}
}