Skip to main content

cdk_common/
state.rs

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