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
134impl MintOOBStatesCreated {
135 fn transitions(
136 &self,
137 operation_id: OperationId,
138 context: &MintClientContext,
139 global_context: &DynGlobalClientContext,
140 ) -> Vec<StateTransition<MintOOBStateMachine>> {
141 let user_cancel_gc = global_context.clone();
142 let timeout_cancel_gc = global_context.clone();
143 vec![
144 StateTransition::new(
145 context.await_cancel_oob_payment(operation_id),
146 move |dbtx, (), state| {
147 Box::pin(transition_user_cancel(state, dbtx, user_cancel_gc.clone()))
148 },
149 ),
150 StateTransition::new(
151 await_timeout_cancel(self.timeout),
152 move |dbtx, (), state| {
153 Box::pin(transition_timeout_cancel(
154 state,
155 dbtx,
156 timeout_cancel_gc.clone(),
157 ))
158 },
159 ),
160 ]
161 }
162}
163
164impl MintOOBStatesCreatedMulti {
165 fn transitions(
166 &self,
167 operation_id: OperationId,
168 context: &MintClientContext,
169 global_context: &DynGlobalClientContext,
170 ) -> Vec<StateTransition<MintOOBStateMachine>> {
171 let user_cancel_gc = global_context.clone();
172 let timeout_cancel_gc = global_context.clone();
173 vec![
174 StateTransition::new(
175 context.await_cancel_oob_payment(operation_id),
176 move |dbtx, (), state| {
177 Box::pin(transition_user_cancel_multi(
178 state,
179 dbtx,
180 user_cancel_gc.clone(),
181 ))
182 },
183 ),
184 StateTransition::new(
185 await_timeout_cancel(self.timeout),
186 move |dbtx, (), state| {
187 Box::pin(transition_timeout_cancel_multi(
188 state,
189 dbtx,
190 timeout_cancel_gc.clone(),
191 ))
192 },
193 ),
194 ]
195 }
196}
197
198async fn transition_user_cancel(
199 prev_state: MintOOBStateMachine,
200 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
201 global_context: DynGlobalClientContext,
202) -> MintOOBStateMachine {
203 let (amount, spendable_note) = match prev_state.state {
204 MintOOBStates::Created(created) => (created.amount, created.spendable_note),
205 _ => panic!("Invalid previous state: {prev_state:?}"),
206 };
207
208 let refund_txid = try_cancel_oob_spend(
209 dbtx,
210 prev_state.operation_id,
211 amount,
212 spendable_note,
213 global_context,
214 )
215 .await;
216 MintOOBStateMachine {
217 operation_id: prev_state.operation_id,
218 state: MintOOBStates::UserRefund(MintOOBStatesUserRefund { refund_txid }),
219 }
220}
221
222async fn transition_user_cancel_multi(
223 prev_state: MintOOBStateMachine,
224 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
225 global_context: DynGlobalClientContext,
226) -> MintOOBStateMachine {
227 let spendable_notes = match prev_state.state {
228 MintOOBStates::CreatedMulti(created) => created.spendable_notes,
229 _ => panic!("Invalid previous state: {prev_state:?}"),
230 };
231
232 let refund_txid = try_cancel_oob_spend_multi(
233 dbtx,
234 prev_state.operation_id,
235 spendable_notes.clone(),
236 global_context,
237 )
238 .await;
239 MintOOBStateMachine {
240 operation_id: prev_state.operation_id,
241 state: MintOOBStates::UserRefundMulti(MintOOBStatesUserRefundMulti {
242 refund_txid,
243 spendable_notes,
244 }),
245 }
246}
247
248async fn await_timeout_cancel(deadline: SystemTime) {
249 if let Ok(time_until_deadline) = deadline.duration_since(fedimint_core::time::now()) {
250 runtime::sleep(time_until_deadline).await;
251 }
252}
253
254async fn transition_timeout_cancel(
255 prev_state: MintOOBStateMachine,
256 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
257 global_context: DynGlobalClientContext,
258) -> MintOOBStateMachine {
259 let (amount, spendable_note) = match prev_state.state {
260 MintOOBStates::Created(created) => (created.amount, created.spendable_note),
261 _ => panic!("Invalid previous state: {prev_state:?}"),
262 };
263
264 let refund_txid = try_cancel_oob_spend(
265 dbtx,
266 prev_state.operation_id,
267 amount,
268 spendable_note,
269 global_context,
270 )
271 .await;
272 MintOOBStateMachine {
273 operation_id: prev_state.operation_id,
274 state: MintOOBStates::TimeoutRefund(MintOOBStatesTimeoutRefund { refund_txid }),
275 }
276}
277
278async fn transition_timeout_cancel_multi(
279 prev_state: MintOOBStateMachine,
280 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
281 global_context: DynGlobalClientContext,
282) -> MintOOBStateMachine {
283 let spendable_notes = match prev_state.state {
284 MintOOBStates::CreatedMulti(created) => created.spendable_notes,
285 _ => panic!("Invalid previous state: {prev_state:?}"),
286 };
287
288 let refund_txid = try_cancel_oob_spend_multi(
289 dbtx,
290 prev_state.operation_id,
291 spendable_notes,
292 global_context,
293 )
294 .await;
295 MintOOBStateMachine {
296 operation_id: prev_state.operation_id,
297 state: MintOOBStates::TimeoutRefund(MintOOBStatesTimeoutRefund { refund_txid }),
298 }
299}
300
301async fn try_cancel_oob_spend(
302 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
303 operation_id: OperationId,
304 amount: Amount,
305 spendable_note: SpendableNote,
306 global_context: DynGlobalClientContext,
307) -> TransactionId {
308 try_cancel_oob_spend_multi(
309 dbtx,
310 operation_id,
311 vec![(amount, spendable_note)],
312 global_context,
313 )
314 .await
315}
316
317async fn try_cancel_oob_spend_multi(
318 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
319 operation_id: OperationId,
320 spendable_notes: Vec<(Amount, SpendableNote)>,
321 global_context: DynGlobalClientContext,
322) -> TransactionId {
323 let inputs = spendable_notes
324 .clone()
325 .into_iter()
326 .map(|(amount, spendable_note)| ClientInput {
327 input: MintInput::new_v0(amount, spendable_note.note()),
328 keys: vec![spendable_note.spend_key],
329 amounts: Amounts::new_bitcoin(amount),
330 })
331 .collect();
332
333 let sm = ClientInputSM {
334 state_machines: Arc::new(move |out_point_range: OutPointRange| {
335 debug_assert_eq!(out_point_range.count(), spendable_notes.len());
336 vec![MintClientStateMachines::Input(MintInputStateMachine {
337 common: MintInputCommon {
338 operation_id,
339 out_point_range,
340 },
341 state: MintInputStates::RefundedBundle(MintInputStateRefundedBundle {
346 refund_txid: out_point_range.txid(),
347 spendable_notes: spendable_notes.clone(),
348 }),
349 })]
350 }),
351 };
352
353 global_context
354 .claim_inputs(dbtx, ClientInputBundle::new(inputs, vec![sm]))
355 .await
356 .expect("Cannot claim input, additional funding needed")
357 .txid()
358}