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")]
128pub enum IncomingSmError {
129 #[error("Violated fee policy. Offer amount {offer_amount} Payment amount: {payment_amount}")]
130 ViolatedFeePolicy {
131 offer_amount: Amount,
132 payment_amount: Amount,
133 },
134 #[error("Invalid offer. Offer hash: {offer_hash} Payment hash: {payment_hash}")]
135 InvalidOffer {
136 offer_hash: sha256::Hash,
137 payment_hash: sha256::Hash,
138 },
139 #[error("Timed out fetching the offer")]
140 TimeoutFetchingOffer { payment_hash: sha256::Hash },
141 #[error("Error fetching the contract {payment_hash}. Error: {error_message}")]
142 FetchContractError {
143 payment_hash: sha256::Hash,
144 error_message: String,
145 },
146 #[error("Invalid preimage. Contract: {contract:?}")]
147 InvalidPreimage {
148 contract: Box<IncomingContractAccount>,
149 },
150 #[error("There was a failure when funding the contract: {error_message}")]
151 FailedToFundContract { error_message: String },
152 #[error("Failed to parse the amount from the invoice: {invoice}")]
153 AmountError { invoice: Bolt11Invoice },
154 #[error(
159 "A contract already exists for payment hash {payment_hash}, funding it again would credit the existing account"
160 )]
161 ContractAlreadyExists { payment_hash: sha256::Hash },
162}
163
164#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
165pub struct FundingOfferState {
166 pub txid: TransactionId,
167}
168
169impl FundingOfferState {
170 fn transitions(
171 &self,
172 global_context: &DynGlobalClientContext,
173 ) -> Vec<StateTransition<IncomingStateMachine>> {
174 let txid = self.txid;
175 vec![StateTransition::new(
176 Self::await_funding_success(global_context.clone(), txid),
177 |_dbtx, result, old_state| {
178 Box::pin(async { Self::transition_funding_success(result, old_state) })
179 },
180 )]
181 }
182
183 async fn await_funding_success(
184 global_context: DynGlobalClientContext,
185 txid: TransactionId,
186 ) -> Result<(), IncomingSmError> {
187 global_context
188 .await_tx_accepted(txid)
189 .await
190 .map_err(|error_message| IncomingSmError::FailedToFundContract { error_message })
191 }
192
193 fn transition_funding_success(
194 result: Result<(), IncomingSmError>,
195 old_state: IncomingStateMachine,
196 ) -> IncomingStateMachine {
197 let txid = match old_state.state {
198 IncomingSmStates::FundingOffer(refund) => refund.txid,
199 _ => panic!("Invalid state transition"),
200 };
201
202 match result {
203 Ok(()) => IncomingStateMachine {
204 common: old_state.common,
205 state: IncomingSmStates::DecryptingPreimage(DecryptingPreimageState { txid }),
206 },
207 Err(error) => IncomingStateMachine {
208 common: old_state.common,
209 state: IncomingSmStates::FundingFailed { error },
210 },
211 }
212 }
213}
214
215#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
216pub struct DecryptingPreimageState {
217 txid: TransactionId,
218}
219
220impl DecryptingPreimageState {
221 fn transitions(
222 common: &IncomingSmCommon,
223 global_context: &DynGlobalClientContext,
224 context: &LightningClientContext,
225 ) -> Vec<StateTransition<IncomingStateMachine>> {
226 let success_context = global_context.clone();
227 let gateway_context = context.clone();
228
229 vec![StateTransition::new(
230 Self::await_preimage_decryption(success_context.clone(), common.contract_id),
231 move |dbtx, result, old_state| {
232 let gateway_context = gateway_context.clone();
233 let success_context = success_context.clone();
234 Box::pin(Self::transition_incoming_contract_funded(
235 result,
236 old_state,
237 dbtx,
238 success_context,
239 gateway_context,
240 ))
241 },
242 )]
243 }
244
245 async fn await_preimage_decryption(
246 global_context: DynGlobalClientContext,
247 contract_id: ContractId,
248 ) -> Result<Preimage, IncomingSmError> {
249 loop {
250 debug!("Awaiting preimage decryption for contract {contract_id:?}");
251 match global_context
252 .module_api()
253 .wait_preimage_decrypted(contract_id)
254 .await
255 {
256 Ok((incoming_contract_account, preimage)) => {
257 if let Some(preimage) = preimage {
258 debug!("Preimage decrypted for contract {contract_id:?}");
259 return Ok(preimage);
260 }
261
262 info!("Invalid preimage for contract {contract_id:?}");
263 return Err(IncomingSmError::InvalidPreimage {
264 contract: Box::new(incoming_contract_account),
265 });
266 }
267 Err(error) => {
268 warn!(
269 "Incoming contract {contract_id:?} error waiting for preimage decryption: {error:?}, will keep retrying..."
270 );
271 }
272 }
273
274 sleep(Duration::from_secs(1)).await;
275 }
276 }
277
278 async fn transition_incoming_contract_funded(
279 result: Result<Preimage, IncomingSmError>,
280 old_state: IncomingStateMachine,
281 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
282 global_context: DynGlobalClientContext,
283 context: LightningClientContext,
284 ) -> IncomingStateMachine {
285 assert_matches!(old_state.state, IncomingSmStates::DecryptingPreimage(_));
286
287 match result {
288 Ok(preimage) => {
289 let contract_id = old_state.common.contract_id;
290 let payment_hash = old_state.common.payment_hash;
291 set_payment_result(
292 &mut dbtx.module_tx(),
293 payment_hash,
294 PayType::Internal(old_state.common.operation_id),
295 contract_id,
296 Amount::from_msats(0),
297 )
298 .await;
299
300 if let Some(ref client_ctx) = context.client_ctx {
302 client_ctx
303 .log_event(
304 &mut dbtx.module_tx(),
305 crate::events::SendPaymentUpdateEvent {
306 operation_id: old_state.common.operation_id,
307 status: crate::events::SendPaymentStatus::Success(preimage.0),
308 },
309 )
310 .await;
311 }
312
313 IncomingStateMachine {
314 common: old_state.common,
315 state: IncomingSmStates::Preimage(preimage),
316 }
317 }
318 Err(IncomingSmError::InvalidPreimage { contract }) => {
319 Self::refund_incoming_contract(dbtx, global_context, context, old_state, contract)
320 .await
321 }
322 Err(e) => IncomingStateMachine {
323 common: old_state.common,
324 state: IncomingSmStates::Failure(format!(
325 "Unexpected internal error occurred while decrypting the preimage: {e:?}"
326 )),
327 },
328 }
329 }
330
331 async fn refund_incoming_contract(
332 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
333 global_context: DynGlobalClientContext,
334 context: LightningClientContext,
335 old_state: IncomingStateMachine,
336 contract: Box<IncomingContractAccount>,
337 ) -> IncomingStateMachine {
338 debug!("Refunding incoming contract {contract:?}");
339 let claim_input = contract.claim();
340 let client_input = ClientInput::<LightningInput> {
341 input: claim_input,
342 amounts: Amounts::new_bitcoin(contract.amount),
343 keys: vec![context.redeem_key],
344 };
345
346 let change_range = global_context
347 .claim_inputs(dbtx, ClientInputBundle::new_no_sm(vec![client_input]))
348 .await
349 .expect("Cannot claim input, additional funding needed");
350 debug!("Refunded incoming contract {contract:?} with {change_range:?}");
351
352 if let Some(ref client_ctx) = context.client_ctx {
354 client_ctx
355 .log_event(
356 &mut dbtx.module_tx(),
357 crate::events::SendPaymentUpdateEvent {
358 operation_id: old_state.common.operation_id,
359 status: crate::events::SendPaymentStatus::Refunded,
360 },
361 )
362 .await;
363 }
364
365 IncomingStateMachine {
366 common: old_state.common,
367 state: IncomingSmStates::RefundSubmitted {
368 out_points: change_range.into_iter().collect(),
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}