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;
23pub mod rpc_server;
24mod types;
25
26use std::collections::{BTreeMap, BTreeSet};
27use std::env;
28use std::fmt::Display;
29use std::net::SocketAddr;
30use std::str::FromStr;
31use std::sync::Arc;
32use std::time::{Duration, UNIX_EPOCH};
33
34use anyhow::{Context, anyhow, ensure};
35use async_trait::async_trait;
36use bitcoin::hashes::sha256;
37use bitcoin::{Address, Network, Txid};
38use clap::Parser;
39use client::GatewayClientBuilder;
40pub use config::GatewayParameters;
41use config::{DatabaseBackend, GatewayOpts};
42use envs::FM_GATEWAY_SKIP_WAIT_FOR_SYNC_ENV;
43use error::FederationNotConnected;
44use events::ALL_GATEWAY_EVENTS;
45use federation_manager::FederationManager;
46use fedimint_api_client::api::net::Connector;
47use fedimint_bip39::{Bip39RootSecretStrategy, Language, Mnemonic};
48use fedimint_bitcoind::bitcoincore::BitcoindClient;
49use fedimint_bitcoind::{EsploraClient, IBitcoindRpc};
50use fedimint_client::module_init::ClientModuleInitRegistry;
51use fedimint_client::secret::RootSecretStrategy;
52use fedimint_client::{Client, ClientHandleArc};
53use fedimint_core::config::FederationId;
54use fedimint_core::core::OperationId;
55use fedimint_core::db::{Database, DatabaseTransaction, apply_migrations};
56use fedimint_core::envs::is_env_var_set;
57use fedimint_core::invite_code::InviteCode;
58use fedimint_core::module::CommonModuleInit;
59use fedimint_core::module::registry::ModuleDecoderRegistry;
60use fedimint_core::rustls::install_crypto_provider;
61use fedimint_core::secp256k1::PublicKey;
62use fedimint_core::secp256k1::schnorr::Signature;
63use fedimint_core::task::{TaskGroup, TaskHandle, TaskShutdownToken, sleep};
64use fedimint_core::time::duration_since_epoch;
65use fedimint_core::util::{FmtCompact, FmtCompactAnyhow, SafeUrl, Spanned};
66use fedimint_core::{
67    Amount, BitcoinAmountOrAll, crit, fedimint_build_code_version_env, get_network_for_address,
68};
69use fedimint_eventlog::{DBTransactionEventLogExt, EventLogId, StructuredPaymentEvents};
70use fedimint_gateway_common::{
71    BackupPayload, ChainSource, CloseChannelsWithPeerRequest, CloseChannelsWithPeerResponse,
72    ConnectFedPayload, CreateInvoiceForOperatorPayload, CreateOfferPayload, CreateOfferResponse,
73    DepositAddressPayload, DepositAddressRecheckPayload, FederationBalanceInfo, FederationConfig,
74    FederationInfo, GatewayBalances, GatewayFedConfig, GatewayInfo, GetInvoiceRequest,
75    GetInvoiceResponse, LeaveFedPayload, LightningMode, ListTransactionsPayload,
76    ListTransactionsResponse, MnemonicResponse, OpenChannelRequest, PayInvoiceForOperatorPayload,
77    PayOfferPayload, PayOfferResponse, PaymentLogPayload, PaymentLogResponse, PaymentStats,
78    PaymentSummaryPayload, PaymentSummaryResponse, ReceiveEcashPayload, ReceiveEcashResponse,
79    SendOnchainRequest, SetFeesPayload, SpendEcashPayload, SpendEcashResponse, V1_API_ENDPOINT,
80    WithdrawPayload, WithdrawResponse,
81};
82use fedimint_gateway_server_db::{GatewayDbtxNcExt as _, get_gatewayd_database_migrations};
83use fedimint_gw_client::events::compute_lnv1_stats;
84use fedimint_gw_client::pay::{OutgoingPaymentError, OutgoingPaymentErrorType};
85use fedimint_gw_client::{
86    GatewayClientModule, GatewayExtPayStates, GatewayExtReceiveStates, IGatewayClientV1,
87    SwapParameters,
88};
89use fedimint_gwv2_client::events::compute_lnv2_stats;
90use fedimint_gwv2_client::{
91    EXPIRATION_DELTA_MINIMUM_V2, FinalReceiveState, GatewayClientModuleV2, IGatewayClientV2,
92};
93use fedimint_lightning::lnd::GatewayLndClient;
94use fedimint_lightning::{
95    CreateInvoiceRequest, ILnRpcClient, InterceptPaymentRequest, InterceptPaymentResponse,
96    InvoiceDescription, LightningContext, LightningRpcError, PayInvoiceResponse, PaymentAction,
97    RouteHtlcStream, ldk,
98};
99use fedimint_ln_client::pay::PaymentData;
100use fedimint_ln_common::LightningCommonInit;
101use fedimint_ln_common::config::LightningClientConfig;
102use fedimint_ln_common::contracts::outgoing::OutgoingContractAccount;
103use fedimint_ln_common::contracts::{IdentifiableContract, Preimage};
104use fedimint_lnv2_common::Bolt11InvoiceDescription;
105use fedimint_lnv2_common::contracts::{IncomingContract, PaymentImage};
106use fedimint_lnv2_common::gateway_api::{
107    CreateBolt11InvoicePayload, PaymentFee, RoutingInfo, SendPaymentPayload,
108};
109use fedimint_lnv2_common::lnurl::VerifyResponse;
110use fedimint_logging::LOG_GATEWAY;
111use fedimint_mint_client::{
112    MintClientInit, MintClientModule, SelectNotesWithAtleastAmount, SelectNotesWithExactAmount,
113};
114use fedimint_wallet_client::{WalletClientInit, WalletClientModule, WithdrawState};
115use futures::stream::StreamExt;
116use lightning_invoice::{Bolt11Invoice, RoutingFees};
117use rand::rngs::OsRng;
118use tokio::sync::RwLock;
119use tracing::{debug, info, info_span, warn};
120
121use crate::envs::FM_GATEWAY_MNEMONIC_ENV;
122use crate::error::{AdminGatewayError, LNv1Error, LNv2Error, PublicGatewayError};
123use crate::events::get_events_for_duration;
124use crate::rpc_server::run_webserver;
125use crate::types::PrettyInterceptPaymentRequest;
126
127/// How long a gateway announcement stays valid
128const GW_ANNOUNCEMENT_TTL: Duration = Duration::from_secs(600);
129
130/// The default number of route hints that the legacy gateway provides for
131/// invoice creation.
132const DEFAULT_NUM_ROUTE_HINTS: u32 = 1;
133
134/// Default Bitcoin network for testing purposes.
135pub const DEFAULT_NETWORK: Network = Network::Regtest;
136
137pub type Result<T> = std::result::Result<T, PublicGatewayError>;
138pub type AdminResult<T> = std::result::Result<T, AdminGatewayError>;
139
140/// Name of the gateway's database that is used for metadata and configuration
141/// storage.
142const DB_FILE: &str = "gatewayd.db";
143
144/// Name of the folder that the gateway uses to store its node database when
145/// running in LDK mode.
146const LDK_NODE_DB_FOLDER: &str = "ldk_node";
147
148#[cfg_attr(doc, aquamarine::aquamarine)]
149/// ```mermaid
150/// graph LR
151/// classDef virtual fill:#fff,stroke-dasharray: 5 5
152///
153///    Disconnected -- establish lightning connection --> Connected
154///    Connected -- load federation clients --> Running
155///    Connected -- not synced to chain --> Syncing
156///    Syncing -- load federation clients --> Running
157///    Running -- disconnected from lightning node --> Disconnected
158///    Running -- shutdown initiated --> ShuttingDown
159/// ```
160#[derive(Clone, Debug)]
161pub enum GatewayState {
162    Disconnected,
163    Syncing,
164    Connected,
165    Running { lightning_context: LightningContext },
166    ShuttingDown { lightning_context: LightningContext },
167}
168
169impl Display for GatewayState {
170    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
171        match self {
172            GatewayState::Disconnected => write!(f, "Disconnected"),
173            GatewayState::Syncing => write!(f, "Syncing"),
174            GatewayState::Connected => write!(f, "Connected"),
175            GatewayState::Running { .. } => write!(f, "Running"),
176            GatewayState::ShuttingDown { .. } => write!(f, "ShuttingDown"),
177        }
178    }
179}
180
181/// The action to take after handling a payment stream.
182enum ReceivePaymentStreamAction {
183    RetryAfterDelay,
184    NoRetry,
185}
186
187#[derive(Clone)]
188pub struct Gateway {
189    /// The gateway's federation manager.
190    federation_manager: Arc<RwLock<FederationManager>>,
191
192    // The mnemonic for the gateway
193    mnemonic: Mnemonic,
194
195    /// The mode that specifies the lightning connection parameters
196    lightning_mode: LightningMode,
197
198    /// The current state of the Gateway.
199    state: Arc<RwLock<GatewayState>>,
200
201    /// Builder struct that allows the gateway to build a Fedimint client, which
202    /// handles the communication with a federation.
203    client_builder: GatewayClientBuilder,
204
205    /// Database for Gateway metadata.
206    gateway_db: Database,
207
208    /// A public key representing the identity of the gateway. Private key is
209    /// not used.
210    gateway_id: PublicKey,
211
212    /// The Gateway's API URL.
213    versioned_api: SafeUrl,
214
215    /// The socket the gateway listens on.
216    listen: SocketAddr,
217
218    /// The task group for all tasks related to the gateway.
219    task_group: TaskGroup,
220
221    /// The bcrypt password hash used to authenticate the gateway.
222    /// This is an `Arc` because `bcrypt::HashParts` does not implement `Clone`.
223    bcrypt_password_hash: Arc<bcrypt::HashParts>,
224
225    /// The number of route hints to include in LNv1 invoices.
226    num_route_hints: u32,
227
228    /// The Bitcoin network that the Lightning network is configured to.
229    network: Network,
230
231    /// The source of the Bitcoin blockchain data
232    chain_source: ChainSource,
233
234    /// The default routing fees for new federations
235    default_routing_fees: PaymentFee,
236
237    /// The default transaction fees for new federations
238    default_transaction_fees: PaymentFee,
239}
240
241impl std::fmt::Debug for Gateway {
242    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
243        f.debug_struct("Gateway")
244            .field("federation_manager", &self.federation_manager)
245            .field("state", &self.state)
246            .field("client_builder", &self.client_builder)
247            .field("gateway_db", &self.gateway_db)
248            .field("gateway_id", &self.gateway_id)
249            .field("versioned_api", &self.versioned_api)
250            .field("listen", &self.listen)
251            .finish_non_exhaustive()
252    }
253}
254
255impl Gateway {
256    /// Creates a new gateway but with a custom module registry provided inside
257    /// `client_builder`. Currently only used for testing.
258    #[allow(clippy::too_many_arguments)]
259    pub async fn new_with_custom_registry(
260        lightning_mode: LightningMode,
261        client_builder: GatewayClientBuilder,
262        listen: SocketAddr,
263        api_addr: SafeUrl,
264        bcrypt_password_hash: bcrypt::HashParts,
265        network: Network,
266        num_route_hints: u32,
267        gateway_db: Database,
268        gateway_state: GatewayState,
269        chain_source: ChainSource,
270    ) -> anyhow::Result<Gateway> {
271        let versioned_api = api_addr
272            .join(V1_API_ENDPOINT)
273            .expect("Failed to version gateway API address");
274        Gateway::new(
275            lightning_mode,
276            GatewayParameters {
277                listen,
278                versioned_api,
279                bcrypt_password_hash,
280                network,
281                num_route_hints,
282                default_routing_fees: PaymentFee::TRANSACTION_FEE_DEFAULT,
283                default_transaction_fees: PaymentFee::TRANSACTION_FEE_DEFAULT,
284            },
285            gateway_db,
286            client_builder,
287            gateway_state,
288            chain_source,
289        )
290        .await
291    }
292
293    /// Returns a bitcoind client using the credentials that were passed in from
294    /// the environment variables.
295    fn get_bitcoind_client(
296        opts: &GatewayOpts,
297        network: bitcoin::Network,
298        gateway_id: &PublicKey,
299    ) -> anyhow::Result<(BitcoindClient, ChainSource)> {
300        let bitcoind_username = opts
301            .bitcoind_username
302            .clone()
303            .expect("FM_BITCOIND_URL is set but FM_BITCOIND_USERNAME is not");
304        let url = opts.bitcoind_url.clone().expect("No bitcoind url set");
305        let password = opts
306            .bitcoind_password
307            .clone()
308            .expect("FM_BITCOIND_URL is set but FM_BITCOIND_PASSWORD is not");
309
310        let chain_source = ChainSource::Bitcoind {
311            username: bitcoind_username.clone(),
312            password: password.clone(),
313            server_url: url.clone(),
314        };
315        let wallet_name = format!("gatewayd-{gateway_id}");
316        let client = BitcoindClient::new(&url, bitcoind_username, password, &wallet_name, network)?;
317        Ok((client, chain_source))
318    }
319
320    /// Default function for creating a gateway with the `Mint`, `Wallet`, and
321    /// `Gateway` modules.
322    pub async fn new_with_default_modules() -> anyhow::Result<Gateway> {
323        let opts = GatewayOpts::parse();
324        let gateway_parameters = opts.to_gateway_parameters()?;
325        let decoders = ModuleDecoderRegistry::default();
326
327        let db_path = opts.data_dir.join(DB_FILE);
328        let gateway_db = match opts.db_backend {
329            DatabaseBackend::RocksDb => {
330                debug!(target: LOG_GATEWAY, "Using RocksDB database backend");
331                Database::new(fedimint_rocksdb::RocksDb::open(db_path).await?, decoders)
332            }
333            DatabaseBackend::CursedRedb => {
334                debug!(target: LOG_GATEWAY, "Using CursedRedb database backend");
335                Database::new(
336                    fedimint_cursed_redb::MemAndRedb::new(db_path).await?,
337                    decoders,
338                )
339            }
340        };
341
342        let gateway_id = Self::load_or_create_gateway_id(&gateway_db).await;
343        let (dyn_bitcoin_rpc, chain_source) =
344            match (opts.bitcoind_url.as_ref(), opts.esplora_url.as_ref()) {
345                (Some(_), None) => {
346                    let (client, chain_source) =
347                        Self::get_bitcoind_client(&opts, gateway_parameters.network, &gateway_id)?;
348                    (client.into_dyn(), chain_source)
349                }
350                (None, Some(url)) => {
351                    let client = EsploraClient::new(url)
352                        .expect("Could not create EsploraClient")
353                        .into_dyn();
354                    let chain_source = ChainSource::Esplora {
355                        server_url: url.clone(),
356                    };
357                    (client, chain_source)
358                }
359                (Some(_), Some(_)) => {
360                    // Use bitcoind by default if both are set
361                    let (client, chain_source) =
362                        Self::get_bitcoind_client(&opts, gateway_parameters.network, &gateway_id)?;
363                    (client.into_dyn(), chain_source)
364                }
365                _ => unreachable!("ArgGroup already enforced XOR relation"),
366            };
367
368        // Gateway module will be attached when the federation clients are created
369        // because the LN RPC will be injected with `GatewayClientGen`.
370        let mut registry = ClientModuleInitRegistry::new();
371        registry.attach(MintClientInit);
372        registry.attach(WalletClientInit::new(dyn_bitcoin_rpc));
373
374        let client_builder =
375            GatewayClientBuilder::new(opts.data_dir.clone(), registry, opts.db_backend);
376
377        info!(
378            target: LOG_GATEWAY,
379            version = %fedimint_build_code_version_env!(),
380            "Starting gatewayd",
381        );
382
383        Gateway::new(
384            opts.mode,
385            gateway_parameters,
386            gateway_db,
387            client_builder,
388            GatewayState::Disconnected,
389            chain_source,
390        )
391        .await
392    }
393
394    /// Helper function for creating a gateway from either
395    /// `new_with_default_modules` or `new_with_custom_registry`.
396    async fn new(
397        lightning_mode: LightningMode,
398        gateway_parameters: GatewayParameters,
399        gateway_db: Database,
400        client_builder: GatewayClientBuilder,
401        gateway_state: GatewayState,
402        chain_source: ChainSource,
403    ) -> anyhow::Result<Gateway> {
404        // Apply database migrations before using the database to ensure old database
405        // structures are readable.
406        apply_migrations(
407            &gateway_db,
408            (),
409            "gatewayd".to_string(),
410            get_gatewayd_database_migrations(),
411            None,
412            None,
413        )
414        .await?;
415
416        let num_route_hints = gateway_parameters.num_route_hints;
417        let network = gateway_parameters.network;
418
419        let task_group = TaskGroup::new();
420        task_group.install_kill_handler();
421
422        Ok(Self {
423            federation_manager: Arc::new(RwLock::new(FederationManager::new())),
424            mnemonic: Self::load_or_generate_mnemonic(&gateway_db).await?,
425            lightning_mode,
426            state: Arc::new(RwLock::new(gateway_state)),
427            client_builder,
428            gateway_id: Self::load_or_create_gateway_id(&gateway_db).await,
429            gateway_db,
430            versioned_api: gateway_parameters.versioned_api,
431            listen: gateway_parameters.listen,
432            task_group,
433            bcrypt_password_hash: Arc::new(gateway_parameters.bcrypt_password_hash),
434            num_route_hints,
435            network,
436            chain_source,
437            default_routing_fees: gateway_parameters.default_routing_fees,
438            default_transaction_fees: gateway_parameters.default_transaction_fees,
439        })
440    }
441
442    /// Returns a `PublicKey` that uniquely identifies the Gateway.
443    async fn load_or_create_gateway_id(gateway_db: &Database) -> PublicKey {
444        let mut dbtx = gateway_db.begin_transaction().await;
445        let keypair = dbtx.load_or_create_gateway_keypair().await;
446        dbtx.commit_tx().await;
447        keypair.public_key()
448    }
449
450    pub fn gateway_id(&self) -> PublicKey {
451        self.gateway_id
452    }
453
454    pub fn versioned_api(&self) -> &SafeUrl {
455        &self.versioned_api
456    }
457
458    async fn get_state(&self) -> GatewayState {
459        self.state.read().await.clone()
460    }
461
462    /// Reads and serializes structures from the Gateway's database for the
463    /// purpose for serializing to JSON for inspection.
464    pub async fn dump_database(
465        dbtx: &mut DatabaseTransaction<'_>,
466        prefix_names: Vec<String>,
467    ) -> BTreeMap<String, Box<dyn erased_serde::Serialize + Send>> {
468        dbtx.dump_database(prefix_names).await
469    }
470
471    /// Main entrypoint into the gateway that starts the client registration
472    /// timer, loads the federation clients from the persisted config,
473    /// begins listening for intercepted payments, and starts the webserver
474    /// to service requests.
475    pub async fn run(
476        self,
477        runtime: Arc<tokio::runtime::Runtime>,
478    ) -> anyhow::Result<TaskShutdownToken> {
479        install_crypto_provider().await;
480        self.register_clients_timer();
481        self.load_clients().await?;
482        self.start_gateway(runtime);
483        // start webserver last to avoid handling requests before fully initialized
484        let handle = self.task_group.make_handle();
485        run_webserver(Arc::new(self)).await?;
486        let shutdown_receiver = handle.make_shutdown_rx();
487        Ok(shutdown_receiver)
488    }
489
490    /// Begins the task for listening for intercepted payments from the
491    /// lightning node.
492    fn start_gateway(&self, runtime: Arc<tokio::runtime::Runtime>) {
493        const PAYMENT_STREAM_RETRY_SECONDS: u64 = 5;
494
495        let self_copy = self.clone();
496        let tg = self.task_group.clone();
497        self.task_group.spawn(
498            "Subscribe to intercepted lightning payments in stream",
499            |handle| async move {
500                // Repeatedly attempt to establish a connection to the lightning node and create a payment stream, re-trying if the connection is broken.
501                loop {
502                    if handle.is_shutting_down() {
503                        info!(target: LOG_GATEWAY, "Gateway lightning payment stream handler loop is shutting down");
504                        break;
505                    }
506
507                    let payment_stream_task_group = tg.make_subgroup();
508                    let lnrpc_route = self_copy.create_lightning_client(runtime.clone());
509
510                    debug!(target: LOG_GATEWAY, "Establishing lightning payment stream...");
511                    let (stream, ln_client) = match lnrpc_route.route_htlcs(&payment_stream_task_group).await
512                    {
513                        Ok((stream, ln_client)) => (stream, ln_client),
514                        Err(err) => {
515                            warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Failed to open lightning payment stream");
516                            continue
517                        }
518                    };
519
520                    // Successful calls to `route_htlcs` establish a connection
521                    self_copy.set_gateway_state(GatewayState::Connected).await;
522                    info!(target: LOG_GATEWAY, "Established lightning payment stream");
523
524                    let route_payments_response =
525                        self_copy.route_lightning_payments(&handle, stream, ln_client).await;
526
527                    self_copy.set_gateway_state(GatewayState::Disconnected).await;
528                    if let Err(err) = payment_stream_task_group.shutdown_join_all(None).await {
529                        crit!(target: LOG_GATEWAY, err = %err.fmt_compact_anyhow(), "Lightning payment stream task group shutdown");
530                    }
531
532                    self_copy.unannounce_from_all_federations().await;
533
534                    match route_payments_response {
535                        ReceivePaymentStreamAction::RetryAfterDelay => {
536                            warn!(target: LOG_GATEWAY, retry_interval = %PAYMENT_STREAM_RETRY_SECONDS, "Disconnected from lightning node");
537                            sleep(Duration::from_secs(PAYMENT_STREAM_RETRY_SECONDS)).await;
538                        }
539                        ReceivePaymentStreamAction::NoRetry => break,
540                    }
541                }
542            },
543        );
544    }
545
546    /// Handles a stream of incoming payments from the lightning node after
547    /// ensuring the gateway is properly configured. Awaits until the stream
548    /// is closed, then returns with the appropriate action to take.
549    async fn route_lightning_payments<'a>(
550        &'a self,
551        handle: &TaskHandle,
552        mut stream: RouteHtlcStream<'a>,
553        ln_client: Arc<dyn ILnRpcClient>,
554    ) -> ReceivePaymentStreamAction {
555        let (lightning_public_key, lightning_alias, lightning_network, synced_to_chain) =
556            match ln_client.parsed_node_info().await {
557                Ok((
558                    lightning_public_key,
559                    lightning_alias,
560                    lightning_network,
561                    _block_height,
562                    synced_to_chain,
563                )) => (
564                    lightning_public_key,
565                    lightning_alias,
566                    lightning_network,
567                    synced_to_chain,
568                ),
569                Err(err) => {
570                    warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Failed to retrieve Lightning info");
571                    return ReceivePaymentStreamAction::RetryAfterDelay;
572                }
573            };
574
575        assert!(
576            self.network == lightning_network,
577            "Lightning node network does not match Gateway's network. LN: {lightning_network} Gateway: {}",
578            self.network
579        );
580
581        if synced_to_chain || is_env_var_set(FM_GATEWAY_SKIP_WAIT_FOR_SYNC_ENV) {
582            info!(target: LOG_GATEWAY, "Gateway is already synced to chain");
583        } else {
584            self.set_gateway_state(GatewayState::Syncing).await;
585            info!(target: LOG_GATEWAY, "Waiting for chain sync");
586            if let Err(err) = ln_client.wait_for_chain_sync().await {
587                warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Failed to wait for chain sync");
588                return ReceivePaymentStreamAction::RetryAfterDelay;
589            }
590        }
591
592        let lightning_context = LightningContext {
593            lnrpc: ln_client,
594            lightning_public_key,
595            lightning_alias,
596            lightning_network,
597        };
598        self.set_gateway_state(GatewayState::Running { lightning_context })
599            .await;
600        info!(target: LOG_GATEWAY, "Gateway is running");
601
602        if matches!(self.lightning_mode, LightningMode::Lnd { .. }) {
603            // Re-register the gateway with all federations after connecting to the
604            // lightning node
605            let mut dbtx = self.gateway_db.begin_transaction_nc().await;
606            let all_federations_configs =
607                dbtx.load_federation_configs().await.into_iter().collect();
608            self.register_federations(&all_federations_configs, &self.task_group)
609                .await;
610        }
611
612        // Runs until the connection to the lightning node breaks or we receive the
613        // shutdown signal.
614        if handle
615            .cancel_on_shutdown(async move {
616                loop {
617                    let payment_request_or = tokio::select! {
618                        payment_request_or = stream.next() => {
619                            payment_request_or
620                        }
621                        () = self.is_shutting_down_safely() => {
622                            break;
623                        }
624                    };
625
626                    let Some(payment_request) = payment_request_or else {
627                        warn!(
628                            target: LOG_GATEWAY,
629                            "Unexpected response from incoming lightning payment stream. Shutting down payment processor"
630                        );
631                        break;
632                    };
633
634                    let state_guard = self.state.read().await;
635                    if let GatewayState::Running { ref lightning_context } = *state_guard {
636                        self.handle_lightning_payment(payment_request, lightning_context).await;
637                    } else {
638                        warn!(
639                            target: LOG_GATEWAY,
640                            state = %state_guard,
641                            "Gateway isn't in a running state, cannot handle incoming payments."
642                        );
643                        break;
644                    }
645                }
646            })
647            .await
648            .is_ok()
649        {
650            warn!(target: LOG_GATEWAY, "Lightning payment stream connection broken. Gateway is disconnected");
651            ReceivePaymentStreamAction::RetryAfterDelay
652        } else {
653            info!(target: LOG_GATEWAY, "Received shutdown signal");
654            ReceivePaymentStreamAction::NoRetry
655        }
656    }
657
658    /// Polls the Gateway's state waiting for it to shutdown so the thread
659    /// processing payment requests can exit.
660    async fn is_shutting_down_safely(&self) {
661        loop {
662            if let GatewayState::ShuttingDown { .. } = self.get_state().await {
663                return;
664            }
665
666            fedimint_core::task::sleep(Duration::from_secs(1)).await;
667        }
668    }
669
670    /// Handles an intercepted lightning payment. If the payment is part of an
671    /// incoming payment to a federation, spawns a state machine and hands the
672    /// payment off to it. Otherwise, forwards the payment to the next hop like
673    /// a normal lightning node.
674    async fn handle_lightning_payment(
675        &self,
676        payment_request: InterceptPaymentRequest,
677        lightning_context: &LightningContext,
678    ) {
679        info!(
680            target: LOG_GATEWAY,
681            lightning_payment = %PrettyInterceptPaymentRequest(&payment_request),
682            "Intercepting lightning payment",
683        );
684
685        if self
686            .try_handle_lightning_payment_lnv2(&payment_request, lightning_context)
687            .await
688            .is_ok()
689        {
690            return;
691        }
692
693        if self
694            .try_handle_lightning_payment_ln_legacy(&payment_request)
695            .await
696            .is_ok()
697        {
698            return;
699        }
700
701        Self::forward_lightning_payment(payment_request, lightning_context).await;
702    }
703
704    /// Tries to handle a lightning payment using the LNv2 protocol.
705    /// Returns `Ok` if the payment was handled, `Err` otherwise.
706    async fn try_handle_lightning_payment_lnv2(
707        &self,
708        htlc_request: &InterceptPaymentRequest,
709        lightning_context: &LightningContext,
710    ) -> Result<()> {
711        // If `payment_hash` has been registered as a LNv2 payment, we try to complete
712        // the payment by getting the preimage from the federation
713        // using the LNv2 protocol. If the `payment_hash` is not registered,
714        // this payment is either a legacy Lightning payment or the end destination is
715        // not a Fedimint.
716        let (contract, client) = self
717            .get_registered_incoming_contract_and_client_v2(
718                PaymentImage::Hash(htlc_request.payment_hash),
719                htlc_request.amount_msat,
720            )
721            .await?;
722
723        if let Err(err) = client
724            .get_first_module::<GatewayClientModuleV2>()
725            .expect("Must have client module")
726            .relay_incoming_htlc(
727                htlc_request.payment_hash,
728                htlc_request.incoming_chan_id,
729                htlc_request.htlc_id,
730                contract,
731                htlc_request.amount_msat,
732            )
733            .await
734        {
735            warn!(target: LOG_GATEWAY, err = %err.fmt_compact_anyhow(), "Error relaying incoming lightning payment");
736
737            let outcome = InterceptPaymentResponse {
738                action: PaymentAction::Cancel,
739                payment_hash: htlc_request.payment_hash,
740                incoming_chan_id: htlc_request.incoming_chan_id,
741                htlc_id: htlc_request.htlc_id,
742            };
743
744            if let Err(err) = lightning_context.lnrpc.complete_htlc(outcome).await {
745                warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Error sending HTLC response to lightning node");
746            }
747        }
748
749        Ok(())
750    }
751
752    /// Tries to handle a lightning payment using the legacy lightning protocol.
753    /// Returns `Ok` if the payment was handled, `Err` otherwise.
754    async fn try_handle_lightning_payment_ln_legacy(
755        &self,
756        htlc_request: &InterceptPaymentRequest,
757    ) -> Result<()> {
758        // Check if the payment corresponds to a federation supporting legacy Lightning.
759        let Some(federation_index) = htlc_request.short_channel_id else {
760            return Err(PublicGatewayError::LNv1(LNv1Error::IncomingPayment(
761                "Incoming payment has not last hop short channel id".to_string(),
762            )));
763        };
764
765        let Some(client) = self
766            .federation_manager
767            .read()
768            .await
769            .get_client_for_index(federation_index)
770        else {
771            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())));
772        };
773
774        client
775            .borrow()
776            .with(|client| async {
777                let htlc = htlc_request.clone().try_into();
778                match htlc {
779                    Ok(htlc) => {
780                        match client
781                            .get_first_module::<GatewayClientModule>()
782                            .expect("Must have client module")
783                            .gateway_handle_intercepted_htlc(htlc)
784                            .await
785                        {
786                            Ok(_) => Ok(()),
787                            Err(e) => Err(PublicGatewayError::LNv1(LNv1Error::IncomingPayment(
788                                format!("Error intercepting lightning payment {e:?}"),
789                            ))),
790                        }
791                    }
792                    _ => Err(PublicGatewayError::LNv1(LNv1Error::IncomingPayment(
793                        "Could not convert InterceptHtlcResult into an HTLC".to_string(),
794                    ))),
795                }
796            })
797            .await
798    }
799
800    /// Forwards a lightning payment to the next hop like a normal lightning
801    /// node. Only necessary for LNv1, since LNv2 uses hold invoices instead
802    /// of HTLC interception for routing incoming payments.
803    async fn forward_lightning_payment(
804        htlc_request: InterceptPaymentRequest,
805        lightning_context: &LightningContext,
806    ) {
807        let outcome = InterceptPaymentResponse {
808            action: PaymentAction::Forward,
809            payment_hash: htlc_request.payment_hash,
810            incoming_chan_id: htlc_request.incoming_chan_id,
811            htlc_id: htlc_request.htlc_id,
812        };
813
814        if let Err(err) = lightning_context.lnrpc.complete_htlc(outcome).await {
815            warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Error sending lightning payment response to lightning node");
816        }
817    }
818
819    /// Helper function for atomically changing the Gateway's internal state.
820    async fn set_gateway_state(&self, state: GatewayState) {
821        let mut lock = self.state.write().await;
822        *lock = state;
823    }
824
825    /// Returns information about the Gateway back to the client when requested
826    /// via the webserver.
827    pub async fn handle_get_info(&self) -> AdminResult<GatewayInfo> {
828        let GatewayState::Running { lightning_context } = self.get_state().await else {
829            return Ok(GatewayInfo {
830                federations: vec![],
831                federation_fake_scids: None,
832                version_hash: fedimint_build_code_version_env!().to_string(),
833                lightning_pub_key: None,
834                lightning_alias: None,
835                gateway_id: self.gateway_id,
836                gateway_state: self.state.read().await.to_string(),
837                network: self.network,
838                block_height: None,
839                synced_to_chain: false,
840                api: self.versioned_api.clone(),
841                lightning_mode: self.lightning_mode.clone(),
842            });
843        };
844
845        let dbtx = self.gateway_db.begin_transaction_nc().await;
846        let federations = self
847            .federation_manager
848            .read()
849            .await
850            .federation_info_all_federations(dbtx)
851            .await;
852
853        let channels: BTreeMap<u64, FederationId> = federations
854            .iter()
855            .map(|federation_info| {
856                (
857                    federation_info.config.federation_index,
858                    federation_info.federation_id,
859                )
860            })
861            .collect();
862
863        let node_info = lightning_context.lnrpc.parsed_node_info().await?;
864
865        Ok(GatewayInfo {
866            federations,
867            federation_fake_scids: Some(channels),
868            version_hash: fedimint_build_code_version_env!().to_string(),
869            lightning_pub_key: Some(lightning_context.lightning_public_key.to_string()),
870            lightning_alias: Some(lightning_context.lightning_alias.clone()),
871            gateway_id: self.gateway_id,
872            gateway_state: self.state.read().await.to_string(),
873            network: self.network,
874            block_height: Some(node_info.3),
875            synced_to_chain: node_info.4,
876            api: self.versioned_api.clone(),
877            lightning_mode: self.lightning_mode.clone(),
878        })
879    }
880
881    /// If the Gateway is connected to the Lightning node, returns the
882    /// `ClientConfig` for each federation that the Gateway is connected to.
883    pub async fn handle_get_federation_config(
884        &self,
885        federation_id_or: Option<FederationId>,
886    ) -> AdminResult<GatewayFedConfig> {
887        if !matches!(self.get_state().await, GatewayState::Running { .. }) {
888            return Ok(GatewayFedConfig {
889                federations: BTreeMap::new(),
890            });
891        }
892
893        let federations = if let Some(federation_id) = federation_id_or {
894            let mut federations = BTreeMap::new();
895            federations.insert(
896                federation_id,
897                self.federation_manager
898                    .read()
899                    .await
900                    .get_federation_config(federation_id)
901                    .await?,
902            );
903            federations
904        } else {
905            self.federation_manager
906                .read()
907                .await
908                .get_all_federation_configs()
909                .await
910        };
911
912        Ok(GatewayFedConfig { federations })
913    }
914
915    /// Returns a Bitcoin deposit on-chain address for pegging in Bitcoin for a
916    /// specific connected federation.
917    pub async fn handle_address_msg(&self, payload: DepositAddressPayload) -> AdminResult<Address> {
918        let (_, address, _) = self
919            .select_client(payload.federation_id)
920            .await?
921            .value()
922            .get_first_module::<WalletClientModule>()
923            .expect("Must have client module")
924            .allocate_deposit_address_expert_only(())
925            .await?;
926        Ok(address)
927    }
928
929    /// Returns a Bitcoin TXID from a peg-out transaction for a specific
930    /// connected federation.
931    pub async fn handle_withdraw_msg(
932        &self,
933        payload: WithdrawPayload,
934    ) -> AdminResult<WithdrawResponse> {
935        let WithdrawPayload {
936            amount,
937            address,
938            federation_id,
939        } = payload;
940
941        let address_network = get_network_for_address(&address);
942        let gateway_network = self.network;
943        let Ok(address) = address.require_network(gateway_network) else {
944            return Err(AdminGatewayError::WithdrawError {
945                failure_reason: format!(
946                    "Gateway is running on network {gateway_network}, but provided withdraw address is for network {address_network}"
947                ),
948            });
949        };
950
951        let client = self.select_client(federation_id).await?;
952        let wallet_module = client.value().get_first_module::<WalletClientModule>()?;
953
954        // TODO: Fees should probably be passed in as a parameter
955        let (amount, fees) = match amount {
956            // If the amount is "all", then we need to subtract the fees from
957            // the amount we are withdrawing
958            BitcoinAmountOrAll::All => {
959                let balance = bitcoin::Amount::from_sat(
960                    client
961                        .value()
962                        .get_balance_for_btc()
963                        .await
964                        .map_err(|err| {
965                            AdminGatewayError::Unexpected(anyhow!(
966                                "Balance not available: {}",
967                                err.fmt_compact_anyhow()
968                            ))
969                        })?
970                        .msats
971                        / 1000,
972                );
973                let fees = wallet_module.get_withdraw_fees(&address, balance).await?;
974                let withdraw_amount = balance.checked_sub(fees.amount());
975                if withdraw_amount.is_none() {
976                    return Err(AdminGatewayError::WithdrawError {
977                        failure_reason: format!(
978                            "Insufficient funds. Balance: {balance} Fees: {fees:?}"
979                        ),
980                    });
981                }
982                (withdraw_amount.unwrap(), fees)
983            }
984            BitcoinAmountOrAll::Amount(amount) => (
985                amount,
986                wallet_module.get_withdraw_fees(&address, amount).await?,
987            ),
988        };
989
990        let operation_id = wallet_module.withdraw(&address, amount, fees, ()).await?;
991        let mut updates = wallet_module
992            .subscribe_withdraw_updates(operation_id)
993            .await?
994            .into_stream();
995
996        while let Some(update) = updates.next().await {
997            match update {
998                WithdrawState::Succeeded(txid) => {
999                    info!(target: LOG_GATEWAY, amount = %amount, address = %address, "Sent funds");
1000                    return Ok(WithdrawResponse { txid, fees });
1001                }
1002                WithdrawState::Failed(e) => {
1003                    return Err(AdminGatewayError::WithdrawError { failure_reason: e });
1004                }
1005                WithdrawState::Created => {}
1006            }
1007        }
1008
1009        Err(AdminGatewayError::WithdrawError {
1010            failure_reason: "Ran out of state updates while withdrawing".to_string(),
1011        })
1012    }
1013
1014    /// Creates an invoice that is directly payable to the gateway's lightning
1015    /// node.
1016    async fn handle_create_invoice_for_operator_msg(
1017        &self,
1018        payload: CreateInvoiceForOperatorPayload,
1019    ) -> AdminResult<Bolt11Invoice> {
1020        let GatewayState::Running { lightning_context } = self.get_state().await else {
1021            return Err(AdminGatewayError::Lightning(
1022                LightningRpcError::FailedToConnect,
1023            ));
1024        };
1025
1026        Bolt11Invoice::from_str(
1027            &lightning_context
1028                .lnrpc
1029                .create_invoice(CreateInvoiceRequest {
1030                    payment_hash: None, /* Empty payment hash indicates an invoice payable
1031                                         * directly to the gateway. */
1032                    amount_msat: payload.amount_msats,
1033                    expiry_secs: payload.expiry_secs.unwrap_or(3600),
1034                    description: payload.description.map(InvoiceDescription::Direct),
1035                })
1036                .await?
1037                .invoice,
1038        )
1039        .map_err(|e| {
1040            AdminGatewayError::Lightning(LightningRpcError::InvalidMetadata {
1041                failure_reason: e.to_string(),
1042            })
1043        })
1044    }
1045
1046    /// Requests the gateway to pay an outgoing LN invoice using its own funds.
1047    /// Returns the payment hash's preimage on success.
1048    async fn handle_pay_invoice_for_operator_msg(
1049        &self,
1050        payload: PayInvoiceForOperatorPayload,
1051    ) -> AdminResult<Preimage> {
1052        // Those are the ldk defaults
1053        const BASE_FEE: u64 = 50;
1054        const FEE_DENOMINATOR: u64 = 100;
1055        const MAX_DELAY: u64 = 1008;
1056
1057        let GatewayState::Running { lightning_context } = self.get_state().await else {
1058            return Err(AdminGatewayError::Lightning(
1059                LightningRpcError::FailedToConnect,
1060            ));
1061        };
1062
1063        let max_fee = BASE_FEE
1064            + payload
1065                .invoice
1066                .amount_milli_satoshis()
1067                .context("Invoice is missing amount")?
1068                .saturating_div(FEE_DENOMINATOR);
1069
1070        let res = lightning_context
1071            .lnrpc
1072            .pay(payload.invoice, MAX_DELAY, Amount::from_msats(max_fee))
1073            .await?;
1074        Ok(res.preimage)
1075    }
1076
1077    /// Requests the gateway to pay an outgoing LN invoice on behalf of a
1078    /// Fedimint client. Returns the payment hash's preimage on success.
1079    async fn handle_pay_invoice_msg(
1080        &self,
1081        payload: fedimint_ln_client::pay::PayInvoicePayload,
1082    ) -> Result<Preimage> {
1083        let GatewayState::Running { .. } = self.get_state().await else {
1084            return Err(PublicGatewayError::Lightning(
1085                LightningRpcError::FailedToConnect,
1086            ));
1087        };
1088
1089        debug!(target: LOG_GATEWAY, "Handling pay invoice message");
1090        let client = self.select_client(payload.federation_id).await?;
1091        let contract_id = payload.contract_id;
1092        let gateway_module = &client
1093            .value()
1094            .get_first_module::<GatewayClientModule>()
1095            .map_err(LNv1Error::OutgoingPayment)
1096            .map_err(PublicGatewayError::LNv1)?;
1097        let operation_id = gateway_module
1098            .gateway_pay_bolt11_invoice(payload)
1099            .await
1100            .map_err(LNv1Error::OutgoingPayment)
1101            .map_err(PublicGatewayError::LNv1)?;
1102        let mut updates = gateway_module
1103            .gateway_subscribe_ln_pay(operation_id)
1104            .await
1105            .map_err(LNv1Error::OutgoingPayment)
1106            .map_err(PublicGatewayError::LNv1)?
1107            .into_stream();
1108        while let Some(update) = updates.next().await {
1109            match update {
1110                GatewayExtPayStates::Success { preimage, .. } => {
1111                    debug!(target: LOG_GATEWAY, contract_id = %contract_id, "Successfully paid invoice");
1112                    return Ok(preimage);
1113                }
1114                GatewayExtPayStates::Fail {
1115                    error,
1116                    error_message,
1117                } => {
1118                    return Err(PublicGatewayError::LNv1(LNv1Error::OutgoingContract {
1119                        error: Box::new(error),
1120                        message: format!(
1121                            "{error_message} while paying invoice with contract id {contract_id}"
1122                        ),
1123                    }));
1124                }
1125                GatewayExtPayStates::Canceled { error } => {
1126                    return Err(PublicGatewayError::LNv1(LNv1Error::OutgoingContract {
1127                        error: Box::new(error.clone()),
1128                        message: format!(
1129                            "Cancelled with {error} while paying invoice with contract id {contract_id}"
1130                        ),
1131                    }));
1132                }
1133                GatewayExtPayStates::Created => {
1134                    debug!(target: LOG_GATEWAY, contract_id = %contract_id, "Start pay invoice state machine");
1135                }
1136                other => {
1137                    debug!(target: LOG_GATEWAY, state = ?other, contract_id = %contract_id, "Got state while paying invoice");
1138                }
1139            }
1140        }
1141
1142        Err(PublicGatewayError::LNv1(LNv1Error::OutgoingPayment(
1143            anyhow!("Ran out of state updates while paying invoice"),
1144        )))
1145    }
1146
1147    /// Handles a connection request to join a new federation. The gateway will
1148    /// download the federation's client configuration, construct a new
1149    /// client, registers, the gateway with the federation, and persists the
1150    /// necessary config to reconstruct the client when restarting the gateway.
1151    pub async fn handle_connect_federation(
1152        &self,
1153        payload: ConnectFedPayload,
1154    ) -> AdminResult<FederationInfo> {
1155        let GatewayState::Running { lightning_context } = self.get_state().await else {
1156            return Err(AdminGatewayError::Lightning(
1157                LightningRpcError::FailedToConnect,
1158            ));
1159        };
1160
1161        let invite_code = InviteCode::from_str(&payload.invite_code).map_err(|e| {
1162            AdminGatewayError::ClientCreationError(anyhow!(format!(
1163                "Invalid federation member string {e:?}"
1164            )))
1165        })?;
1166
1167        #[cfg(feature = "tor")]
1168        let connector = match &payload.use_tor {
1169            Some(true) => Connector::tor(),
1170            Some(false) => Connector::default(),
1171            None => {
1172                debug!(target: LOG_GATEWAY, "Missing `use_tor` payload field, defaulting to `Connector::Tcp` variant!");
1173                Connector::default()
1174            }
1175        };
1176
1177        #[cfg(not(feature = "tor"))]
1178        let connector = Connector::default();
1179
1180        let federation_id = invite_code.federation_id();
1181
1182        let mut federation_manager = self.federation_manager.write().await;
1183
1184        // Check if this federation has already been registered
1185        if federation_manager.has_federation(federation_id) {
1186            return Err(AdminGatewayError::ClientCreationError(anyhow!(
1187                "Federation has already been registered"
1188            )));
1189        }
1190
1191        // The gateway deterministically assigns a unique identifier (u64) to each
1192        // federation connected.
1193        let federation_index = federation_manager.pop_next_index()?;
1194
1195        let federation_config = FederationConfig {
1196            invite_code,
1197            federation_index,
1198            lightning_fee: self.default_routing_fees,
1199            transaction_fee: self.default_transaction_fees,
1200            connector,
1201        };
1202
1203        let recover = payload.recover.unwrap_or(false);
1204        if recover {
1205            self.client_builder
1206                .recover(
1207                    federation_config.clone(),
1208                    Arc::new(self.clone()),
1209                    &self.mnemonic,
1210                )
1211                .await?;
1212        }
1213
1214        let client = self
1215            .client_builder
1216            .build(
1217                federation_config.clone(),
1218                Arc::new(self.clone()),
1219                &self.mnemonic,
1220            )
1221            .await?;
1222
1223        if recover {
1224            client.wait_for_all_active_state_machines().await?;
1225        }
1226
1227        // Instead of using `FederationManager::federation_info`, we manually create
1228        // federation info here because short channel id is not yet persisted.
1229        let federation_info = FederationInfo {
1230            federation_id,
1231            federation_name: federation_manager.federation_name(&client).await,
1232            balance_msat: client.get_balance_for_btc().await.unwrap_or_else(|err| {
1233                warn!(
1234                    target: LOG_GATEWAY,
1235                    err = %err.fmt_compact_anyhow(),
1236                    %federation_id,
1237                    "Balance not immediately available after joining/recovering."
1238                );
1239                Amount::default()
1240            }),
1241            config: federation_config.clone(),
1242        };
1243
1244        Self::check_lnv1_federation_network(&client, self.network).await?;
1245        Self::check_lnv2_federation_network(&client, self.network).await?;
1246        if matches!(self.lightning_mode, LightningMode::Lnd { .. }) {
1247            client
1248                .get_first_module::<GatewayClientModule>()?
1249                .try_register_with_federation(
1250                    // Route hints will be updated in the background
1251                    Vec::new(),
1252                    GW_ANNOUNCEMENT_TTL,
1253                    federation_config.lightning_fee.into(),
1254                    lightning_context,
1255                    self.versioned_api.clone(),
1256                    self.gateway_id,
1257                )
1258                .await;
1259        }
1260
1261        // no need to enter span earlier, because connect-fed has a span
1262        federation_manager.add_client(
1263            federation_index,
1264            Spanned::new(
1265                info_span!(target: LOG_GATEWAY, "client", federation_id=%federation_id.clone()),
1266                async { client },
1267            )
1268            .await,
1269        );
1270
1271        let mut dbtx = self.gateway_db.begin_transaction().await;
1272        dbtx.save_federation_config(&federation_config).await;
1273        dbtx.commit_tx().await;
1274        debug!(
1275            target: LOG_GATEWAY,
1276            federation_id = %federation_id,
1277            federation_index = %federation_index,
1278            "Federation connected"
1279        );
1280
1281        Ok(federation_info)
1282    }
1283
1284    /// Handle a request to have the Gateway leave a federation. The Gateway
1285    /// will request the federation to remove the registration record and
1286    /// the gateway will remove the configuration needed to construct the
1287    /// federation client.
1288    pub async fn handle_leave_federation(
1289        &self,
1290        payload: LeaveFedPayload,
1291    ) -> AdminResult<FederationInfo> {
1292        // Lock the federation manager before starting the db transaction to reduce the
1293        // chance of db write conflicts.
1294        let mut federation_manager = self.federation_manager.write().await;
1295        let mut dbtx = self.gateway_db.begin_transaction().await;
1296
1297        let federation_info = federation_manager
1298            .leave_federation(payload.federation_id, &mut dbtx.to_ref_nc())
1299            .await?;
1300
1301        dbtx.remove_federation_config(payload.federation_id).await;
1302        dbtx.commit_tx().await;
1303        Ok(federation_info)
1304    }
1305
1306    /// Handles a request for the gateway to backup a connected federation's
1307    /// ecash.
1308    pub async fn handle_backup_msg(
1309        &self,
1310        BackupPayload { federation_id }: BackupPayload,
1311    ) -> AdminResult<()> {
1312        let federation_manager = self.federation_manager.read().await;
1313        let client = federation_manager
1314            .client(&federation_id)
1315            .ok_or(AdminGatewayError::ClientCreationError(anyhow::anyhow!(
1316                format!("Gateway has not connected to {federation_id}")
1317            )))?
1318            .value();
1319        let metadata: BTreeMap<String, String> = BTreeMap::new();
1320        client
1321            .backup_to_federation(fedimint_client::backup::Metadata::from_json_serialized(
1322                metadata,
1323            ))
1324            .await?;
1325        Ok(())
1326    }
1327
1328    /// Handles an authenticated request for the gateway's mnemonic. This also
1329    /// returns a vector of federations that are not using the mnemonic
1330    /// backup strategy.
1331    pub async fn handle_mnemonic_msg(&self) -> AdminResult<MnemonicResponse> {
1332        let words = self
1333            .mnemonic
1334            .words()
1335            .map(std::string::ToString::to_string)
1336            .collect::<Vec<_>>();
1337        let all_federations = self
1338            .federation_manager
1339            .read()
1340            .await
1341            .get_all_federation_configs()
1342            .await
1343            .keys()
1344            .copied()
1345            .collect::<BTreeSet<_>>();
1346        let legacy_federations = self.client_builder.legacy_federations(all_federations);
1347        let mnemonic_response = MnemonicResponse {
1348            mnemonic: words,
1349            legacy_federations,
1350        };
1351        Ok(mnemonic_response)
1352    }
1353
1354    /// Handles a request to change the lightning or transaction fees for all
1355    /// federations or a federation specified by the `FederationId`.
1356    pub async fn handle_set_fees_msg(
1357        &self,
1358        SetFeesPayload {
1359            federation_id,
1360            lightning_base,
1361            lightning_parts_per_million,
1362            transaction_base,
1363            transaction_parts_per_million,
1364        }: SetFeesPayload,
1365    ) -> AdminResult<()> {
1366        let mut dbtx = self.gateway_db.begin_transaction().await;
1367        let mut fed_configs = if let Some(fed_id) = federation_id {
1368            dbtx.load_federation_configs()
1369                .await
1370                .into_iter()
1371                .filter(|(id, _)| *id == fed_id)
1372                .collect::<BTreeMap<_, _>>()
1373        } else {
1374            dbtx.load_federation_configs().await
1375        };
1376
1377        let federation_manager = self.federation_manager.read().await;
1378
1379        for (federation_id, config) in &mut fed_configs {
1380            let mut lightning_fee = config.lightning_fee;
1381            if let Some(lightning_base) = lightning_base {
1382                lightning_fee.base = lightning_base;
1383            }
1384
1385            if let Some(lightning_ppm) = lightning_parts_per_million {
1386                lightning_fee.parts_per_million = lightning_ppm;
1387            }
1388
1389            let mut transaction_fee = config.transaction_fee;
1390            if let Some(transaction_base) = transaction_base {
1391                transaction_fee.base = transaction_base;
1392            }
1393
1394            if let Some(transaction_ppm) = transaction_parts_per_million {
1395                transaction_fee.parts_per_million = transaction_ppm;
1396            }
1397
1398            let client =
1399                federation_manager
1400                    .client(federation_id)
1401                    .ok_or(FederationNotConnected {
1402                        federation_id_prefix: federation_id.to_prefix(),
1403                    })?;
1404            let client_config = client.value().config().await;
1405            let contains_lnv2 = client_config
1406                .modules
1407                .values()
1408                .any(|m| fedimint_lnv2_common::LightningCommonInit::KIND == m.kind);
1409
1410            // Check if the lightning fee + transaction fee is higher than the send limit
1411            let send_fees = lightning_fee + transaction_fee;
1412            if contains_lnv2 && send_fees.gt(&PaymentFee::SEND_FEE_LIMIT) {
1413                return Err(AdminGatewayError::GatewayConfigurationError(format!(
1414                    "Total Send fees exceeded {}",
1415                    PaymentFee::SEND_FEE_LIMIT
1416                )));
1417            }
1418
1419            // Check if the transaction fee is higher than the receive limit
1420            if contains_lnv2 && transaction_fee.gt(&PaymentFee::RECEIVE_FEE_LIMIT) {
1421                return Err(AdminGatewayError::GatewayConfigurationError(format!(
1422                    "Transaction fees exceeded RECEIVE LIMIT {}",
1423                    PaymentFee::RECEIVE_FEE_LIMIT
1424                )));
1425            }
1426
1427            config.lightning_fee = lightning_fee;
1428            config.transaction_fee = transaction_fee;
1429            dbtx.save_federation_config(config).await;
1430        }
1431
1432        dbtx.commit_tx().await;
1433
1434        if matches!(self.lightning_mode, LightningMode::Lnd { .. }) {
1435            let register_task_group = TaskGroup::new();
1436
1437            self.register_federations(&fed_configs, &register_task_group)
1438                .await;
1439        }
1440
1441        Ok(())
1442    }
1443
1444    /// Generates an onchain address to fund the gateway's lightning node.
1445    pub async fn handle_get_ln_onchain_address_msg(&self) -> AdminResult<Address> {
1446        let context = self.get_lightning_context().await?;
1447        let response = context.lnrpc.get_ln_onchain_address().await?;
1448
1449        let address = Address::from_str(&response.address).map_err(|e| {
1450            AdminGatewayError::Lightning(LightningRpcError::InvalidMetadata {
1451                failure_reason: e.to_string(),
1452            })
1453        })?;
1454
1455        address.require_network(self.network).map_err(|e| {
1456            AdminGatewayError::Lightning(LightningRpcError::InvalidMetadata {
1457                failure_reason: e.to_string(),
1458            })
1459        })
1460    }
1461
1462    /// Instructs the Gateway's Lightning node to open a channel to a peer
1463    /// specified by `pubkey`.
1464    pub async fn handle_open_channel_msg(&self, payload: OpenChannelRequest) -> AdminResult<Txid> {
1465        info!(target: LOG_GATEWAY, pubkey = %payload.pubkey, host = %payload.host, amount = %payload.channel_size_sats, "Opening Lightning channel...");
1466        let context = self.get_lightning_context().await?;
1467        let res = context.lnrpc.open_channel(payload).await?;
1468        info!(target: LOG_GATEWAY, txid = %res.funding_txid, "Initiated channel open");
1469        Txid::from_str(&res.funding_txid).map_err(|e| {
1470            AdminGatewayError::Lightning(LightningRpcError::InvalidMetadata {
1471                failure_reason: format!("Received invalid channel funding txid string {e}"),
1472            })
1473        })
1474    }
1475
1476    /// Instructs the Gateway's Lightning node to close all channels with a peer
1477    /// specified by `pubkey`.
1478    pub async fn handle_close_channels_with_peer_msg(
1479        &self,
1480        payload: CloseChannelsWithPeerRequest,
1481    ) -> AdminResult<CloseChannelsWithPeerResponse> {
1482        let context = self.get_lightning_context().await?;
1483        let response = context.lnrpc.close_channels_with_peer(payload).await?;
1484        Ok(response)
1485    }
1486
1487    /// Returns a list of Lightning network channels from the Gateway's
1488    /// Lightning node.
1489    pub async fn handle_list_channels_msg(
1490        &self,
1491    ) -> AdminResult<Vec<fedimint_gateway_common::ChannelInfo>> {
1492        let context = self.get_lightning_context().await?;
1493        let response = context.lnrpc.list_channels().await?;
1494        Ok(response.channels)
1495    }
1496
1497    /// Send funds from the gateway's lightning node on-chain wallet.
1498    pub async fn handle_send_onchain_msg(&self, payload: SendOnchainRequest) -> AdminResult<Txid> {
1499        let context = self.get_lightning_context().await?;
1500        let response = context.lnrpc.send_onchain(payload).await?;
1501        Txid::from_str(&response.txid).map_err(|e| AdminGatewayError::WithdrawError {
1502            failure_reason: format!("Failed to parse withdrawal TXID: {e}"),
1503        })
1504    }
1505
1506    /// Trigger rechecking for deposits on an address
1507    pub async fn handle_recheck_address_msg(
1508        &self,
1509        payload: DepositAddressRecheckPayload,
1510    ) -> AdminResult<()> {
1511        self.select_client(payload.federation_id)
1512            .await?
1513            .value()
1514            .get_first_module::<WalletClientModule>()
1515            .expect("Must have client module")
1516            .recheck_pegin_address_by_address(payload.address)
1517            .await?;
1518        Ok(())
1519    }
1520
1521    /// Returns the ecash, lightning, and onchain balances for the gateway and
1522    /// the gateway's lightning node.
1523    pub async fn handle_get_balances_msg(&self) -> AdminResult<GatewayBalances> {
1524        let dbtx = self.gateway_db.begin_transaction_nc().await;
1525        let federation_infos = self
1526            .federation_manager
1527            .read()
1528            .await
1529            .federation_info_all_federations(dbtx)
1530            .await;
1531
1532        let ecash_balances: Vec<FederationBalanceInfo> = federation_infos
1533            .iter()
1534            .map(|federation_info| FederationBalanceInfo {
1535                federation_id: federation_info.federation_id,
1536                ecash_balance_msats: Amount {
1537                    msats: federation_info.balance_msat.msats,
1538                },
1539            })
1540            .collect();
1541
1542        let context = self.get_lightning_context().await?;
1543        let lightning_node_balances = context.lnrpc.get_balances().await?;
1544
1545        Ok(GatewayBalances {
1546            onchain_balance_sats: lightning_node_balances.onchain_balance_sats,
1547            lightning_balance_msats: lightning_node_balances.lightning_balance_msats,
1548            ecash_balances,
1549            inbound_lightning_liquidity_msats: lightning_node_balances
1550                .inbound_lightning_liquidity_msats,
1551        })
1552    }
1553
1554    // Handles a request the spend the gateway's ecash for a given federation.
1555    pub async fn handle_spend_ecash_msg(
1556        &self,
1557        payload: SpendEcashPayload,
1558    ) -> AdminResult<SpendEcashResponse> {
1559        let client = self
1560            .select_client(payload.federation_id)
1561            .await?
1562            .into_value();
1563        let mint_module = client.get_first_module::<MintClientModule>()?;
1564        let timeout = Duration::from_secs(payload.timeout);
1565        let (operation_id, notes) = if payload.allow_overpay {
1566            let (operation_id, notes) = mint_module
1567                .spend_notes_with_selector(
1568                    &SelectNotesWithAtleastAmount,
1569                    payload.amount,
1570                    timeout,
1571                    payload.include_invite,
1572                    (),
1573                )
1574                .await?;
1575
1576            let overspend_amount = notes.total_amount().saturating_sub(payload.amount);
1577            if overspend_amount != Amount::ZERO {
1578                warn!(
1579                    target: LOG_GATEWAY,
1580                    overspend_amount = %overspend_amount,
1581                    "Selected notes worth more than requested",
1582                );
1583            }
1584
1585            (operation_id, notes)
1586        } else {
1587            mint_module
1588                .spend_notes_with_selector(
1589                    &SelectNotesWithExactAmount,
1590                    payload.amount,
1591                    timeout,
1592                    payload.include_invite,
1593                    (),
1594                )
1595                .await?
1596        };
1597
1598        debug!(target: LOG_GATEWAY, ?operation_id, ?notes, "Spend ecash notes");
1599
1600        Ok(SpendEcashResponse {
1601            operation_id,
1602            notes,
1603        })
1604    }
1605
1606    /// Handles a request to receive ecash into the gateway.
1607    pub async fn handle_receive_ecash_msg(
1608        &self,
1609        payload: ReceiveEcashPayload,
1610    ) -> Result<ReceiveEcashResponse> {
1611        let amount = payload.notes.total_amount();
1612        let client = self
1613            .federation_manager
1614            .read()
1615            .await
1616            .get_client_for_federation_id_prefix(payload.notes.federation_id_prefix())
1617            .ok_or(FederationNotConnected {
1618                federation_id_prefix: payload.notes.federation_id_prefix(),
1619            })?;
1620        let mint = client
1621            .value()
1622            .get_first_module::<MintClientModule>()
1623            .map_err(|e| PublicGatewayError::ReceiveEcashError {
1624                failure_reason: format!("Mint module does not exist: {e:?}"),
1625            })?;
1626
1627        let operation_id = mint
1628            .reissue_external_notes(payload.notes, ())
1629            .await
1630            .map_err(|e| PublicGatewayError::ReceiveEcashError {
1631                failure_reason: e.to_string(),
1632            })?;
1633        if payload.wait {
1634            let mut updates = mint
1635                .subscribe_reissue_external_notes(operation_id)
1636                .await
1637                .unwrap()
1638                .into_stream();
1639
1640            while let Some(update) = updates.next().await {
1641                if let fedimint_mint_client::ReissueExternalNotesState::Failed(e) = update {
1642                    return Err(PublicGatewayError::ReceiveEcashError {
1643                        failure_reason: e.to_string(),
1644                    });
1645                }
1646            }
1647        }
1648
1649        Ok(ReceiveEcashResponse { amount })
1650    }
1651
1652    /// Instructs the gateway to shutdown, but only after all incoming payments
1653    /// have been handlded.
1654    pub async fn handle_shutdown_msg(&self, task_group: TaskGroup) -> AdminResult<()> {
1655        // Take the write lock on the state so that no additional payments are processed
1656        let mut state_guard = self.state.write().await;
1657        if let GatewayState::Running { lightning_context } = state_guard.clone() {
1658            *state_guard = GatewayState::ShuttingDown { lightning_context };
1659
1660            self.federation_manager
1661                .read()
1662                .await
1663                .wait_for_incoming_payments()
1664                .await?;
1665        }
1666
1667        let tg = task_group.clone();
1668        tg.spawn("Kill Gateway", |_task_handle| async {
1669            if let Err(err) = task_group.shutdown_join_all(Duration::from_secs(180)).await {
1670                warn!(target: LOG_GATEWAY, err = %err.fmt_compact_anyhow(), "Error shutting down gateway");
1671            }
1672        });
1673        Ok(())
1674    }
1675
1676    /// Queries the client log for payment events and returns to the user.
1677    pub async fn handle_payment_log_msg(
1678        &self,
1679        PaymentLogPayload {
1680            end_position,
1681            pagination_size,
1682            federation_id,
1683            event_kinds,
1684        }: PaymentLogPayload,
1685    ) -> AdminResult<PaymentLogResponse> {
1686        const BATCH_SIZE: u64 = 10_000;
1687        let federation_manager = self.federation_manager.read().await;
1688        let client = federation_manager
1689            .client(&federation_id)
1690            .ok_or(FederationNotConnected {
1691                federation_id_prefix: federation_id.to_prefix(),
1692            })?
1693            .value();
1694
1695        let event_kinds = if event_kinds.is_empty() {
1696            ALL_GATEWAY_EVENTS.to_vec()
1697        } else {
1698            event_kinds
1699        };
1700
1701        let end_position = if let Some(position) = end_position {
1702            position
1703        } else {
1704            let mut dbtx = client.db().begin_transaction_nc().await;
1705            dbtx.get_next_event_log_id().await
1706        };
1707
1708        let mut start_position = end_position.saturating_sub(BATCH_SIZE);
1709
1710        let mut payment_log = Vec::new();
1711
1712        while payment_log.len() < pagination_size {
1713            let batch = client.get_event_log(Some(start_position), BATCH_SIZE).await;
1714            let mut filtered_batch = batch
1715                .into_iter()
1716                .filter(|e| e.event_id <= end_position && event_kinds.contains(&e.event_kind))
1717                .collect::<Vec<_>>();
1718            filtered_batch.reverse();
1719            payment_log.extend(filtered_batch);
1720
1721            // Compute the start position for the next batch query
1722            start_position = start_position.saturating_sub(BATCH_SIZE);
1723
1724            if start_position == EventLogId::LOG_START {
1725                break;
1726            }
1727        }
1728
1729        // Truncate the payment log to the expected pagination size
1730        payment_log.truncate(pagination_size);
1731
1732        Ok(PaymentLogResponse(payment_log))
1733    }
1734
1735    /// Computes the 24 hour payment summary statistics for this gateway.
1736    /// Combines the LNv1 and LNv2 stats together.
1737    pub async fn handle_payment_summary_msg(
1738        &self,
1739        PaymentSummaryPayload {
1740            start_millis,
1741            end_millis,
1742        }: PaymentSummaryPayload,
1743    ) -> AdminResult<PaymentSummaryResponse> {
1744        let federation_manager = self.federation_manager.read().await;
1745        let fed_configs = federation_manager.get_all_federation_configs().await;
1746        let federation_ids = fed_configs.keys().collect::<Vec<_>>();
1747        let start = UNIX_EPOCH + Duration::from_millis(start_millis);
1748        let end = UNIX_EPOCH + Duration::from_millis(end_millis);
1749
1750        if start > end {
1751            return Err(AdminGatewayError::Unexpected(anyhow!("Invalid time range")));
1752        }
1753
1754        let mut outgoing = StructuredPaymentEvents::default();
1755        let mut incoming = StructuredPaymentEvents::default();
1756        for fed_id in federation_ids {
1757            let client = federation_manager
1758                .client(fed_id)
1759                .expect("No client available")
1760                .value();
1761            let all_events = &get_events_for_duration(client, start, end).await;
1762
1763            let (mut lnv1_outgoing, mut lnv1_incoming) = compute_lnv1_stats(all_events);
1764            let (mut lnv2_outgoing, mut lnv2_incoming) = compute_lnv2_stats(all_events);
1765            outgoing.combine(&mut lnv1_outgoing);
1766            incoming.combine(&mut lnv1_incoming);
1767            outgoing.combine(&mut lnv2_outgoing);
1768            incoming.combine(&mut lnv2_incoming);
1769        }
1770
1771        Ok(PaymentSummaryResponse {
1772            outgoing: PaymentStats::compute(&outgoing),
1773            incoming: PaymentStats::compute(&incoming),
1774        })
1775    }
1776
1777    /// Retrieves an invoice by the payment hash if it exists, otherwise returns
1778    /// `None`.
1779    pub async fn handle_get_invoice_msg(
1780        &self,
1781        payload: GetInvoiceRequest,
1782    ) -> AdminResult<Option<GetInvoiceResponse>> {
1783        let lightning_context = self.get_lightning_context().await?;
1784        let invoice = lightning_context.lnrpc.get_invoice(payload).await?;
1785        Ok(invoice)
1786    }
1787
1788    pub async fn handle_list_transactions_msg(
1789        &self,
1790        payload: ListTransactionsPayload,
1791    ) -> AdminResult<ListTransactionsResponse> {
1792        let lightning_context = self.get_lightning_context().await?;
1793        let response = lightning_context
1794            .lnrpc
1795            .list_transactions(payload.start_secs, payload.end_secs)
1796            .await?;
1797        Ok(response)
1798    }
1799
1800    /// Creates a BOLT12 offer using the gateway's lightning node
1801    pub async fn handle_create_offer_for_operator_msg(
1802        &self,
1803        payload: CreateOfferPayload,
1804    ) -> AdminResult<CreateOfferResponse> {
1805        let lightning_context = self.get_lightning_context().await?;
1806        let offer = lightning_context.lnrpc.create_offer(
1807            payload.amount,
1808            payload.description,
1809            payload.expiry_secs,
1810            payload.quantity,
1811        )?;
1812        Ok(CreateOfferResponse { offer })
1813    }
1814
1815    /// Pays a BOLT12 offer using the gateway's lightning node
1816    pub async fn handle_pay_offer_for_operator_msg(
1817        &self,
1818        payload: PayOfferPayload,
1819    ) -> AdminResult<PayOfferResponse> {
1820        let lightning_context = self.get_lightning_context().await?;
1821        let preimage = lightning_context
1822            .lnrpc
1823            .pay_offer(
1824                payload.offer,
1825                payload.quantity,
1826                payload.amount,
1827                payload.payer_note,
1828            )
1829            .await?;
1830        Ok(PayOfferResponse {
1831            preimage: preimage.to_string(),
1832        })
1833    }
1834
1835    /// Registers the gateway with each specified federation.
1836    async fn register_federations(
1837        &self,
1838        federations: &BTreeMap<FederationId, FederationConfig>,
1839        register_task_group: &TaskGroup,
1840    ) {
1841        if let Ok(lightning_context) = self.get_lightning_context().await {
1842            let route_hints = lightning_context
1843                .lnrpc
1844                .parsed_route_hints(self.num_route_hints)
1845                .await;
1846            if route_hints.is_empty() {
1847                warn!(target: LOG_GATEWAY, "Gateway did not retrieve any route hints, may reduce receive success rate.");
1848            }
1849
1850            for (federation_id, federation_config) in federations {
1851                let fed_manager = self.federation_manager.read().await;
1852                if let Some(client) = fed_manager.client(federation_id) {
1853                    let client_arc = client.clone().into_value();
1854                    let route_hints = route_hints.clone();
1855                    let lightning_context = lightning_context.clone();
1856                    let federation_config = federation_config.clone();
1857                    let api = self.versioned_api.clone();
1858                    let gateway_id = self.gateway_id;
1859
1860                    if let Err(err) = register_task_group
1861                        .spawn_cancellable("register_federation", async move {
1862                            let gateway_client = client_arc
1863                                .get_first_module::<GatewayClientModule>()
1864                                .expect("No GatewayClientModule exists");
1865                            gateway_client
1866                                .try_register_with_federation(
1867                                    route_hints,
1868                                    GW_ANNOUNCEMENT_TTL,
1869                                    federation_config.lightning_fee.into(),
1870                                    lightning_context,
1871                                    api,
1872                                    gateway_id,
1873                                )
1874                                .await;
1875                        })
1876                        .await
1877                    {
1878                        warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Failed to shutdown register federation task");
1879                    }
1880                }
1881            }
1882        }
1883    }
1884
1885    /// Retrieves a `ClientHandleArc` from the Gateway's in memory structures
1886    /// that keep track of available clients, given a `federation_id`.
1887    pub async fn select_client(
1888        &self,
1889        federation_id: FederationId,
1890    ) -> std::result::Result<Spanned<fedimint_client::ClientHandleArc>, FederationNotConnected>
1891    {
1892        self.federation_manager
1893            .read()
1894            .await
1895            .client(&federation_id)
1896            .cloned()
1897            .ok_or(FederationNotConnected {
1898                federation_id_prefix: federation_id.to_prefix(),
1899            })
1900    }
1901
1902    /// Loads a mnemonic from the database or generates a new one if the
1903    /// mnemonic does not exist. Before generating a new mnemonic, this
1904    /// function will check if a mnemonic has been provided in the environment
1905    /// variable and use that if provided.
1906    async fn load_or_generate_mnemonic(gateway_db: &Database) -> AdminResult<Mnemonic> {
1907        Ok(
1908            if let Ok(entropy) = Client::load_decodable_client_secret::<Vec<u8>>(gateway_db).await {
1909                Mnemonic::from_entropy(&entropy)
1910                    .map_err(|e| AdminGatewayError::MnemonicError(anyhow!(e.to_string())))?
1911            } else {
1912                let mnemonic = if let Ok(words) = std::env::var(FM_GATEWAY_MNEMONIC_ENV) {
1913                    info!(target: LOG_GATEWAY, "Using provided mnemonic from environment variable");
1914                    Mnemonic::parse_in_normalized(Language::English, words.as_str()).map_err(
1915                        |e| {
1916                            AdminGatewayError::MnemonicError(anyhow!(format!(
1917                                "Seed phrase provided in environment was invalid {e:?}"
1918                            )))
1919                        },
1920                    )?
1921                } else {
1922                    debug!(target: LOG_GATEWAY, "Generating mnemonic and writing entropy to client storage");
1923                    Bip39RootSecretStrategy::<12>::random(&mut OsRng)
1924                };
1925
1926                Client::store_encodable_client_secret(gateway_db, mnemonic.to_entropy())
1927                    .await
1928                    .map_err(AdminGatewayError::MnemonicError)?;
1929                mnemonic
1930            },
1931        )
1932    }
1933
1934    /// Reads the connected federation client configs from the Gateway's
1935    /// database and reconstructs the clients necessary for interacting with
1936    /// connection federations.
1937    async fn load_clients(&self) -> AdminResult<()> {
1938        let mut federation_manager = self.federation_manager.write().await;
1939
1940        let configs = {
1941            let mut dbtx = self.gateway_db.begin_transaction_nc().await;
1942            dbtx.load_federation_configs().await
1943        };
1944
1945        if let Some(max_federation_index) = configs.values().map(|cfg| cfg.federation_index).max() {
1946            federation_manager.set_next_index(max_federation_index + 1);
1947        }
1948
1949        for (federation_id, config) in configs {
1950            let federation_index = config.federation_index;
1951            match Box::pin(Spanned::try_new(
1952                info_span!(target: LOG_GATEWAY, "client", federation_id  = %federation_id.clone()),
1953                self.client_builder
1954                    .build(config, Arc::new(self.clone()), &self.mnemonic),
1955            ))
1956            .await
1957            {
1958                Ok(client) => {
1959                    federation_manager.add_client(federation_index, client);
1960                }
1961                _ => {
1962                    warn!(target: LOG_GATEWAY, federation_id = %federation_id, "Failed to load client");
1963                }
1964            }
1965        }
1966
1967        Ok(())
1968    }
1969
1970    /// Legacy mechanism for registering the Gateway with connected federations.
1971    /// This will spawn a task that will re-register the Gateway with
1972    /// connected federations every 8.5 mins. Only registers the Gateway if it
1973    /// has successfully connected to the Lightning node, so that it can
1974    /// include route hints in the registration.
1975    fn register_clients_timer(&self) {
1976        // Only spawn background registration thread if gateway is LND
1977        if matches!(self.lightning_mode, LightningMode::Lnd { .. }) {
1978            info!(target: LOG_GATEWAY, "Spawning register task...");
1979            let gateway = self.clone();
1980            let register_task_group = self.task_group.make_subgroup();
1981            self.task_group.spawn_cancellable("register clients", async move {
1982                loop {
1983                    let gateway_state = gateway.get_state().await;
1984                    if let GatewayState::Running { .. } = &gateway_state {
1985                        let mut dbtx = gateway.gateway_db.begin_transaction_nc().await;
1986                        let all_federations_configs = dbtx.load_federation_configs().await.into_iter().collect();
1987                        gateway.register_federations(&all_federations_configs, &register_task_group).await;
1988                    } else {
1989                        // We need to retry more often if the gateway is not in the Running state
1990                        const NOT_RUNNING_RETRY: Duration = Duration::from_secs(10);
1991                        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");
1992                        sleep(NOT_RUNNING_RETRY).await;
1993                        continue;
1994                    }
1995
1996                    // Allow a 15% buffer of the TTL before the re-registering gateway
1997                    // with the federations.
1998                    sleep(GW_ANNOUNCEMENT_TTL.mul_f32(0.85)).await;
1999                }
2000            });
2001        }
2002    }
2003
2004    /// Verifies that the supplied `network` matches the Bitcoin network in the
2005    /// connected client's LNv1 configuration.
2006    async fn check_lnv1_federation_network(
2007        client: &ClientHandleArc,
2008        network: Network,
2009    ) -> AdminResult<()> {
2010        let federation_id = client.federation_id();
2011        let config = client.config().await;
2012        let cfg = config
2013            .modules
2014            .values()
2015            .find(|m| LightningCommonInit::KIND == m.kind)
2016            .ok_or(AdminGatewayError::ClientCreationError(anyhow!(format!(
2017                "Federation {federation_id} does not have an LNv1 module"
2018            ))))?;
2019        let ln_cfg: &LightningClientConfig = cfg.cast()?;
2020
2021        if ln_cfg.network.0 != network {
2022            crit!(
2023                target: LOG_GATEWAY,
2024                federation_id = %federation_id,
2025                network = %network,
2026                "Incorrect network for federation",
2027            );
2028            return Err(AdminGatewayError::ClientCreationError(anyhow!(format!(
2029                "Unsupported network {}",
2030                ln_cfg.network
2031            ))));
2032        }
2033
2034        Ok(())
2035    }
2036
2037    /// Verifies that the supplied `network` matches the Bitcoin network in the
2038    /// connected client's LNv2 configuration.
2039    async fn check_lnv2_federation_network(
2040        client: &ClientHandleArc,
2041        network: Network,
2042    ) -> AdminResult<()> {
2043        let federation_id = client.federation_id();
2044        let config = client.config().await;
2045        let cfg = config
2046            .modules
2047            .values()
2048            .find(|m| fedimint_lnv2_common::LightningCommonInit::KIND == m.kind);
2049
2050        // If the federation does not have LNv2, we don't need to verify the network
2051        if let Some(cfg) = cfg {
2052            let ln_cfg: &fedimint_lnv2_common::config::LightningClientConfig = cfg.cast()?;
2053
2054            if ln_cfg.network != network {
2055                crit!(
2056                    target: LOG_GATEWAY,
2057                    federation_id = %federation_id,
2058                    network = %network,
2059                    "Incorrect network for federation",
2060                );
2061                return Err(AdminGatewayError::ClientCreationError(anyhow!(format!(
2062                    "Unsupported network {}",
2063                    ln_cfg.network
2064                ))));
2065            }
2066        }
2067
2068        Ok(())
2069    }
2070
2071    /// Checks the Gateway's current state and returns the proper
2072    /// `LightningContext` if it is available. Sometimes the lightning node
2073    /// will not be connected and this will return an error.
2074    pub async fn get_lightning_context(
2075        &self,
2076    ) -> std::result::Result<LightningContext, LightningRpcError> {
2077        match self.get_state().await {
2078            GatewayState::Running { lightning_context }
2079            | GatewayState::ShuttingDown { lightning_context } => Ok(lightning_context),
2080            _ => Err(LightningRpcError::FailedToConnect),
2081        }
2082    }
2083
2084    /// Iterates through all of the federations the gateway is registered with
2085    /// and requests to remove the registration record.
2086    pub async fn unannounce_from_all_federations(&self) {
2087        if matches!(self.lightning_mode, LightningMode::Lnd { .. }) {
2088            let mut dbtx = self.gateway_db.begin_transaction_nc().await;
2089            let gateway_keypair = dbtx.load_gateway_keypair_assert_exists().await;
2090
2091            self.federation_manager
2092                .read()
2093                .await
2094                .unannounce_from_all_federations(gateway_keypair)
2095                .await;
2096        }
2097    }
2098
2099    fn create_lightning_client(
2100        &self,
2101        runtime: Arc<tokio::runtime::Runtime>,
2102    ) -> Box<dyn ILnRpcClient> {
2103        match self.lightning_mode.clone() {
2104            LightningMode::Lnd {
2105                lnd_rpc_addr,
2106                lnd_tls_cert,
2107                lnd_macaroon,
2108            } => Box::new(GatewayLndClient::new(
2109                lnd_rpc_addr,
2110                lnd_tls_cert,
2111                lnd_macaroon,
2112                None,
2113            )),
2114            LightningMode::Ldk {
2115                lightning_port,
2116                alias,
2117            } => Box::new(
2118                ldk::GatewayLdkClient::new(
2119                    &self.client_builder.data_dir().join(LDK_NODE_DB_FOLDER),
2120                    self.chain_source.clone(),
2121                    self.network,
2122                    lightning_port,
2123                    alias,
2124                    self.mnemonic.clone(),
2125                    runtime,
2126                )
2127                .expect("Failed to create LDK client"),
2128            ),
2129        }
2130    }
2131}
2132
2133// LNv2 Gateway implementation
2134impl Gateway {
2135    /// Retrieves the `PublicKey` of the Gateway module for a given federation
2136    /// for LNv2. This is NOT the same as the `gateway_id`, it is different
2137    /// per-connected federation.
2138    async fn public_key_v2(&self, federation_id: &FederationId) -> Option<PublicKey> {
2139        self.federation_manager
2140            .read()
2141            .await
2142            .client(federation_id)
2143            .map(|client| {
2144                client
2145                    .value()
2146                    .get_first_module::<GatewayClientModuleV2>()
2147                    .expect("Must have client module")
2148                    .keypair
2149                    .public_key()
2150            })
2151    }
2152
2153    /// Returns payment information that LNv2 clients can use to instruct this
2154    /// Gateway to pay an invoice or receive a payment.
2155    pub async fn routing_info_v2(
2156        &self,
2157        federation_id: &FederationId,
2158    ) -> Result<Option<RoutingInfo>> {
2159        let context = self.get_lightning_context().await?;
2160
2161        let mut dbtx = self.gateway_db.begin_transaction_nc().await;
2162        let fed_config = dbtx.load_federation_config(*federation_id).await.ok_or(
2163            PublicGatewayError::FederationNotConnected(FederationNotConnected {
2164                federation_id_prefix: federation_id.to_prefix(),
2165            }),
2166        )?;
2167
2168        let lightning_fee = fed_config.lightning_fee;
2169        let transaction_fee = fed_config.transaction_fee;
2170
2171        Ok(self
2172            .public_key_v2(federation_id)
2173            .await
2174            .map(|module_public_key| RoutingInfo {
2175                lightning_public_key: context.lightning_public_key,
2176                module_public_key,
2177                send_fee_default: lightning_fee + transaction_fee,
2178                // The base fee ensures that the gateway does not loose sats sending the payment due
2179                // to fees paid on the transaction claiming the outgoing contract or
2180                // subsequent transactions spending the newly issued ecash
2181                send_fee_minimum: transaction_fee,
2182                expiration_delta_default: 1440,
2183                expiration_delta_minimum: EXPIRATION_DELTA_MINIMUM_V2,
2184                // The base fee ensures that the gateway does not loose sats receiving the payment
2185                // due to fees paid on the transaction funding the incoming contract
2186                receive_fee: transaction_fee,
2187            }))
2188    }
2189
2190    /// Instructs this gateway to pay a Lightning network invoice via the LNv2
2191    /// protocol.
2192    async fn send_payment_v2(
2193        &self,
2194        payload: SendPaymentPayload,
2195    ) -> Result<std::result::Result<[u8; 32], Signature>> {
2196        self.select_client(payload.federation_id)
2197            .await?
2198            .value()
2199            .get_first_module::<GatewayClientModuleV2>()
2200            .expect("Must have client module")
2201            .send_payment(payload)
2202            .await
2203            .map_err(LNv2Error::OutgoingPayment)
2204            .map_err(PublicGatewayError::LNv2)
2205    }
2206
2207    /// For the LNv2 protocol, this will create an invoice by fetching it from
2208    /// the connected Lightning node, then save the payment hash so that
2209    /// incoming lightning payments can be matched as a receive attempt to a
2210    /// specific federation.
2211    async fn create_bolt11_invoice_v2(
2212        &self,
2213        payload: CreateBolt11InvoicePayload,
2214    ) -> Result<Bolt11Invoice> {
2215        if !payload.contract.verify() {
2216            return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
2217                "The contract is invalid".to_string(),
2218            )));
2219        }
2220
2221        let payment_info = self.routing_info_v2(&payload.federation_id).await?.ok_or(
2222            LNv2Error::IncomingPayment(format!(
2223                "Federation {} does not exist",
2224                payload.federation_id
2225            )),
2226        )?;
2227
2228        if payload.contract.commitment.refund_pk != payment_info.module_public_key {
2229            return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
2230                "The incoming contract is keyed to another gateway".to_string(),
2231            )));
2232        }
2233
2234        let contract_amount = payment_info.receive_fee.subtract_from(payload.amount.msats);
2235
2236        if contract_amount == Amount::ZERO {
2237            return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
2238                "Zero amount incoming contracts are not supported".to_string(),
2239            )));
2240        }
2241
2242        if contract_amount != payload.contract.commitment.amount {
2243            return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
2244                "The contract amount does not pay the correct amount of fees".to_string(),
2245            )));
2246        }
2247
2248        if payload.contract.commitment.expiration <= duration_since_epoch().as_secs() {
2249            return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
2250                "The contract has already expired".to_string(),
2251            )));
2252        }
2253
2254        let payment_hash = match payload.contract.commitment.payment_image {
2255            PaymentImage::Hash(payment_hash) => payment_hash,
2256            PaymentImage::Point(..) => {
2257                return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
2258                    "PaymentImage is not a payment hash".to_string(),
2259                )));
2260            }
2261        };
2262
2263        let invoice = self
2264            .create_invoice_via_lnrpc_v2(
2265                payment_hash,
2266                payload.amount,
2267                payload.description.clone(),
2268                payload.expiry_secs,
2269            )
2270            .await?;
2271
2272        let mut dbtx = self.gateway_db.begin_transaction().await;
2273
2274        if dbtx
2275            .save_registered_incoming_contract(
2276                payload.federation_id,
2277                payload.amount,
2278                payload.contract,
2279            )
2280            .await
2281            .is_some()
2282        {
2283            return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
2284                "PaymentHash is already registered".to_string(),
2285            )));
2286        }
2287
2288        dbtx.commit_tx_result().await.map_err(|_| {
2289            PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
2290                "Payment hash is already registered".to_string(),
2291            ))
2292        })?;
2293
2294        Ok(invoice)
2295    }
2296
2297    /// Retrieves a BOLT11 invoice from the connected Lightning node with a
2298    /// specific `payment_hash`.
2299    pub async fn create_invoice_via_lnrpc_v2(
2300        &self,
2301        payment_hash: sha256::Hash,
2302        amount: Amount,
2303        description: Bolt11InvoiceDescription,
2304        expiry_time: u32,
2305    ) -> std::result::Result<Bolt11Invoice, LightningRpcError> {
2306        let lnrpc = self.get_lightning_context().await?.lnrpc;
2307
2308        let response = match description {
2309            Bolt11InvoiceDescription::Direct(description) => {
2310                lnrpc
2311                    .create_invoice(CreateInvoiceRequest {
2312                        payment_hash: Some(payment_hash),
2313                        amount_msat: amount.msats,
2314                        expiry_secs: expiry_time,
2315                        description: Some(InvoiceDescription::Direct(description)),
2316                    })
2317                    .await?
2318            }
2319            Bolt11InvoiceDescription::Hash(hash) => {
2320                lnrpc
2321                    .create_invoice(CreateInvoiceRequest {
2322                        payment_hash: Some(payment_hash),
2323                        amount_msat: amount.msats,
2324                        expiry_secs: expiry_time,
2325                        description: Some(InvoiceDescription::Hash(hash)),
2326                    })
2327                    .await?
2328            }
2329        };
2330
2331        Bolt11Invoice::from_str(&response.invoice).map_err(|e| {
2332            LightningRpcError::FailedToGetInvoice {
2333                failure_reason: e.to_string(),
2334            }
2335        })
2336    }
2337
2338    pub async fn verify_bolt11_preimage_v2(
2339        &self,
2340        payment_hash: sha256::Hash,
2341        wait: bool,
2342    ) -> std::result::Result<VerifyResponse, String> {
2343        let registered_contract = self
2344            .gateway_db
2345            .begin_transaction_nc()
2346            .await
2347            .load_registered_incoming_contract(PaymentImage::Hash(payment_hash))
2348            .await
2349            .ok_or("Unknown payment hash".to_string())?;
2350
2351        let client = self
2352            .select_client(registered_contract.federation_id)
2353            .await
2354            .map_err(|_| "Not connected to federation".to_string())?
2355            .into_value();
2356
2357        let operation_id = OperationId::from_encodable(&registered_contract.contract);
2358
2359        if !(wait || client.operation_exists(operation_id).await) {
2360            return Ok(VerifyResponse {
2361                status: "OK".to_string(),
2362                settled: false,
2363                preimage: None,
2364            });
2365        }
2366
2367        let state = client
2368            .get_first_module::<GatewayClientModuleV2>()
2369            .expect("Must have client module")
2370            .await_receive(operation_id)
2371            .await;
2372
2373        let preimage = match state {
2374            FinalReceiveState::Success(preimage) => Ok(preimage),
2375            FinalReceiveState::Failure => Err("Payment has failed".to_string()),
2376            FinalReceiveState::Refunded => Err("Payment has been refunded".to_string()),
2377            FinalReceiveState::Rejected => Err("Payment has been rejected".to_string()),
2378        }?;
2379
2380        Ok(VerifyResponse {
2381            status: "OK".to_string(),
2382            settled: true,
2383            preimage: Some(preimage),
2384        })
2385    }
2386
2387    /// Retrieves the persisted `CreateInvoicePayload` from the database
2388    /// specified by the `payment_hash` and the `ClientHandleArc` specified
2389    /// by the payload's `federation_id`.
2390    pub async fn get_registered_incoming_contract_and_client_v2(
2391        &self,
2392        payment_image: PaymentImage,
2393        amount_msats: u64,
2394    ) -> Result<(IncomingContract, ClientHandleArc)> {
2395        let registered_incoming_contract = self
2396            .gateway_db
2397            .begin_transaction_nc()
2398            .await
2399            .load_registered_incoming_contract(payment_image)
2400            .await
2401            .ok_or(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
2402                "No corresponding decryption contract available".to_string(),
2403            )))?;
2404
2405        if registered_incoming_contract.incoming_amount_msats != amount_msats {
2406            return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
2407                "The available decryption contract's amount is not equal to the requested amount"
2408                    .to_string(),
2409            )));
2410        }
2411
2412        let client = self
2413            .select_client(registered_incoming_contract.federation_id)
2414            .await?
2415            .into_value();
2416
2417        Ok((registered_incoming_contract.contract, client))
2418    }
2419}
2420
2421#[async_trait]
2422impl IGatewayClientV2 for Gateway {
2423    async fn complete_htlc(&self, htlc_response: InterceptPaymentResponse) {
2424        loop {
2425            match self.get_lightning_context().await {
2426                Ok(lightning_context) => {
2427                    match lightning_context
2428                        .lnrpc
2429                        .complete_htlc(htlc_response.clone())
2430                        .await
2431                    {
2432                        Ok(..) => return,
2433                        Err(err) => {
2434                            warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Failure trying to complete payment");
2435                        }
2436                    }
2437                }
2438                Err(err) => {
2439                    warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Failure trying to complete payment");
2440                }
2441            }
2442
2443            sleep(Duration::from_secs(5)).await;
2444        }
2445    }
2446
2447    async fn is_direct_swap(
2448        &self,
2449        invoice: &Bolt11Invoice,
2450    ) -> anyhow::Result<Option<(IncomingContract, ClientHandleArc)>> {
2451        let lightning_context = self.get_lightning_context().await?;
2452        if lightning_context.lightning_public_key == invoice.get_payee_pub_key() {
2453            let (contract, client) = self
2454                .get_registered_incoming_contract_and_client_v2(
2455                    PaymentImage::Hash(*invoice.payment_hash()),
2456                    invoice
2457                        .amount_milli_satoshis()
2458                        .expect("The amount invoice has been previously checked"),
2459                )
2460                .await?;
2461            Ok(Some((contract, client)))
2462        } else {
2463            Ok(None)
2464        }
2465    }
2466
2467    async fn pay(
2468        &self,
2469        invoice: Bolt11Invoice,
2470        max_delay: u64,
2471        max_fee: Amount,
2472    ) -> std::result::Result<[u8; 32], LightningRpcError> {
2473        let lightning_context = self.get_lightning_context().await?;
2474        lightning_context
2475            .lnrpc
2476            .pay(invoice, max_delay, max_fee)
2477            .await
2478            .map(|response| response.preimage.0)
2479    }
2480
2481    async fn min_contract_amount(
2482        &self,
2483        federation_id: &FederationId,
2484        amount: u64,
2485    ) -> anyhow::Result<Amount> {
2486        Ok(self
2487            .routing_info_v2(federation_id)
2488            .await?
2489            .ok_or(anyhow!("Routing Info not available"))?
2490            .send_fee_minimum
2491            .add_to(amount))
2492    }
2493
2494    async fn is_lnv1_invoice(&self, invoice: &Bolt11Invoice) -> Option<Spanned<ClientHandleArc>> {
2495        let rhints = invoice.route_hints();
2496        match rhints.first().and_then(|rh| rh.0.last()) {
2497            None => None,
2498            Some(hop) => match self.get_lightning_context().await {
2499                Ok(lightning_context) => {
2500                    if hop.src_node_id != lightning_context.lightning_public_key {
2501                        return None;
2502                    }
2503
2504                    self.federation_manager
2505                        .read()
2506                        .await
2507                        .get_client_for_index(hop.short_channel_id)
2508                }
2509                Err(_) => None,
2510            },
2511        }
2512    }
2513
2514    async fn relay_lnv1_swap(
2515        &self,
2516        client: &ClientHandleArc,
2517        invoice: &Bolt11Invoice,
2518    ) -> anyhow::Result<FinalReceiveState> {
2519        let swap_params = SwapParameters {
2520            payment_hash: *invoice.payment_hash(),
2521            amount_msat: Amount::from_msats(
2522                invoice
2523                    .amount_milli_satoshis()
2524                    .ok_or(anyhow!("Amountless invoice not supported"))?,
2525            ),
2526        };
2527        let lnv1 = client
2528            .get_first_module::<GatewayClientModule>()
2529            .expect("No LNv1 module");
2530        let operation_id = lnv1.gateway_handle_direct_swap(swap_params).await?;
2531        let mut stream = lnv1
2532            .gateway_subscribe_ln_receive(operation_id)
2533            .await?
2534            .into_stream();
2535        let mut final_state = FinalReceiveState::Failure;
2536        while let Some(update) = stream.next().await {
2537            match update {
2538                GatewayExtReceiveStates::Funding => {}
2539                GatewayExtReceiveStates::FundingFailed { error: _ } => {
2540                    final_state = FinalReceiveState::Rejected;
2541                }
2542                GatewayExtReceiveStates::Preimage(preimage) => {
2543                    final_state = FinalReceiveState::Success(preimage.0);
2544                }
2545                GatewayExtReceiveStates::RefundError {
2546                    error_message: _,
2547                    error: _,
2548                } => {
2549                    final_state = FinalReceiveState::Failure;
2550                }
2551                GatewayExtReceiveStates::RefundSuccess {
2552                    out_points: _,
2553                    error: _,
2554                } => {
2555                    final_state = FinalReceiveState::Refunded;
2556                }
2557            }
2558        }
2559
2560        Ok(final_state)
2561    }
2562}
2563
2564#[async_trait]
2565impl IGatewayClientV1 for Gateway {
2566    async fn verify_preimage_authentication(
2567        &self,
2568        payment_hash: sha256::Hash,
2569        preimage_auth: sha256::Hash,
2570        contract: OutgoingContractAccount,
2571    ) -> std::result::Result<(), OutgoingPaymentError> {
2572        let mut dbtx = self.gateway_db.begin_transaction().await;
2573        if let Some(secret_hash) = dbtx.load_preimage_authentication(payment_hash).await {
2574            if secret_hash != preimage_auth {
2575                return Err(OutgoingPaymentError {
2576                    error_type: OutgoingPaymentErrorType::InvalidInvoicePreimage,
2577                    contract_id: contract.contract.contract_id(),
2578                    contract: Some(contract),
2579                });
2580            }
2581        } else {
2582            // Committing the `preimage_auth` to the database can fail if two users try to
2583            // pay the same invoice at the same time.
2584            dbtx.save_new_preimage_authentication(payment_hash, preimage_auth)
2585                .await;
2586            return dbtx
2587                .commit_tx_result()
2588                .await
2589                .map_err(|_| OutgoingPaymentError {
2590                    error_type: OutgoingPaymentErrorType::InvoiceAlreadyPaid,
2591                    contract_id: contract.contract.contract_id(),
2592                    contract: Some(contract),
2593                });
2594        }
2595
2596        Ok(())
2597    }
2598
2599    async fn verify_pruned_invoice(&self, payment_data: PaymentData) -> anyhow::Result<()> {
2600        let lightning_context = self.get_lightning_context().await?;
2601
2602        if matches!(payment_data, PaymentData::PrunedInvoice { .. }) {
2603            ensure!(
2604                lightning_context.lnrpc.supports_private_payments(),
2605                "Private payments are not supported by the lightning node"
2606            );
2607        }
2608
2609        Ok(())
2610    }
2611
2612    async fn get_routing_fees(&self, federation_id: FederationId) -> Option<RoutingFees> {
2613        let mut gateway_dbtx = self.gateway_db.begin_transaction_nc().await;
2614        gateway_dbtx
2615            .load_federation_config(federation_id)
2616            .await
2617            .map(|c| c.lightning_fee.into())
2618    }
2619
2620    async fn get_client(&self, federation_id: &FederationId) -> Option<Spanned<ClientHandleArc>> {
2621        self.federation_manager
2622            .read()
2623            .await
2624            .client(federation_id)
2625            .cloned()
2626    }
2627
2628    async fn get_client_for_invoice(
2629        &self,
2630        payment_data: PaymentData,
2631    ) -> Option<Spanned<ClientHandleArc>> {
2632        let rhints = payment_data.route_hints();
2633        match rhints.first().and_then(|rh| rh.0.last()) {
2634            None => None,
2635            Some(hop) => match self.get_lightning_context().await {
2636                Ok(lightning_context) => {
2637                    if hop.src_node_id != lightning_context.lightning_public_key {
2638                        return None;
2639                    }
2640
2641                    self.federation_manager
2642                        .read()
2643                        .await
2644                        .get_client_for_index(hop.short_channel_id)
2645                }
2646                Err(_) => None,
2647            },
2648        }
2649    }
2650
2651    async fn pay(
2652        &self,
2653        payment_data: PaymentData,
2654        max_delay: u64,
2655        max_fee: Amount,
2656    ) -> std::result::Result<PayInvoiceResponse, LightningRpcError> {
2657        let lightning_context = self.get_lightning_context().await?;
2658
2659        match payment_data {
2660            PaymentData::Invoice(invoice) => {
2661                lightning_context
2662                    .lnrpc
2663                    .pay(invoice, max_delay, max_fee)
2664                    .await
2665            }
2666            PaymentData::PrunedInvoice(invoice) => {
2667                lightning_context
2668                    .lnrpc
2669                    .pay_private(invoice, max_delay, max_fee)
2670                    .await
2671            }
2672        }
2673    }
2674
2675    async fn complete_htlc(
2676        &self,
2677        htlc: InterceptPaymentResponse,
2678    ) -> std::result::Result<(), LightningRpcError> {
2679        // Wait until the lightning node is online to complete the HTLC.
2680        let lightning_context = loop {
2681            match self.get_lightning_context().await {
2682                Ok(lightning_context) => break lightning_context,
2683                Err(err) => {
2684                    warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Failure trying to complete payment");
2685                    sleep(Duration::from_secs(5)).await;
2686                }
2687            }
2688        };
2689
2690        lightning_context.lnrpc.complete_htlc(htlc).await
2691    }
2692
2693    async fn is_lnv2_direct_swap(
2694        &self,
2695        payment_hash: sha256::Hash,
2696        amount: Amount,
2697    ) -> anyhow::Result<
2698        Option<(
2699            fedimint_lnv2_common::contracts::IncomingContract,
2700            ClientHandleArc,
2701        )>,
2702    > {
2703        let (contract, client) = self
2704            .get_registered_incoming_contract_and_client_v2(
2705                PaymentImage::Hash(payment_hash),
2706                amount.msats,
2707            )
2708            .await?;
2709        Ok(Some((contract, client)))
2710    }
2711}