Skip to main content

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