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