fedimint_dummy_client/
states.rs

1use std::time::Duration;
2
3use fedimint_client::sm::{DynState, State, StateTransition};
4use fedimint_client::DynGlobalClientContext;
5use fedimint_core::core::{Decoder, IntoDynInstance, ModuleInstanceId, OperationId};
6use fedimint_core::db::{DatabaseTransaction, IDatabaseTransactionOpsCoreTyped};
7use fedimint_core::encoding::{Decodable, Encodable};
8use fedimint_core::task::sleep;
9use fedimint_core::{Amount, OutPoint, TransactionId};
10use fedimint_dummy_common::DummyOutputOutcome;
11use serde::{Deserialize, Serialize};
12use thiserror::Error;
13use tracing::debug;
14
15use crate::db::DummyClientFundsKeyV1;
16use crate::{get_funds, DummyClientContext};
17
18const RETRY_DELAY: Duration = Duration::from_secs(1);
19
20/// Tracks a transaction
21#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
22pub enum DummyStateMachine {
23    Input(Amount, TransactionId, OperationId),
24    Output(Amount, TransactionId, OperationId),
25    InputDone(OperationId),
26    OutputDone(Amount, TransactionId, OperationId),
27    Refund(OperationId),
28    Unreachable(OperationId, Amount),
29}
30
31impl State for DummyStateMachine {
32    type ModuleContext = DummyClientContext;
33
34    fn transitions(
35        &self,
36        context: &Self::ModuleContext,
37        global_context: &DynGlobalClientContext,
38    ) -> Vec<StateTransition<Self>> {
39        match self.clone() {
40            DummyStateMachine::Input(amount, txid, id) => vec![StateTransition::new(
41                await_tx_accepted(global_context.clone(), txid),
42                move |dbtx, res, _state: Self| match res {
43                    // accepted, we are done
44                    Ok(()) => Box::pin(async move { DummyStateMachine::InputDone(id) }),
45                    // tx rejected, we refund ourselves
46                    Err(_) => Box::pin(async move {
47                        add_funds(amount, dbtx.module_tx()).await;
48                        DummyStateMachine::Refund(id)
49                    }),
50                },
51            )],
52            DummyStateMachine::Output(amount, txid, id) => vec![StateTransition::new(
53                await_dummy_output_outcome(
54                    global_context.clone(),
55                    OutPoint { txid, out_idx: 0 },
56                    context.dummy_decoder.clone(),
57                ),
58                move |dbtx, res, _state: Self| match res {
59                    // output accepted, add funds
60                    Ok(()) => Box::pin(async move {
61                        add_funds(amount, dbtx.module_tx()).await;
62                        DummyStateMachine::OutputDone(amount, txid, id)
63                    }),
64                    // output rejected, do not add funds
65                    Err(_) => Box::pin(async move { DummyStateMachine::Refund(id) }),
66                },
67            )],
68            DummyStateMachine::InputDone(_)
69            | DummyStateMachine::OutputDone(_, _, _)
70            | DummyStateMachine::Refund(_)
71            | DummyStateMachine::Unreachable(_, _) => vec![],
72        }
73    }
74
75    fn operation_id(&self) -> OperationId {
76        match self {
77            DummyStateMachine::Input(_, _, id)
78            | DummyStateMachine::Output(_, _, id)
79            | DummyStateMachine::InputDone(id)
80            | DummyStateMachine::OutputDone(_, _, id)
81            | DummyStateMachine::Refund(id)
82            | DummyStateMachine::Unreachable(id, _) => *id,
83        }
84    }
85}
86
87async fn add_funds(amount: Amount, mut dbtx: DatabaseTransaction<'_>) {
88    let funds = get_funds(&mut dbtx).await + amount;
89    dbtx.insert_entry(&DummyClientFundsKeyV1, &funds).await;
90}
91
92// TODO: Boiler-plate, should return OutputOutcome
93async fn await_tx_accepted(
94    context: DynGlobalClientContext,
95    txid: TransactionId,
96) -> Result<(), String> {
97    context.await_tx_accepted(txid).await
98}
99
100async fn await_dummy_output_outcome(
101    global_context: DynGlobalClientContext,
102    outpoint: OutPoint,
103    module_decoder: Decoder,
104) -> Result<(), DummyError> {
105    loop {
106        match global_context
107            .api()
108            .await_output_outcome::<DummyOutputOutcome>(
109                outpoint,
110                Duration::from_millis(i32::MAX as u64),
111                &module_decoder,
112            )
113            .await
114        {
115            Ok(_) => {
116                return Ok(());
117            }
118            Err(e) if e.is_rejected() => {
119                return Err(DummyError::DummyInternalError);
120            }
121            Err(e) => {
122                e.report_if_important();
123                debug!(error = %e, "Awaiting output outcome failed, retrying");
124            }
125        }
126        sleep(RETRY_DELAY).await;
127    }
128}
129
130// TODO: Boiler-plate
131impl IntoDynInstance for DummyStateMachine {
132    type DynType = DynState;
133
134    fn into_dyn(self, instance_id: ModuleInstanceId) -> Self::DynType {
135        DynState::from_typed(instance_id, self)
136    }
137}
138
139#[derive(Error, Debug, Serialize, Deserialize, Encodable, Decodable, Clone, Eq, PartialEq)]
140pub enum DummyError {
141    #[error("Dummy module had an internal error")]
142    DummyInternalError,
143}