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