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