Skip to main content

fedimint_gw_client/
lib.rs

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