Skip to main content

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