fedimint_gateway_server/
lib.rs

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