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