1use std::sync::Arc;
2use std::time::SystemTime;
3
4use fedimint_client_module::DynGlobalClientContext;
5use fedimint_client_module::module::OutPointRange;
6use fedimint_client_module::sm::{ClientSMDatabaseTransaction, State, StateTransition};
7use fedimint_client_module::transaction::{ClientInput, ClientInputBundle, ClientInputSM};
8use fedimint_core::core::OperationId;
9use fedimint_core::encoding::{
10 Decodable, DecodeError, Encodable, decode_field_from_finite_reader,
11 decode_legacy_system_time_from_finite_reader, encode_legacy_system_time, with_decoding_context,
12};
13use fedimint_core::module::Amounts;
14use fedimint_core::module::registry::ModuleDecoderRegistry;
15use fedimint_core::{Amount, TransactionId, runtime};
16use fedimint_mint_common::MintInput;
17
18use crate::input::{
19 MintInputCommon, MintInputStateMachine, MintInputStateRefundedBundle, MintInputStates,
20};
21use crate::{MintClientContext, MintClientStateMachines, SpendableNote};
22
23#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
24pub enum MintOOBStatesV0 {
25 Created(MintOOBStatesCreated),
28 UserRefund(MintOOBStatesUserRefund),
30 TimeoutRefund(MintOOBStatesTimeoutRefund),
34}
35
36#[cfg_attr(doc, aquamarine::aquamarine)]
37#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
46pub enum MintOOBStates {
47 CreatedMulti(MintOOBStatesCreatedMulti),
50 UserRefundMulti(MintOOBStatesUserRefundMulti),
52 TimeoutRefund(MintOOBStatesTimeoutRefund),
56
57 Created(MintOOBStatesCreated),
62 UserRefund(MintOOBStatesUserRefund),
65}
66
67#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
68pub struct MintOOBStateMachineV0 {
69 pub(crate) operation_id: OperationId,
70 pub(crate) state: MintOOBStatesV0,
71}
72
73#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
74pub struct MintOOBStateMachine {
75 pub(crate) operation_id: OperationId,
76 pub(crate) state: MintOOBStates,
77}
78
79#[derive(Debug, Clone, Eq, PartialEq, Hash)]
80pub struct MintOOBStatesCreated {
81 pub(crate) amount: Amount,
82 pub(crate) spendable_note: SpendableNote,
83 pub(crate) timeout: SystemTime,
84}
85
86impl Encodable for MintOOBStatesCreated {
87 fn consensus_encode<W: std::io::Write>(&self, writer: &mut W) -> Result<(), std::io::Error> {
88 self.amount.consensus_encode(writer)?;
89 self.spendable_note.consensus_encode(writer)?;
90 encode_legacy_system_time(&self.timeout, writer)
91 }
92}
93
94impl Decodable for MintOOBStatesCreated {
95 fn consensus_decode_partial_from_finite_reader<D: std::io::Read>(
96 decoder: &mut D,
97 modules: &ModuleDecoderRegistry,
98 ) -> Result<Self, DecodeError> {
99 Ok(Self {
100 amount: decode_field_from_finite_reader(
101 decoder,
102 modules,
103 "Decoding named block field: MintOOBStatesCreated{ ... amount ... }",
104 )?,
105 spendable_note: decode_field_from_finite_reader(
106 decoder,
107 modules,
108 "Decoding named block field: MintOOBStatesCreated{ ... spendable_note ... }",
109 )?,
110 timeout: with_decoding_context(
111 decode_legacy_system_time_from_finite_reader(decoder, modules),
112 "Decoding named block field: MintOOBStatesCreated{ ... timeout ... }",
113 )?,
114 })
115 }
116}
117
118#[derive(Debug, Clone, Eq, PartialEq, Hash)]
119pub struct MintOOBStatesCreatedMulti {
120 pub(crate) spendable_notes: Vec<(Amount, SpendableNote)>,
121 pub(crate) timeout: SystemTime,
122}
123
124impl Encodable for MintOOBStatesCreatedMulti {
125 fn consensus_encode<W: std::io::Write>(&self, writer: &mut W) -> Result<(), std::io::Error> {
126 self.spendable_notes.consensus_encode(writer)?;
127 encode_legacy_system_time(&self.timeout, writer)
128 }
129}
130
131impl Decodable for MintOOBStatesCreatedMulti {
132 fn consensus_decode_partial_from_finite_reader<D: std::io::Read>(
133 decoder: &mut D,
134 modules: &ModuleDecoderRegistry,
135 ) -> Result<Self, DecodeError> {
136 Ok(Self {
137 spendable_notes: decode_field_from_finite_reader(
138 decoder,
139 modules,
140 "Decoding named block field: MintOOBStatesCreatedMulti{ ... spendable_notes ... }",
141 )?,
142 timeout: with_decoding_context(
143 decode_legacy_system_time_from_finite_reader(decoder, modules),
144 "Decoding named block field: MintOOBStatesCreatedMulti{ ... timeout ... }",
145 )?,
146 })
147 }
148}
149
150#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
151pub struct MintOOBStatesUserRefundMulti {
152 pub(crate) refund_txid: TransactionId,
154 pub(crate) spendable_notes: Vec<(Amount, SpendableNote)>,
156}
157
158#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
159pub struct MintOOBStatesUserRefund {
160 pub(crate) refund_txid: TransactionId,
161}
162
163#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
164pub struct MintOOBStatesTimeoutRefund {
165 pub(crate) refund_txid: TransactionId,
166}
167
168impl State for MintOOBStateMachine {
169 type ModuleContext = MintClientContext;
170
171 fn transitions(
172 &self,
173 context: &Self::ModuleContext,
174 global_context: &DynGlobalClientContext,
175 ) -> Vec<StateTransition<Self>> {
176 match &self.state {
177 MintOOBStates::Created(created) => {
178 created.transitions(self.operation_id, context, global_context)
179 }
180 MintOOBStates::CreatedMulti(created) => {
181 created.transitions(self.operation_id, context, global_context)
182 }
183 MintOOBStates::UserRefund(_)
184 | MintOOBStates::TimeoutRefund(_)
185 | MintOOBStates::UserRefundMulti(_) => {
186 vec![]
187 }
188 }
189 }
190
191 fn operation_id(&self) -> OperationId {
192 self.operation_id
193 }
194
195 fn fmt_visualization(&self, f: &mut dyn std::fmt::Write, indent: &str) -> std::fmt::Result {
196 match &self.state {
197 MintOOBStates::Created(c) => {
198 write!(
199 f,
200 "{indent}MintOOBStateMachine\n\
201 {indent} state: Created amount={} nonce={}",
202 c.amount,
203 c.spendable_note.nonce().fmt_short(),
204 )
205 }
206 MintOOBStates::CreatedMulti(c) => {
207 let total: Amount = c.spendable_notes.iter().map(|(a, _)| *a).sum();
208 write!(
209 f,
210 "{indent}MintOOBStateMachine\n\
211 {indent} state: CreatedMulti {} notes, total={total}",
212 c.spendable_notes.len(),
213 )?;
214 for (amount, note) in &c.spendable_notes {
215 write!(
216 f,
217 "\n{indent} note: amount={amount} nonce={}",
218 note.nonce().fmt_short(),
219 )?;
220 }
221 Ok(())
222 }
223 MintOOBStates::UserRefund(r) => {
224 write!(
225 f,
226 "{indent}MintOOBStateMachine\n\
227 {indent} state: UserRefund refund_txid={}",
228 r.refund_txid.fmt_short(),
229 )
230 }
231 MintOOBStates::UserRefundMulti(r) => {
232 let total: Amount = r.spendable_notes.iter().map(|(a, _)| *a).sum();
233 write!(
234 f,
235 "{indent}MintOOBStateMachine\n\
236 {indent} state: UserRefundMulti refund_txid={} {} notes, total={total}",
237 r.refund_txid.fmt_short(),
238 r.spendable_notes.len(),
239 )
240 }
241 MintOOBStates::TimeoutRefund(r) => {
242 write!(
243 f,
244 "{indent}MintOOBStateMachine\n\
245 {indent} state: TimeoutRefund refund_txid={}",
246 r.refund_txid.fmt_short(),
247 )
248 }
249 }
250 }
251}
252
253impl MintOOBStatesCreated {
254 fn transitions(
255 &self,
256 operation_id: OperationId,
257 context: &MintClientContext,
258 global_context: &DynGlobalClientContext,
259 ) -> Vec<StateTransition<MintOOBStateMachine>> {
260 let user_cancel_gc = global_context.clone();
261 let timeout_cancel_gc = global_context.clone();
262 vec![
263 StateTransition::new(
264 context.await_cancel_oob_payment(operation_id),
265 move |dbtx, (), state| {
266 Box::pin(transition_user_cancel(state, dbtx, user_cancel_gc.clone()))
267 },
268 ),
269 StateTransition::new(
270 await_timeout_cancel(self.timeout),
271 move |dbtx, (), state| {
272 Box::pin(transition_timeout_cancel(
273 state,
274 dbtx,
275 timeout_cancel_gc.clone(),
276 ))
277 },
278 ),
279 ]
280 }
281}
282
283impl MintOOBStatesCreatedMulti {
284 fn transitions(
285 &self,
286 operation_id: OperationId,
287 context: &MintClientContext,
288 global_context: &DynGlobalClientContext,
289 ) -> Vec<StateTransition<MintOOBStateMachine>> {
290 let user_cancel_gc = global_context.clone();
291 let timeout_cancel_gc = global_context.clone();
292 vec![
293 StateTransition::new(
294 context.await_cancel_oob_payment(operation_id),
295 move |dbtx, (), state| {
296 Box::pin(transition_user_cancel_multi(
297 state,
298 dbtx,
299 user_cancel_gc.clone(),
300 ))
301 },
302 ),
303 StateTransition::new(
304 await_timeout_cancel(self.timeout),
305 move |dbtx, (), state| {
306 Box::pin(transition_timeout_cancel_multi(
307 state,
308 dbtx,
309 timeout_cancel_gc.clone(),
310 ))
311 },
312 ),
313 ]
314 }
315}
316
317async fn transition_user_cancel(
318 prev_state: MintOOBStateMachine,
319 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
320 global_context: DynGlobalClientContext,
321) -> MintOOBStateMachine {
322 let (amount, spendable_note) = match prev_state.state {
323 MintOOBStates::Created(created) => (created.amount, created.spendable_note),
324 _ => panic!("Invalid previous state: {prev_state:?}"),
325 };
326
327 let refund_txid = try_cancel_oob_spend(
328 dbtx,
329 prev_state.operation_id,
330 amount,
331 spendable_note,
332 global_context,
333 )
334 .await;
335 MintOOBStateMachine {
336 operation_id: prev_state.operation_id,
337 state: MintOOBStates::UserRefund(MintOOBStatesUserRefund { refund_txid }),
338 }
339}
340
341async fn transition_user_cancel_multi(
342 prev_state: MintOOBStateMachine,
343 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
344 global_context: DynGlobalClientContext,
345) -> MintOOBStateMachine {
346 let spendable_notes = match prev_state.state {
347 MintOOBStates::CreatedMulti(created) => created.spendable_notes,
348 _ => panic!("Invalid previous state: {prev_state:?}"),
349 };
350
351 let refund_txid = try_cancel_oob_spend_multi(
352 dbtx,
353 prev_state.operation_id,
354 spendable_notes.clone(),
355 global_context,
356 )
357 .await;
358 MintOOBStateMachine {
359 operation_id: prev_state.operation_id,
360 state: MintOOBStates::UserRefundMulti(MintOOBStatesUserRefundMulti {
361 refund_txid,
362 spendable_notes,
363 }),
364 }
365}
366
367async fn await_timeout_cancel(deadline: SystemTime) {
368 if let Ok(time_until_deadline) = deadline.duration_since(fedimint_core::time::now()) {
369 runtime::sleep(time_until_deadline).await;
370 }
371}
372
373async fn transition_timeout_cancel(
374 prev_state: MintOOBStateMachine,
375 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
376 global_context: DynGlobalClientContext,
377) -> MintOOBStateMachine {
378 let (amount, spendable_note) = match prev_state.state {
379 MintOOBStates::Created(created) => (created.amount, created.spendable_note),
380 _ => panic!("Invalid previous state: {prev_state:?}"),
381 };
382
383 let refund_txid = try_cancel_oob_spend(
384 dbtx,
385 prev_state.operation_id,
386 amount,
387 spendable_note,
388 global_context,
389 )
390 .await;
391 MintOOBStateMachine {
392 operation_id: prev_state.operation_id,
393 state: MintOOBStates::TimeoutRefund(MintOOBStatesTimeoutRefund { refund_txid }),
394 }
395}
396
397async fn transition_timeout_cancel_multi(
398 prev_state: MintOOBStateMachine,
399 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
400 global_context: DynGlobalClientContext,
401) -> MintOOBStateMachine {
402 let spendable_notes = match prev_state.state {
403 MintOOBStates::CreatedMulti(created) => created.spendable_notes,
404 _ => panic!("Invalid previous state: {prev_state:?}"),
405 };
406
407 let refund_txid = try_cancel_oob_spend_multi(
408 dbtx,
409 prev_state.operation_id,
410 spendable_notes,
411 global_context,
412 )
413 .await;
414 MintOOBStateMachine {
415 operation_id: prev_state.operation_id,
416 state: MintOOBStates::TimeoutRefund(MintOOBStatesTimeoutRefund { refund_txid }),
417 }
418}
419
420async fn try_cancel_oob_spend(
421 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
422 operation_id: OperationId,
423 amount: Amount,
424 spendable_note: SpendableNote,
425 global_context: DynGlobalClientContext,
426) -> TransactionId {
427 try_cancel_oob_spend_multi(
428 dbtx,
429 operation_id,
430 vec![(amount, spendable_note)],
431 global_context,
432 )
433 .await
434}
435
436async fn try_cancel_oob_spend_multi(
437 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
438 operation_id: OperationId,
439 spendable_notes: Vec<(Amount, SpendableNote)>,
440 global_context: DynGlobalClientContext,
441) -> TransactionId {
442 let inputs = spendable_notes
443 .clone()
444 .into_iter()
445 .map(|(amount, spendable_note)| ClientInput {
446 input: MintInput::new_v0(amount, spendable_note.note()),
447 keys: vec![spendable_note.spend_key],
448 amounts: Amounts::new_bitcoin(amount),
449 })
450 .collect();
451
452 let sm = ClientInputSM {
453 state_machines: Arc::new(move |out_point_range: OutPointRange| {
454 debug_assert_eq!(out_point_range.count(), spendable_notes.len());
455 vec![MintClientStateMachines::Input(MintInputStateMachine {
456 common: MintInputCommon {
457 operation_id,
458 out_point_range,
459 },
460 state: MintInputStates::RefundedBundle(MintInputStateRefundedBundle {
465 refund_txid: out_point_range.txid(),
466 spendable_notes: spendable_notes.clone(),
467 }),
468 })]
469 }),
470 };
471
472 global_context
473 .claim_inputs(dbtx, ClientInputBundle::new(inputs, vec![sm]))
474 .await
475 .expect("Cannot claim input, additional funding needed")
476 .txid()
477}