Skip to main content

fedimint_wallet_client/
withdraw.rs

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