1use cashu::{MeltQuoteState, State};
4
5#[derive(thiserror::Error, Debug)]
7pub enum Error {
8 #[error("Token already pending for another update")]
10 Pending,
11 #[error("Token already spent")]
13 AlreadySpent,
14 #[error("Invalid transition: From {0} to {1}")]
16 InvalidTransition(State, State),
17 #[error("Quote already paid")]
19 AlreadyPaid,
20 #[error("Invalid melt quote state transition: From {0} to {1}")]
22 InvalidMeltQuoteTransition(MeltQuoteState, MeltQuoteState),
23}
24
25#[inline]
26pub 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 _ => 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]
48pub 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}