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