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