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