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