fedimint_wallet_client/
withdraw.rs

1use bitcoin::Txid;
2use fedimint_api_client::api::{FederationApiExt, deserialize_outcome};
3use fedimint_client_module::DynGlobalClientContext;
4use fedimint_client_module::sm::{ClientSMDatabaseTransaction, State, StateTransition};
5use fedimint_core::OutPoint;
6use fedimint_core::core::OperationId;
7use fedimint_core::encoding::{Decodable, Encodable};
8#[allow(deprecated)]
9use fedimint_core::endpoint_constants::AWAIT_OUTPUT_OUTCOME_ENDPOINT;
10use fedimint_core::module::ApiRequestErased;
11use fedimint_wallet_common::WalletOutputOutcome;
12use futures::future::pending;
13use tracing::warn;
14
15use crate::WalletClientContext;
16use crate::events::WithdrawRequest;
17
18// TODO: track tx confirmations
19#[aquamarine::aquamarine]
20/// graph LR
21///     Created --> Success
22///     Created --> Aborted
23#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
24pub struct WithdrawStateMachine {
25    pub(crate) operation_id: OperationId,
26    pub(crate) state: WithdrawStates,
27}
28
29impl State for WithdrawStateMachine {
30    type ModuleContext = WalletClientContext;
31
32    fn transitions(
33        &self,
34        context: &Self::ModuleContext,
35        global_context: &DynGlobalClientContext,
36    ) -> Vec<StateTransition<Self>> {
37        let wallet_context = context.clone();
38        match &self.state {
39            WithdrawStates::Created(created) => {
40                vec![StateTransition::new(
41                    await_withdraw_processed(
42                        global_context.clone(),
43                        context.clone(),
44                        created.clone(),
45                    ),
46                    move |dbtx, res, old_state| {
47                        Box::pin(transition_withdraw_processed(
48                            res,
49                            old_state,
50                            wallet_context.clone(),
51                            dbtx,
52                        ))
53                    },
54                )]
55            }
56            WithdrawStates::Success(_) | WithdrawStates::Aborted(_) => {
57                vec![]
58            }
59        }
60    }
61
62    fn operation_id(&self) -> OperationId {
63        self.operation_id
64    }
65}
66
67async fn await_withdraw_processed(
68    global_context: DynGlobalClientContext,
69    context: WalletClientContext,
70    created: CreatedWithdrawState,
71) -> Result<Txid, String> {
72    global_context
73        .await_tx_accepted(created.fm_outpoint.txid)
74        .await?;
75
76    #[allow(deprecated)]
77    let outcome = global_context
78        .api()
79        .request_current_consensus_retry(
80            AWAIT_OUTPUT_OUTCOME_ENDPOINT.to_owned(),
81            ApiRequestErased::new(created.fm_outpoint),
82        )
83        .await;
84
85    match deserialize_outcome::<WalletOutputOutcome>(&outcome, &context.wallet_decoder)
86        .map_err(|e| e.to_string())
87        .and_then(|outcome| {
88            outcome
89                .ensure_v0_ref()
90                .map(|outcome| outcome.0)
91                .map_err(|e| e.to_string())
92        }) {
93        Ok(txid) => Ok(txid),
94        Err(e) => {
95            warn!("Failed to process wallet output outcome: {e}");
96
97            pending().await
98        }
99    }
100}
101
102async fn transition_withdraw_processed(
103    res: Result<Txid, String>,
104    old_state: WithdrawStateMachine,
105    client_ctx: WalletClientContext,
106    dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
107) -> WithdrawStateMachine {
108    assert!(
109        matches!(old_state.state, WithdrawStates::Created(_)),
110        "Unexpected old state: got {:?}, expected Created",
111        old_state.state
112    );
113
114    let new_state = match res {
115        Ok(txid) => {
116            client_ctx
117                .client_ctx
118                .log_event(&mut dbtx.module_tx(), WithdrawRequest { txid })
119                .await;
120            WithdrawStates::Success(SuccessWithdrawState { txid })
121        }
122        Err(error) => WithdrawStates::Aborted(AbortedWithdrawState { error }),
123    };
124
125    WithdrawStateMachine {
126        operation_id: old_state.operation_id,
127        state: new_state,
128    }
129}
130
131#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
132pub enum WithdrawStates {
133    Created(CreatedWithdrawState),
134    Success(SuccessWithdrawState),
135    Aborted(AbortedWithdrawState),
136}
137
138#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
139pub struct CreatedWithdrawState {
140    pub(crate) fm_outpoint: OutPoint,
141}
142
143#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
144pub struct SuccessWithdrawState {
145    pub(crate) txid: Txid,
146}
147
148#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
149pub struct AbortedWithdrawState {
150    pub(crate) error: String,
151}