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