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#[aquamarine::aquamarine]
21#[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}