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