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