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
15pub mod client;
16pub mod config;
17mod db;
18pub mod envs;
19mod error;
20mod events;
21mod federation_manager;
22pub mod gateway_module_v2;
23pub mod rpc;
24pub mod state_machine;
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;
34
35use anyhow::{anyhow, Context};
36use async_trait::async_trait;
37use bitcoin::hashes::sha256;
38use bitcoin::{Address, Network, Txid};
39use clap::Parser;
40use client::GatewayClientBuilder;
41pub use config::GatewayParameters;
42use config::{GatewayOpts, LightningMode};
43use db::GatewayDbtxNcExt;
44use envs::FM_GATEWAY_SKIP_WAIT_FOR_SYNC_ENV;
45use error::FederationNotConnected;
46use events::ALL_GATEWAY_EVENTS;
47use federation_manager::FederationManager;
48use fedimint_api_client::api::net::Connector;
49use fedimint_bip39::{Bip39RootSecretStrategy, Language, Mnemonic};
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::{
55 ModuleInstanceId, ModuleKind, LEGACY_HARDCODED_INSTANCE_ID_MINT,
56 LEGACY_HARDCODED_INSTANCE_ID_WALLET,
57};
58use fedimint_core::db::{apply_migrations_server, Database, DatabaseTransaction};
59use fedimint_core::envs::is_env_var_set;
60use fedimint_core::invite_code::InviteCode;
61use fedimint_core::module::CommonModuleInit;
62use fedimint_core::secp256k1::schnorr::Signature;
63use fedimint_core::secp256k1::PublicKey;
64use fedimint_core::task::{sleep, TaskGroup, TaskHandle, TaskShutdownToken};
65use fedimint_core::time::duration_since_epoch;
66use fedimint_core::util::{SafeUrl, Spanned};
67use fedimint_core::{
68 fedimint_build_code_version_env, get_network_for_address, Amount, BitcoinAmountOrAll,
69};
70use fedimint_eventlog::{DBTransactionEventLogExt, EventLogId};
71use fedimint_lightning::ldk::{self, GatewayLdkChainSourceConfig};
72use fedimint_lightning::lnd::GatewayLndClient;
73use fedimint_lightning::{
74 CloseChannelsWithPeerRequest, CloseChannelsWithPeerResponse, CreateInvoiceRequest,
75 ILnRpcClient, InterceptPaymentRequest, InterceptPaymentResponse, InvoiceDescription,
76 LightningContext, LightningRpcError, LightningV2Manager, OpenChannelRequest, PaymentAction,
77 RouteHtlcStream, SendOnchainRequest,
78};
79use fedimint_ln_common::config::LightningClientConfig;
80use fedimint_ln_common::contracts::Preimage;
81use fedimint_ln_common::LightningCommonInit;
82use fedimint_lnv2_common::contracts::{IncomingContract, PaymentImage};
83use fedimint_lnv2_common::gateway_api::{
84 CreateBolt11InvoicePayload, PaymentFee, RoutingInfo, SendPaymentPayload,
85};
86use fedimint_lnv2_common::Bolt11InvoiceDescription;
87use fedimint_mint_client::{
88 MintClientInit, MintClientModule, MintCommonInit, SelectNotesWithAtleastAmount,
89 SelectNotesWithExactAmount,
90};
91use fedimint_wallet_client::{
92 WalletClientInit, WalletClientModule, WalletCommonInit, WithdrawState,
93};
94use futures::stream::StreamExt;
95use lightning_invoice::Bolt11Invoice;
96use rand::thread_rng;
97use rpc::{
98 CreateInvoiceForOperatorPayload, DepositAddressRecheckPayload, FederationInfo,
99 GatewayFedConfig, GatewayInfo, LeaveFedPayload, MnemonicResponse, PayInvoiceForOperatorPayload,
100 PaymentLogPayload, PaymentLogResponse, ReceiveEcashPayload, ReceiveEcashResponse,
101 SetFeesPayload, SpendEcashPayload, SpendEcashResponse, WithdrawResponse, V1_API_ENDPOINT,
102};
103use state_machine::{GatewayClientModule, GatewayExtPayStates};
104use tokio::sync::RwLock;
105use tracing::{debug, error, info, info_span, warn};
106
107use crate::config::LightningModuleMode;
108use crate::db::{get_gatewayd_database_migrations, FederationConfig};
109use crate::envs::FM_GATEWAY_MNEMONIC_ENV;
110use crate::error::{AdminGatewayError, LNv1Error, LNv2Error, PublicGatewayError};
111use crate::gateway_module_v2::GatewayClientModuleV2;
112use crate::rpc::rpc_server::run_webserver;
113use crate::rpc::{
114 BackupPayload, ConnectFedPayload, DepositAddressPayload, FederationBalanceInfo,
115 GatewayBalances, WithdrawPayload,
116};
117use crate::types::PrettyInterceptPaymentRequest;
118
119const GW_ANNOUNCEMENT_TTL: Duration = Duration::from_secs(600);
121
122const DEFAULT_NUM_ROUTE_HINTS: u32 = 1;
125
126pub const DEFAULT_NETWORK: Network = Network::Regtest;
128
129const EXPIRATION_DELTA_MINIMUM_V2: u64 = 144;
131
132pub type Result<T> = std::result::Result<T, PublicGatewayError>;
133pub type AdminResult<T> = std::result::Result<T, AdminGatewayError>;
134
135const DB_FILE: &str = "gatewayd.db";
138
139const LDK_NODE_DB_FOLDER: &str = "ldk_node";
142
143const DEFAULT_MODULE_KINDS: [(ModuleInstanceId, &ModuleKind); 2] = [
145 (LEGACY_HARDCODED_INSTANCE_ID_MINT, &MintCommonInit::KIND),
146 (LEGACY_HARDCODED_INSTANCE_ID_WALLET, &WalletCommonInit::KIND),
147];
148
149#[cfg_attr(doc, aquamarine::aquamarine)]
150#[derive(Clone, Debug)]
162pub enum GatewayState {
163 Disconnected,
164 Syncing,
165 Connected,
166 Running { lightning_context: LightningContext },
167 ShuttingDown { lightning_context: LightningContext },
168}
169
170impl Display for GatewayState {
171 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
172 match self {
173 GatewayState::Disconnected => write!(f, "Disconnected"),
174 GatewayState::Syncing => write!(f, "Syncing"),
175 GatewayState::Connected => write!(f, "Connected"),
176 GatewayState::Running { .. } => write!(f, "Running"),
177 GatewayState::ShuttingDown { .. } => write!(f, "ShuttingDown"),
178 }
179 }
180}
181
182enum ReceivePaymentStreamAction {
184 RetryAfterDelay,
185 NoRetry,
186}
187
188#[derive(Clone)]
189pub struct Gateway {
190 federation_manager: Arc<RwLock<FederationManager>>,
192
193 mnemonic: Mnemonic,
195
196 lightning_mode: LightningMode,
198
199 state: Arc<RwLock<GatewayState>>,
201
202 client_builder: GatewayClientBuilder,
205
206 gateway_db: Database,
208
209 gateway_id: PublicKey,
212
213 versioned_api: SafeUrl,
215
216 listen: SocketAddr,
218
219 lightning_module_mode: LightningModuleMode,
221
222 task_group: TaskGroup,
224
225 bcrypt_password_hash: Arc<bcrypt::HashParts>,
228
229 num_route_hints: u32,
231
232 network: Network,
234}
235
236impl std::fmt::Debug for Gateway {
237 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
238 f.debug_struct("Gateway")
239 .field("federation_manager", &self.federation_manager)
240 .field("state", &self.state)
241 .field("client_builder", &self.client_builder)
242 .field("gateway_db", &self.gateway_db)
243 .field("gateway_id", &self.gateway_id)
244 .field("versioned_api", &self.versioned_api)
245 .field("listen", &self.listen)
246 .finish_non_exhaustive()
247 }
248}
249
250impl Gateway {
251 #[allow(clippy::too_many_arguments)]
254 pub async fn new_with_custom_registry(
255 lightning_mode: LightningMode,
256 client_builder: GatewayClientBuilder,
257 listen: SocketAddr,
258 api_addr: SafeUrl,
259 bcrypt_password_hash: bcrypt::HashParts,
260 network: Network,
261 num_route_hints: u32,
262 gateway_db: Database,
263 gateway_state: GatewayState,
264 lightning_module_mode: LightningModuleMode,
265 ) -> anyhow::Result<Gateway> {
266 let versioned_api = api_addr
267 .join(V1_API_ENDPOINT)
268 .expect("Failed to version gateway API address");
269 Gateway::new(
270 lightning_mode,
271 GatewayParameters {
272 listen,
273 versioned_api,
274 bcrypt_password_hash,
275 network,
276 num_route_hints,
277 lightning_module_mode,
278 },
279 gateway_db,
280 client_builder,
281 gateway_state,
282 )
283 .await
284 }
285
286 pub async fn new_with_default_modules() -> anyhow::Result<Gateway> {
289 let opts = GatewayOpts::parse();
290
291 let mut registry = ClientModuleInitRegistry::new();
294 registry.attach(MintClientInit);
295 registry.attach(WalletClientInit::default());
296
297 let decoders = registry.available_decoders(DEFAULT_MODULE_KINDS.iter().copied())?;
298
299 let gateway_db = Database::new(
300 fedimint_rocksdb::RocksDb::open(opts.data_dir.join(DB_FILE))?,
301 decoders,
302 );
303
304 let client_builder =
305 GatewayClientBuilder::new(opts.data_dir.clone(), registry, fedimint_mint_client::KIND);
306
307 info!(
308 "Starting gatewayd (version: {})",
309 fedimint_build_code_version_env!()
310 );
311
312 let mut gateway_parameters = opts.to_gateway_parameters()?;
313
314 if gateway_parameters.lightning_module_mode != LightningModuleMode::LNv2
315 && matches!(opts.mode, LightningMode::Ldk { .. })
316 {
317 warn!("Overriding LDK Gateway to only run LNv2...");
318 gateway_parameters.lightning_module_mode = LightningModuleMode::LNv2;
319 }
320
321 Gateway::new(
322 opts.mode,
323 gateway_parameters,
324 gateway_db,
325 client_builder,
326 GatewayState::Disconnected,
327 )
328 .await
329 }
330
331 async fn new(
334 lightning_mode: LightningMode,
335 gateway_parameters: GatewayParameters,
336 gateway_db: Database,
337 client_builder: GatewayClientBuilder,
338 gateway_state: GatewayState,
339 ) -> anyhow::Result<Gateway> {
340 apply_migrations_server(
343 &gateway_db,
344 "gatewayd".to_string(),
345 get_gatewayd_database_migrations(),
346 )
347 .await?;
348
349 let num_route_hints = gateway_parameters.num_route_hints;
350 let network = gateway_parameters.network;
351
352 let task_group = TaskGroup::new();
353 task_group.install_kill_handler();
354
355 Ok(Self {
356 federation_manager: Arc::new(RwLock::new(FederationManager::new())),
357 mnemonic: Self::load_or_generate_mnemonic(&gateway_db).await?,
358 lightning_mode,
359 state: Arc::new(RwLock::new(gateway_state)),
360 client_builder,
361 gateway_id: Self::load_or_create_gateway_id(&gateway_db).await,
362 gateway_db,
363 versioned_api: gateway_parameters.versioned_api,
364 listen: gateway_parameters.listen,
365 lightning_module_mode: gateway_parameters.lightning_module_mode,
366 task_group,
367 bcrypt_password_hash: Arc::new(gateway_parameters.bcrypt_password_hash),
368 num_route_hints,
369 network,
370 })
371 }
372
373 async fn load_or_create_gateway_id(gateway_db: &Database) -> PublicKey {
375 let mut dbtx = gateway_db.begin_transaction().await;
376 let keypair = dbtx.load_or_create_gateway_keypair().await;
377 dbtx.commit_tx().await;
378 keypair.public_key()
379 }
380
381 pub fn gateway_id(&self) -> PublicKey {
382 self.gateway_id
383 }
384
385 pub fn versioned_api(&self) -> &SafeUrl {
386 &self.versioned_api
387 }
388
389 async fn get_state(&self) -> GatewayState {
390 self.state.read().await.clone()
391 }
392
393 pub async fn dump_database(
396 dbtx: &mut DatabaseTransaction<'_>,
397 prefix_names: Vec<String>,
398 ) -> BTreeMap<String, Box<dyn erased_serde::Serialize + Send>> {
399 dbtx.dump_database(prefix_names).await
400 }
401
402 pub async fn run(
407 self,
408 runtime: Arc<tokio::runtime::Runtime>,
409 ) -> anyhow::Result<TaskShutdownToken> {
410 self.register_clients_timer();
411 self.load_clients().await?;
412 self.start_gateway(runtime);
413 let handle = self.task_group.make_handle();
415 run_webserver(Arc::new(self)).await?;
416 let shutdown_receiver = handle.make_shutdown_rx();
417 Ok(shutdown_receiver)
418 }
419
420 fn start_gateway(&self, runtime: Arc<tokio::runtime::Runtime>) {
423 const PAYMENT_STREAM_RETRY_SECONDS: u64 = 5;
424
425 let self_copy = self.clone();
426 let tg = self.task_group.clone();
427 self.task_group.spawn(
428 "Subscribe to intercepted lightning payments in stream",
429 |handle| async move {
430 loop {
432 if handle.is_shutting_down() {
433 info!("Gateway lightning payment stream handler loop is shutting down");
434 break;
435 }
436
437 let payment_stream_task_group = tg.make_subgroup();
438 let lnrpc_route = self_copy.create_lightning_client(runtime.clone());
439
440 debug!("Establishing lightning payment stream...");
441 let (stream, ln_client) = match lnrpc_route.route_htlcs(&payment_stream_task_group).await
442 {
443 Ok((stream, ln_client)) => (stream, ln_client),
444 Err(e) => {
445 warn!(?e, "Failed to open lightning payment stream");
446 continue
447 }
448 };
449
450 self_copy.set_gateway_state(GatewayState::Connected).await;
452 info!("Established lightning payment stream");
453
454 let route_payments_response =
455 self_copy.route_lightning_payments(&handle, stream, ln_client).await;
456
457 self_copy.set_gateway_state(GatewayState::Disconnected).await;
458 if let Err(e) = payment_stream_task_group.shutdown_join_all(None).await {
459 error!("Lightning payment stream task group shutdown errors: {}", e);
460 }
461
462 match route_payments_response {
463 ReceivePaymentStreamAction::RetryAfterDelay => {
464 warn!("Disconnected from lightning node. Waiting {PAYMENT_STREAM_RETRY_SECONDS} seconds and trying again");
465 sleep(Duration::from_secs(PAYMENT_STREAM_RETRY_SECONDS)).await;
466 }
467 ReceivePaymentStreamAction::NoRetry => break,
468 }
469 }
470 },
471 );
472 }
473
474 async fn route_lightning_payments<'a>(
478 &'a self,
479 handle: &TaskHandle,
480 mut stream: RouteHtlcStream<'a>,
481 ln_client: Arc<dyn ILnRpcClient>,
482 ) -> ReceivePaymentStreamAction {
483 let (lightning_public_key, lightning_alias, lightning_network, synced_to_chain) =
484 match ln_client.parsed_node_info().await {
485 Ok((
486 lightning_public_key,
487 lightning_alias,
488 lightning_network,
489 _block_height,
490 synced_to_chain,
491 )) => (
492 lightning_public_key,
493 lightning_alias,
494 lightning_network,
495 synced_to_chain,
496 ),
497 Err(e) => {
498 warn!("Failed to retrieve Lightning info: {e:?}");
499 return ReceivePaymentStreamAction::RetryAfterDelay;
500 }
501 };
502
503 assert!(self.network == lightning_network, "Lightning node network does not match Gateway's network. LN: {lightning_network} Gateway: {}", self.network);
504
505 if synced_to_chain || is_env_var_set(FM_GATEWAY_SKIP_WAIT_FOR_SYNC_ENV) {
506 info!("Gateway is already synced to chain");
507 } else {
508 self.set_gateway_state(GatewayState::Syncing).await;
509 if let Err(e) = ln_client.wait_for_chain_sync().await {
510 error!(?e, "Failed to wait for chain sync");
511 return ReceivePaymentStreamAction::RetryAfterDelay;
512 }
513 }
514
515 let lightning_context = LightningContext {
516 lnrpc: ln_client,
517 lightning_public_key,
518 lightning_alias,
519 lightning_network,
520 };
521 self.set_gateway_state(GatewayState::Running { lightning_context })
522 .await;
523 info!("Gateway is running");
524
525 if handle
528 .cancel_on_shutdown(async move {
529 loop {
530 let payment_request_or = tokio::select! {
531 payment_request_or = stream.next() => {
532 payment_request_or
533 }
534 () = self.is_shutting_down_safely() => {
535 break;
536 }
537 };
538
539 let Some(payment_request) = payment_request_or else {
540 warn!(
541 "Unexpected response from incoming lightning payment stream. Exiting from loop..."
542 );
543 break;
544 };
545
546 let state_guard = self.state.read().await;
547 if let GatewayState::Running { ref lightning_context } = *state_guard {
548 self.handle_lightning_payment(payment_request, lightning_context).await;
549 } else {
550 warn!(
551 ?state_guard,
552 "Gateway isn't in a running state, cannot handle incoming payments."
553 );
554 break;
555 };
556 }
557 })
558 .await
559 .is_ok()
560 {
561 warn!("Lightning payment stream connection broken. Gateway is disconnected");
562 ReceivePaymentStreamAction::RetryAfterDelay
563 } else {
564 info!("Received shutdown signal");
565 ReceivePaymentStreamAction::NoRetry
566 }
567 }
568
569 async fn is_shutting_down_safely(&self) {
572 loop {
573 if let GatewayState::ShuttingDown { .. } = self.get_state().await {
574 return;
575 }
576
577 fedimint_core::task::sleep(Duration::from_secs(1)).await;
578 }
579 }
580
581 async fn handle_lightning_payment(
586 &self,
587 payment_request: InterceptPaymentRequest,
588 lightning_context: &LightningContext,
589 ) {
590 info!(
591 "Intercepting lightning payment {}",
592 PrettyInterceptPaymentRequest(&payment_request)
593 );
594
595 if self
596 .try_handle_lightning_payment_lnv2(&payment_request, lightning_context)
597 .await
598 .is_ok()
599 {
600 return;
601 }
602
603 if self
604 .try_handle_lightning_payment_ln_legacy(&payment_request)
605 .await
606 .is_ok()
607 {
608 return;
609 }
610
611 Self::forward_lightning_payment(payment_request, lightning_context).await;
612 }
613
614 async fn try_handle_lightning_payment_lnv2(
617 &self,
618 htlc_request: &InterceptPaymentRequest,
619 lightning_context: &LightningContext,
620 ) -> Result<()> {
621 let (contract, client) = self
627 .get_registered_incoming_contract_and_client_v2(
628 PaymentImage::Hash(htlc_request.payment_hash),
629 htlc_request.amount_msat,
630 )
631 .await?;
632
633 if let Err(error) = client
634 .get_first_module::<GatewayClientModuleV2>()
635 .expect("Must have client module")
636 .relay_incoming_htlc(
637 htlc_request.payment_hash,
638 htlc_request.incoming_chan_id,
639 htlc_request.htlc_id,
640 contract,
641 htlc_request.amount_msat,
642 )
643 .await
644 {
645 error!("Error relaying incoming lightning payment: {error:?}");
646
647 let outcome = InterceptPaymentResponse {
648 action: PaymentAction::Cancel,
649 payment_hash: htlc_request.payment_hash,
650 incoming_chan_id: htlc_request.incoming_chan_id,
651 htlc_id: htlc_request.htlc_id,
652 };
653
654 if let Err(error) = lightning_context.lnrpc.complete_htlc(outcome).await {
655 error!("Error sending HTLC response to lightning node: {error:?}");
656 }
657 }
658
659 Ok(())
660 }
661
662 async fn try_handle_lightning_payment_ln_legacy(
665 &self,
666 htlc_request: &InterceptPaymentRequest,
667 ) -> Result<()> {
668 let Some(federation_index) = htlc_request.short_channel_id else {
670 return Err(PublicGatewayError::LNv1(LNv1Error::IncomingPayment(
671 "Incoming payment has not last hop short channel id".to_string(),
672 )));
673 };
674
675 let Some(client) = self
676 .federation_manager
677 .read()
678 .await
679 .get_client_for_index(federation_index)
680 else {
681 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())));
682 };
683
684 client
685 .borrow()
686 .with(|client| async {
687 let htlc = htlc_request.clone().try_into();
688 if let Ok(htlc) = htlc {
689 match client
690 .get_first_module::<GatewayClientModule>()
691 .expect("Must have client module")
692 .gateway_handle_intercepted_htlc(htlc)
693 .await
694 {
695 Ok(_) => Ok(()),
696 Err(e) => Err(PublicGatewayError::LNv1(LNv1Error::IncomingPayment(
697 format!("Error intercepting lightning payment {e:?}"),
698 ))),
699 }
700 } else {
701 Err(PublicGatewayError::LNv1(LNv1Error::IncomingPayment(
702 "Could not convert InterceptHtlcResult into an HTLC".to_string(),
703 )))
704 }
705 })
706 .await
707 }
708
709 async fn forward_lightning_payment(
713 htlc_request: InterceptPaymentRequest,
714 lightning_context: &LightningContext,
715 ) {
716 let outcome = InterceptPaymentResponse {
717 action: PaymentAction::Forward,
718 payment_hash: htlc_request.payment_hash,
719 incoming_chan_id: htlc_request.incoming_chan_id,
720 htlc_id: htlc_request.htlc_id,
721 };
722
723 if let Err(error) = lightning_context.lnrpc.complete_htlc(outcome).await {
724 error!("Error sending lightning payment response to lightning node: {error:?}");
725 }
726 }
727
728 async fn set_gateway_state(&self, state: GatewayState) {
730 let mut lock = self.state.write().await;
731 *lock = state;
732 }
733
734 pub async fn handle_get_info(&self) -> AdminResult<GatewayInfo> {
737 let GatewayState::Running { lightning_context } = self.get_state().await else {
738 return Ok(GatewayInfo {
739 federations: vec![],
740 federation_fake_scids: None,
741 version_hash: fedimint_build_code_version_env!().to_string(),
742 lightning_pub_key: None,
743 lightning_alias: None,
744 gateway_id: self.gateway_id,
745 gateway_state: self.state.read().await.to_string(),
746 network: self.network,
747 block_height: None,
748 synced_to_chain: false,
749 api: self.versioned_api.clone(),
750 lightning_mode: self.lightning_mode.clone(),
751 });
752 };
753
754 let dbtx = self.gateway_db.begin_transaction_nc().await;
755 let federations = self
756 .federation_manager
757 .read()
758 .await
759 .federation_info_all_federations(dbtx)
760 .await;
761
762 let channels: BTreeMap<u64, FederationId> = federations
763 .iter()
764 .map(|federation_info| {
765 (
766 federation_info.config.federation_index,
767 federation_info.federation_id,
768 )
769 })
770 .collect();
771
772 let node_info = lightning_context.lnrpc.parsed_node_info().await?;
773
774 Ok(GatewayInfo {
775 federations,
776 federation_fake_scids: Some(channels),
777 version_hash: fedimint_build_code_version_env!().to_string(),
778 lightning_pub_key: Some(lightning_context.lightning_public_key.to_string()),
779 lightning_alias: Some(lightning_context.lightning_alias.clone()),
780 gateway_id: self.gateway_id,
781 gateway_state: self.state.read().await.to_string(),
782 network: self.network,
783 block_height: Some(node_info.3),
784 synced_to_chain: node_info.4,
785 api: self.versioned_api.clone(),
786 lightning_mode: self.lightning_mode.clone(),
787 })
788 }
789
790 pub async fn handle_get_federation_config(
793 &self,
794 federation_id_or: Option<FederationId>,
795 ) -> AdminResult<GatewayFedConfig> {
796 if !matches!(self.get_state().await, GatewayState::Running { .. }) {
797 return Ok(GatewayFedConfig {
798 federations: BTreeMap::new(),
799 });
800 }
801
802 let federations = if let Some(federation_id) = federation_id_or {
803 let mut federations = BTreeMap::new();
804 federations.insert(
805 federation_id,
806 self.federation_manager
807 .read()
808 .await
809 .get_federation_config(federation_id)
810 .await?,
811 );
812 federations
813 } else {
814 self.federation_manager
815 .read()
816 .await
817 .get_all_federation_configs()
818 .await
819 };
820
821 Ok(GatewayFedConfig { federations })
822 }
823
824 pub async fn handle_address_msg(&self, payload: DepositAddressPayload) -> AdminResult<Address> {
827 let (_, address, _) = self
828 .select_client(payload.federation_id)
829 .await?
830 .value()
831 .get_first_module::<WalletClientModule>()
832 .expect("Must have client module")
833 .allocate_deposit_address_expert_only(())
834 .await?;
835 Ok(address)
836 }
837
838 pub async fn handle_withdraw_msg(
841 &self,
842 payload: WithdrawPayload,
843 ) -> AdminResult<WithdrawResponse> {
844 let WithdrawPayload {
845 amount,
846 address,
847 federation_id,
848 } = payload;
849
850 let address_network = get_network_for_address(&address);
851 let gateway_network = self.network;
852 let Ok(address) = address.require_network(gateway_network) else {
853 return Err(AdminGatewayError::WithdrawError {
854 failure_reason: format!("Gateway is running on network {gateway_network}, but provided withdraw address is for network {address_network}")
855 });
856 };
857
858 let client = self.select_client(federation_id).await?;
859 let wallet_module = client.value().get_first_module::<WalletClientModule>()?;
860
861 let (amount, fees) = match amount {
863 BitcoinAmountOrAll::All => {
866 let balance =
867 bitcoin::Amount::from_sat(client.value().get_balance().await.msats / 1000);
868 let fees = wallet_module.get_withdraw_fees(&address, balance).await?;
869 let withdraw_amount = balance.checked_sub(fees.amount());
870 if withdraw_amount.is_none() {
871 return Err(AdminGatewayError::WithdrawError {
872 failure_reason: format!(
873 "Insufficient funds. Balance: {balance} Fees: {fees:?}"
874 ),
875 });
876 }
877 (withdraw_amount.unwrap(), fees)
878 }
879 BitcoinAmountOrAll::Amount(amount) => (
880 amount,
881 wallet_module.get_withdraw_fees(&address, amount).await?,
882 ),
883 };
884
885 let operation_id = wallet_module.withdraw(&address, amount, fees, ()).await?;
886 let mut updates = wallet_module
887 .subscribe_withdraw_updates(operation_id)
888 .await?
889 .into_stream();
890
891 while let Some(update) = updates.next().await {
892 match update {
893 WithdrawState::Succeeded(txid) => {
894 info!("Sent {amount} funds to address {}", address);
895 return Ok(WithdrawResponse { txid, fees });
896 }
897 WithdrawState::Failed(e) => {
898 return Err(AdminGatewayError::WithdrawError { failure_reason: e });
899 }
900 WithdrawState::Created => {}
901 }
902 }
903
904 Err(AdminGatewayError::WithdrawError {
905 failure_reason: "Ran out of state updates while withdrawing".to_string(),
906 })
907 }
908
909 async fn handle_create_invoice_for_operator_msg(
912 &self,
913 payload: CreateInvoiceForOperatorPayload,
914 ) -> AdminResult<Bolt11Invoice> {
915 let GatewayState::Running { lightning_context } = self.get_state().await else {
916 return Err(AdminGatewayError::Lightning(
917 LightningRpcError::FailedToConnect,
918 ));
919 };
920
921 Bolt11Invoice::from_str(
922 &lightning_context
923 .lnrpc
924 .create_invoice(CreateInvoiceRequest {
925 payment_hash: None, amount_msat: payload.amount_msats,
928 expiry_secs: payload.expiry_secs.unwrap_or(3600),
929 description: payload.description.map(InvoiceDescription::Direct),
930 })
931 .await?
932 .invoice,
933 )
934 .map_err(|e| {
935 AdminGatewayError::Lightning(LightningRpcError::InvalidMetadata {
936 failure_reason: e.to_string(),
937 })
938 })
939 }
940
941 async fn handle_pay_invoice_for_operator_msg(
944 &self,
945 payload: PayInvoiceForOperatorPayload,
946 ) -> AdminResult<Preimage> {
947 const BASE_FEE: u64 = 50;
949 const FEE_DENOMINATOR: u64 = 100;
950 const MAX_DELAY: u64 = 1008;
951
952 let GatewayState::Running { lightning_context } = self.get_state().await else {
953 return Err(AdminGatewayError::Lightning(
954 LightningRpcError::FailedToConnect,
955 ));
956 };
957
958 let max_fee = BASE_FEE
959 + payload
960 .invoice
961 .amount_milli_satoshis()
962 .context("Invoice is missing amount")?
963 .saturating_div(FEE_DENOMINATOR);
964
965 let res = lightning_context
966 .lnrpc
967 .pay(payload.invoice, MAX_DELAY, Amount::from_msats(max_fee))
968 .await?;
969 Ok(res.preimage)
970 }
971
972 async fn handle_pay_invoice_msg(
975 &self,
976 payload: fedimint_ln_client::pay::PayInvoicePayload,
977 ) -> Result<Preimage> {
978 let GatewayState::Running { .. } = self.get_state().await else {
979 return Err(PublicGatewayError::Lightning(
980 LightningRpcError::FailedToConnect,
981 ));
982 };
983
984 debug!("Handling pay invoice message: {payload:?}");
985 let client = self.select_client(payload.federation_id).await?;
986 let contract_id = payload.contract_id;
987 let gateway_module = &client
988 .value()
989 .get_first_module::<GatewayClientModule>()
990 .map_err(LNv1Error::OutgoingPayment)
991 .map_err(PublicGatewayError::LNv1)?;
992 let operation_id = gateway_module
993 .gateway_pay_bolt11_invoice(payload)
994 .await
995 .map_err(LNv1Error::OutgoingPayment)
996 .map_err(PublicGatewayError::LNv1)?;
997 let mut updates = gateway_module
998 .gateway_subscribe_ln_pay(operation_id)
999 .await
1000 .map_err(LNv1Error::OutgoingPayment)
1001 .map_err(PublicGatewayError::LNv1)?
1002 .into_stream();
1003 while let Some(update) = updates.next().await {
1004 match update {
1005 GatewayExtPayStates::Success { preimage, .. } => {
1006 debug!("Successfully paid invoice: {contract_id}");
1007 return Ok(preimage);
1008 }
1009 GatewayExtPayStates::Fail {
1010 error,
1011 error_message,
1012 } => {
1013 return Err(PublicGatewayError::LNv1(LNv1Error::OutgoingContract {
1014 error: Box::new(error),
1015 message: format!(
1016 "{error_message} while paying invoice with contract id {contract_id}"
1017 ),
1018 }));
1019 }
1020 GatewayExtPayStates::Canceled { error } => {
1021 return Err(PublicGatewayError::LNv1(LNv1Error::OutgoingContract { error: Box::new(error.clone()), message: format!("Cancelled with {error} while paying invoice with contract id {contract_id}") }));
1022 }
1023 GatewayExtPayStates::Created => {
1024 debug!("Got initial state Created while paying invoice: {contract_id}");
1025 }
1026 other => {
1027 info!("Got state {other:?} while paying invoice: {contract_id}");
1028 }
1029 };
1030 }
1031
1032 Err(PublicGatewayError::LNv1(LNv1Error::OutgoingPayment(
1033 anyhow!("Ran out of state updates while paying invoice"),
1034 )))
1035 }
1036
1037 pub async fn handle_connect_federation(
1042 &self,
1043 payload: ConnectFedPayload,
1044 ) -> AdminResult<FederationInfo> {
1045 let GatewayState::Running { lightning_context } = self.get_state().await else {
1046 return Err(AdminGatewayError::Lightning(
1047 LightningRpcError::FailedToConnect,
1048 ));
1049 };
1050
1051 let invite_code = InviteCode::from_str(&payload.invite_code).map_err(|e| {
1052 AdminGatewayError::ClientCreationError(anyhow!(format!(
1053 "Invalid federation member string {e:?}"
1054 )))
1055 })?;
1056
1057 #[cfg(feature = "tor")]
1058 let connector = match &payload.use_tor {
1059 Some(true) => Connector::tor(),
1060 Some(false) => Connector::default(),
1061 None => {
1062 info!("Missing `use_tor` payload field, defaulting to `Connector::Tcp` variant!");
1063 Connector::default()
1064 }
1065 };
1066
1067 #[cfg(not(feature = "tor"))]
1068 let connector = Connector::default();
1069
1070 let federation_id = invite_code.federation_id();
1071
1072 let mut federation_manager = self.federation_manager.write().await;
1073
1074 if federation_manager.has_federation(federation_id) {
1076 return Err(AdminGatewayError::ClientCreationError(anyhow!(
1077 "Federation has already been registered"
1078 )));
1079 }
1080
1081 let federation_index = federation_manager.pop_next_index()?;
1084
1085 let federation_config = FederationConfig {
1086 invite_code,
1087 federation_index,
1088 lightning_fee: PaymentFee::TRANSACTION_FEE_DEFAULT,
1089 transaction_fee: PaymentFee::TRANSACTION_FEE_DEFAULT,
1090 connector,
1091 };
1092
1093 let recover = payload.recover.unwrap_or(false);
1094 if recover {
1095 self.client_builder
1096 .recover(
1097 federation_config.clone(),
1098 Arc::new(self.clone()),
1099 &self.mnemonic,
1100 )
1101 .await?;
1102 }
1103
1104 let client = self
1105 .client_builder
1106 .build(
1107 federation_config.clone(),
1108 Arc::new(self.clone()),
1109 &self.mnemonic,
1110 )
1111 .await?;
1112
1113 if recover {
1114 client.wait_for_all_active_state_machines().await?;
1115 }
1116
1117 let federation_info = FederationInfo {
1120 federation_id,
1121 federation_name: federation_manager.federation_name(&client).await,
1122 balance_msat: client.get_balance().await,
1123 config: federation_config.clone(),
1124 };
1125
1126 if self.is_running_lnv1() {
1127 Self::check_lnv1_federation_network(&client, self.network).await?;
1128 client
1129 .get_first_module::<GatewayClientModule>()?
1130 .try_register_with_federation(
1131 Vec::new(),
1133 GW_ANNOUNCEMENT_TTL,
1134 federation_config.lightning_fee.into(),
1135 lightning_context,
1136 )
1137 .await;
1138 }
1139
1140 if self.is_running_lnv2() {
1141 Self::check_lnv2_federation_network(&client, self.network).await?;
1142 }
1143
1144 federation_manager.add_client(
1146 federation_index,
1147 Spanned::new(
1148 info_span!("client", federation_id=%federation_id.clone()),
1149 async { client },
1150 )
1151 .await,
1152 );
1153
1154 let mut dbtx = self.gateway_db.begin_transaction().await;
1155 dbtx.save_federation_config(&federation_config).await;
1156 dbtx.commit_tx().await;
1157 debug!("Federation with ID: {federation_id} connected and assigned federation index: {federation_index}");
1158
1159 Ok(federation_info)
1160 }
1161
1162 pub async fn handle_leave_federation(
1167 &self,
1168 payload: LeaveFedPayload,
1169 ) -> AdminResult<FederationInfo> {
1170 let mut federation_manager = self.federation_manager.write().await;
1173 let mut dbtx = self.gateway_db.begin_transaction().await;
1174
1175 let federation_info = federation_manager
1176 .leave_federation(payload.federation_id, &mut dbtx.to_ref_nc())
1177 .await?;
1178
1179 dbtx.remove_federation_config(payload.federation_id).await;
1180 dbtx.commit_tx().await;
1181 Ok(federation_info)
1182 }
1183
1184 pub async fn handle_backup_msg(
1187 &self,
1188 BackupPayload { federation_id }: BackupPayload,
1189 ) -> AdminResult<()> {
1190 let federation_manager = self.federation_manager.read().await;
1191 let client = federation_manager
1192 .client(&federation_id)
1193 .ok_or(AdminGatewayError::ClientCreationError(anyhow::anyhow!(
1194 format!("Gateway has not connected to {federation_id}")
1195 )))?
1196 .value();
1197 let metadata: BTreeMap<String, String> = BTreeMap::new();
1198 client
1199 .backup_to_federation(fedimint_client::backup::Metadata::from_json_serialized(
1200 metadata,
1201 ))
1202 .await?;
1203 Ok(())
1204 }
1205
1206 pub async fn handle_mnemonic_msg(&self) -> AdminResult<MnemonicResponse> {
1210 let words = self
1211 .mnemonic
1212 .words()
1213 .map(std::string::ToString::to_string)
1214 .collect::<Vec<_>>();
1215 let all_federations = self
1216 .federation_manager
1217 .read()
1218 .await
1219 .get_all_federation_configs()
1220 .await
1221 .keys()
1222 .copied()
1223 .collect::<BTreeSet<_>>();
1224 let legacy_federations = self.client_builder.legacy_federations(all_federations);
1225 let mnemonic_response = MnemonicResponse {
1226 mnemonic: words,
1227 legacy_federations,
1228 };
1229 Ok(mnemonic_response)
1230 }
1231
1232 pub async fn handle_set_fees_msg(
1235 &self,
1236 SetFeesPayload {
1237 federation_id,
1238 lightning_base,
1239 lightning_parts_per_million,
1240 transaction_base,
1241 transaction_parts_per_million,
1242 }: SetFeesPayload,
1243 ) -> AdminResult<()> {
1244 let mut dbtx = self.gateway_db.begin_transaction().await;
1245 let mut fed_configs = if let Some(fed_id) = federation_id {
1246 dbtx.load_federation_configs()
1247 .await
1248 .into_iter()
1249 .filter(|(id, _)| *id == fed_id)
1250 .collect::<BTreeMap<_, _>>()
1251 } else {
1252 dbtx.load_federation_configs().await
1253 };
1254
1255 for config in &mut fed_configs.values_mut() {
1256 let mut lightning_fee = config.lightning_fee;
1257 if let Some(lightning_base) = lightning_base {
1258 lightning_fee.base = lightning_base;
1259 }
1260
1261 if let Some(lightning_ppm) = lightning_parts_per_million {
1262 lightning_fee.parts_per_million = lightning_ppm;
1263 }
1264
1265 let mut transaction_fee = config.transaction_fee;
1266 if let Some(transaction_base) = transaction_base {
1267 transaction_fee.base = transaction_base;
1268 }
1269
1270 if let Some(transaction_ppm) = transaction_parts_per_million {
1271 transaction_fee.parts_per_million = transaction_ppm;
1272 }
1273
1274 let send_fees = lightning_fee + transaction_fee;
1276 if !self.is_running_lnv1() && send_fees.gt(&PaymentFee::SEND_FEE_LIMIT) {
1277 return Err(AdminGatewayError::GatewayConfigurationError(format!(
1278 "Total Send fees exceeded {}",
1279 PaymentFee::SEND_FEE_LIMIT
1280 )));
1281 }
1282
1283 if !self.is_running_lnv1() && transaction_fee.gt(&PaymentFee::RECEIVE_FEE_LIMIT) {
1285 return Err(AdminGatewayError::GatewayConfigurationError(format!(
1286 "Transaction fees exceeded RECEIVE LIMIT {}",
1287 PaymentFee::RECEIVE_FEE_LIMIT
1288 )));
1289 }
1290
1291 config.lightning_fee = lightning_fee;
1292 config.transaction_fee = transaction_fee;
1293 dbtx.save_federation_config(config).await;
1294 }
1295
1296 dbtx.commit_tx().await;
1297
1298 if self.is_running_lnv1() {
1299 let register_task_group = TaskGroup::new();
1300
1301 self.register_federations(&fed_configs, ®ister_task_group)
1302 .await;
1303 }
1304
1305 Ok(())
1306 }
1307
1308 pub async fn handle_get_ln_onchain_address_msg(&self) -> AdminResult<Address> {
1310 let context = self.get_lightning_context().await?;
1311 let response = context.lnrpc.get_ln_onchain_address().await?;
1312
1313 let address = Address::from_str(&response.address).map_err(|e| {
1314 AdminGatewayError::Lightning(LightningRpcError::InvalidMetadata {
1315 failure_reason: e.to_string(),
1316 })
1317 })?;
1318
1319 address.require_network(self.network).map_err(|e| {
1320 AdminGatewayError::Lightning(LightningRpcError::InvalidMetadata {
1321 failure_reason: e.to_string(),
1322 })
1323 })
1324 }
1325
1326 pub async fn handle_open_channel_msg(&self, payload: OpenChannelRequest) -> AdminResult<Txid> {
1329 let context = self.get_lightning_context().await?;
1330 let res = context.lnrpc.open_channel(payload).await?;
1331 Txid::from_str(&res.funding_txid).map_err(|e| {
1332 AdminGatewayError::Lightning(LightningRpcError::InvalidMetadata {
1333 failure_reason: format!("Received invalid channel funding txid string {e}"),
1334 })
1335 })
1336 }
1337
1338 pub async fn handle_close_channels_with_peer_msg(
1341 &self,
1342 payload: CloseChannelsWithPeerRequest,
1343 ) -> AdminResult<CloseChannelsWithPeerResponse> {
1344 let context = self.get_lightning_context().await?;
1345 let response = context.lnrpc.close_channels_with_peer(payload).await?;
1346 Ok(response)
1347 }
1348
1349 pub async fn handle_list_active_channels_msg(
1352 &self,
1353 ) -> AdminResult<Vec<fedimint_lightning::ChannelInfo>> {
1354 let context = self.get_lightning_context().await?;
1355 let response = context.lnrpc.list_active_channels().await?;
1356 Ok(response.channels)
1357 }
1358
1359 pub async fn handle_send_onchain_msg(&self, payload: SendOnchainRequest) -> AdminResult<Txid> {
1361 let context = self.get_lightning_context().await?;
1362 let response = context.lnrpc.send_onchain(payload).await?;
1363 Txid::from_str(&response.txid).map_err(|e| AdminGatewayError::WithdrawError {
1364 failure_reason: format!("Failed to parse withdrawal TXID: {e}"),
1365 })
1366 }
1367
1368 pub async fn handle_recheck_address_msg(
1370 &self,
1371 payload: DepositAddressRecheckPayload,
1372 ) -> AdminResult<()> {
1373 self.select_client(payload.federation_id)
1374 .await?
1375 .value()
1376 .get_first_module::<WalletClientModule>()
1377 .expect("Must have client module")
1378 .recheck_pegin_address_by_address(payload.address)
1379 .await?;
1380 Ok(())
1381 }
1382
1383 pub async fn handle_get_balances_msg(&self) -> AdminResult<GatewayBalances> {
1386 let dbtx = self.gateway_db.begin_transaction_nc().await;
1387 let federation_infos = self
1388 .federation_manager
1389 .read()
1390 .await
1391 .federation_info_all_federations(dbtx)
1392 .await;
1393
1394 let ecash_balances: Vec<FederationBalanceInfo> = federation_infos
1395 .iter()
1396 .map(|federation_info| FederationBalanceInfo {
1397 federation_id: federation_info.federation_id,
1398 ecash_balance_msats: Amount {
1399 msats: federation_info.balance_msat.msats,
1400 },
1401 })
1402 .collect();
1403
1404 let context = self.get_lightning_context().await?;
1405 let lightning_node_balances = context.lnrpc.get_balances().await?;
1406
1407 Ok(GatewayBalances {
1408 onchain_balance_sats: lightning_node_balances.onchain_balance_sats,
1409 lightning_balance_msats: lightning_node_balances.lightning_balance_msats,
1410 ecash_balances,
1411 inbound_lightning_liquidity_msats: lightning_node_balances
1412 .inbound_lightning_liquidity_msats,
1413 })
1414 }
1415
1416 pub async fn handle_spend_ecash_msg(
1418 &self,
1419 payload: SpendEcashPayload,
1420 ) -> AdminResult<SpendEcashResponse> {
1421 let client = self
1422 .select_client(payload.federation_id)
1423 .await?
1424 .into_value();
1425 let mint_module = client.get_first_module::<MintClientModule>()?;
1426 let timeout = Duration::from_secs(payload.timeout);
1427 let (operation_id, notes) = if payload.allow_overpay {
1428 let (operation_id, notes) = mint_module
1429 .spend_notes_with_selector(
1430 &SelectNotesWithAtleastAmount,
1431 payload.amount,
1432 timeout,
1433 payload.include_invite,
1434 (),
1435 )
1436 .await?;
1437
1438 let overspend_amount = notes.total_amount().saturating_sub(payload.amount);
1439 if overspend_amount != Amount::ZERO {
1440 warn!(
1441 "Selected notes {} worth more than requested",
1442 overspend_amount
1443 );
1444 }
1445
1446 (operation_id, notes)
1447 } else {
1448 mint_module
1449 .spend_notes_with_selector(
1450 &SelectNotesWithExactAmount,
1451 payload.amount,
1452 timeout,
1453 payload.include_invite,
1454 (),
1455 )
1456 .await?
1457 };
1458
1459 info!("Spend ecash operation id: {:?}", operation_id);
1460 info!("Spend ecash notes: {:?}", notes);
1461
1462 Ok(SpendEcashResponse {
1463 operation_id,
1464 notes,
1465 })
1466 }
1467
1468 pub async fn handle_receive_ecash_msg(
1470 &self,
1471 payload: ReceiveEcashPayload,
1472 ) -> Result<ReceiveEcashResponse> {
1473 let amount = payload.notes.total_amount();
1474 let client = self
1475 .federation_manager
1476 .read()
1477 .await
1478 .get_client_for_federation_id_prefix(payload.notes.federation_id_prefix())
1479 .ok_or(FederationNotConnected {
1480 federation_id_prefix: payload.notes.federation_id_prefix(),
1481 })?;
1482 let mint = client
1483 .value()
1484 .get_first_module::<MintClientModule>()
1485 .map_err(|e| PublicGatewayError::ReceiveEcashError {
1486 failure_reason: format!("Mint module does not exist: {e:?}"),
1487 })?;
1488
1489 let operation_id = mint
1490 .reissue_external_notes(payload.notes, ())
1491 .await
1492 .map_err(|e| PublicGatewayError::ReceiveEcashError {
1493 failure_reason: e.to_string(),
1494 })?;
1495 if payload.wait {
1496 let mut updates = mint
1497 .subscribe_reissue_external_notes(operation_id)
1498 .await
1499 .unwrap()
1500 .into_stream();
1501
1502 while let Some(update) = updates.next().await {
1503 if let fedimint_mint_client::ReissueExternalNotesState::Failed(e) = update {
1504 return Err(PublicGatewayError::ReceiveEcashError {
1505 failure_reason: e.to_string(),
1506 });
1507 }
1508 }
1509 }
1510
1511 Ok(ReceiveEcashResponse { amount })
1512 }
1513
1514 pub async fn handle_shutdown_msg(&self, task_group: TaskGroup) -> AdminResult<()> {
1517 let mut state_guard = self.state.write().await;
1519 if let GatewayState::Running { lightning_context } = state_guard.clone() {
1520 *state_guard = GatewayState::ShuttingDown { lightning_context };
1521
1522 self.federation_manager
1523 .read()
1524 .await
1525 .wait_for_incoming_payments()
1526 .await?;
1527 }
1528
1529 let tg = task_group.clone();
1530 tg.spawn("Kill Gateway", |_task_handle| async {
1531 if let Err(e) = task_group.shutdown_join_all(Duration::from_secs(180)).await {
1532 error!(?e, "Error shutting down gateway");
1533 }
1534 });
1535 Ok(())
1536 }
1537
1538 pub async fn handle_payment_log_msg(
1540 &self,
1541 PaymentLogPayload {
1542 end_position,
1543 pagination_size,
1544 federation_id,
1545 event_kinds,
1546 }: PaymentLogPayload,
1547 ) -> AdminResult<PaymentLogResponse> {
1548 const BATCH_SIZE: u64 = 10_000;
1549 let federation_manager = self.federation_manager.read().await;
1550 let client = federation_manager
1551 .client(&federation_id)
1552 .ok_or(FederationNotConnected {
1553 federation_id_prefix: federation_id.to_prefix(),
1554 })?
1555 .value();
1556
1557 let event_kinds = if event_kinds.is_empty() {
1558 ALL_GATEWAY_EVENTS.to_vec()
1559 } else {
1560 event_kinds
1561 };
1562
1563 let end_position = if let Some(position) = end_position {
1564 position
1565 } else {
1566 let mut dbtx = client.db().begin_transaction_nc().await;
1567 dbtx.get_next_event_log_id().await
1568 };
1569
1570 let mut start_position = end_position.saturating_sub(BATCH_SIZE);
1571
1572 let mut payment_log = Vec::new();
1573
1574 let log_start = EventLogId::new(0);
1575 while payment_log.len() < pagination_size {
1576 let batch = client.get_event_log(Some(start_position), BATCH_SIZE).await;
1577 let mut filtered_batch = batch
1578 .into_iter()
1579 .filter(|e| e.0 <= end_position && event_kinds.contains(&e.1))
1580 .collect::<Vec<_>>();
1581 filtered_batch.reverse();
1582 payment_log.extend(filtered_batch);
1583
1584 start_position = start_position.saturating_sub(BATCH_SIZE);
1586
1587 if start_position == log_start {
1588 break;
1589 }
1590 }
1591
1592 payment_log.truncate(pagination_size);
1594
1595 Ok(PaymentLogResponse(payment_log))
1596 }
1597
1598 async fn register_federations(
1600 &self,
1601 federations: &BTreeMap<FederationId, FederationConfig>,
1602 register_task_group: &TaskGroup,
1603 ) {
1604 if let Ok(lightning_context) = self.get_lightning_context().await {
1605 let route_hints = lightning_context
1606 .lnrpc
1607 .parsed_route_hints(self.num_route_hints)
1608 .await;
1609 if route_hints.is_empty() {
1610 warn!("Gateway did not retrieve any route hints, may reduce receive success rate.");
1611 }
1612
1613 for (federation_id, federation_config) in federations {
1614 let fed_manager = self.federation_manager.read().await;
1615 if let Some(client) = fed_manager.client(federation_id) {
1616 let client_arc = client.clone().into_value();
1617 let route_hints = route_hints.clone();
1618 let lightning_context = lightning_context.clone();
1619 let federation_config = federation_config.clone();
1620
1621 if let Err(e) = register_task_group
1622 .spawn_cancellable("register_federation", async move {
1623 let gateway_client = client_arc
1624 .get_first_module::<GatewayClientModule>()
1625 .expect("No GatewayClientModule exists");
1626 gateway_client
1627 .try_register_with_federation(
1628 route_hints,
1629 GW_ANNOUNCEMENT_TTL,
1630 federation_config.lightning_fee.into(),
1631 lightning_context,
1632 )
1633 .await;
1634 })
1635 .await
1636 {
1637 warn!(?e, "Failed to shutdown register federation task");
1638 }
1639 }
1640 }
1641 }
1642 }
1643
1644 pub async fn select_client(
1647 &self,
1648 federation_id: FederationId,
1649 ) -> std::result::Result<Spanned<fedimint_client::ClientHandleArc>, FederationNotConnected>
1650 {
1651 self.federation_manager
1652 .read()
1653 .await
1654 .client(&federation_id)
1655 .cloned()
1656 .ok_or(FederationNotConnected {
1657 federation_id_prefix: federation_id.to_prefix(),
1658 })
1659 }
1660
1661 async fn load_or_generate_mnemonic(gateway_db: &Database) -> AdminResult<Mnemonic> {
1666 Ok(
1667 if let Ok(entropy) = Client::load_decodable_client_secret::<Vec<u8>>(gateway_db).await {
1668 Mnemonic::from_entropy(&entropy)
1669 .map_err(|e| AdminGatewayError::MnemonicError(anyhow!(e.to_string())))?
1670 } else {
1671 let mnemonic = if let Ok(words) = std::env::var(FM_GATEWAY_MNEMONIC_ENV) {
1672 info!("Using provided mnemonic from environment variable");
1673 Mnemonic::parse_in_normalized(Language::English, words.as_str()).map_err(
1674 |e| {
1675 AdminGatewayError::MnemonicError(anyhow!(format!(
1676 "Seed phrase provided in environment was invalid {e:?}"
1677 )))
1678 },
1679 )?
1680 } else {
1681 debug!("Generating mnemonic and writing entropy to client storage");
1682 Bip39RootSecretStrategy::<12>::random(&mut thread_rng())
1683 };
1684
1685 Client::store_encodable_client_secret(gateway_db, mnemonic.to_entropy())
1686 .await
1687 .map_err(AdminGatewayError::MnemonicError)?;
1688 mnemonic
1689 },
1690 )
1691 }
1692
1693 async fn load_clients(&self) -> AdminResult<()> {
1697 let mut federation_manager = self.federation_manager.write().await;
1698
1699 let configs = {
1700 let mut dbtx = self.gateway_db.begin_transaction_nc().await;
1701 dbtx.load_federation_configs().await
1702 };
1703
1704 if let Some(max_federation_index) = configs.values().map(|cfg| cfg.federation_index).max() {
1705 federation_manager.set_next_index(max_federation_index + 1);
1706 }
1707
1708 for (federation_id, config) in configs {
1709 let federation_index = config.federation_index;
1710 if let Ok(client) = Box::pin(Spanned::try_new(
1711 info_span!("client", federation_id = %federation_id.clone()),
1712 self.client_builder
1713 .build(config, Arc::new(self.clone()), &self.mnemonic),
1714 ))
1715 .await
1716 {
1717 federation_manager.add_client(federation_index, client);
1718 } else {
1719 warn!("Failed to load client for federation: {federation_id}");
1720 }
1721 }
1722
1723 Ok(())
1724 }
1725
1726 fn register_clients_timer(&self) {
1732 if self.is_running_lnv1() {
1734 let lightning_module_mode = self.lightning_module_mode;
1735 info!(?lightning_module_mode, "Spawning register task...");
1736 let gateway = self.clone();
1737 let register_task_group = self.task_group.make_subgroup();
1738 self.task_group.spawn_cancellable("register clients", async move {
1739 loop {
1740 let gateway_state = gateway.get_state().await;
1741 if let GatewayState::Running { .. } = &gateway_state {
1742 let mut dbtx = gateway.gateway_db.begin_transaction_nc().await;
1743 let all_federations_configs = dbtx.load_federation_configs().await.into_iter().collect();
1744 gateway.register_federations(&all_federations_configs, ®ister_task_group).await;
1745 } else {
1746 const NOT_RUNNING_RETRY: Duration = Duration::from_secs(10);
1748 info!("Will not register federation yet because gateway still not in Running state. Current state: {gateway_state:?}. Will keep waiting, next retry in {NOT_RUNNING_RETRY:?}...");
1749 sleep(NOT_RUNNING_RETRY).await;
1750 continue;
1751 }
1752
1753 sleep(GW_ANNOUNCEMENT_TTL.mul_f32(0.85)).await;
1756 }
1757 });
1758 }
1759 }
1760
1761 async fn check_lnv1_federation_network(
1764 client: &ClientHandleArc,
1765 network: Network,
1766 ) -> AdminResult<()> {
1767 let federation_id = client.federation_id();
1768 let config = client.config().await;
1769 let cfg = config
1770 .modules
1771 .values()
1772 .find(|m| LightningCommonInit::KIND == m.kind)
1773 .ok_or(AdminGatewayError::ClientCreationError(anyhow!(format!(
1774 "Federation {federation_id} does not have an LNv1 module"
1775 ))))?;
1776 let ln_cfg: &LightningClientConfig = cfg.cast()?;
1777
1778 if ln_cfg.network.0 != network {
1779 error!(
1780 "Federation {federation_id} runs on {} but this gateway supports {network}",
1781 ln_cfg.network,
1782 );
1783 return Err(AdminGatewayError::ClientCreationError(anyhow!(format!(
1784 "Unsupported network {}",
1785 ln_cfg.network
1786 ))));
1787 }
1788
1789 Ok(())
1790 }
1791
1792 async fn check_lnv2_federation_network(
1795 client: &ClientHandleArc,
1796 network: Network,
1797 ) -> AdminResult<()> {
1798 let federation_id = client.federation_id();
1799 let config = client.config().await;
1800 let cfg = config
1801 .modules
1802 .values()
1803 .find(|m| fedimint_lnv2_common::LightningCommonInit::KIND == m.kind)
1804 .ok_or(AdminGatewayError::ClientCreationError(anyhow!(format!(
1805 "Federation {federation_id} does not have an LNv2 module"
1806 ))))?;
1807 let ln_cfg: &fedimint_lnv2_common::config::LightningClientConfig = cfg.cast()?;
1808
1809 if ln_cfg.network != network {
1810 error!(
1811 "Federation {federation_id} runs on {} but this gateway supports {network}",
1812 ln_cfg.network,
1813 );
1814 return Err(AdminGatewayError::ClientCreationError(anyhow!(format!(
1815 "Unsupported network {}",
1816 ln_cfg.network
1817 ))));
1818 }
1819
1820 Ok(())
1821 }
1822
1823 pub async fn get_lightning_context(
1827 &self,
1828 ) -> std::result::Result<LightningContext, LightningRpcError> {
1829 match self.get_state().await {
1830 GatewayState::Running { lightning_context }
1831 | GatewayState::ShuttingDown { lightning_context } => Ok(lightning_context),
1832 _ => Err(LightningRpcError::FailedToConnect),
1833 }
1834 }
1835
1836 pub async fn unannounce_from_all_federations(&self) {
1839 if self.is_running_lnv1() {
1840 let mut dbtx = self.gateway_db.begin_transaction_nc().await;
1841 let gateway_keypair = dbtx.load_gateway_keypair_assert_exists().await;
1842
1843 self.federation_manager
1844 .read()
1845 .await
1846 .unannounce_from_all_federations(gateway_keypair)
1847 .await;
1848 }
1849 }
1850
1851 fn create_lightning_client(
1852 &self,
1853 runtime: Arc<tokio::runtime::Runtime>,
1854 ) -> Box<dyn ILnRpcClient> {
1855 match self.lightning_mode.clone() {
1856 LightningMode::Lnd {
1857 lnd_rpc_addr,
1858 lnd_tls_cert,
1859 lnd_macaroon,
1860 } => Box::new(GatewayLndClient::new(
1861 lnd_rpc_addr,
1862 lnd_tls_cert,
1863 lnd_macaroon,
1864 None,
1865 Arc::new(self.clone()),
1866 )),
1867 LightningMode::Ldk {
1868 esplora_server_url,
1869 bitcoind_rpc_url,
1870 network,
1871 lightning_port,
1872 } => {
1873 let chain_source_config = {
1874 match (esplora_server_url, bitcoind_rpc_url) {
1875 (Some(esplora_server_url), None) => GatewayLdkChainSourceConfig::Esplora {
1876 server_url: SafeUrl::parse(&esplora_server_url.clone()).unwrap(),
1877 },
1878 (None, Some(bitcoind_rpc_url)) => GatewayLdkChainSourceConfig::Bitcoind {
1879 server_url: SafeUrl::parse(&bitcoind_rpc_url.clone()).unwrap(),
1880 },
1881 (None, None) => {
1882 panic!("Either esplora or bitcoind chain info source must be provided")
1883 }
1884 (Some(_), Some(_)) => {
1885 panic!("Either esplora or bitcoind chain info source must be provided, but not both")
1886 }
1887 }
1888 };
1889
1890 Box::new(
1891 ldk::GatewayLdkClient::new(
1892 &self.client_builder.data_dir().join(LDK_NODE_DB_FOLDER),
1893 chain_source_config,
1894 network,
1895 lightning_port,
1896 self.mnemonic.clone(),
1897 runtime,
1898 )
1899 .expect("Failed to create LDK client"),
1900 )
1901 }
1902 }
1903 }
1904}
1905
1906impl Gateway {
1908 async fn public_key_v2(&self, federation_id: &FederationId) -> Option<PublicKey> {
1912 self.federation_manager
1913 .read()
1914 .await
1915 .client(federation_id)
1916 .map(|client| {
1917 client
1918 .value()
1919 .get_first_module::<GatewayClientModuleV2>()
1920 .expect("Must have client module")
1921 .keypair
1922 .public_key()
1923 })
1924 }
1925
1926 pub async fn routing_info_v2(
1929 &self,
1930 federation_id: &FederationId,
1931 ) -> Result<Option<RoutingInfo>> {
1932 let context = self.get_lightning_context().await?;
1933
1934 let mut dbtx = self.gateway_db.begin_transaction_nc().await;
1935 let fed_config = dbtx.load_federation_config(*federation_id).await.ok_or(
1936 PublicGatewayError::FederationNotConnected(FederationNotConnected {
1937 federation_id_prefix: federation_id.to_prefix(),
1938 }),
1939 )?;
1940
1941 let lightning_fee = fed_config.lightning_fee;
1942 let transaction_fee = fed_config.transaction_fee;
1943
1944 Ok(self
1945 .public_key_v2(federation_id)
1946 .await
1947 .map(|module_public_key| RoutingInfo {
1948 lightning_public_key: context.lightning_public_key,
1949 module_public_key,
1950 send_fee_default: lightning_fee + transaction_fee,
1951 send_fee_minimum: transaction_fee,
1955 expiration_delta_default: 1440,
1956 expiration_delta_minimum: EXPIRATION_DELTA_MINIMUM_V2,
1957 receive_fee: transaction_fee,
1960 }))
1961 }
1962
1963 async fn send_payment_v2(
1966 &self,
1967 payload: SendPaymentPayload,
1968 ) -> Result<std::result::Result<[u8; 32], Signature>> {
1969 self.select_client(payload.federation_id)
1970 .await?
1971 .value()
1972 .get_first_module::<GatewayClientModuleV2>()
1973 .expect("Must have client module")
1974 .send_payment(payload)
1975 .await
1976 .map_err(LNv2Error::OutgoingPayment)
1977 .map_err(PublicGatewayError::LNv2)
1978 }
1979
1980 async fn create_bolt11_invoice_v2(
1985 &self,
1986 payload: CreateBolt11InvoicePayload,
1987 ) -> Result<Bolt11Invoice> {
1988 if !payload.contract.verify() {
1989 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
1990 "The contract is invalid".to_string(),
1991 )));
1992 }
1993
1994 let payment_info = self.routing_info_v2(&payload.federation_id).await?.ok_or(
1995 LNv2Error::IncomingPayment(format!(
1996 "Federation {} does not exist",
1997 payload.federation_id
1998 )),
1999 )?;
2000
2001 if payload.contract.commitment.refund_pk != payment_info.module_public_key {
2002 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
2003 "The incoming contract is keyed to another gateway".to_string(),
2004 )));
2005 }
2006
2007 let contract_amount = payment_info.receive_fee.subtract_from(payload.amount.msats);
2008
2009 if contract_amount == Amount::ZERO {
2010 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
2011 "Zero amount incoming contracts are not supported".to_string(),
2012 )));
2013 }
2014
2015 if contract_amount != payload.contract.commitment.amount {
2016 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
2017 "The contract amount does not pay the correct amount of fees".to_string(),
2018 )));
2019 }
2020
2021 if payload.contract.commitment.expiration <= duration_since_epoch().as_secs() {
2022 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
2023 "The contract has already expired".to_string(),
2024 )));
2025 }
2026
2027 let payment_hash = match payload.contract.commitment.payment_image {
2028 PaymentImage::Hash(payment_hash) => payment_hash,
2029 PaymentImage::Point(..) => {
2030 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
2031 "PaymentImage is not a payment hash".to_string(),
2032 )));
2033 }
2034 };
2035
2036 let invoice = self
2037 .create_invoice_via_lnrpc_v2(
2038 payment_hash,
2039 payload.amount,
2040 payload.description.clone(),
2041 payload.expiry_secs,
2042 )
2043 .await?;
2044
2045 let mut dbtx = self.gateway_db.begin_transaction().await;
2046
2047 if dbtx
2048 .save_registered_incoming_contract(
2049 payload.federation_id,
2050 payload.amount,
2051 payload.contract,
2052 )
2053 .await
2054 .is_some()
2055 {
2056 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
2057 "PaymentHash is already registered".to_string(),
2058 )));
2059 }
2060
2061 dbtx.commit_tx_result().await.map_err(|_| {
2062 PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
2063 "Payment hash is already registered".to_string(),
2064 ))
2065 })?;
2066
2067 Ok(invoice)
2068 }
2069
2070 pub async fn create_invoice_via_lnrpc_v2(
2073 &self,
2074 payment_hash: sha256::Hash,
2075 amount: Amount,
2076 description: Bolt11InvoiceDescription,
2077 expiry_time: u32,
2078 ) -> std::result::Result<Bolt11Invoice, LightningRpcError> {
2079 let lnrpc = self.get_lightning_context().await?.lnrpc;
2080
2081 let response = match description {
2082 Bolt11InvoiceDescription::Direct(description) => {
2083 lnrpc
2084 .create_invoice(CreateInvoiceRequest {
2085 payment_hash: Some(payment_hash),
2086 amount_msat: amount.msats,
2087 expiry_secs: expiry_time,
2088 description: Some(InvoiceDescription::Direct(description)),
2089 })
2090 .await?
2091 }
2092 Bolt11InvoiceDescription::Hash(hash) => {
2093 lnrpc
2094 .create_invoice(CreateInvoiceRequest {
2095 payment_hash: Some(payment_hash),
2096 amount_msat: amount.msats,
2097 expiry_secs: expiry_time,
2098 description: Some(InvoiceDescription::Hash(hash)),
2099 })
2100 .await?
2101 }
2102 };
2103
2104 Bolt11Invoice::from_str(&response.invoice).map_err(|e| {
2105 LightningRpcError::FailedToGetInvoice {
2106 failure_reason: e.to_string(),
2107 }
2108 })
2109 }
2110
2111 pub async fn get_registered_incoming_contract_and_client_v2(
2115 &self,
2116 payment_image: PaymentImage,
2117 amount_msats: u64,
2118 ) -> Result<(IncomingContract, ClientHandleArc)> {
2119 let registered_incoming_contract = self
2120 .gateway_db
2121 .begin_transaction_nc()
2122 .await
2123 .load_registered_incoming_contract(payment_image)
2124 .await
2125 .ok_or(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
2126 "No corresponding decryption contract available".to_string(),
2127 )))?;
2128
2129 if registered_incoming_contract.incoming_amount_msats != amount_msats {
2130 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
2131 "The available decryption contract's amount is not equal to the requested amount"
2132 .to_string(),
2133 )));
2134 }
2135
2136 let client = self
2137 .select_client(registered_incoming_contract.federation_id)
2138 .await?
2139 .into_value();
2140
2141 Ok((registered_incoming_contract.contract, client))
2142 }
2143
2144 fn is_running_lnv2(&self) -> bool {
2146 self.lightning_module_mode == LightningModuleMode::LNv2
2147 || self.lightning_module_mode == LightningModuleMode::All
2148 }
2149
2150 fn is_running_lnv1(&self) -> bool {
2152 self.lightning_module_mode == LightningModuleMode::LNv1
2153 || self.lightning_module_mode == LightningModuleMode::All
2154 }
2155}
2156
2157#[async_trait]
2158impl LightningV2Manager for Gateway {
2159 async fn contains_incoming_contract(&self, payment_image: PaymentImage) -> bool {
2160 self.gateway_db
2161 .begin_transaction_nc()
2162 .await
2163 .load_registered_incoming_contract(payment_image)
2164 .await
2165 .is_some()
2166 }
2167}