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