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