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