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