Skip to main content

fedimint_gateway_server/
lib.rs

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