Skip to main content

fedimint_gateway_server/
lib.rs

1#![deny(clippy::pedantic)]
2#![allow(clippy::cast_possible_truncation)]
3#![allow(clippy::cast_possible_wrap)]
4#![allow(clippy::cast_sign_loss)]
5#![allow(clippy::default_trait_access)]
6#![allow(clippy::doc_markdown)]
7#![allow(clippy::missing_errors_doc)]
8#![allow(clippy::missing_panics_doc)]
9#![allow(clippy::module_name_repetitions)]
10#![allow(clippy::must_use_candidate)]
11#![allow(clippy::return_self_not_must_use)]
12#![allow(clippy::similar_names)]
13#![allow(clippy::too_many_lines)]
14#![allow(clippy::large_futures)]
15#![allow(clippy::struct_field_names)]
16
17pub mod client;
18pub mod config;
19pub mod envs;
20mod error;
21mod events;
22mod federation_manager;
23mod federation_status;
24mod iroh_server;
25mod metrics;
26mod rate_limit;
27mod registration_health;
28pub mod rpc_server;
29mod types;
30
31use std::collections::{BTreeMap, BTreeSet};
32use std::env;
33use std::fmt::Display;
34use std::net::SocketAddr;
35use std::str::FromStr;
36use std::sync::Arc;
37use std::time::{Duration, UNIX_EPOCH};
38
39use anyhow::{Context, anyhow};
40use async_trait::async_trait;
41use bitcoin::hashes::sha256;
42use bitcoin::{Address, Network, Txid, secp256k1};
43use clap::Parser;
44use client::GatewayClientBuilder;
45pub use config::GatewayParameters;
46use config::{DatabaseBackend, GatewayOpts};
47use envs::FM_GATEWAY_SKIP_WAIT_FOR_SYNC_ENV;
48use error::FederationNotConnected;
49use events::ALL_GATEWAY_EVENTS;
50use federation_manager::FederationManager;
51use fedimint_bip39::{Bip39RootSecretStrategy, Language, Mnemonic};
52use fedimint_bitcoind::bitcoincore::BitcoindClient;
53use fedimint_bitcoind::{EsploraClient, IBitcoindRpc};
54use fedimint_client::module_init::ClientModuleInitRegistry;
55use fedimint_client::secret::RootSecretStrategy;
56use fedimint_client::{Client, ClientHandleArc};
57use fedimint_core::base32::{self, FEDIMINT_PREFIX};
58use fedimint_core::config::FederationId;
59use fedimint_core::core::OperationId;
60use fedimint_core::db::{Committable, Database, DatabaseTransaction, apply_migrations};
61use fedimint_core::envs::is_env_var_set;
62use fedimint_core::invite_code::InviteCode;
63use fedimint_core::module::CommonModuleInit;
64use fedimint_core::module::registry::ModuleDecoderRegistry;
65use fedimint_core::rustls::install_crypto_provider;
66use fedimint_core::secp256k1::PublicKey;
67use fedimint_core::secp256k1::schnorr::Signature;
68use fedimint_core::task::{TaskGroup, TaskHandle, TaskShutdownToken, sleep, timeout};
69use fedimint_core::time::duration_since_epoch;
70use fedimint_core::util::backoff_util::fibonacci_max_one_hour;
71use fedimint_core::util::{FmtCompact, SafeUrl, Spanned, retry};
72use fedimint_core::{
73    Amount, BitcoinAmountOrAll, PeerId, TieredCounts, crit, fedimint_build_code_version_env,
74    get_network_for_address,
75};
76use fedimint_eventlog::{DBTransactionEventLogExt, EventLogId, StructuredPaymentEvents};
77use fedimint_gateway_common::{
78    BackupPayload, ChainSource, CloseChannelsWithPeerRequest, CloseChannelsWithPeerResponse,
79    ConnectFedPayload, ConnectPeerRequest, ConnectorType, CreateInvoiceForOperatorPayload,
80    CreateOfferPayload, CreateOfferResponse, DepositAddressPayload, DepositAddressRecheckPayload,
81    FederationBalanceInfo, FederationConfig, FederationInfo, GatewayBalances, GatewayFedConfig,
82    GatewayInfo, GetInvoiceRequest, GetInvoiceResponse, LeaveFedPayload, LightningInfo,
83    LightningMode, ListTransactionsPayload, ListTransactionsResponse, MnemonicResponse,
84    OpenChannelRequest, PayInvoiceForOperatorPayload, PayOfferPayload, PayOfferResponse,
85    PaymentLogPayload, PaymentLogResponse, PaymentPolicy, PaymentPolicyGuard, PaymentStats,
86    PaymentSummaryPayload, PaymentSummaryResponse, PeginFromOnchainPayload, ReceiveEcashPayload,
87    ReceiveEcashResponse, RegisteredProtocol, SendOnchainRequest, SetChannelFeesRequest,
88    SetFeesPayload, SetMnemonicPayload, SetPaymentPolicyPayload, SpendEcashPayload,
89    SpendEcashResponse, V1_API_ENDPOINT, WithdrawPayload, WithdrawPreviewPayload,
90    WithdrawPreviewResponse, WithdrawResponse, WithdrawToOnchainPayload,
91};
92use fedimint_gateway_server_db::{GatewayDbtxNcExt as _, get_gatewayd_database_migrations};
93pub use fedimint_gateway_ui::IAdminGateway;
94use fedimint_gw_client::events::compute_lnv1_stats;
95use fedimint_gw_client::pay::{OutgoingPaymentError, OutgoingPaymentErrorType};
96use fedimint_gw_client::{
97    GatewayClientModule, GatewayClientV1Error, GatewayExtPayStates, GatewayExtReceiveStates, Htlc,
98    IGatewayClientV1, SwapParameters,
99};
100use fedimint_gwv2_client::events::compute_lnv2_stats;
101use fedimint_gwv2_client::{
102    EXPIRATION_DELTA_MINIMUM_V2, FinalReceiveState, GatewayClientModuleV2, GatewayClientV2Error,
103    IGatewayClientV2,
104};
105use fedimint_lightning::ldk::MnemonicSource;
106use fedimint_lightning::lnd::GatewayLndClient;
107use fedimint_lightning::{
108    CreateInvoiceRequest, ILnRpcClient, InterceptPaymentRequest, InterceptPaymentResponse,
109    InvoiceDescription, LightningContext, LightningRpcError, LnRpcTracked, Lnv2HoldInvoiceFilter,
110    PayInvoiceResponse, PaymentAction, RouteHtlcStream, ldk,
111};
112use fedimint_ln_client::pay::PaymentData;
113use fedimint_ln_common::config::LightningClientConfig;
114use fedimint_ln_common::contracts::outgoing::OutgoingContractAccount;
115use fedimint_ln_common::contracts::{IdentifiableContract, Preimage};
116use fedimint_ln_common::{LightningCommonInit, PreimageAuth};
117use fedimint_lnurl::VerifyResponse;
118use fedimint_lnv2_common::Bolt11InvoiceDescription;
119use fedimint_lnv2_common::contracts::{IncomingContract, PaymentImage};
120use fedimint_lnv2_common::gateway_api::{
121    CreateBolt11InvoicePayload, MAX_INVOICE_EXPIRY_SECS, PaymentFee, RoutingInfo,
122    SendPaymentPayload,
123};
124use fedimint_logging::LOG_GATEWAY;
125use fedimint_mint_client::{MintClientInit, MintClientModule, OOBNotes, ReissueExternalNotesState};
126use fedimint_mintv2_client::{
127    MintClientInit as MintV2ClientInit, MintClientModule as MintV2ClientModule,
128};
129use fedimint_wallet_client::{PegOutFees, WalletClientInit, WalletClientModule, WithdrawState};
130use futures::stream::StreamExt;
131use lightning_invoice::{Bolt11Invoice, RoutingFees};
132use rand::rngs::OsRng;
133use tokio::sync::RwLock;
134use tracing::{debug, info, info_span, warn};
135
136use crate::envs::FM_GATEWAY_MNEMONIC_ENV;
137use crate::error::{AdminGatewayError, LNv1Error, LNv2Error, PublicGatewayError};
138use crate::events::get_events_for_duration;
139use crate::rate_limit::TokenBucketRateLimiter;
140use crate::registration_health::RegistrationHealthTracker;
141use crate::rpc_server::run_webserver;
142use crate::types::PrettyInterceptPaymentRequest;
143
144/// How long a gateway announcement stays valid
145const GW_ANNOUNCEMENT_TTL: Duration = Duration::from_mins(10);
146
147/// The default number of route hints that the legacy gateway provides for
148/// invoice creation.
149const DEFAULT_NUM_ROUTE_HINTS: u32 = 1;
150
151/// Default maximum burst of requests to the public invoice creation endpoint.
152const DEFAULT_INVOICE_RATE_LIMIT_BURST: u32 = 50;
153
154/// Default sustained number of requests per second to the public invoice
155/// creation endpoint.
156const DEFAULT_INVOICE_RATE_LIMIT_PER_SECOND: u32 = 5;
157
158/// Default Bitcoin network for testing purposes.
159pub const DEFAULT_NETWORK: Network = Network::Regtest;
160
161/// How long code that needs to talk to the lightning node backs off before
162/// re-checking whether the gateway has (re)connected to it.
163const LIGHTNING_CONTEXT_RETRY_INTERVAL: Duration = Duration::from_secs(5);
164
165/// How long an LNURL-verify request that asked to wait blocks before reporting
166/// the payment as not settled yet.
167///
168/// The payment being waited for may never arrive, so the wait needs an upper
169/// bound to stop callers from parking a request handler indefinitely. Reporting
170/// "not settled" on expiry is a regular LNURL-verify response, so clients
171/// simply poll again.
172const VERIFY_WAIT_TIMEOUT: Duration = Duration::from_secs(30);
173
174pub type Result<T> = std::result::Result<T, PublicGatewayError>;
175pub type AdminResult<T> = std::result::Result<T, AdminGatewayError>;
176
177/// Name of the gateway's database that is used for metadata and configuration
178/// storage.
179const DB_FILE: &str = "gatewayd.db";
180
181/// Name of the folder that the gateway uses to store its node database when
182/// running in LDK mode.
183const LDK_NODE_DB_FOLDER: &str = "ldk_node";
184
185/// ```mermaid
186/// graph LR
187/// classDef virtual fill:#fff,stroke-dasharray: 5 5
188///
189///    NotConfigured -- create or recover wallet --> Disconnected
190///    Disconnected -- establish lightning connection --> Connected
191///    Connected -- load federation clients --> Running
192///    Connected -- not synced to chain --> Syncing
193///    Syncing -- load federation clients --> Running
194///    Running -- disconnected from lightning node --> Disconnected
195///    Running -- shutdown initiated --> ShuttingDown
196/// ```
197#[derive(Clone, Debug)]
198pub enum GatewayState {
199    NotConfigured {
200        // Broadcast channel to alert gateway background threads that the mnemonic has been
201        // created/set, and how it was obtained.
202        mnemonic_sender: tokio::sync::broadcast::Sender<MnemonicSource>,
203    },
204    Disconnected,
205    Syncing,
206    Connected,
207    Running {
208        lightning_context: LightningContext,
209    },
210    ShuttingDown {
211        lightning_context: LightningContext,
212    },
213}
214
215impl Display for GatewayState {
216    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
217        match self {
218            GatewayState::NotConfigured { .. } => write!(f, "NotConfigured"),
219            GatewayState::Disconnected => write!(f, "Disconnected"),
220            GatewayState::Syncing => write!(f, "Syncing"),
221            GatewayState::Connected => write!(f, "Connected"),
222            GatewayState::Running { .. } => write!(f, "Running"),
223            GatewayState::ShuttingDown { .. } => write!(f, "ShuttingDown"),
224        }
225    }
226}
227
228/// Helper struct for storing the registration parameters for LNv1 for each
229/// network protocol.
230#[derive(Debug, Clone)]
231struct Registration {
232    /// The url to advertise in the registration that clients can use to connect
233    endpoint_url: SafeUrl,
234
235    /// Keypair that was used to register the gateway registration
236    keypair: secp256k1::Keypair,
237}
238
239impl Registration {
240    pub async fn new(db: &Database, endpoint_url: SafeUrl, protocol: RegisteredProtocol) -> Self {
241        let keypair = Gateway::load_or_create_gateway_keypair(db, protocol).await;
242        Self {
243            endpoint_url,
244            keypair,
245        }
246    }
247}
248
249#[bon::bon]
250impl Gateway {
251    /// Construct a [`Gateway`] using a fluent builder API.
252    ///
253    /// # Example
254    /// ```ignore
255    /// let gateway = Gateway::builder(lightning_mode, client_builder, gateway_db)
256    ///     .listen(addr)
257    ///     .api_addr(url)
258    ///     .bcrypt_password_hash(hash)
259    ///     .network(Network::Regtest)
260    ///     .gateway_state(state)
261    ///     .chain_source(chain_source)
262    ///     .build()
263    ///     .await?;
264    /// ```
265    #[builder(start_fn = builder, finish_fn = build)]
266    pub async fn new_with_builder(
267        #[builder(start_fn)] lightning_mode: LightningMode,
268        #[builder(start_fn)] client_builder: GatewayClientBuilder,
269        #[builder(start_fn)] gateway_db: Database,
270        bcrypt_password_hash: bcrypt::HashParts,
271        bcrypt_liquidity_manager_password_hash: Option<bcrypt::HashParts>,
272        gateway_state: GatewayState,
273        chain_source: ChainSource,
274        #[builder(default = ([127, 0, 0, 1], 80).into())] listen: SocketAddr,
275        api_addr: Option<SafeUrl>,
276        #[builder(default = DEFAULT_NETWORK)] network: Network,
277        #[builder(default = DEFAULT_NUM_ROUTE_HINTS)] num_route_hints: u32,
278        #[builder(default = PaymentFee::TRANSACTION_FEE_DEFAULT)] default_routing_fees: PaymentFee,
279        #[builder(default = PaymentFee::TRANSACTION_FEE_DEFAULT)]
280        default_transaction_fees: PaymentFee,
281        iroh_listen: Option<SocketAddr>,
282        iroh_dns: Option<SafeUrl>,
283        #[builder(default)] iroh_relays: Vec<SafeUrl>,
284        metrics_listen: Option<SocketAddr>,
285    ) -> anyhow::Result<Gateway> {
286        let versioned_api = api_addr.map(|addr| {
287            addr.join(V1_API_ENDPOINT)
288                .expect("Failed to version gateway API address")
289        });
290
291        let metrics_listen = metrics_listen.unwrap_or_else(|| {
292            SocketAddr::new(
293                std::net::IpAddr::V4(std::net::Ipv4Addr::LOCALHOST),
294                listen.port() + 1,
295            )
296        });
297
298        Gateway::new(
299            lightning_mode,
300            GatewayParameters {
301                listen,
302                versioned_api,
303                bcrypt_password_hash,
304                bcrypt_liquidity_manager_password_hash,
305                network,
306                num_route_hints,
307                default_routing_fees,
308                default_transaction_fees,
309                iroh_listen,
310                iroh_dns,
311                iroh_relays,
312                skip_setup: true,
313                metrics_listen,
314                invoice_rate_limit_burst: DEFAULT_INVOICE_RATE_LIMIT_BURST,
315                invoice_rate_limit_per_second: DEFAULT_INVOICE_RATE_LIMIT_PER_SECOND,
316            },
317            gateway_db,
318            client_builder,
319            gateway_state,
320            chain_source,
321            MnemonicSource::Existing,
322        )
323        .await
324    }
325}
326
327/// The action to take after handling a payment stream.
328enum ReceivePaymentStreamAction {
329    RetryAfterDelay,
330    NoRetry,
331}
332
333#[derive(Clone)]
334pub struct Gateway {
335    /// The gateway's federation manager.
336    federation_manager: Arc<RwLock<FederationManager>>,
337
338    /// The mode that specifies the lightning connection parameters
339    lightning_mode: LightningMode,
340
341    /// The current state of the Gateway.
342    state: Arc<RwLock<GatewayState>>,
343
344    /// Builder struct that allows the gateway to build a Fedimint client, which
345    /// handles the communication with a federation.
346    client_builder: GatewayClientBuilder,
347
348    /// Database for Gateway metadata.
349    gateway_db: Database,
350
351    /// The socket the gateway listens on.
352    listen: SocketAddr,
353
354    /// The socket the gateway's metrics server listens on.
355    metrics_listen: SocketAddr,
356
357    /// The task group for all tasks related to the gateway.
358    task_group: TaskGroup,
359
360    /// The bcrypt password hash used to authenticate the gateway.
361    bcrypt_password_hash: String,
362
363    /// The bcrypt password hash used to authenticate the gateway liquidity
364    /// manager.
365    bcrypt_liquidity_manager_password_hash: Option<String>,
366
367    /// The number of route hints to include in LNv1 invoices.
368    num_route_hints: u32,
369
370    /// The Bitcoin network that the Lightning network is configured to.
371    network: Network,
372
373    /// The source of the Bitcoin blockchain data
374    chain_source: ChainSource,
375
376    /// The default routing fees for new federations
377    default_routing_fees: PaymentFee,
378
379    /// The default transaction fees for new federations
380    default_transaction_fees: PaymentFee,
381
382    /// The secret key for the Iroh `Endpoint`
383    iroh_sk: iroh::SecretKey,
384
385    /// The socket that the gateway listens on for the Iroh `Endpoint`
386    iroh_listen: Option<SocketAddr>,
387
388    /// Optional DNS server used for discovery of the Iroh `Endpoint`
389    iroh_dns: Option<SafeUrl>,
390
391    /// List of additional relays that can be used to establish a connection to
392    /// the Iroh `Endpoint`
393    iroh_relays: Vec<SafeUrl>,
394
395    /// A map of the network protocols the gateway supports to the data needed
396    /// for registering with a federation.
397    registrations: BTreeMap<RegisteredProtocol, Registration>,
398
399    /// Detail-free retained results of LNv1 federation registration attempts.
400    registration_health: RegistrationHealthTracker,
401
402    /// Rate limiter for the public invoice creation endpoint.
403    invoice_rate_limiter: Arc<TokenBucketRateLimiter>,
404
405    /// Locks the payment hash of each HOLD invoice whose delivery is being
406    /// handled, so that a duplicate delivery is dropped; see
407    /// [`Self::handle_lightning_payment`].
408    lnv2_hold_invoice_lock_pool: Arc<lockable::LockPool<sha256::Hash>>,
409
410    /// Serializes payment policy changes against the funding of the payments
411    /// those policies admit.
412    payment_policy_lock: Arc<RwLock<()>>,
413
414    /// How the mnemonic was obtained at startup. When the gateway starts
415    /// unconfigured, the source of the mnemonic set later arrives through the
416    /// `mnemonic_sender` channel instead.
417    mnemonic_source: MnemonicSource,
418}
419
420impl std::fmt::Debug for Gateway {
421    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
422        f.debug_struct("Gateway")
423            .field("federation_manager", &self.federation_manager)
424            .field("state", &self.state)
425            .field("client_builder", &self.client_builder)
426            .field("gateway_db", &self.gateway_db)
427            .field("listen", &self.listen)
428            .field("registrations", &self.registrations)
429            .finish_non_exhaustive()
430    }
431}
432
433/// Internal helper for on-chain withdrawal calculations
434struct WithdrawDetails {
435    amount: Amount,
436    mint_fees: Option<Amount>,
437    peg_out_fees: PegOutFees,
438    flat_peg_out_fee: bool,
439}
440
441/// Waits for the ecash change of a successful withdrawal to be issued.
442///
443/// A withdrawal is reported successful once its transaction is accepted, but
444/// the primary module issues the change from funding it asynchronously. Until
445/// then the change is missing from the balance, so callers that read the
446/// balance right after a withdrawal would see it understated by the change.
447/// The withdrawal itself has already succeeded, so a failure here is only
448/// logged.
449async fn await_withdraw_change(
450    client: &ClientHandleArc,
451    operation_id: OperationId,
452    change: Vec<fedimint_core::OutPoint>,
453) {
454    if let Err(err) = client
455        .await_primary_bitcoin_module_outputs(operation_id, change)
456        .await
457    {
458        warn!(
459            target: LOG_GATEWAY,
460            err = %err.fmt_compact(),
461            ?operation_id,
462            "Failed to await withdrawal change outputs"
463        );
464    }
465}
466
467/// Executes a withdrawal using the walletv2 module
468async fn withdraw_v2(
469    client: &ClientHandleArc,
470    wallet_module: &fedimint_walletv2_client::WalletClientModule,
471    address: &Address,
472    amount: BitcoinAmountOrAll,
473    quoted_fee: Option<bitcoin::Amount>,
474) -> AdminResult<WithdrawResponse> {
475    let current_fee =
476        wallet_module
477            .send_fee()
478            .await
479            .map_err(|e| AdminGatewayError::WithdrawError {
480                failure_reason: e.fmt_compact().to_string(),
481            })?;
482
483    // The federation rejects any send whose fee is below its current send fee,
484    // so a quote that has gone stale would only produce a doomed transaction.
485    // Sending with the quoted fee otherwise keeps the operator paying exactly
486    // what they confirmed in the preview.
487    let fee = match quoted_fee {
488        Some(quoted_fee) if quoted_fee < current_fee => {
489            return Err(AdminGatewayError::WithdrawError {
490                failure_reason: format!(
491                    "The on-chain fee increased from {} to {} sats since the preview, please preview the withdrawal again",
492                    quoted_fee.to_sat(),
493                    current_fee.to_sat()
494                ),
495            });
496        }
497        Some(quoted_fee) => quoted_fee,
498        None => current_fee,
499    };
500
501    let withdraw_amount = match amount {
502        BitcoinAmountOrAll::All => {
503            let balance = client.get_balance_for_btc().await.map_err(|err| {
504                AdminGatewayError::Unexpected(anyhow!(
505                    "Balance not available: {}",
506                    err.fmt_compact()
507                ))
508            })?;
509
510            // The on-chain fee is only part of the cost: funding the wallet
511            // output also incurs the federation's per-note fees. `fee` is
512            // passed on to `send` below so both are computed against the same
513            // on-chain fee.
514            wallet_module
515                .max_sendable_amount(balance, fee)
516                .await
517                .map_err(|err| AdminGatewayError::WithdrawError {
518                    failure_reason: format!(
519                        "Insufficient funds. Balance: {balance} Fee: {fee}: {}",
520                        err.fmt_compact()
521                    ),
522                })?
523        }
524        BitcoinAmountOrAll::Amount(a) => a,
525    };
526
527    let operation_id = wallet_module
528        .send(
529            address.as_unchecked().clone(),
530            withdraw_amount,
531            Some(fee),
532            serde_json::Value::Null,
533        )
534        .await
535        .map_err(|e| AdminGatewayError::WithdrawError {
536            failure_reason: e.fmt_compact().to_string(),
537        })?;
538
539    let result = wallet_module
540        .await_final_send_operation_state(operation_id)
541        .await
542        .map_err(|e| AdminGatewayError::WithdrawError {
543            failure_reason: e.fmt_compact().to_string(),
544        })?;
545
546    let fees = PegOutFees::from_amount(fee);
547
548    match result {
549        fedimint_walletv2_client::FinalSendOperationState::Success(txid) => {
550            if let Some(Ok(fedimint_walletv2_client::WalletOperationMeta::Send(meta))) = client
551                .operation_log()
552                .get_operation(operation_id)
553                .await
554                .map(|operation| operation.try_meta())
555            {
556                await_withdraw_change(
557                    client,
558                    operation_id,
559                    meta.change_outpoint_range.into_iter().collect(),
560                )
561                .await;
562            }
563            info!(target: LOG_GATEWAY, amount = %withdraw_amount, address = %address, "Sent funds via walletv2");
564            Ok(WithdrawResponse { txid, fees })
565        }
566        fedimint_walletv2_client::FinalSendOperationState::Aborted => {
567            Err(AdminGatewayError::WithdrawError {
568                failure_reason: "Withdrawal transaction was aborted".to_string(),
569            })
570        }
571        fedimint_walletv2_client::FinalSendOperationState::Failure => {
572            Err(AdminGatewayError::WithdrawError {
573                failure_reason: "Withdrawal failed".to_string(),
574            })
575        }
576    }
577}
578
579/// Calculates an estimated max withdrawable amount on-chain
580async fn calculate_max_withdrawable(
581    client: &ClientHandleArc,
582    address: &Address,
583) -> AdminResult<WithdrawDetails> {
584    let balance = client.get_balance_for_btc().await.map_err(|err| {
585        AdminGatewayError::Unexpected(anyhow!("Balance not available: {}", err.fmt_compact()))
586    })?;
587
588    if let Ok(wallet_module) =
589        client.get_first_module::<fedimint_walletv2_client::WalletClientModule>()
590    {
591        let fee = wallet_module
592            .send_fee()
593            .await
594            .map_err(|e| AdminGatewayError::WithdrawError {
595                failure_reason: e.fmt_compact().to_string(),
596            })?;
597
598        let max_withdrawable = wallet_module
599            .max_sendable_amount(balance, fee)
600            .await
601            .map_err(|err| AdminGatewayError::WithdrawError {
602                failure_reason: err.fmt_compact().to_string(),
603            })?;
604
605        // Everything the balance does not become an on-chain payment or miner
606        // fee is the federation fee: the wallet output fee, the mint input
607        // fees on the funding notes, any change output fees and dust. This is
608        // exact by construction, so it needs no separate estimate.
609        let federation_fees = balance
610            .saturating_sub(Amount::from_sats(max_withdrawable.to_sat()))
611            .saturating_sub(Amount::from_sats(fee.to_sat()));
612
613        return Ok(WithdrawDetails {
614            amount: Amount::from_sats(max_withdrawable.to_sat()),
615            mint_fees: Some(federation_fees),
616            peg_out_fees: PegOutFees::from_amount(fee),
617            flat_peg_out_fee: true,
618        });
619    }
620
621    let Ok(wallet_module) = client.get_first_module::<WalletClientModule>() else {
622        return Err(AdminGatewayError::Unexpected(anyhow!(
623            "No wallet module found"
624        )));
625    };
626
627    let (max_withdrawable, peg_out_fees) = wallet_module
628        .max_withdrawable_amount(address, balance)
629        .await
630        .map_err(|err| AdminGatewayError::WithdrawError {
631            failure_reason: err.fmt_compact().to_string(),
632        })?;
633
634    // Everything the balance does not become an on-chain payment or miner fee
635    // is the federation fee: the peg-out output fee, the mint input fees on the
636    // funding notes, any change output fees and dust. This is exact by
637    // construction, so it needs no separate estimate.
638    let federation_fees = balance
639        .saturating_sub(Amount::from_sats(max_withdrawable.to_sat()))
640        .saturating_sub(Amount::from_sats(peg_out_fees.amount().to_sat()));
641
642    Ok(WithdrawDetails {
643        amount: Amount::from_sats(max_withdrawable.to_sat()),
644        mint_fees: Some(federation_fees),
645        peg_out_fees,
646        flat_peg_out_fee: false,
647    })
648}
649
650impl Gateway {
651    /// Returns a bitcoind client using the credentials that were passed in from
652    /// the environment variables.
653    fn get_bitcoind_client(
654        opts: &GatewayOpts,
655        network: bitcoin::Network,
656        gateway_id: &PublicKey,
657    ) -> anyhow::Result<(BitcoindClient, ChainSource)> {
658        let bitcoind_username = opts
659            .bitcoind_username
660            .clone()
661            .expect("FM_BITCOIND_URL is set but FM_BITCOIND_USERNAME is not");
662        let url = opts.bitcoind_url.clone().expect("No bitcoind url set");
663        let password = opts
664            .bitcoind_password
665            .clone()
666            .expect("FM_BITCOIND_URL is set but FM_BITCOIND_PASSWORD is not");
667
668        let chain_source = ChainSource::Bitcoind {
669            username: bitcoind_username.clone(),
670            password: password.clone(),
671            server_url: url.clone(),
672        };
673        let wallet_name = format!("gatewayd-{gateway_id}");
674        let client = BitcoindClient::new(&url, bitcoind_username, password, &wallet_name, network)?;
675        Ok((client, chain_source))
676    }
677
678    /// Default function for creating a gateway with the `Mint`, `Wallet`, and
679    /// `Gateway` modules.
680    pub async fn new_with_default_modules(
681        mnemonic_sender: tokio::sync::broadcast::Sender<MnemonicSource>,
682    ) -> anyhow::Result<Gateway> {
683        let opts = GatewayOpts::parse();
684        let gateway_parameters = opts.to_gateway_parameters()?;
685        let decoders = ModuleDecoderRegistry::default();
686
687        let db_path = opts.data_dir.join(DB_FILE);
688        let gateway_db = match opts.db_backend {
689            DatabaseBackend::RocksDb => {
690                debug!(target: LOG_GATEWAY, "Using RocksDB database backend");
691                Database::new(
692                    fedimint_rocksdb::RocksDb::build(db_path).open().await?,
693                    decoders,
694                )
695            }
696            DatabaseBackend::CursedRedb => {
697                debug!(target: LOG_GATEWAY, "Using CursedRedb database backend");
698                Database::new(
699                    fedimint_cursed_redb::MemAndRedb::new(db_path).await?,
700                    decoders,
701                )
702            }
703        };
704
705        // Apply database migrations before using the database to ensure old database
706        // structures are readable.
707        apply_migrations(
708            &gateway_db,
709            (),
710            "gatewayd".to_string(),
711            get_gatewayd_database_migrations(),
712            None,
713            None,
714        )
715        .await?;
716
717        // For legacy reasons, we use the http id for the unique identifier of the
718        // bitcoind watch-only wallet
719        let http_id = Self::load_or_create_gateway_keypair(&gateway_db, RegisteredProtocol::Http)
720            .await
721            .public_key();
722        let (dyn_bitcoin_rpc, chain_source) =
723            match (opts.bitcoind_url.as_ref(), opts.esplora_url.as_ref()) {
724                (Some(_), None) => {
725                    let (client, chain_source) =
726                        Self::get_bitcoind_client(&opts, gateway_parameters.network, &http_id)?;
727                    (client.into_dyn(), chain_source)
728                }
729                (None, Some(url)) => {
730                    let client = EsploraClient::new(url)
731                        .expect("Could not create EsploraClient")
732                        .into_dyn();
733                    let chain_source = ChainSource::Esplora {
734                        server_url: url.clone(),
735                    };
736                    (client, chain_source)
737                }
738                (Some(_), Some(_)) => {
739                    // Use bitcoind by default if both are set
740                    let (client, chain_source) =
741                        Self::get_bitcoind_client(&opts, gateway_parameters.network, &http_id)?;
742                    (client.into_dyn(), chain_source)
743                }
744                _ => unreachable!("ArgGroup already enforced XOR relation"),
745            };
746
747        // Gateway module will be attached when the federation clients are created
748        // because the LN RPC will be injected with `GatewayClientGen`.
749        let mut registry = ClientModuleInitRegistry::new();
750        registry.attach(MintClientInit);
751        registry.attach(MintV2ClientInit);
752        registry.attach(WalletClientInit::new(dyn_bitcoin_rpc));
753        registry.attach(fedimint_walletv2_client::WalletClientInit);
754
755        let client_builder =
756            GatewayClientBuilder::new(opts.data_dir.clone(), registry, opts.db_backend).await?;
757
758        let (gateway_state, mnemonic_source) = if Self::load_mnemonic(&gateway_db).await.is_some() {
759            (GatewayState::Disconnected, MnemonicSource::Existing)
760        } else {
761            // Generate a mnemonic or use one from an environment variable if `skip_setup`
762            // is true
763            if gateway_parameters.skip_setup {
764                let (mnemonic, mnemonic_source) = if let Ok(words) =
765                    std::env::var(FM_GATEWAY_MNEMONIC_ENV)
766                {
767                    info!(target: LOG_GATEWAY, "Using provided mnemonic from environment variable");
768                    let mnemonic = Mnemonic::parse_in_normalized(Language::English, words.as_str())
769                        .map_err(|e| {
770                            AdminGatewayError::MnemonicError(anyhow!(format!(
771                                "Seed phrase provided in environment was invalid {e:?}"
772                            )))
773                        })?;
774                    (mnemonic, MnemonicSource::Existing)
775                } else {
776                    debug!(target: LOG_GATEWAY, "Generating mnemonic and writing entropy to client storage");
777                    (
778                        Bip39RootSecretStrategy::<12>::random(&mut OsRng),
779                        MnemonicSource::Generated,
780                    )
781                };
782
783                Client::store_encodable_client_secret(&gateway_db, mnemonic.to_entropy())
784                    .await
785                    .map_err(|err| AdminGatewayError::MnemonicError(err.into()))?;
786                (GatewayState::Disconnected, mnemonic_source)
787            } else {
788                // The mnemonic's source arrives through `mnemonic_sender` once
789                // it is set
790                (
791                    GatewayState::NotConfigured { mnemonic_sender },
792                    MnemonicSource::Existing,
793                )
794            }
795        };
796
797        info!(
798            target: LOG_GATEWAY,
799            version = %fedimint_build_code_version_env!(),
800            "Starting gatewayd",
801        );
802
803        Gateway::new(
804            opts.mode,
805            gateway_parameters,
806            gateway_db,
807            client_builder,
808            gateway_state,
809            chain_source,
810            mnemonic_source,
811        )
812        .await
813    }
814
815    /// Helper function for creating a gateway from either
816    /// `new_with_default_modules` or `Gateway::builder`.
817    async fn new(
818        lightning_mode: LightningMode,
819        gateway_parameters: GatewayParameters,
820        gateway_db: Database,
821        client_builder: GatewayClientBuilder,
822        gateway_state: GatewayState,
823        chain_source: ChainSource,
824        mnemonic_source: MnemonicSource,
825    ) -> anyhow::Result<Gateway> {
826        let num_route_hints = gateway_parameters.num_route_hints;
827        let network = gateway_parameters.network;
828
829        let task_group = TaskGroup::new();
830        task_group.install_kill_handler();
831
832        let mut registrations = BTreeMap::new();
833        if let Some(http_url) = gateway_parameters.versioned_api {
834            registrations.insert(
835                RegisteredProtocol::Http,
836                Registration::new(&gateway_db, http_url, RegisteredProtocol::Http).await,
837            );
838        }
839
840        let iroh_sk = Self::load_or_create_iroh_key(&gateway_db).await;
841        if gateway_parameters.iroh_listen.is_some() {
842            let endpoint_url = SafeUrl::parse(&format!("iroh://{}", iroh_sk.public()))?;
843            registrations.insert(
844                RegisteredProtocol::Iroh,
845                Registration::new(&gateway_db, endpoint_url, RegisteredProtocol::Iroh).await,
846            );
847        }
848
849        Ok(Self {
850            federation_manager: Arc::new(RwLock::new(FederationManager::new())),
851            lightning_mode,
852            state: Arc::new(RwLock::new(gateway_state)),
853            client_builder,
854            gateway_db: gateway_db.clone(),
855            listen: gateway_parameters.listen,
856            metrics_listen: gateway_parameters.metrics_listen,
857            task_group,
858            bcrypt_password_hash: gateway_parameters.bcrypt_password_hash.to_string(),
859            bcrypt_liquidity_manager_password_hash: gateway_parameters
860                .bcrypt_liquidity_manager_password_hash
861                .map(|h| h.to_string()),
862            num_route_hints,
863            network,
864            chain_source,
865            default_routing_fees: gateway_parameters.default_routing_fees,
866            default_transaction_fees: gateway_parameters.default_transaction_fees,
867            iroh_sk,
868            iroh_dns: gateway_parameters.iroh_dns,
869            iroh_relays: gateway_parameters.iroh_relays,
870            iroh_listen: gateway_parameters.iroh_listen,
871            registrations,
872            registration_health: RegistrationHealthTracker::default(),
873            invoice_rate_limiter: Arc::new(TokenBucketRateLimiter::new(
874                gateway_parameters.invoice_rate_limit_burst,
875                gateway_parameters.invoice_rate_limit_per_second,
876            )),
877            lnv2_hold_invoice_lock_pool: Arc::new(lockable::LockPool::new()),
878            payment_policy_lock: Arc::new(RwLock::new(())),
879            mnemonic_source,
880        })
881    }
882
883    async fn load_or_create_gateway_keypair(
884        gateway_db: &Database,
885        protocol: RegisteredProtocol,
886    ) -> secp256k1::Keypair {
887        let mut dbtx = gateway_db.begin_transaction().await;
888        let keypair = dbtx.load_or_create_gateway_keypair(protocol).await;
889        dbtx.commit_tx().await;
890        keypair
891    }
892
893    /// Returns `iroh::SecretKey` and saves it to the database if it does not
894    /// exist
895    async fn load_or_create_iroh_key(gateway_db: &Database) -> iroh::SecretKey {
896        let mut dbtx = gateway_db.begin_transaction().await;
897        let iroh_sk = dbtx.load_or_create_iroh_key().await;
898        dbtx.commit_tx().await;
899        iroh_sk
900    }
901
902    pub async fn http_gateway_id(&self) -> PublicKey {
903        Self::load_or_create_gateway_keypair(&self.gateway_db, RegisteredProtocol::Http)
904            .await
905            .public_key()
906    }
907
908    async fn get_state(&self) -> GatewayState {
909        self.state.read().await.clone()
910    }
911
912    /// Reads and serializes structures from the Gateway's database for the
913    /// purpose for serializing to JSON for inspection.
914    pub async fn dump_database(
915        dbtx: &mut DatabaseTransaction<'_>,
916        prefix_names: Vec<String>,
917    ) -> BTreeMap<String, Box<dyn erased_serde::Serialize + Send>> {
918        dbtx.dump_database(prefix_names).await
919    }
920
921    /// Main entrypoint into the gateway that starts the client registration
922    /// timer, loads the federation clients from the persisted config,
923    /// begins listening for intercepted payments, and starts the webserver
924    /// to service requests.
925    pub async fn run(
926        self,
927        runtime: Arc<tokio::runtime::Runtime>,
928        mnemonic_receiver: tokio::sync::broadcast::Receiver<MnemonicSource>,
929    ) -> anyhow::Result<TaskShutdownToken> {
930        install_crypto_provider().await;
931        self.register_clients_timer();
932        self.load_clients().await?;
933        self.start_gateway(runtime, mnemonic_receiver.resubscribe());
934        self.spawn_backup_task();
935        self.spawn_prune_registered_contracts_task();
936        // start metrics server
937        fedimint_metrics::spawn_api_server(self.metrics_listen, self.task_group.clone()).await?;
938        // start webserver last to avoid handling requests before fully initialized
939        let handle = self.task_group.make_handle();
940        run_webserver(Arc::new(self), mnemonic_receiver.resubscribe()).await?;
941        let shutdown_receiver = handle.make_shutdown_rx();
942        Ok(shutdown_receiver)
943    }
944
945    /// Spawns a background task that checks every `BACKUP_UPDATE_INTERVAL` to
946    /// see if any federations need to be backed up.
947    fn spawn_backup_task(&self) {
948        let self_copy = self.clone();
949        self.task_group
950            .spawn_cancellable_silent("backup ecash", async move {
951                const BACKUP_UPDATE_INTERVAL: Duration = Duration::from_hours(1);
952                let mut interval = tokio::time::interval(BACKUP_UPDATE_INTERVAL);
953                interval.tick().await;
954                loop {
955                    {
956                        let mut dbtx = self_copy.gateway_db.begin_transaction().await;
957                        self_copy.backup_all_federations(&mut dbtx).await;
958                        dbtx.commit_tx().await;
959                        interval.tick().await;
960                    }
961                }
962            });
963    }
964
965    /// Spawns a background task that periodically deletes registered incoming
966    /// contract records whose invoice has long expired, so unpaid invoice
967    /// registrations cannot grow the database without bound.
968    fn spawn_prune_registered_contracts_task(&self) {
969        let self_copy = self.clone();
970        self.task_group.spawn_cancellable_silent(
971            "prune registered incoming contracts",
972            async move {
973                const PRUNE_INTERVAL: Duration = Duration::from_hours(1);
974                // Records are kept for a day past invoice expiry so payments
975                // settled shortly before expiry can still complete and be
976                // verified via the preimage verification endpoint.
977                const RETENTION_AFTER_EXPIRY: Duration = Duration::from_hours(24);
978
979                let mut interval = tokio::time::interval(PRUNE_INTERVAL);
980                loop {
981                    interval.tick().await;
982
983                    let cutoff_secs = duration_since_epoch()
984                        .saturating_sub(RETENTION_AFTER_EXPIRY)
985                        .as_secs();
986
987                    let mut dbtx = self_copy.gateway_db.begin_transaction().await;
988                    let num_pruned = dbtx.prune_registered_incoming_contracts(cutoff_secs).await;
989                    match dbtx.commit_tx_result().await {
990                        Ok(()) => {
991                            if num_pruned > 0 {
992                                info!(
993                                    target: LOG_GATEWAY,
994                                    num_pruned,
995                                    "Pruned expired incoming contract records"
996                                );
997                            }
998                        }
999                        Err(err) => {
1000                            warn!(
1001                                target: LOG_GATEWAY,
1002                                err = %err.fmt_compact(),
1003                                "Failed to prune expired incoming contract records"
1004                            );
1005                        }
1006                    }
1007                }
1008            },
1009        );
1010    }
1011
1012    /// Loops through all federations and checks their last save backup time. If
1013    /// the last saved backup time is past the threshold time, backup the
1014    /// federation.
1015    pub async fn backup_all_federations(&self, dbtx: &mut DatabaseTransaction<'_, Committable>) {
1016        /// How long the federation manager should wait to backup the ecash for
1017        /// each federation
1018        const BACKUP_THRESHOLD_DURATION: Duration = Duration::from_hours(24);
1019
1020        let now = fedimint_core::time::now();
1021        let threshold = now
1022            .checked_sub(BACKUP_THRESHOLD_DURATION)
1023            .expect("Cannot be negative");
1024        for (id, last_backup) in dbtx.load_backup_records().await {
1025            match last_backup {
1026                Some(backup_time) if backup_time < threshold => {
1027                    let fed_manager = self.federation_manager.read().await;
1028                    fed_manager.backup_federation(&id, dbtx, now).await;
1029                }
1030                None => {
1031                    let fed_manager = self.federation_manager.read().await;
1032                    fed_manager.backup_federation(&id, dbtx, now).await;
1033                }
1034                _ => {}
1035            }
1036        }
1037    }
1038
1039    /// Begins the task for listening for intercepted payments from the
1040    /// lightning node.
1041    fn start_gateway(
1042        &self,
1043        runtime: Arc<tokio::runtime::Runtime>,
1044        mut mnemonic_receiver: tokio::sync::broadcast::Receiver<MnemonicSource>,
1045    ) {
1046        const PAYMENT_STREAM_RETRY_SECONDS: u64 = 60;
1047
1048        let self_copy = self.clone();
1049        let tg = self.task_group.clone();
1050        self.task_group.spawn(
1051            "Subscribe to intercepted lightning payments in stream",
1052            |handle| async move {
1053                let mut mnemonic_source = self_copy.mnemonic_source;
1054                // Repeatedly attempt to establish a connection to the lightning node and create a payment stream, re-trying if the connection is broken.
1055                loop {
1056                    if handle.is_shutting_down() {
1057                        info!(target: LOG_GATEWAY, "Gateway lightning payment stream handler loop is shutting down");
1058                        break;
1059                    }
1060
1061                    if let GatewayState::NotConfigured{ .. } = self_copy.get_state().await {
1062                        info!(
1063                            target: LOG_GATEWAY,
1064                            "Waiting for the mnemonic to be set before starting lightning receive loop."
1065                        );
1066                        info!(
1067                            target: LOG_GATEWAY,
1068                            "You might need to provide it from the UI or refer to documentation w.r.t how to initialize it."
1069                        );
1070
1071                        if let Ok(source) = mnemonic_receiver.recv().await {
1072                            mnemonic_source = source;
1073                        }
1074                        info!(
1075                            target: LOG_GATEWAY,
1076                            ?mnemonic_source,
1077                            "Received mnemonic, attempting to start lightning receive loop"
1078                        );
1079                    }
1080
1081                    let payment_stream_task_group = tg.make_subgroup();
1082                    let lnrpc_route = self_copy.create_lightning_client(runtime.clone(), mnemonic_source).await;
1083
1084                    debug!(target: LOG_GATEWAY, "Establishing lightning payment stream...");
1085                    let (stream, ln_client) = match lnrpc_route.route_htlcs(&payment_stream_task_group).await
1086                    {
1087                        Ok((stream, ln_client)) => (stream, ln_client),
1088                        Err(err) => {
1089                            warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Failed to open lightning payment stream");
1090                            // `route_htlcs` may have already spawned tasks into the
1091                            // subgroup before failing (e.g. the LNv1 interceptor is
1092                            // spawned before LNv2 setup, which can fail). Tear the
1093                            // subgroup down so no stale task keeps owning the LND HTLC
1094                            // stream, which would prevent the retry from taking over and
1095                            // could cause it to cancel real HTLCs after `gateway_receiver`
1096                            // is dropped.
1097                            if let Err(err) = payment_stream_task_group.shutdown_join_all(None).await {
1098                                crit!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Lightning payment stream task group shutdown");
1099                            }
1100                            sleep(Duration::from_secs(PAYMENT_STREAM_RETRY_SECONDS)).await;
1101                            continue
1102                        }
1103                    };
1104
1105                    // Successful calls to `route_htlcs` establish a connection
1106                    self_copy.set_gateway_state(GatewayState::Connected).await;
1107                    info!(target: LOG_GATEWAY, "Established lightning payment stream");
1108
1109                    let route_payments_response =
1110                        self_copy.route_lightning_payments(&handle, stream, ln_client).await;
1111
1112                    self_copy.set_gateway_state(GatewayState::Disconnected).await;
1113                    if let Err(err) = payment_stream_task_group.shutdown_join_all(None).await {
1114                        crit!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Lightning payment stream task group shutdown");
1115                    }
1116
1117                    self_copy.unannounce_from_all_federations().await;
1118
1119                    match route_payments_response {
1120                        ReceivePaymentStreamAction::RetryAfterDelay => {
1121                            warn!(target: LOG_GATEWAY, retry_interval = %PAYMENT_STREAM_RETRY_SECONDS, "Disconnected from lightning node");
1122                            sleep(Duration::from_secs(PAYMENT_STREAM_RETRY_SECONDS)).await;
1123                        }
1124                        ReceivePaymentStreamAction::NoRetry => break,
1125                    }
1126                }
1127            },
1128        );
1129    }
1130
1131    /// Handles a stream of incoming payments from the lightning node after
1132    /// ensuring the gateway is properly configured. Awaits until the stream
1133    /// is closed, then returns with the appropriate action to take.
1134    async fn route_lightning_payments<'a>(
1135        &'a self,
1136        handle: &TaskHandle,
1137        mut stream: RouteHtlcStream<'a>,
1138        ln_client: Arc<dyn ILnRpcClient>,
1139    ) -> ReceivePaymentStreamAction {
1140        let LightningInfo::Connected {
1141            public_key: lightning_public_key,
1142            alias: lightning_alias,
1143            network: lightning_network,
1144            block_height: _,
1145            synced_to_chain,
1146        } = ln_client.parsed_node_info().await
1147        else {
1148            warn!(target: LOG_GATEWAY, "Failed to retrieve Lightning info");
1149            return ReceivePaymentStreamAction::RetryAfterDelay;
1150        };
1151
1152        assert!(
1153            self.network == lightning_network,
1154            "Lightning node network does not match Gateway's network. LN: {lightning_network} Gateway: {}",
1155            self.network
1156        );
1157
1158        if synced_to_chain || is_env_var_set(FM_GATEWAY_SKIP_WAIT_FOR_SYNC_ENV) {
1159            info!(target: LOG_GATEWAY, "Gateway is already synced to chain");
1160        } else {
1161            self.set_gateway_state(GatewayState::Syncing).await;
1162            info!(target: LOG_GATEWAY, "Waiting for chain sync");
1163            if let Err(err) = ln_client.wait_for_chain_sync().await {
1164                warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Failed to wait for chain sync");
1165                return ReceivePaymentStreamAction::RetryAfterDelay;
1166            }
1167        }
1168
1169        let lightning_context = LightningContext {
1170            lnrpc: LnRpcTracked::new(ln_client, "gateway"),
1171            lightning_public_key,
1172            lightning_alias,
1173            lightning_network,
1174        };
1175        if let GatewayState::ShuttingDown { .. } = self
1176            .set_gateway_state(GatewayState::Running { lightning_context })
1177            .await
1178        {
1179            info!(
1180                target: LOG_GATEWAY,
1181                "Reconnected to the lightning node while shutting down, not accepting payments"
1182            );
1183        } else {
1184            info!(target: LOG_GATEWAY, "Gateway is running");
1185        }
1186
1187        if matches!(self.lightning_mode, LightningMode::Lnd { .. }) {
1188            // Re-register the gateway with all federations after connecting to the
1189            // lightning node
1190            let mut dbtx = self.gateway_db.begin_transaction_nc().await;
1191            let all_federations_configs =
1192                dbtx.load_federation_configs().await.into_iter().collect();
1193            self.register_federations(&all_federations_configs, &self.task_group)
1194                .await;
1195        }
1196
1197        // Runs until the connection to the lightning node breaks or we receive the
1198        // shutdown signal.
1199        let htlc_task_group = self.task_group.make_subgroup();
1200        if handle
1201            .cancel_on_shutdown(async move {
1202                loop {
1203                    let payment_request_or = tokio::select! {
1204                        payment_request_or = stream.next() => {
1205                            payment_request_or
1206                        }
1207                        () = self.is_shutting_down_safely() => {
1208                            break;
1209                        }
1210                    };
1211
1212                    let Some(payment_request) = payment_request_or else {
1213                        warn!(
1214                            target: LOG_GATEWAY,
1215                            "Unexpected response from incoming lightning payment stream. Shutting down payment processor"
1216                        );
1217                        break;
1218                    };
1219
1220                    let state_guard = self.state.read().await;
1221                    if let GatewayState::Running { ref lightning_context } = *state_guard {
1222                        // Spawn a subtask to handle each payment in parallel
1223                        let gateway = self.clone();
1224                        let lightning_context = lightning_context.clone();
1225                        htlc_task_group.spawn_cancellable_silent(
1226                            "handle_lightning_payment",
1227                            async move {
1228                                let start = fedimint_core::time::now();
1229                                let outcome = gateway
1230                                    .handle_lightning_payment(payment_request, &lightning_context)
1231                                    .await;
1232                                metrics::HTLC_HANDLING_DURATION_SECONDS
1233                                    .with_label_values(&[outcome])
1234                                    .observe(
1235                                        fedimint_core::time::now()
1236                                            .duration_since(start)
1237                                            .unwrap_or_default()
1238                                            .as_secs_f64(),
1239                                    );
1240                            },
1241                        );
1242                    } else {
1243                        warn!(
1244                            target: LOG_GATEWAY,
1245                            state = %state_guard,
1246                            "Gateway isn't in a running state, cannot handle incoming payments."
1247                        );
1248                        break;
1249                    }
1250                }
1251            })
1252            .await
1253            .is_ok()
1254        {
1255            warn!(target: LOG_GATEWAY, "Lightning payment stream connection broken. Gateway is disconnected");
1256            ReceivePaymentStreamAction::RetryAfterDelay
1257        } else {
1258            info!(target: LOG_GATEWAY, "Received shutdown signal");
1259            ReceivePaymentStreamAction::NoRetry
1260        }
1261    }
1262
1263    /// Polls the Gateway's state waiting for it to shutdown so the thread
1264    /// processing payment requests can exit.
1265    async fn is_shutting_down_safely(&self) {
1266        loop {
1267            if let GatewayState::ShuttingDown { .. } = self.get_state().await {
1268                return;
1269            }
1270
1271            fedimint_core::task::sleep(Duration::from_secs(1)).await;
1272        }
1273    }
1274
1275    /// Handles an intercepted lightning payment. If the payment is part of an
1276    /// incoming payment to a federation, spawns a state machine and hands the
1277    /// payment off to it. If the payment's last-hop short channel id maps to
1278    /// a known federation but no LNv1 or LNv2 offer matched, cancels (fails
1279    /// back) the HTLC so the sender can retry rather than treating the
1280    /// gateway as a dead route. Otherwise (real-channel forwards), resumes
1281    /// the HTLC so LND can route it as a normal forward. A duplicate delivery
1282    /// of a HOLD invoice that is still being handled is dropped.
1283    ///
1284    /// Returns the outcome label for metrics tracking.
1285    pub async fn handle_lightning_payment(
1286        &self,
1287        payment_request: InterceptPaymentRequest,
1288        lightning_context: &LightningContext,
1289    ) -> &'static str {
1290        info!(
1291            target: LOG_GATEWAY,
1292            lightning_payment = %PrettyInterceptPaymentRequest(&payment_request),
1293            "Intercepting lightning payment",
1294        );
1295
1296        // A payment without an incoming circuit is held by a HOLD invoice, which
1297        // the node can deliver more than once. Only one delivery is handled at a
1298        // time, from here through settling or cancelling the invoice. A
1299        // duplicate is dropped rather than made to wait: whatever the first
1300        // delivery decides resolves the whole invoice, and a duplicate handled
1301        // afterwards could fund an invoice the first has just cancelled. LND,
1302        // for one, replays every accepted invoice when the gateway resubscribes
1303        // after a reconnect. Forwards are not deduplicated: each has
1304        // its own circuit, and a replay after a reconnect may be the only
1305        // delivery still able to resolve it.
1306        let _hold_invoice_guard = if payment_request.incoming_circuit().is_none() {
1307            let Some(guard) = self
1308                .lnv2_hold_invoice_lock_pool
1309                .try_lock(payment_request.payment_hash)
1310            else {
1311                info!(
1312                    target: LOG_GATEWAY,
1313                    payment_hash = %payment_request.payment_hash,
1314                    "Dropping duplicate delivery of a HOLD invoice that is already being handled",
1315                );
1316                return "duplicate";
1317            };
1318            Some(guard)
1319        } else {
1320            None
1321        };
1322
1323        let lnv2_start = fedimint_core::time::now();
1324        let lnv2_result = self
1325            .try_handle_lightning_payment_lnv2(&payment_request, lightning_context)
1326            .await;
1327        let lnv2_outcome = if lnv2_result.is_ok() {
1328            "success"
1329        } else {
1330            "error"
1331        };
1332        metrics::HTLC_LNV2_ATTEMPT_DURATION_SECONDS
1333            .with_label_values(&[lnv2_outcome])
1334            .observe(
1335                fedimint_core::time::now()
1336                    .duration_since(lnv2_start)
1337                    .unwrap_or_default()
1338                    .as_secs_f64(),
1339            );
1340        if lnv2_result.is_ok() {
1341            return "lnv2";
1342        }
1343
1344        // A payment held for a registered LNv2 contract is left to that
1345        // contract's receive, or to the Lightning node's expiry, even if its
1346        // amount does not match: cancelling it would also fail back HTLCs the
1347        // receive may already be settling.
1348        if payment_request.incoming_circuit().is_none()
1349            && let Err(PublicGatewayError::LNv2(err @ LNv2Error::IncomingAmountMismatch { .. })) =
1350                &lnv2_result
1351        {
1352            warn!(
1353                target: LOG_GATEWAY,
1354                payment_hash = %payment_request.payment_hash,
1355                err = %err.fmt_compact(),
1356                "Ignoring payment for a registered LNv2 contract whose amount does not match",
1357            );
1358            return "ignore";
1359        }
1360
1361        let lnv1_start = fedimint_core::time::now();
1362        let lnv1_result = self
1363            .try_handle_lightning_payment_ln_legacy(&payment_request, lightning_context)
1364            .await;
1365        let lnv1_outcome = if lnv1_result.is_ok() {
1366            "success"
1367        } else {
1368            "error"
1369        };
1370        metrics::HTLC_LNV1_ATTEMPT_DURATION_SECONDS
1371            .with_label_values(&[lnv1_outcome])
1372            .observe(
1373                fedimint_core::time::now()
1374                    .duration_since(lnv1_start)
1375                    .unwrap_or_default()
1376                    .as_secs_f64(),
1377            );
1378        if lnv1_result.is_ok() {
1379            return "lnv1";
1380        }
1381
1382        // Neither LNv1 nor LNv2 matched. If the last-hop scid is one of our
1383        // federation virtual scids, cancel so the sender gets a non-permanent
1384        // failure (avoiding `UNKNOWN_NEXT_PEER` blacklisting). If the scid is
1385        // for a real channel, resume so LND forwards normally.
1386        let is_federation_scid = match payment_request.short_channel_id {
1387            Some(scid) => self
1388                .federation_manager
1389                .read()
1390                .await
1391                .get_client_for_index(scid)
1392                .is_some(),
1393            None => false,
1394        };
1395
1396        if is_federation_scid {
1397            // The HTLC targeted a federation we serve but we couldn't claim
1398            // it (no LNv1 offer / no LNv2 contract / receives turned off /
1399            // underfunded gateway / federation timeout / etc.). Surface the
1400            // underlying error variants so operators can diagnose the cause;
1401            // otherwise both `Err` values are dropped on the floor and the
1402            // only visible signal is the metric label `"error"`.
1403            warn!(
1404                target: LOG_GATEWAY,
1405                payment_hash = %payment_request.payment_hash,
1406                short_channel_id = ?payment_request.short_channel_id,
1407                amount_msat = payment_request.amount_msat,
1408                incoming_chan_id = payment_request.incoming_chan_id,
1409                htlc_id = payment_request.htlc_id,
1410                lnv2_err = ?lnv2_result.as_ref().err(),
1411                lnv1_err = ?lnv1_result.as_ref().err(),
1412                "Lightning payment for a served federation could not be accepted: cancelling HTLC",
1413            );
1414            Self::cancel_unmatched_lightning_payment(payment_request, lightning_context).await;
1415            "cancel"
1416        } else {
1417            // Normal route-through traffic: the gateway's LND interceptor
1418            // sees every HTLC, but only federation-scid HTLCs are ours to
1419            // handle. Resume so LND forwards the rest as a regular routing
1420            // node — no warning needed since this is the expected path.
1421            Self::forward_lightning_payment(payment_request, lightning_context).await;
1422            "forward"
1423        }
1424    }
1425
1426    /// Tries to handle a lightning payment using the LNv2 protocol.
1427    /// Returns `Ok` if the payment was handled, `Err` otherwise.
1428    async fn try_handle_lightning_payment_lnv2(
1429        &self,
1430        htlc_request: &InterceptPaymentRequest,
1431        lightning_context: &LightningContext,
1432    ) -> Result<()> {
1433        // If `payment_hash` has been registered as a LNv2 payment, we try to complete
1434        // the payment by getting the preimage from the federation
1435        // using the LNv2 protocol. If the `payment_hash` is not registered,
1436        // this payment is either a legacy Lightning payment or the end destination is
1437        // not a Fedimint.
1438        // Match and fund against the amount actually locked in the incoming
1439        // HTLC, not the sender-controlled onion forward amount, so a forged
1440        // `amt_to_forward` cannot satisfy the registered contract's amount
1441        // check while only a token amount is really locked.
1442        let (contract, client) = self
1443            .get_registered_incoming_contract_and_client_v2(
1444                PaymentImage::Hash(htlc_request.payment_hash),
1445                htlc_request.incoming_amount_msat,
1446            )
1447            .await?;
1448
1449        // An intercepted forward, or an unknown current height, leaves no time
1450        // to fund a fresh contract; see `lnv2_blocks_to_claim_deadline`.
1451        let current_block_height = lightning_context
1452            .lnrpc
1453            .info()
1454            .await
1455            .ok()
1456            .map(|info| info.block_height);
1457        let blocks_to_claim_deadline =
1458            htlc_request.lnv2_blocks_to_claim_deadline(current_block_height);
1459
1460        if let Err(err) = client
1461            .get_first_module::<GatewayClientModuleV2>()
1462            .expect("Must have client module")
1463            .relay_incoming_htlc(
1464                htlc_request.payment_hash,
1465                htlc_request.incoming_chan_id,
1466                htlc_request.htlc_id,
1467                contract,
1468                htlc_request.incoming_amount_msat,
1469                blocks_to_claim_deadline,
1470            )
1471            .await
1472        {
1473            warn!(
1474                target: LOG_GATEWAY,
1475                err = %err.fmt_compact(),
1476                "Error relaying incoming lightning payment"
1477            );
1478
1479            let outcome = InterceptPaymentResponse {
1480                action: PaymentAction::Cancel,
1481                payment_hash: htlc_request.payment_hash,
1482                incoming_chan_id: htlc_request.incoming_chan_id,
1483                htlc_id: htlc_request.htlc_id,
1484            };
1485
1486            if let Err(err) = lightning_context.lnrpc.complete_htlc(outcome).await {
1487                warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Error sending HTLC response to lightning node");
1488            }
1489        }
1490
1491        Ok(())
1492    }
1493
1494    /// Tries to handle a lightning payment using the legacy lightning protocol.
1495    /// Returns `Ok` if the payment was handled, `Err` otherwise.
1496    async fn try_handle_lightning_payment_ln_legacy(
1497        &self,
1498        htlc_request: &InterceptPaymentRequest,
1499        lightning_context: &LightningContext,
1500    ) -> Result<()> {
1501        // Check if the payment corresponds to a federation supporting legacy Lightning.
1502        let Some(federation_index) = htlc_request.short_channel_id else {
1503            return Err(PublicGatewayError::LNv1(LNv1Error::IncomingPayment(
1504                "Incoming payment has not last hop short channel id".to_string(),
1505            )));
1506        };
1507
1508        let Some(client) = self
1509            .federation_manager
1510            .read()
1511            .await
1512            .get_client_for_index(federation_index)
1513        else {
1514            return Err(PublicGatewayError::LNv1(LNv1Error::IncomingPayment("Incoming payment has a last hop short channel id that does not map to a known federation".to_string())));
1515        };
1516
1517        // Both LND's `incoming_expiry` and LDK's `claim_deadline` are absolute
1518        // Bitcoin heights. LDK does not currently produce LNv1 forwards (it has
1519        // no federation short-channel id), but using the backend's own best
1520        // height keeps the unit and chain view consistent for every backend.
1521        client
1522            .borrow()
1523            .with(|client| async {
1524                let htlc = Htlc::from(htlc_request.clone());
1525                let lnv1 = client
1526                    .get_first_module::<GatewayClientModule>()
1527                    .map_err(|_| {
1528                        PublicGatewayError::LNv1(LNv1Error::IncomingPayment(
1529                            "Federation does not have LNv1 module".to_string(),
1530                        ))
1531                    })?;
1532                match lnv1
1533                    .gateway_handle_intercepted_htlc(htlc, async {
1534                        Ok(lightning_context.lnrpc.info().await?.block_height)
1535                    })
1536                    .await
1537                {
1538                    Ok(_) => Ok(()),
1539                    Err(e) => Err(PublicGatewayError::LNv1(LNv1Error::IncomingPayment(
1540                        format!("Error intercepting lightning payment {}", e.fmt_compact()),
1541                    ))),
1542                }
1543            })
1544            .await
1545    }
1546
1547    /// Cancels (fails back) a lightning payment whose last-hop scid maps to a
1548    /// known federation but matched no LNv1 or LNv2 offer.
1549    ///
1550    /// Returning `PaymentAction::Forward` here would tell LND to resume the
1551    /// HTLC as a normal forward, but the last-hop short channel id is a
1552    /// virtual scid (no real channel exists), so LND would fail it back with
1553    /// the permanent error `UNKNOWN_NEXT_PEER`. Senders' mission control
1554    /// treats that as a permanent blacklist signal against the gateway,
1555    /// breaking future payments across all federations.
1556    ///
1557    /// `PaymentAction::Cancel` maps to `ResolveHoldForwardAction::Fail`, which
1558    /// fails the HTLC back with a non-permanent reason so the sender can
1559    /// retry instead of blacklisting the gateway.
1560    async fn cancel_unmatched_lightning_payment(
1561        htlc_request: InterceptPaymentRequest,
1562        lightning_context: &LightningContext,
1563    ) {
1564        let outcome = InterceptPaymentResponse {
1565            action: PaymentAction::Cancel,
1566            payment_hash: htlc_request.payment_hash,
1567            incoming_chan_id: htlc_request.incoming_chan_id,
1568            htlc_id: htlc_request.htlc_id,
1569        };
1570
1571        if let Err(err) = lightning_context.lnrpc.complete_htlc(outcome).await {
1572            warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Error sending lightning payment response to lightning node");
1573        }
1574    }
1575
1576    /// Forwards a lightning payment to the next hop like a normal lightning
1577    /// node. Used when the intercepted HTLC is not destined for any federation
1578    /// this gateway serves, so LND should route it normally over a real
1579    /// channel.
1580    async fn forward_lightning_payment(
1581        htlc_request: InterceptPaymentRequest,
1582        lightning_context: &LightningContext,
1583    ) {
1584        let outcome = InterceptPaymentResponse {
1585            action: PaymentAction::Forward,
1586            payment_hash: htlc_request.payment_hash,
1587            incoming_chan_id: htlc_request.incoming_chan_id,
1588            htlc_id: htlc_request.htlc_id,
1589        };
1590
1591        if let Err(err) = lightning_context.lnrpc.complete_htlc(outcome).await {
1592            warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Error sending lightning payment response to lightning node");
1593        }
1594    }
1595
1596    /// Helper function for atomically changing the Gateway's internal state.
1597    ///
1598    /// Shutting down is one-way. The lightning connection loop keeps running
1599    /// while `handle_shutdown_msg` drains the payments that are still in
1600    /// flight, and a reconnect in that window must not move the gateway back
1601    /// into a state that accepts new payments. A reconnect may still hand over
1602    /// a fresh `LightningContext`, which the drain needs to complete the
1603    /// payments it is waiting for.
1604    /// Returns the state that is in effect afterwards, which is not the
1605    /// requested one if the gateway is shutting down.
1606    async fn set_gateway_state(&self, state: GatewayState) -> GatewayState {
1607        let mut lock = self.state.write().await;
1608
1609        if let GatewayState::ShuttingDown { .. } = *lock {
1610            match state {
1611                GatewayState::Running { lightning_context } => {
1612                    *lock = GatewayState::ShuttingDown { lightning_context };
1613                }
1614                ignored => {
1615                    info!(
1616                        target: LOG_GATEWAY,
1617                        ignored_state = %ignored,
1618                        "Gateway is shutting down, ignoring state change"
1619                    );
1620                }
1621            }
1622        } else {
1623            *lock = state;
1624        }
1625
1626        lock.clone()
1627    }
1628
1629    /// Drives the gateway's state directly, bypassing the lightning connection
1630    /// loop in [`Self::start_gateway`] that owns every real transition.
1631    ///
1632    /// Tests need to observe behaviour in states the builder cannot start them
1633    /// in -- notably "connected later than the federation clients" -- and have
1634    /// no lightning node to get there with. Nothing in production should call
1635    /// this.
1636    #[doc(hidden)]
1637    pub async fn set_gateway_state_out_of_band(&self, state: GatewayState) {
1638        self.set_gateway_state(state).await;
1639    }
1640
1641    /// If the Gateway is connected to the Lightning node, returns the
1642    /// `ClientConfig` for each federation that the Gateway is connected to.
1643    pub async fn handle_get_federation_config(
1644        &self,
1645        federation_id_or: Option<FederationId>,
1646    ) -> AdminResult<GatewayFedConfig> {
1647        if !matches!(self.get_state().await, GatewayState::Running { .. }) {
1648            return Ok(GatewayFedConfig {
1649                federations: BTreeMap::new(),
1650            });
1651        }
1652
1653        let federations = if let Some(federation_id) = federation_id_or {
1654            let mut federations = BTreeMap::new();
1655            federations.insert(
1656                federation_id,
1657                self.federation_manager
1658                    .read()
1659                    .await
1660                    .get_federation_config(federation_id)
1661                    .await?,
1662            );
1663            federations
1664        } else {
1665            self.federation_manager
1666                .read()
1667                .await
1668                .get_all_federation_configs()
1669                .await
1670        };
1671
1672        Ok(GatewayFedConfig { federations })
1673    }
1674
1675    /// Returns a Bitcoin deposit on-chain address for pegging in Bitcoin for a
1676    /// specific connected federation.
1677    pub async fn handle_address_msg(&self, payload: DepositAddressPayload) -> AdminResult<Address> {
1678        let client = self.select_client(payload.federation_id).await?;
1679
1680        if let Ok(wallet_module) = client.value().get_first_module::<WalletClientModule>() {
1681            let address = wallet_module
1682                .allocate_deposit_address_expert_only(())
1683                .await
1684                .map_err(|e| AdminGatewayError::Unexpected(e.into()))?
1685                .address;
1686            Ok(address)
1687        } else if let Ok(wallet_module) = client
1688            .value()
1689            .get_first_module::<fedimint_walletv2_client::WalletClientModule>()
1690        {
1691            Ok(wallet_module.receive().await)
1692        } else {
1693            Err(AdminGatewayError::Unexpected(anyhow!(
1694                "No wallet module found"
1695            )))
1696        }
1697    }
1698
1699    /// Requests the gateway to pay an outgoing LN invoice on behalf of a
1700    /// Fedimint client. Returns the payment hash's preimage on success.
1701    async fn handle_pay_invoice_msg(
1702        &self,
1703        payload: fedimint_ln_client::pay::PayInvoicePayload,
1704    ) -> Result<Preimage> {
1705        let GatewayState::Running { .. } = self.get_state().await else {
1706            return Err(PublicGatewayError::Lightning(
1707                LightningRpcError::FailedToConnect,
1708            ));
1709        };
1710
1711        debug!(target: LOG_GATEWAY, "Handling pay invoice message");
1712        let client = self.select_client(payload.federation_id).await?;
1713        let contract_id = payload.contract_id;
1714        let gateway_module = &client
1715            .value()
1716            .get_first_module::<GatewayClientModule>()
1717            .map_err(|err| LNv1Error::OutgoingPayment(err.into()))
1718            .map_err(PublicGatewayError::LNv1)?;
1719        let operation_id = gateway_module
1720            .gateway_pay_bolt11_invoice(payload)
1721            .await
1722            .map_err(|err| LNv1Error::OutgoingPayment(err.into()))
1723            .map_err(PublicGatewayError::LNv1)?;
1724        let mut updates = gateway_module
1725            .gateway_subscribe_ln_pay(operation_id)
1726            .await
1727            .map_err(|err| LNv1Error::OutgoingPayment(err.into()))
1728            .map_err(PublicGatewayError::LNv1)?
1729            .into_stream();
1730        while let Some(update) = updates.next().await {
1731            match update {
1732                GatewayExtPayStates::Success { preimage, .. } => {
1733                    debug!(target: LOG_GATEWAY, contract_id = %contract_id, "Successfully paid invoice");
1734                    return Ok(preimage);
1735                }
1736                GatewayExtPayStates::Fail {
1737                    error,
1738                    error_message,
1739                } => {
1740                    return Err(PublicGatewayError::LNv1(LNv1Error::OutgoingContract {
1741                        error: Box::new(error),
1742                        message: format!(
1743                            "{error_message} while paying invoice with contract id {contract_id}"
1744                        ),
1745                    }));
1746                }
1747                GatewayExtPayStates::Canceled { error } => {
1748                    return Err(PublicGatewayError::LNv1(LNv1Error::OutgoingContract {
1749                        error: Box::new(error.clone()),
1750                        message: format!(
1751                            "Cancelled with {error} while paying invoice with contract id {contract_id}"
1752                        ),
1753                    }));
1754                }
1755                GatewayExtPayStates::Created => {
1756                    debug!(target: LOG_GATEWAY, contract_id = %contract_id, "Start pay invoice state machine");
1757                }
1758                other => {
1759                    debug!(target: LOG_GATEWAY, state = ?other, contract_id = %contract_id, "Got state while paying invoice");
1760                }
1761            }
1762        }
1763
1764        Err(PublicGatewayError::LNv1(LNv1Error::OutgoingPayment(
1765            anyhow!("Ran out of state updates while paying invoice"),
1766        )))
1767    }
1768
1769    /// Handles a request for the gateway to backup a connected federation's
1770    /// ecash.
1771    pub async fn handle_backup_msg(
1772        &self,
1773        BackupPayload { federation_id }: BackupPayload,
1774    ) -> AdminResult<()> {
1775        let federation_manager = self.federation_manager.read().await;
1776        let client = federation_manager
1777            .client(&federation_id)
1778            .ok_or(AdminGatewayError::ClientCreationError(anyhow::anyhow!(
1779                format!("Gateway has not connected to {federation_id}")
1780            )))?
1781            .value();
1782        let metadata: BTreeMap<String, String> = BTreeMap::new();
1783        #[allow(deprecated)]
1784        client
1785            .backup_to_federation(fedimint_client::backup::Metadata::from_json_serialized(
1786                metadata,
1787            ))
1788            .await
1789            .map_err(anyhow::Error::from)?;
1790        Ok(())
1791    }
1792
1793    /// Trigger rechecking for deposits on an address
1794    pub async fn handle_recheck_address_msg(
1795        &self,
1796        payload: DepositAddressRecheckPayload,
1797    ) -> AdminResult<()> {
1798        let client = self.select_client(payload.federation_id).await?;
1799
1800        if let Ok(wallet_module) = client.value().get_first_module::<WalletClientModule>() {
1801            wallet_module
1802                .recheck_pegin_address_by_address(payload.address)
1803                .await
1804                .map_err(|e| AdminGatewayError::Unexpected(e.into()))?;
1805            Ok(())
1806        } else if client
1807            .value()
1808            .get_first_module::<fedimint_walletv2_client::WalletClientModule>()
1809            .is_ok()
1810        {
1811            // Walletv2 auto-claims deposits, so this is a no-op
1812            Ok(())
1813        } else {
1814            Err(AdminGatewayError::Unexpected(anyhow!(
1815                "No wallet module found"
1816            )))
1817        }
1818    }
1819
1820    /// Handles a request to receive ecash into the gateway.
1821    pub async fn handle_receive_ecash_msg(
1822        &self,
1823        payload: ReceiveEcashPayload,
1824    ) -> Result<ReceiveEcashResponse> {
1825        // Extract federation_id_prefix from either format
1826        let federation_id_prefix = base32::decode_prefixed::<fedimint_mintv2_client::ECash>(
1827            FEDIMINT_PREFIX,
1828            &payload.notes,
1829        )
1830        .ok()
1831        .and_then(|e| e.mint())
1832        .map(|id| id.to_prefix())
1833        .or_else(|| {
1834            OOBNotes::from_str(&payload.notes)
1835                .ok()
1836                .map(|n| n.federation_id_prefix())
1837        })
1838        .ok_or_else(|| PublicGatewayError::ReceiveEcashError {
1839            failure_reason: "Invalid ecash format: could not parse as ECash or OOBNotes"
1840                .to_string(),
1841        })?;
1842
1843        let client = self
1844            .federation_manager
1845            .read()
1846            .await
1847            .get_client_for_federation_id_prefix(federation_id_prefix)
1848            .ok_or(FederationNotConnected {
1849                federation_id_prefix,
1850            })?;
1851
1852        // Check which module is present and parse accordingly
1853        if let Ok(mint) = client.value().get_first_module::<MintClientModule>() {
1854            let notes = OOBNotes::from_str(&payload.notes).map_err(|e| {
1855                PublicGatewayError::ReceiveEcashError {
1856                    failure_reason: format!(
1857                        "Expected OOBNotes for MintV1 federation: {}",
1858                        e.fmt_compact()
1859                    ),
1860                }
1861            })?;
1862            let amount = notes.total_amount();
1863
1864            let operation_id = mint.reissue_external_notes(notes, ()).await.map_err(|e| {
1865                PublicGatewayError::ReceiveEcashError {
1866                    failure_reason: e.fmt_compact().to_string(),
1867                }
1868            })?;
1869
1870            let mut updates = mint
1871                .subscribe_reissue_external_notes(operation_id)
1872                .await
1873                .map_err(|e| PublicGatewayError::ReceiveEcashError {
1874                    failure_reason: format!(
1875                        "Could not subscribe to reissue operation: {}",
1876                        e.fmt_compact()
1877                    ),
1878                })?
1879                .into_stream();
1880
1881            // Only `Done` and `Failed` are terminal for this stream. Ending on
1882            // `Created` or `Issuing` means the outputs were never finalized, so
1883            // their blind signatures were never verified, and reporting the notes'
1884            // claimed amount would credit e-cash the gateway does not hold.
1885            let mut reissued = false;
1886            while let Some(update) = updates.next().await {
1887                match update {
1888                    ReissueExternalNotesState::Failed(failure_reason) => {
1889                        return Err(PublicGatewayError::ReceiveEcashError { failure_reason });
1890                    }
1891                    ReissueExternalNotesState::Done => reissued = true,
1892                    ReissueExternalNotesState::Created | ReissueExternalNotesState::Issuing => {}
1893                }
1894            }
1895
1896            if !reissued {
1897                return Err(PublicGatewayError::ReceiveEcashError {
1898                    failure_reason: "Reissue operation ended before the notes were reissued"
1899                        .to_string(),
1900                });
1901            }
1902
1903            Ok(ReceiveEcashResponse { amount })
1904        } else if let Ok(mint) = client
1905            .value()
1906            .get_primary_module_for_unit::<MintV2ClientModule>(
1907                fedimint_core::module::AmountUnit::BITCOIN,
1908            )
1909        {
1910            let ecash: fedimint_mintv2_client::ECash =
1911                base32::decode_prefixed(FEDIMINT_PREFIX, &payload.notes).map_err(|e| {
1912                    PublicGatewayError::ReceiveEcashError {
1913                        failure_reason: format!("Expected ECash for MintV2 federation: {e}"),
1914                    }
1915                })?;
1916            let amount = ecash.amount();
1917
1918            let operation_id = mint
1919                .receive(ecash, serde_json::Value::Null)
1920                .await
1921                .map_err(|e| PublicGatewayError::ReceiveEcashError {
1922                    failure_reason: e.fmt_compact().to_string(),
1923                })?;
1924
1925            let final_state = mint
1926                .await_final_receive_operation_state(operation_id)
1927                .await
1928                .map_err(|e| PublicGatewayError::ReceiveEcashError {
1929                    failure_reason: e.fmt_compact().to_string(),
1930                })?;
1931            match final_state {
1932                fedimint_mintv2_client::FinalReceiveOperationState::Success => {}
1933                fedimint_mintv2_client::FinalReceiveOperationState::Rejected => {
1934                    return Err(PublicGatewayError::ReceiveEcashError {
1935                        failure_reason: "ECash receive was rejected".to_string(),
1936                    });
1937                }
1938            }
1939
1940            Ok(ReceiveEcashResponse { amount })
1941        } else {
1942            Err(PublicGatewayError::ReceiveEcashError {
1943                failure_reason: "No mint module found".to_string(),
1944            })
1945        }
1946    }
1947
1948    /// Retrieves an invoice by the payment hash if it exists, otherwise returns
1949    /// `None`.
1950    pub async fn handle_get_invoice_msg(
1951        &self,
1952        payload: GetInvoiceRequest,
1953    ) -> AdminResult<Option<GetInvoiceResponse>> {
1954        let lightning_context = self.get_lightning_context().await?;
1955        let invoice = lightning_context.lnrpc.get_invoice(payload).await?;
1956        Ok(invoice)
1957    }
1958
1959    /// Withdraws ecash from a federation and pegs-out to the Lightning node's
1960    /// onchain wallet
1961    pub async fn handle_withdraw_to_onchain_msg(
1962        &self,
1963        payload: WithdrawToOnchainPayload,
1964    ) -> AdminResult<WithdrawResponse> {
1965        let address = self.handle_get_ln_onchain_address_msg().await?;
1966        let withdraw = WithdrawPayload {
1967            address: address.into_unchecked(),
1968            federation_id: payload.federation_id,
1969            amount: payload.amount,
1970            quoted_fees: None,
1971        };
1972        self.handle_withdraw_msg(withdraw).await
1973    }
1974
1975    /// Deposits the specified amount from the gateway's onchain wallet into the
1976    /// Federation's ecash wallet
1977    pub async fn handle_pegin_from_onchain_msg(
1978        &self,
1979        payload: PeginFromOnchainPayload,
1980    ) -> AdminResult<Txid> {
1981        let deposit = DepositAddressPayload {
1982            federation_id: payload.federation_id,
1983        };
1984        let address = self.handle_address_msg(deposit).await?;
1985        let send_onchain = SendOnchainRequest {
1986            address: address.into_unchecked(),
1987            amount: payload.amount,
1988            fee_rate_sats_per_vbyte: payload.fee_rate_sats_per_vbyte,
1989        };
1990        let txid = self.handle_send_onchain_msg(send_onchain).await?;
1991
1992        Ok(txid)
1993    }
1994
1995    /// Registers the gateway with each specified federation.
1996    ///
1997    /// Does nothing once the gateway is shutting down: the lightning connection
1998    /// loop keeps reconnecting while `handle_shutdown_msg` drains the payments
1999    /// that are still in flight, and re-announcing there would advertise a
2000    /// route that is about to disappear, undoing the
2001    /// `unannounce_from_all_federations` the shutdown just performed.
2002    async fn register_federations(
2003        &self,
2004        federations: &BTreeMap<FederationId, FederationConfig>,
2005        register_task_group: &TaskGroup,
2006    ) {
2007        if let GatewayState::ShuttingDown { .. } = self.get_state().await {
2008            info!(
2009                target: LOG_GATEWAY,
2010                "Gateway is shutting down, skipping federation registration"
2011            );
2012            return;
2013        }
2014
2015        if let Ok(lightning_context) = self.get_lightning_context().await {
2016            let route_hints = lightning_context
2017                .lnrpc
2018                .parsed_route_hints(self.num_route_hints)
2019                .await;
2020            if route_hints.is_empty() {
2021                warn!(target: LOG_GATEWAY, "Gateway did not retrieve any route hints, may reduce receive success rate.");
2022            }
2023
2024            for (federation_id, federation_config) in federations {
2025                // A fee that predates the fee limits may be too large to announce. Skip
2026                // that federation rather than failing the whole registration pass: this
2027                // runs on the root task group at startup, so an error here would keep
2028                // the gateway from booting at all.
2029                let routing_fees = match RoutingFees::try_from(federation_config.lightning_fee) {
2030                    Ok(routing_fees) => routing_fees,
2031                    Err(err) => {
2032                        warn!(
2033                            target: LOG_GATEWAY,
2034                            %federation_id,
2035                            err = %err.fmt_compact(),
2036                            "Skipping registration, the configured lightning fee cannot be announced. Set a smaller fee with `set_fees`."
2037                        );
2038                        continue;
2039                    }
2040                };
2041
2042                let fed_manager = self.federation_manager.read().await;
2043                if let Some(client) = fed_manager.client(federation_id) {
2044                    let federation_id = *federation_id;
2045                    let client_arc = client.clone().into_value();
2046                    let route_hints = route_hints.clone();
2047                    let lightning_context = lightning_context.clone();
2048                    let registration_health = self.registration_health.clone();
2049                    let registrations = self
2050                        .registrations
2051                        .clone()
2052                        .into_iter()
2053                        .map(|(protocol, registration)| {
2054                            let attempt =
2055                                registration_health.begin_lnv1_attempt(federation_id, protocol);
2056                            (registration, attempt)
2057                        })
2058                        .collect::<Vec<_>>();
2059
2060                    register_task_group.spawn_cancellable_silent(
2061                        "register federation",
2062                        async move {
2063                            let Ok(gateway_client) =
2064                                client_arc.get_first_module::<GatewayClientModule>()
2065                            else {
2066                                return;
2067                            };
2068
2069                            for (registration, attempt) in registrations {
2070                                let succeeded = gateway_client
2071                                    .try_register_with_federation(
2072                                        route_hints.clone(),
2073                                        GW_ANNOUNCEMENT_TTL,
2074                                        routing_fees,
2075                                        lightning_context.clone(),
2076                                        registration.endpoint_url,
2077                                        registration.keypair,
2078                                    )
2079                                    .await;
2080                                registration_health
2081                                    .complete_attempt(
2082                                        attempt,
2083                                        succeeded,
2084                                        fedimint_core::time::now(),
2085                                        fedimint_core::runtime::Instant::now(),
2086                                    )
2087                                    .await;
2088                            }
2089                        },
2090                    );
2091                }
2092            }
2093        }
2094    }
2095
2096    /// Retrieves a `ClientHandleArc` from the Gateway's in memory structures
2097    /// that keep track of available clients, given a `federation_id`.
2098    pub async fn select_client(
2099        &self,
2100        federation_id: FederationId,
2101    ) -> std::result::Result<Spanned<fedimint_client::ClientHandleArc>, FederationNotConnected>
2102    {
2103        self.federation_manager
2104            .read()
2105            .await
2106            .client(&federation_id)
2107            .cloned()
2108            .ok_or(FederationNotConnected {
2109                federation_id_prefix: federation_id.to_prefix(),
2110            })
2111    }
2112
2113    async fn load_mnemonic(gateway_db: &Database) -> Option<Mnemonic> {
2114        let secret = Client::load_decodable_client_secret::<Vec<u8>>(gateway_db)
2115            .await
2116            .ok()?;
2117        Mnemonic::from_entropy(&secret).ok()
2118    }
2119
2120    /// Reads the connected federation client configs from the Gateway's
2121    /// database and reconstructs the clients necessary for interacting with
2122    /// connection federations.
2123    async fn load_clients(&self) -> AdminResult<()> {
2124        if let GatewayState::NotConfigured { .. } = self.get_state().await {
2125            return Ok(());
2126        }
2127
2128        let mut federation_manager = self.federation_manager.write().await;
2129
2130        let configs = {
2131            let mut dbtx = self.gateway_db.begin_transaction_nc().await;
2132            dbtx.load_federation_configs().await
2133        };
2134
2135        if let Some(max_federation_index) = configs.values().map(|cfg| cfg.federation_index).max() {
2136            federation_manager.set_next_index(max_federation_index + 1);
2137        }
2138
2139        let mnemonic = Self::load_mnemonic(&self.gateway_db)
2140            .await
2141            .expect("mnemonic should be set");
2142
2143        for (federation_id, config) in configs {
2144            let federation_index = config.federation_index;
2145            match Box::pin(Spanned::try_new(
2146                info_span!(target: LOG_GATEWAY, "client", federation_id  = %federation_id.clone()),
2147                self.client_builder
2148                    .build(config, Arc::new(self.clone()), &mnemonic),
2149            ))
2150            .await
2151            {
2152                Ok(client) => {
2153                    federation_manager.add_client(federation_index, client);
2154                }
2155                _ => {
2156                    warn!(target: LOG_GATEWAY, federation_id = %federation_id, "Failed to load client");
2157                }
2158            }
2159        }
2160
2161        Ok(())
2162    }
2163
2164    /// Legacy mechanism for registering the Gateway with connected federations.
2165    /// This will spawn a task that will re-register the Gateway with
2166    /// connected federations every 8.5 mins. Only registers the Gateway if it
2167    /// has successfully connected to the Lightning node, so that it can
2168    /// include route hints in the registration.
2169    fn register_clients_timer(&self) {
2170        // Only spawn background registration thread if gateway is LND
2171        if matches!(self.lightning_mode, LightningMode::Lnd { .. }) {
2172            info!(target: LOG_GATEWAY, "Spawning register task...");
2173            let gateway = self.clone();
2174            let register_task_group = self.task_group.make_subgroup();
2175            self.task_group.spawn_cancellable("register clients", async move {
2176                loop {
2177                    let gateway_state = gateway.get_state().await;
2178                    if let GatewayState::Running { .. } = &gateway_state {
2179                        let mut dbtx = gateway.gateway_db.begin_transaction_nc().await;
2180                        let all_federations_configs = dbtx.load_federation_configs().await.into_iter().collect();
2181                        gateway.register_federations(&all_federations_configs, &register_task_group).await;
2182                    } else {
2183                        // We need to retry more often if the gateway is not in the Running state
2184                        const NOT_RUNNING_RETRY: Duration = Duration::from_secs(10);
2185                        warn!(target: LOG_GATEWAY, gateway_state = %gateway_state, retry_interval = ?NOT_RUNNING_RETRY, "Will not register federation yet because gateway still not in Running state");
2186                        sleep(NOT_RUNNING_RETRY).await;
2187                        continue;
2188                    }
2189
2190                    // Allow a 15% buffer of the TTL before the re-registering gateway
2191                    // with the federations.
2192                    sleep(GW_ANNOUNCEMENT_TTL.mul_f32(0.85)).await;
2193                }
2194            });
2195        }
2196    }
2197
2198    /// Verifies that the federation has at least one lightning module (LNv1 or
2199    /// LNv2) and that the network matches the gateway's network.
2200    async fn check_federation_network(
2201        client: &ClientHandleArc,
2202        network: Network,
2203    ) -> AdminResult<()> {
2204        let federation_id = client.federation_id();
2205        let config = client.config().await;
2206
2207        let lnv1_cfg = config
2208            .modules
2209            .values()
2210            .find(|m| LightningCommonInit::KIND == m.kind);
2211
2212        let lnv2_cfg = config
2213            .modules
2214            .values()
2215            .find(|m| fedimint_lnv2_common::LightningCommonInit::KIND == m.kind);
2216
2217        // Ensure the federation has at least one lightning module
2218        if lnv1_cfg.is_none() && lnv2_cfg.is_none() {
2219            return Err(AdminGatewayError::ClientCreationError(anyhow!(
2220                "Federation {federation_id} does not have any lightning module (LNv1 or LNv2)"
2221            )));
2222        }
2223
2224        // Verify the LNv1 network if present
2225        if let Some(cfg) = lnv1_cfg {
2226            let ln_cfg: &LightningClientConfig = cfg.cast().map_err(anyhow::Error::from)?;
2227
2228            if ln_cfg.network.0 != network {
2229                crit!(
2230                    target: LOG_GATEWAY,
2231                    federation_id = %federation_id,
2232                    network = %network,
2233                    "Incorrect LNv1 network for federation",
2234                );
2235                return Err(AdminGatewayError::ClientCreationError(anyhow!(format!(
2236                    "Unsupported LNv1 network {}",
2237                    ln_cfg.network
2238                ))));
2239            }
2240        }
2241
2242        // Verify the LNv2 network if present
2243        if let Some(cfg) = lnv2_cfg {
2244            let ln_cfg: &fedimint_lnv2_common::config::LightningClientConfig =
2245                cfg.cast().map_err(anyhow::Error::from)?;
2246
2247            if ln_cfg.network != network {
2248                crit!(
2249                    target: LOG_GATEWAY,
2250                    federation_id = %federation_id,
2251                    network = %network,
2252                    "Incorrect LNv2 network for federation",
2253                );
2254                return Err(AdminGatewayError::ClientCreationError(anyhow!(format!(
2255                    "Unsupported LNv2 network {}",
2256                    ln_cfg.network
2257                ))));
2258            }
2259        }
2260
2261        Ok(())
2262    }
2263
2264    /// Checks the Gateway's current state and returns the proper
2265    /// `LightningContext` if it is available.
2266    ///
2267    /// The error is synthesised from the gateway's own state: no RPC is
2268    /// attempted, so `Err` means "this process does not currently hold a
2269    /// session with the lightning node", never "the lightning node was asked
2270    /// and answered no". Callers that would turn a failure here into a
2271    /// decision about a payment must use `await_lightning_context`
2272    /// instead.
2273    pub async fn get_lightning_context(
2274        &self,
2275    ) -> std::result::Result<LightningContext, LightningRpcError> {
2276        match self.get_state().await {
2277            GatewayState::Running { lightning_context }
2278            | GatewayState::ShuttingDown { lightning_context } => Ok(lightning_context),
2279            _ => Err(LightningRpcError::FailedToConnect),
2280        }
2281    }
2282
2283    /// Waits until the gateway holds a `LightningContext` and returns it.
2284    ///
2285    /// The lightning node is the only oracle for whether an HTLC of ours is in
2286    /// flight, so code deciding the fate of a payment must actually ask it.
2287    /// [`Self::get_lightning_context`] cannot stand in for that: its `Err` is
2288    /// produced locally, and the gateway spends part of every startup without
2289    /// a context. [`Self::run`] awaits `load_clients` before `start_gateway`,
2290    /// and building a client starts its executor, so payment state machines
2291    /// persisted across a restart re-enter while the state is still
2292    /// `Disconnected`. Reading that as a payment failure cancels an outgoing
2293    /// contract whose HTLC the previous process may already have settled,
2294    /// leaving the gateway out of pocket for a payment it did make.
2295    ///
2296    /// Waiting is the conservative side of that trade. It ends when the
2297    /// gateway connects, or when the caller is dropped: every caller runs
2298    /// inside a client state machine transition or a webserver request, both
2299    /// of which are cancelled when the gateway shuts down. It does not strand
2300    /// the payer either, since the outgoing contract's timelock refunds them
2301    /// without the gateway's cooperation, whereas a cancellation is final (see
2302    /// `LightningInput` processing in `fedimint-ln-server`).
2303    async fn await_lightning_context(&self) -> LightningContext {
2304        loop {
2305            match self.get_lightning_context().await {
2306                Ok(lightning_context) => return lightning_context,
2307                Err(err) => {
2308                    let state = self.get_state().await;
2309
2310                    warn!(
2311                        target: LOG_GATEWAY,
2312                        err = %err.fmt_compact(),
2313                        %state,
2314                        retry_interval_secs = LIGHTNING_CONTEXT_RETRY_INTERVAL.as_secs(),
2315                        "Not connected to the lightning node, waiting before asking it again",
2316                    );
2317
2318                    sleep(LIGHTNING_CONTEXT_RETRY_INTERVAL).await;
2319                }
2320            }
2321        }
2322    }
2323
2324    /// Iterates through all of the federations the gateway is registered with
2325    /// and requests to remove the registration record.
2326    pub async fn unannounce_from_all_federations(&self) {
2327        if matches!(self.lightning_mode, LightningMode::Lnd { .. }) {
2328            for registration in self.registrations.values() {
2329                self.federation_manager
2330                    .read()
2331                    .await
2332                    .unannounce_from_all_federations(registration.keypair)
2333                    .await;
2334            }
2335        }
2336    }
2337
2338    async fn create_lightning_client(
2339        &self,
2340        runtime: Arc<tokio::runtime::Runtime>,
2341        mnemonic_source: MnemonicSource,
2342    ) -> Box<dyn ILnRpcClient> {
2343        match self.lightning_mode.clone() {
2344            LightningMode::Lnd {
2345                lnd_rpc_addr,
2346                lnd_tls_cert,
2347                lnd_macaroon,
2348                lnd_time_pref,
2349                lnd_payment_timeout_secs,
2350            } => {
2351                // The LND backend uses this to ignore HOLD invoices on the
2352                // shared LND node that aren't federation-bound. Returns true
2353                // iff there is a registered LNv2 incoming contract for the
2354                // given payment hash.
2355                let gateway_db = self.gateway_db.clone();
2356                let lnv2_filter: Lnv2HoldInvoiceFilter = Arc::new(move |hash| {
2357                    let gateway_db = gateway_db.clone();
2358                    Box::pin(async move {
2359                        gateway_db
2360                            .begin_transaction_nc()
2361                            .await
2362                            .load_registered_incoming_contract(PaymentImage::Hash(hash))
2363                            .await
2364                            .is_some()
2365                    })
2366                });
2367
2368                Box::new(GatewayLndClient::new(
2369                    lnd_rpc_addr,
2370                    lnd_tls_cert,
2371                    lnd_macaroon,
2372                    lnd_time_pref,
2373                    lnd_payment_timeout_secs,
2374                    None,
2375                    lnv2_filter,
2376                ))
2377            }
2378            LightningMode::Ldk {
2379                lightning_port,
2380                alias,
2381            } => {
2382                let mnemonic = Self::load_mnemonic(&self.gateway_db)
2383                    .await
2384                    .expect("mnemonic should be set");
2385                // Retrieving the fees inside of LDK can sometimes fail/time out. To prevent
2386                // crashing the gateway, we wait a bit and just try
2387                // to re-create the client. The gateway cannot proceed until this succeeds.
2388                retry("create LDK Node", fibonacci_max_one_hour(), || async {
2389                    ldk::GatewayLdkClient::new(
2390                        &self.client_builder.data_dir().join(LDK_NODE_DB_FOLDER),
2391                        self.chain_source.clone(),
2392                        self.network,
2393                        lightning_port,
2394                        alias.clone(),
2395                        mnemonic.clone(),
2396                        mnemonic_source,
2397                        runtime.clone(),
2398                    )
2399                    .map(Box::new)
2400                    // `retry` logs each failure with `{:#}`, which prints the
2401                    // cause chain only for `anyhow::Error`.
2402                    .map_err(anyhow::Error::from)
2403                })
2404                .await
2405                .expect("Could not create LDK Node")
2406            }
2407        }
2408    }
2409}
2410
2411#[async_trait]
2412impl IAdminGateway for Gateway {
2413    type Error = AdminGatewayError;
2414
2415    /// Returns information about the Gateway back to the client when requested
2416    /// via the webserver.
2417    async fn handle_get_info(&self) -> AdminResult<GatewayInfo> {
2418        let GatewayState::Running { lightning_context } = self.get_state().await else {
2419            return Ok(GatewayInfo {
2420                federations: vec![],
2421                federation_fake_scids: None,
2422                version_hash: fedimint_build_code_version_env!().to_string(),
2423                gateway_state: self.state.read().await.to_string(),
2424                lightning_info: LightningInfo::NotConnected,
2425                lightning_mode: self.lightning_mode.clone(),
2426                registrations: self
2427                    .registrations
2428                    .iter()
2429                    .map(|(k, v)| (k.clone(), (v.endpoint_url.clone(), v.keypair.public_key())))
2430                    .collect(),
2431            });
2432        };
2433
2434        let dbtx = self.gateway_db.begin_transaction_nc().await;
2435        let federations = self
2436            .federation_manager
2437            .read()
2438            .await
2439            .federation_info_all_federations(dbtx)
2440            .await;
2441
2442        let channels: BTreeMap<u64, FederationId> = federations
2443            .iter()
2444            .map(|federation_info| {
2445                (
2446                    federation_info.config.federation_index,
2447                    federation_info.federation_id,
2448                )
2449            })
2450            .collect();
2451
2452        let lightning_info = lightning_context.lnrpc.parsed_node_info().await;
2453
2454        Ok(GatewayInfo {
2455            federations,
2456            federation_fake_scids: Some(channels),
2457            version_hash: fedimint_build_code_version_env!().to_string(),
2458            gateway_state: self.state.read().await.to_string(),
2459            lightning_info,
2460            lightning_mode: self.lightning_mode.clone(),
2461            registrations: self
2462                .registrations
2463                .iter()
2464                .map(|(k, v)| (k.clone(), (v.endpoint_url.clone(), v.keypair.public_key())))
2465                .collect(),
2466        })
2467    }
2468
2469    /// Returns a list of Lightning network channels from the Gateway's
2470    /// Lightning node.
2471    async fn handle_list_channels_msg(
2472        &self,
2473    ) -> AdminResult<Vec<fedimint_gateway_common::ChannelInfo>> {
2474        let context = self.get_lightning_context().await?;
2475        let response = context.lnrpc.list_channels().await?;
2476        Ok(response.channels)
2477    }
2478
2479    /// Computes the 24 hour payment summary statistics for this gateway.
2480    /// Combines the LNv1 and LNv2 stats together.
2481    async fn handle_payment_summary_msg(
2482        &self,
2483        PaymentSummaryPayload {
2484            start_millis,
2485            end_millis,
2486        }: PaymentSummaryPayload,
2487    ) -> AdminResult<PaymentSummaryResponse> {
2488        let federation_manager = self.federation_manager.read().await;
2489        let fed_configs = federation_manager.get_all_federation_configs().await;
2490        let federation_ids = fed_configs.keys().collect::<Vec<_>>();
2491        let start = UNIX_EPOCH + Duration::from_millis(start_millis);
2492        let end = UNIX_EPOCH + Duration::from_millis(end_millis);
2493
2494        if start > end {
2495            return Err(AdminGatewayError::Unexpected(anyhow!("Invalid time range")));
2496        }
2497
2498        let mut outgoing = StructuredPaymentEvents::default();
2499        let mut incoming = StructuredPaymentEvents::default();
2500        for fed_id in federation_ids {
2501            let client = federation_manager
2502                .client(fed_id)
2503                .expect("No client available")
2504                .value();
2505            let all_events = &get_events_for_duration(client, start, end).await;
2506
2507            let (mut lnv1_outgoing, mut lnv1_incoming) = compute_lnv1_stats(all_events);
2508            let (mut lnv2_outgoing, mut lnv2_incoming) = compute_lnv2_stats(all_events);
2509            outgoing.combine(&mut lnv1_outgoing);
2510            incoming.combine(&mut lnv1_incoming);
2511            outgoing.combine(&mut lnv2_outgoing);
2512            incoming.combine(&mut lnv2_incoming);
2513        }
2514
2515        Ok(PaymentSummaryResponse {
2516            outgoing: PaymentStats::compute(&outgoing),
2517            incoming: PaymentStats::compute(&incoming),
2518        })
2519    }
2520
2521    /// Handle a request to have the Gateway leave a federation. The Gateway
2522    /// will request the federation to remove the registration record and
2523    /// the gateway will remove the configuration needed to construct the
2524    /// federation client.
2525    async fn handle_leave_federation(
2526        &self,
2527        payload: LeaveFedPayload,
2528    ) -> AdminResult<FederationInfo> {
2529        // Lock the federation manager before starting the db transaction to reduce the
2530        // chance of db write conflicts.
2531        let mut federation_manager = self.federation_manager.write().await;
2532        let mut dbtx = self.gateway_db.begin_transaction().await;
2533
2534        let federation_info = federation_manager
2535            .leave_federation(
2536                payload.federation_id,
2537                &mut dbtx.to_ref_nc(),
2538                self.registrations.values().collect(),
2539            )
2540            .await?;
2541
2542        dbtx.remove_federation_config(payload.federation_id).await;
2543        dbtx.commit_tx().await;
2544        self.registration_health
2545            .clear_federation(payload.federation_id)
2546            .await;
2547        Ok(federation_info)
2548    }
2549
2550    /// Handles a connection request to join a new federation. The gateway will
2551    /// download the federation's client configuration, construct a new
2552    /// client, registers, the gateway with the federation, and persists the
2553    /// necessary config to reconstruct the client when restarting the gateway.
2554    async fn handle_connect_federation(
2555        &self,
2556        payload: ConnectFedPayload,
2557    ) -> AdminResult<FederationInfo> {
2558        let GatewayState::Running { lightning_context } = self.get_state().await else {
2559            return Err(AdminGatewayError::Lightning(
2560                LightningRpcError::FailedToConnect,
2561            ));
2562        };
2563
2564        let invite_code = InviteCode::from_str(&payload.invite_code).map_err(|e| {
2565            AdminGatewayError::ClientCreationError(anyhow!(format!(
2566                "Invalid federation member string {e:?}"
2567            )))
2568        })?;
2569
2570        let federation_id = invite_code.federation_id();
2571
2572        let mut federation_manager = self.federation_manager.write().await;
2573
2574        // Check if this federation has already been registered
2575        if federation_manager.has_federation(federation_id) {
2576            return Err(AdminGatewayError::ClientCreationError(anyhow!(
2577                "Federation has already been registered"
2578            )));
2579        }
2580
2581        // The gateway deterministically assigns a unique identifier (u64) to each
2582        // federation connected.
2583        let federation_index = federation_manager.pop_next_index()?;
2584
2585        let federation_config = FederationConfig {
2586            invite_code,
2587            federation_index,
2588            lightning_fee: self.default_routing_fees,
2589            transaction_fee: self.default_transaction_fees,
2590            payment_policies: BTreeSet::new(),
2591            // Note: deprecated, unused
2592            _connector: ConnectorType::Tcp,
2593        };
2594
2595        // The default fees are validated at startup, so this only fails if the gateway
2596        // is running with fees that predate that check. Refuse to join a federation
2597        // whose fee could not be announced rather than persisting its config.
2598        let routing_fees = RoutingFees::try_from(federation_config.lightning_fee)
2599            .map_err(|err| AdminGatewayError::GatewayConfigurationError(err.to_string()))?;
2600
2601        let mnemonic = Self::load_mnemonic(&self.gateway_db)
2602            .await
2603            .expect("mnemonic should be set");
2604        let recover = payload.recover.unwrap_or(false);
2605        if recover {
2606            self.client_builder
2607                .recover(federation_config.clone(), Arc::new(self.clone()), &mnemonic)
2608                .await?;
2609        }
2610
2611        let client = self
2612            .client_builder
2613            .build(federation_config.clone(), Arc::new(self.clone()), &mnemonic)
2614            .await?;
2615
2616        if recover {
2617            client.wait_for_all_active_state_machines().await;
2618        }
2619
2620        // Instead of using `FederationManager::federation_info`, we manually create
2621        // federation info here because short channel id is not yet persisted.
2622        let federation_info = FederationInfo {
2623            federation_id,
2624            federation_name: federation_manager.federation_name(&client).await,
2625            balance_msat: client.get_balance_for_btc().await.unwrap_or_else(|err| {
2626                warn!(
2627                    target: LOG_GATEWAY,
2628                    err = %err.fmt_compact(),
2629                    %federation_id,
2630                    "Balance not immediately available after joining/recovering."
2631                );
2632                Amount::default()
2633            }),
2634            config: federation_config.clone(),
2635            last_backup_time: None,
2636        };
2637
2638        Self::check_federation_network(&client, self.network).await?;
2639        if matches!(self.lightning_mode, LightningMode::Lnd { .. })
2640            && let Ok(lnv1) = client.get_first_module::<GatewayClientModule>()
2641        {
2642            for (protocol, registration) in &self.registrations {
2643                let attempt = self
2644                    .registration_health
2645                    .begin_lnv1_attempt(federation_id, protocol.clone());
2646                let succeeded = lnv1
2647                    .try_register_with_federation(
2648                        // Route hints will be updated in the background
2649                        Vec::new(),
2650                        GW_ANNOUNCEMENT_TTL,
2651                        routing_fees,
2652                        lightning_context.clone(),
2653                        registration.endpoint_url.clone(),
2654                        registration.keypair,
2655                    )
2656                    .await;
2657                self.registration_health
2658                    .complete_attempt(
2659                        attempt,
2660                        succeeded,
2661                        fedimint_core::time::now(),
2662                        fedimint_core::runtime::Instant::now(),
2663                    )
2664                    .await;
2665            }
2666        }
2667
2668        // no need to enter span earlier, because connect-fed has a span
2669        federation_manager.add_client(
2670            federation_index,
2671            Spanned::new(
2672                info_span!(target: LOG_GATEWAY, "client", federation_id=%federation_id.clone()),
2673                async { client },
2674            )
2675            .await,
2676        );
2677
2678        let mut dbtx = self.gateway_db.begin_transaction().await;
2679        dbtx.save_federation_config(&federation_config).await;
2680        dbtx.save_federation_backup_record(federation_id, None)
2681            .await;
2682        dbtx.commit_tx().await;
2683        debug!(
2684            target: LOG_GATEWAY,
2685            federation_id = %federation_id,
2686            federation_index = %federation_index,
2687            "Federation connected"
2688        );
2689
2690        Ok(federation_info)
2691    }
2692
2693    /// Handles a request to change the lightning or transaction fees for all
2694    /// federations or a federation specified by the `FederationId`.
2695    async fn handle_set_fees_msg(
2696        &self,
2697        SetFeesPayload {
2698            federation_id,
2699            lightning_base,
2700            lightning_parts_per_million,
2701            transaction_base,
2702            transaction_parts_per_million,
2703        }: SetFeesPayload,
2704    ) -> AdminResult<()> {
2705        let mut dbtx = self.gateway_db.begin_transaction().await;
2706        let mut fed_configs = if let Some(fed_id) = federation_id {
2707            dbtx.load_federation_configs()
2708                .await
2709                .into_iter()
2710                .filter(|(id, _)| *id == fed_id)
2711                .collect::<BTreeMap<_, _>>()
2712        } else {
2713            dbtx.load_federation_configs().await
2714        };
2715
2716        // Silently succeeding on an unknown federation would let a mistyped id
2717        // pass for fees that were never changed.
2718        if let Some(fed_id) = federation_id
2719            && fed_configs.is_empty()
2720        {
2721            return Err(FederationNotConnected {
2722                federation_id_prefix: fed_id.to_prefix(),
2723            }
2724            .into());
2725        }
2726
2727        let federation_manager = self.federation_manager.read().await;
2728
2729        for (federation_id, config) in &mut fed_configs {
2730            let mut lightning_fee = config.lightning_fee;
2731            if let Some(lightning_base) = lightning_base {
2732                lightning_fee.base = lightning_base;
2733            }
2734
2735            if let Some(lightning_ppm) = lightning_parts_per_million {
2736                lightning_fee.parts_per_million = lightning_ppm;
2737            }
2738
2739            let mut transaction_fee = config.transaction_fee;
2740            if let Some(transaction_base) = transaction_base {
2741                transaction_fee.base = transaction_base;
2742            }
2743
2744            if let Some(transaction_ppm) = transaction_parts_per_million {
2745                transaction_fee.parts_per_million = transaction_ppm;
2746            }
2747
2748            // Changing the fees of a federation the gateway is not connected to is
2749            // rejected, as it was before the fee limits applied to every federation.
2750            federation_manager
2751                .client(federation_id)
2752                .ok_or(FederationNotConnected {
2753                    federation_id_prefix: federation_id.to_prefix(),
2754                })?;
2755
2756            // The limits are enforced for every federation, not just the ones running
2757            // LNv2. An unchecked fee is persisted verbatim and is converted into
2758            // `RoutingFees` on every LNv1 payment and on every registration, including
2759            // the one at startup, so a fee that does not fit into the `u32` components
2760            // of `RoutingFees` would leave the gateway unable to boot.
2761            let send_fees = lightning_fee.checked_add(transaction_fee).ok_or_else(|| {
2762                AdminGatewayError::GatewayConfigurationError(format!(
2763                    "Total Send fees overflowed, they may not exceed {}",
2764                    PaymentFee::SEND_FEE_LIMIT
2765                ))
2766            })?;
2767
2768            // Check if the lightning fee + transaction fee is higher than the send limit
2769            if !send_fees.is_within(&PaymentFee::SEND_FEE_LIMIT) {
2770                return Err(AdminGatewayError::GatewayConfigurationError(format!(
2771                    "Total Send fees exceeded {}",
2772                    PaymentFee::SEND_FEE_LIMIT
2773                )));
2774            }
2775
2776            // Check if the transaction fee is higher than the receive limit
2777            if !transaction_fee.is_within(&PaymentFee::RECEIVE_FEE_LIMIT) {
2778                return Err(AdminGatewayError::GatewayConfigurationError(format!(
2779                    "Transaction fees exceeded RECEIVE LIMIT {}",
2780                    PaymentFee::RECEIVE_FEE_LIMIT
2781                )));
2782            }
2783
2784            config.lightning_fee = lightning_fee;
2785            config.transaction_fee = transaction_fee;
2786            dbtx.save_federation_config(config).await;
2787        }
2788
2789        dbtx.commit_tx().await;
2790
2791        if matches!(self.lightning_mode, LightningMode::Lnd { .. }) {
2792            let register_task_group = TaskGroup::new();
2793
2794            self.register_federations(&fed_configs, &register_task_group)
2795                .await;
2796        }
2797
2798        Ok(())
2799    }
2800
2801    /// Handles a request to change which payments the gateway performs on
2802    /// behalf of the clients of all federations or of the federation specified
2803    /// by the `FederationId`. Like `set_fees`, only the settings present in
2804    /// the payload change.
2805    async fn handle_set_payment_policy_msg(
2806        &self,
2807        SetPaymentPolicyPayload {
2808            federation_id,
2809            receive_enabled,
2810        }: SetPaymentPolicyPayload,
2811    ) -> AdminResult<()> {
2812        // Taken before the policy is read anywhere else: every payment admitted
2813        // under the old policy funds before this returns, and none is admitted
2814        // afterwards.
2815        let _policy_change = self.payment_policy_lock.write().await;
2816
2817        let mut dbtx = self.gateway_db.begin_transaction().await;
2818        let mut fed_configs = if let Some(fed_id) = federation_id {
2819            dbtx.load_federation_configs()
2820                .await
2821                .into_iter()
2822                .filter(|(id, _)| *id == fed_id)
2823                .collect::<BTreeMap<_, _>>()
2824        } else {
2825            dbtx.load_federation_configs().await
2826        };
2827
2828        // Silently succeeding on an unknown federation would let a mistyped id
2829        // pass for a switch that was never flipped.
2830        if let Some(fed_id) = federation_id
2831            && fed_configs.is_empty()
2832        {
2833            return Err(FederationNotConnected {
2834                federation_id_prefix: fed_id.to_prefix(),
2835            }
2836            .into());
2837        }
2838
2839        let federation_manager = self.federation_manager.read().await;
2840
2841        for (federation_id, config) in &mut fed_configs {
2842            federation_manager
2843                .client(federation_id)
2844                .ok_or(FederationNotConnected {
2845                    federation_id_prefix: federation_id.to_prefix(),
2846                })?;
2847
2848            if let Some(receive_enabled) = receive_enabled {
2849                if receive_enabled {
2850                    config
2851                        .payment_policies
2852                        .remove(&PaymentPolicy::ReceivesDisabled);
2853                } else {
2854                    config
2855                        .payment_policies
2856                        .insert(PaymentPolicy::ReceivesDisabled);
2857                }
2858            }
2859
2860            info!(
2861                target: LOG_GATEWAY,
2862                %federation_id,
2863                payment_policies = ?config.payment_policies,
2864                "Updated federation payment policy"
2865            );
2866
2867            dbtx.save_federation_config(config).await;
2868        }
2869
2870        dbtx.commit_tx().await;
2871
2872        Ok(())
2873    }
2874
2875    /// Handles an authenticated request for the gateway's mnemonic. This also
2876    /// returns a vector of federations that are not using the mnemonic
2877    /// backup strategy.
2878    async fn handle_mnemonic_msg(&self) -> AdminResult<MnemonicResponse> {
2879        let mnemonic = Self::load_mnemonic(&self.gateway_db)
2880            .await
2881            .expect("mnemonic should be set");
2882        let words = mnemonic
2883            .words()
2884            .map(std::string::ToString::to_string)
2885            .collect::<Vec<_>>();
2886        let all_federations = self
2887            .federation_manager
2888            .read()
2889            .await
2890            .get_all_federation_configs()
2891            .await
2892            .keys()
2893            .copied()
2894            .collect::<BTreeSet<_>>();
2895        let legacy_federations = self.client_builder.legacy_federations(all_federations);
2896        let mnemonic_response = MnemonicResponse {
2897            mnemonic: words,
2898            legacy_federations,
2899        };
2900        Ok(mnemonic_response)
2901    }
2902
2903    /// Instructs the Gateway's Lightning node to open a channel to a peer
2904    /// specified by `pubkey`.
2905    async fn handle_open_channel_msg(&self, payload: OpenChannelRequest) -> AdminResult<Txid> {
2906        info!(target: LOG_GATEWAY, pubkey = %payload.pubkey, host = %payload.host, amount = %payload.channel_size_sats, "Opening Lightning channel...");
2907        let context = self.get_lightning_context().await?;
2908        let res = context.lnrpc.open_channel(payload).await?;
2909        info!(target: LOG_GATEWAY, txid = %res.funding_txid, "Initiated channel open");
2910        Txid::from_str(&res.funding_txid).map_err(|e| {
2911            AdminGatewayError::Lightning(LightningRpcError::InvalidMetadata {
2912                failure_reason: format!("Received invalid channel funding txid string {e}"),
2913            })
2914        })
2915    }
2916
2917    /// Instructs the Gateway's Lightning node to connect to a peer specified by
2918    /// `pubkey`.
2919    async fn handle_connect_peer_msg(&self, payload: ConnectPeerRequest) -> AdminResult<()> {
2920        info!(
2921            target: LOG_GATEWAY,
2922            pubkey = %payload.node_address.pubkey,
2923            host = %payload.node_address.host_with_port(),
2924            "Connecting to Lightning peer..."
2925        );
2926        let context = self.get_lightning_context().await?;
2927        context.lnrpc.connect_peer(payload).await?;
2928        info!(target: LOG_GATEWAY, "Connected to Lightning peer");
2929        Ok(())
2930    }
2931
2932    /// Instructs the Gateway's Lightning node to close all channels with a peer
2933    /// specified by `pubkey`.
2934    async fn handle_close_channels_with_peer_msg(
2935        &self,
2936        payload: CloseChannelsWithPeerRequest,
2937    ) -> AdminResult<CloseChannelsWithPeerResponse> {
2938        info!(target: LOG_GATEWAY, close_channel_request = %payload, "Closing lightning channel...");
2939        let context = self.get_lightning_context().await?;
2940        let response = context
2941            .lnrpc
2942            .close_channels_with_peer(payload.clone())
2943            .await?;
2944        info!(target: LOG_GATEWAY, close_channel_request = %payload, "Initiated channel closure");
2945        Ok(response)
2946    }
2947
2948    /// Updates the local-side routing fees (base + ppm) on a single channel
2949    /// identified by funding outpoint.
2950    async fn handle_set_channel_fees_msg(&self, payload: SetChannelFeesRequest) -> AdminResult<()> {
2951        info!(
2952            target: LOG_GATEWAY,
2953            funding_outpoint = %payload.funding_outpoint,
2954            base_fee_msat = payload.base_fee_msat,
2955            parts_per_million = payload.parts_per_million,
2956            "Updating channel fees..."
2957        );
2958        let context = self.get_lightning_context().await?;
2959        context.lnrpc.set_channel_fees(payload).await?;
2960        Ok(())
2961    }
2962
2963    /// Returns the ecash, lightning, and onchain balances for the gateway and
2964    /// the gateway's lightning node.
2965    async fn handle_get_balances_msg(&self) -> AdminResult<GatewayBalances> {
2966        let dbtx = self.gateway_db.begin_transaction_nc().await;
2967        let federation_infos = self
2968            .federation_manager
2969            .read()
2970            .await
2971            .federation_info_all_federations(dbtx)
2972            .await;
2973
2974        let ecash_balances: Vec<FederationBalanceInfo> = federation_infos
2975            .iter()
2976            .map(|federation_info| FederationBalanceInfo {
2977                federation_id: federation_info.federation_id,
2978                ecash_balance_msats: Amount {
2979                    msats: federation_info.balance_msat.msats,
2980                },
2981            })
2982            .collect();
2983
2984        let context = self.get_lightning_context().await?;
2985        let lightning_node_balances = context.lnrpc.get_balances().await?;
2986
2987        Ok(GatewayBalances {
2988            onchain_balance_sats: lightning_node_balances.onchain_balance_sats,
2989            lightning_balance_msats: lightning_node_balances.lightning_balance_msats,
2990            ecash_balances,
2991            inbound_lightning_liquidity_msats: lightning_node_balances
2992                .inbound_lightning_liquidity_msats,
2993        })
2994    }
2995
2996    /// Send funds from the gateway's lightning node on-chain wallet.
2997    async fn handle_send_onchain_msg(&self, payload: SendOnchainRequest) -> AdminResult<Txid> {
2998        let context = self.get_lightning_context().await?;
2999        let response = context.lnrpc.send_onchain(payload.clone()).await?;
3000        let txid =
3001            Txid::from_str(&response.txid).map_err(|e| AdminGatewayError::WithdrawError {
3002                failure_reason: format!("Failed to parse withdrawal TXID: {e}"),
3003            })?;
3004        info!(onchain_request = %payload, txid = %txid, "Sent onchain transaction");
3005        Ok(txid)
3006    }
3007
3008    /// Generates an onchain address to fund the gateway's lightning node.
3009    async fn handle_get_ln_onchain_address_msg(&self) -> AdminResult<Address> {
3010        let context = self.get_lightning_context().await?;
3011        let response = context.lnrpc.get_ln_onchain_address().await?;
3012
3013        let address = Address::from_str(&response.address).map_err(|e| {
3014            AdminGatewayError::Lightning(LightningRpcError::InvalidMetadata {
3015                failure_reason: e.to_string(),
3016            })
3017        })?;
3018
3019        address.require_network(self.network).map_err(|e| {
3020            AdminGatewayError::Lightning(LightningRpcError::InvalidMetadata {
3021                failure_reason: e.to_string(),
3022            })
3023        })
3024    }
3025
3026    async fn handle_deposit_address_msg(
3027        &self,
3028        payload: DepositAddressPayload,
3029    ) -> AdminResult<Address> {
3030        self.handle_address_msg(payload).await
3031    }
3032
3033    async fn handle_receive_ecash_msg(
3034        &self,
3035        payload: ReceiveEcashPayload,
3036    ) -> AdminResult<ReceiveEcashResponse> {
3037        Self::handle_receive_ecash_msg(self, payload)
3038            .await
3039            .map_err(|e| AdminGatewayError::Unexpected(anyhow::anyhow!("{e}")))
3040    }
3041
3042    /// Creates an invoice that is directly payable to the gateway's lightning
3043    /// node.
3044    async fn handle_create_invoice_for_operator_msg(
3045        &self,
3046        payload: CreateInvoiceForOperatorPayload,
3047    ) -> AdminResult<Bolt11Invoice> {
3048        let GatewayState::Running { lightning_context } = self.get_state().await else {
3049            return Err(AdminGatewayError::Lightning(
3050                LightningRpcError::FailedToConnect,
3051            ));
3052        };
3053
3054        Bolt11Invoice::from_str(
3055            &lightning_context
3056                .lnrpc
3057                .create_invoice(CreateInvoiceRequest {
3058                    payment_hash: None, /* Empty payment hash indicates an invoice payable
3059                                         * directly to the gateway. */
3060                    amount_msat: payload.amount_msats,
3061                    expiry_secs: payload.expiry_secs.unwrap_or(3600),
3062                    description: payload.description.map(InvoiceDescription::Direct),
3063                })
3064                .await?
3065                .invoice,
3066        )
3067        .map_err(|e| {
3068            AdminGatewayError::Lightning(LightningRpcError::InvalidMetadata {
3069                failure_reason: e.to_string(),
3070            })
3071        })
3072    }
3073
3074    /// Requests the gateway to pay an outgoing LN invoice using its own funds.
3075    /// Returns the payment hash's preimage on success.
3076    async fn handle_pay_invoice_for_operator_msg(
3077        &self,
3078        payload: PayInvoiceForOperatorPayload,
3079    ) -> AdminResult<Preimage> {
3080        // Those are the ldk defaults
3081        const BASE_FEE: u64 = 50;
3082        const FEE_DENOMINATOR: u64 = 100;
3083        const MAX_DELAY: u64 = 1008;
3084
3085        let GatewayState::Running { lightning_context } = self.get_state().await else {
3086            return Err(AdminGatewayError::Lightning(
3087                LightningRpcError::FailedToConnect,
3088            ));
3089        };
3090
3091        let max_fee = BASE_FEE
3092            + payload
3093                .invoice
3094                .amount_milli_satoshis()
3095                .context("Invoice is missing amount")?
3096                .saturating_div(FEE_DENOMINATOR);
3097
3098        let res = lightning_context
3099            .lnrpc
3100            .pay(payload.invoice, MAX_DELAY, Amount::from_msats(max_fee))
3101            .await?;
3102        Ok(res.preimage)
3103    }
3104
3105    /// Lists the transactions that the lightning node has made.
3106    async fn handle_list_transactions_msg(
3107        &self,
3108        payload: ListTransactionsPayload,
3109    ) -> AdminResult<ListTransactionsResponse> {
3110        let lightning_context = self.get_lightning_context().await?;
3111        let response = lightning_context
3112            .lnrpc
3113            .list_transactions(payload.start_secs, payload.end_secs)
3114            .await?;
3115        Ok(response)
3116    }
3117
3118    // Handles a request the spend the gateway's ecash for a given federation.
3119    async fn handle_spend_ecash_msg(
3120        &self,
3121        payload: SpendEcashPayload,
3122    ) -> AdminResult<SpendEcashResponse> {
3123        let client = self
3124            .select_client(payload.federation_id)
3125            .await?
3126            .into_value();
3127
3128        if let Ok(mint_module) = client.get_first_module::<MintClientModule>() {
3129            let notes = mint_module
3130                .send_oob_notes(payload.amount, ())
3131                .await
3132                .map_err(|e| AdminGatewayError::Unexpected(e.into()))?;
3133            debug!(target: LOG_GATEWAY, ?notes, "Spend ecash notes");
3134            Ok(SpendEcashResponse {
3135                notes: notes.to_string(),
3136            })
3137        } else if let Ok(mint_module) = client.get_primary_module_for_unit::<MintV2ClientModule>(
3138            fedimint_core::module::AmountUnit::BITCOIN,
3139        ) {
3140            let (_, ecash) = mint_module
3141                .send(payload.amount, serde_json::Value::Null, true)
3142                .await
3143                .map_err(|e| AdminGatewayError::Unexpected(e.into()))?;
3144
3145            Ok(SpendEcashResponse {
3146                notes: base32::encode_prefixed(FEDIMINT_PREFIX, &ecash),
3147            })
3148        } else {
3149            Err(AdminGatewayError::Unexpected(anyhow::anyhow!(
3150                "No mint module available"
3151            )))
3152        }
3153    }
3154
3155    /// Instructs the gateway to shutdown, but only after all incoming payments
3156    /// have been handled.
3157    async fn handle_shutdown_msg(&self, task_group: TaskGroup) -> AdminResult<()> {
3158        // Take the write lock on the state so that no additional payments are
3159        // processed. `ShuttingDown` is terminal, so the state cannot move back to
3160        // `Running` once this returns.
3161        let was_running = {
3162            let mut state_guard = self.state.write().await;
3163            if let GatewayState::Running { lightning_context } = state_guard.clone() {
3164                *state_guard = GatewayState::ShuttingDown { lightning_context };
3165                true
3166            } else {
3167                false
3168            }
3169        };
3170
3171        // The guard has to be released before waiting. Finishing an incoming payment
3172        // that already bought the preimage from the federation goes through
3173        // `complete_htlc`, which loops on `get_lightning_context` and would block on
3174        // the write guard forever. `/stop` would never return, the HTLC would expire,
3175        // and the gateway would be left having spent ecash for a payment its sender
3176        // gets refunded. `get_lightning_context` accepts `ShuttingDown`, so the
3177        // in-flight payments can complete while the gateway drains.
3178        if was_running {
3179            self.federation_manager
3180                .read()
3181                .await
3182                .wait_for_incoming_payments()
3183                .await?;
3184        }
3185
3186        let tg = task_group.clone();
3187        tg.spawn("Kill Gateway", |_task_handle| async {
3188            if let Err(err) = task_group.shutdown_join_all(Duration::from_mins(3)).await {
3189                warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Error shutting down gateway");
3190            }
3191        });
3192        Ok(())
3193    }
3194
3195    fn get_task_group(&self) -> TaskGroup {
3196        self.task_group.clone()
3197    }
3198
3199    /// Returns a Bitcoin TXID from a peg-out transaction for a specific
3200    /// connected federation.
3201    async fn handle_withdraw_msg(&self, payload: WithdrawPayload) -> AdminResult<WithdrawResponse> {
3202        let WithdrawPayload {
3203            amount,
3204            address,
3205            federation_id,
3206            quoted_fees,
3207        } = payload;
3208
3209        let address_network = get_network_for_address(&address);
3210        let gateway_network = self.network;
3211        let Ok(address) = address.require_network(gateway_network) else {
3212            return Err(AdminGatewayError::WithdrawError {
3213                failure_reason: format!(
3214                    "Gateway is running on network {gateway_network}, but provided withdraw address is for network {address_network}"
3215                ),
3216            });
3217        };
3218
3219        let client = self.select_client(federation_id).await?;
3220
3221        if let Ok(wallet_module) = client
3222            .value()
3223            .get_first_module::<fedimint_walletv2_client::WalletClientModule>()
3224        {
3225            // The UI round-trips the walletv2 flat fee through
3226            // `PegOutFees::from_amount`, so `amount()` recovers it exactly.
3227            let quoted_fee = quoted_fees.map(|fees| fees.amount());
3228            return withdraw_v2(client.value(), &wallet_module, &address, amount, quoted_fee).await;
3229        }
3230
3231        let wallet_module = client
3232            .value()
3233            .get_first_module::<WalletClientModule>()
3234            .map_err(anyhow::Error::from)?;
3235
3236        // If fees are provided (from UI preview flow), use them directly
3237        // Otherwise fetch fees (CLI backwards compatibility)
3238        let (withdraw_amount, fees) = match quoted_fees {
3239            // UI flow: user confirmed these exact values, just use them
3240            Some(fees) => {
3241                let amt = match amount {
3242                    BitcoinAmountOrAll::Amount(a) => a,
3243                    BitcoinAmountOrAll::All => {
3244                        // UI always resolves "all" to specific amount in preview - reject if not
3245                        return Err(AdminGatewayError::WithdrawError {
3246                            failure_reason:
3247                                "Cannot use 'all' with quoted fees - amount must be resolved first"
3248                                    .to_string(),
3249                        });
3250                    }
3251                };
3252                (amt, fees)
3253            }
3254            // CLI flow: fetch fees (existing behavior for backwards compatibility)
3255            None => match amount {
3256                // The on-chain fee is only part of the cost of withdrawing
3257                // everything: funding the peg-out output also incurs the
3258                // federation's per-note fees. The returned fees are quoted at
3259                // the returned amount, so they must be used together.
3260                BitcoinAmountOrAll::All => {
3261                    let balance = client.value().get_balance_for_btc().await.map_err(|err| {
3262                        AdminGatewayError::Unexpected(anyhow!(
3263                            "Balance not available: {}",
3264                            err.fmt_compact()
3265                        ))
3266                    })?;
3267
3268                    wallet_module
3269                        .max_withdrawable_amount(&address, balance)
3270                        .await
3271                        .map_err(|err| AdminGatewayError::WithdrawError {
3272                            failure_reason: format!(
3273                                "Insufficient funds. Balance: {balance}: {}",
3274                                err.fmt_compact()
3275                            ),
3276                        })?
3277                }
3278                BitcoinAmountOrAll::Amount(amount) => (
3279                    amount,
3280                    wallet_module
3281                        .get_withdraw_fees(&address, amount)
3282                        .await
3283                        .map_err(|e| AdminGatewayError::Unexpected(e.into()))?,
3284                ),
3285            },
3286        };
3287
3288        let operation_id = wallet_module
3289            .withdraw(&address, withdraw_amount, fees, ())
3290            .await
3291            .map_err(|e| AdminGatewayError::Unexpected(e.into()))?;
3292        let mut updates = wallet_module
3293            .subscribe_withdraw_updates(operation_id)
3294            .await
3295            .map_err(|e| AdminGatewayError::Unexpected(e.into()))?
3296            .into_stream();
3297
3298        while let Some(update) = updates.next().await {
3299            match update {
3300                WithdrawState::Succeeded(txid) => {
3301                    info!(target: LOG_GATEWAY, amount = %withdraw_amount, address = %address, "Sent funds");
3302                    return Ok(WithdrawResponse { txid, fees });
3303                }
3304                WithdrawState::Failed(e) => {
3305                    return Err(AdminGatewayError::WithdrawError { failure_reason: e });
3306                }
3307                WithdrawState::Created => {}
3308            }
3309        }
3310
3311        Err(AdminGatewayError::WithdrawError {
3312            failure_reason: "Ran out of state updates while withdrawing".to_string(),
3313        })
3314    }
3315
3316    /// Returns a preview of the withdrawal fees without executing the
3317    /// withdrawal. Used by the UI for two-step withdrawal confirmation.
3318    async fn handle_withdraw_preview_msg(
3319        &self,
3320        payload: WithdrawPreviewPayload,
3321    ) -> AdminResult<WithdrawPreviewResponse> {
3322        // Deserialized amounts can bypass BitcoinAmountOrAll::from_str.
3323        // Reject them before converting to millisatoshis or querying either wallet.
3324        if let BitcoinAmountOrAll::Amount(amount) = payload.amount
3325            && amount > bitcoin::Amount::MAX_MONEY
3326        {
3327            return Err(AdminGatewayError::WithdrawError {
3328                failure_reason: "Withdrawal amount exceeds maximum Bitcoin supply".to_string(),
3329            });
3330        }
3331
3332        let gateway_network = self.network;
3333        let address_checked = payload
3334            .address
3335            .clone()
3336            .require_network(gateway_network)
3337            .map_err(|_| AdminGatewayError::WithdrawError {
3338                failure_reason: "Address network mismatch".to_string(),
3339            })?;
3340
3341        let client = self.select_client(payload.federation_id).await?;
3342
3343        let WithdrawDetails {
3344            amount,
3345            mint_fees,
3346            peg_out_fees,
3347            flat_peg_out_fee,
3348        } = match payload.amount {
3349            BitcoinAmountOrAll::All => {
3350                calculate_max_withdrawable(client.value(), &address_checked).await?
3351            }
3352            BitcoinAmountOrAll::Amount(btc_amount) => {
3353                if let Ok(wallet_module) = client.value().get_first_module::<WalletClientModule>() {
3354                    WithdrawDetails {
3355                        amount: btc_amount.into(),
3356                        mint_fees: None,
3357                        peg_out_fees: wallet_module
3358                            .get_withdraw_fees(&address_checked, btc_amount)
3359                            .await
3360                            .map_err(|e| AdminGatewayError::Unexpected(e.into()))?,
3361                        flat_peg_out_fee: false,
3362                    }
3363                } else if let Ok(wallet_module) = client
3364                    .value()
3365                    .get_first_module::<fedimint_walletv2_client::WalletClientModule>(
3366                ) {
3367                    let fee = wallet_module.send_fee().await.map_err(|e| {
3368                        AdminGatewayError::WithdrawError {
3369                            failure_reason: e.fmt_compact().to_string(),
3370                        }
3371                    })?;
3372                    // The wallet output carries the payment plus the on-chain
3373                    // fee; funding it costs the federation fees quoted here.
3374                    let output_amount = btc_amount.checked_add(fee).ok_or_else(|| {
3375                        AdminGatewayError::WithdrawError {
3376                            failure_reason: "Withdrawal amount overflow".to_string(),
3377                        }
3378                    })?;
3379                    let federation_fees = wallet_module
3380                        .send_fee_quote(output_amount)
3381                        .await
3382                        .map_err(|e| AdminGatewayError::WithdrawError {
3383                            failure_reason: e.fmt_compact().to_string(),
3384                        })?
3385                        .total()
3386                        .get_bitcoin();
3387                    WithdrawDetails {
3388                        amount: btc_amount.into(),
3389                        mint_fees: Some(federation_fees),
3390                        peg_out_fees: PegOutFees::from_amount(fee),
3391                        flat_peg_out_fee: true,
3392                    }
3393                } else {
3394                    return Err(AdminGatewayError::Unexpected(anyhow!(
3395                        "No wallet module found"
3396                    )));
3397                }
3398            }
3399        };
3400
3401        let total_cost = amount
3402            .checked_add(peg_out_fees.amount().into())
3403            .and_then(|a| a.checked_add(mint_fees.unwrap_or(Amount::ZERO)))
3404            .ok_or_else(|| AdminGatewayError::Unexpected(anyhow!("Total cost overflow")))?;
3405
3406        Ok(WithdrawPreviewResponse {
3407            withdraw_amount: amount,
3408            address: payload.address.assume_checked().to_string(),
3409            peg_out_fees,
3410            total_cost,
3411            mint_fees,
3412            flat_peg_out_fee,
3413        })
3414    }
3415
3416    /// Queries the client log for payment events and returns to the user.
3417    /// Returns a paginated list of gateway payment-related events, ordered from
3418    /// newest to oldest.
3419    ///
3420    /// If `event_kinds` is empty, only events matching `ALL_GATEWAY_EVENTS`
3421    /// are returned — this is **not** equivalent to "all events". Other
3422    /// internal events (e.g. `tx-created`, `NoteCreated`) share the same event
3423    /// log and consume IDs, so returned event IDs may be non-contiguous.
3424    ///
3425    /// Pagination works backwards from `end_position` (or the log tip if
3426    /// `None`), returning at most `pagination_size` matching events.
3427    async fn handle_payment_log_msg(
3428        &self,
3429        PaymentLogPayload {
3430            end_position,
3431            pagination_size,
3432            federation_id,
3433            event_kinds,
3434        }: PaymentLogPayload,
3435    ) -> AdminResult<PaymentLogResponse> {
3436        const BATCH_SIZE: u64 = 10_000;
3437        let federation_manager = self.federation_manager.read().await;
3438        let client = federation_manager
3439            .client(&federation_id)
3440            .ok_or(FederationNotConnected {
3441                federation_id_prefix: federation_id.to_prefix(),
3442            })?
3443            .value();
3444
3445        // An empty `event_kinds` defaults to gateway payment-related events, not
3446        // "all events". This means returned event IDs may be non-contiguous since
3447        // other internal events share the same ID space.
3448        let event_kinds = if event_kinds.is_empty() {
3449            ALL_GATEWAY_EVENTS.to_vec()
3450        } else {
3451            event_kinds
3452        };
3453
3454        let end_position = if let Some(position) = end_position {
3455            position
3456        } else {
3457            let mut dbtx = client.db().begin_transaction_nc().await;
3458            dbtx.get_next_event_log_id().await
3459        };
3460
3461        let mut start_position = end_position.saturating_sub(BATCH_SIZE);
3462
3463        let mut payment_log = Vec::new();
3464
3465        while payment_log.len() < pagination_size {
3466            let batch = client.get_event_log(Some(start_position), BATCH_SIZE).await;
3467            let mut filtered_batch = batch
3468                .into_iter()
3469                .filter(|e| e.id() <= end_position && event_kinds.contains(&e.as_raw().kind))
3470                .collect::<Vec<_>>();
3471            filtered_batch.reverse();
3472            payment_log.extend(filtered_batch);
3473
3474            // Compute the start position for the next batch query
3475            start_position = start_position.saturating_sub(BATCH_SIZE);
3476
3477            if start_position == EventLogId::LOG_START {
3478                break;
3479            }
3480        }
3481
3482        // Truncate the payment log to the expected pagination size
3483        payment_log.truncate(pagination_size);
3484
3485        Ok(PaymentLogResponse(payment_log))
3486    }
3487
3488    /// Set the gateway's root mnemonic by generating a new one or using the
3489    /// words provided in `SetMnemonicPayload`.
3490    async fn handle_set_mnemonic_msg(&self, payload: SetMnemonicPayload) -> AdminResult<()> {
3491        // The state lock must be held from the `NotConfigured` check until the
3492        // transition to `Disconnected`, otherwise a concurrent call could pass
3493        // the check, lose the race for the seed write, and leave the gateway
3494        // stuck in `NotConfigured` with a seed already stored
3495        let mut state_guard = self.state.write().await;
3496
3497        // Verify the state is NotConfigured
3498        let GatewayState::NotConfigured { mnemonic_sender } = state_guard.clone() else {
3499            return Err(AdminGatewayError::MnemonicError(anyhow!(
3500                "Gateway is not is NotConfigured state"
3501            )));
3502        };
3503
3504        let (mnemonic, mnemonic_source) = if let Some(words) = payload.words {
3505            info!(target: LOG_GATEWAY, "Using user provided mnemonic");
3506            let mnemonic = Mnemonic::parse_in_normalized(Language::English, words.as_str())
3507                .map_err(|e| {
3508                    AdminGatewayError::MnemonicError(anyhow!(format!(
3509                        "Seed phrase provided in environment was invalid {e:?}"
3510                    )))
3511                })?;
3512            (mnemonic, MnemonicSource::Existing)
3513        } else {
3514            debug!(target: LOG_GATEWAY, "Generating mnemonic and writing entropy to client storage");
3515            (
3516                Bip39RootSecretStrategy::<12>::random(&mut OsRng),
3517                MnemonicSource::Generated,
3518            )
3519        };
3520
3521        Client::store_encodable_client_secret(&self.gateway_db, mnemonic.to_entropy())
3522            .await
3523            .map_err(|err| AdminGatewayError::MnemonicError(err.into()))?;
3524
3525        *state_guard = GatewayState::Disconnected;
3526        drop(state_guard);
3527
3528        // Alert the gateway background threads that the mnemonic has been set
3529        let _ = mnemonic_sender.send(mnemonic_source);
3530
3531        Ok(())
3532    }
3533
3534    /// Creates a BOLT12 offer using the gateway's lightning node
3535    async fn handle_create_offer_for_operator_msg(
3536        &self,
3537        payload: CreateOfferPayload,
3538    ) -> AdminResult<CreateOfferResponse> {
3539        let lightning_context = self.get_lightning_context().await?;
3540        let offer = lightning_context.lnrpc.create_offer(
3541            payload.amount,
3542            payload.description,
3543            payload.expiry_secs,
3544            payload.quantity,
3545        )?;
3546        Ok(CreateOfferResponse { offer })
3547    }
3548
3549    /// Pays a BOLT12 offer using the gateway's lightning node
3550    async fn handle_pay_offer_for_operator_msg(
3551        &self,
3552        payload: PayOfferPayload,
3553    ) -> AdminResult<PayOfferResponse> {
3554        let lightning_context = self.get_lightning_context().await?;
3555        let preimage = lightning_context
3556            .lnrpc
3557            .pay_offer(
3558                payload.offer,
3559                payload.quantity,
3560                payload.amount,
3561                payload.payer_note,
3562            )
3563            .await?;
3564        Ok(PayOfferResponse {
3565            preimage: preimage.to_string(),
3566        })
3567    }
3568
3569    /// Returns a `BTreeMap` that is keyed by the `FederationId` and contains
3570    /// all the invite codes (with peer names) for the federation.
3571    async fn handle_export_invite_codes(
3572        &self,
3573    ) -> BTreeMap<FederationId, BTreeMap<PeerId, (String, InviteCode)>> {
3574        let fed_manager = self.federation_manager.read().await;
3575        fed_manager.all_invite_codes().await
3576    }
3577
3578    /// Returns `TieredCounts` which describes the breakdown of notes in the
3579    /// gateway's wallet for the given `FederationId`
3580    async fn handle_get_note_summary_msg(
3581        &self,
3582        federation_id: &FederationId,
3583    ) -> AdminResult<TieredCounts> {
3584        let fed_manager = self.federation_manager.read().await;
3585        fed_manager.get_note_summary(federation_id).await
3586    }
3587
3588    fn get_password_hash(&self) -> String {
3589        self.bcrypt_password_hash.clone()
3590    }
3591
3592    fn gatewayd_version(&self) -> String {
3593        let gatewayd_version = env!("CARGO_PKG_VERSION");
3594        gatewayd_version.to_string()
3595    }
3596
3597    async fn get_chain_source(&self) -> (ChainSource, Network) {
3598        (self.chain_source.clone(), self.network)
3599    }
3600
3601    fn lightning_mode(&self) -> LightningMode {
3602        self.lightning_mode.clone()
3603    }
3604
3605    async fn is_configured(&self) -> bool {
3606        !matches!(self.get_state().await, GatewayState::NotConfigured { .. })
3607    }
3608}
3609
3610// LNv2 Gateway implementation
3611impl Gateway {
3612    /// Retrieves the `PublicKey` of the Gateway module for a given federation
3613    /// for LNv2. This is NOT the same as the `gateway_id`, it is different
3614    /// per-connected federation.
3615    async fn public_key_v2(&self, federation_id: &FederationId) -> Option<PublicKey> {
3616        self.federation_manager
3617            .read()
3618            .await
3619            .client(federation_id)
3620            .and_then(|client| {
3621                // A federation only has to offer one of the two lightning modules, so a
3622                // client we serve over LNv1 may well have no LNv2 module at all.
3623                client
3624                    .value()
3625                    .get_first_module::<GatewayClientModuleV2>()
3626                    .ok()
3627                    .map(|module| module.keypair.public_key())
3628            })
3629    }
3630
3631    /// Whether the gateway currently accepts incoming payments on behalf of
3632    /// the clients of `federation_id`, as set with `set_payment_policy`. A
3633    /// federation without a stored config is treated as accepting them, so the
3634    /// check never turns a missing record into a refused payment.
3635    async fn receive_enabled(&self, federation_id: FederationId) -> bool {
3636        self.gateway_db
3637            .begin_transaction_nc()
3638            .await
3639            .load_federation_config(federation_id)
3640            .await
3641            .is_none_or(|config| config.receive_enabled())
3642    }
3643
3644    /// Admits a fresh incoming payment when the receive policy allows it,
3645    /// returning a guard that holds off a policy change until the caller has
3646    /// funded the payment.
3647    async fn admit_fresh_receive(&self, federation_id: FederationId) -> Option<PaymentPolicyGuard> {
3648        // The policy is read under the guard, so it cannot change between this
3649        // read and the funding the caller performs while holding it.
3650        let guard = self.payment_policy_lock.clone().read_owned().await;
3651
3652        self.receive_enabled(federation_id)
3653            .await
3654            .then(|| PaymentPolicyGuard::new(guard))
3655    }
3656
3657    /// Returns payment information that LNv2 clients can use to instruct this
3658    /// Gateway to pay an invoice or receive a payment.
3659    pub async fn routing_info_v2(
3660        &self,
3661        federation_id: &FederationId,
3662    ) -> Result<Option<RoutingInfo>> {
3663        let context = self.get_lightning_context().await?;
3664
3665        let mut dbtx = self.gateway_db.begin_transaction_nc().await;
3666        let fed_config = dbtx.load_federation_config(*federation_id).await.ok_or(
3667            PublicGatewayError::FederationNotConnected(FederationNotConnected {
3668                federation_id_prefix: federation_id.to_prefix(),
3669            }),
3670        )?;
3671
3672        let lightning_fee = fed_config.lightning_fee;
3673        let transaction_fee = fed_config.transaction_fee;
3674        let receive_enabled = fed_config.receive_enabled();
3675
3676        // This route is public and unauthenticated, so the sum of two fees stored
3677        // before the fee limits applied must not be able to panic here.
3678        let send_fee_default = lightning_fee.checked_add(transaction_fee).ok_or_else(|| {
3679            PublicGatewayError::Unexpected(anyhow!(
3680                "The configured fees of federation {federation_id} cannot be added"
3681            ))
3682        })?;
3683
3684        Ok(self
3685            .public_key_v2(federation_id)
3686            .await
3687            .map(|module_public_key| RoutingInfo {
3688                lightning_public_key: context.lightning_public_key,
3689                lightning_alias: Some(context.lightning_alias.clone()),
3690                module_public_key,
3691                send_fee_default,
3692                // The base fee ensures that the gateway does not loose sats sending the payment due
3693                // to fees paid on the transaction claiming the outgoing contract or
3694                // subsequent transactions spending the newly issued ecash
3695                send_fee_minimum: transaction_fee,
3696                expiration_delta_default: 1440,
3697                expiration_delta_minimum: EXPIRATION_DELTA_MINIMUM_V2,
3698                // The base fee ensures that the gateway does not loose sats receiving the payment
3699                // due to fees paid on the transaction funding the incoming contract
3700                receive_fee: transaction_fee,
3701                receive_enabled,
3702            }))
3703    }
3704
3705    /// Instructs this gateway to pay a Lightning network invoice via the LNv2
3706    /// protocol.
3707    pub async fn send_payment_v2(
3708        &self,
3709        payload: SendPaymentPayload,
3710    ) -> Result<std::result::Result<[u8; 32], Signature>> {
3711        let client = self.select_client(payload.federation_id).await?;
3712        // A federation only has to offer one of the two lightning modules, so a
3713        // client we serve over LNv1 may well have no LNv2 module at all.
3714        let module = client
3715            .value()
3716            .get_first_module::<GatewayClientModuleV2>()
3717            .map_err(|err| PublicGatewayError::LNv2(LNv2Error::OutgoingPayment(err.into())))?;
3718
3719        module
3720            .send_payment(payload)
3721            .await
3722            .map_err(|err| LNv2Error::OutgoingPayment(err.into()))
3723            .map_err(PublicGatewayError::LNv2)
3724    }
3725
3726    /// For the LNv2 protocol, this will create an invoice by fetching it from
3727    /// the connected Lightning node, then save the payment hash so that
3728    /// incoming lightning payments can be matched as a receive attempt to a
3729    /// specific federation.
3730    pub async fn create_bolt11_invoice_v2(
3731        &self,
3732        payload: CreateBolt11InvoicePayload,
3733    ) -> Result<Bolt11Invoice> {
3734        // Unauthenticated invoice creation consumes resources on the Lightning
3735        // node and burns CPU on contract verification, so the request rate is
3736        // limited before any other work is done.
3737        if !self.invoice_rate_limiter.try_acquire() {
3738            return Err(PublicGatewayError::RateLimited);
3739        }
3740
3741        // Verifying the contract is two pairings; keep them off the async workers.
3742        let contract = payload.contract.clone();
3743
3744        if !fedimint_core::runtime::spawn_blocking(move || contract.verify()).await {
3745            return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
3746                "The contract is invalid".to_string(),
3747            )));
3748        }
3749
3750        let payment_info = self.routing_info_v2(&payload.federation_id).await?.ok_or(
3751            LNv2Error::IncomingPayment(format!(
3752                "Federation {} does not exist",
3753                payload.federation_id
3754            )),
3755        )?;
3756
3757        if !payment_info.receive_enabled {
3758            return Err(PublicGatewayError::ReceiveDisabled {
3759                federation_id_prefix: payload.federation_id.to_prefix(),
3760            });
3761        }
3762
3763        if payload.contract.commitment.refund_pk != payment_info.module_public_key {
3764            return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
3765                "The incoming contract is keyed to another gateway".to_string(),
3766            )));
3767        }
3768
3769        let contract_amount = payment_info.receive_fee.subtract_from(payload.amount.msats);
3770
3771        if contract_amount == Amount::ZERO {
3772            return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
3773                "Zero amount incoming contracts are not supported".to_string(),
3774            )));
3775        }
3776
3777        if contract_amount != payload.contract.commitment.amount {
3778            return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
3779                "The contract amount does not pay the correct amount of fees".to_string(),
3780            )));
3781        }
3782
3783        if payload.contract.commitment.expiration_or_fee <= duration_since_epoch().as_secs() {
3784            return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
3785                "The contract has already expired".to_string(),
3786            )));
3787        }
3788
3789        if payload.expiry_secs > MAX_INVOICE_EXPIRY_SECS {
3790            return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
3791                "The invoice expiry exceeds the maximum of one day".to_string(),
3792            )));
3793        }
3794
3795        let payment_hash = match payload.contract.commitment.payment_image {
3796            PaymentImage::Hash(payment_hash) => payment_hash,
3797            PaymentImage::Point(..) => {
3798                return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
3799                    "PaymentImage is not a payment hash".to_string(),
3800                )));
3801            }
3802        };
3803
3804        // Reserve the payment hash in the database before requesting the
3805        // invoice so a replayed payment hash is rejected without creating any
3806        // state on the Lightning node, and so the contract is guaranteed to be
3807        // registered by the time the invoice is payable.
3808        let mut dbtx = self.gateway_db.begin_transaction().await;
3809
3810        let invoice_expires_at_secs = duration_since_epoch()
3811            .as_secs()
3812            .saturating_add(u64::from(payload.expiry_secs));
3813
3814        if dbtx
3815            .save_registered_incoming_contract(
3816                payload.federation_id,
3817                payload.amount,
3818                invoice_expires_at_secs,
3819                payload.contract,
3820            )
3821            .await
3822            .is_some()
3823        {
3824            return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
3825                "PaymentHash is already registered".to_string(),
3826            )));
3827        }
3828
3829        dbtx.commit_tx_result().await.map_err(|_| {
3830            PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
3831                "Payment hash is already registered".to_string(),
3832            ))
3833        })?;
3834
3835        match self
3836            .create_invoice_via_lnrpc_v2(
3837                payment_hash,
3838                payload.amount,
3839                payload.description.clone(),
3840                payload.expiry_secs,
3841            )
3842            .await
3843        {
3844            Ok(invoice) => Ok(invoice),
3845            Err(err) => {
3846                // Release the reservation so the payment hash is not burned by
3847                // a transient Lightning node failure.
3848                let mut dbtx = self.gateway_db.begin_transaction().await;
3849                dbtx.delete_registered_incoming_contract(PaymentImage::Hash(payment_hash))
3850                    .await;
3851                if let Err(db_err) = dbtx.commit_tx_result().await {
3852                    warn!(
3853                        target: LOG_GATEWAY,
3854                        err = %db_err.fmt_compact(),
3855                        %payment_hash,
3856                        "Failed to release incoming contract reservation after Lightning error"
3857                    );
3858                }
3859
3860                Err(err.into())
3861            }
3862        }
3863    }
3864
3865    /// Retrieves a BOLT11 invoice from the connected Lightning node with a
3866    /// specific `payment_hash`.
3867    pub async fn create_invoice_via_lnrpc_v2(
3868        &self,
3869        payment_hash: sha256::Hash,
3870        amount: Amount,
3871        description: Bolt11InvoiceDescription,
3872        expiry_time: u32,
3873    ) -> std::result::Result<Bolt11Invoice, LightningRpcError> {
3874        let lnrpc = self.get_lightning_context().await?.lnrpc;
3875
3876        let response = match description {
3877            Bolt11InvoiceDescription::Direct(description) => {
3878                lnrpc
3879                    .create_invoice(CreateInvoiceRequest {
3880                        payment_hash: Some(payment_hash),
3881                        amount_msat: amount.msats,
3882                        expiry_secs: expiry_time,
3883                        description: Some(InvoiceDescription::Direct(description)),
3884                    })
3885                    .await?
3886            }
3887            Bolt11InvoiceDescription::Hash(hash) => {
3888                lnrpc
3889                    .create_invoice(CreateInvoiceRequest {
3890                        payment_hash: Some(payment_hash),
3891                        amount_msat: amount.msats,
3892                        expiry_secs: expiry_time,
3893                        description: Some(InvoiceDescription::Hash(hash)),
3894                    })
3895                    .await?
3896            }
3897        };
3898
3899        Bolt11Invoice::from_str(&response.invoice).map_err(|e| {
3900            LightningRpcError::FailedToGetInvoice {
3901                failure_reason: e.to_string(),
3902            }
3903        })
3904    }
3905
3906    pub async fn verify_bolt11_preimage_v2(
3907        &self,
3908        payment_hash: sha256::Hash,
3909        wait: bool,
3910    ) -> std::result::Result<VerifyResponse, String> {
3911        let registered_contract = self
3912            .gateway_db
3913            .begin_transaction_nc()
3914            .await
3915            .load_registered_incoming_contract(PaymentImage::Hash(payment_hash))
3916            .await
3917            .ok_or("Unknown payment hash".to_string())?;
3918
3919        let client = self
3920            .select_client(registered_contract.federation_id)
3921            .await
3922            .map_err(|_| "Not connected to federation".to_string())?
3923            .into_value();
3924
3925        let operation_id = OperationId::from_encodable(&registered_contract.contract);
3926
3927        if !(wait || client.operation_exists(operation_id).await) {
3928            return Ok(VerifyResponse::pending());
3929        }
3930
3931        let module = client
3932            .get_first_module::<GatewayClientModuleV2>()
3933            .expect("Must have client module");
3934
3935        let Ok(state) = timeout(VERIFY_WAIT_TIMEOUT, module.await_receive(operation_id)).await
3936        else {
3937            return Ok(VerifyResponse::pending());
3938        };
3939
3940        let preimage = match state {
3941            FinalReceiveState::Success(preimage) => Ok(preimage),
3942            FinalReceiveState::Failure => Err("Payment has failed".to_string()),
3943            FinalReceiveState::Refunded => Err("Payment has been refunded".to_string()),
3944            FinalReceiveState::Rejected => Err("Payment has been rejected".to_string()),
3945        }?;
3946
3947        Ok(VerifyResponse::settled(preimage))
3948    }
3949
3950    /// Retrieves the persisted `CreateInvoicePayload` from the database
3951    /// specified by the `payment_hash` and the `ClientHandleArc` specified
3952    /// by the payload's `federation_id`.
3953    pub async fn get_registered_incoming_contract_and_client_v2(
3954        &self,
3955        payment_image: PaymentImage,
3956        amount_msats: u64,
3957    ) -> Result<(IncomingContract, ClientHandleArc)> {
3958        let registered_incoming_contract = self
3959            .gateway_db
3960            .begin_transaction_nc()
3961            .await
3962            .load_registered_incoming_contract(payment_image)
3963            .await
3964            .ok_or(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
3965                "No corresponding decryption contract available".to_string(),
3966            )))?;
3967
3968        if registered_incoming_contract.incoming_amount_msats != amount_msats {
3969            return Err(PublicGatewayError::LNv2(
3970                LNv2Error::IncomingAmountMismatch {
3971                    registered_msats: registered_incoming_contract.incoming_amount_msats,
3972                    payment_msats: amount_msats,
3973                },
3974            ));
3975        }
3976
3977        let client = self
3978            .select_client(registered_incoming_contract.federation_id)
3979            .await?
3980            .into_value();
3981
3982        Ok((registered_incoming_contract.contract, client))
3983    }
3984
3985    /// Completes (settles or cancels) an intercepted HTLC, retrying transient
3986    /// failures until Lightning reports a terminal outcome.
3987    ///
3988    /// Serves [`IGatewayClientV1::complete_htlc`] and
3989    /// [`IGatewayClientV2::complete_htlc`]: by now the incoming contract is
3990    /// funded, so giving up on a transient failure would strand it. Only
3991    /// [`LightningRpcError::HtlcCompletionRejected`], a permanent state, is
3992    /// returned to the caller.
3993    async fn await_complete_htlc(
3994        &self,
3995        htlc: InterceptPaymentResponse,
3996    ) -> std::result::Result<(), LightningRpcError> {
3997        loop {
3998            let lightning_context = self.await_lightning_context().await;
3999
4000            match lightning_context.lnrpc.complete_htlc(htlc.clone()).await {
4001                Ok(()) => return Ok(()),
4002                Err(err @ LightningRpcError::HtlcCompletionRejected { .. }) => {
4003                    warn!(
4004                        target: LOG_GATEWAY,
4005                        err = %err.fmt_compact(),
4006                        "Lightning cannot reach the requested terminal HTLC outcome",
4007                    );
4008                    return Err(err);
4009                }
4010                Err(err) => {
4011                    warn!(
4012                        target: LOG_GATEWAY,
4013                        err = %err.fmt_compact(),
4014                        "Failure trying to complete HTLC, retrying",
4015                    );
4016                }
4017            }
4018
4019            sleep(LIGHTNING_CONTEXT_RETRY_INTERVAL).await;
4020        }
4021    }
4022
4023    /// Answers whether the connected Lightning node has any record of an
4024    /// outbound payment for `payment_hash`, retrying transient lookup
4025    /// failures until the node itself can answer.
4026    ///
4027    /// Serves [`IGatewayClientV1::outbound_payment_exists`] and
4028    /// [`IGatewayClientV2::outbound_payment_exists`]: a wrong `false` lets a
4029    /// resumed state machine cancel a contract whose payment is still in
4030    /// flight, so no answer is synthesised from a failure.
4031    async fn await_outbound_payment_exists(&self, payment_hash: sha256::Hash) -> bool {
4032        loop {
4033            let lightning_context = self.await_lightning_context().await;
4034
4035            match lightning_context
4036                .lnrpc
4037                .outbound_payment_exists(payment_hash)
4038                .await
4039            {
4040                Ok(exists) => return exists,
4041                Err(err) => {
4042                    warn!(
4043                        target: LOG_GATEWAY,
4044                        err = %err.fmt_compact(),
4045                        %payment_hash,
4046                        "Failed to check for a dispatched payment, retrying",
4047                    );
4048                }
4049            }
4050
4051            sleep(LIGHTNING_CONTEXT_RETRY_INTERVAL).await;
4052        }
4053    }
4054}
4055
4056#[async_trait]
4057impl IGatewayClientV2 for Gateway {
4058    async fn complete_htlc(
4059        &self,
4060        htlc_response: InterceptPaymentResponse,
4061    ) -> std::result::Result<(), LightningRpcError> {
4062        self.await_complete_htlc(htlc_response).await
4063    }
4064
4065    async fn is_direct_swap(
4066        &self,
4067        invoice: &Bolt11Invoice,
4068    ) -> std::result::Result<Option<(IncomingContract, ClientHandleArc)>, GatewayClientV2Error>
4069    {
4070        // Deciding this from a locally synthesised "not connected" would route a
4071        // direct swap onto the lightning network, or -- once the send state
4072        // machine turns the error into a cancellation -- forfeit a contract we
4073        // may already be committed to. Ask once we can actually answer.
4074        let lightning_context = self.await_lightning_context().await;
4075        if lightning_context.lightning_public_key == invoice.get_payee_pub_key() {
4076            let (contract, client) = self
4077                .get_registered_incoming_contract_and_client_v2(
4078                    PaymentImage::Hash(*invoice.payment_hash()),
4079                    invoice
4080                        .amount_milli_satoshis()
4081                        .expect("The amount invoice has been previously checked"),
4082                )
4083                .await
4084                .map_err(GatewayClientV2Error::new)?;
4085            Ok(Some((contract, client)))
4086        } else {
4087            Ok(None)
4088        }
4089    }
4090
4091    async fn pay(
4092        &self,
4093        invoice: Bolt11Invoice,
4094        max_delay: u64,
4095        max_fee: Amount,
4096    ) -> std::result::Result<[u8; 32], LightningRpcError> {
4097        // The send state machine forfeits the outgoing contract on any error from
4098        // here, so only the lightning node gets to say this payment failed.
4099        let lightning_context = self.await_lightning_context().await;
4100        lightning_context
4101            .lnrpc
4102            .pay(invoice, max_delay, max_fee)
4103            .await
4104            .map(|response| response.preimage.0)
4105    }
4106
4107    async fn outbound_payment_exists(&self, payment_hash: sha256::Hash) -> bool {
4108        self.await_outbound_payment_exists(payment_hash).await
4109    }
4110
4111    async fn begin_fresh_receive(
4112        &self,
4113        federation_id: &FederationId,
4114    ) -> Option<PaymentPolicyGuard> {
4115        self.admit_fresh_receive(*federation_id).await
4116    }
4117
4118    async fn min_contract_amount(
4119        &self,
4120        federation_id: &FederationId,
4121        amount: u64,
4122    ) -> std::result::Result<Amount, GatewayClientV2Error> {
4123        Ok(self
4124            .routing_info_v2(federation_id)
4125            .await
4126            .map_err(GatewayClientV2Error::new)?
4127            .ok_or_else(|| GatewayClientV2Error::new("Routing Info not available"))?
4128            .send_fee_minimum
4129            .add_to(amount))
4130    }
4131
4132    async fn is_lnv1_invoice(&self, invoice: &Bolt11Invoice) -> Option<Spanned<ClientHandleArc>> {
4133        let rhints = invoice.route_hints();
4134        let hop = rhints.first().and_then(|rh| rh.0.last())?;
4135
4136        // Answering `None` because we happen to be between lightning connections
4137        // sends a swap that never needed the lightning network out over it.
4138        let lightning_context = self.await_lightning_context().await;
4139        if hop.src_node_id != lightning_context.lightning_public_key {
4140            return None;
4141        }
4142
4143        self.federation_manager
4144            .read()
4145            .await
4146            .get_client_for_index(hop.short_channel_id)
4147    }
4148
4149    async fn relay_lnv1_swap(
4150        &self,
4151        client: &ClientHandleArc,
4152        invoice: &Bolt11Invoice,
4153        allow_fresh_dispatch: bool,
4154    ) -> std::result::Result<Option<FinalReceiveState>, GatewayClientV2Error> {
4155        let swap_params =
4156            SwapParameters {
4157                payment_hash: *invoice.payment_hash(),
4158                amount_msat: Amount::from_msats(invoice.amount_milli_satoshis().ok_or_else(
4159                    || GatewayClientV2Error::new("Amountless invoice not supported"),
4160                )?),
4161            };
4162        // Nothing has been paid out yet, so failing here only cancels the send.
4163        let lnv1 = client
4164            .get_first_module::<GatewayClientModule>()
4165            .map_err(|_| GatewayClientV2Error::new("Federation does not have an LNv1 module"))?;
4166        let Some(operation_id) = lnv1
4167            .gateway_handle_direct_swap(swap_params, allow_fresh_dispatch)
4168            .await
4169            .map_err(GatewayClientV2Error::new)?
4170        else {
4171            return Ok(None);
4172        };
4173        let mut stream = lnv1
4174            .gateway_subscribe_ln_receive(operation_id)
4175            .await
4176            .map_err(GatewayClientV2Error::new)?
4177            .into_stream();
4178        let mut final_state = FinalReceiveState::Failure;
4179        while let Some(update) = stream.next().await {
4180            match update {
4181                GatewayExtReceiveStates::Funding => {}
4182                GatewayExtReceiveStates::FundingFailed { error: _ } => {
4183                    final_state = FinalReceiveState::Rejected;
4184                }
4185                GatewayExtReceiveStates::Preimage(preimage) => {
4186                    final_state = FinalReceiveState::Success(preimage.0);
4187                }
4188                GatewayExtReceiveStates::RefundError {
4189                    error_message: _,
4190                    error: _,
4191                } => {
4192                    final_state = FinalReceiveState::Failure;
4193                }
4194                GatewayExtReceiveStates::RefundSuccess {
4195                    out_points: _,
4196                    error: _,
4197                } => {
4198                    final_state = FinalReceiveState::Refunded;
4199                }
4200            }
4201        }
4202
4203        Ok(Some(final_state))
4204    }
4205
4206    async fn claim_payment_image(
4207        &self,
4208        payment_image: &PaymentImage,
4209        operation_id: OperationId,
4210    ) -> bool {
4211        // `autocommit` retries on write-write conflicts, so concurrent claims for
4212        // the same payment image are serialized: exactly one records itself as the
4213        // claimer, the rest observe it and forfeit.
4214        self.gateway_db
4215            .autocommit(
4216                |dbtx, _| {
4217                    let payment_image = payment_image.clone();
4218                    Box::pin(async move {
4219                        let claimer = dbtx
4220                            .claim_outgoing_payment_image(payment_image, operation_id)
4221                            .await;
4222                        Ok::<_, std::convert::Infallible>(claimer == operation_id)
4223                    })
4224                },
4225                None,
4226            )
4227            .await
4228            .expect("Retries until the transaction commits")
4229    }
4230
4231    async fn is_lightning_connected(&self) -> bool {
4232        self.get_lightning_context().await.is_ok()
4233    }
4234
4235    async fn await_lightning_connected(&self) {
4236        self.await_lightning_context().await;
4237    }
4238}
4239
4240#[async_trait]
4241impl IGatewayClientV1 for Gateway {
4242    async fn claim_payment_image(
4243        &self,
4244        payment_image: &PaymentImage,
4245        operation_id: OperationId,
4246    ) -> bool {
4247        IGatewayClientV2::claim_payment_image(self, payment_image, operation_id).await
4248    }
4249
4250    async fn verify_preimage_authentication(
4251        &self,
4252        payment_hash: sha256::Hash,
4253        preimage_auth: sha256::Hash,
4254        contract: OutgoingContractAccount,
4255    ) -> std::result::Result<(), OutgoingPaymentError> {
4256        let mut dbtx = self.gateway_db.begin_transaction().await;
4257        if let Some(secret_hash) = dbtx.load_preimage_authentication(payment_hash).await {
4258            if !PreimageAuth::new(secret_hash).verifies(preimage_auth) {
4259                return Err(OutgoingPaymentError {
4260                    error_type: OutgoingPaymentErrorType::InvalidInvoicePreimage,
4261                    contract_id: contract.contract.contract_id(),
4262                    contract: Some(contract),
4263                });
4264            }
4265        } else {
4266            // Committing the `preimage_auth` to the database can fail if two users try to
4267            // pay the same invoice at the same time.
4268            dbtx.save_new_preimage_authentication(payment_hash, preimage_auth)
4269                .await;
4270            return dbtx
4271                .commit_tx_result()
4272                .await
4273                .map_err(|_| OutgoingPaymentError {
4274                    error_type: OutgoingPaymentErrorType::InvoiceAlreadyPaid,
4275                    contract_id: contract.contract.contract_id(),
4276                    contract: Some(contract),
4277                });
4278        }
4279
4280        Ok(())
4281    }
4282
4283    async fn verify_pruned_invoice(
4284        &self,
4285        payment_data: PaymentData,
4286    ) -> std::result::Result<(), GatewayClientV1Error> {
4287        if matches!(payment_data, PaymentData::PrunedInvoice { .. }) {
4288            let lightning_context = self
4289                .get_lightning_context()
4290                .await
4291                .map_err(GatewayClientV1Error::new)?;
4292
4293            if !lightning_context.lnrpc.supports_private_payments() {
4294                return Err(GatewayClientV1Error::new(
4295                    "Private payments are not supported by the lightning node",
4296                ));
4297            }
4298        }
4299
4300        Ok(())
4301    }
4302
4303    async fn begin_fresh_receive(&self, federation_id: FederationId) -> Option<PaymentPolicyGuard> {
4304        self.admit_fresh_receive(federation_id).await
4305    }
4306
4307    async fn get_routing_fees(&self, federation_id: FederationId) -> Option<RoutingFees> {
4308        let mut gateway_dbtx = self.gateway_db.begin_transaction_nc().await;
4309        let lightning_fee = gateway_dbtx
4310            .load_federation_config(federation_id)
4311            .await?
4312            .lightning_fee;
4313
4314        // A fee stored before the fee limits applied may not be announceable. The
4315        // caller treats `None` as a federation configuration error, which is
4316        // preferable to panicking in the middle of a payment.
4317        RoutingFees::try_from(lightning_fee)
4318            .inspect_err(|err| {
4319                warn!(
4320                    target: LOG_GATEWAY,
4321                    %federation_id,
4322                    err = %err.fmt_compact(),
4323                    "Configured lightning fee cannot be used. Set a smaller fee with `set_fees`."
4324                );
4325            })
4326            .ok()
4327    }
4328
4329    async fn get_client(&self, federation_id: &FederationId) -> Option<Spanned<ClientHandleArc>> {
4330        self.federation_manager
4331            .read()
4332            .await
4333            .client(federation_id)
4334            .cloned()
4335    }
4336
4337    async fn get_client_for_invoice(
4338        &self,
4339        payment_data: PaymentData,
4340    ) -> Option<Spanned<ClientHandleArc>> {
4341        let rhints = payment_data.route_hints();
4342        let hop = rhints.first().and_then(|rh| rh.0.last())?;
4343
4344        // Answering `None` because we happen to be between lightning connections
4345        // sends a swap that never needed the lightning network out over it.
4346        let lightning_context = self.await_lightning_context().await;
4347        if hop.src_node_id != lightning_context.lightning_public_key {
4348            return None;
4349        }
4350
4351        self.federation_manager
4352            .read()
4353            .await
4354            .get_client_for_index(hop.short_channel_id)
4355    }
4356
4357    async fn pay(
4358        &self,
4359        payment_data: PaymentData,
4360        max_delay: u64,
4361        max_fee: Amount,
4362    ) -> std::result::Result<PayInvoiceResponse, LightningRpcError> {
4363        // `GatewayPayInvoice` cancels the outgoing contract on any error from
4364        // here, so only the lightning node gets to say this payment failed.
4365        let lightning_context = self.await_lightning_context().await;
4366
4367        match payment_data {
4368            PaymentData::Invoice(invoice) => {
4369                lightning_context
4370                    .lnrpc
4371                    .pay(invoice, max_delay, max_fee)
4372                    .await
4373            }
4374            PaymentData::PrunedInvoice(invoice) => {
4375                lightning_context
4376                    .lnrpc
4377                    .pay_private(invoice, max_delay, max_fee)
4378                    .await
4379            }
4380        }
4381    }
4382
4383    async fn outbound_payment_exists(&self, payment_hash: sha256::Hash) -> bool {
4384        self.await_outbound_payment_exists(payment_hash).await
4385    }
4386
4387    async fn complete_htlc(
4388        &self,
4389        htlc: InterceptPaymentResponse,
4390    ) -> std::result::Result<(), LightningRpcError> {
4391        self.await_complete_htlc(htlc).await
4392    }
4393
4394    async fn is_lnv2_direct_swap(
4395        &self,
4396        payment_hash: sha256::Hash,
4397        amount: Amount,
4398    ) -> std::result::Result<
4399        Option<(
4400            fedimint_lnv2_common::contracts::IncomingContract,
4401            ClientHandleArc,
4402        )>,
4403        GatewayClientV1Error,
4404    > {
4405        let (contract, client) = self
4406            .get_registered_incoming_contract_and_client_v2(
4407                PaymentImage::Hash(payment_hash),
4408                amount.msats,
4409            )
4410            .await
4411            .map_err(GatewayClientV1Error::new)?;
4412        Ok(Some((contract, client)))
4413    }
4414}