Skip to main content

fedimint_gateway_server/
lib.rs

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