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