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#[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}