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#[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 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}