fedimint_gw_client/
complete.rs

1use std::fmt;
2
3use fedimint_client::DynGlobalClientContext;
4use fedimint_client_module::sm::{ClientSMDatabaseTransaction, StateTransition};
5use fedimint_core::core::OperationId;
6use fedimint_core::encoding::{Decodable, Encodable};
7use fedimint_lightning::{InterceptPaymentResponse, PaymentAction};
8use fedimint_ln_client::incoming::IncomingSmStates;
9use fedimint_ln_common::contracts::Preimage;
10use futures::StreamExt;
11use serde::{Deserialize, Serialize};
12use thiserror::Error;
13use tracing::{debug, info, warn};
14
15use super::events::{
16    CompleteLightningPaymentSucceeded, IncomingPaymentFailed, IncomingPaymentSucceeded,
17};
18use super::{GatewayClientContext, GatewayClientStateMachines};
19
20#[derive(Error, Debug, Serialize, Deserialize, Encodable, Decodable, Clone, Eq, PartialEq)]
21enum CompleteHtlcError {
22    #[error("Incoming contract was not funded")]
23    IncomingContractNotFunded,
24    #[error("Failed to complete HTLC")]
25    FailedToCompleteHtlc,
26}
27
28#[cfg_attr(doc, aquamarine::aquamarine)]
29/// State machine that completes the incoming payment by contacting the
30/// lightning node when the incoming contract has been funded and the preimage
31/// is available.
32///
33/// ```mermaid
34/// graph LR
35/// classDef virtual fill:#fff,stroke-dasharray: 5 5
36///
37///    WaitForPreimage -- incoming contract not funded --> Failure
38///    WaitForPreimage -- successfully retrieved preimage --> CompleteHtlc
39///    CompleteHtlc -- successfully completed or canceled htlc --> HtlcFinished
40///    CompleteHtlc -- failed to finish htlc --> Failure
41/// ```
42#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
43pub enum GatewayCompleteStates {
44    WaitForPreimage(WaitForPreimageState),
45    CompleteHtlc(CompleteHtlcState),
46    HtlcFinished,
47    Failure,
48}
49
50impl fmt::Display for GatewayCompleteStates {
51    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52        match self {
53            GatewayCompleteStates::WaitForPreimage(_) => write!(f, "WaitForPreimage"),
54            GatewayCompleteStates::CompleteHtlc(_) => write!(f, "CompleteHtlc"),
55            GatewayCompleteStates::HtlcFinished => write!(f, "HtlcFinished"),
56            GatewayCompleteStates::Failure => write!(f, "Failure"),
57        }
58    }
59}
60
61#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
62pub struct GatewayCompleteCommon {
63    pub operation_id: OperationId,
64    pub payment_hash: bitcoin::hashes::sha256::Hash,
65    pub incoming_chan_id: u64,
66    pub htlc_id: u64,
67}
68
69#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
70pub struct GatewayCompleteStateMachine {
71    pub common: GatewayCompleteCommon,
72    pub state: GatewayCompleteStates,
73}
74
75impl fmt::Display for GatewayCompleteStateMachine {
76    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77        write!(
78            f,
79            "Gateway Complete State Machine Operation ID: {:?} State: {}",
80            self.common.operation_id, self.state
81        )
82    }
83}
84
85impl fedimint_client_module::sm::State for GatewayCompleteStateMachine {
86    type ModuleContext = GatewayClientContext;
87
88    fn transitions(
89        &self,
90        context: &Self::ModuleContext,
91        _global_context: &DynGlobalClientContext,
92    ) -> Vec<StateTransition<Self>> {
93        match &self.state {
94            GatewayCompleteStates::WaitForPreimage(_state) => {
95                WaitForPreimageState::transitions(context.clone(), self.common.clone())
96            }
97            GatewayCompleteStates::CompleteHtlc(state) => {
98                state.transitions(context.clone(), self.common.clone())
99            }
100            _ => vec![],
101        }
102    }
103
104    fn operation_id(&self) -> fedimint_core::core::OperationId {
105        self.common.operation_id
106    }
107}
108
109#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
110pub struct WaitForPreimageState;
111
112impl WaitForPreimageState {
113    fn transitions(
114        context: GatewayClientContext,
115        common: GatewayCompleteCommon,
116    ) -> Vec<StateTransition<GatewayCompleteStateMachine>> {
117        let gw_context = context.clone();
118        vec![StateTransition::new(
119            Self::await_preimage(context, common.clone()),
120            move |dbtx, result, _old_state| {
121                let common = common.clone();
122                Box::pin(Self::transition_complete_htlc(
123                    result,
124                    common,
125                    gw_context.clone(),
126                    dbtx,
127                ))
128            },
129        )]
130    }
131
132    async fn await_preimage(
133        context: GatewayClientContext,
134        common: GatewayCompleteCommon,
135    ) -> Result<Preimage, CompleteHtlcError> {
136        let mut stream = context.notifier.subscribe(common.operation_id).await;
137        loop {
138            debug!("Waiting for preimage for {common:?}");
139            let Some(GatewayClientStateMachines::Receive(state)) = stream.next().await else {
140                continue;
141            };
142
143            match state.state {
144                IncomingSmStates::Preimage(preimage) => {
145                    debug!("Received preimage for {common:?}");
146                    return Ok(preimage);
147                }
148                IncomingSmStates::RefundSubmitted { out_points, error } => {
149                    info!("Refund submitted for {common:?}: {out_points:?} {error}");
150                    return Err(CompleteHtlcError::IncomingContractNotFunded);
151                }
152                IncomingSmStates::FundingFailed { error } => {
153                    warn!("Funding failed for {common:?}: {error}");
154                    return Err(CompleteHtlcError::IncomingContractNotFunded);
155                }
156                _ => {}
157            }
158        }
159    }
160
161    async fn transition_complete_htlc(
162        result: Result<Preimage, CompleteHtlcError>,
163        common: GatewayCompleteCommon,
164        context: GatewayClientContext,
165        dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
166    ) -> GatewayCompleteStateMachine {
167        match result {
168            Ok(preimage) => {
169                context
170                    .client_ctx
171                    .log_event(
172                        &mut dbtx.module_tx(),
173                        IncomingPaymentSucceeded {
174                            payment_hash: common.payment_hash,
175                            preimage: preimage.consensus_encode_to_hex(),
176                        },
177                    )
178                    .await;
179
180                GatewayCompleteStateMachine {
181                    common,
182                    state: GatewayCompleteStates::CompleteHtlc(CompleteHtlcState {
183                        outcome: HtlcOutcome::Success(preimage),
184                    }),
185                }
186            }
187            Err(e) => {
188                context
189                    .client_ctx
190                    .log_event(
191                        &mut dbtx.module_tx(),
192                        IncomingPaymentFailed {
193                            payment_hash: common.payment_hash,
194                            error: e.to_string(),
195                        },
196                    )
197                    .await;
198
199                GatewayCompleteStateMachine {
200                    common,
201                    state: GatewayCompleteStates::CompleteHtlc(CompleteHtlcState {
202                        outcome: HtlcOutcome::Failure(e.to_string()),
203                    }),
204                }
205            }
206        }
207    }
208}
209
210#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
211enum HtlcOutcome {
212    Success(Preimage),
213    Failure(String),
214}
215
216#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
217pub struct CompleteHtlcState {
218    outcome: HtlcOutcome,
219}
220
221impl CompleteHtlcState {
222    fn transitions(
223        &self,
224        context: GatewayClientContext,
225        common: GatewayCompleteCommon,
226    ) -> Vec<StateTransition<GatewayCompleteStateMachine>> {
227        let gw_context = context.clone();
228        vec![StateTransition::new(
229            Self::await_complete_htlc(context, common.clone(), self.outcome.clone()),
230            move |dbtx, result, _| {
231                let common = common.clone();
232                Box::pin(Self::transition_success(
233                    result,
234                    common,
235                    dbtx,
236                    gw_context.clone(),
237                ))
238            },
239        )]
240    }
241
242    async fn await_complete_htlc(
243        context: GatewayClientContext,
244        common: GatewayCompleteCommon,
245        htlc_outcome: HtlcOutcome,
246    ) -> Result<(), CompleteHtlcError> {
247        let htlc = InterceptPaymentResponse {
248            action: match htlc_outcome {
249                HtlcOutcome::Success(preimage) => PaymentAction::Settle(preimage),
250                HtlcOutcome::Failure(_) => PaymentAction::Cancel,
251            },
252            payment_hash: common.payment_hash,
253            incoming_chan_id: common.incoming_chan_id,
254            htlc_id: common.htlc_id,
255        };
256
257        context
258            .lightning_manager
259            .complete_htlc(htlc)
260            .await
261            .map_err(|_| CompleteHtlcError::FailedToCompleteHtlc)
262    }
263
264    async fn transition_success(
265        result: Result<(), CompleteHtlcError>,
266        common: GatewayCompleteCommon,
267        dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
268        context: GatewayClientContext,
269    ) -> GatewayCompleteStateMachine {
270        GatewayCompleteStateMachine {
271            common: common.clone(),
272            state: match result {
273                Ok(()) => {
274                    context
275                        .client_ctx
276                        .log_event(
277                            &mut dbtx.module_tx(),
278                            CompleteLightningPaymentSucceeded {
279                                payment_hash: common.payment_hash,
280                            },
281                        )
282                        .await;
283                    GatewayCompleteStates::HtlcFinished
284                }
285                Err(_) => GatewayCompleteStates::Failure,
286            },
287        }
288    }
289}