fedimint_gw_client/
lib.rs

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