fedimint_gateway_server/
lib.rs

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