fedimint_gateway_server/
lib.rs

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