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