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