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