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