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
653        // The receive policy admits a fresh payment only here, below the replay
654        // checks above: a circuit this gateway already funded must still be
655        // settled, or the gateway is out of pocket for a contract it paid for.
656        // The guard is held across the funding write, so a payment cannot slip
657        // in after disabling has returned.
658        let federation_id = self
659            .client_ctx
660            .get_config()
661            .await
662            .global
663            .calculate_federation_id();
664        let _policy_guard = self
665            .lightning_manager
666            .begin_fresh_receive(federation_id)
667            .await
668            .ok_or(HandleInterceptedHtlcError::ReceiveDisabled { federation_id })?;
669
670        self.client_ctx
671            .finalize_and_submit_transaction(operation_id, KIND.as_str(), operation_meta_gen, tx)
672            .await?;
673        debug!(?operation_id, "Submitted transaction for HTLC {htlc:?}");
674        let mut dbtx = self.client_ctx.module_db().begin_transaction().await;
675        self.client_ctx
676            .log_event(
677                &mut dbtx,
678                IncomingPaymentStarted {
679                    contract_id,
680                    payment_hash: htlc.payment_hash,
681                    invoice_amount: htlc.incoming_amount_msat,
682                    contract_amount: amount,
683                    operation_id,
684                },
685            )
686            .await;
687        dbtx.commit_tx().await;
688        Ok(operation_id)
689    }
690
691    /// Attempt buying preimage from this federation in order to fulfill a pay
692    /// request in another federation served by this gateway. In direct swap
693    /// scenario, the gateway DOES NOT send payment over the lightning network
694    ///
695    /// The operation is keyed on the payment hash, so an existing one means the
696    /// incoming contract for this swap is already funded and only ever pays out
697    /// one preimage. A second call therefore has nothing to fund; it wants the
698    /// preimage the first call is buying, and gets it by being handed the
699    /// operation already in progress for
700    /// [`pay::GatewayPayWaitForSwapPreimage`] to follow. Reporting a failure
701    /// instead would cancel the outgoing contract that this swap is the other
702    /// half of, refunding the sender while the recipient still gets paid out of
703    /// the gateway's own funds.
704    ///
705    /// `allow_fresh_dispatch` is consulted only when no such operation exists:
706    /// callers pass `false` when a wall-clock gate such as invoice expiry
707    /// forbids starting a new swap, and receive `Ok(None)` to signal that
708    /// nothing was started.
709    ///
710    /// # Errors
711    ///
712    /// Fails with [`HandleDirectSwapError::IncomingContract`] if the
713    /// federation's offer cannot be funded,
714    /// [`HandleDirectSwapError::Transaction`] if the funding transaction
715    /// cannot be submitted, and [`HandleDirectSwapError::Database`] if it
716    /// cannot be recorded.
717    pub async fn gateway_handle_direct_swap(
718        &self,
719        swap_params: SwapParameters,
720        allow_fresh_dispatch: bool,
721    ) -> Result<Option<OperationId>, HandleDirectSwapError> {
722        debug!("Handling direct swap {swap_params:?}");
723
724        let payment_hash = swap_params.payment_hash;
725        let operation_id = OperationId(payment_hash.to_byte_array());
726
727        // Check before the funding helper: funding the incoming contract consumes
728        // the federation's offer, and the await-offer endpoint waits for one to
729        // appear rather than reporting that it is gone, so a re-entrant call gets
730        // no further than that helper's timeout.
731        if self.client_ctx.operation_exists(operation_id).await {
732            debug!(
733                operation_id = %operation_id.fmt_short(),
734                %payment_hash,
735                "Direct swap already in progress, returning the operation already funding it"
736            );
737
738            return Ok(Some(operation_id));
739        }
740
741        if !allow_fresh_dispatch {
742            return Ok(None);
743        }
744
745        let (op_id_from_funding, client_output, client_output_sm) = self
746            .create_funding_incoming_contract_output_from_swap(swap_params.clone())
747            .await?;
748        // Keep the direct derivation above in sync with the funding helper.
749        if op_id_from_funding != operation_id {
750            return Err(HandleDirectSwapError::OperationIdMismatch {
751                expected: operation_id,
752                derived: op_id_from_funding,
753            });
754        }
755
756        // A swap is how funds from another federation enter this one through
757        // the gateway, so it is admitted only while receives are on, and the
758        // guard holds that admission across the funding transaction below. A
759        // swap already funded resumes above regardless.
760        let federation_id = self
761            .client_ctx
762            .get_config()
763            .await
764            .global
765            .calculate_federation_id();
766        let _policy_guard = self
767            .lightning_manager
768            .begin_fresh_receive(federation_id)
769            .await
770            .ok_or(HandleDirectSwapError::ReceiveDisabled { federation_id })?;
771
772        self.client_ctx
773            .module_db()
774            .autocommit(
775                |dbtx, _| {
776                    let client_output = client_output.clone();
777                    let client_output_sm = client_output_sm.clone();
778                    Box::pin(async move {
779                        // The check above raced anyone who got here first; this one
780                        // shares a transaction with the write that would settle the
781                        // race, so exactly one of us funds the contract and the
782                        // other joins that operation.
783                        if self
784                            .client_ctx
785                            .get_operation_dbtx(dbtx, operation_id)
786                            .await
787                            .is_some()
788                        {
789                            debug!(
790                                operation_id = %operation_id.fmt_short(),
791                                %payment_hash,
792                                "Concurrent direct swap won the race, returning the operation already funding it"
793                            );
794
795                            return Ok(Some(operation_id));
796                        }
797
798                        let output = ClientOutput {
799                            output: LightningOutput::V0(client_output.output),
800                            amounts: client_output.amounts,
801                        };
802                        let tx = TransactionBuilder::new().with_outputs(
803                            self.client_ctx.make_client_outputs(ClientOutputBundle::new(
804                                vec![output],
805                                vec![client_output_sm],
806                            )),
807                        );
808
809                        self.client_ctx
810                            .finalize_and_submit_transaction_dbtx(
811                                dbtx,
812                                operation_id,
813                                KIND.as_str(),
814                                |_: OutPointRange| GatewayMeta::Receive,
815                                tx,
816                            )
817                            .await?;
818
819                        debug!(
820                            ?operation_id,
821                            %payment_hash,
822                            "Submitted funding transaction for direct swap"
823                        );
824
825                        Ok::<_, HandleDirectSwapError>(Some(operation_id))
826                    })
827                },
828                Some(100),
829            )
830            .await
831            .map_err(HandleDirectSwapError::from)
832    }
833
834    /// Subscribe to updates when the gateway is handling an intercepted HTLC,
835    /// or direct swap between federations
836    ///
837    /// # Errors
838    ///
839    /// Fails with an [`OperationLookupError`] if no operation with this id
840    /// exists, or if another module started it.
841    pub async fn gateway_subscribe_ln_receive(
842        &self,
843        operation_id: OperationId,
844    ) -> Result<UpdateStreamOrOutcome<GatewayExtReceiveStates>, OperationLookupError> {
845        let operation = self.client_ctx.get_operation(operation_id).await?;
846        let mut stream = self.notifier.subscribe(operation_id).await;
847        let client_ctx = self.client_ctx.clone();
848
849        Ok(self.client_ctx.outcome_or_updates(&operation, operation_id, |state| match state {
850                GatewayExtReceiveStates::Funding => false,
851                GatewayExtReceiveStates::Preimage(_)
852                | GatewayExtReceiveStates::RefundSuccess { .. }
853                | GatewayExtReceiveStates::RefundError { .. }
854                | GatewayExtReceiveStates::FundingFailed { .. } => true,
855            }, move || {
856            stream! {
857
858                yield GatewayExtReceiveStates::Funding;
859
860                let state = loop {
861                    debug!("Getting next ln receive state for {}", operation_id.fmt_short());
862                    if let Some(GatewayClientStateMachines::Receive(state)) = stream.next().await {
863                        match state.state {
864                            IncomingSmStates::Preimage(preimage) =>{
865                                debug!(?operation_id, "Received preimage");
866                                break GatewayExtReceiveStates::Preimage(preimage)
867                            },
868                            IncomingSmStates::RefundSubmitted { out_points, error } => {
869                                debug!(?operation_id, "Refund submitted for {out_points:?} {error}");
870                                match client_ctx.await_primary_module_outputs(operation_id, out_points.clone()).await {
871                                    Ok(()) => {
872                                        debug!(?operation_id, "Refund success");
873                                        break GatewayExtReceiveStates::RefundSuccess { out_points, error }
874                                    },
875                                    Err(e) => {
876                                        warn!(?operation_id, "Got failure {e:?} while awaiting for refund outputs {out_points:?}");
877                                        break GatewayExtReceiveStates::RefundError{ error_message: e.fmt_compact().to_string(), error }
878                                    },
879                                }
880                            },
881                            IncomingSmStates::FundingFailed { error } => {
882                                warn!(?operation_id, "Funding failed: {error:?}");
883                                break GatewayExtReceiveStates::FundingFailed{ error }
884                            },
885                            other => {
886                                debug!("Got state {other:?} while awaiting for output of {}", operation_id.fmt_short());
887                            }
888                        }
889                    }
890                };
891                yield state;
892            }
893        }))
894    }
895
896    /// For the given `OperationId`, this function will wait until the Complete
897    /// state machine has finished or failed.
898    pub async fn await_completion(&self, operation_id: OperationId) {
899        let mut stream = self.notifier.subscribe(operation_id).await;
900        loop {
901            match stream.next().await {
902                Some(GatewayClientStateMachines::Complete(state)) => match state.state {
903                    GatewayCompleteStates::HtlcFinished => {
904                        info!(%state, "LNv1 completion state machine finished");
905                        return;
906                    }
907                    GatewayCompleteStates::Failure => {
908                        error!(%state, "LNv1 completion state machine failed");
909                        return;
910                    }
911                    _ => {
912                        info!(%state, "Waiting for LNv1 completion state machine");
913                        continue;
914                    }
915                },
916                Some(GatewayClientStateMachines::Receive(state)) => {
917                    info!(%state, "Waiting for LNv1 completion state machine");
918                    continue;
919                }
920                Some(state) => {
921                    warn!(%state, "Operation is not an LNv1 completion state machine");
922                    return;
923                }
924                None => return,
925            }
926        }
927    }
928
929    /// Pay lightning invoice on behalf of federation user
930    ///
931    /// A funded contract whose invoice hash belongs to another operation is
932    /// cancelled without attempting a payment. The hash claim is retained.
933    ///
934    /// # Errors
935    ///
936    /// Fails with [`GatewayPayInvoiceError::MissingInvoiceAmount`] if the
937    /// invoice has no amount, [`GatewayPayInvoiceError::PrunedInvoiceRejected`]
938    /// if the gateway cannot pay a pruned invoice,
939    /// [`GatewayPayInvoiceError::PaymentHashAlreadyClaimed`] if another
940    /// operation already claimed the invoice's payment hash and this request
941    /// has no matching funded contract to refund,
942    /// [`GatewayPayInvoiceError::ContractLookup`] if the contract cannot be
943    /// fetched for a refund,
944    /// [`GatewayPayInvoiceError::Unauthorized`] if a payment of this contract
945    /// is already under way and the request does not carry the
946    /// authentication it was started with,
947    /// [`GatewayPayInvoiceError::StateMachines`] if the payment cannot be
948    /// started, and [`GatewayPayInvoiceError::Database`] if it cannot be
949    /// recorded.
950    pub async fn gateway_pay_bolt11_invoice(
951        &self,
952        pay_invoice_payload: PayInvoicePayload,
953    ) -> Result<OperationId, GatewayPayInvoiceError> {
954        let payload = pay_invoice_payload.clone();
955
956        // `payment_data` is caller-supplied on the unauthenticated `/pay_invoice`
957        // route, so an amountless BOLT11 reaches us here. The state machine
958        // rejects it in `validate_outgoing_account`, but that runs only after
959        // this function has already recorded the invoice amount, so the amount
960        // has to be resolved before any of that work starts.
961        let invoice_amount = pay_invoice_payload
962            .payment_data
963            .amount()
964            .ok_or(GatewayPayInvoiceError::MissingInvoiceAmount)?;
965
966        self.lightning_manager
967            .verify_pruned_invoice(pay_invoice_payload.payment_data)
968            .await
969            .map_err(GatewayPayInvoiceError::PrunedInvoiceRejected)?;
970
971        // Only the first operation to claim a payment image may ever pay it out,
972        // across LNv1, LNv2 and all federations; see the LNv2 `send_payment`.
973        // The claim is taken before the operation is created, and a retry of the
974        // same contract finds its own claim.
975        let initial_state = if self
976            .lightning_manager
977            .claim_payment_image(
978                &fedimint_lnv2_common::contracts::PaymentImage::Hash(
979                    payload.payment_data.payment_hash(),
980                ),
981                OperationId(payload.contract_id.to_byte_array()),
982            )
983            .await
984        {
985            GatewayPayStates::PayInvoice(GatewayPayInvoice {
986                pay_invoice_payload: payload.clone(),
987            })
988        } else {
989            let account = self
990                .module_api
991                .fetch_contract(payload.contract_id)
992                .await
993                .map_err(GatewayPayInvoiceError::ContractLookup)?
994                .ok_or(GatewayPayInvoiceError::PaymentHashAlreadyClaimed)?;
995            let FundedContract::Outgoing(contract) = account.contract else {
996                return Err(GatewayPayInvoiceError::PaymentHashAlreadyClaimed);
997            };
998
999            if account.amount == Amount::ZERO
1000                || contract.gateway_key != self.redeem_key.public_key()
1001                || contract.hash != payload.payment_data.payment_hash()
1002            {
1003                return Err(GatewayPayInvoiceError::PaymentHashAlreadyClaimed);
1004            }
1005            GatewayPayStates::CancelContract(Box::new(GatewayPayCancelContract::for_claimed_hash(
1006                OutgoingContractAccount {
1007                    amount: account.amount,
1008                    contract,
1009                },
1010            )))
1011        };
1012
1013        self.client_ctx.module_db()
1014            .autocommit(
1015                |dbtx, _| {
1016                    Box::pin(async {
1017                        let operation_id = OperationId(payload.contract_id.to_byte_array());
1018
1019                        // The operation id is the contract id, so an existing entry means we
1020                        // already accepted a request to pay this very contract. The state
1021                        // machine's own dedupe key covers the whole payload, so a caller who
1022                        // varies any field of it -- `preimage_auth`, say -- slips past that
1023                        // and would have us buy the preimage a second time, spending the
1024                        // gateway's funds twice against a contract that only pays out once.
1025                        // Hand back the operation already under way instead.
1026                        if let Some(entry) =
1027                            self.client_ctx.get_operation_dbtx(dbtx, operation_id).await
1028                        {
1029                            // This operation id yields the preimage, so only the
1030                            // caller that started the payment may join it. The
1031                            // state machine's own check comes too late for a
1032                            // request that never reaches one, and it is keyed on
1033                            // `payment_data`, which the caller supplies
1034                            // independently of `contract_id` -- so it would answer
1035                            // for the wrong payment here.
1036                            if !matches!(
1037                                entry.try_meta::<GatewayMeta>(),
1038                                Ok(GatewayMeta::Pay { preimage_auth })
1039                                    if PreimageAuth::new(preimage_auth)
1040                                        .verifies(payload.preimage_auth)
1041                            ) {
1042                                return Err(GatewayPayInvoiceError::Unauthorized {
1043                                    contract_id: payload.contract_id,
1044                                });
1045                            }
1046
1047                            debug!(
1048                                operation_id = %operation_id.fmt_short(),
1049                                contract_id = %payload.contract_id,
1050                                "Duplicate request to pay an outgoing contract, returning the operation already in progress"
1051                            );
1052
1053                            return Ok(operation_id);
1054                        }
1055
1056                        self.client_ctx.log_event(dbtx, OutgoingPaymentStarted {
1057                            contract_id: payload.contract_id,
1058                            invoice_amount,
1059                            operation_id,
1060                        }).await;
1061
1062                        let state_machines =
1063                            vec![GatewayClientStateMachines::Pay(GatewayPayStateMachine {
1064                                common: GatewayPayCommon { operation_id },
1065                                state: initial_state.clone(),
1066                            })];
1067
1068                        let dyn_states = state_machines
1069                            .into_iter()
1070                            .map(|s| self.client_ctx.make_dyn(s))
1071                            .collect();
1072
1073                            match self.client_ctx.add_state_machines_dbtx(dbtx, dyn_states).await {
1074                                Ok(()) => {
1075                                    self.client_ctx
1076                                        .add_operation_log_entry_dbtx(
1077                                            dbtx,
1078                                            operation_id,
1079                                            KIND.as_str(),
1080                                            GatewayMeta::Pay {
1081                                                preimage_auth: payload.preimage_auth,
1082                                            },
1083                                        )
1084                                        .await;
1085                                }
1086                                Err(AddStateMachinesError::StateAlreadyExists) => {
1087                                    info!("State machine for operation {} already exists, will not add a new one", operation_id.fmt_short());
1088                                }
1089                                Err(other) => {
1090                                    return Err(GatewayPayInvoiceError::StateMachines(other));
1091                                }
1092                            }
1093                            Ok::<_, GatewayPayInvoiceError>(operation_id)
1094                    })
1095                },
1096                Some(100),
1097            )
1098            .await
1099            .map_err(GatewayPayInvoiceError::from)
1100    }
1101
1102    /// Subscribe to updates of a payment started with
1103    /// [`Self::gateway_pay_bolt11_invoice`].
1104    ///
1105    /// # Errors
1106    ///
1107    /// Fails with an [`OperationLookupError`] if no operation with this id
1108    /// exists, or if another module started it.
1109    pub async fn gateway_subscribe_ln_pay(
1110        &self,
1111        operation_id: OperationId,
1112    ) -> Result<UpdateStreamOrOutcome<GatewayExtPayStates>, OperationLookupError> {
1113        let mut stream = self.notifier.subscribe(operation_id).await;
1114        let operation = self.client_ctx.get_operation(operation_id).await?;
1115        let client_ctx = self.client_ctx.clone();
1116
1117        Ok(self.client_ctx.outcome_or_updates(&operation, operation_id, |state| match state {
1118                GatewayExtPayStates::Created | GatewayExtPayStates::Preimage { .. } => false,
1119                GatewayExtPayStates::Success { .. }
1120                | GatewayExtPayStates::Canceled { .. }
1121                | GatewayExtPayStates::Fail { .. }
1122                | GatewayExtPayStates::OfferDoesNotExist { .. } => true,
1123            }, move || {
1124            stream! {
1125                yield GatewayExtPayStates::Created;
1126
1127                loop {
1128                    debug!("Getting next ln pay state for {}", operation_id.fmt_short());
1129                    match stream.next().await { Some(GatewayClientStateMachines::Pay(state)) => {
1130                        match state.state {
1131                            GatewayPayStates::Preimage(out_points, preimage) => {
1132                                yield GatewayExtPayStates::Preimage{ preimage: preimage.clone() };
1133
1134                                match client_ctx.await_primary_module_outputs(operation_id, out_points.clone()).await {
1135                                    Ok(()) => {
1136                                        debug!(?operation_id, "Success");
1137                                        yield GatewayExtPayStates::Success{ preimage: preimage.clone(), out_points };
1138                                        return;
1139
1140                                    }
1141                                    Err(e) => {
1142                                        warn!(?operation_id, "Got failure {e:?} while awaiting for outputs {out_points:?}");
1143                                        // TODO: yield something here?
1144                                    }
1145                                }
1146                            }
1147                            GatewayPayStates::Canceled { txid, contract_id, error } => {
1148                                debug!(?operation_id, "Trying to cancel contract {contract_id:?} due to {error:?}");
1149                                match client_ctx.transaction_updates(operation_id).await.await_tx_accepted(txid).await {
1150                                    Ok(()) => {
1151                                        debug!(?operation_id, "Canceled contract {contract_id:?} due to {error:?}");
1152                                        yield GatewayExtPayStates::Canceled{ error };
1153                                        return;
1154                                    }
1155                                    Err(e) => {
1156                                        warn!(?operation_id, "Got failure {e:?} while awaiting for transaction {txid} to be accepted for");
1157                                        yield GatewayExtPayStates::Fail { error, error_message: format!("Refund transaction {txid} was not accepted by the federation. OperationId: {} Error: {e:?}", operation_id.fmt_short()) };
1158                                    }
1159                                }
1160                            }
1161                            GatewayPayStates::OfferDoesNotExist(contract_id) => {
1162                                warn!("Yielding OfferDoesNotExist state for {} and contract {contract_id}", operation_id.fmt_short());
1163                                yield GatewayExtPayStates::OfferDoesNotExist { contract_id };
1164                            }
1165                            GatewayPayStates::Failed{ error, error_message } => {
1166                                warn!("Yielding Fail state for {} due to {error:?} {error_message:?}", operation_id.fmt_short());
1167                                yield GatewayExtPayStates::Fail{ error, error_message };
1168                            },
1169                            GatewayPayStates::PayInvoice(_) => {
1170                                debug!("Got initial state PayInvoice while awaiting for output of {}", operation_id.fmt_short());
1171                            }
1172                            other => {
1173                                info!("Got state {other:?} while awaiting for output of {}", operation_id.fmt_short());
1174                            }
1175                        }
1176                    } _ => {
1177                        warn!("Got None while getting next ln pay state for {}", operation_id.fmt_short());
1178                    }}
1179                }
1180            }
1181        }))
1182    }
1183}
1184
1185#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
1186struct HtlcCircuitKey {
1187    operation_id: OperationId,
1188    incoming_chan_id: u64,
1189    htlc_id: u64,
1190}
1191
1192impl HtlcCircuitKey {
1193    fn matches_state(self, state: &GatewayClientStateMachines) -> bool {
1194        matches!(
1195            state,
1196            GatewayClientStateMachines::Complete(sm)
1197                if sm.common.operation_id == self.operation_id
1198                    && sm.common.incoming_chan_id == self.incoming_chan_id
1199                    && sm.common.htlc_id == self.htlc_id
1200        )
1201    }
1202}
1203
1204#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
1205pub enum GatewayClientStateMachines {
1206    Pay(GatewayPayStateMachine),
1207    Receive(IncomingStateMachine),
1208    Complete(GatewayCompleteStateMachine),
1209}
1210
1211impl fmt::Display for GatewayClientStateMachines {
1212    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1213        match self {
1214            GatewayClientStateMachines::Pay(pay) => {
1215                write!(f, "{pay}")
1216            }
1217            GatewayClientStateMachines::Receive(receive) => {
1218                write!(f, "{receive}")
1219            }
1220            GatewayClientStateMachines::Complete(complete) => {
1221                write!(f, "{complete}")
1222            }
1223        }
1224    }
1225}
1226
1227impl IntoDynInstance for GatewayClientStateMachines {
1228    type DynType = DynState;
1229
1230    fn into_dyn(self, instance_id: ModuleInstanceId) -> Self::DynType {
1231        DynState::from_typed(instance_id, self)
1232    }
1233}
1234
1235impl State for GatewayClientStateMachines {
1236    type ModuleContext = GatewayClientContext;
1237
1238    fn transitions(
1239        &self,
1240        context: &Self::ModuleContext,
1241        global_context: &DynGlobalClientContext,
1242    ) -> Vec<StateTransition<Self>> {
1243        match self {
1244            GatewayClientStateMachines::Pay(pay_state) => {
1245                sm_enum_variant_translation!(
1246                    pay_state.transitions(context, global_context),
1247                    GatewayClientStateMachines::Pay
1248                )
1249            }
1250            GatewayClientStateMachines::Receive(receive_state) => {
1251                sm_enum_variant_translation!(
1252                    receive_state.transitions(&context.into(), global_context),
1253                    GatewayClientStateMachines::Receive
1254                )
1255            }
1256            GatewayClientStateMachines::Complete(complete_state) => {
1257                sm_enum_variant_translation!(
1258                    complete_state.transitions(context, global_context),
1259                    GatewayClientStateMachines::Complete
1260                )
1261            }
1262        }
1263    }
1264
1265    fn operation_id(&self) -> fedimint_core::core::OperationId {
1266        match self {
1267            GatewayClientStateMachines::Pay(pay_state) => pay_state.operation_id(),
1268            GatewayClientStateMachines::Receive(receive_state) => receive_state.operation_id(),
1269            GatewayClientStateMachines::Complete(complete_state) => complete_state.operation_id(),
1270        }
1271    }
1272}
1273
1274#[derive(Debug, Clone, Eq, PartialEq)]
1275pub struct Htlc {
1276    /// The HTLC payment hash.
1277    pub payment_hash: sha256::Hash,
1278    /// The incoming HTLC amount in millisatoshi.
1279    pub incoming_amount_msat: Amount,
1280    /// The outgoing HTLC amount in millisatoshi
1281    pub outgoing_amount_msat: Amount,
1282    /// Absolute Bitcoin block height at which the incoming HTLC expires.
1283    ///
1284    /// This uses the same coordinate system as the Lightning backend's current
1285    /// best block height.
1286    pub incoming_expiry: u32,
1287    /// The short channel id of the HTLC.
1288    pub short_channel_id: Option<u64>,
1289    /// The id of the incoming channel
1290    pub incoming_chan_id: u64,
1291    /// The index of the incoming htlc in the incoming channel
1292    pub htlc_id: u64,
1293}
1294
1295/// An intercepted LNv1 HTLC does not leave enough time for safe settlement.
1296#[derive(Debug, Error, Clone, Eq, PartialEq)]
1297#[error(
1298    "incoming HTLC expiry is unsafe: expiry {incoming_expiry}, current height {current_block_height}, required remaining blocks greater than {expiry_safety_margin}"
1299)]
1300pub struct UnsafeHtlcExpiry {
1301    /// Absolute Bitcoin block height at which the HTLC expires.
1302    pub incoming_expiry: u32,
1303    /// Lightning backend's current best Bitcoin block height.
1304    pub current_block_height: u32,
1305    /// Reserved settlement and on-chain safety margin in blocks.
1306    pub expiry_safety_margin: u32,
1307}
1308
1309impl Htlc {
1310    /// Rejects an HTLC unless its remaining lifetime exceeds the LNv1 safety
1311    /// margin.
1312    pub fn ensure_safe_expiry(&self, current_block_height: u32) -> Result<(), UnsafeHtlcExpiry> {
1313        let remaining_blocks = self.incoming_expiry.saturating_sub(current_block_height);
1314        if LNV1_HTLC_EXPIRY_SAFETY_MARGIN < remaining_blocks {
1315            return Ok(());
1316        }
1317
1318        Err(UnsafeHtlcExpiry {
1319            incoming_expiry: self.incoming_expiry,
1320            current_block_height,
1321            expiry_safety_margin: LNV1_HTLC_EXPIRY_SAFETY_MARGIN,
1322        })
1323    }
1324}
1325
1326impl From<InterceptPaymentRequest> for Htlc {
1327    fn from(s: InterceptPaymentRequest) -> Self {
1328        Self {
1329            payment_hash: s.payment_hash,
1330            // Keep the two amounts distinct: `incoming_amount_msat` is the real
1331            // value locked in the HTLC, while `amount_msat` is the sender-written
1332            // onion forward amount. Collapsing them lets a sender forge the
1333            // amount the gateway funds against.
1334            incoming_amount_msat: Amount::from_msats(s.incoming_amount_msat),
1335            outgoing_amount_msat: Amount::from_msats(s.amount_msat),
1336            incoming_expiry: s.expiry,
1337            short_channel_id: s.short_channel_id,
1338            incoming_chan_id: s.incoming_chan_id,
1339            htlc_id: s.htlc_id,
1340        }
1341    }
1342}
1343
1344#[derive(Debug, Clone)]
1345pub struct SwapParameters {
1346    pub payment_hash: sha256::Hash,
1347    pub amount_msat: Amount,
1348}
1349
1350impl TryFrom<PaymentData> for SwapParameters {
1351    type Error = MissingInvoiceAmountError;
1352
1353    fn try_from(s: PaymentData) -> Result<Self, Self::Error> {
1354        let payment_hash = s.payment_hash();
1355        let amount_msat = s.amount().ok_or(MissingInvoiceAmountError)?;
1356        Ok(Self {
1357            payment_hash,
1358            amount_msat,
1359        })
1360    }
1361}
1362
1363/// An interface between module implementation and the general `Gateway`
1364///
1365/// To abstract away and decouple the core gateway from the modules, the
1366/// interface between them is expressed as a trait. The gateway handles
1367/// operations that require Lightning node access or database access.
1368#[async_trait]
1369pub trait IGatewayClientV1: Debug + Send + Sync {
1370    /// Verifies that the supplied `preimage_auth` is the same as the
1371    /// `preimage_auth` that initiated the payment.
1372    ///
1373    /// If it is not, then this will return an error because this client is not
1374    /// authorized to receive the preimage.
1375    async fn verify_preimage_authentication(
1376        &self,
1377        payment_hash: sha256::Hash,
1378        preimage_auth: sha256::Hash,
1379        contract: OutgoingContractAccount,
1380    ) -> Result<(), OutgoingPaymentError>;
1381
1382    /// Verify that the lightning node supports private payments if a pruned
1383    /// invoice is supplied.
1384    ///
1385    /// # Errors
1386    ///
1387    /// Fails with a [`GatewayClientV1Error`] if the invoice is pruned and the
1388    /// gateway cannot pay it.
1389    async fn verify_pruned_invoice(
1390        &self,
1391        payment_data: PaymentData,
1392    ) -> Result<(), GatewayClientV1Error>;
1393
1394    /// Retrieves the federation's routing fees from the federation's config.
1395    async fn get_routing_fees(&self, federation_id: FederationId) -> Option<RoutingFees>;
1396
1397    /// Admits a fresh incoming payment for the clients of `federation_id` when
1398    /// the operator's receive policy allows one, or answers `None` when
1399    /// receives are disabled.
1400    ///
1401    /// The returned guard must be held until the funding operation exists.
1402    /// That is what makes disabling atomic: a policy change waits for the
1403    /// guards outstanding when it runs, so a payment admitted here always
1404    /// funds, and none is admitted once the change has returned.
1405    async fn begin_fresh_receive(
1406        &self,
1407        federation_id: FederationId,
1408    ) -> Option<fedimint_gateway_common::PaymentPolicyGuard>;
1409
1410    /// Retrieve a client given a federation ID, used for swapping ecash between
1411    /// federations.
1412    async fn get_client(&self, federation_id: &FederationId) -> Option<Spanned<ClientHandleArc>>;
1413
1414    // Retrieve a client given an invoice.
1415    //
1416    // Checks if the invoice route hint last hop has source node id matching this
1417    // gateways node pubkey and if the short channel id matches one assigned by
1418    // this gateway to a connected federation. In this case, the gateway can
1419    // avoid paying the invoice over the lightning network and instead perform a
1420    // direct swap between the two federations.
1421    async fn get_client_for_invoice(
1422        &self,
1423        payment_data: PaymentData,
1424    ) -> Option<Spanned<ClientHandleArc>>;
1425
1426    /// Pay a Lightning invoice using the gateway's lightning node.
1427    async fn pay(
1428        &self,
1429        payment_data: PaymentData,
1430        max_delay: u64,
1431        max_fee: Amount,
1432    ) -> Result<PayInvoiceResponse, LightningRpcError>;
1433
1434    /// Returns whether the gateway's Lightning node has any record of an
1435    /// outbound payment for `payment_hash`, whatever its state.
1436    ///
1437    /// The pay state machine consults this when it resumes after a restart:
1438    /// a payment the node already knows was dispatched before the crash and
1439    /// must be resolved through [`IGatewayClientV1::pay`]'s idempotent resume
1440    /// path rather than cancelled by pre-dispatch checks. A wrong `false`
1441    /// cancels a contract whose payment may still settle, so implementations
1442    /// must absorb transient node failures and only answer once the node's
1443    /// payment store could actually be consulted.
1444    async fn outbound_payment_exists(&self, payment_hash: sha256::Hash) -> bool;
1445
1446    /// Uses the gateway's lightning node to complete (settle or cancel) a
1447    /// previously intercepted HTLC.
1448    ///
1449    /// By the time the gateway settles the upstream HTLC it has already funded
1450    /// the incoming contract, so a transient failure here must not strand it
1451    /// out of pocket. Implementations must absorb and retry every transient
1452    /// node or connectivity failure, returning `Err` only when Lightning has
1453    /// reached a permanent state that makes the requested outcome impossible.
1454    /// The future may block while retrying and must remain cancellation-safe.
1455    /// The completion state machine persists any returned error as a terminal
1456    /// failure.
1457    async fn complete_htlc(
1458        &self,
1459        htlc_response: InterceptPaymentResponse,
1460    ) -> Result<(), LightningRpcError>;
1461
1462    /// Check if the gateway satisfy the LNv1 payment by funding an LNv2
1463    /// `IncomingContract`
1464    ///
1465    /// # Errors
1466    ///
1467    /// Fails with a [`GatewayClientV1Error`] if the gateway holds no LNv2
1468    /// incoming contract it can use for this payment. The pay state machine
1469    /// treats a failure like `None` and pays the invoice some other way.
1470    async fn is_lnv2_direct_swap(
1471        &self,
1472        payment_hash: sha256::Hash,
1473        amount: Amount,
1474    ) -> Result<
1475        Option<(
1476            fedimint_lnv2_common::contracts::IncomingContract,
1477            ClientHandleArc,
1478        )>,
1479        GatewayClientV1Error,
1480    >;
1481
1482    /// Claims the payment image for `operation_id` in the claim table shared
1483    /// with LNv2, returning `false` if another operation already claimed it.
1484    async fn claim_payment_image(
1485        &self,
1486        payment_image: &fedimint_lnv2_common::contracts::PaymentImage,
1487        operation_id: OperationId,
1488    ) -> bool;
1489}