Skip to main content

fedimint_mintv2_client/
input.rs

1use fedimint_client::DynGlobalClientContext;
2use fedimint_client::transaction::{ClientInput, ClientInputBundle};
3use fedimint_client_module::module::OutPointRange;
4use fedimint_client_module::sm::{ClientSMDatabaseTransaction, State, StateTransition};
5use fedimint_core::TransactionId;
6use fedimint_core::core::OperationId;
7use fedimint_core::encoding::{Decodable, Encodable};
8use fedimint_core::module::Amounts;
9use fedimint_mintv2_common::MintInput;
10
11use crate::{MintClientContext, SpendableNote};
12
13#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
14pub struct InputStateMachine {
15    pub common: InputSMCommon,
16    pub state: InputSMState,
17}
18
19#[derive(Debug, Clone, Eq, Hash, PartialEq, Decodable, Encodable)]
20pub struct InputSMCommon {
21    pub operation_id: OperationId,
22    pub txid: TransactionId,
23    pub spendable_notes: Vec<SpendableNote>,
24}
25
26#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
27pub enum InputSMState {
28    Pending,
29    Success,
30    Refunding(OutPointRange),
31}
32
33impl State for InputStateMachine {
34    type ModuleContext = MintClientContext;
35
36    fn transitions(
37        &self,
38        _context: &Self::ModuleContext,
39        global_context: &DynGlobalClientContext,
40    ) -> Vec<StateTransition<Self>> {
41        let gc = global_context.clone();
42
43        match &self.state {
44            InputSMState::Pending => {
45                vec![StateTransition::new(
46                    Self::await_pending_transaction(gc.clone(), self.common.txid),
47                    move |dbtx, result, old_state| {
48                        Box::pin(Self::transition_pending_transaction(
49                            gc.clone(),
50                            dbtx,
51                            result,
52                            old_state,
53                        ))
54                    },
55                )]
56            }
57            InputSMState::Success | InputSMState::Refunding(..) => {
58                vec![]
59            }
60        }
61    }
62
63    fn operation_id(&self) -> OperationId {
64        self.common.operation_id
65    }
66}
67
68impl InputStateMachine {
69    async fn await_pending_transaction(
70        global_context: DynGlobalClientContext,
71        txid: TransactionId,
72    ) -> Result<(), String> {
73        global_context.await_tx_accepted(txid).await
74    }
75
76    async fn transition_pending_transaction(
77        global_context: DynGlobalClientContext,
78        dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
79        result: Result<(), String>,
80        old_state: InputStateMachine,
81    ) -> InputStateMachine {
82        if result.is_ok() {
83            return InputStateMachine {
84                common: old_state.common,
85                state: InputSMState::Success,
86            };
87        }
88
89        let inputs = old_state
90            .common
91            .spendable_notes
92            .iter()
93            .map(|spendable_note| ClientInput::<MintInput> {
94                input: MintInput::new_v0(spendable_note.note()),
95                keys: vec![spendable_note.keypair],
96                amounts: Amounts::new_bitcoin(spendable_note.amount()),
97            })
98            .collect();
99
100        let change_range = global_context
101            .claim_inputs(dbtx, ClientInputBundle::new_no_sm(inputs))
102            .await
103            .expect("Cannot claim input, additional funding needed");
104
105        InputStateMachine {
106            common: old_state.common,
107            state: InputSMState::Refunding(change_range),
108        }
109    }
110}