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