1use core::fmt;
11use std::time::Duration;
12
13use bitcoin::hashes::sha256;
14use fedimint_client_module::DynGlobalClientContext;
15use fedimint_client_module::sm::{ClientSMDatabaseTransaction, State, StateTransition};
16use fedimint_client_module::transaction::{ClientInput, ClientInputBundle};
17use fedimint_core::core::OperationId;
18use fedimint_core::encoding::{Decodable, Encodable};
19use fedimint_core::module::Amounts;
20use fedimint_core::runtime::sleep;
21use fedimint_core::{Amount, OutPoint, TransactionId};
22use fedimint_ln_common::LightningInput;
23use fedimint_ln_common::contracts::incoming::IncomingContractAccount;
24use fedimint_ln_common::contracts::{ContractId, Preimage};
25use lightning_invoice::Bolt11Invoice;
26use serde::{Deserialize, Serialize};
27use thiserror::Error;
28use tracing::{debug, error, info, warn};
29
30use crate::api::LnFederationApi;
31use crate::{LightningClientContext, PayType, set_payment_result};
32
33#[cfg_attr(doc, aquamarine::aquamarine)]
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}
154
155#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
156pub struct FundingOfferState {
157 pub txid: TransactionId,
158}
159
160impl FundingOfferState {
161 fn transitions(
162 &self,
163 global_context: &DynGlobalClientContext,
164 ) -> Vec<StateTransition<IncomingStateMachine>> {
165 let txid = self.txid;
166 vec![StateTransition::new(
167 Self::await_funding_success(global_context.clone(), txid),
168 |_dbtx, result, old_state| {
169 Box::pin(async { Self::transition_funding_success(result, old_state) })
170 },
171 )]
172 }
173
174 async fn await_funding_success(
175 global_context: DynGlobalClientContext,
176 txid: TransactionId,
177 ) -> Result<(), IncomingSmError> {
178 global_context
179 .await_tx_accepted(txid)
180 .await
181 .map_err(|error_message| IncomingSmError::FailedToFundContract { error_message })
182 }
183
184 fn transition_funding_success(
185 result: Result<(), IncomingSmError>,
186 old_state: IncomingStateMachine,
187 ) -> IncomingStateMachine {
188 let txid = match old_state.state {
189 IncomingSmStates::FundingOffer(refund) => refund.txid,
190 _ => panic!("Invalid state transition"),
191 };
192
193 match result {
194 Ok(()) => IncomingStateMachine {
195 common: old_state.common,
196 state: IncomingSmStates::DecryptingPreimage(DecryptingPreimageState { txid }),
197 },
198 Err(error) => IncomingStateMachine {
199 common: old_state.common,
200 state: IncomingSmStates::FundingFailed { error },
201 },
202 }
203 }
204}
205
206#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
207pub struct DecryptingPreimageState {
208 txid: TransactionId,
209}
210
211impl DecryptingPreimageState {
212 fn transitions(
213 common: &IncomingSmCommon,
214 global_context: &DynGlobalClientContext,
215 context: &LightningClientContext,
216 ) -> Vec<StateTransition<IncomingStateMachine>> {
217 let success_context = global_context.clone();
218 let gateway_context = context.clone();
219
220 vec![StateTransition::new(
221 Self::await_preimage_decryption(success_context.clone(), common.contract_id),
222 move |dbtx, result, old_state| {
223 let gateway_context = gateway_context.clone();
224 let success_context = success_context.clone();
225 Box::pin(Self::transition_incoming_contract_funded(
226 result,
227 old_state,
228 dbtx,
229 success_context,
230 gateway_context,
231 ))
232 },
233 )]
234 }
235
236 async fn await_preimage_decryption(
237 global_context: DynGlobalClientContext,
238 contract_id: ContractId,
239 ) -> Result<Preimage, IncomingSmError> {
240 loop {
241 debug!("Awaiting preimage decryption for contract {contract_id:?}");
242 match global_context
243 .module_api()
244 .wait_preimage_decrypted(contract_id)
245 .await
246 {
247 Ok((incoming_contract_account, preimage)) => {
248 if let Some(preimage) = preimage {
249 debug!("Preimage decrypted for contract {contract_id:?}");
250 return Ok(preimage);
251 }
252
253 info!("Invalid preimage for contract {contract_id:?}");
254 return Err(IncomingSmError::InvalidPreimage {
255 contract: Box::new(incoming_contract_account),
256 });
257 }
258 Err(error) => {
259 warn!(
260 "Incoming contract {contract_id:?} error waiting for preimage decryption: {error:?}, will keep retrying..."
261 );
262 }
263 }
264
265 sleep(Duration::from_secs(1)).await;
266 }
267 }
268
269 async fn transition_incoming_contract_funded(
270 result: Result<Preimage, IncomingSmError>,
271 old_state: IncomingStateMachine,
272 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
273 global_context: DynGlobalClientContext,
274 context: LightningClientContext,
275 ) -> IncomingStateMachine {
276 assert!(matches!(
277 old_state.state,
278 IncomingSmStates::DecryptingPreimage(_)
279 ));
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 IncomingStateMachine {
295 common: old_state.common,
296 state: IncomingSmStates::Preimage(preimage),
297 }
298 }
299 Err(IncomingSmError::InvalidPreimage { contract }) => {
300 Self::refund_incoming_contract(dbtx, global_context, context, old_state, contract)
301 .await
302 }
303 Err(e) => IncomingStateMachine {
304 common: old_state.common,
305 state: IncomingSmStates::Failure(format!(
306 "Unexpected internal error occurred while decrypting the preimage: {e:?}"
307 )),
308 },
309 }
310 }
311
312 async fn refund_incoming_contract(
313 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
314 global_context: DynGlobalClientContext,
315 context: LightningClientContext,
316 old_state: IncomingStateMachine,
317 contract: Box<IncomingContractAccount>,
318 ) -> IncomingStateMachine {
319 debug!("Refunding incoming contract {contract:?}");
320 let claim_input = contract.claim();
321 let client_input = ClientInput::<LightningInput> {
322 input: claim_input,
323 amounts: Amounts::new_bitcoin(contract.amount),
324 keys: vec![context.redeem_key],
325 };
326
327 let change_range = global_context
328 .claim_inputs(dbtx, ClientInputBundle::new_no_sm(vec![client_input]))
329 .await
330 .expect("Cannot claim input, additional funding needed");
331 debug!("Refunded incoming contract {contract:?} with {change_range:?}");
332
333 IncomingStateMachine {
334 common: old_state.common,
335 state: IncomingSmStates::RefundSubmitted {
336 out_points: change_range.into_iter().collect(),
337 error: IncomingSmError::InvalidPreimage { contract },
338 },
339 }
340 }
341}
342
343#[derive(Debug, Clone, Eq, PartialEq, Decodable, Encodable)]
344pub struct AwaitingPreimageDecryption {
345 txid: TransactionId,
346}
347
348#[derive(Debug, Clone, Eq, PartialEq, Decodable, Encodable)]
349pub struct PreimageState {
350 preimage: Preimage,
351}
352
353#[derive(Debug, Clone, Eq, PartialEq, Decodable, Encodable)]
354pub struct RefundSuccessState {
355 refund_txid: TransactionId,
356}