1use assert_matches::assert_matches;
2use fedimint_client_module::DynGlobalClientContext;
3use fedimint_client_module::module::OutPointRange;
4use fedimint_client_module::sm::{ClientSMDatabaseTransaction, State, StateTransition};
5use fedimint_client_module::transaction::{ClientInput, ClientInputBundle};
6use fedimint_core::core::OperationId;
7use fedimint_core::encoding::{Decodable, Encodable};
8use fedimint_core::module::Amounts;
9use fedimint_core::util::FmtCompactAnyhow;
10use fedimint_core::{Amount, TransactionId};
11use fedimint_logging::LOG_CLIENT_MODULE_MINT;
12use fedimint_mint_common::MintInput;
13use tracing::{debug, warn};
14
15use crate::{MintClientContext, SpendableNote};
16
17#[cfg_attr(doc, aquamarine::aquamarine)]
18#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
32pub enum MintInputStates {
33 #[deprecated(note = "Use CreateMulti instead")]
34 Created(MintInputStateCreated),
35 Refund(MintInputStateRefund),
36 Success(MintInputStateSuccess),
37 Error(MintInputStateError),
38 RefundSuccess(MintInputStateRefundSuccess),
39 CreatedBundle(MintInputStateCreatedBundle),
44 RefundedBundle(MintInputStateRefundedBundle),
47 RefundedPerNote(MintInputStateRefundedPerNote),
49}
50
51#[derive(Debug, Clone, Eq, Hash, PartialEq, Decodable, Encodable)]
52pub struct MintInputCommonV0 {
53 pub(crate) operation_id: OperationId,
54 pub(crate) txid: TransactionId,
55 pub(crate) input_idx: u64,
56}
57
58#[derive(Debug, Copy, Clone, Eq, Hash, PartialEq, Decodable, Encodable)]
59pub struct MintInputCommon {
60 pub(crate) operation_id: OperationId,
61 pub(crate) out_point_range: OutPointRange,
62}
63
64#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
65pub struct MintInputStateMachineV0 {
66 pub(crate) common: MintInputCommonV0,
67 pub(crate) state: MintInputStates,
68}
69
70#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
71pub struct MintInputStateMachine {
72 pub(crate) common: MintInputCommon,
73 pub(crate) state: MintInputStates,
74}
75
76impl State for MintInputStateMachine {
77 type ModuleContext = MintClientContext;
78
79 #[allow(deprecated)]
80 fn transitions(
81 &self,
82 _context: &Self::ModuleContext,
83 global_context: &DynGlobalClientContext,
84 ) -> Vec<StateTransition<Self>> {
85 match &self.state {
86 MintInputStates::Created(_) => {
87 MintInputStateCreated::transitions(self.common, global_context)
88 }
89 MintInputStates::CreatedBundle(_) => {
90 MintInputStateCreatedBundle::transitions(self.common, global_context)
91 }
92 MintInputStates::RefundedBundle(state) => {
93 MintInputStateRefundedBundle::transitions(state, global_context)
94 }
95 MintInputStates::Refund(refund) => refund.transitions(global_context),
96 MintInputStates::Success(_)
97 | MintInputStates::Error(_)
98 | MintInputStates::RefundSuccess(_)
99 | MintInputStates::RefundedPerNote(_) => {
102 vec![]
103 }
104 }
105 }
106
107 fn operation_id(&self) -> OperationId {
108 self.common.operation_id
109 }
110
111 fn fmt_visualization(&self, f: &mut dyn std::fmt::Write, indent: &str) -> std::fmt::Result {
112 let txid = self.common.out_point_range.txid();
113 let start = self.common.out_point_range.start_idx();
114 let count = self.common.out_point_range.count();
115 match &self.state {
116 #[allow(deprecated)]
117 MintInputStates::Created(c) => {
118 write!(
119 f,
120 "{indent}MintInputStateMachine\n{indent} state: Created tx={}:[{start},{end}) amount={}",
121 txid.fmt_short(),
122 c.amount,
123 end = start + count as u64,
124 )
125 }
126 MintInputStates::CreatedBundle(c) => {
127 let total: Amount = c.notes.iter().map(|(a, _)| *a).sum();
128 write!(
129 f,
130 "{indent}MintInputStateMachine\n{indent} state: CreatedBundle tx={}:[{start},{end}) notes={}, total={total}",
131 txid.fmt_short(),
132 c.notes.len(),
133 end = start + count as u64,
134 )
135 }
136 MintInputStates::Success(_) => {
137 write!(f, "{indent}MintInputStateMachine\n{indent} state: Success",)
138 }
139 MintInputStates::Refund(r) => {
140 write!(
141 f,
142 "{indent}MintInputStateMachine\n{indent} state: Refund refund_txid={}",
143 r.refund_txid.fmt_short(),
144 )
145 }
146 MintInputStates::RefundedBundle(r) => {
147 let total: Amount = r.spendable_notes.iter().map(|(a, _)| *a).sum();
148 write!(
149 f,
150 "{indent}MintInputStateMachine\n{indent} state: RefundedBundle refund_txid={} notes={}, total={total}",
151 r.refund_txid.fmt_short(),
152 r.spendable_notes.len(),
153 )
154 }
155 MintInputStates::RefundedPerNote(r) => {
156 write!(
157 f,
158 "{indent}MintInputStateMachine\n{indent} state: RefundedPerNote txids={}",
159 r.refund_txids.len(),
160 )
161 }
162 MintInputStates::RefundSuccess(r) => {
163 write!(
164 f,
165 "{indent}MintInputStateMachine\n{indent} state: RefundSuccess refund_txid={}",
166 r.refund_txid.fmt_short(),
167 )
168 }
169 MintInputStates::Error(e) => {
170 write!(
171 f,
172 "{indent}MintInputStateMachine\n{indent} state: Error error={}",
173 e.error,
174 )
175 }
176 }
177 }
178}
179
180#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
181pub struct MintInputStateCreated {
182 pub(crate) amount: Amount,
183 pub(crate) spendable_note: SpendableNote,
184}
185
186impl MintInputStateCreated {
187 fn transitions(
188 common: MintInputCommon,
189 global_context: &DynGlobalClientContext,
190 ) -> Vec<StateTransition<MintInputStateMachine>> {
191 let global_context = global_context.clone();
192 vec![StateTransition::new(
193 Self::await_success(common, global_context.clone()),
194 move |dbtx, result, old_state| {
195 Box::pin(Self::transition_success(
196 result,
197 old_state,
198 dbtx,
199 global_context.clone(),
200 ))
201 },
202 )]
203 }
204
205 async fn await_success(
206 common: MintInputCommon,
207 global_context: DynGlobalClientContext,
208 ) -> Result<(), String> {
209 global_context
210 .await_tx_accepted(common.out_point_range.txid())
211 .await
212 }
213
214 #[allow(deprecated)]
215 async fn transition_success(
216 result: Result<(), String>,
217 old_state: MintInputStateMachine,
218 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
219 global_context: DynGlobalClientContext,
220 ) -> MintInputStateMachine {
221 assert_matches!(old_state.state, MintInputStates::Created(_));
222
223 match result {
224 Ok(()) => {
225 MintInputStateMachine {
227 common: old_state.common,
228 state: MintInputStates::Success(MintInputStateSuccess {}),
229 }
230 }
231 Err(err) => {
232 debug!(target: LOG_CLIENT_MODULE_MINT, err = %err.as_str(), "Refunding mint transaction input due to transaction error");
234 Self::refund(dbtx, old_state, global_context).await
235 }
236 }
237 }
238
239 #[allow(deprecated)]
240 async fn refund(
241 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
242 old_state: MintInputStateMachine,
243 global_context: DynGlobalClientContext,
244 ) -> MintInputStateMachine {
245 let (amount, spendable_note) = match old_state.state {
246 MintInputStates::Created(created) => (created.amount, created.spendable_note),
247 _ => panic!("Invalid state transition"),
248 };
249
250 let refund_input = ClientInput::<MintInput> {
251 input: MintInput::new_v0(amount, spendable_note.note()),
252 keys: vec![spendable_note.spend_key],
253 amounts: Amounts::new_bitcoin(amount),
254 };
255
256 let change_range = global_context
257 .claim_inputs(
258 dbtx,
259 ClientInputBundle::new_no_sm(vec![refund_input]),
262 )
263 .await
264 .expect("Cannot claim input, additional funding needed");
265
266 MintInputStateMachine {
267 common: old_state.common,
268 state: MintInputStates::Refund(MintInputStateRefund {
269 refund_txid: change_range.txid(),
270 }),
271 }
272 }
273}
274
275#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
276pub struct MintInputStateCreatedBundle {
277 pub(crate) notes: Vec<(Amount, SpendableNote)>,
278}
279
280impl MintInputStateCreatedBundle {
281 fn transitions(
282 common: MintInputCommon,
283 global_context: &DynGlobalClientContext,
284 ) -> Vec<StateTransition<MintInputStateMachine>> {
285 let global_context = global_context.clone();
286 vec![StateTransition::new(
287 Self::await_success(common, global_context.clone()),
288 move |dbtx, result, old_state| {
289 Box::pin(Self::transition_success(
290 result,
291 old_state,
292 dbtx,
293 global_context.clone(),
294 ))
295 },
296 )]
297 }
298
299 async fn await_success(
300 common: MintInputCommon,
301 global_context: DynGlobalClientContext,
302 ) -> Result<(), String> {
303 global_context
304 .await_tx_accepted(common.out_point_range.txid())
305 .await
306 }
307
308 async fn transition_success(
309 result: Result<(), String>,
310 old_state: MintInputStateMachine,
311 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
312 global_context: DynGlobalClientContext,
313 ) -> MintInputStateMachine {
314 assert_matches!(old_state.state, MintInputStates::CreatedBundle(_));
315
316 match result {
317 Ok(()) => {
318 MintInputStateMachine {
320 common: old_state.common,
321 state: MintInputStates::Success(MintInputStateSuccess {}),
322 }
323 }
324 Err(err) => {
325 debug!(target: LOG_CLIENT_MODULE_MINT, err = %err.as_str(), "Refunding mint transaction input due to transaction error");
327 Self::refund(dbtx, old_state, global_context).await
328 }
329 }
330 }
331
332 async fn refund(
333 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
334 old_state: MintInputStateMachine,
335 global_context: DynGlobalClientContext,
336 ) -> MintInputStateMachine {
337 let spendable_notes = match old_state.state {
338 MintInputStates::CreatedBundle(created) => created.notes,
339 _ => panic!("Invalid state transition"),
340 };
341
342 let mut inputs = Vec::new();
343
344 for (amount, spendable_note) in spendable_notes.clone() {
345 inputs.push(ClientInput::<MintInput> {
346 input: MintInput::new_v0(amount, spendable_note.note()),
347 keys: vec![spendable_note.spend_key],
348 amounts: Amounts::new_bitcoin(amount),
349 });
350 }
351
352 let change_range = global_context
353 .claim_inputs(
354 dbtx,
355 ClientInputBundle::new_no_sm(inputs),
357 )
358 .await
359 .expect("Cannot claim input, additional funding needed");
360
361 let refund_txid = change_range.txid();
362 MintInputStateMachine {
363 common: old_state.common,
364 state: if spendable_notes.len() == 1 {
365 MintInputStates::RefundedPerNote(MintInputStateRefundedPerNote {
367 refund_txids: vec![refund_txid],
368 })
369 } else {
370 MintInputStates::RefundedBundle(MintInputStateRefundedBundle {
371 refund_txid,
372 spendable_notes,
373 })
374 },
375 }
376 }
377}
378
379#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
380pub struct MintInputStateRefundedBundle {
381 pub(crate) refund_txid: TransactionId,
382 pub(crate) spendable_notes: Vec<(Amount, SpendableNote)>,
383}
384
385impl MintInputStateRefundedBundle {
386 fn transitions(
387 state: &MintInputStateRefundedBundle,
388 global_context: &DynGlobalClientContext,
389 ) -> Vec<StateTransition<MintInputStateMachine>> {
390 let global_context = global_context.clone();
391 vec![StateTransition::new(
392 Self::await_success(state.refund_txid, global_context.clone()),
393 move |dbtx, result, old_state| {
394 Box::pin(Self::transition_success(
395 result,
396 old_state,
397 dbtx,
398 global_context.clone(),
399 ))
400 },
401 )]
402 }
403
404 async fn await_success(
405 txid: TransactionId,
406 global_context: DynGlobalClientContext,
407 ) -> Result<(), String> {
408 global_context.await_tx_accepted(txid).await
409 }
410
411 async fn transition_success(
412 result: Result<(), String>,
413 old_state: MintInputStateMachine,
414 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
415 global_context: DynGlobalClientContext,
416 ) -> MintInputStateMachine {
417 assert_matches!(old_state.state, MintInputStates::RefundedBundle(_));
418
419 match result {
420 Ok(()) => {
421 MintInputStateMachine {
423 common: old_state.common,
424 state: MintInputStates::Success(MintInputStateSuccess {}),
425 }
426 }
427 Err(err) => {
428 debug!(target: LOG_CLIENT_MODULE_MINT, err = %err.as_str(), "Refunding mint transaction input due to transaction error on multi-note refund");
430 Self::refund(dbtx, old_state, global_context).await
431 }
432 }
433 }
434
435 async fn refund(
436 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
437 old_state: MintInputStateMachine,
438 global_context: DynGlobalClientContext,
439 ) -> MintInputStateMachine {
440 let spendable_notes = match old_state.state {
441 MintInputStates::RefundedBundle(created) => created.spendable_notes,
442 _ => panic!("Invalid state transition"),
443 };
444
445 let mut refund_txids = vec![];
446 for (amount, spendable_note) in spendable_notes {
447 let refund_input = ClientInput::<MintInput> {
448 input: MintInput::new_v0(amount, spendable_note.note()),
449 keys: vec![spendable_note.spend_key],
450 amounts: Amounts::new_bitcoin(amount),
451 };
452 match global_context
453 .claim_inputs(
454 dbtx,
455 ClientInputBundle::new_no_sm(vec![refund_input.clone()]),
458 )
459 .await
460 {
461 Ok(change_range) => {
462 refund_txids.push(change_range.txid());
463 }
464 Err(err) => {
465 warn!(
466 target: LOG_CLIENT_MODULE_MINT,
467 err = %err.fmt_compact_anyhow(),
468 refund_input_amounts = ?refund_input.amounts,
469 input = %refund_input.input,
470 "Failed to remint a single note"
471 );
472 }
473 }
474 }
475
476 MintInputStateMachine {
477 common: old_state.common,
478 state: MintInputStates::RefundedPerNote(MintInputStateRefundedPerNote { refund_txids }),
479 }
480 }
481}
482
483#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
484pub struct MintInputStateRefund {
485 pub refund_txid: TransactionId,
486}
487
488impl MintInputStateRefund {
489 fn transitions(
490 &self,
491 global_context: &DynGlobalClientContext,
492 ) -> Vec<StateTransition<MintInputStateMachine>> {
493 vec![StateTransition::new(
494 Self::await_refund_success(global_context.clone(), self.refund_txid),
495 |_dbtx, result, old_state| {
496 Box::pin(async { Self::transition_refund_success(result, old_state) })
497 },
498 )]
499 }
500
501 async fn await_refund_success(
502 global_context: DynGlobalClientContext,
503 refund_txid: TransactionId,
504 ) -> Result<(), String> {
505 global_context.await_tx_accepted(refund_txid).await
506 }
507
508 fn transition_refund_success(
509 result: Result<(), String>,
510 old_state: MintInputStateMachine,
511 ) -> MintInputStateMachine {
512 let refund_txid = match old_state.state {
513 MintInputStates::Refund(refund) => refund.refund_txid,
514 _ => panic!("Invalid state transition"),
515 };
516
517 match result {
518 Ok(()) => {
519 MintInputStateMachine {
521 common: old_state.common,
522 state: MintInputStates::RefundSuccess(MintInputStateRefundSuccess {
523 refund_txid,
524 }),
525 }
526 }
527 Err(err) => {
528 warn!(target: LOG_CLIENT_MODULE_MINT, err = %err.as_str(), %refund_txid, "Refund transaction rejected. Notes probably lost.");
531 MintInputStateMachine {
532 common: old_state.common,
533 state: MintInputStates::Error(MintInputStateError {
534 error: format!("Refund transaction {refund_txid} was rejected"),
535 }),
536 }
537 }
538 }
539 }
540}
541
542#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
543pub struct MintInputStateRefundedPerNote {
544 pub refund_txids: Vec<TransactionId>,
545}
546
547#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
548pub struct MintInputStateSuccess {}
549
550#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
551pub struct MintInputStateError {
552 error: String,
553}
554
555#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
556pub struct MintInputStateRefundSuccess {
557 refund_txid: TransactionId,
558}