fedimint_wallet_client/
withdraw.rs1use bitcoin::Txid;
2use fedimint_api_client::api::{FederationApiExt, deserialize_outcome};
3use fedimint_client_module::DynGlobalClientContext;
4use fedimint_client_module::sm::{ClientSMDatabaseTransaction, State, StateTransition};
5use fedimint_core::OutPoint;
6use fedimint_core::core::OperationId;
7use fedimint_core::encoding::{Decodable, Encodable};
8#[allow(deprecated)]
9use fedimint_core::endpoint_constants::AWAIT_OUTPUT_OUTCOME_ENDPOINT;
10use fedimint_core::module::ApiRequestErased;
11use fedimint_wallet_common::WalletOutputOutcome;
12use futures::future::pending;
13use tracing::warn;
14
15use crate::WalletClientContext;
16use crate::events::WithdrawRequest;
17
18#[aquamarine::aquamarine]
20#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
24pub struct WithdrawStateMachine {
25 pub(crate) operation_id: OperationId,
26 pub(crate) state: WithdrawStates,
27}
28
29impl State for WithdrawStateMachine {
30 type ModuleContext = WalletClientContext;
31
32 fn transitions(
33 &self,
34 context: &Self::ModuleContext,
35 global_context: &DynGlobalClientContext,
36 ) -> Vec<StateTransition<Self>> {
37 let wallet_context = context.clone();
38 match &self.state {
39 WithdrawStates::Created(created) => {
40 vec![StateTransition::new(
41 await_withdraw_processed(
42 global_context.clone(),
43 context.clone(),
44 created.clone(),
45 ),
46 move |dbtx, res, old_state| {
47 Box::pin(transition_withdraw_processed(
48 res,
49 old_state,
50 wallet_context.clone(),
51 dbtx,
52 ))
53 },
54 )]
55 }
56 WithdrawStates::Success(_) | WithdrawStates::Aborted(_) => {
57 vec![]
58 }
59 }
60 }
61
62 fn operation_id(&self) -> OperationId {
63 self.operation_id
64 }
65}
66
67async fn await_withdraw_processed(
68 global_context: DynGlobalClientContext,
69 context: WalletClientContext,
70 created: CreatedWithdrawState,
71) -> Result<Txid, String> {
72 global_context
73 .await_tx_accepted(created.fm_outpoint.txid)
74 .await?;
75
76 #[allow(deprecated)]
77 let outcome = global_context
78 .api()
79 .request_current_consensus_retry(
80 AWAIT_OUTPUT_OUTCOME_ENDPOINT.to_owned(),
81 ApiRequestErased::new(created.fm_outpoint),
82 )
83 .await;
84
85 match deserialize_outcome::<WalletOutputOutcome>(&outcome, &context.wallet_decoder)
86 .map_err(|e| e.to_string())
87 .and_then(|outcome| {
88 outcome
89 .ensure_v0_ref()
90 .map(|outcome| outcome.0)
91 .map_err(|e| e.to_string())
92 }) {
93 Ok(txid) => Ok(txid),
94 Err(e) => {
95 warn!("Failed to process wallet output outcome: {e}");
96
97 pending().await
98 }
99 }
100}
101
102async fn transition_withdraw_processed(
103 res: Result<Txid, String>,
104 old_state: WithdrawStateMachine,
105 client_ctx: WalletClientContext,
106 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
107) -> WithdrawStateMachine {
108 assert!(
109 matches!(old_state.state, WithdrawStates::Created(_)),
110 "Unexpected old state: got {:?}, expected Created",
111 old_state.state
112 );
113
114 let new_state = match res {
115 Ok(txid) => {
116 client_ctx
117 .client_ctx
118 .log_event(&mut dbtx.module_tx(), WithdrawRequest { txid })
119 .await;
120 WithdrawStates::Success(SuccessWithdrawState { txid })
121 }
122 Err(error) => WithdrawStates::Aborted(AbortedWithdrawState { error }),
123 };
124
125 WithdrawStateMachine {
126 operation_id: old_state.operation_id,
127 state: new_state,
128 }
129}
130
131#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
132pub enum WithdrawStates {
133 Created(CreatedWithdrawState),
134 Success(SuccessWithdrawState),
135 Aborted(AbortedWithdrawState),
136}
137
138#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
139pub struct CreatedWithdrawState {
140 pub(crate) fm_outpoint: OutPoint,
141}
142
143#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
144pub struct SuccessWithdrawState {
145 pub(crate) txid: Txid,
146}
147
148#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
149pub struct AbortedWithdrawState {
150 pub(crate) error: String,
151}