state.rs 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  1. //! State transition rules
  2. use cashu::{MeltQuoteState, State};
  3. /// State transition Error
  4. #[derive(thiserror::Error, Debug)]
  5. pub enum Error {
  6. /// Pending Token
  7. #[error("Token already pending for another update")]
  8. Pending,
  9. /// Already spent
  10. #[error("Token already spent")]
  11. AlreadySpent,
  12. /// Invalid transition
  13. #[error("Invalid transition: From {0} to {1}")]
  14. InvalidTransition(State, State),
  15. /// Already paid
  16. #[error("Quote already paid")]
  17. AlreadyPaid,
  18. /// Invalid transition
  19. #[error("Invalid melt quote state transition: From {0} to {1}")]
  20. InvalidMeltQuoteTransition(MeltQuoteState, MeltQuoteState),
  21. }
  22. #[inline]
  23. /// Check if the state transition is allowed
  24. pub fn check_state_transition(current_state: State, new_state: State) -> Result<(), Error> {
  25. let is_valid_transition = match current_state {
  26. State::Unspent => matches!(new_state, State::Pending | State::Spent),
  27. State::Pending => matches!(new_state, State::Unspent | State::Spent),
  28. // Any other state shouldn't be updated by the mint, and the wallet does not use this
  29. // function
  30. _ => false,
  31. };
  32. if !is_valid_transition {
  33. Err(match current_state {
  34. State::Pending => Error::Pending,
  35. State::Spent => Error::AlreadySpent,
  36. _ => Error::InvalidTransition(current_state, new_state),
  37. })
  38. } else {
  39. Ok(())
  40. }
  41. }
  42. #[inline]
  43. /// Check if the melt quote state transition is allowed
  44. ///
  45. /// Valid transitions:
  46. /// - Unpaid -> Pending, Failed
  47. /// - Pending -> Unpaid, Paid, Failed
  48. /// - Paid -> (no transitions allowed)
  49. /// - Failed -> Pending
  50. pub fn check_melt_quote_state_transition(
  51. current_state: MeltQuoteState,
  52. new_state: MeltQuoteState,
  53. ) -> Result<(), Error> {
  54. let is_valid_transition = match current_state {
  55. MeltQuoteState::Unpaid => {
  56. matches!(new_state, MeltQuoteState::Pending | MeltQuoteState::Failed)
  57. }
  58. MeltQuoteState::Pending => matches!(
  59. new_state,
  60. MeltQuoteState::Unpaid | MeltQuoteState::Paid | MeltQuoteState::Failed
  61. ),
  62. MeltQuoteState::Failed => {
  63. matches!(new_state, MeltQuoteState::Pending | MeltQuoteState::Unpaid)
  64. }
  65. MeltQuoteState::Paid => false,
  66. MeltQuoteState::Unknown => true,
  67. };
  68. if !is_valid_transition {
  69. Err(match current_state {
  70. MeltQuoteState::Pending => Error::Pending,
  71. MeltQuoteState::Paid => Error::AlreadyPaid,
  72. _ => Error::InvalidMeltQuoteTransition(current_state, new_state),
  73. })
  74. } else {
  75. Ok(())
  76. }
  77. }
  78. #[cfg(test)]
  79. mod tests {
  80. use super::*;
  81. mod proof_state_transitions {
  82. use super::*;
  83. #[test]
  84. fn unspent_to_pending_is_valid() {
  85. assert!(check_state_transition(State::Unspent, State::Pending).is_ok());
  86. }
  87. #[test]
  88. fn unspent_to_spent_is_valid() {
  89. assert!(check_state_transition(State::Unspent, State::Spent).is_ok());
  90. }
  91. #[test]
  92. fn pending_to_unspent_is_valid() {
  93. assert!(check_state_transition(State::Pending, State::Unspent).is_ok());
  94. }
  95. #[test]
  96. fn pending_to_spent_is_valid() {
  97. assert!(check_state_transition(State::Pending, State::Spent).is_ok());
  98. }
  99. #[test]
  100. fn unspent_to_unspent_is_invalid() {
  101. let result = check_state_transition(State::Unspent, State::Unspent);
  102. assert!(matches!(result, Err(Error::InvalidTransition(_, _))));
  103. }
  104. #[test]
  105. fn pending_to_pending_returns_pending_error() {
  106. let result = check_state_transition(State::Pending, State::Pending);
  107. assert!(matches!(result, Err(Error::Pending)));
  108. }
  109. #[test]
  110. fn spent_to_any_returns_already_spent() {
  111. assert!(matches!(
  112. check_state_transition(State::Spent, State::Unspent),
  113. Err(Error::AlreadySpent)
  114. ));
  115. assert!(matches!(
  116. check_state_transition(State::Spent, State::Pending),
  117. Err(Error::AlreadySpent)
  118. ));
  119. assert!(matches!(
  120. check_state_transition(State::Spent, State::Spent),
  121. Err(Error::AlreadySpent)
  122. ));
  123. }
  124. #[test]
  125. fn reserved_state_is_invalid_source() {
  126. let result = check_state_transition(State::Reserved, State::Unspent);
  127. assert!(matches!(result, Err(Error::InvalidTransition(_, _))));
  128. }
  129. }
  130. mod melt_quote_state_transitions {
  131. use super::*;
  132. #[test]
  133. fn unpaid_to_pending_is_valid() {
  134. assert!(check_melt_quote_state_transition(
  135. MeltQuoteState::Unpaid,
  136. MeltQuoteState::Pending
  137. )
  138. .is_ok());
  139. }
  140. #[test]
  141. fn unpaid_to_failed_is_valid() {
  142. assert!(check_melt_quote_state_transition(
  143. MeltQuoteState::Unpaid,
  144. MeltQuoteState::Failed
  145. )
  146. .is_ok());
  147. }
  148. #[test]
  149. fn pending_to_unpaid_is_valid() {
  150. assert!(check_melt_quote_state_transition(
  151. MeltQuoteState::Pending,
  152. MeltQuoteState::Unpaid
  153. )
  154. .is_ok());
  155. }
  156. #[test]
  157. fn pending_to_paid_is_valid() {
  158. assert!(check_melt_quote_state_transition(
  159. MeltQuoteState::Pending,
  160. MeltQuoteState::Paid
  161. )
  162. .is_ok());
  163. }
  164. #[test]
  165. fn pending_to_failed_is_valid() {
  166. assert!(check_melt_quote_state_transition(
  167. MeltQuoteState::Pending,
  168. MeltQuoteState::Failed
  169. )
  170. .is_ok());
  171. }
  172. #[test]
  173. fn failed_to_pending_is_valid() {
  174. assert!(check_melt_quote_state_transition(
  175. MeltQuoteState::Failed,
  176. MeltQuoteState::Pending
  177. )
  178. .is_ok());
  179. }
  180. #[test]
  181. fn failed_to_unpaid_is_valid() {
  182. assert!(check_melt_quote_state_transition(
  183. MeltQuoteState::Failed,
  184. MeltQuoteState::Unpaid
  185. )
  186. .is_ok());
  187. }
  188. #[test]
  189. fn unknown_to_any_is_valid() {
  190. assert!(check_melt_quote_state_transition(
  191. MeltQuoteState::Unknown,
  192. MeltQuoteState::Unpaid
  193. )
  194. .is_ok());
  195. assert!(check_melt_quote_state_transition(
  196. MeltQuoteState::Unknown,
  197. MeltQuoteState::Pending
  198. )
  199. .is_ok());
  200. assert!(check_melt_quote_state_transition(
  201. MeltQuoteState::Unknown,
  202. MeltQuoteState::Paid
  203. )
  204. .is_ok());
  205. assert!(check_melt_quote_state_transition(
  206. MeltQuoteState::Unknown,
  207. MeltQuoteState::Failed
  208. )
  209. .is_ok());
  210. }
  211. #[test]
  212. fn unpaid_to_paid_is_invalid() {
  213. let result =
  214. check_melt_quote_state_transition(MeltQuoteState::Unpaid, MeltQuoteState::Paid);
  215. assert!(matches!(
  216. result,
  217. Err(Error::InvalidMeltQuoteTransition(_, _))
  218. ));
  219. }
  220. #[test]
  221. fn unpaid_to_unpaid_is_invalid() {
  222. let result =
  223. check_melt_quote_state_transition(MeltQuoteState::Unpaid, MeltQuoteState::Unpaid);
  224. assert!(matches!(
  225. result,
  226. Err(Error::InvalidMeltQuoteTransition(_, _))
  227. ));
  228. }
  229. #[test]
  230. fn pending_to_pending_returns_pending_error() {
  231. let result =
  232. check_melt_quote_state_transition(MeltQuoteState::Pending, MeltQuoteState::Pending);
  233. assert!(matches!(result, Err(Error::Pending)));
  234. }
  235. #[test]
  236. fn paid_to_any_returns_already_paid() {
  237. assert!(matches!(
  238. check_melt_quote_state_transition(MeltQuoteState::Paid, MeltQuoteState::Unpaid),
  239. Err(Error::AlreadyPaid)
  240. ));
  241. assert!(matches!(
  242. check_melt_quote_state_transition(MeltQuoteState::Paid, MeltQuoteState::Pending),
  243. Err(Error::AlreadyPaid)
  244. ));
  245. assert!(matches!(
  246. check_melt_quote_state_transition(MeltQuoteState::Paid, MeltQuoteState::Paid),
  247. Err(Error::AlreadyPaid)
  248. ));
  249. assert!(matches!(
  250. check_melt_quote_state_transition(MeltQuoteState::Paid, MeltQuoteState::Failed),
  251. Err(Error::AlreadyPaid)
  252. ));
  253. }
  254. #[test]
  255. fn failed_to_paid_is_invalid() {
  256. let result =
  257. check_melt_quote_state_transition(MeltQuoteState::Failed, MeltQuoteState::Paid);
  258. assert!(matches!(
  259. result,
  260. Err(Error::InvalidMeltQuoteTransition(_, _))
  261. ));
  262. }
  263. #[test]
  264. fn failed_to_failed_is_invalid() {
  265. let result =
  266. check_melt_quote_state_transition(MeltQuoteState::Failed, MeltQuoteState::Failed);
  267. assert!(matches!(
  268. result,
  269. Err(Error::InvalidMeltQuoteTransition(_, _))
  270. ));
  271. }
  272. }
  273. }