ln_gateway/state_machine/
mod.rs

1mod complete;
2pub mod pay;
3
4use std::collections::BTreeMap;
5use std::fmt;
6use std::sync::Arc;
7use std::time::Duration;
8
9use anyhow::ensure;
10use async_stream::stream;
11use bitcoin::hashes::{sha256, Hash};
12use bitcoin::key::Secp256k1;
13use bitcoin::secp256k1::All;
14use fedimint_api_client::api::DynModuleApi;
15use fedimint_client::derivable_secret::ChildId;
16use fedimint_client::module::init::{ClientModuleInit, ClientModuleInitArgs};
17use fedimint_client::module::recovery::NoModuleBackup;
18use fedimint_client::module::{ClientContext, ClientModule, IClientModule, OutPointRange};
19use fedimint_client::oplog::UpdateStreamOrOutcome;
20use fedimint_client::sm::util::MapStateTransitions;
21use fedimint_client::sm::{Context, DynState, ModuleNotifier, State};
22use fedimint_client::transaction::{
23    ClientOutput, ClientOutputBundle, ClientOutputSM, TransactionBuilder,
24};
25use fedimint_client::{sm_enum_variant_translation, AddStateMachinesError, DynGlobalClientContext};
26use fedimint_core::core::{Decoder, IntoDynInstance, ModuleInstanceId, ModuleKind, OperationId};
27use fedimint_core::db::{AutocommitError, DatabaseTransaction};
28use fedimint_core::encoding::{Decodable, Encodable};
29use fedimint_core::module::{ApiVersion, ModuleInit, MultiApiVersion};
30use fedimint_core::{apply, async_trait_maybe_send, secp256k1, Amount, OutPoint, TransactionId};
31use fedimint_ln_client::api::LnFederationApi;
32use fedimint_ln_client::incoming::{
33    FundingOfferState, IncomingSmCommon, IncomingSmError, IncomingSmStates, IncomingStateMachine,
34};
35use fedimint_ln_client::pay::{PayInvoicePayload, PaymentData};
36use fedimint_ln_client::{
37    create_incoming_contract_output, LightningClientContext, LightningClientInit,
38    RealGatewayConnection,
39};
40use fedimint_ln_common::config::LightningClientConfig;
41use fedimint_ln_common::contracts::{ContractId, Preimage};
42use fedimint_ln_common::route_hints::RouteHint;
43use fedimint_ln_common::{
44    create_gateway_remove_message, LightningCommonInit, LightningGateway,
45    LightningGatewayAnnouncement, LightningModuleTypes, LightningOutput, LightningOutputV0,
46    RemoveGatewayRequest, KIND,
47};
48use futures::StreamExt;
49use lightning_invoice::RoutingFees;
50use secp256k1::Keypair;
51use serde::{Deserialize, Serialize};
52use tracing::{debug, error, info, warn};
53
54use self::complete::GatewayCompleteStateMachine;
55use self::pay::{
56    GatewayPayCommon, GatewayPayInvoice, GatewayPayStateMachine, GatewayPayStates,
57    OutgoingPaymentError,
58};
59use crate::lightning::{InterceptPaymentRequest, LightningContext};
60use crate::state_machine::complete::{
61    GatewayCompleteCommon, GatewayCompleteStates, WaitForPreimageState,
62};
63use crate::Gateway;
64
65/// The high-level state of a reissue operation started with
66/// [`GatewayClientModule::gateway_pay_bolt11_invoice`].
67#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
68pub enum GatewayExtPayStates {
69    Created,
70    Preimage {
71        preimage: Preimage,
72    },
73    Success {
74        preimage: Preimage,
75        out_points: Vec<OutPoint>,
76    },
77    Canceled {
78        error: OutgoingPaymentError,
79    },
80    Fail {
81        error: OutgoingPaymentError,
82        error_message: String,
83    },
84    OfferDoesNotExist {
85        contract_id: ContractId,
86    },
87}
88
89/// The high-level state of an intercepted HTLC operation started with
90/// [`GatewayClientModule::gateway_handle_intercepted_htlc`].
91#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
92pub enum GatewayExtReceiveStates {
93    Funding,
94    Preimage(Preimage),
95    RefundSuccess {
96        out_points: Vec<OutPoint>,
97        error: IncomingSmError,
98    },
99    RefundError {
100        error_message: String,
101        error: IncomingSmError,
102    },
103    FundingFailed {
104        error: IncomingSmError,
105    },
106}
107
108#[derive(Debug, Clone, Serialize, Deserialize)]
109pub enum GatewayMeta {
110    Pay,
111    Receive,
112}
113
114#[derive(Debug, Clone)]
115pub struct GatewayClientInit {
116    pub timelock_delta: u64,
117    pub federation_index: u64,
118    pub gateway: Arc<Gateway>,
119}
120
121impl ModuleInit for GatewayClientInit {
122    type Common = LightningCommonInit;
123
124    async fn dump_database(
125        &self,
126        _dbtx: &mut DatabaseTransaction<'_>,
127        _prefix_names: Vec<String>,
128    ) -> Box<dyn Iterator<Item = (String, Box<dyn erased_serde::Serialize + Send>)> + '_> {
129        Box::new(vec![].into_iter())
130    }
131}
132
133#[apply(async_trait_maybe_send!)]
134impl ClientModuleInit for GatewayClientInit {
135    type Module = GatewayClientModule;
136
137    fn supported_api_versions(&self) -> MultiApiVersion {
138        MultiApiVersion::try_from_iter([ApiVersion { major: 0, minor: 0 }])
139            .expect("no version conflicts")
140    }
141
142    async fn init(&self, args: &ClientModuleInitArgs<Self>) -> anyhow::Result<Self::Module> {
143        Ok(GatewayClientModule {
144            cfg: args.cfg().clone(),
145            notifier: args.notifier().clone(),
146            redeem_key: args
147                .module_root_secret()
148                .child_key(ChildId(0))
149                .to_secp_key(&fedimint_core::secp256k1::Secp256k1::new()),
150            module_api: args.module_api().clone(),
151            timelock_delta: self.timelock_delta,
152            federation_index: self.federation_index,
153            client_ctx: args.context(),
154            gateway: self.gateway.clone(),
155        })
156    }
157}
158
159#[derive(Debug, Clone)]
160pub struct GatewayClientContext {
161    redeem_key: Keypair,
162    timelock_delta: u64,
163    secp: Secp256k1<All>,
164    pub ln_decoder: Decoder,
165    notifier: ModuleNotifier<GatewayClientStateMachines>,
166    gateway: Arc<Gateway>,
167}
168
169impl Context for GatewayClientContext {
170    const KIND: Option<ModuleKind> = Some(fedimint_ln_common::KIND);
171}
172
173impl From<&GatewayClientContext> for LightningClientContext {
174    fn from(ctx: &GatewayClientContext) -> Self {
175        LightningClientContext {
176            ln_decoder: ctx.ln_decoder.clone(),
177            redeem_key: ctx.redeem_key,
178            gateway_conn: Arc::new(RealGatewayConnection::default()),
179        }
180    }
181}
182
183/// Client side Lightning module **for the gateway**.
184///
185/// For the client side Lightning module for normal clients,
186/// see [`fedimint_ln_client::LightningClientModule`]
187#[derive(Debug)]
188pub struct GatewayClientModule {
189    cfg: LightningClientConfig,
190    pub notifier: ModuleNotifier<GatewayClientStateMachines>,
191    pub redeem_key: Keypair,
192    timelock_delta: u64,
193    federation_index: u64,
194    module_api: DynModuleApi,
195    client_ctx: ClientContext<Self>,
196    gateway: Arc<Gateway>,
197}
198
199impl ClientModule for GatewayClientModule {
200    type Init = LightningClientInit;
201    type Common = LightningModuleTypes;
202    type Backup = NoModuleBackup;
203    type ModuleStateMachineContext = GatewayClientContext;
204    type States = GatewayClientStateMachines;
205
206    fn context(&self) -> Self::ModuleStateMachineContext {
207        Self::ModuleStateMachineContext {
208            redeem_key: self.redeem_key,
209            timelock_delta: self.timelock_delta,
210            secp: Secp256k1::new(),
211            ln_decoder: self.decoder(),
212            notifier: self.notifier.clone(),
213            gateway: self.gateway.clone(),
214        }
215    }
216
217    fn input_fee(
218        &self,
219        _amount: Amount,
220        _input: &<Self::Common as fedimint_core::module::ModuleCommon>::Input,
221    ) -> Option<Amount> {
222        Some(self.cfg.fee_consensus.contract_input)
223    }
224
225    fn output_fee(
226        &self,
227        _amount: Amount,
228        output: &<Self::Common as fedimint_core::module::ModuleCommon>::Output,
229    ) -> Option<Amount> {
230        match output.maybe_v0_ref()? {
231            LightningOutputV0::Contract(_) => Some(self.cfg.fee_consensus.contract_output),
232            LightningOutputV0::Offer(_) | LightningOutputV0::CancelOutgoing { .. } => {
233                Some(Amount::ZERO)
234            }
235        }
236    }
237}
238
239impl GatewayClientModule {
240    fn to_gateway_registration_info(
241        &self,
242        route_hints: Vec<RouteHint>,
243        ttl: Duration,
244        fees: RoutingFees,
245        lightning_context: LightningContext,
246    ) -> LightningGatewayAnnouncement {
247        LightningGatewayAnnouncement {
248            info: LightningGateway {
249                federation_index: self.federation_index,
250                gateway_redeem_key: self.redeem_key.public_key(),
251                node_pub_key: lightning_context.lightning_public_key,
252                lightning_alias: lightning_context.lightning_alias,
253                api: self.gateway.versioned_api.clone(),
254                route_hints,
255                fees,
256                gateway_id: self.gateway.gateway_id,
257                supports_private_payments: lightning_context.lnrpc.supports_private_payments(),
258            },
259            ttl,
260            vetted: false,
261        }
262    }
263
264    async fn create_funding_incoming_contract_output_from_htlc(
265        &self,
266        htlc: Htlc,
267    ) -> Result<
268        (
269            OperationId,
270            Amount,
271            ClientOutput<LightningOutputV0>,
272            ClientOutputSM<GatewayClientStateMachines>,
273        ),
274        IncomingSmError,
275    > {
276        let operation_id = OperationId(htlc.payment_hash.to_byte_array());
277        let (incoming_output, amount, contract_id) = create_incoming_contract_output(
278            &self.module_api,
279            htlc.payment_hash,
280            htlc.outgoing_amount_msat,
281            &self.redeem_key,
282        )
283        .await?;
284
285        let client_output = ClientOutput::<LightningOutputV0> {
286            output: incoming_output,
287            amount,
288        };
289        let client_output_sm = ClientOutputSM::<GatewayClientStateMachines> {
290            state_machines: Arc::new(move |out_point_range: OutPointRange| {
291                assert_eq!(out_point_range.count(), 1);
292                vec![
293                    GatewayClientStateMachines::Receive(IncomingStateMachine {
294                        common: IncomingSmCommon {
295                            operation_id,
296                            contract_id,
297                            payment_hash: htlc.payment_hash,
298                        },
299                        state: IncomingSmStates::FundingOffer(FundingOfferState {
300                            txid: out_point_range.txid(),
301                        }),
302                    }),
303                    GatewayClientStateMachines::Complete(GatewayCompleteStateMachine {
304                        common: GatewayCompleteCommon {
305                            operation_id,
306                            payment_hash: htlc.payment_hash,
307                            incoming_chan_id: htlc.incoming_chan_id,
308                            htlc_id: htlc.htlc_id,
309                        },
310                        state: GatewayCompleteStates::WaitForPreimage(WaitForPreimageState),
311                    }),
312                ]
313            }),
314        };
315        Ok((operation_id, amount, client_output, client_output_sm))
316    }
317
318    async fn create_funding_incoming_contract_output_from_swap(
319        &self,
320        swap: SwapParameters,
321    ) -> Result<
322        (
323            OperationId,
324            ClientOutput<LightningOutputV0>,
325            ClientOutputSM<GatewayClientStateMachines>,
326        ),
327        IncomingSmError,
328    > {
329        let payment_hash = swap.payment_hash;
330        let operation_id = OperationId(payment_hash.to_byte_array());
331        let (incoming_output, amount, contract_id) = create_incoming_contract_output(
332            &self.module_api,
333            payment_hash,
334            swap.amount_msat,
335            &self.redeem_key,
336        )
337        .await?;
338
339        let client_output = ClientOutput::<LightningOutputV0> {
340            output: incoming_output,
341            amount,
342        };
343        let client_output_sm = ClientOutputSM::<GatewayClientStateMachines> {
344            state_machines: Arc::new(move |out_point_range| {
345                assert_eq!(out_point_range.count(), 1);
346                vec![GatewayClientStateMachines::Receive(IncomingStateMachine {
347                    common: IncomingSmCommon {
348                        operation_id,
349                        contract_id,
350                        payment_hash,
351                    },
352                    state: IncomingSmStates::FundingOffer(FundingOfferState {
353                        txid: out_point_range.txid(),
354                    }),
355                })]
356            }),
357        };
358        Ok((operation_id, client_output, client_output_sm))
359    }
360
361    /// Register gateway with federation
362    pub async fn try_register_with_federation(
363        &self,
364        route_hints: Vec<RouteHint>,
365        time_to_live: Duration,
366        fees: RoutingFees,
367        lightning_context: LightningContext,
368    ) {
369        let registration_info =
370            self.to_gateway_registration_info(route_hints, time_to_live, fees, lightning_context);
371        let gateway_id = registration_info.info.gateway_id;
372
373        let federation_id = self
374            .client_ctx
375            .get_config()
376            .await
377            .global
378            .calculate_federation_id();
379        if let Err(e) = self.module_api.register_gateway(&registration_info).await {
380            warn!(
381                ?e,
382                "Failed to register gateway {gateway_id} with federation {federation_id}"
383            );
384        } else {
385            info!("Successfully registered gateway {gateway_id} with federation {federation_id}");
386        }
387    }
388
389    /// Attempts to remove a gateway's registration from the federation. Since
390    /// removing gateway registrations is best effort, this does not return
391    /// an error and simply emits a warning when the registration cannot be
392    /// removed.
393    pub async fn remove_from_federation(&self, gateway_keypair: Keypair) {
394        // Removing gateway registrations is best effort, so just emit a warning if it
395        // fails
396        if let Err(e) = self.remove_from_federation_inner(gateway_keypair).await {
397            let gateway_id = gateway_keypair.public_key();
398            let federation_id = self
399                .client_ctx
400                .get_config()
401                .await
402                .global
403                .calculate_federation_id();
404            warn!("Failed to remove gateway {gateway_id} from federation {federation_id}: {e:?}");
405        }
406    }
407
408    /// Retrieves the signing challenge from each federation peer. Since each
409    /// peer maintains their own list of registered gateways, the gateway
410    /// needs to provide a signature that is signed by the private key of the
411    /// gateway id to remove the registration.
412    async fn remove_from_federation_inner(&self, gateway_keypair: Keypair) -> anyhow::Result<()> {
413        let gateway_id = gateway_keypair.public_key();
414        let challenges = self
415            .module_api
416            .get_remove_gateway_challenge(gateway_id)
417            .await;
418
419        let fed_public_key = self.cfg.threshold_pub_key;
420        let signatures = challenges
421            .into_iter()
422            .filter_map(|(peer_id, challenge)| {
423                let msg = create_gateway_remove_message(fed_public_key, peer_id, challenge?);
424                let signature = gateway_keypair.sign_schnorr(msg);
425                Some((peer_id, signature))
426            })
427            .collect::<BTreeMap<_, _>>();
428
429        let remove_gateway_request = RemoveGatewayRequest {
430            gateway_id,
431            signatures,
432        };
433
434        self.module_api.remove_gateway(remove_gateway_request).await;
435
436        Ok(())
437    }
438
439    /// Attempt fulfill HTLC by buying preimage from the federation
440    pub async fn gateway_handle_intercepted_htlc(&self, htlc: Htlc) -> anyhow::Result<OperationId> {
441        debug!("Handling intercepted HTLC {htlc:?}");
442        let (operation_id, amount, client_output, client_output_sm) = self
443            .create_funding_incoming_contract_output_from_htlc(htlc.clone())
444            .await?;
445
446        let output = ClientOutput {
447            output: LightningOutput::V0(client_output.output),
448            amount,
449        };
450
451        let tx = TransactionBuilder::new().with_outputs(self.client_ctx.make_client_outputs(
452            ClientOutputBundle::new(vec![output], vec![client_output_sm]),
453        ));
454        let operation_meta_gen = |_: TransactionId, _: Vec<OutPoint>| GatewayMeta::Receive;
455        self.client_ctx
456            .finalize_and_submit_transaction(operation_id, KIND.as_str(), operation_meta_gen, tx)
457            .await?;
458        debug!(?operation_id, "Submitted transaction for HTLC {htlc:?}");
459        Ok(operation_id)
460    }
461
462    /// Attempt buying preimage from this federation in order to fulfill a pay
463    /// request in another federation served by this gateway. In direct swap
464    /// scenario, the gateway DOES NOT send payment over the lightning network
465    async fn gateway_handle_direct_swap(
466        &self,
467        swap_params: SwapParameters,
468    ) -> anyhow::Result<OperationId> {
469        debug!("Handling direct swap {swap_params:?}");
470        let (operation_id, client_output, client_output_sm) = self
471            .create_funding_incoming_contract_output_from_swap(swap_params.clone())
472            .await?;
473
474        let output = ClientOutput {
475            output: LightningOutput::V0(client_output.output),
476            amount: client_output.amount,
477        };
478        let tx = TransactionBuilder::new().with_outputs(self.client_ctx.make_client_outputs(
479            ClientOutputBundle::new(vec![output], vec![client_output_sm]),
480        ));
481        let operation_meta_gen = |_: TransactionId, _: Vec<OutPoint>| GatewayMeta::Receive;
482        self.client_ctx
483            .finalize_and_submit_transaction(operation_id, KIND.as_str(), operation_meta_gen, tx)
484            .await?;
485        debug!(
486            ?operation_id,
487            "Submitted transaction for direct swap {swap_params:?}"
488        );
489        Ok(operation_id)
490    }
491
492    /// Subscribe to updates when the gateway is handling an intercepted HTLC,
493    /// or direct swap between federations
494    pub async fn gateway_subscribe_ln_receive(
495        &self,
496        operation_id: OperationId,
497    ) -> anyhow::Result<UpdateStreamOrOutcome<GatewayExtReceiveStates>> {
498        let operation = self.client_ctx.get_operation(operation_id).await?;
499        let mut stream = self.notifier.subscribe(operation_id).await;
500        let client_ctx = self.client_ctx.clone();
501
502        Ok(self.client_ctx.outcome_or_updates(&operation, operation_id, || {
503            stream! {
504
505                yield GatewayExtReceiveStates::Funding;
506
507                let state = loop {
508                    debug!("Getting next ln receive state for {}", operation_id.fmt_short());
509                    if let Some(GatewayClientStateMachines::Receive(state)) = stream.next().await {
510                        match state.state {
511                            IncomingSmStates::Preimage(preimage) =>{
512                                debug!(?operation_id, "Received preimage");
513                                break GatewayExtReceiveStates::Preimage(preimage)
514                            },
515                            IncomingSmStates::RefundSubmitted { out_points, error } => {
516                                debug!(?operation_id, "Refund submitted for {out_points:?} {error}");
517                                match client_ctx.await_primary_module_outputs(operation_id, out_points.clone()).await {
518                                    Ok(()) => {
519                                        debug!(?operation_id, "Refund success");
520                                        break GatewayExtReceiveStates::RefundSuccess { out_points, error }
521                                    },
522                                    Err(e) => {
523                                        warn!(?operation_id, "Got failure {e:?} while awaiting for refund outputs {out_points:?}");
524                                        break GatewayExtReceiveStates::RefundError{ error_message: e.to_string(), error }
525                                    },
526                                }
527                            },
528                            IncomingSmStates::FundingFailed { error } => {
529                                warn!(?operation_id, "Funding failed: {error:?}");
530                                break GatewayExtReceiveStates::FundingFailed{ error }
531                            },
532                            other => {
533                                debug!("Got state {other:?} while awaiting for output of {}", operation_id.fmt_short());
534                            }
535                        }
536                    }
537                };
538                yield state;
539            }
540        }))
541    }
542
543    /// For the given `OperationId`, this function will wait until the Complete
544    /// state machine has finished or failed.
545    pub async fn await_completion(&self, operation_id: OperationId) {
546        let mut stream = self.notifier.subscribe(operation_id).await;
547        loop {
548            match stream.next().await {
549                Some(GatewayClientStateMachines::Complete(state)) => match state.state {
550                    GatewayCompleteStates::HtlcFinished => {
551                        info!(%state, "LNv1 completion state machine finished");
552                        return;
553                    }
554                    GatewayCompleteStates::Failure => {
555                        error!(%state, "LNv1 completion state machine failed");
556                        return;
557                    }
558                    _ => {
559                        info!(%state, "Waiting for LNv1 completion state machine");
560                        continue;
561                    }
562                },
563                Some(GatewayClientStateMachines::Receive(state)) => {
564                    info!(%state, "Waiting for LNv1 completion state machine");
565                    continue;
566                }
567                Some(state) => {
568                    warn!(%state, "Operation is not an LNv1 completion state machine");
569                    return;
570                }
571                None => return,
572            }
573        }
574    }
575
576    /// Pay lightning invoice on behalf of federation user
577    pub async fn gateway_pay_bolt11_invoice(
578        &self,
579        pay_invoice_payload: PayInvoicePayload,
580    ) -> anyhow::Result<OperationId> {
581        let payload = pay_invoice_payload.clone();
582        let lightning_context = self.gateway.get_lightning_context().await?;
583
584        if matches!(
585            pay_invoice_payload.payment_data,
586            PaymentData::PrunedInvoice { .. }
587        ) {
588            ensure!(
589                lightning_context.lnrpc.supports_private_payments(),
590                "Private payments are not supported by the lightning node"
591            );
592        }
593
594        self.client_ctx.module_db()
595            .autocommit(
596                |dbtx, _| {
597                    Box::pin(async {
598                        let operation_id = OperationId(payload.contract_id.to_byte_array());
599
600                        let state_machines =
601                            vec![GatewayClientStateMachines::Pay(GatewayPayStateMachine {
602                                common: GatewayPayCommon { operation_id },
603                                state: GatewayPayStates::PayInvoice(GatewayPayInvoice {
604                                    pay_invoice_payload: payload.clone(),
605                                }),
606                            })];
607
608                        let dyn_states = state_machines
609                            .into_iter()
610                            .map(|s| self.client_ctx.make_dyn(s))
611                            .collect();
612
613                            match self.client_ctx.add_state_machines_dbtx(dbtx, dyn_states).await {
614                                Ok(()) => {
615                                    self.client_ctx
616                                        .add_operation_log_entry_dbtx(
617                                            dbtx,
618                                            operation_id,
619                                            KIND.as_str(),
620                                            GatewayMeta::Pay,
621                                        )
622                                        .await;
623                                }
624                                Err(AddStateMachinesError::StateAlreadyExists) => {
625                                    info!("State machine for operation {} already exists, will not add a new one", operation_id.fmt_short());
626                                }
627                                Err(other) => {
628                                    anyhow::bail!("Failed to add state machines: {other:?}")
629                                }
630                            }
631                            Ok(operation_id)
632                    })
633                },
634                Some(100),
635            )
636            .await
637            .map_err(|e| match e {
638                AutocommitError::ClosureError { error, .. } => error,
639                AutocommitError::CommitFailed { last_error, .. } => {
640                    anyhow::anyhow!("Commit to DB failed: {last_error}")
641                }
642            })
643    }
644
645    pub async fn gateway_subscribe_ln_pay(
646        &self,
647        operation_id: OperationId,
648    ) -> anyhow::Result<UpdateStreamOrOutcome<GatewayExtPayStates>> {
649        let mut stream = self.notifier.subscribe(operation_id).await;
650        let operation = self.client_ctx.get_operation(operation_id).await?;
651        let client_ctx = self.client_ctx.clone();
652
653        Ok(self.client_ctx.outcome_or_updates(&operation, operation_id, || {
654            stream! {
655                yield GatewayExtPayStates::Created;
656
657                loop {
658                    debug!("Getting next ln pay state for {}", operation_id.fmt_short());
659                    if let Some(GatewayClientStateMachines::Pay(state)) = stream.next().await {
660                        match state.state {
661                            GatewayPayStates::Preimage(out_points, preimage) => {
662                                yield GatewayExtPayStates::Preimage{ preimage: preimage.clone() };
663
664                                match client_ctx.await_primary_module_outputs(operation_id, out_points.clone()).await {
665                                    Ok(()) => {
666                                        debug!(?operation_id, "Success");
667                                        yield GatewayExtPayStates::Success{ preimage: preimage.clone(), out_points };
668                                        return;
669
670                                    }
671                                    Err(e) => {
672                                        warn!(?operation_id, "Got failure {e:?} while awaiting for outputs {out_points:?}");
673                                        // TODO: yield something here?
674                                    }
675                                }
676                            }
677                            GatewayPayStates::Canceled { txid, contract_id, error } => {
678                                debug!(?operation_id, "Trying to cancel contract {contract_id:?} due to {error:?}");
679                                match client_ctx.transaction_updates(operation_id).await.await_tx_accepted(txid).await {
680                                    Ok(()) => {
681                                        debug!(?operation_id, "Canceled contract {contract_id:?} due to {error:?}");
682                                        yield GatewayExtPayStates::Canceled{ error };
683                                        return;
684                                    }
685                                    Err(e) => {
686                                        warn!(?operation_id, "Got failure {e:?} while awaiting for transaction {txid} to be accepted for");
687                                        yield GatewayExtPayStates::Fail { error, error_message: format!("Refund transaction {txid} was not accepted by the federation. OperationId: {} Error: {e:?}", operation_id.fmt_short()) };
688                                    }
689                                }
690                            }
691                            GatewayPayStates::OfferDoesNotExist(contract_id) => {
692                                warn!("Yielding OfferDoesNotExist state for {} and contract {contract_id}", operation_id.fmt_short());
693                                yield GatewayExtPayStates::OfferDoesNotExist { contract_id };
694                            }
695                            GatewayPayStates::Failed{ error, error_message } => {
696                                warn!("Yielding Fail state for {} due to {error:?} {error_message:?}", operation_id.fmt_short());
697                                yield GatewayExtPayStates::Fail{ error, error_message };
698                            },
699                            GatewayPayStates::PayInvoice(_) => {
700                                debug!("Got initial state PayInvoice while awaiting for output of {}", operation_id.fmt_short());
701                            }
702                            other => {
703                                info!("Got state {other:?} while awaiting for output of {}", operation_id.fmt_short());
704                            }
705                        }
706                    } else {
707                        warn!("Got None while getting next ln pay state for {}", operation_id.fmt_short());
708                    }
709                }
710            }
711        }))
712    }
713}
714
715#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
716pub enum GatewayClientStateMachines {
717    Pay(GatewayPayStateMachine),
718    Receive(IncomingStateMachine),
719    Complete(GatewayCompleteStateMachine),
720}
721
722impl fmt::Display for GatewayClientStateMachines {
723    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
724        match self {
725            GatewayClientStateMachines::Pay(pay) => {
726                write!(f, "{pay}")
727            }
728            GatewayClientStateMachines::Receive(receive) => {
729                write!(f, "{receive}")
730            }
731            GatewayClientStateMachines::Complete(complete) => {
732                write!(f, "{complete}")
733            }
734        }
735    }
736}
737
738impl IntoDynInstance for GatewayClientStateMachines {
739    type DynType = DynState;
740
741    fn into_dyn(self, instance_id: ModuleInstanceId) -> Self::DynType {
742        DynState::from_typed(instance_id, self)
743    }
744}
745
746impl State for GatewayClientStateMachines {
747    type ModuleContext = GatewayClientContext;
748
749    fn transitions(
750        &self,
751        context: &Self::ModuleContext,
752        global_context: &DynGlobalClientContext,
753    ) -> Vec<fedimint_client::sm::StateTransition<Self>> {
754        match self {
755            GatewayClientStateMachines::Pay(pay_state) => {
756                sm_enum_variant_translation!(
757                    pay_state.transitions(context, global_context),
758                    GatewayClientStateMachines::Pay
759                )
760            }
761            GatewayClientStateMachines::Receive(receive_state) => {
762                sm_enum_variant_translation!(
763                    receive_state.transitions(&context.into(), global_context),
764                    GatewayClientStateMachines::Receive
765                )
766            }
767            GatewayClientStateMachines::Complete(complete_state) => {
768                sm_enum_variant_translation!(
769                    complete_state.transitions(context, global_context),
770                    GatewayClientStateMachines::Complete
771                )
772            }
773        }
774    }
775
776    fn operation_id(&self) -> fedimint_core::core::OperationId {
777        match self {
778            GatewayClientStateMachines::Pay(pay_state) => pay_state.operation_id(),
779            GatewayClientStateMachines::Receive(receive_state) => receive_state.operation_id(),
780            GatewayClientStateMachines::Complete(complete_state) => complete_state.operation_id(),
781        }
782    }
783}
784
785#[derive(Debug, Clone, Eq, PartialEq)]
786pub struct Htlc {
787    /// The HTLC payment hash.
788    pub payment_hash: sha256::Hash,
789    /// The incoming HTLC amount in millisatoshi.
790    pub incoming_amount_msat: Amount,
791    /// The outgoing HTLC amount in millisatoshi
792    pub outgoing_amount_msat: Amount,
793    /// The incoming HTLC expiry
794    pub incoming_expiry: u32,
795    /// The short channel id of the HTLC.
796    pub short_channel_id: Option<u64>,
797    /// The id of the incoming channel
798    pub incoming_chan_id: u64,
799    /// The index of the incoming htlc in the incoming channel
800    pub htlc_id: u64,
801}
802
803impl TryFrom<InterceptPaymentRequest> for Htlc {
804    type Error = anyhow::Error;
805
806    fn try_from(s: InterceptPaymentRequest) -> Result<Self, Self::Error> {
807        Ok(Self {
808            payment_hash: s.payment_hash,
809            incoming_amount_msat: Amount::from_msats(s.amount_msat),
810            outgoing_amount_msat: Amount::from_msats(s.amount_msat),
811            incoming_expiry: s.expiry,
812            short_channel_id: s.short_channel_id,
813            incoming_chan_id: s.incoming_chan_id,
814            htlc_id: s.htlc_id,
815        })
816    }
817}
818
819#[derive(Debug, Clone)]
820struct SwapParameters {
821    payment_hash: sha256::Hash,
822    amount_msat: Amount,
823}
824
825impl TryFrom<PaymentData> for SwapParameters {
826    type Error = anyhow::Error;
827
828    fn try_from(s: PaymentData) -> Result<Self, Self::Error> {
829        let payment_hash = s.payment_hash();
830        let amount_msat = s
831            .amount()
832            .ok_or_else(|| anyhow::anyhow!("Amountless invoice cannot be used in direct swap"))?;
833        Ok(Self {
834            payment_hash,
835            amount_msat,
836        })
837    }
838}