Skip to main content

fedimint_gateway_server/
lib.rs

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