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    async fn handle_payment_log_msg(
2655        &self,
2656        PaymentLogPayload {
2657            end_position,
2658            pagination_size,
2659            federation_id,
2660            event_kinds,
2661        }: PaymentLogPayload,
2662    ) -> AdminResult<PaymentLogResponse> {
2663        const BATCH_SIZE: u64 = 10_000;
2664        let federation_manager = self.federation_manager.read().await;
2665        let client = federation_manager
2666            .client(&federation_id)
2667            .ok_or(FederationNotConnected {
2668                federation_id_prefix: federation_id.to_prefix(),
2669            })?
2670            .value();
2671
2672        let event_kinds = if event_kinds.is_empty() {
2673            ALL_GATEWAY_EVENTS.to_vec()
2674        } else {
2675            event_kinds
2676        };
2677
2678        let end_position = if let Some(position) = end_position {
2679            position
2680        } else {
2681            let mut dbtx = client.db().begin_transaction_nc().await;
2682            dbtx.get_next_event_log_id().await
2683        };
2684
2685        let mut start_position = end_position.saturating_sub(BATCH_SIZE);
2686
2687        let mut payment_log = Vec::new();
2688
2689        while payment_log.len() < pagination_size {
2690            let batch = client.get_event_log(Some(start_position), BATCH_SIZE).await;
2691            let mut filtered_batch = batch
2692                .into_iter()
2693                .filter(|e| e.id() <= end_position && event_kinds.contains(&e.as_raw().kind))
2694                .collect::<Vec<_>>();
2695            filtered_batch.reverse();
2696            payment_log.extend(filtered_batch);
2697
2698            // Compute the start position for the next batch query
2699            start_position = start_position.saturating_sub(BATCH_SIZE);
2700
2701            if start_position == EventLogId::LOG_START {
2702                break;
2703            }
2704        }
2705
2706        // Truncate the payment log to the expected pagination size
2707        payment_log.truncate(pagination_size);
2708
2709        Ok(PaymentLogResponse(payment_log))
2710    }
2711
2712    /// Set the gateway's root mnemonic by generating a new one or using the
2713    /// words provided in `SetMnemonicPayload`.
2714    async fn handle_set_mnemonic_msg(&self, payload: SetMnemonicPayload) -> AdminResult<()> {
2715        // Verify the state is NotConfigured
2716        let GatewayState::NotConfigured { mnemonic_sender } = self.get_state().await else {
2717            return Err(AdminGatewayError::MnemonicError(anyhow!(
2718                "Gateway is not is NotConfigured state"
2719            )));
2720        };
2721
2722        let mnemonic = if let Some(words) = payload.words {
2723            info!(target: LOG_GATEWAY, "Using user provided mnemonic");
2724            Mnemonic::parse_in_normalized(Language::English, words.as_str()).map_err(|e| {
2725                AdminGatewayError::MnemonicError(anyhow!(format!(
2726                    "Seed phrase provided in environment was invalid {e:?}"
2727                )))
2728            })?
2729        } else {
2730            debug!(target: LOG_GATEWAY, "Generating mnemonic and writing entropy to client storage");
2731            Bip39RootSecretStrategy::<12>::random(&mut OsRng)
2732        };
2733
2734        Client::store_encodable_client_secret(&self.gateway_db, mnemonic.to_entropy())
2735            .await
2736            .map_err(AdminGatewayError::MnemonicError)?;
2737
2738        self.set_gateway_state(GatewayState::Disconnected).await;
2739
2740        // Alert the gateway background threads that the mnemonic has been set
2741        let _ = mnemonic_sender.send(());
2742
2743        Ok(())
2744    }
2745
2746    /// Creates a BOLT12 offer using the gateway's lightning node
2747    async fn handle_create_offer_for_operator_msg(
2748        &self,
2749        payload: CreateOfferPayload,
2750    ) -> AdminResult<CreateOfferResponse> {
2751        let lightning_context = self.get_lightning_context().await?;
2752        let offer = lightning_context.lnrpc.create_offer(
2753            payload.amount,
2754            payload.description,
2755            payload.expiry_secs,
2756            payload.quantity,
2757        )?;
2758        Ok(CreateOfferResponse { offer })
2759    }
2760
2761    /// Pays a BOLT12 offer using the gateway's lightning node
2762    async fn handle_pay_offer_for_operator_msg(
2763        &self,
2764        payload: PayOfferPayload,
2765    ) -> AdminResult<PayOfferResponse> {
2766        let lightning_context = self.get_lightning_context().await?;
2767        let preimage = lightning_context
2768            .lnrpc
2769            .pay_offer(
2770                payload.offer,
2771                payload.quantity,
2772                payload.amount,
2773                payload.payer_note,
2774            )
2775            .await?;
2776        Ok(PayOfferResponse {
2777            preimage: preimage.to_string(),
2778        })
2779    }
2780
2781    /// Returns a `BTreeMap` that is keyed by the `FederationId` and contains
2782    /// all the invite codes (with peer names) for the federation.
2783    async fn handle_export_invite_codes(
2784        &self,
2785    ) -> BTreeMap<FederationId, BTreeMap<PeerId, (String, InviteCode)>> {
2786        let fed_manager = self.federation_manager.read().await;
2787        fed_manager.all_invite_codes().await
2788    }
2789
2790    /// Returns `TieredCounts` which describes the breakdown of notes in the
2791    /// gateway's wallet for the given `FederationId`
2792    async fn handle_get_note_summary_msg(
2793        &self,
2794        federation_id: &FederationId,
2795    ) -> AdminResult<TieredCounts> {
2796        let fed_manager = self.federation_manager.read().await;
2797        fed_manager.get_note_summary(federation_id).await
2798    }
2799
2800    fn get_password_hash(&self) -> String {
2801        self.bcrypt_password_hash.clone()
2802    }
2803
2804    fn gatewayd_version(&self) -> String {
2805        let gatewayd_version = env!("CARGO_PKG_VERSION");
2806        gatewayd_version.to_string()
2807    }
2808
2809    async fn get_chain_source(&self) -> (ChainSource, Network) {
2810        (self.chain_source.clone(), self.network)
2811    }
2812
2813    fn lightning_mode(&self) -> LightningMode {
2814        self.lightning_mode.clone()
2815    }
2816
2817    async fn is_configured(&self) -> bool {
2818        !matches!(self.get_state().await, GatewayState::NotConfigured { .. })
2819    }
2820}
2821
2822// LNv2 Gateway implementation
2823impl Gateway {
2824    /// Retrieves the `PublicKey` of the Gateway module for a given federation
2825    /// for LNv2. This is NOT the same as the `gateway_id`, it is different
2826    /// per-connected federation.
2827    async fn public_key_v2(&self, federation_id: &FederationId) -> Option<PublicKey> {
2828        self.federation_manager
2829            .read()
2830            .await
2831            .client(federation_id)
2832            .map(|client| {
2833                client
2834                    .value()
2835                    .get_first_module::<GatewayClientModuleV2>()
2836                    .expect("Must have client module")
2837                    .keypair
2838                    .public_key()
2839            })
2840    }
2841
2842    /// Returns payment information that LNv2 clients can use to instruct this
2843    /// Gateway to pay an invoice or receive a payment.
2844    pub async fn routing_info_v2(
2845        &self,
2846        federation_id: &FederationId,
2847    ) -> Result<Option<RoutingInfo>> {
2848        let context = self.get_lightning_context().await?;
2849
2850        let mut dbtx = self.gateway_db.begin_transaction_nc().await;
2851        let fed_config = dbtx.load_federation_config(*federation_id).await.ok_or(
2852            PublicGatewayError::FederationNotConnected(FederationNotConnected {
2853                federation_id_prefix: federation_id.to_prefix(),
2854            }),
2855        )?;
2856
2857        let lightning_fee = fed_config.lightning_fee;
2858        let transaction_fee = fed_config.transaction_fee;
2859
2860        Ok(self
2861            .public_key_v2(federation_id)
2862            .await
2863            .map(|module_public_key| RoutingInfo {
2864                lightning_public_key: context.lightning_public_key,
2865                lightning_alias: Some(context.lightning_alias.clone()),
2866                module_public_key,
2867                send_fee_default: lightning_fee + transaction_fee,
2868                // The base fee ensures that the gateway does not loose sats sending the payment due
2869                // to fees paid on the transaction claiming the outgoing contract or
2870                // subsequent transactions spending the newly issued ecash
2871                send_fee_minimum: transaction_fee,
2872                expiration_delta_default: 1440,
2873                expiration_delta_minimum: EXPIRATION_DELTA_MINIMUM_V2,
2874                // The base fee ensures that the gateway does not loose sats receiving the payment
2875                // due to fees paid on the transaction funding the incoming contract
2876                receive_fee: transaction_fee,
2877            }))
2878    }
2879
2880    /// Instructs this gateway to pay a Lightning network invoice via the LNv2
2881    /// protocol.
2882    async fn send_payment_v2(
2883        &self,
2884        payload: SendPaymentPayload,
2885    ) -> Result<std::result::Result<[u8; 32], Signature>> {
2886        self.select_client(payload.federation_id)
2887            .await?
2888            .value()
2889            .get_first_module::<GatewayClientModuleV2>()
2890            .expect("Must have client module")
2891            .send_payment(payload)
2892            .await
2893            .map_err(LNv2Error::OutgoingPayment)
2894            .map_err(PublicGatewayError::LNv2)
2895    }
2896
2897    /// For the LNv2 protocol, this will create an invoice by fetching it from
2898    /// the connected Lightning node, then save the payment hash so that
2899    /// incoming lightning payments can be matched as a receive attempt to a
2900    /// specific federation.
2901    async fn create_bolt11_invoice_v2(
2902        &self,
2903        payload: CreateBolt11InvoicePayload,
2904    ) -> Result<Bolt11Invoice> {
2905        if !payload.contract.verify() {
2906            return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
2907                "The contract is invalid".to_string(),
2908            )));
2909        }
2910
2911        let payment_info = self.routing_info_v2(&payload.federation_id).await?.ok_or(
2912            LNv2Error::IncomingPayment(format!(
2913                "Federation {} does not exist",
2914                payload.federation_id
2915            )),
2916        )?;
2917
2918        if payload.contract.commitment.refund_pk != payment_info.module_public_key {
2919            return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
2920                "The incoming contract is keyed to another gateway".to_string(),
2921            )));
2922        }
2923
2924        let contract_amount = payment_info.receive_fee.subtract_from(payload.amount.msats);
2925
2926        if contract_amount == Amount::ZERO {
2927            return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
2928                "Zero amount incoming contracts are not supported".to_string(),
2929            )));
2930        }
2931
2932        if contract_amount != payload.contract.commitment.amount {
2933            return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
2934                "The contract amount does not pay the correct amount of fees".to_string(),
2935            )));
2936        }
2937
2938        if payload.contract.commitment.expiration <= duration_since_epoch().as_secs() {
2939            return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
2940                "The contract has already expired".to_string(),
2941            )));
2942        }
2943
2944        let payment_hash = match payload.contract.commitment.payment_image {
2945            PaymentImage::Hash(payment_hash) => payment_hash,
2946            PaymentImage::Point(..) => {
2947                return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
2948                    "PaymentImage is not a payment hash".to_string(),
2949                )));
2950            }
2951        };
2952
2953        let invoice = self
2954            .create_invoice_via_lnrpc_v2(
2955                payment_hash,
2956                payload.amount,
2957                payload.description.clone(),
2958                payload.expiry_secs,
2959            )
2960            .await?;
2961
2962        let mut dbtx = self.gateway_db.begin_transaction().await;
2963
2964        if dbtx
2965            .save_registered_incoming_contract(
2966                payload.federation_id,
2967                payload.amount,
2968                payload.contract,
2969            )
2970            .await
2971            .is_some()
2972        {
2973            return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
2974                "PaymentHash is already registered".to_string(),
2975            )));
2976        }
2977
2978        dbtx.commit_tx_result().await.map_err(|_| {
2979            PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
2980                "Payment hash is already registered".to_string(),
2981            ))
2982        })?;
2983
2984        Ok(invoice)
2985    }
2986
2987    /// Retrieves a BOLT11 invoice from the connected Lightning node with a
2988    /// specific `payment_hash`.
2989    pub async fn create_invoice_via_lnrpc_v2(
2990        &self,
2991        payment_hash: sha256::Hash,
2992        amount: Amount,
2993        description: Bolt11InvoiceDescription,
2994        expiry_time: u32,
2995    ) -> std::result::Result<Bolt11Invoice, LightningRpcError> {
2996        let lnrpc = self.get_lightning_context().await?.lnrpc;
2997
2998        let response = match description {
2999            Bolt11InvoiceDescription::Direct(description) => {
3000                lnrpc
3001                    .create_invoice(CreateInvoiceRequest {
3002                        payment_hash: Some(payment_hash),
3003                        amount_msat: amount.msats,
3004                        expiry_secs: expiry_time,
3005                        description: Some(InvoiceDescription::Direct(description)),
3006                    })
3007                    .await?
3008            }
3009            Bolt11InvoiceDescription::Hash(hash) => {
3010                lnrpc
3011                    .create_invoice(CreateInvoiceRequest {
3012                        payment_hash: Some(payment_hash),
3013                        amount_msat: amount.msats,
3014                        expiry_secs: expiry_time,
3015                        description: Some(InvoiceDescription::Hash(hash)),
3016                    })
3017                    .await?
3018            }
3019        };
3020
3021        Bolt11Invoice::from_str(&response.invoice).map_err(|e| {
3022            LightningRpcError::FailedToGetInvoice {
3023                failure_reason: e.to_string(),
3024            }
3025        })
3026    }
3027
3028    pub async fn verify_bolt11_preimage_v2(
3029        &self,
3030        payment_hash: sha256::Hash,
3031        wait: bool,
3032    ) -> std::result::Result<VerifyResponse, String> {
3033        let registered_contract = self
3034            .gateway_db
3035            .begin_transaction_nc()
3036            .await
3037            .load_registered_incoming_contract(PaymentImage::Hash(payment_hash))
3038            .await
3039            .ok_or("Unknown payment hash".to_string())?;
3040
3041        let client = self
3042            .select_client(registered_contract.federation_id)
3043            .await
3044            .map_err(|_| "Not connected to federation".to_string())?
3045            .into_value();
3046
3047        let operation_id = OperationId::from_encodable(&registered_contract.contract);
3048
3049        if !(wait || client.operation_exists(operation_id).await) {
3050            return Ok(VerifyResponse {
3051                settled: false,
3052                preimage: None,
3053            });
3054        }
3055
3056        let state = client
3057            .get_first_module::<GatewayClientModuleV2>()
3058            .expect("Must have client module")
3059            .await_receive(operation_id)
3060            .await;
3061
3062        let preimage = match state {
3063            FinalReceiveState::Success(preimage) => Ok(preimage),
3064            FinalReceiveState::Failure => Err("Payment has failed".to_string()),
3065            FinalReceiveState::Refunded => Err("Payment has been refunded".to_string()),
3066            FinalReceiveState::Rejected => Err("Payment has been rejected".to_string()),
3067        }?;
3068
3069        Ok(VerifyResponse {
3070            settled: true,
3071            preimage: Some(preimage),
3072        })
3073    }
3074
3075    /// Retrieves the persisted `CreateInvoicePayload` from the database
3076    /// specified by the `payment_hash` and the `ClientHandleArc` specified
3077    /// by the payload's `federation_id`.
3078    pub async fn get_registered_incoming_contract_and_client_v2(
3079        &self,
3080        payment_image: PaymentImage,
3081        amount_msats: u64,
3082    ) -> Result<(IncomingContract, ClientHandleArc)> {
3083        let registered_incoming_contract = self
3084            .gateway_db
3085            .begin_transaction_nc()
3086            .await
3087            .load_registered_incoming_contract(payment_image)
3088            .await
3089            .ok_or(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
3090                "No corresponding decryption contract available".to_string(),
3091            )))?;
3092
3093        if registered_incoming_contract.incoming_amount_msats != amount_msats {
3094            return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
3095                "The available decryption contract's amount is not equal to the requested amount"
3096                    .to_string(),
3097            )));
3098        }
3099
3100        let client = self
3101            .select_client(registered_incoming_contract.federation_id)
3102            .await?
3103            .into_value();
3104
3105        Ok((registered_incoming_contract.contract, client))
3106    }
3107}
3108
3109#[async_trait]
3110impl IGatewayClientV2 for Gateway {
3111    async fn complete_htlc(&self, htlc_response: InterceptPaymentResponse) {
3112        loop {
3113            match self.get_lightning_context().await {
3114                Ok(lightning_context) => {
3115                    match lightning_context
3116                        .lnrpc
3117                        .complete_htlc(htlc_response.clone())
3118                        .await
3119                    {
3120                        Ok(..) => return,
3121                        Err(err) => {
3122                            warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Failure trying to complete payment");
3123                        }
3124                    }
3125                }
3126                Err(err) => {
3127                    warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Failure trying to complete payment");
3128                }
3129            }
3130
3131            sleep(Duration::from_secs(5)).await;
3132        }
3133    }
3134
3135    async fn is_direct_swap(
3136        &self,
3137        invoice: &Bolt11Invoice,
3138    ) -> anyhow::Result<Option<(IncomingContract, ClientHandleArc)>> {
3139        let lightning_context = self.get_lightning_context().await?;
3140        if lightning_context.lightning_public_key == invoice.get_payee_pub_key() {
3141            let (contract, client) = self
3142                .get_registered_incoming_contract_and_client_v2(
3143                    PaymentImage::Hash(*invoice.payment_hash()),
3144                    invoice
3145                        .amount_milli_satoshis()
3146                        .expect("The amount invoice has been previously checked"),
3147                )
3148                .await?;
3149            Ok(Some((contract, client)))
3150        } else {
3151            Ok(None)
3152        }
3153    }
3154
3155    async fn pay(
3156        &self,
3157        invoice: Bolt11Invoice,
3158        max_delay: u64,
3159        max_fee: Amount,
3160    ) -> std::result::Result<[u8; 32], LightningRpcError> {
3161        let lightning_context = self.get_lightning_context().await?;
3162        lightning_context
3163            .lnrpc
3164            .pay(invoice, max_delay, max_fee)
3165            .await
3166            .map(|response| response.preimage.0)
3167    }
3168
3169    async fn min_contract_amount(
3170        &self,
3171        federation_id: &FederationId,
3172        amount: u64,
3173    ) -> anyhow::Result<Amount> {
3174        Ok(self
3175            .routing_info_v2(federation_id)
3176            .await?
3177            .ok_or(anyhow!("Routing Info not available"))?
3178            .send_fee_minimum
3179            .add_to(amount))
3180    }
3181
3182    async fn is_lnv1_invoice(&self, invoice: &Bolt11Invoice) -> Option<Spanned<ClientHandleArc>> {
3183        let rhints = invoice.route_hints();
3184        match rhints.first().and_then(|rh| rh.0.last()) {
3185            None => None,
3186            Some(hop) => match self.get_lightning_context().await {
3187                Ok(lightning_context) => {
3188                    if hop.src_node_id != lightning_context.lightning_public_key {
3189                        return None;
3190                    }
3191
3192                    self.federation_manager
3193                        .read()
3194                        .await
3195                        .get_client_for_index(hop.short_channel_id)
3196                }
3197                Err(_) => None,
3198            },
3199        }
3200    }
3201
3202    async fn relay_lnv1_swap(
3203        &self,
3204        client: &ClientHandleArc,
3205        invoice: &Bolt11Invoice,
3206    ) -> anyhow::Result<FinalReceiveState> {
3207        let swap_params = SwapParameters {
3208            payment_hash: *invoice.payment_hash(),
3209            amount_msat: Amount::from_msats(
3210                invoice
3211                    .amount_milli_satoshis()
3212                    .ok_or(anyhow!("Amountless invoice not supported"))?,
3213            ),
3214        };
3215        let lnv1 = client
3216            .get_first_module::<GatewayClientModule>()
3217            .expect("No LNv1 module");
3218        let operation_id = lnv1.gateway_handle_direct_swap(swap_params).await?;
3219        let mut stream = lnv1
3220            .gateway_subscribe_ln_receive(operation_id)
3221            .await?
3222            .into_stream();
3223        let mut final_state = FinalReceiveState::Failure;
3224        while let Some(update) = stream.next().await {
3225            match update {
3226                GatewayExtReceiveStates::Funding => {}
3227                GatewayExtReceiveStates::FundingFailed { error: _ } => {
3228                    final_state = FinalReceiveState::Rejected;
3229                }
3230                GatewayExtReceiveStates::Preimage(preimage) => {
3231                    final_state = FinalReceiveState::Success(preimage.0);
3232                }
3233                GatewayExtReceiveStates::RefundError {
3234                    error_message: _,
3235                    error: _,
3236                } => {
3237                    final_state = FinalReceiveState::Failure;
3238                }
3239                GatewayExtReceiveStates::RefundSuccess {
3240                    out_points: _,
3241                    error: _,
3242                } => {
3243                    final_state = FinalReceiveState::Refunded;
3244                }
3245            }
3246        }
3247
3248        Ok(final_state)
3249    }
3250}
3251
3252#[async_trait]
3253impl IGatewayClientV1 for Gateway {
3254    async fn verify_preimage_authentication(
3255        &self,
3256        payment_hash: sha256::Hash,
3257        preimage_auth: sha256::Hash,
3258        contract: OutgoingContractAccount,
3259    ) -> std::result::Result<(), OutgoingPaymentError> {
3260        let mut dbtx = self.gateway_db.begin_transaction().await;
3261        if let Some(secret_hash) = dbtx.load_preimage_authentication(payment_hash).await {
3262            if secret_hash != preimage_auth {
3263                return Err(OutgoingPaymentError {
3264                    error_type: OutgoingPaymentErrorType::InvalidInvoicePreimage,
3265                    contract_id: contract.contract.contract_id(),
3266                    contract: Some(contract),
3267                });
3268            }
3269        } else {
3270            // Committing the `preimage_auth` to the database can fail if two users try to
3271            // pay the same invoice at the same time.
3272            dbtx.save_new_preimage_authentication(payment_hash, preimage_auth)
3273                .await;
3274            return dbtx
3275                .commit_tx_result()
3276                .await
3277                .map_err(|_| OutgoingPaymentError {
3278                    error_type: OutgoingPaymentErrorType::InvoiceAlreadyPaid,
3279                    contract_id: contract.contract.contract_id(),
3280                    contract: Some(contract),
3281                });
3282        }
3283
3284        Ok(())
3285    }
3286
3287    async fn verify_pruned_invoice(&self, payment_data: PaymentData) -> anyhow::Result<()> {
3288        let lightning_context = self.get_lightning_context().await?;
3289
3290        if matches!(payment_data, PaymentData::PrunedInvoice { .. }) {
3291            ensure!(
3292                lightning_context.lnrpc.supports_private_payments(),
3293                "Private payments are not supported by the lightning node"
3294            );
3295        }
3296
3297        Ok(())
3298    }
3299
3300    async fn get_routing_fees(&self, federation_id: FederationId) -> Option<RoutingFees> {
3301        let mut gateway_dbtx = self.gateway_db.begin_transaction_nc().await;
3302        gateway_dbtx
3303            .load_federation_config(federation_id)
3304            .await
3305            .map(|c| c.lightning_fee.into())
3306    }
3307
3308    async fn get_client(&self, federation_id: &FederationId) -> Option<Spanned<ClientHandleArc>> {
3309        self.federation_manager
3310            .read()
3311            .await
3312            .client(federation_id)
3313            .cloned()
3314    }
3315
3316    async fn get_client_for_invoice(
3317        &self,
3318        payment_data: PaymentData,
3319    ) -> Option<Spanned<ClientHandleArc>> {
3320        let rhints = payment_data.route_hints();
3321        match rhints.first().and_then(|rh| rh.0.last()) {
3322            None => None,
3323            Some(hop) => match self.get_lightning_context().await {
3324                Ok(lightning_context) => {
3325                    if hop.src_node_id != lightning_context.lightning_public_key {
3326                        return None;
3327                    }
3328
3329                    self.federation_manager
3330                        .read()
3331                        .await
3332                        .get_client_for_index(hop.short_channel_id)
3333                }
3334                Err(_) => None,
3335            },
3336        }
3337    }
3338
3339    async fn pay(
3340        &self,
3341        payment_data: PaymentData,
3342        max_delay: u64,
3343        max_fee: Amount,
3344    ) -> std::result::Result<PayInvoiceResponse, LightningRpcError> {
3345        let lightning_context = self.get_lightning_context().await?;
3346
3347        match payment_data {
3348            PaymentData::Invoice(invoice) => {
3349                lightning_context
3350                    .lnrpc
3351                    .pay(invoice, max_delay, max_fee)
3352                    .await
3353            }
3354            PaymentData::PrunedInvoice(invoice) => {
3355                lightning_context
3356                    .lnrpc
3357                    .pay_private(invoice, max_delay, max_fee)
3358                    .await
3359            }
3360        }
3361    }
3362
3363    async fn complete_htlc(
3364        &self,
3365        htlc: InterceptPaymentResponse,
3366    ) -> std::result::Result<(), LightningRpcError> {
3367        // Wait until the lightning node is online to complete the HTLC.
3368        let lightning_context = loop {
3369            match self.get_lightning_context().await {
3370                Ok(lightning_context) => break lightning_context,
3371                Err(err) => {
3372                    warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Failure trying to complete payment");
3373                    sleep(Duration::from_secs(5)).await;
3374                }
3375            }
3376        };
3377
3378        lightning_context.lnrpc.complete_htlc(htlc).await
3379    }
3380
3381    async fn is_lnv2_direct_swap(
3382        &self,
3383        payment_hash: sha256::Hash,
3384        amount: Amount,
3385    ) -> anyhow::Result<
3386        Option<(
3387            fedimint_lnv2_common::contracts::IncomingContract,
3388            ClientHandleArc,
3389        )>,
3390    > {
3391        let (contract, client) = self
3392            .get_registered_incoming_contract_and_client_v2(
3393                PaymentImage::Hash(payment_hash),
3394                amount.msats,
3395            )
3396            .await?;
3397        Ok(Some((contract, client)))
3398    }
3399}