1use core::fmt;
11use std::time::Duration;
12
13use bitcoin::hashes::sha256;
14use fedimint_client::sm::{ClientSMDatabaseTransaction, State, StateTransition};
15use fedimint_client::transaction::{ClientInput, ClientInputBundle};
16use fedimint_client::DynGlobalClientContext;
17use fedimint_core::core::OperationId;
18use fedimint_core::encoding::{Decodable, Encodable};
19use fedimint_core::runtime::sleep;
20use fedimint_core::{Amount, OutPoint, TransactionId};
21use fedimint_ln_common::contracts::incoming::IncomingContractAccount;
22use fedimint_ln_common::contracts::{ContractId, Preimage};
23use fedimint_ln_common::{LightningInput, LightningOutputOutcome};
24use lightning_invoice::Bolt11Invoice;
25use serde::{Deserialize, Serialize};
26use thiserror::Error;
27use tracing::{debug, error, info, warn};
28
29use crate::api::LnFederationApi;
30use crate::{set_payment_result, LightningClientContext, PayType};
31
32#[cfg_attr(doc, aquamarine::aquamarine)]
33#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
48pub enum IncomingSmStates {
49 FundingOffer(FundingOfferState),
50 DecryptingPreimage(DecryptingPreimageState),
51 Preimage(Preimage),
52 RefundSubmitted {
53 out_points: Vec<OutPoint>,
54 error: IncomingSmError,
55 },
56 FundingFailed {
57 error: IncomingSmError,
58 },
59 Failure(String),
60}
61
62impl fmt::Display for IncomingSmStates {
63 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64 match self {
65 IncomingSmStates::FundingOffer(_) => write!(f, "FundingOffer"),
66 IncomingSmStates::DecryptingPreimage(_) => write!(f, "DecryptingPreimage"),
67 IncomingSmStates::Preimage(_) => write!(f, "Preimage"),
68 IncomingSmStates::RefundSubmitted { .. } => write!(f, "RefundSubmitted"),
69 IncomingSmStates::FundingFailed { .. } => write!(f, "FundingFailed"),
70 IncomingSmStates::Failure(_) => write!(f, "Failure"),
71 }
72 }
73}
74
75#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
76pub struct IncomingSmCommon {
77 pub operation_id: OperationId,
78 pub contract_id: ContractId,
79 pub payment_hash: sha256::Hash,
80}
81
82#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
83pub struct IncomingStateMachine {
84 pub common: IncomingSmCommon,
85 pub state: IncomingSmStates,
86}
87
88impl fmt::Display for IncomingStateMachine {
89 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90 write!(
91 f,
92 "Incoming State Machine Operation ID: {:?} State: {}",
93 self.common.operation_id, self.state
94 )
95 }
96}
97
98impl State for IncomingStateMachine {
99 type ModuleContext = LightningClientContext;
100
101 fn transitions(
102 &self,
103 context: &Self::ModuleContext,
104 global_context: &DynGlobalClientContext,
105 ) -> Vec<fedimint_client::sm::StateTransition<Self>> {
106 match &self.state {
107 IncomingSmStates::FundingOffer(state) => state.transitions(global_context, context),
108 IncomingSmStates::DecryptingPreimage(_state) => {
109 DecryptingPreimageState::transitions(&self.common, global_context, context)
110 }
111 _ => {
112 vec![]
113 }
114 }
115 }
116
117 fn operation_id(&self) -> fedimint_core::core::OperationId {
118 self.common.operation_id
119 }
120}
121
122#[derive(
123 Error, Debug, Serialize, Deserialize, Encodable, Decodable, Hash, Clone, Eq, PartialEq,
124)]
125#[serde(rename_all = "snake_case")]
126pub enum IncomingSmError {
127 #[error("Violated fee policy. Offer amount {offer_amount} Payment amount: {payment_amount}")]
128 ViolatedFeePolicy {
129 offer_amount: Amount,
130 payment_amount: Amount,
131 },
132 #[error("Invalid offer. Offer hash: {offer_hash} Payment hash: {payment_hash}")]
133 InvalidOffer {
134 offer_hash: sha256::Hash,
135 payment_hash: sha256::Hash,
136 },
137 #[error("Timed out fetching the offer")]
138 TimeoutFetchingOffer { payment_hash: sha256::Hash },
139 #[error("Error fetching the contract {payment_hash}. Error: {error_message}")]
140 FetchContractError {
141 payment_hash: sha256::Hash,
142 error_message: String,
143 },
144 #[error("Invalid preimage. Contract: {contract:?}")]
145 InvalidPreimage {
146 contract: Box<IncomingContractAccount>,
147 },
148 #[error("There was a failure when funding the contract: {error_message}")]
149 FailedToFundContract { error_message: String },
150 #[error("Failed to parse the amount from the invoice: {invoice}")]
151 AmountError { invoice: Bolt11Invoice },
152}
153
154#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
155pub struct FundingOfferState {
156 pub txid: TransactionId,
157}
158
159impl FundingOfferState {
160 fn transitions(
161 &self,
162 global_context: &DynGlobalClientContext,
163 context: &LightningClientContext,
164 ) -> Vec<StateTransition<IncomingStateMachine>> {
165 let txid = self.txid;
166 vec![StateTransition::new(
167 Self::await_funding_success(
168 global_context.clone(),
169 OutPoint { txid, out_idx: 0 },
170 context.clone(),
171 ),
172 |_dbtx, result, old_state| {
173 Box::pin(async { Self::transition_funding_success(result, old_state) })
174 },
175 )]
176 }
177
178 async fn await_funding_success(
179 global_context: DynGlobalClientContext,
180 out_point: OutPoint,
181 context: LightningClientContext,
182 ) -> Result<(), IncomingSmError> {
183 debug!("Awaiting funding success for outpoint: {out_point:?}");
184 for retry in 0.. {
185 let sleep = (retry * 15).min(90);
186 match global_context
187 .api()
188 .await_output_outcome::<LightningOutputOutcome>(
189 out_point,
190 Duration::from_secs(90),
191 &context.ln_decoder,
192 )
193 .await
194 {
195 Ok(_) => {
196 debug!("Funding success for outpoint: {out_point:?}");
197 return Ok(());
198 }
199 Err(e) if e.is_rejected() => {
200 warn!("Funding failed for outpoint: {out_point:?}: {e:?}");
201 return Err(IncomingSmError::FailedToFundContract {
202 error_message: e.to_string(),
203 });
204 }
205 Err(e) => {
206 e.report_if_important();
207 debug!(error = %e, "Awaiting output outcome failed, retrying in {sleep}s",);
208 }
209 }
210 fedimint_core::runtime::sleep(Duration::from_secs(sleep)).await;
212 }
213
214 unreachable!("there is too many u64s to ever get here")
215 }
216
217 fn transition_funding_success(
218 result: Result<(), IncomingSmError>,
219 old_state: IncomingStateMachine,
220 ) -> IncomingStateMachine {
221 let txid = match old_state.state {
222 IncomingSmStates::FundingOffer(refund) => refund.txid,
223 _ => panic!("Invalid state transition"),
224 };
225
226 match result {
227 Ok(()) => IncomingStateMachine {
228 common: old_state.common,
229 state: IncomingSmStates::DecryptingPreimage(DecryptingPreimageState { txid }),
230 },
231 Err(error) => IncomingStateMachine {
232 common: old_state.common,
233 state: IncomingSmStates::FundingFailed { error },
234 },
235 }
236 }
237}
238
239#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
240pub struct DecryptingPreimageState {
241 txid: TransactionId,
242}
243
244impl DecryptingPreimageState {
245 fn transitions(
246 common: &IncomingSmCommon,
247 global_context: &DynGlobalClientContext,
248 context: &LightningClientContext,
249 ) -> Vec<StateTransition<IncomingStateMachine>> {
250 let success_context = global_context.clone();
251 let gateway_context = context.clone();
252
253 vec![StateTransition::new(
254 Self::await_preimage_decryption(success_context.clone(), common.contract_id),
255 move |dbtx, result, old_state| {
256 let gateway_context = gateway_context.clone();
257 let success_context = success_context.clone();
258 Box::pin(Self::transition_incoming_contract_funded(
259 result,
260 old_state,
261 dbtx,
262 success_context,
263 gateway_context,
264 ))
265 },
266 )]
267 }
268
269 async fn await_preimage_decryption(
270 global_context: DynGlobalClientContext,
271 contract_id: ContractId,
272 ) -> Result<Preimage, IncomingSmError> {
273 loop {
274 debug!("Awaiting preimage decryption for contract {contract_id:?}");
275 match global_context
276 .module_api()
277 .wait_preimage_decrypted(contract_id)
278 .await
279 {
280 Ok((incoming_contract_account, preimage)) => {
281 if let Some(preimage) = preimage {
282 debug!("Preimage decrypted for contract {contract_id:?}");
283 return Ok(preimage);
284 }
285
286 info!("Invalid preimage for contract {contract_id:?}");
287 return Err(IncomingSmError::InvalidPreimage {
288 contract: Box::new(incoming_contract_account),
289 });
290 }
291 Err(error) => {
292 warn!("Incoming contract {contract_id:?} error waiting for preimage decryption: {error:?}, will keep retrying...");
293 }
294 }
295
296 sleep(Duration::from_secs(1)).await;
297 }
298 }
299
300 async fn transition_incoming_contract_funded(
301 result: Result<Preimage, IncomingSmError>,
302 old_state: IncomingStateMachine,
303 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
304 global_context: DynGlobalClientContext,
305 context: LightningClientContext,
306 ) -> IncomingStateMachine {
307 assert!(matches!(
308 old_state.state,
309 IncomingSmStates::DecryptingPreimage(_)
310 ));
311
312 match result {
313 Ok(preimage) => {
314 let contract_id = old_state.common.contract_id;
315 let payment_hash = old_state.common.payment_hash;
316 set_payment_result(
317 &mut dbtx.module_tx(),
318 payment_hash,
319 PayType::Internal(old_state.common.operation_id),
320 contract_id,
321 Amount::from_msats(0),
322 )
323 .await;
324
325 IncomingStateMachine {
326 common: old_state.common,
327 state: IncomingSmStates::Preimage(preimage),
328 }
329 }
330 Err(IncomingSmError::InvalidPreimage { contract }) => {
331 Self::refund_incoming_contract(dbtx, global_context, context, old_state, contract)
332 .await
333 }
334 Err(e) => IncomingStateMachine {
335 common: old_state.common,
336 state: IncomingSmStates::Failure(format!(
337 "Unexpected internal error occurred while decrypting the preimage: {e:?}"
338 )),
339 },
340 }
341 }
342
343 async fn refund_incoming_contract(
344 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
345 global_context: DynGlobalClientContext,
346 context: LightningClientContext,
347 old_state: IncomingStateMachine,
348 contract: Box<IncomingContractAccount>,
349 ) -> IncomingStateMachine {
350 debug!("Refunding incoming contract {contract:?}");
351 let claim_input = contract.claim();
352 let client_input = ClientInput::<LightningInput> {
353 input: claim_input,
354 amount: contract.amount,
355 keys: vec![context.redeem_key],
356 };
357
358 let out_points = global_context
359 .claim_inputs(dbtx, ClientInputBundle::new_no_sm(vec![client_input]))
360 .await
361 .expect("Cannot claim input, additional funding needed")
362 .1;
363 debug!("Refunded incoming contract {contract:?} with {out_points:?}");
364
365 IncomingStateMachine {
366 common: old_state.common,
367 state: IncomingSmStates::RefundSubmitted {
368 out_points,
369 error: IncomingSmError::InvalidPreimage { contract },
370 },
371 }
372 }
373}
374
375#[derive(Debug, Clone, Eq, PartialEq, Decodable, Encodable)]
376pub struct AwaitingPreimageDecryption {
377 txid: TransactionId,
378}
379
380#[derive(Debug, Clone, Eq, PartialEq, Decodable, Encodable)]
381pub struct PreimageState {
382 preimage: Preimage,
383}
384
385#[derive(Debug, Clone, Eq, PartialEq, Decodable, Encodable)]
386pub struct RefundSuccessState {
387 refund_txid: TransactionId,
388}