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
112#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
113pub struct MintInputStateCreated {
114 pub(crate) amount: Amount,
115 pub(crate) spendable_note: SpendableNote,
116}
117
118impl MintInputStateCreated {
119 fn transitions(
120 common: MintInputCommon,
121 global_context: &DynGlobalClientContext,
122 ) -> Vec<StateTransition<MintInputStateMachine>> {
123 let global_context = global_context.clone();
124 vec![StateTransition::new(
125 Self::await_success(common, global_context.clone()),
126 move |dbtx, result, old_state| {
127 Box::pin(Self::transition_success(
128 result,
129 old_state,
130 dbtx,
131 global_context.clone(),
132 ))
133 },
134 )]
135 }
136
137 async fn await_success(
138 common: MintInputCommon,
139 global_context: DynGlobalClientContext,
140 ) -> Result<(), String> {
141 global_context
142 .await_tx_accepted(common.out_point_range.txid())
143 .await
144 }
145
146 #[allow(deprecated)]
147 async fn transition_success(
148 result: Result<(), String>,
149 old_state: MintInputStateMachine,
150 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
151 global_context: DynGlobalClientContext,
152 ) -> MintInputStateMachine {
153 assert_matches!(old_state.state, MintInputStates::Created(_));
154
155 match result {
156 Ok(()) => {
157 MintInputStateMachine {
159 common: old_state.common,
160 state: MintInputStates::Success(MintInputStateSuccess {}),
161 }
162 }
163 Err(err) => {
164 debug!(target: LOG_CLIENT_MODULE_MINT, err = %err.as_str(), "Refunding mint transaction input due to transaction error");
166 Self::refund(dbtx, old_state, global_context).await
167 }
168 }
169 }
170
171 #[allow(deprecated)]
172 async fn refund(
173 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
174 old_state: MintInputStateMachine,
175 global_context: DynGlobalClientContext,
176 ) -> MintInputStateMachine {
177 let (amount, spendable_note) = match old_state.state {
178 MintInputStates::Created(created) => (created.amount, created.spendable_note),
179 _ => panic!("Invalid state transition"),
180 };
181
182 let refund_input = ClientInput::<MintInput> {
183 input: MintInput::new_v0(amount, spendable_note.note()),
184 keys: vec![spendable_note.spend_key],
185 amounts: Amounts::new_bitcoin(amount),
186 };
187
188 let change_range = global_context
189 .claim_inputs(
190 dbtx,
191 ClientInputBundle::new_no_sm(vec![refund_input]),
194 )
195 .await
196 .expect("Cannot claim input, additional funding needed");
197
198 MintInputStateMachine {
199 common: old_state.common,
200 state: MintInputStates::Refund(MintInputStateRefund {
201 refund_txid: change_range.txid(),
202 }),
203 }
204 }
205}
206
207#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
208pub struct MintInputStateCreatedBundle {
209 pub(crate) notes: Vec<(Amount, SpendableNote)>,
210}
211
212impl MintInputStateCreatedBundle {
213 fn transitions(
214 common: MintInputCommon,
215 global_context: &DynGlobalClientContext,
216 ) -> Vec<StateTransition<MintInputStateMachine>> {
217 let global_context = global_context.clone();
218 vec![StateTransition::new(
219 Self::await_success(common, global_context.clone()),
220 move |dbtx, result, old_state| {
221 Box::pin(Self::transition_success(
222 result,
223 old_state,
224 dbtx,
225 global_context.clone(),
226 ))
227 },
228 )]
229 }
230
231 async fn await_success(
232 common: MintInputCommon,
233 global_context: DynGlobalClientContext,
234 ) -> Result<(), String> {
235 global_context
236 .await_tx_accepted(common.out_point_range.txid())
237 .await
238 }
239
240 async fn transition_success(
241 result: Result<(), String>,
242 old_state: MintInputStateMachine,
243 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
244 global_context: DynGlobalClientContext,
245 ) -> MintInputStateMachine {
246 assert_matches!(old_state.state, MintInputStates::CreatedBundle(_));
247
248 match result {
249 Ok(()) => {
250 MintInputStateMachine {
252 common: old_state.common,
253 state: MintInputStates::Success(MintInputStateSuccess {}),
254 }
255 }
256 Err(err) => {
257 debug!(target: LOG_CLIENT_MODULE_MINT, err = %err.as_str(), "Refunding mint transaction input due to transaction error");
259 Self::refund(dbtx, old_state, global_context).await
260 }
261 }
262 }
263
264 async fn refund(
265 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
266 old_state: MintInputStateMachine,
267 global_context: DynGlobalClientContext,
268 ) -> MintInputStateMachine {
269 let spendable_notes = match old_state.state {
270 MintInputStates::CreatedBundle(created) => created.notes,
271 _ => panic!("Invalid state transition"),
272 };
273
274 let mut inputs = Vec::new();
275
276 for (amount, spendable_note) in spendable_notes.clone() {
277 inputs.push(ClientInput::<MintInput> {
278 input: MintInput::new_v0(amount, spendable_note.note()),
279 keys: vec![spendable_note.spend_key],
280 amounts: Amounts::new_bitcoin(amount),
281 });
282 }
283
284 let change_range = global_context
285 .claim_inputs(
286 dbtx,
287 ClientInputBundle::new_no_sm(inputs),
289 )
290 .await
291 .expect("Cannot claim input, additional funding needed");
292
293 let refund_txid = change_range.txid();
294 MintInputStateMachine {
295 common: old_state.common,
296 state: if spendable_notes.len() == 1 {
297 MintInputStates::RefundedPerNote(MintInputStateRefundedPerNote {
299 refund_txids: vec![refund_txid],
300 })
301 } else {
302 MintInputStates::RefundedBundle(MintInputStateRefundedBundle {
303 refund_txid,
304 spendable_notes,
305 })
306 },
307 }
308 }
309}
310
311#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
312pub struct MintInputStateRefundedBundle {
313 pub(crate) refund_txid: TransactionId,
314 pub(crate) spendable_notes: Vec<(Amount, SpendableNote)>,
315}
316
317impl MintInputStateRefundedBundle {
318 fn transitions(
319 state: &MintInputStateRefundedBundle,
320 global_context: &DynGlobalClientContext,
321 ) -> Vec<StateTransition<MintInputStateMachine>> {
322 let global_context = global_context.clone();
323 vec![StateTransition::new(
324 Self::await_success(state.refund_txid, global_context.clone()),
325 move |dbtx, result, old_state| {
326 Box::pin(Self::transition_success(
327 result,
328 old_state,
329 dbtx,
330 global_context.clone(),
331 ))
332 },
333 )]
334 }
335
336 async fn await_success(
337 txid: TransactionId,
338 global_context: DynGlobalClientContext,
339 ) -> Result<(), String> {
340 global_context.await_tx_accepted(txid).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!(old_state.state, MintInputStates::RefundedBundle(_));
350
351 match result {
352 Ok(()) => {
353 MintInputStateMachine {
355 common: old_state.common,
356 state: MintInputStates::Success(MintInputStateSuccess {}),
357 }
358 }
359 Err(err) => {
360 debug!(target: LOG_CLIENT_MODULE_MINT, err = %err.as_str(), "Refunding mint transaction input due to transaction error on multi-note refund");
362 Self::refund(dbtx, old_state, global_context).await
363 }
364 }
365 }
366
367 async fn refund(
368 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
369 old_state: MintInputStateMachine,
370 global_context: DynGlobalClientContext,
371 ) -> MintInputStateMachine {
372 let spendable_notes = match old_state.state {
373 MintInputStates::RefundedBundle(created) => created.spendable_notes,
374 _ => panic!("Invalid state transition"),
375 };
376
377 let mut refund_txids = vec![];
378 for (amount, spendable_note) in spendable_notes {
379 let refund_input = ClientInput::<MintInput> {
380 input: MintInput::new_v0(amount, spendable_note.note()),
381 keys: vec![spendable_note.spend_key],
382 amounts: Amounts::new_bitcoin(amount),
383 };
384 match global_context
385 .claim_inputs(
386 dbtx,
387 ClientInputBundle::new_no_sm(vec![refund_input.clone()]),
390 )
391 .await
392 {
393 Ok(change_range) => {
394 refund_txids.push(change_range.txid());
395 }
396 Err(err) => {
397 warn!(
398 target: LOG_CLIENT_MODULE_MINT,
399 err = %err.fmt_compact_anyhow(),
400 refund_input_amounts = ?refund_input.amounts,
401 input = %refund_input.input,
402 "Failed to remint a single note"
403 );
404 }
405 }
406 }
407
408 MintInputStateMachine {
409 common: old_state.common,
410 state: MintInputStates::RefundedPerNote(MintInputStateRefundedPerNote { refund_txids }),
411 }
412 }
413}
414
415#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
416pub struct MintInputStateRefund {
417 pub refund_txid: TransactionId,
418}
419
420impl MintInputStateRefund {
421 fn transitions(
422 &self,
423 global_context: &DynGlobalClientContext,
424 ) -> Vec<StateTransition<MintInputStateMachine>> {
425 vec![StateTransition::new(
426 Self::await_refund_success(global_context.clone(), self.refund_txid),
427 |_dbtx, result, old_state| {
428 Box::pin(async { Self::transition_refund_success(result, old_state) })
429 },
430 )]
431 }
432
433 async fn await_refund_success(
434 global_context: DynGlobalClientContext,
435 refund_txid: TransactionId,
436 ) -> Result<(), String> {
437 global_context.await_tx_accepted(refund_txid).await
438 }
439
440 fn transition_refund_success(
441 result: Result<(), String>,
442 old_state: MintInputStateMachine,
443 ) -> MintInputStateMachine {
444 let refund_txid = match old_state.state {
445 MintInputStates::Refund(refund) => refund.refund_txid,
446 _ => panic!("Invalid state transition"),
447 };
448
449 match result {
450 Ok(()) => {
451 MintInputStateMachine {
453 common: old_state.common,
454 state: MintInputStates::RefundSuccess(MintInputStateRefundSuccess {
455 refund_txid,
456 }),
457 }
458 }
459 Err(err) => {
460 warn!(target: LOG_CLIENT_MODULE_MINT, err = %err.as_str(), %refund_txid, "Refund transaction rejected. Notes probably lost.");
463 MintInputStateMachine {
464 common: old_state.common,
465 state: MintInputStates::Error(MintInputStateError {
466 error: format!("Refund transaction {refund_txid} was rejected"),
467 }),
468 }
469 }
470 }
471 }
472}
473
474#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
475pub struct MintInputStateRefundedPerNote {
476 pub refund_txids: Vec<TransactionId>,
477}
478
479#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
480pub struct MintInputStateSuccess {}
481
482#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
483pub struct MintInputStateError {
484 error: String,
485}
486
487#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
488pub struct MintInputStateRefundSuccess {
489 refund_txid: TransactionId,
490}