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