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