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