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::{SendPaymentStatus, SendPaymentStatusEvent, 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
121            client_ctx
122                .client_ctx
123                .log_event(
124                    &mut dbtx.module_tx(),
125                    SendPaymentStatusEvent {
126                        operation_id: old_state.operation_id,
127                        status: SendPaymentStatus::Success(txid),
128                    },
129                )
130                .await;
131
132            WithdrawStates::Success(SuccessWithdrawState { txid })
133        }
134        Err(error) => {
135            client_ctx
136                .client_ctx
137                .log_event(
138                    &mut dbtx.module_tx(),
139                    SendPaymentStatusEvent {
140                        operation_id: old_state.operation_id,
141                        status: SendPaymentStatus::Aborted,
142                    },
143                )
144                .await;
145
146            WithdrawStates::Aborted(AbortedWithdrawState { error })
147        }
148    };
149
150    WithdrawStateMachine {
151        operation_id: old_state.operation_id,
152        state: new_state,
153    }
154}
155
156#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
157pub enum WithdrawStates {
158    Created(CreatedWithdrawState),
159    Success(SuccessWithdrawState),
160    Aborted(AbortedWithdrawState),
161}
162
163#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
164pub struct CreatedWithdrawState {
165    pub(crate) fm_outpoint: OutPoint,
166}
167
168#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
169pub struct SuccessWithdrawState {
170    pub(crate) txid: Txid,
171}
172
173#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
174pub struct AbortedWithdrawState {
175    pub(crate) error: String,
176}