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