1use fedimint_client_module::DynGlobalClientContext;
2use fedimint_client_module::module::OutPointRange;
3use fedimint_client_module::sm::{ClientSMDatabaseTransaction, State, StateTransition};
4use fedimint_client_module::transaction::{ClientInput, ClientInputBundle};
5use fedimint_core::core::OperationId;
6use fedimint_core::encoding::{Decodable, Encodable};
7use fedimint_core::{Amount, TransactionId};
8use fedimint_logging::LOG_CLIENT_MODULE_MINT;
9use fedimint_mint_common::MintInput;
10use tracing::{debug, warn};
11
12use crate::{MintClientContext, SpendableNote};
13
14#[cfg_attr(doc, aquamarine::aquamarine)]
15#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
29pub enum MintInputStates {
30 #[deprecated(note = "Use CreateMulti instead")]
31 Created(MintInputStateCreated),
32 Refund(MintInputStateRefund),
33 Success(MintInputStateSuccess),
34 Error(MintInputStateError),
35 RefundSuccess(MintInputStateRefundSuccess),
36 CreatedBundle(MintInputStateCreatedBundle),
41 RefundedBundle(MintInputStateRefundedBundle),
44 RefundedPerNote(MintInputStateRefundedPerNote),
46}
47
48#[derive(Debug, Clone, Eq, Hash, PartialEq, Decodable, Encodable)]
49pub struct MintInputCommonV0 {
50 pub(crate) operation_id: OperationId,
51 pub(crate) txid: TransactionId,
52 pub(crate) input_idx: u64,
53}
54
55#[derive(Debug, Copy, Clone, Eq, Hash, PartialEq, Decodable, Encodable)]
56pub struct MintInputCommon {
57 pub(crate) operation_id: OperationId,
58 pub(crate) out_point_range: OutPointRange,
59}
60
61#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
62pub struct MintInputStateMachineV0 {
63 pub(crate) common: MintInputCommonV0,
64 pub(crate) state: MintInputStates,
65}
66
67#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
68pub struct MintInputStateMachine {
69 pub(crate) common: MintInputCommon,
70 pub(crate) state: MintInputStates,
71}
72
73impl State for MintInputStateMachine {
74 type ModuleContext = MintClientContext;
75
76 #[allow(deprecated)]
77 fn transitions(
78 &self,
79 _context: &Self::ModuleContext,
80 global_context: &DynGlobalClientContext,
81 ) -> Vec<StateTransition<Self>> {
82 match &self.state {
83 MintInputStates::Created(_) => {
84 MintInputStateCreated::transitions(self.common, global_context)
85 }
86 MintInputStates::CreatedBundle(_) => {
87 MintInputStateCreatedBundle::transitions(self.common, global_context)
88 }
89 MintInputStates::RefundedBundle(_) => {
90 MintInputStateRefundedBundle::transitions(self.common, global_context)
91 }
92 MintInputStates::Refund(refund) => refund.transitions(global_context),
93 MintInputStates::Success(_)
94 | MintInputStates::Error(_)
95 | MintInputStates::RefundSuccess(_)
96 | MintInputStates::RefundedPerNote(_) => {
99 vec![]
100 }
101 }
102 }
103
104 fn operation_id(&self) -> OperationId {
105 self.common.operation_id
106 }
107}
108
109#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
110pub struct MintInputStateCreated {
111 pub(crate) amount: Amount,
112 pub(crate) spendable_note: SpendableNote,
113}
114
115impl MintInputStateCreated {
116 fn transitions(
117 common: MintInputCommon,
118 global_context: &DynGlobalClientContext,
119 ) -> Vec<StateTransition<MintInputStateMachine>> {
120 let global_context = global_context.clone();
121 vec![StateTransition::new(
122 Self::await_success(common, global_context.clone()),
123 move |dbtx, result, old_state| {
124 Box::pin(Self::transition_success(
125 result,
126 old_state,
127 dbtx,
128 global_context.clone(),
129 ))
130 },
131 )]
132 }
133
134 async fn await_success(
135 common: MintInputCommon,
136 global_context: DynGlobalClientContext,
137 ) -> Result<(), String> {
138 global_context
139 .await_tx_accepted(common.out_point_range.txid())
140 .await
141 }
142
143 #[allow(deprecated)]
144 async fn transition_success(
145 result: Result<(), String>,
146 old_state: MintInputStateMachine,
147 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
148 global_context: DynGlobalClientContext,
149 ) -> MintInputStateMachine {
150 assert!(matches!(old_state.state, MintInputStates::Created(_)));
151
152 match result {
153 Ok(()) => {
154 MintInputStateMachine {
156 common: old_state.common,
157 state: MintInputStates::Success(MintInputStateSuccess {}),
158 }
159 }
160 Err(err) => {
161 debug!(target: LOG_CLIENT_MODULE_MINT, err = %err.as_str(), "Refunding mint transaction input due to transaction error");
163 Self::refund(dbtx, old_state, global_context).await
164 }
165 }
166 }
167
168 #[allow(deprecated)]
169 async fn refund(
170 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
171 old_state: MintInputStateMachine,
172 global_context: DynGlobalClientContext,
173 ) -> MintInputStateMachine {
174 let (amount, spendable_note) = match old_state.state {
175 MintInputStates::Created(created) => (created.amount, created.spendable_note),
176 _ => panic!("Invalid state transition"),
177 };
178
179 let refund_input = ClientInput::<MintInput> {
180 input: MintInput::new_v0(amount, spendable_note.note()),
181 keys: vec![spendable_note.spend_key],
182 amount,
183 };
184
185 let change_range = global_context
186 .claim_inputs(
187 dbtx,
188 ClientInputBundle::new_no_sm(vec![refund_input]),
191 )
192 .await
193 .expect("Cannot claim input, additional funding needed");
194
195 MintInputStateMachine {
196 common: old_state.common,
197 state: MintInputStates::Refund(MintInputStateRefund {
198 refund_txid: change_range.txid(),
199 }),
200 }
201 }
202}
203
204#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
205pub struct MintInputStateCreatedBundle {
206 pub(crate) notes: Vec<(Amount, SpendableNote)>,
207}
208
209impl MintInputStateCreatedBundle {
210 fn transitions(
211 common: MintInputCommon,
212 global_context: &DynGlobalClientContext,
213 ) -> Vec<StateTransition<MintInputStateMachine>> {
214 let global_context = global_context.clone();
215 vec![StateTransition::new(
216 Self::await_success(common, global_context.clone()),
217 move |dbtx, result, old_state| {
218 Box::pin(Self::transition_success(
219 result,
220 old_state,
221 dbtx,
222 global_context.clone(),
223 ))
224 },
225 )]
226 }
227
228 async fn await_success(
229 common: MintInputCommon,
230 global_context: DynGlobalClientContext,
231 ) -> Result<(), String> {
232 global_context
233 .await_tx_accepted(common.out_point_range.txid())
234 .await
235 }
236
237 async fn transition_success(
238 result: Result<(), String>,
239 old_state: MintInputStateMachine,
240 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
241 global_context: DynGlobalClientContext,
242 ) -> MintInputStateMachine {
243 assert!(matches!(old_state.state, MintInputStates::CreatedBundle(_)));
244
245 match result {
246 Ok(()) => {
247 MintInputStateMachine {
249 common: old_state.common,
250 state: MintInputStates::Success(MintInputStateSuccess {}),
251 }
252 }
253 Err(err) => {
254 debug!(target: LOG_CLIENT_MODULE_MINT, err = %err.as_str(), "Refunding mint transaction input due to transaction error");
256 Self::refund(dbtx, old_state, global_context).await
257 }
258 }
259 }
260
261 async fn refund(
262 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
263 old_state: MintInputStateMachine,
264 global_context: DynGlobalClientContext,
265 ) -> MintInputStateMachine {
266 let spendable_notes = match old_state.state {
267 MintInputStates::CreatedBundle(created) => created.notes,
268 _ => panic!("Invalid state transition"),
269 };
270
271 let mut inputs = Vec::new();
272
273 for (amount, spendable_note) in spendable_notes.clone() {
274 inputs.push(ClientInput::<MintInput> {
275 input: MintInput::new_v0(amount, spendable_note.note()),
276 keys: vec![spendable_note.spend_key],
277 amount,
278 });
279 }
280
281 let change_range = global_context
282 .claim_inputs(
283 dbtx,
284 ClientInputBundle::new_no_sm(inputs),
286 )
287 .await
288 .expect("Cannot claim input, additional funding needed");
289
290 let refund_txid = change_range.txid();
291 MintInputStateMachine {
292 common: old_state.common,
293 state: if spendable_notes.len() == 1 {
294 MintInputStates::RefundedPerNote(MintInputStateRefundedPerNote {
296 refund_txids: vec![refund_txid],
297 })
298 } else {
299 MintInputStates::RefundedBundle(MintInputStateRefundedBundle {
300 refund_txid,
301 spendable_notes,
302 })
303 },
304 }
305 }
306}
307
308#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
309pub struct MintInputStateRefundedBundle {
310 pub(crate) refund_txid: TransactionId,
311 pub(crate) spendable_notes: Vec<(Amount, SpendableNote)>,
312}
313
314impl MintInputStateRefundedBundle {
315 fn transitions(
316 common: MintInputCommon,
317 global_context: &DynGlobalClientContext,
318 ) -> Vec<StateTransition<MintInputStateMachine>> {
319 let global_context = global_context.clone();
320 vec![StateTransition::new(
321 Self::await_success(common, global_context.clone()),
322 move |dbtx, result, old_state| {
323 Box::pin(Self::transition_success(
324 result,
325 old_state,
326 dbtx,
327 global_context.clone(),
328 ))
329 },
330 )]
331 }
332
333 async fn await_success(
334 common: MintInputCommon,
335 global_context: DynGlobalClientContext,
336 ) -> Result<(), String> {
337 global_context
338 .await_tx_accepted(common.out_point_range.txid())
339 .await
340 }
341
342 async fn transition_success(
343 result: Result<(), String>,
344 old_state: MintInputStateMachine,
345 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
346 global_context: DynGlobalClientContext,
347 ) -> MintInputStateMachine {
348 assert!(matches!(
349 old_state.state,
350 MintInputStates::RefundedBundle(_)
351 ));
352
353 match result {
354 Ok(()) => {
355 MintInputStateMachine {
357 common: old_state.common,
358 state: MintInputStates::Success(MintInputStateSuccess {}),
359 }
360 }
361 Err(err) => {
362 debug!(target: LOG_CLIENT_MODULE_MINT, err = %err.as_str(), "Refunding mint transaction input due to transaction error on multi-note refund");
364 Self::refund(dbtx, old_state, global_context).await
365 }
366 }
367 }
368
369 async fn refund(
370 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
371 old_state: MintInputStateMachine,
372 global_context: DynGlobalClientContext,
373 ) -> MintInputStateMachine {
374 let spendable_notes = match old_state.state {
375 MintInputStates::RefundedBundle(created) => created.spendable_notes,
376 _ => panic!("Invalid state transition"),
377 };
378
379 let mut refund_txids = vec![];
380 for (amount, spendable_note) in spendable_notes {
381 let refund_input = ClientInput::<MintInput> {
382 input: MintInput::new_v0(amount, spendable_note.note()),
383 keys: vec![spendable_note.spend_key],
384 amount,
385 };
386 let change_range = global_context
387 .claim_inputs(
388 dbtx,
389 ClientInputBundle::new_no_sm(vec![refund_input]),
392 )
393 .await
394 .expect("Cannot claim input, additional funding needed");
395
396 refund_txids.push(change_range.txid());
397 }
398
399 assert!(!refund_txids.is_empty());
400 MintInputStateMachine {
401 common: old_state.common,
402 state: MintInputStates::RefundedPerNote(MintInputStateRefundedPerNote { refund_txids }),
403 }
404 }
405}
406
407#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
408pub struct MintInputStateRefund {
409 pub refund_txid: TransactionId,
410}
411
412impl MintInputStateRefund {
413 fn transitions(
414 &self,
415 global_context: &DynGlobalClientContext,
416 ) -> Vec<StateTransition<MintInputStateMachine>> {
417 vec![StateTransition::new(
418 Self::await_refund_success(global_context.clone(), self.refund_txid),
419 |_dbtx, result, old_state| {
420 Box::pin(async { Self::transition_refund_success(result, old_state) })
421 },
422 )]
423 }
424
425 async fn await_refund_success(
426 global_context: DynGlobalClientContext,
427 refund_txid: TransactionId,
428 ) -> Result<(), String> {
429 global_context.await_tx_accepted(refund_txid).await
430 }
431
432 fn transition_refund_success(
433 result: Result<(), String>,
434 old_state: MintInputStateMachine,
435 ) -> MintInputStateMachine {
436 let refund_txid = match old_state.state {
437 MintInputStates::Refund(refund) => refund.refund_txid,
438 _ => panic!("Invalid state transition"),
439 };
440
441 match result {
442 Ok(()) => {
443 MintInputStateMachine {
445 common: old_state.common,
446 state: MintInputStates::RefundSuccess(MintInputStateRefundSuccess {
447 refund_txid,
448 }),
449 }
450 }
451 Err(err) => {
452 warn!(target: LOG_CLIENT_MODULE_MINT, err = %err.as_str(), %refund_txid, "Refund transaction rejected. Notes probably lost.");
455 MintInputStateMachine {
456 common: old_state.common,
457 state: MintInputStates::Error(MintInputStateError {
458 error: format!("Refund transaction {refund_txid} was rejected"),
459 }),
460 }
461 }
462 }
463 }
464}
465
466#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
467pub struct MintInputStateRefundedPerNote {
468 pub refund_txids: Vec<TransactionId>,
469}
470
471#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
472pub struct MintInputStateSuccess {}
473
474#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
475pub struct MintInputStateError {
476 error: String,
477}
478
479#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
480pub struct MintInputStateRefundSuccess {
481 refund_txid: TransactionId,
482}