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