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