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