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