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