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