Skip to main content

fedimint_gw_client/
lib.rs

1mod complete;
2pub mod events;
3pub mod pay;
4#[cfg(test)]
5mod tests;
6
7use std::collections::BTreeMap;
8use std::fmt;
9use std::fmt::Debug;
10use std::future::Future;
11use std::sync::Arc;
12use std::time::Duration;
13
14use async_stream::stream;
15use async_trait::async_trait;
16use bitcoin::hashes::{Hash, sha256};
17use bitcoin::key::Secp256k1;
18use bitcoin::secp256k1::All;
19use complete::{GatewayCompleteCommon, GatewayCompleteStates, WaitForPreimageState};
20use events::{IncomingPaymentStarted, OutgoingPaymentStarted};
21use fedimint_api_client::api::DynModuleApi;
22use fedimint_client::ClientHandleArc;
23use fedimint_client_module::module::init::{ClientModuleInit, ClientModuleInitArgs};
24use fedimint_client_module::module::recovery::NoModuleBackup;
25use fedimint_client_module::module::{ClientContext, ClientModule, IClientModule, OutPointRange};
26use fedimint_client_module::oplog::UpdateStreamOrOutcome;
27use fedimint_client_module::sm::{Context, DynState, ModuleNotifier, State, StateTransition};
28use fedimint_client_module::transaction::{
29    ClientOutput, ClientOutputBundle, ClientOutputSM, TransactionBuilder,
30};
31use fedimint_client_module::{
32    AddStateMachinesError, DynGlobalClientContext, sm_enum_variant_translation,
33};
34use fedimint_connectors::ConnectorRegistry;
35use fedimint_core::config::FederationId;
36use fedimint_core::core::{Decoder, IntoDynInstance, ModuleInstanceId, ModuleKind, OperationId};
37use fedimint_core::db::{AutocommitError, DatabaseTransaction};
38use fedimint_core::encoding::{Decodable, Encodable};
39use fedimint_core::module::{Amounts, ApiVersion, ModuleInit, MultiApiVersion};
40use fedimint_core::time::duration_since_epoch;
41use fedimint_core::util::{FmtCompact, SafeUrl, Spanned};
42use fedimint_core::{Amount, OutPoint, apply, async_trait_maybe_send, secp256k1};
43use fedimint_derive_secret::ChildId;
44use fedimint_lightning::{
45    InterceptPaymentRequest, InterceptPaymentResponse, LightningContext, LightningRpcError,
46    PayInvoiceResponse,
47};
48use fedimint_ln_client::api::LnFederationApi;
49use fedimint_ln_client::incoming::{
50    FundingOfferState, IncomingSmCommon, IncomingSmError, IncomingSmStates, IncomingStateMachine,
51};
52use fedimint_ln_client::pay::{PayInvoicePayload, PaymentData};
53use fedimint_ln_client::{
54    LightningClientContext, LightningClientInit, RealGatewayConnection,
55    create_incoming_contract_output,
56};
57use fedimint_ln_common::config::LightningClientConfig;
58use fedimint_ln_common::contracts::outgoing::OutgoingContractAccount;
59use fedimint_ln_common::contracts::{ContractId, Preimage};
60use fedimint_ln_common::route_hints::RouteHint;
61use fedimint_ln_common::{
62    GatewayRegistrationAuth, KIND, LNV1_INCOMING_HTLC_ADVERTISED_EXPIRY_DELTA,
63    LNV1_INCOMING_HTLC_EXPIRY_SAFETY_MARGIN, LightningCommonInit, LightningGateway,
64    LightningGatewayAnnouncement, LightningModuleTypes, LightningOutput, LightningOutputV0,
65    RemoveGatewayRequest, create_gateway_registration_message, create_gateway_remove_message,
66};
67use fedimint_lnv2_common::GatewayApi;
68use futures::StreamExt;
69use lightning_invoice::RoutingFees;
70use secp256k1::Keypair;
71use serde::{Deserialize, Serialize};
72use thiserror::Error;
73use tracing::{debug, error, info, warn};
74
75use self::complete::GatewayCompleteStateMachine;
76use self::pay::{
77    GatewayPayCommon, GatewayPayInvoice, GatewayPayStateMachine, GatewayPayStates,
78    OutgoingContractError, OutgoingPaymentError,
79};
80
81/// Exclusive remaining-CLTV safety margin for an intercepted LNv1 HTLC.
82///
83/// This reserves the worst-case time to claim the HTLC on-chain after
84/// federation funding and threshold decryption. Exactly the margin is
85/// rejected; fresh funding requires at least one additional block. The value
86/// deliberately matches the route-hint delta advertised by pre-upgrade
87/// clients so their invoices remain payable; see
88/// [`LNV1_INCOMING_HTLC_EXPIRY_SAFETY_MARGIN`] for the plan to raise it.
89pub const LNV1_HTLC_EXPIRY_SAFETY_MARGIN: u32 = LNV1_INCOMING_HTLC_EXPIRY_SAFETY_MARGIN as u32;
90
91/// The high-level state of a reissue operation started with
92/// [`GatewayClientModule::gateway_pay_bolt11_invoice`].
93#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
94pub enum GatewayExtPayStates {
95    Created,
96    Preimage {
97        preimage: Preimage,
98    },
99    Success {
100        preimage: Preimage,
101        out_points: Vec<OutPoint>,
102    },
103    Canceled {
104        error: OutgoingPaymentError,
105    },
106    Fail {
107        error: OutgoingPaymentError,
108        error_message: String,
109    },
110    OfferDoesNotExist {
111        contract_id: ContractId,
112    },
113}
114
115/// The high-level state of an intercepted HTLC operation started with
116/// [`GatewayClientModule::gateway_handle_intercepted_htlc`].
117#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
118pub enum GatewayExtReceiveStates {
119    Funding,
120    Preimage(Preimage),
121    RefundSuccess {
122        out_points: Vec<OutPoint>,
123        error: IncomingSmError,
124    },
125    RefundError {
126        error_message: String,
127        error: IncomingSmError,
128    },
129    FundingFailed {
130        error: IncomingSmError,
131    },
132}
133
134#[derive(Debug, Clone, Serialize, Deserialize)]
135pub enum GatewayMeta {
136    /// Carries the `preimage_auth` of the request that started the payment.
137    ///
138    /// The operation id of a payment is its contract id, which is public in the
139    /// funding transaction, and joining the operation yields the preimage. So
140    /// the operation has to record who is allowed to join it. Entries written
141    /// before this field existed decode as an error and are treated as
142    /// unauthorized, which only affects payments in flight across an upgrade.
143    Pay {
144        preimage_auth: sha256::Hash,
145    },
146    Receive,
147}
148
149#[derive(Debug, Clone)]
150pub struct GatewayClientInit {
151    pub federation_index: u64,
152    pub lightning_manager: Arc<dyn IGatewayClientV1>,
153}
154
155impl ModuleInit for GatewayClientInit {
156    type Common = LightningCommonInit;
157
158    async fn dump_database(
159        &self,
160        _dbtx: &mut DatabaseTransaction<'_>,
161        _prefix_names: Vec<String>,
162    ) -> Box<dyn Iterator<Item = (String, Box<dyn erased_serde::Serialize + Send>)> + '_> {
163        Box::new(vec![].into_iter())
164    }
165}
166
167#[apply(async_trait_maybe_send!)]
168impl ClientModuleInit for GatewayClientInit {
169    type Module = GatewayClientModule;
170
171    fn supported_api_versions(&self) -> MultiApiVersion {
172        MultiApiVersion::try_from_iter([ApiVersion { major: 0, minor: 0 }])
173            .expect("no version conflicts")
174    }
175
176    async fn init(&self, args: &ClientModuleInitArgs<Self>) -> anyhow::Result<Self::Module> {
177        Ok(GatewayClientModule {
178            cfg: args.cfg().clone(),
179            notifier: args.notifier().clone(),
180            redeem_key: args
181                .module_root_secret()
182                .child_key(ChildId(0))
183                .to_secp_key(&fedimint_core::secp256k1::Secp256k1::new()),
184            module_api: args.module_api().clone(),
185            federation_index: self.federation_index,
186            client_ctx: args.context(),
187            lightning_manager: self.lightning_manager.clone(),
188            connector_registry: args.connector_registry.clone(),
189            intercepted_htlc_lock_pool: lockable::LockPool::new(),
190        })
191    }
192}
193
194#[derive(Debug, Clone)]
195pub struct GatewayClientContext {
196    redeem_key: Keypair,
197    secp: Secp256k1<All>,
198    pub ln_decoder: Decoder,
199    notifier: ModuleNotifier<GatewayClientStateMachines>,
200    pub client_ctx: ClientContext<GatewayClientModule>,
201    pub lightning_manager: Arc<dyn IGatewayClientV1>,
202    pub connector_registry: ConnectorRegistry,
203}
204
205impl Context for GatewayClientContext {
206    const KIND: Option<ModuleKind> = Some(fedimint_ln_common::KIND);
207}
208
209impl From<&GatewayClientContext> for LightningClientContext {
210    fn from(ctx: &GatewayClientContext) -> Self {
211        let gateway_conn = RealGatewayConnection {
212            api: GatewayApi::new(None, ctx.connector_registry.clone()),
213        };
214        LightningClientContext {
215            ln_decoder: ctx.ln_decoder.clone(),
216            redeem_key: ctx.redeem_key,
217            gateway_conn: Arc::new(gateway_conn),
218            client_ctx: None,
219        }
220    }
221}
222
223/// Client side Lightning module **for the gateway**.
224///
225/// For the client side Lightning module for normal clients,
226/// see [`fedimint_ln_client::LightningClientModule`]
227pub struct GatewayClientModule {
228    cfg: LightningClientConfig,
229    pub notifier: ModuleNotifier<GatewayClientStateMachines>,
230    pub redeem_key: Keypair,
231    federation_index: u64,
232    module_api: DynModuleApi,
233    client_ctx: ClientContext<Self>,
234    pub lightning_manager: Arc<dyn IGatewayClientV1>,
235    connector_registry: ConnectorRegistry,
236    intercepted_htlc_lock_pool: lockable::LockPool<HtlcCircuitKey>,
237}
238
239impl fmt::Debug for GatewayClientModule {
240    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
241        f.debug_struct("GatewayClientModule")
242            .finish_non_exhaustive()
243    }
244}
245
246impl ClientModule for GatewayClientModule {
247    type Init = LightningClientInit;
248    type Common = LightningModuleTypes;
249    type Backup = NoModuleBackup;
250    type ModuleStateMachineContext = GatewayClientContext;
251    type States = GatewayClientStateMachines;
252
253    fn context(&self) -> Self::ModuleStateMachineContext {
254        Self::ModuleStateMachineContext {
255            redeem_key: self.redeem_key,
256            secp: Secp256k1::new(),
257            ln_decoder: self.decoder(),
258            notifier: self.notifier.clone(),
259            client_ctx: self.client_ctx.clone(),
260            lightning_manager: self.lightning_manager.clone(),
261            connector_registry: self.connector_registry.clone(),
262        }
263    }
264
265    fn input_fee(
266        &self,
267        _amount: &Amounts,
268        _input: &<Self::Common as fedimint_core::module::ModuleCommon>::Input,
269    ) -> Option<Amounts> {
270        Some(Amounts::new_bitcoin(self.cfg.fee_consensus.contract_input))
271    }
272
273    fn output_fee(
274        &self,
275        _amount: &Amounts,
276        output: &<Self::Common as fedimint_core::module::ModuleCommon>::Output,
277    ) -> Option<Amounts> {
278        match output.maybe_v0_ref()? {
279            LightningOutputV0::Contract(_) => {
280                Some(Amounts::new_bitcoin(self.cfg.fee_consensus.contract_output))
281            }
282            LightningOutputV0::Offer(_) | LightningOutputV0::CancelOutgoing { .. } => {
283                Some(Amounts::ZERO)
284            }
285        }
286    }
287}
288
289impl GatewayClientModule {
290    fn to_gateway_registration_info(
291        &self,
292        route_hints: Vec<RouteHint>,
293        ttl: Duration,
294        fees: RoutingFees,
295        lightning_context: LightningContext,
296        api: SafeUrl,
297        gateway_keypair: Keypair,
298    ) -> LightningGatewayAnnouncement {
299        let info = LightningGateway {
300            federation_index: self.federation_index,
301            gateway_redeem_key: self.redeem_key.public_key(),
302            node_pub_key: lightning_context.lightning_public_key,
303            lightning_alias: lightning_context.lightning_alias,
304            api,
305            route_hints,
306            fees,
307            gateway_id: gateway_keypair.public_key(),
308            supports_private_payments: lightning_context.lnrpc.supports_private_payments(),
309        };
310
311        // Proves to the guardians that we hold the key behind `gateway_id`, so
312        // nobody else can replace our registration. Wall-clock milliseconds give
313        // a nonce that keeps increasing across restarts without persisting
314        // state; guardians only compare it to the nonce they already hold for
315        // us, so our clock need not agree with theirs.
316        let nonce = u64::try_from(duration_since_epoch().as_millis())
317            .expect("milliseconds since the epoch do not exceed u64 for another 500m years");
318        let signature = gateway_keypair.sign_schnorr(create_gateway_registration_message(
319            self.cfg.threshold_pub_key,
320            nonce,
321            &info,
322        ));
323
324        LightningGatewayAnnouncement {
325            info,
326            ttl,
327            vetted: false,
328            auth: Some(GatewayRegistrationAuth { nonce, signature }),
329        }
330    }
331
332    async fn create_funding_incoming_contract_output_from_htlc(
333        &self,
334        htlc: Htlc,
335    ) -> Result<
336        (
337            OperationId,
338            Amount,
339            ClientOutput<LightningOutputV0>,
340            ClientOutputSM<GatewayClientStateMachines>,
341            ContractId,
342        ),
343        IncomingSmError,
344    > {
345        let operation_id = OperationId(htlc.payment_hash.to_byte_array());
346        let (incoming_output, amount, contract_id) = create_incoming_contract_output(
347            &self.module_api,
348            htlc.payment_hash,
349            htlc.outgoing_amount_msat,
350            &self.redeem_key,
351        )
352        .await?;
353
354        let client_output = ClientOutput::<LightningOutputV0> {
355            output: incoming_output,
356            amounts: Amounts::new_bitcoin(amount),
357        };
358        let client_output_sm = ClientOutputSM::<GatewayClientStateMachines> {
359            state_machines: Arc::new(move |out_point_range: OutPointRange| {
360                assert_eq!(out_point_range.count(), 1);
361                vec![
362                    GatewayClientStateMachines::Receive(IncomingStateMachine {
363                        common: IncomingSmCommon {
364                            operation_id,
365                            contract_id,
366                            payment_hash: htlc.payment_hash,
367                        },
368                        state: IncomingSmStates::FundingOffer(FundingOfferState {
369                            txid: out_point_range.txid(),
370                        }),
371                    }),
372                    GatewayClientStateMachines::Complete(GatewayCompleteStateMachine {
373                        common: GatewayCompleteCommon {
374                            operation_id,
375                            payment_hash: htlc.payment_hash,
376                            incoming_chan_id: htlc.incoming_chan_id,
377                            htlc_id: htlc.htlc_id,
378                        },
379                        state: GatewayCompleteStates::WaitForPreimage(WaitForPreimageState),
380                    }),
381                ]
382            }),
383        };
384        Ok((
385            operation_id,
386            amount,
387            client_output,
388            client_output_sm,
389            contract_id,
390        ))
391    }
392
393    async fn create_funding_incoming_contract_output_from_swap(
394        &self,
395        swap: SwapParameters,
396    ) -> Result<
397        (
398            OperationId,
399            ClientOutput<LightningOutputV0>,
400            ClientOutputSM<GatewayClientStateMachines>,
401        ),
402        IncomingSmError,
403    > {
404        let payment_hash = swap.payment_hash;
405        let operation_id = OperationId(payment_hash.to_byte_array());
406        let (incoming_output, amount, contract_id) = create_incoming_contract_output(
407            &self.module_api,
408            payment_hash,
409            swap.amount_msat,
410            &self.redeem_key,
411        )
412        .await?;
413
414        let client_output = ClientOutput::<LightningOutputV0> {
415            output: incoming_output,
416            amounts: Amounts::new_bitcoin(amount),
417        };
418        let client_output_sm = ClientOutputSM::<GatewayClientStateMachines> {
419            state_machines: Arc::new(move |out_point_range| {
420                assert_eq!(out_point_range.count(), 1);
421                vec![GatewayClientStateMachines::Receive(IncomingStateMachine {
422                    common: IncomingSmCommon {
423                        operation_id,
424                        contract_id,
425                        payment_hash,
426                    },
427                    state: IncomingSmStates::FundingOffer(FundingOfferState {
428                        txid: out_point_range.txid(),
429                    }),
430                })]
431            }),
432        };
433        Ok((operation_id, client_output, client_output_sm))
434    }
435
436    /// Registers the gateway with a federation and reports whether it
437    /// succeeded.
438    ///
439    /// Detailed errors remain in gateway logs; callers can retain the boolean
440    /// result without exposing federation internals.
441    pub async fn try_register_with_federation(
442        &self,
443        route_hints: Vec<RouteHint>,
444        time_to_live: Duration,
445        fees: RoutingFees,
446        lightning_context: LightningContext,
447        api: SafeUrl,
448        gateway_keypair: Keypair,
449    ) -> bool {
450        let registration_info = self.to_gateway_registration_info(
451            route_hints,
452            time_to_live,
453            fees,
454            lightning_context,
455            api,
456            gateway_keypair,
457        );
458        let gateway_id = registration_info.info.gateway_id;
459
460        let federation_id = self
461            .client_ctx
462            .get_config()
463            .await
464            .global
465            .calculate_federation_id();
466        match self.module_api.register_gateway(&registration_info).await {
467            Err(e) => {
468                warn!(
469                    e = %e.fmt_compact(),
470                    "Failed to register gateway {gateway_id} with federation {federation_id}"
471                );
472                false
473            }
474            _ => {
475                info!(
476                    "Successfully registered gateway {gateway_id} with federation {federation_id}"
477                );
478                true
479            }
480        }
481    }
482
483    /// Attempts to remove a gateway's registration from the federation. Since
484    /// removing gateway registrations is best effort, this does not return
485    /// an error and simply emits a warning when the registration cannot be
486    /// removed.
487    pub async fn remove_from_federation(&self, gateway_keypair: Keypair) {
488        // Removing gateway registrations is best effort, so just emit a warning if it
489        // fails
490        if let Err(e) = self.remove_from_federation_inner(gateway_keypair).await {
491            let gateway_id = gateway_keypair.public_key();
492            let federation_id = self
493                .client_ctx
494                .get_config()
495                .await
496                .global
497                .calculate_federation_id();
498            warn!("Failed to remove gateway {gateway_id} from federation {federation_id}: {e:?}");
499        }
500    }
501
502    /// Retrieves the signing challenge from each federation peer. Since each
503    /// peer maintains their own list of registered gateways, the gateway
504    /// needs to provide a signature that is signed by the private key of the
505    /// gateway id to remove the registration.
506    async fn remove_from_federation_inner(&self, gateway_keypair: Keypair) -> anyhow::Result<()> {
507        let gateway_id = gateway_keypair.public_key();
508        let challenges = self
509            .module_api
510            .get_remove_gateway_challenge(gateway_id)
511            .await;
512
513        let fed_public_key = self.cfg.threshold_pub_key;
514        let signatures = challenges
515            .into_iter()
516            .filter_map(|(peer_id, challenge)| {
517                let msg = create_gateway_remove_message(fed_public_key, peer_id, challenge?);
518                let signature = gateway_keypair.sign_schnorr(msg);
519                Some((peer_id, signature))
520            })
521            .collect::<BTreeMap<_, _>>();
522
523        let remove_gateway_request = RemoveGatewayRequest {
524            gateway_id,
525            signatures,
526        };
527
528        self.module_api.remove_gateway(remove_gateway_request).await;
529
530        Ok(())
531    }
532
533    /// Attempt to fulfill an HTLC by buying its preimage from the federation.
534    ///
535    /// LND can replay a still-pending HTLC after interceptor reconnect or
536    /// gatewayd restart. Since the operation id is deterministic from the
537    /// payment hash, the replay must not re-fetch the consumed federation offer
538    /// or submit the same funding transaction again.
539    ///
540    /// We only short-circuit if a `GatewayCompleteStateMachine` is or was
541    /// handling the exact same LND circuit. Direct swaps with the same payment
542    /// hash and different circuits fall through to the normal failure/cancel
543    /// path. Exact-circuit replays bypass fresh-funding expiry validation so an
544    /// already-funded operation can still settle after reconnect or restart.
545    ///
546    /// `current_block_height` resolves to the Lightning backend's absolute best
547    /// Bitcoin block height. It is awaited only after replay detection, before
548    /// any fresh funding.
549    pub async fn gateway_handle_intercepted_htlc(
550        &self,
551        htlc: Htlc,
552        current_block_height: impl Future<Output = anyhow::Result<u32>>,
553    ) -> anyhow::Result<OperationId> {
554        debug!("Handling intercepted HTLC {htlc:?}");
555
556        let operation_id = OperationId(htlc.payment_hash.to_byte_array());
557        let circuit_key = HtlcCircuitKey {
558            operation_id,
559            incoming_chan_id: htlc.incoming_chan_id,
560            htlc_id: htlc.htlc_id,
561        };
562
563        // Serialize same-circuit handling so the active-state replay scan and
564        // operation creation are atomic for LND stream-reconnect replays.
565        let _circuit_lock_guard = self
566            .intercepted_htlc_lock_pool
567            .async_lock(circuit_key)
568            .await;
569
570        // Check before the funding helper: the first handling consumes the
571        // federation offer. Match the full circuit key, not just `operation_id`.
572        let replay_of_active_circuit = self
573            .client_ctx
574            .get_own_operation_active_states(operation_id)
575            .await
576            .into_iter()
577            .any(|(state, _)| circuit_key.matches_state(&state));
578        if replay_of_active_circuit {
579            debug!(
580                ?operation_id,
581                incoming_chan_id = htlc.incoming_chan_id,
582                htlc_id = htlc.htlc_id,
583                "HTLC circuit already being handled by an active completion state machine, treating as in-flight (likely an LND stream-reconnect replay)"
584            );
585            return Ok(operation_id);
586        }
587        let replay_of_inactive_circuit = self
588            .client_ctx
589            .get_own_operation_inactive_states(operation_id)
590            .await
591            .into_iter()
592            .any(|(state, _)| circuit_key.matches_state(&state));
593        if replay_of_inactive_circuit {
594            debug!(
595                ?operation_id,
596                incoming_chan_id = htlc.incoming_chan_id,
597                htlc_id = htlc.htlc_id,
598                "HTLC circuit was already handled by a completion state machine, treating as idempotent replay"
599            );
600            return Ok(operation_id);
601        }
602
603        let current_block_height = current_block_height.await?;
604        htlc.ensure_safe_expiry(current_block_height)?;
605        let remaining_blocks = htlc.incoming_expiry.saturating_sub(current_block_height);
606        if remaining_blocks <= u32::from(LNV1_INCOMING_HTLC_ADVERTISED_EXPIRY_DELTA) {
607            // Tracks how much traffic still arrives via pre-upgrade invoices;
608            // enforcement can be raised to the advertised delta once this stops
609            // firing in the wild.
610            warn!(
611                payment_hash = %htlc.payment_hash,
612                remaining_blocks,
613                advertised_delta = LNV1_INCOMING_HTLC_ADVERTISED_EXPIRY_DELTA,
614                "Accepting LNv1 HTLC with less remaining expiry than newly created invoices advertise, likely paid to a pre-upgrade invoice"
615            );
616        }
617
618        let (op_id_from_funding, amount, client_output, client_output_sm, contract_id) = self
619            .create_funding_incoming_contract_output_from_htlc(htlc.clone())
620            .await?;
621        // Keep the direct derivation above in sync with the funding helper. Return
622        // an error instead of panicking so the caller can fail back the HTLC cleanly.
623        anyhow::ensure!(
624            op_id_from_funding == operation_id,
625            "operation id derivation must match: {op_id_from_funding:?} != {operation_id:?}"
626        );
627
628        let output = ClientOutput {
629            output: LightningOutput::V0(client_output.output),
630            amounts: Amounts::new_bitcoin(amount),
631        };
632
633        let tx = TransactionBuilder::new().with_outputs(self.client_ctx.make_client_outputs(
634            ClientOutputBundle::new(vec![output], vec![client_output_sm]),
635        ));
636        let operation_meta_gen = |_: OutPointRange| GatewayMeta::Receive;
637        self.client_ctx
638            .finalize_and_submit_transaction(operation_id, KIND.as_str(), operation_meta_gen, tx)
639            .await?;
640        debug!(?operation_id, "Submitted transaction for HTLC {htlc:?}");
641        let mut dbtx = self.client_ctx.module_db().begin_transaction().await;
642        self.client_ctx
643            .log_event(
644                &mut dbtx,
645                IncomingPaymentStarted {
646                    contract_id,
647                    payment_hash: htlc.payment_hash,
648                    invoice_amount: htlc.outgoing_amount_msat,
649                    contract_amount: amount,
650                    operation_id,
651                },
652            )
653            .await;
654        dbtx.commit_tx().await;
655        Ok(operation_id)
656    }
657
658    /// Attempt buying preimage from this federation in order to fulfill a pay
659    /// request in another federation served by this gateway. In direct swap
660    /// scenario, the gateway DOES NOT send payment over the lightning network
661    ///
662    /// The operation is keyed on the payment hash, so an existing one means the
663    /// incoming contract for this swap is already funded and only ever pays out
664    /// one preimage. A second call therefore has nothing to fund; it wants the
665    /// preimage the first call is buying, and gets it by being handed the
666    /// operation already in progress for
667    /// [`pay::GatewayPayWaitForSwapPreimage`] to follow. Reporting a failure
668    /// instead would cancel the outgoing contract that this swap is the other
669    /// half of, refunding the sender while the recipient still gets paid out of
670    /// the gateway's own funds.
671    pub async fn gateway_handle_direct_swap(
672        &self,
673        swap_params: SwapParameters,
674    ) -> anyhow::Result<OperationId> {
675        debug!("Handling direct swap {swap_params:?}");
676
677        let payment_hash = swap_params.payment_hash;
678        let operation_id = OperationId(payment_hash.to_byte_array());
679
680        // Check before the funding helper: funding the incoming contract consumes
681        // the federation's offer, and the await-offer endpoint waits for one to
682        // appear rather than reporting that it is gone, so a re-entrant call gets
683        // no further than that helper's timeout.
684        if self.client_ctx.operation_exists(operation_id).await {
685            debug!(
686                operation_id = %operation_id.fmt_short(),
687                %payment_hash,
688                "Direct swap already in progress, returning the operation already funding it"
689            );
690
691            return Ok(operation_id);
692        }
693
694        let (op_id_from_funding, client_output, client_output_sm) = self
695            .create_funding_incoming_contract_output_from_swap(swap_params.clone())
696            .await?;
697        // Keep the direct derivation above in sync with the funding helper.
698        anyhow::ensure!(
699            op_id_from_funding == operation_id,
700            "operation id derivation must match: {op_id_from_funding:?} != {operation_id:?}"
701        );
702
703        self.client_ctx
704            .module_db()
705            .autocommit(
706                |dbtx, _| {
707                    let client_output = client_output.clone();
708                    let client_output_sm = client_output_sm.clone();
709                    Box::pin(async move {
710                        // The check above raced anyone who got here first; this one
711                        // shares a transaction with the write that would settle the
712                        // race, so exactly one of us funds the contract and the
713                        // other joins that operation.
714                        if self
715                            .client_ctx
716                            .get_operation_dbtx(dbtx, operation_id)
717                            .await
718                            .is_some()
719                        {
720                            debug!(
721                                operation_id = %operation_id.fmt_short(),
722                                %payment_hash,
723                                "Concurrent direct swap won the race, returning the operation already funding it"
724                            );
725
726                            return Ok(operation_id);
727                        }
728
729                        let output = ClientOutput {
730                            output: LightningOutput::V0(client_output.output),
731                            amounts: client_output.amounts,
732                        };
733                        let tx = TransactionBuilder::new().with_outputs(
734                            self.client_ctx.make_client_outputs(ClientOutputBundle::new(
735                                vec![output],
736                                vec![client_output_sm],
737                            )),
738                        );
739
740                        self.client_ctx
741                            .finalize_and_submit_transaction_dbtx(
742                                dbtx,
743                                operation_id,
744                                KIND.as_str(),
745                                |_: OutPointRange| GatewayMeta::Receive,
746                                tx,
747                            )
748                            .await?;
749
750                        debug!(
751                            ?operation_id,
752                            %payment_hash,
753                            "Submitted funding transaction for direct swap"
754                        );
755
756                        Ok(operation_id)
757                    })
758                },
759                Some(100),
760            )
761            .await
762            .map_err(|e| match e {
763                AutocommitError::ClosureError { error, .. } => error,
764                AutocommitError::CommitFailed { last_error, .. } => {
765                    anyhow::anyhow!("Commit to DB failed: {last_error}")
766                }
767            })
768    }
769
770    /// Subscribe to updates when the gateway is handling an intercepted HTLC,
771    /// or direct swap between federations
772    pub async fn gateway_subscribe_ln_receive(
773        &self,
774        operation_id: OperationId,
775    ) -> anyhow::Result<UpdateStreamOrOutcome<GatewayExtReceiveStates>> {
776        let operation = self.client_ctx.get_operation(operation_id).await?;
777        let mut stream = self.notifier.subscribe(operation_id).await;
778        let client_ctx = self.client_ctx.clone();
779
780        Ok(self.client_ctx.outcome_or_updates(&operation, operation_id, |state| match state {
781                GatewayExtReceiveStates::Funding => false,
782                GatewayExtReceiveStates::Preimage(_)
783                | GatewayExtReceiveStates::RefundSuccess { .. }
784                | GatewayExtReceiveStates::RefundError { .. }
785                | GatewayExtReceiveStates::FundingFailed { .. } => true,
786            }, move || {
787            stream! {
788
789                yield GatewayExtReceiveStates::Funding;
790
791                let state = loop {
792                    debug!("Getting next ln receive state for {}", operation_id.fmt_short());
793                    if let Some(GatewayClientStateMachines::Receive(state)) = stream.next().await {
794                        match state.state {
795                            IncomingSmStates::Preimage(preimage) =>{
796                                debug!(?operation_id, "Received preimage");
797                                break GatewayExtReceiveStates::Preimage(preimage)
798                            },
799                            IncomingSmStates::RefundSubmitted { out_points, error } => {
800                                debug!(?operation_id, "Refund submitted for {out_points:?} {error}");
801                                match client_ctx.await_primary_module_outputs(operation_id, out_points.clone()).await {
802                                    Ok(()) => {
803                                        debug!(?operation_id, "Refund success");
804                                        break GatewayExtReceiveStates::RefundSuccess { out_points, error }
805                                    },
806                                    Err(e) => {
807                                        warn!(?operation_id, "Got failure {e:?} while awaiting for refund outputs {out_points:?}");
808                                        break GatewayExtReceiveStates::RefundError{ error_message: e.to_string(), error }
809                                    },
810                                }
811                            },
812                            IncomingSmStates::FundingFailed { error } => {
813                                warn!(?operation_id, "Funding failed: {error:?}");
814                                break GatewayExtReceiveStates::FundingFailed{ error }
815                            },
816                            other => {
817                                debug!("Got state {other:?} while awaiting for output of {}", operation_id.fmt_short());
818                            }
819                        }
820                    }
821                };
822                yield state;
823            }
824        }))
825    }
826
827    /// For the given `OperationId`, this function will wait until the Complete
828    /// state machine has finished or failed.
829    pub async fn await_completion(&self, operation_id: OperationId) {
830        let mut stream = self.notifier.subscribe(operation_id).await;
831        loop {
832            match stream.next().await {
833                Some(GatewayClientStateMachines::Complete(state)) => match state.state {
834                    GatewayCompleteStates::HtlcFinished => {
835                        info!(%state, "LNv1 completion state machine finished");
836                        return;
837                    }
838                    GatewayCompleteStates::Failure => {
839                        error!(%state, "LNv1 completion state machine failed");
840                        return;
841                    }
842                    _ => {
843                        info!(%state, "Waiting for LNv1 completion state machine");
844                        continue;
845                    }
846                },
847                Some(GatewayClientStateMachines::Receive(state)) => {
848                    info!(%state, "Waiting for LNv1 completion state machine");
849                    continue;
850                }
851                Some(state) => {
852                    warn!(%state, "Operation is not an LNv1 completion state machine");
853                    return;
854                }
855                None => return,
856            }
857        }
858    }
859
860    /// Pay lightning invoice on behalf of federation user
861    pub async fn gateway_pay_bolt11_invoice(
862        &self,
863        pay_invoice_payload: PayInvoicePayload,
864    ) -> anyhow::Result<OperationId> {
865        let payload = pay_invoice_payload.clone();
866
867        // `payment_data` is caller-supplied on the unauthenticated `/pay_invoice`
868        // route, so an amountless BOLT11 reaches us here. The state machine
869        // rejects it in `validate_outgoing_account`, but that runs only after
870        // this function has already recorded the invoice amount, so the amount
871        // has to be resolved before any of that work starts.
872        let invoice_amount = pay_invoice_payload
873            .payment_data
874            .amount()
875            .ok_or(OutgoingContractError::InvoiceMissingAmount)?;
876
877        self.lightning_manager
878            .verify_pruned_invoice(pay_invoice_payload.payment_data)
879            .await?;
880
881        self.client_ctx.module_db()
882            .autocommit(
883                |dbtx, _| {
884                    Box::pin(async {
885                        let operation_id = OperationId(payload.contract_id.to_byte_array());
886
887                        // The operation id is the contract id, so an existing entry means we
888                        // already accepted a request to pay this very contract. The state
889                        // machine's own dedupe key covers the whole payload, so a caller who
890                        // varies any field of it -- `preimage_auth`, say -- slips past that
891                        // and would have us buy the preimage a second time, spending the
892                        // gateway's funds twice against a contract that only pays out once.
893                        // Hand back the operation already under way instead.
894                        if let Some(entry) =
895                            self.client_ctx.get_operation_dbtx(dbtx, operation_id).await
896                        {
897                            // This operation id yields the preimage, so only the
898                            // caller that started the payment may join it. The
899                            // state machine's own check comes too late for a
900                            // request that never reaches one, and it is keyed on
901                            // `payment_data`, which the caller supplies
902                            // independently of `contract_id` -- so it would answer
903                            // for the wrong payment here.
904                            if !matches!(
905                                entry.try_meta::<GatewayMeta>(),
906                                Ok(GatewayMeta::Pay { preimage_auth })
907                                    if preimage_auth == payload.preimage_auth
908                            ) {
909                                anyhow::bail!(
910                                    "Not authorized to receive the preimage for contract {}",
911                                    payload.contract_id
912                                );
913                            }
914
915                            debug!(
916                                operation_id = %operation_id.fmt_short(),
917                                contract_id = %payload.contract_id,
918                                "Duplicate request to pay an outgoing contract, returning the operation already in progress"
919                            );
920
921                            return Ok(operation_id);
922                        }
923
924                        self.client_ctx.log_event(dbtx, OutgoingPaymentStarted {
925                            contract_id: payload.contract_id,
926                            invoice_amount,
927                            operation_id,
928                        }).await;
929
930                        let state_machines =
931                            vec![GatewayClientStateMachines::Pay(GatewayPayStateMachine {
932                                common: GatewayPayCommon { operation_id },
933                                state: GatewayPayStates::PayInvoice(GatewayPayInvoice {
934                                    pay_invoice_payload: payload.clone(),
935                                }),
936                            })];
937
938                        let dyn_states = state_machines
939                            .into_iter()
940                            .map(|s| self.client_ctx.make_dyn(s))
941                            .collect();
942
943                            match self.client_ctx.add_state_machines_dbtx(dbtx, dyn_states).await {
944                                Ok(()) => {
945                                    self.client_ctx
946                                        .add_operation_log_entry_dbtx(
947                                            dbtx,
948                                            operation_id,
949                                            KIND.as_str(),
950                                            GatewayMeta::Pay {
951                                                preimage_auth: payload.preimage_auth,
952                                            },
953                                        )
954                                        .await;
955                                }
956                                Err(AddStateMachinesError::StateAlreadyExists) => {
957                                    info!("State machine for operation {} already exists, will not add a new one", operation_id.fmt_short());
958                                }
959                                Err(other) => {
960                                    anyhow::bail!("Failed to add state machines: {other:?}")
961                                }
962                            }
963                            Ok(operation_id)
964                    })
965                },
966                Some(100),
967            )
968            .await
969            .map_err(|e| match e {
970                AutocommitError::ClosureError { error, .. } => error,
971                AutocommitError::CommitFailed { last_error, .. } => {
972                    anyhow::anyhow!("Commit to DB failed: {last_error}")
973                }
974            })
975    }
976
977    pub async fn gateway_subscribe_ln_pay(
978        &self,
979        operation_id: OperationId,
980    ) -> anyhow::Result<UpdateStreamOrOutcome<GatewayExtPayStates>> {
981        let mut stream = self.notifier.subscribe(operation_id).await;
982        let operation = self.client_ctx.get_operation(operation_id).await?;
983        let client_ctx = self.client_ctx.clone();
984
985        Ok(self.client_ctx.outcome_or_updates(&operation, operation_id, |state| match state {
986                GatewayExtPayStates::Created | GatewayExtPayStates::Preimage { .. } => false,
987                GatewayExtPayStates::Success { .. }
988                | GatewayExtPayStates::Canceled { .. }
989                | GatewayExtPayStates::Fail { .. }
990                | GatewayExtPayStates::OfferDoesNotExist { .. } => true,
991            }, move || {
992            stream! {
993                yield GatewayExtPayStates::Created;
994
995                loop {
996                    debug!("Getting next ln pay state for {}", operation_id.fmt_short());
997                    match stream.next().await { Some(GatewayClientStateMachines::Pay(state)) => {
998                        match state.state {
999                            GatewayPayStates::Preimage(out_points, preimage) => {
1000                                yield GatewayExtPayStates::Preimage{ preimage: preimage.clone() };
1001
1002                                match client_ctx.await_primary_module_outputs(operation_id, out_points.clone()).await {
1003                                    Ok(()) => {
1004                                        debug!(?operation_id, "Success");
1005                                        yield GatewayExtPayStates::Success{ preimage: preimage.clone(), out_points };
1006                                        return;
1007
1008                                    }
1009                                    Err(e) => {
1010                                        warn!(?operation_id, "Got failure {e:?} while awaiting for outputs {out_points:?}");
1011                                        // TODO: yield something here?
1012                                    }
1013                                }
1014                            }
1015                            GatewayPayStates::Canceled { txid, contract_id, error } => {
1016                                debug!(?operation_id, "Trying to cancel contract {contract_id:?} due to {error:?}");
1017                                match client_ctx.transaction_updates(operation_id).await.await_tx_accepted(txid).await {
1018                                    Ok(()) => {
1019                                        debug!(?operation_id, "Canceled contract {contract_id:?} due to {error:?}");
1020                                        yield GatewayExtPayStates::Canceled{ error };
1021                                        return;
1022                                    }
1023                                    Err(e) => {
1024                                        warn!(?operation_id, "Got failure {e:?} while awaiting for transaction {txid} to be accepted for");
1025                                        yield GatewayExtPayStates::Fail { error, error_message: format!("Refund transaction {txid} was not accepted by the federation. OperationId: {} Error: {e:?}", operation_id.fmt_short()) };
1026                                    }
1027                                }
1028                            }
1029                            GatewayPayStates::OfferDoesNotExist(contract_id) => {
1030                                warn!("Yielding OfferDoesNotExist state for {} and contract {contract_id}", operation_id.fmt_short());
1031                                yield GatewayExtPayStates::OfferDoesNotExist { contract_id };
1032                            }
1033                            GatewayPayStates::Failed{ error, error_message } => {
1034                                warn!("Yielding Fail state for {} due to {error:?} {error_message:?}", operation_id.fmt_short());
1035                                yield GatewayExtPayStates::Fail{ error, error_message };
1036                            },
1037                            GatewayPayStates::PayInvoice(_) => {
1038                                debug!("Got initial state PayInvoice while awaiting for output of {}", operation_id.fmt_short());
1039                            }
1040                            other => {
1041                                info!("Got state {other:?} while awaiting for output of {}", operation_id.fmt_short());
1042                            }
1043                        }
1044                    } _ => {
1045                        warn!("Got None while getting next ln pay state for {}", operation_id.fmt_short());
1046                    }}
1047                }
1048            }
1049        }))
1050    }
1051}
1052
1053#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
1054struct HtlcCircuitKey {
1055    operation_id: OperationId,
1056    incoming_chan_id: u64,
1057    htlc_id: u64,
1058}
1059
1060impl HtlcCircuitKey {
1061    fn matches_state(self, state: &GatewayClientStateMachines) -> bool {
1062        matches!(
1063            state,
1064            GatewayClientStateMachines::Complete(sm)
1065                if sm.common.operation_id == self.operation_id
1066                    && sm.common.incoming_chan_id == self.incoming_chan_id
1067                    && sm.common.htlc_id == self.htlc_id
1068        )
1069    }
1070}
1071
1072#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
1073pub enum GatewayClientStateMachines {
1074    Pay(GatewayPayStateMachine),
1075    Receive(IncomingStateMachine),
1076    Complete(GatewayCompleteStateMachine),
1077}
1078
1079impl fmt::Display for GatewayClientStateMachines {
1080    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1081        match self {
1082            GatewayClientStateMachines::Pay(pay) => {
1083                write!(f, "{pay}")
1084            }
1085            GatewayClientStateMachines::Receive(receive) => {
1086                write!(f, "{receive}")
1087            }
1088            GatewayClientStateMachines::Complete(complete) => {
1089                write!(f, "{complete}")
1090            }
1091        }
1092    }
1093}
1094
1095impl IntoDynInstance for GatewayClientStateMachines {
1096    type DynType = DynState;
1097
1098    fn into_dyn(self, instance_id: ModuleInstanceId) -> Self::DynType {
1099        DynState::from_typed(instance_id, self)
1100    }
1101}
1102
1103impl State for GatewayClientStateMachines {
1104    type ModuleContext = GatewayClientContext;
1105
1106    fn transitions(
1107        &self,
1108        context: &Self::ModuleContext,
1109        global_context: &DynGlobalClientContext,
1110    ) -> Vec<StateTransition<Self>> {
1111        match self {
1112            GatewayClientStateMachines::Pay(pay_state) => {
1113                sm_enum_variant_translation!(
1114                    pay_state.transitions(context, global_context),
1115                    GatewayClientStateMachines::Pay
1116                )
1117            }
1118            GatewayClientStateMachines::Receive(receive_state) => {
1119                sm_enum_variant_translation!(
1120                    receive_state.transitions(&context.into(), global_context),
1121                    GatewayClientStateMachines::Receive
1122                )
1123            }
1124            GatewayClientStateMachines::Complete(complete_state) => {
1125                sm_enum_variant_translation!(
1126                    complete_state.transitions(context, global_context),
1127                    GatewayClientStateMachines::Complete
1128                )
1129            }
1130        }
1131    }
1132
1133    fn operation_id(&self) -> fedimint_core::core::OperationId {
1134        match self {
1135            GatewayClientStateMachines::Pay(pay_state) => pay_state.operation_id(),
1136            GatewayClientStateMachines::Receive(receive_state) => receive_state.operation_id(),
1137            GatewayClientStateMachines::Complete(complete_state) => complete_state.operation_id(),
1138        }
1139    }
1140}
1141
1142#[derive(Debug, Clone, Eq, PartialEq)]
1143pub struct Htlc {
1144    /// The HTLC payment hash.
1145    pub payment_hash: sha256::Hash,
1146    /// The incoming HTLC amount in millisatoshi.
1147    pub incoming_amount_msat: Amount,
1148    /// The outgoing HTLC amount in millisatoshi
1149    pub outgoing_amount_msat: Amount,
1150    /// Absolute Bitcoin block height at which the incoming HTLC expires.
1151    ///
1152    /// This uses the same coordinate system as the Lightning backend's current
1153    /// best block height.
1154    pub incoming_expiry: u32,
1155    /// The short channel id of the HTLC.
1156    pub short_channel_id: Option<u64>,
1157    /// The id of the incoming channel
1158    pub incoming_chan_id: u64,
1159    /// The index of the incoming htlc in the incoming channel
1160    pub htlc_id: u64,
1161}
1162
1163/// An intercepted LNv1 HTLC does not leave enough time for safe settlement.
1164#[derive(Debug, Error, Clone, Eq, PartialEq)]
1165#[error(
1166    "incoming HTLC expiry is unsafe: expiry {incoming_expiry}, current height {current_block_height}, required remaining blocks greater than {expiry_safety_margin}"
1167)]
1168pub struct UnsafeHtlcExpiry {
1169    /// Absolute Bitcoin block height at which the HTLC expires.
1170    pub incoming_expiry: u32,
1171    /// Lightning backend's current best Bitcoin block height.
1172    pub current_block_height: u32,
1173    /// Reserved settlement and on-chain safety margin in blocks.
1174    pub expiry_safety_margin: u32,
1175}
1176
1177impl Htlc {
1178    /// Rejects an HTLC unless its remaining lifetime exceeds the LNv1 safety
1179    /// margin.
1180    pub fn ensure_safe_expiry(&self, current_block_height: u32) -> Result<(), UnsafeHtlcExpiry> {
1181        let remaining_blocks = self.incoming_expiry.saturating_sub(current_block_height);
1182        if LNV1_HTLC_EXPIRY_SAFETY_MARGIN < remaining_blocks {
1183            return Ok(());
1184        }
1185
1186        Err(UnsafeHtlcExpiry {
1187            incoming_expiry: self.incoming_expiry,
1188            current_block_height,
1189            expiry_safety_margin: LNV1_HTLC_EXPIRY_SAFETY_MARGIN,
1190        })
1191    }
1192}
1193
1194impl TryFrom<InterceptPaymentRequest> for Htlc {
1195    type Error = anyhow::Error;
1196
1197    fn try_from(s: InterceptPaymentRequest) -> Result<Self, Self::Error> {
1198        Ok(Self {
1199            payment_hash: s.payment_hash,
1200            incoming_amount_msat: Amount::from_msats(s.amount_msat),
1201            outgoing_amount_msat: Amount::from_msats(s.amount_msat),
1202            incoming_expiry: s.expiry,
1203            short_channel_id: s.short_channel_id,
1204            incoming_chan_id: s.incoming_chan_id,
1205            htlc_id: s.htlc_id,
1206        })
1207    }
1208}
1209
1210#[derive(Debug, Clone)]
1211pub struct SwapParameters {
1212    pub payment_hash: sha256::Hash,
1213    pub amount_msat: Amount,
1214}
1215
1216impl TryFrom<PaymentData> for SwapParameters {
1217    type Error = anyhow::Error;
1218
1219    fn try_from(s: PaymentData) -> Result<Self, Self::Error> {
1220        let payment_hash = s.payment_hash();
1221        let amount_msat = s
1222            .amount()
1223            .ok_or_else(|| anyhow::anyhow!("Amountless invoice cannot be used in direct swap"))?;
1224        Ok(Self {
1225            payment_hash,
1226            amount_msat,
1227        })
1228    }
1229}
1230
1231/// An interface between module implementation and the general `Gateway`
1232///
1233/// To abstract away and decouple the core gateway from the modules, the
1234/// interface between them is expressed as a trait. The gateway handles
1235/// operations that require Lightning node access or database access.
1236#[async_trait]
1237pub trait IGatewayClientV1: Debug + Send + Sync {
1238    /// Verifies that the supplied `preimage_auth` is the same as the
1239    /// `preimage_auth` that initiated the payment.
1240    ///
1241    /// If it is not, then this will return an error because this client is not
1242    /// authorized to receive the preimage.
1243    async fn verify_preimage_authentication(
1244        &self,
1245        payment_hash: sha256::Hash,
1246        preimage_auth: sha256::Hash,
1247        contract: OutgoingContractAccount,
1248    ) -> Result<(), OutgoingPaymentError>;
1249
1250    /// Verify that the lightning node supports private payments if a pruned
1251    /// invoice is supplied.
1252    async fn verify_pruned_invoice(&self, payment_data: PaymentData) -> anyhow::Result<()>;
1253
1254    /// Retrieves the federation's routing fees from the federation's config.
1255    async fn get_routing_fees(&self, federation_id: FederationId) -> Option<RoutingFees>;
1256
1257    /// Retrieve a client given a federation ID, used for swapping ecash between
1258    /// federations.
1259    async fn get_client(&self, federation_id: &FederationId) -> Option<Spanned<ClientHandleArc>>;
1260
1261    // Retrieve a client given an invoice.
1262    //
1263    // Checks if the invoice route hint last hop has source node id matching this
1264    // gateways node pubkey and if the short channel id matches one assigned by
1265    // this gateway to a connected federation. In this case, the gateway can
1266    // avoid paying the invoice over the lightning network and instead perform a
1267    // direct swap between the two federations.
1268    async fn get_client_for_invoice(
1269        &self,
1270        payment_data: PaymentData,
1271    ) -> Option<Spanned<ClientHandleArc>>;
1272
1273    /// Pay a Lightning invoice using the gateway's lightning node.
1274    async fn pay(
1275        &self,
1276        payment_data: PaymentData,
1277        max_delay: u64,
1278        max_fee: Amount,
1279    ) -> Result<PayInvoiceResponse, LightningRpcError>;
1280
1281    /// Use the gateway's lightning node to send a complete HTLC response.
1282    async fn complete_htlc(
1283        &self,
1284        htlc_response: InterceptPaymentResponse,
1285    ) -> Result<(), LightningRpcError>;
1286
1287    /// Check if the gateway satisfy the LNv1 payment by funding an LNv2
1288    /// `IncomingContract`
1289    async fn is_lnv2_direct_swap(
1290        &self,
1291        payment_hash: sha256::Hash,
1292        amount: Amount,
1293    ) -> anyhow::Result<
1294        Option<(
1295            fedimint_lnv2_common::contracts::IncomingContract,
1296            ClientHandleArc,
1297        )>,
1298    >;
1299}