1use std::time::Duration;
2
3use fedimint_api_client::api::DynModuleApi;
4use fedimint_client_module::DynGlobalClientContext;
5use fedimint_client_module::module::OutPointRange;
6use fedimint_client_module::sm::{ClientSMDatabaseTransaction, DynState, State, StateTransition};
7use fedimint_client_module::transaction::{ClientInput, ClientInputBundle};
8use fedimint_core::core::{IntoDynInstance, ModuleInstanceId, OperationId};
9use fedimint_core::encoding::{Decodable, Encodable};
10use fedimint_core::module::Amounts;
11use fedimint_core::secp256k1::Keypair;
12use fedimint_core::task::sleep;
13use fedimint_core::util::FmtCompact as _;
14use fedimint_core::{OutPoint, TransactionId};
15use fedimint_ln_common::LightningInput;
16use fedimint_ln_common::contracts::incoming::IncomingContractAccount;
17use fedimint_ln_common::contracts::{DecryptedPreimage, FundedContract};
18use fedimint_ln_common::federation_endpoint_constants::ACCOUNT_ENDPOINT;
19use fedimint_logging::LOG_CLIENT_MODULE_LN;
20use lightning_invoice::Bolt11Invoice;
21use serde::{Deserialize, Serialize};
22use thiserror::Error;
23use tracing::{debug, info};
24
25use crate::api::LnFederationApi;
26use crate::{LightningClientContext, ReceivingKey};
27
28const RETRY_DELAY: Duration = Duration::from_secs(1);
29
30#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
44pub enum LightningReceiveStates {
45 SubmittedOffer(LightningReceiveSubmittedOffer),
46 Canceled(LightningReceiveError),
47 ConfirmedInvoice(LightningReceiveConfirmedInvoice),
48 Funded(LightningReceiveFunded),
49 Success(Vec<OutPoint>),
50}
51
52#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
53pub struct LightningReceiveStateMachine {
54 pub operation_id: OperationId,
55 pub state: LightningReceiveStates,
56}
57
58impl State for LightningReceiveStateMachine {
59 type ModuleContext = LightningClientContext;
60
61 fn transitions(
62 &self,
63 context: &Self::ModuleContext,
64 global_context: &DynGlobalClientContext,
65 ) -> Vec<StateTransition<Self>> {
66 match &self.state {
67 LightningReceiveStates::SubmittedOffer(submitted_offer) => {
68 submitted_offer.transitions(global_context)
69 }
70 LightningReceiveStates::ConfirmedInvoice(confirmed_invoice) => {
71 confirmed_invoice.transitions(context, global_context)
72 }
73 LightningReceiveStates::Funded(funded) => funded.transitions(global_context),
74 LightningReceiveStates::Success(_) | LightningReceiveStates::Canceled(_) => {
75 vec![]
76 }
77 }
78 }
79
80 fn operation_id(&self) -> fedimint_core::core::OperationId {
81 self.operation_id
82 }
83}
84
85impl IntoDynInstance for LightningReceiveStateMachine {
86 type DynType = DynState;
87
88 fn into_dyn(self, instance_id: ModuleInstanceId) -> Self::DynType {
89 DynState::from_typed(instance_id, self)
90 }
91}
92
93#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
95pub struct LightningReceiveSubmittedOfferV0 {
96 pub offer_txid: TransactionId,
97 pub invoice: Bolt11Invoice,
98 pub payment_keypair: Keypair,
99}
100
101#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
102pub struct LightningReceiveSubmittedOffer {
103 pub offer_txid: TransactionId,
104 pub invoice: Bolt11Invoice,
105 pub receiving_key: ReceivingKey,
106}
107
108#[derive(
109 Error, Clone, Debug, Serialize, Deserialize, Encodable, Decodable, Eq, PartialEq, Hash,
110)]
111#[serde(rename_all = "snake_case")]
112pub enum LightningReceiveError {
113 #[error("Offer transaction was rejected")]
114 Rejected,
115 #[error("Incoming Lightning invoice was not paid within the timeout")]
116 Timeout,
117 #[error("Claim transaction was rejected")]
118 ClaimRejected,
119 #[error("The decrypted preimage was invalid")]
120 InvalidPreimage,
121}
122
123impl LightningReceiveSubmittedOffer {
124 fn transitions(
125 &self,
126 global_context: &DynGlobalClientContext,
127 ) -> Vec<StateTransition<LightningReceiveStateMachine>> {
128 let global_context = global_context.clone();
129 let txid = self.offer_txid;
130 let invoice = self.invoice.clone();
131 let receiving_key = self.receiving_key;
132 vec![StateTransition::new(
133 Self::await_invoice_confirmation(global_context, txid),
134 move |_dbtx, result, old_state| {
135 let invoice = invoice.clone();
136 Box::pin(async move {
137 Self::transition_confirmed_invoice(&result, &old_state, invoice, receiving_key)
138 })
139 },
140 )]
141 }
142
143 async fn await_invoice_confirmation(
144 global_context: DynGlobalClientContext,
145 txid: TransactionId,
146 ) -> Result<(), String> {
147 global_context.await_tx_accepted(txid).await
150 }
151
152 fn transition_confirmed_invoice(
153 result: &Result<(), String>,
154 old_state: &LightningReceiveStateMachine,
155 invoice: Bolt11Invoice,
156 receiving_key: ReceivingKey,
157 ) -> LightningReceiveStateMachine {
158 match result {
159 Ok(()) => LightningReceiveStateMachine {
160 operation_id: old_state.operation_id,
161 state: LightningReceiveStates::ConfirmedInvoice(LightningReceiveConfirmedInvoice {
162 invoice,
163 receiving_key,
164 }),
165 },
166 Err(_) => LightningReceiveStateMachine {
167 operation_id: old_state.operation_id,
168 state: LightningReceiveStates::Canceled(LightningReceiveError::Rejected),
169 },
170 }
171 }
172}
173
174#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
175pub struct LightningReceiveConfirmedInvoice {
176 pub(crate) invoice: Bolt11Invoice,
177 pub(crate) receiving_key: ReceivingKey,
178}
179
180impl LightningReceiveConfirmedInvoice {
181 fn transitions(
182 &self,
183 context: &LightningClientContext,
184 global_context: &DynGlobalClientContext,
185 ) -> Vec<StateTransition<LightningReceiveStateMachine>> {
186 let invoice = self.invoice.clone();
187 let receiving_key = self.receiving_key;
188 let global_context = global_context.clone();
189 let context = context.clone();
190 vec![StateTransition::new(
191 Self::await_incoming_contract_account(invoice, global_context.clone()),
192 move |dbtx, contract, old_state| {
193 let context = context.clone();
194 Box::pin(Self::transition_funded(
195 old_state,
196 receiving_key,
197 contract,
198 dbtx,
199 global_context.clone(),
200 context,
201 ))
202 },
203 )]
204 }
205
206 async fn await_incoming_contract_account(
207 invoice: Bolt11Invoice,
208 global_context: DynGlobalClientContext,
209 ) -> Result<IncomingContractAccount, LightningReceiveError> {
210 let contract_id = (*invoice.payment_hash()).into();
211 loop {
212 let now_epoch = fedimint_core::time::duration_since_epoch();
214 match get_incoming_contract(global_context.module_api(), contract_id).await {
215 Ok(Some(incoming_contract_account)) => {
216 match incoming_contract_account.contract.decrypted_preimage {
217 DecryptedPreimage::Pending => {
218 info!("Waiting for preimage decryption for contract {contract_id}");
221 }
222 DecryptedPreimage::Some(_) => return Ok(incoming_contract_account),
223 DecryptedPreimage::Invalid => {
224 return Err(LightningReceiveError::InvalidPreimage);
225 }
226 }
227 }
228 Ok(None) => {
229 const CLOCK_SKEW_TOLERANCE: Duration = Duration::from_mins(1);
232 if has_invoice_expired(&invoice, now_epoch, CLOCK_SKEW_TOLERANCE) {
233 return Err(LightningReceiveError::Timeout);
234 }
235 debug!("Still waiting preimage decryption for contract {contract_id}");
236 }
237 Err(error) => {
238 error.report_if_unusual("Awaiting incoming contract");
239 debug!(
240 target: LOG_CLIENT_MODULE_LN,
241 err = %error.fmt_compact(),
242 "External LN payment retryable error waiting for preimage decryption"
243 );
244 }
245 }
246 sleep(RETRY_DELAY).await;
247 }
248 }
249
250 async fn transition_funded(
251 old_state: LightningReceiveStateMachine,
252 receiving_key: ReceivingKey,
253 result: Result<IncomingContractAccount, LightningReceiveError>,
254 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
255 global_context: DynGlobalClientContext,
256 context: LightningClientContext,
257 ) -> LightningReceiveStateMachine {
258 match result {
259 Ok(contract) => {
260 if let Some(ref client_ctx) = context.client_ctx {
262 client_ctx
263 .log_event(
264 &mut dbtx.module_tx(),
265 crate::events::ReceivePaymentEvent {
266 operation_id: old_state.operation_id,
267 amount: contract.amount,
268 },
269 )
270 .await;
271 }
272
273 match receiving_key {
274 ReceivingKey::Personal(keypair) => {
275 let change_range =
276 Self::claim_incoming_contract(dbtx, contract, keypair, global_context)
277 .await;
278 LightningReceiveStateMachine {
279 operation_id: old_state.operation_id,
280 state: LightningReceiveStates::Funded(LightningReceiveFunded {
281 txid: change_range.txid(),
282 out_points: change_range.into_iter().collect(),
283 }),
284 }
285 }
286 ReceivingKey::External(_) => {
287 LightningReceiveStateMachine {
289 operation_id: old_state.operation_id,
290 state: LightningReceiveStates::Success(vec![]),
291 }
292 }
293 }
294 }
295 Err(e) => LightningReceiveStateMachine {
296 operation_id: old_state.operation_id,
297 state: LightningReceiveStates::Canceled(e),
298 },
299 }
300 }
301
302 async fn claim_incoming_contract(
303 dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
304 contract: IncomingContractAccount,
305 keypair: Keypair,
306 global_context: DynGlobalClientContext,
307 ) -> OutPointRange {
308 let input = contract.claim();
309 let client_input = ClientInput::<LightningInput> {
310 input,
311 amounts: Amounts::new_bitcoin(contract.amount),
312 keys: vec![keypair],
313 };
314
315 global_context
316 .claim_inputs(
317 dbtx,
318 ClientInputBundle::new_no_sm(vec![client_input]),
321 )
322 .await
323 .expect("Cannot claim input, additional funding needed")
324 }
325}
326
327fn has_invoice_expired(
328 invoice: &Bolt11Invoice,
329 now_epoch: Duration,
330 clock_skew_tolerance: Duration,
331) -> bool {
332 assert!(now_epoch >= clock_skew_tolerance);
333 invoice.would_expire(now_epoch.checked_sub(clock_skew_tolerance).unwrap())
335}
336
337pub async fn get_incoming_contract(
338 module_api: DynModuleApi,
339 contract_id: fedimint_ln_common::contracts::ContractId,
340) -> Result<Option<IncomingContractAccount>, fedimint_api_client::api::FederationError> {
341 match module_api.fetch_contract(contract_id).await {
342 Ok(Some(contract)) => {
343 if let FundedContract::Incoming(incoming) = contract.contract {
344 Ok(Some(IncomingContractAccount {
345 amount: contract.amount,
346 contract: incoming.contract,
347 }))
348 } else {
349 Err(fedimint_api_client::api::FederationError::general(
350 ACCOUNT_ENDPOINT,
351 contract_id,
352 fedimint_api_client::api::FederationGeneralError::UnexpectedResponse {
353 message: format!("Contract {contract_id} is not an incoming contract"),
354 },
355 ))
356 }
357 }
358 Ok(None) => Ok(None),
359 Err(e) => Err(e),
360 }
361}
362
363#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
364pub struct LightningReceiveFunded {
365 txid: TransactionId,
366 out_points: Vec<OutPoint>,
367}
368
369impl LightningReceiveFunded {
370 fn transitions(
371 &self,
372 global_context: &DynGlobalClientContext,
373 ) -> Vec<StateTransition<LightningReceiveStateMachine>> {
374 let out_points = self.out_points.clone();
375 vec![StateTransition::new(
376 Self::await_claim_success(global_context.clone(), self.txid),
377 move |_dbtx, result, old_state| {
378 let out_points = out_points.clone();
379 Box::pin(
380 async move { Self::transition_claim_success(&result, &old_state, out_points) },
381 )
382 },
383 )]
384 }
385
386 async fn await_claim_success(
387 global_context: DynGlobalClientContext,
388 txid: TransactionId,
389 ) -> Result<(), String> {
390 global_context.await_tx_accepted(txid).await
393 }
394
395 fn transition_claim_success(
396 result: &Result<(), String>,
397 old_state: &LightningReceiveStateMachine,
398 out_points: Vec<OutPoint>,
399 ) -> LightningReceiveStateMachine {
400 match result {
401 Ok(()) => {
402 LightningReceiveStateMachine {
404 operation_id: old_state.operation_id,
405 state: LightningReceiveStates::Success(out_points),
406 }
407 }
408 Err(_) => {
409 LightningReceiveStateMachine {
411 operation_id: old_state.operation_id,
412 state: LightningReceiveStates::Canceled(LightningReceiveError::ClaimRejected),
413 }
414 }
415 }
416 }
417}
418
419#[cfg(test)]
420mod tests;