Skip to main content

fedimint_gwv2_client/
complete_sm.rs

1use std::fmt;
2
3use fedimint_client::DynGlobalClientContext;
4use fedimint_client_module::sm::{ClientSMDatabaseTransaction, State, StateTransition};
5use fedimint_core::core::OperationId;
6use fedimint_core::encoding::{Decodable, Encodable};
7use fedimint_lightning::{InterceptPaymentResponse, LightningRpcError, PaymentAction, Preimage};
8use fedimint_lnv2_common::contracts::PaymentImage;
9
10use super::FinalReceiveState;
11use super::events::CompleteLightningPaymentSucceeded;
12use crate::GatewayClientContextV2;
13
14/// State machine that completes the incoming payment by contacting the
15/// lightning node when the incoming contract has been funded and the preimage
16/// is available.
17///
18/// This is the legacy combined-operation representation. It remains decodable
19/// so upgrades can resume existing operations; new incoming circuits use
20/// [`CircuitCompleteStateMachine`].
21///
22/// ```mermaid
23/// graph LR
24/// classDef virtual fill:#fff,stroke-dasharray: 5 5
25///
26///    Pending -- receive preimage or fail --> Completing
27///    Completing -- htlc is completed  --> Completed
28///    Completing -- permanent outcome conflict --> CompletionFailed
29/// ```
30
31#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
32pub struct CompleteStateMachine {
33    pub common: CompleteSMCommon,
34    pub state: CompleteSMState,
35}
36
37impl CompleteStateMachine {
38    pub fn update(&self, state: CompleteSMState) -> Self {
39        Self {
40            common: self.common.clone(),
41            state,
42        }
43    }
44}
45
46impl fmt::Display for CompleteStateMachine {
47    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48        write!(
49            f,
50            "Complete State Machine Operation ID: {:?} State: {}",
51            self.common.operation_id, self.state
52        )
53    }
54}
55
56#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
57pub struct CompleteSMCommon {
58    pub operation_id: OperationId,
59    pub payment_hash: bitcoin::hashes::sha256::Hash,
60    pub incoming_chan_id: u64,
61    pub htlc_id: u64,
62}
63
64/// State machine that completes one distinct incoming Lightning circuit.
65///
66/// New incoming payments use this state machine. [`CompleteStateMachine`]
67/// remains in the state enum so clients can decode and resume operations
68/// created before circuit-specific completion operations were introduced.
69#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
70pub struct CircuitCompleteStateMachine {
71    /// Data shared by every state of this circuit completion.
72    pub common: CircuitCompleteSMCommon,
73    /// Current completion state.
74    pub state: CompleteSMState,
75}
76
77/// Data needed to complete a distinct incoming Lightning circuit.
78#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
79pub struct CircuitCompleteSMCommon {
80    /// Circuit-specific operation identifier.
81    pub operation_id: OperationId,
82    /// Original receive operation whose result determines the outcome.
83    pub receive_operation_id: OperationId,
84    /// Payment hash carried by the incoming HTLC.
85    pub payment_hash: bitcoin::hashes::sha256::Hash,
86    /// Incoming circuit to resolve.
87    pub circuit: IncomingCircuitKey,
88}
89
90/// Stable identity of an incoming Lightning circuit.
91#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Decodable, Encodable)]
92pub struct IncomingCircuitKey {
93    /// Incoming channel identifier, or the no-circuit marker.
94    pub incoming_chan_id: u64,
95    /// Incoming HTLC identifier, or the no-circuit marker.
96    pub htlc_id: u64,
97}
98
99async fn await_receive(
100    context: GatewayClientContextV2,
101    operation_id: OperationId,
102) -> FinalReceiveState {
103    context.module.await_receive(operation_id).await
104}
105
106async fn complete_circuit(
107    context: GatewayClientContextV2,
108    payment_hash: bitcoin::hashes::sha256::Hash,
109    final_receive_state: FinalReceiveState,
110    circuit: IncomingCircuitKey,
111) -> Result<(), LightningRpcError> {
112    let action = if let FinalReceiveState::Success(preimage) = final_receive_state {
113        PaymentAction::Settle(Preimage(preimage))
114    } else {
115        PaymentAction::Cancel
116    };
117    let IncomingCircuitKey {
118        incoming_chan_id,
119        htlc_id,
120    } = circuit;
121
122    context
123        .gateway
124        .complete_htlc(InterceptPaymentResponse {
125            incoming_chan_id,
126            htlc_id,
127            payment_hash,
128            action,
129        })
130        .await
131}
132
133async fn log_completion(
134    dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
135    client_ctx: GatewayClientContextV2,
136    payment_hash: bitcoin::hashes::sha256::Hash,
137) {
138    client_ctx
139        .module
140        .client_ctx
141        .log_event(
142            &mut dbtx.module_tx(),
143            CompleteLightningPaymentSucceeded {
144                payment_image: PaymentImage::Hash(payment_hash),
145            },
146        )
147        .await;
148}
149
150#[derive(Debug, Eq, PartialEq)]
151pub(super) enum CompletionOutcome {
152    Succeeded,
153    Failed(String),
154}
155
156pub(super) fn completion_outcome(result: Result<(), LightningRpcError>) -> CompletionOutcome {
157    match result {
158        Ok(()) => CompletionOutcome::Succeeded,
159        Err(error) => CompletionOutcome::Failed(error.to_string()),
160    }
161}
162
163#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
164pub enum CompleteSMState {
165    Pending,
166    Completing(FinalReceiveState),
167    Completed,
168    /// Lightning reached an incompatible permanent terminal outcome.
169    CompletionFailed(String),
170}
171
172impl fmt::Display for CompleteSMState {
173    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
174        match self {
175            CompleteSMState::Pending => write!(f, "Pending"),
176            CompleteSMState::Completing(_) => write!(f, "Completing"),
177            CompleteSMState::Completed => write!(f, "Completed"),
178            CompleteSMState::CompletionFailed(_) => write!(f, "Completion Failed"),
179        }
180    }
181}
182
183impl State for CompleteStateMachine {
184    type ModuleContext = GatewayClientContextV2;
185
186    fn transitions(
187        &self,
188        context: &Self::ModuleContext,
189        _global_context: &DynGlobalClientContext,
190    ) -> Vec<StateTransition<Self>> {
191        let gateway_context = context.clone();
192        match &self.state {
193            CompleteSMState::Pending => vec![StateTransition::new(
194                await_receive(context.clone(), self.common.operation_id),
195                |_, result, old_state| {
196                    Box::pin(async move { Self::transition_receive(result, &old_state) })
197                },
198            )],
199            CompleteSMState::Completing(finale_receive_state) => vec![StateTransition::new(
200                complete_circuit(
201                    gateway_context.clone(),
202                    self.common.payment_hash,
203                    finale_receive_state.clone(),
204                    IncomingCircuitKey {
205                        incoming_chan_id: self.common.incoming_chan_id,
206                        htlc_id: self.common.htlc_id,
207                    },
208                ),
209                move |dbtx, result, old_state| {
210                    Box::pin(Self::transition_completion(
211                        old_state,
212                        dbtx,
213                        gateway_context.clone(),
214                        result,
215                    ))
216                },
217            )],
218            CompleteSMState::Completed | CompleteSMState::CompletionFailed(_) => Vec::new(),
219        }
220    }
221
222    fn operation_id(&self) -> OperationId {
223        self.common.operation_id
224    }
225}
226
227impl fmt::Display for CircuitCompleteStateMachine {
228    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
229        write!(
230            f,
231            "Circuit Complete State Machine Operation ID: {:?} State: {}",
232            self.common.operation_id, self.state
233        )
234    }
235}
236
237impl CircuitCompleteStateMachine {
238    fn update(&self, state: CompleteSMState) -> Self {
239        Self {
240            common: self.common.clone(),
241            state,
242        }
243    }
244}
245
246impl State for CircuitCompleteStateMachine {
247    type ModuleContext = GatewayClientContextV2;
248
249    fn transitions(
250        &self,
251        context: &Self::ModuleContext,
252        _global_context: &DynGlobalClientContext,
253    ) -> Vec<StateTransition<Self>> {
254        let gateway_context = context.clone();
255        match &self.state {
256            CompleteSMState::Pending => vec![StateTransition::new(
257                await_receive(context.clone(), self.common.receive_operation_id),
258                |_, result, old_state: CircuitCompleteStateMachine| {
259                    Box::pin(async move { old_state.update(CompleteSMState::Completing(result)) })
260                },
261            )],
262            CompleteSMState::Completing(final_receive_state) => vec![StateTransition::new(
263                complete_circuit(
264                    gateway_context.clone(),
265                    self.common.payment_hash,
266                    final_receive_state.clone(),
267                    self.common.circuit,
268                ),
269                move |dbtx, result, old_state: CircuitCompleteStateMachine| {
270                    let gateway_context = gateway_context.clone();
271                    Box::pin(async move {
272                        match completion_outcome(result) {
273                            CompletionOutcome::Succeeded => {
274                                log_completion(
275                                    dbtx,
276                                    gateway_context,
277                                    old_state.common.payment_hash,
278                                )
279                                .await;
280                                old_state.update(CompleteSMState::Completed)
281                            }
282                            CompletionOutcome::Failed(error) => {
283                                old_state.update(CompleteSMState::CompletionFailed(error))
284                            }
285                        }
286                    })
287                },
288            )],
289            CompleteSMState::Completed | CompleteSMState::CompletionFailed(_) => Vec::new(),
290        }
291    }
292
293    fn operation_id(&self) -> OperationId {
294        self.common.operation_id
295    }
296}
297
298impl CompleteStateMachine {
299    fn transition_receive(
300        final_receive_state: FinalReceiveState,
301        old_state: &CompleteStateMachine,
302    ) -> CompleteStateMachine {
303        old_state.update(CompleteSMState::Completing(final_receive_state))
304    }
305
306    async fn transition_completion(
307        old_state: CompleteStateMachine,
308        dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
309        client_ctx: GatewayClientContextV2,
310        result: Result<(), LightningRpcError>,
311    ) -> CompleteStateMachine {
312        match completion_outcome(result) {
313            CompletionOutcome::Succeeded => {
314                log_completion(dbtx, client_ctx, old_state.common.payment_hash).await;
315                old_state.update(CompleteSMState::Completed)
316            }
317            CompletionOutcome::Failed(error) => {
318                old_state.update(CompleteSMState::CompletionFailed(error))
319            }
320        }
321    }
322}