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