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;
23pub mod rpc_server;
24mod types;
25
26use std::collections::{BTreeMap, BTreeSet};
27use std::env;
28use std::fmt::Display;
29use std::net::SocketAddr;
30use std::str::FromStr;
31use std::sync::Arc;
32use std::time::{Duration, UNIX_EPOCH};
33
34use anyhow::{Context, anyhow, ensure};
35use async_trait::async_trait;
36use bitcoin::hashes::sha256;
37use bitcoin::{Address, Network, Txid};
38use clap::Parser;
39use client::GatewayClientBuilder;
40pub use config::GatewayParameters;
41use config::{DatabaseBackend, GatewayOpts};
42use envs::{FM_GATEWAY_OVERRIDE_LN_MODULE_CHECK_ENV, FM_GATEWAY_SKIP_WAIT_FOR_SYNC_ENV};
43use error::FederationNotConnected;
44use events::ALL_GATEWAY_EVENTS;
45use federation_manager::FederationManager;
46use fedimint_api_client::api::net::Connector;
47use fedimint_bip39::{Bip39RootSecretStrategy, Language, Mnemonic};
48use fedimint_client::module_init::ClientModuleInitRegistry;
49use fedimint_client::secret::RootSecretStrategy;
50use fedimint_client::{Client, ClientHandleArc};
51use fedimint_core::config::FederationId;
52use fedimint_core::core::{
53 LEGACY_HARDCODED_INSTANCE_ID_MINT, LEGACY_HARDCODED_INSTANCE_ID_WALLET, ModuleInstanceId,
54 ModuleKind,
55};
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::secp256k1::PublicKey;
61use fedimint_core::secp256k1::schnorr::Signature;
62use fedimint_core::task::{TaskGroup, TaskHandle, TaskShutdownToken, sleep};
63use fedimint_core::time::duration_since_epoch;
64use fedimint_core::util::{FmtCompact, FmtCompactAnyhow, SafeUrl, Spanned};
65use fedimint_core::{
66 Amount, BitcoinAmountOrAll, crit, default_esplora_server, fedimint_build_code_version_env,
67 get_network_for_address,
68};
69use fedimint_eventlog::{DBTransactionEventLogExt, EventLogId, StructuredPaymentEvents};
70use fedimint_gateway_common::{
71 BackupPayload, CloseChannelsWithPeerRequest, CloseChannelsWithPeerResponse, ConnectFedPayload,
72 CreateInvoiceForOperatorPayload, CreateOfferPayload, CreateOfferResponse,
73 DepositAddressPayload, DepositAddressRecheckPayload, FederationBalanceInfo, FederationConfig,
74 FederationInfo, GatewayBalances, GatewayFedConfig, GatewayInfo, GetInvoiceRequest,
75 GetInvoiceResponse, LeaveFedPayload, LightningMode, ListTransactionsPayload,
76 ListTransactionsResponse, MnemonicResponse, OpenChannelRequest, PayInvoiceForOperatorPayload,
77 PayOfferPayload, PayOfferResponse, PaymentLogPayload, PaymentLogResponse, PaymentStats,
78 PaymentSummaryPayload, PaymentSummaryResponse, ReceiveEcashPayload, ReceiveEcashResponse,
79 SendOnchainRequest, SetFeesPayload, SpendEcashPayload, SpendEcashResponse, V1_API_ENDPOINT,
80 WithdrawPayload, WithdrawResponse,
81};
82use fedimint_gateway_server_db::{GatewayDbtxNcExt as _, get_gatewayd_database_migrations};
83use fedimint_gw_client::events::compute_lnv1_stats;
84use fedimint_gw_client::pay::{OutgoingPaymentError, OutgoingPaymentErrorType};
85use fedimint_gw_client::{GatewayClientModule, GatewayExtPayStates, IGatewayClientV1};
86use fedimint_gwv2_client::events::compute_lnv2_stats;
87use fedimint_gwv2_client::{EXPIRATION_DELTA_MINIMUM_V2, GatewayClientModuleV2, IGatewayClientV2};
88use fedimint_lightning::ldk::{self, GatewayLdkChainSourceConfig};
89use fedimint_lightning::lnd::GatewayLndClient;
90use fedimint_lightning::{
91 CreateInvoiceRequest, ILnRpcClient, InterceptPaymentRequest, InterceptPaymentResponse,
92 InvoiceDescription, LightningContext, LightningRpcError, PayInvoiceResponse, PaymentAction,
93 RouteHtlcStream,
94};
95use fedimint_ln_client::pay::PaymentData;
96use fedimint_ln_common::LightningCommonInit;
97use fedimint_ln_common::config::LightningClientConfig;
98use fedimint_ln_common::contracts::outgoing::OutgoingContractAccount;
99use fedimint_ln_common::contracts::{IdentifiableContract, Preimage};
100use fedimint_lnv2_common::Bolt11InvoiceDescription;
101use fedimint_lnv2_common::contracts::{IncomingContract, PaymentImage};
102use fedimint_lnv2_common::gateway_api::{
103 CreateBolt11InvoicePayload, PaymentFee, RoutingInfo, SendPaymentPayload,
104};
105use fedimint_logging::LOG_GATEWAY;
106use fedimint_mint_client::{
107 MintClientInit, MintClientModule, MintCommonInit, SelectNotesWithAtleastAmount,
108 SelectNotesWithExactAmount,
109};
110use fedimint_wallet_client::envs::FM_PORT_ESPLORA_ENV;
111use fedimint_wallet_client::{
112 WalletClientInit, WalletClientModule, WalletCommonInit, WithdrawState,
113};
114use futures::stream::StreamExt;
115use lightning_invoice::{Bolt11Invoice, RoutingFees};
116use rand::thread_rng;
117use tokio::sync::RwLock;
118use tracing::{debug, info, info_span, warn};
119
120use crate::config::LightningModuleMode;
121use crate::envs::FM_GATEWAY_MNEMONIC_ENV;
122use crate::error::{AdminGatewayError, LNv1Error, LNv2Error, PublicGatewayError};
123use crate::events::get_events_for_duration;
124use crate::rpc_server::run_webserver;
125use crate::types::PrettyInterceptPaymentRequest;
126
127const GW_ANNOUNCEMENT_TTL: Duration = Duration::from_secs(600);
129
130const DEFAULT_NUM_ROUTE_HINTS: u32 = 1;
133
134pub const DEFAULT_NETWORK: Network = Network::Regtest;
136
137pub type Result<T> = std::result::Result<T, PublicGatewayError>;
138pub type AdminResult<T> = std::result::Result<T, AdminGatewayError>;
139
140const DB_FILE: &str = "gatewayd.db";
143
144const LDK_NODE_DB_FOLDER: &str = "ldk_node";
147
148const DEFAULT_MODULE_KINDS: [(ModuleInstanceId, &ModuleKind); 2] = [
150 (LEGACY_HARDCODED_INSTANCE_ID_MINT, &MintCommonInit::KIND),
151 (LEGACY_HARDCODED_INSTANCE_ID_WALLET, &WalletCommonInit::KIND),
152];
153
154#[cfg_attr(doc, aquamarine::aquamarine)]
155#[derive(Clone, Debug)]
167pub enum GatewayState {
168 Disconnected,
169 Syncing,
170 Connected,
171 Running { lightning_context: LightningContext },
172 ShuttingDown { lightning_context: LightningContext },
173}
174
175impl Display for GatewayState {
176 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
177 match self {
178 GatewayState::Disconnected => write!(f, "Disconnected"),
179 GatewayState::Syncing => write!(f, "Syncing"),
180 GatewayState::Connected => write!(f, "Connected"),
181 GatewayState::Running { .. } => write!(f, "Running"),
182 GatewayState::ShuttingDown { .. } => write!(f, "ShuttingDown"),
183 }
184 }
185}
186
187enum ReceivePaymentStreamAction {
189 RetryAfterDelay,
190 NoRetry,
191}
192
193#[derive(Clone)]
194pub struct Gateway {
195 federation_manager: Arc<RwLock<FederationManager>>,
197
198 mnemonic: Mnemonic,
200
201 lightning_mode: LightningMode,
203
204 state: Arc<RwLock<GatewayState>>,
206
207 client_builder: GatewayClientBuilder,
210
211 gateway_db: Database,
213
214 gateway_id: PublicKey,
217
218 versioned_api: SafeUrl,
220
221 listen: SocketAddr,
223
224 lightning_module_mode: LightningModuleMode,
226
227 task_group: TaskGroup,
229
230 bcrypt_password_hash: Arc<bcrypt::HashParts>,
233
234 num_route_hints: u32,
236
237 network: Network,
239
240 default_routing_fees: PaymentFee,
242
243 default_transaction_fees: PaymentFee,
245}
246
247impl std::fmt::Debug for Gateway {
248 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
249 f.debug_struct("Gateway")
250 .field("federation_manager", &self.federation_manager)
251 .field("state", &self.state)
252 .field("client_builder", &self.client_builder)
253 .field("gateway_db", &self.gateway_db)
254 .field("gateway_id", &self.gateway_id)
255 .field("versioned_api", &self.versioned_api)
256 .field("listen", &self.listen)
257 .finish_non_exhaustive()
258 }
259}
260
261impl Gateway {
262 #[allow(clippy::too_many_arguments)]
265 pub async fn new_with_custom_registry(
266 lightning_mode: LightningMode,
267 client_builder: GatewayClientBuilder,
268 listen: SocketAddr,
269 api_addr: SafeUrl,
270 bcrypt_password_hash: bcrypt::HashParts,
271 network: Network,
272 num_route_hints: u32,
273 gateway_db: Database,
274 gateway_state: GatewayState,
275 lightning_module_mode: LightningModuleMode,
276 ) -> anyhow::Result<Gateway> {
277 let versioned_api = api_addr
278 .join(V1_API_ENDPOINT)
279 .expect("Failed to version gateway API address");
280 Gateway::new(
281 lightning_mode,
282 GatewayParameters {
283 listen,
284 versioned_api,
285 bcrypt_password_hash,
286 network,
287 num_route_hints,
288 lightning_module_mode,
289 default_routing_fees: PaymentFee::TRANSACTION_FEE_DEFAULT,
290 default_transaction_fees: PaymentFee::TRANSACTION_FEE_DEFAULT,
291 },
292 gateway_db,
293 client_builder,
294 gateway_state,
295 )
296 .await
297 }
298
299 pub async fn new_with_default_modules() -> anyhow::Result<Gateway> {
302 let opts = GatewayOpts::parse();
303
304 let mut registry = ClientModuleInitRegistry::new();
307 registry.attach(MintClientInit);
308 registry.attach(WalletClientInit::default());
309
310 let decoders = registry.available_decoders(DEFAULT_MODULE_KINDS.iter().copied())?;
311
312 let db_path = opts.data_dir.join(DB_FILE);
313 let gateway_db = match opts.db_backend {
314 DatabaseBackend::RocksDb => {
315 debug!(target: LOG_GATEWAY, "Using RocksDB database backend");
316 Database::new(fedimint_rocksdb::RocksDb::open(db_path).await?, decoders)
317 }
318 DatabaseBackend::CursedRedb => {
319 debug!(target: LOG_GATEWAY, "Using CursedRedb database backend");
320 Database::new(
321 fedimint_cursed_redb::MemAndRedb::new(db_path).await?,
322 decoders,
323 )
324 }
325 };
326
327 let client_builder = GatewayClientBuilder::new(
328 opts.data_dir.clone(),
329 registry,
330 fedimint_mint_client::KIND,
331 opts.db_backend,
332 );
333
334 info!(
335 target: LOG_GATEWAY,
336 version = %fedimint_build_code_version_env!(),
337 "Starting gatewayd",
338 );
339
340 let mut gateway_parameters = opts.to_gateway_parameters()?;
341
342 if gateway_parameters.lightning_module_mode != LightningModuleMode::LNv2
343 && matches!(opts.mode, LightningMode::Ldk { .. })
344 {
345 warn!(target: LOG_GATEWAY, "Overriding LDK Gateway to only run LNv2...");
346 gateway_parameters.lightning_module_mode = LightningModuleMode::LNv2;
347 }
348
349 Gateway::new(
350 opts.mode,
351 gateway_parameters,
352 gateway_db,
353 client_builder,
354 GatewayState::Disconnected,
355 )
356 .await
357 }
358
359 async fn new(
362 lightning_mode: LightningMode,
363 gateway_parameters: GatewayParameters,
364 gateway_db: Database,
365 client_builder: GatewayClientBuilder,
366 gateway_state: GatewayState,
367 ) -> anyhow::Result<Gateway> {
368 apply_migrations(
371 &gateway_db,
372 (),
373 "gatewayd".to_string(),
374 get_gatewayd_database_migrations(),
375 None,
376 None,
377 )
378 .await?;
379
380 let num_route_hints = gateway_parameters.num_route_hints;
381 let network = gateway_parameters.network;
382
383 let task_group = TaskGroup::new();
384 task_group.install_kill_handler();
385
386 Ok(Self {
387 federation_manager: Arc::new(RwLock::new(FederationManager::new())),
388 mnemonic: Self::load_or_generate_mnemonic(&gateway_db).await?,
389 lightning_mode,
390 state: Arc::new(RwLock::new(gateway_state)),
391 client_builder,
392 gateway_id: Self::load_or_create_gateway_id(&gateway_db).await,
393 gateway_db,
394 versioned_api: gateway_parameters.versioned_api,
395 listen: gateway_parameters.listen,
396 lightning_module_mode: gateway_parameters.lightning_module_mode,
397 task_group,
398 bcrypt_password_hash: Arc::new(gateway_parameters.bcrypt_password_hash),
399 num_route_hints,
400 network,
401 default_routing_fees: gateway_parameters.default_routing_fees,
402 default_transaction_fees: gateway_parameters.default_transaction_fees,
403 })
404 }
405
406 async fn load_or_create_gateway_id(gateway_db: &Database) -> PublicKey {
408 let mut dbtx = gateway_db.begin_transaction().await;
409 let keypair = dbtx.load_or_create_gateway_keypair().await;
410 dbtx.commit_tx().await;
411 keypair.public_key()
412 }
413
414 pub fn gateway_id(&self) -> PublicKey {
415 self.gateway_id
416 }
417
418 pub fn versioned_api(&self) -> &SafeUrl {
419 &self.versioned_api
420 }
421
422 async fn get_state(&self) -> GatewayState {
423 self.state.read().await.clone()
424 }
425
426 pub async fn dump_database(
429 dbtx: &mut DatabaseTransaction<'_>,
430 prefix_names: Vec<String>,
431 ) -> BTreeMap<String, Box<dyn erased_serde::Serialize + Send>> {
432 dbtx.dump_database(prefix_names).await
433 }
434
435 pub async fn run(
440 self,
441 runtime: Arc<tokio::runtime::Runtime>,
442 ) -> anyhow::Result<TaskShutdownToken> {
443 self.verify_lightning_module_mode()?;
444 self.register_clients_timer();
445 self.load_clients().await?;
446 self.start_gateway(runtime);
447 let handle = self.task_group.make_handle();
449 run_webserver(Arc::new(self)).await?;
450 let shutdown_receiver = handle.make_shutdown_rx();
451 Ok(shutdown_receiver)
452 }
453
454 fn verify_lightning_module_mode(&self) -> anyhow::Result<()> {
457 if !is_env_var_set(FM_GATEWAY_OVERRIDE_LN_MODULE_CHECK_ENV)
458 && self.network == Network::Bitcoin
459 && self.lightning_module_mode == LightningModuleMode::All
460 {
461 crit!(
462 "It is not recommended to run the Gateway with `LightningModuleMode::All`, because LNv2 invoices cannot be paid with LNv1 clients. If you really know what you're doing and want to bypass this, please set FM_GATEWAY_OVERRIDE_LN_MODULE_CHECK"
463 );
464 return Err(anyhow!(
465 "Cannot run gateway with LightningModuleMode::All on mainnet"
466 ));
467 }
468
469 Ok(())
470 }
471
472 fn start_gateway(&self, runtime: Arc<tokio::runtime::Runtime>) {
475 const PAYMENT_STREAM_RETRY_SECONDS: u64 = 5;
476
477 let self_copy = self.clone();
478 let tg = self.task_group.clone();
479 self.task_group.spawn(
480 "Subscribe to intercepted lightning payments in stream",
481 |handle| async move {
482 loop {
484 if handle.is_shutting_down() {
485 info!(target: LOG_GATEWAY, "Gateway lightning payment stream handler loop is shutting down");
486 break;
487 }
488
489 let payment_stream_task_group = tg.make_subgroup();
490 let lnrpc_route = self_copy.create_lightning_client(runtime.clone());
491
492 debug!(target: LOG_GATEWAY, "Establishing lightning payment stream...");
493 let (stream, ln_client) = match lnrpc_route.route_htlcs(&payment_stream_task_group).await
494 {
495 Ok((stream, ln_client)) => (stream, ln_client),
496 Err(err) => {
497 warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Failed to open lightning payment stream");
498 continue
499 }
500 };
501
502 self_copy.set_gateway_state(GatewayState::Connected).await;
504 info!(target: LOG_GATEWAY, "Established lightning payment stream");
505
506 let route_payments_response =
507 self_copy.route_lightning_payments(&handle, stream, ln_client).await;
508
509 self_copy.set_gateway_state(GatewayState::Disconnected).await;
510 if let Err(err) = payment_stream_task_group.shutdown_join_all(None).await {
511 crit!(target: LOG_GATEWAY, err = %err.fmt_compact_anyhow(), "Lightning payment stream task group shutdown");
512 }
513
514 self_copy.unannounce_from_all_federations().await;
515
516 match route_payments_response {
517 ReceivePaymentStreamAction::RetryAfterDelay => {
518 warn!(target: LOG_GATEWAY, retry_interval = %PAYMENT_STREAM_RETRY_SECONDS, "Disconnected from lightning node");
519 sleep(Duration::from_secs(PAYMENT_STREAM_RETRY_SECONDS)).await;
520 }
521 ReceivePaymentStreamAction::NoRetry => break,
522 }
523 }
524 },
525 );
526 }
527
528 async fn route_lightning_payments<'a>(
532 &'a self,
533 handle: &TaskHandle,
534 mut stream: RouteHtlcStream<'a>,
535 ln_client: Arc<dyn ILnRpcClient>,
536 ) -> ReceivePaymentStreamAction {
537 let (lightning_public_key, lightning_alias, lightning_network, synced_to_chain) =
538 match ln_client.parsed_node_info().await {
539 Ok((
540 lightning_public_key,
541 lightning_alias,
542 lightning_network,
543 _block_height,
544 synced_to_chain,
545 )) => (
546 lightning_public_key,
547 lightning_alias,
548 lightning_network,
549 synced_to_chain,
550 ),
551 Err(err) => {
552 warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Failed to retrieve Lightning info");
553 return ReceivePaymentStreamAction::RetryAfterDelay;
554 }
555 };
556
557 assert!(
558 self.network == lightning_network,
559 "Lightning node network does not match Gateway's network. LN: {lightning_network} Gateway: {}",
560 self.network
561 );
562
563 if synced_to_chain || is_env_var_set(FM_GATEWAY_SKIP_WAIT_FOR_SYNC_ENV) {
564 info!(target: LOG_GATEWAY, "Gateway is already synced to chain");
565 } else {
566 self.set_gateway_state(GatewayState::Syncing).await;
567 info!(target: LOG_GATEWAY, "Waiting for chain sync");
568 if let Err(err) = ln_client.wait_for_chain_sync().await {
569 warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Failed to wait for chain sync");
570 return ReceivePaymentStreamAction::RetryAfterDelay;
571 }
572 }
573
574 let lightning_context = LightningContext {
575 lnrpc: ln_client,
576 lightning_public_key,
577 lightning_alias,
578 lightning_network,
579 };
580 self.set_gateway_state(GatewayState::Running { lightning_context })
581 .await;
582 info!(target: LOG_GATEWAY, "Gateway is running");
583
584 if self.is_running_lnv1() {
585 let mut dbtx = self.gateway_db.begin_transaction_nc().await;
588 let all_federations_configs =
589 dbtx.load_federation_configs().await.into_iter().collect();
590 self.register_federations(&all_federations_configs, &self.task_group)
591 .await;
592 }
593
594 if handle
597 .cancel_on_shutdown(async move {
598 loop {
599 let payment_request_or = tokio::select! {
600 payment_request_or = stream.next() => {
601 payment_request_or
602 }
603 () = self.is_shutting_down_safely() => {
604 break;
605 }
606 };
607
608 let Some(payment_request) = payment_request_or else {
609 warn!(
610 target: LOG_GATEWAY,
611 "Unexpected response from incoming lightning payment stream. Shutting down payment processor"
612 );
613 break;
614 };
615
616 let state_guard = self.state.read().await;
617 if let GatewayState::Running { ref lightning_context } = *state_guard {
618 self.handle_lightning_payment(payment_request, lightning_context).await;
619 } else {
620 warn!(
621 target: LOG_GATEWAY,
622 state = %state_guard,
623 "Gateway isn't in a running state, cannot handle incoming payments."
624 );
625 break;
626 }
627 }
628 })
629 .await
630 .is_ok()
631 {
632 warn!(target: LOG_GATEWAY, "Lightning payment stream connection broken. Gateway is disconnected");
633 ReceivePaymentStreamAction::RetryAfterDelay
634 } else {
635 info!(target: LOG_GATEWAY, "Received shutdown signal");
636 ReceivePaymentStreamAction::NoRetry
637 }
638 }
639
640 async fn is_shutting_down_safely(&self) {
643 loop {
644 if let GatewayState::ShuttingDown { .. } = self.get_state().await {
645 return;
646 }
647
648 fedimint_core::task::sleep(Duration::from_secs(1)).await;
649 }
650 }
651
652 async fn handle_lightning_payment(
657 &self,
658 payment_request: InterceptPaymentRequest,
659 lightning_context: &LightningContext,
660 ) {
661 info!(
662 target: LOG_GATEWAY,
663 lightning_payment = %PrettyInterceptPaymentRequest(&payment_request),
664 "Intercepting lightning payment",
665 );
666
667 if self
668 .try_handle_lightning_payment_lnv2(&payment_request, lightning_context)
669 .await
670 .is_ok()
671 {
672 return;
673 }
674
675 if self
676 .try_handle_lightning_payment_ln_legacy(&payment_request)
677 .await
678 .is_ok()
679 {
680 return;
681 }
682
683 Self::forward_lightning_payment(payment_request, lightning_context).await;
684 }
685
686 async fn try_handle_lightning_payment_lnv2(
689 &self,
690 htlc_request: &InterceptPaymentRequest,
691 lightning_context: &LightningContext,
692 ) -> Result<()> {
693 let (contract, client) = self
699 .get_registered_incoming_contract_and_client_v2(
700 PaymentImage::Hash(htlc_request.payment_hash),
701 htlc_request.amount_msat,
702 )
703 .await?;
704
705 if let Err(err) = client
706 .get_first_module::<GatewayClientModuleV2>()
707 .expect("Must have client module")
708 .relay_incoming_htlc(
709 htlc_request.payment_hash,
710 htlc_request.incoming_chan_id,
711 htlc_request.htlc_id,
712 contract,
713 htlc_request.amount_msat,
714 )
715 .await
716 {
717 warn!(target: LOG_GATEWAY, err = %err.fmt_compact_anyhow(), "Error relaying incoming lightning payment");
718
719 let outcome = InterceptPaymentResponse {
720 action: PaymentAction::Cancel,
721 payment_hash: htlc_request.payment_hash,
722 incoming_chan_id: htlc_request.incoming_chan_id,
723 htlc_id: htlc_request.htlc_id,
724 };
725
726 if let Err(err) = lightning_context.lnrpc.complete_htlc(outcome).await {
727 warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Error sending HTLC response to lightning node");
728 }
729 }
730
731 Ok(())
732 }
733
734 async fn try_handle_lightning_payment_ln_legacy(
737 &self,
738 htlc_request: &InterceptPaymentRequest,
739 ) -> Result<()> {
740 let Some(federation_index) = htlc_request.short_channel_id else {
742 return Err(PublicGatewayError::LNv1(LNv1Error::IncomingPayment(
743 "Incoming payment has not last hop short channel id".to_string(),
744 )));
745 };
746
747 let Some(client) = self
748 .federation_manager
749 .read()
750 .await
751 .get_client_for_index(federation_index)
752 else {
753 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())));
754 };
755
756 client
757 .borrow()
758 .with(|client| async {
759 let htlc = htlc_request.clone().try_into();
760 match htlc {
761 Ok(htlc) => {
762 match client
763 .get_first_module::<GatewayClientModule>()
764 .expect("Must have client module")
765 .gateway_handle_intercepted_htlc(htlc)
766 .await
767 {
768 Ok(_) => Ok(()),
769 Err(e) => Err(PublicGatewayError::LNv1(LNv1Error::IncomingPayment(
770 format!("Error intercepting lightning payment {e:?}"),
771 ))),
772 }
773 }
774 _ => Err(PublicGatewayError::LNv1(LNv1Error::IncomingPayment(
775 "Could not convert InterceptHtlcResult into an HTLC".to_string(),
776 ))),
777 }
778 })
779 .await
780 }
781
782 async fn forward_lightning_payment(
786 htlc_request: InterceptPaymentRequest,
787 lightning_context: &LightningContext,
788 ) {
789 let outcome = InterceptPaymentResponse {
790 action: PaymentAction::Forward,
791 payment_hash: htlc_request.payment_hash,
792 incoming_chan_id: htlc_request.incoming_chan_id,
793 htlc_id: htlc_request.htlc_id,
794 };
795
796 if let Err(err) = lightning_context.lnrpc.complete_htlc(outcome).await {
797 warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Error sending lightning payment response to lightning node");
798 }
799 }
800
801 async fn set_gateway_state(&self, state: GatewayState) {
803 let mut lock = self.state.write().await;
804 *lock = state;
805 }
806
807 pub async fn handle_get_info(&self) -> AdminResult<GatewayInfo> {
810 let GatewayState::Running { lightning_context } = self.get_state().await else {
811 return Ok(GatewayInfo {
812 federations: vec![],
813 federation_fake_scids: None,
814 version_hash: fedimint_build_code_version_env!().to_string(),
815 lightning_pub_key: None,
816 lightning_alias: None,
817 gateway_id: self.gateway_id,
818 gateway_state: self.state.read().await.to_string(),
819 network: self.network,
820 block_height: None,
821 synced_to_chain: false,
822 api: self.versioned_api.clone(),
823 lightning_mode: self.lightning_mode.clone(),
824 });
825 };
826
827 let dbtx = self.gateway_db.begin_transaction_nc().await;
828 let federations = self
829 .federation_manager
830 .read()
831 .await
832 .federation_info_all_federations(dbtx)
833 .await;
834
835 let channels: BTreeMap<u64, FederationId> = federations
836 .iter()
837 .map(|federation_info| {
838 (
839 federation_info.config.federation_index,
840 federation_info.federation_id,
841 )
842 })
843 .collect();
844
845 let node_info = lightning_context.lnrpc.parsed_node_info().await?;
846
847 Ok(GatewayInfo {
848 federations,
849 federation_fake_scids: Some(channels),
850 version_hash: fedimint_build_code_version_env!().to_string(),
851 lightning_pub_key: Some(lightning_context.lightning_public_key.to_string()),
852 lightning_alias: Some(lightning_context.lightning_alias.clone()),
853 gateway_id: self.gateway_id,
854 gateway_state: self.state.read().await.to_string(),
855 network: self.network,
856 block_height: Some(node_info.3),
857 synced_to_chain: node_info.4,
858 api: self.versioned_api.clone(),
859 lightning_mode: self.lightning_mode.clone(),
860 })
861 }
862
863 pub async fn handle_get_federation_config(
866 &self,
867 federation_id_or: Option<FederationId>,
868 ) -> AdminResult<GatewayFedConfig> {
869 if !matches!(self.get_state().await, GatewayState::Running { .. }) {
870 return Ok(GatewayFedConfig {
871 federations: BTreeMap::new(),
872 });
873 }
874
875 let federations = if let Some(federation_id) = federation_id_or {
876 let mut federations = BTreeMap::new();
877 federations.insert(
878 federation_id,
879 self.federation_manager
880 .read()
881 .await
882 .get_federation_config(federation_id)
883 .await?,
884 );
885 federations
886 } else {
887 self.federation_manager
888 .read()
889 .await
890 .get_all_federation_configs()
891 .await
892 };
893
894 Ok(GatewayFedConfig { federations })
895 }
896
897 pub async fn handle_address_msg(&self, payload: DepositAddressPayload) -> AdminResult<Address> {
900 let (_, address, _) = self
901 .select_client(payload.federation_id)
902 .await?
903 .value()
904 .get_first_module::<WalletClientModule>()
905 .expect("Must have client module")
906 .allocate_deposit_address_expert_only(())
907 .await?;
908 Ok(address)
909 }
910
911 pub async fn handle_withdraw_msg(
914 &self,
915 payload: WithdrawPayload,
916 ) -> AdminResult<WithdrawResponse> {
917 let WithdrawPayload {
918 amount,
919 address,
920 federation_id,
921 } = payload;
922
923 let address_network = get_network_for_address(&address);
924 let gateway_network = self.network;
925 let Ok(address) = address.require_network(gateway_network) else {
926 return Err(AdminGatewayError::WithdrawError {
927 failure_reason: format!(
928 "Gateway is running on network {gateway_network}, but provided withdraw address is for network {address_network}"
929 ),
930 });
931 };
932
933 let client = self.select_client(federation_id).await?;
934 let wallet_module = client.value().get_first_module::<WalletClientModule>()?;
935
936 let (amount, fees) = match amount {
938 BitcoinAmountOrAll::All => {
941 let balance =
942 bitcoin::Amount::from_sat(client.value().get_balance().await.msats / 1000);
943 let fees = wallet_module.get_withdraw_fees(&address, balance).await?;
944 let withdraw_amount = balance.checked_sub(fees.amount());
945 if withdraw_amount.is_none() {
946 return Err(AdminGatewayError::WithdrawError {
947 failure_reason: format!(
948 "Insufficient funds. Balance: {balance} Fees: {fees:?}"
949 ),
950 });
951 }
952 (withdraw_amount.unwrap(), fees)
953 }
954 BitcoinAmountOrAll::Amount(amount) => (
955 amount,
956 wallet_module.get_withdraw_fees(&address, amount).await?,
957 ),
958 };
959
960 let operation_id = wallet_module.withdraw(&address, amount, fees, ()).await?;
961 let mut updates = wallet_module
962 .subscribe_withdraw_updates(operation_id)
963 .await?
964 .into_stream();
965
966 while let Some(update) = updates.next().await {
967 match update {
968 WithdrawState::Succeeded(txid) => {
969 info!(target: LOG_GATEWAY, amount = %amount, address = %address, "Sent funds");
970 return Ok(WithdrawResponse { txid, fees });
971 }
972 WithdrawState::Failed(e) => {
973 return Err(AdminGatewayError::WithdrawError { failure_reason: e });
974 }
975 WithdrawState::Created => {}
976 }
977 }
978
979 Err(AdminGatewayError::WithdrawError {
980 failure_reason: "Ran out of state updates while withdrawing".to_string(),
981 })
982 }
983
984 async fn handle_create_invoice_for_operator_msg(
987 &self,
988 payload: CreateInvoiceForOperatorPayload,
989 ) -> AdminResult<Bolt11Invoice> {
990 let GatewayState::Running { lightning_context } = self.get_state().await else {
991 return Err(AdminGatewayError::Lightning(
992 LightningRpcError::FailedToConnect,
993 ));
994 };
995
996 Bolt11Invoice::from_str(
997 &lightning_context
998 .lnrpc
999 .create_invoice(CreateInvoiceRequest {
1000 payment_hash: None, amount_msat: payload.amount_msats,
1003 expiry_secs: payload.expiry_secs.unwrap_or(3600),
1004 description: payload.description.map(InvoiceDescription::Direct),
1005 })
1006 .await?
1007 .invoice,
1008 )
1009 .map_err(|e| {
1010 AdminGatewayError::Lightning(LightningRpcError::InvalidMetadata {
1011 failure_reason: e.to_string(),
1012 })
1013 })
1014 }
1015
1016 async fn handle_pay_invoice_for_operator_msg(
1019 &self,
1020 payload: PayInvoiceForOperatorPayload,
1021 ) -> AdminResult<Preimage> {
1022 const BASE_FEE: u64 = 50;
1024 const FEE_DENOMINATOR: u64 = 100;
1025 const MAX_DELAY: u64 = 1008;
1026
1027 let GatewayState::Running { lightning_context } = self.get_state().await else {
1028 return Err(AdminGatewayError::Lightning(
1029 LightningRpcError::FailedToConnect,
1030 ));
1031 };
1032
1033 let max_fee = BASE_FEE
1034 + payload
1035 .invoice
1036 .amount_milli_satoshis()
1037 .context("Invoice is missing amount")?
1038 .saturating_div(FEE_DENOMINATOR);
1039
1040 let res = lightning_context
1041 .lnrpc
1042 .pay(payload.invoice, MAX_DELAY, Amount::from_msats(max_fee))
1043 .await?;
1044 Ok(res.preimage)
1045 }
1046
1047 async fn handle_pay_invoice_msg(
1050 &self,
1051 payload: fedimint_ln_client::pay::PayInvoicePayload,
1052 ) -> Result<Preimage> {
1053 let GatewayState::Running { .. } = self.get_state().await else {
1054 return Err(PublicGatewayError::Lightning(
1055 LightningRpcError::FailedToConnect,
1056 ));
1057 };
1058
1059 debug!(target: LOG_GATEWAY, "Handling pay invoice message");
1060 let client = self.select_client(payload.federation_id).await?;
1061 let contract_id = payload.contract_id;
1062 let gateway_module = &client
1063 .value()
1064 .get_first_module::<GatewayClientModule>()
1065 .map_err(LNv1Error::OutgoingPayment)
1066 .map_err(PublicGatewayError::LNv1)?;
1067 let operation_id = gateway_module
1068 .gateway_pay_bolt11_invoice(payload)
1069 .await
1070 .map_err(LNv1Error::OutgoingPayment)
1071 .map_err(PublicGatewayError::LNv1)?;
1072 let mut updates = gateway_module
1073 .gateway_subscribe_ln_pay(operation_id)
1074 .await
1075 .map_err(LNv1Error::OutgoingPayment)
1076 .map_err(PublicGatewayError::LNv1)?
1077 .into_stream();
1078 while let Some(update) = updates.next().await {
1079 match update {
1080 GatewayExtPayStates::Success { preimage, .. } => {
1081 debug!(target: LOG_GATEWAY, contract_id = %contract_id, "Successfully paid invoice");
1082 return Ok(preimage);
1083 }
1084 GatewayExtPayStates::Fail {
1085 error,
1086 error_message,
1087 } => {
1088 return Err(PublicGatewayError::LNv1(LNv1Error::OutgoingContract {
1089 error: Box::new(error),
1090 message: format!(
1091 "{error_message} while paying invoice with contract id {contract_id}"
1092 ),
1093 }));
1094 }
1095 GatewayExtPayStates::Canceled { error } => {
1096 return Err(PublicGatewayError::LNv1(LNv1Error::OutgoingContract {
1097 error: Box::new(error.clone()),
1098 message: format!(
1099 "Cancelled with {error} while paying invoice with contract id {contract_id}"
1100 ),
1101 }));
1102 }
1103 GatewayExtPayStates::Created => {
1104 debug!(target: LOG_GATEWAY, contract_id = %contract_id, "Start pay invoice state machine");
1105 }
1106 other => {
1107 debug!(target: LOG_GATEWAY, state = ?other, contract_id = %contract_id, "Got state while paying invoice");
1108 }
1109 }
1110 }
1111
1112 Err(PublicGatewayError::LNv1(LNv1Error::OutgoingPayment(
1113 anyhow!("Ran out of state updates while paying invoice"),
1114 )))
1115 }
1116
1117 pub async fn handle_connect_federation(
1122 &self,
1123 payload: ConnectFedPayload,
1124 ) -> AdminResult<FederationInfo> {
1125 let GatewayState::Running { lightning_context } = self.get_state().await else {
1126 return Err(AdminGatewayError::Lightning(
1127 LightningRpcError::FailedToConnect,
1128 ));
1129 };
1130
1131 let invite_code = InviteCode::from_str(&payload.invite_code).map_err(|e| {
1132 AdminGatewayError::ClientCreationError(anyhow!(format!(
1133 "Invalid federation member string {e:?}"
1134 )))
1135 })?;
1136
1137 #[cfg(feature = "tor")]
1138 let connector = match &payload.use_tor {
1139 Some(true) => Connector::tor(),
1140 Some(false) => Connector::default(),
1141 None => {
1142 debug!(target: LOG_GATEWAY, "Missing `use_tor` payload field, defaulting to `Connector::Tcp` variant!");
1143 Connector::default()
1144 }
1145 };
1146
1147 #[cfg(not(feature = "tor"))]
1148 let connector = Connector::default();
1149
1150 let federation_id = invite_code.federation_id();
1151
1152 let mut federation_manager = self.federation_manager.write().await;
1153
1154 if federation_manager.has_federation(federation_id) {
1156 return Err(AdminGatewayError::ClientCreationError(anyhow!(
1157 "Federation has already been registered"
1158 )));
1159 }
1160
1161 let federation_index = federation_manager.pop_next_index()?;
1164
1165 let federation_config = FederationConfig {
1166 invite_code,
1167 federation_index,
1168 lightning_fee: self.default_routing_fees,
1169 transaction_fee: self.default_transaction_fees,
1170 connector,
1171 };
1172
1173 let recover = payload.recover.unwrap_or(false);
1174 if recover {
1175 self.client_builder
1176 .recover(
1177 federation_config.clone(),
1178 Arc::new(self.clone()),
1179 &self.mnemonic,
1180 )
1181 .await?;
1182 }
1183
1184 let client = self
1185 .client_builder
1186 .build(
1187 federation_config.clone(),
1188 Arc::new(self.clone()),
1189 &self.mnemonic,
1190 )
1191 .await?;
1192
1193 if recover {
1194 client.wait_for_all_active_state_machines().await?;
1195 }
1196
1197 let federation_info = FederationInfo {
1200 federation_id,
1201 federation_name: federation_manager.federation_name(&client).await,
1202 balance_msat: client.get_balance().await,
1203 config: federation_config.clone(),
1204 };
1205
1206 if self.is_running_lnv1() {
1207 Self::check_lnv1_federation_network(&client, self.network).await?;
1208 client
1209 .get_first_module::<GatewayClientModule>()?
1210 .try_register_with_federation(
1211 Vec::new(),
1213 GW_ANNOUNCEMENT_TTL,
1214 federation_config.lightning_fee.into(),
1215 lightning_context,
1216 self.versioned_api.clone(),
1217 self.gateway_id,
1218 )
1219 .await;
1220 }
1221
1222 if self.is_running_lnv2() {
1223 Self::check_lnv2_federation_network(&client, self.network).await?;
1224 }
1225
1226 federation_manager.add_client(
1228 federation_index,
1229 Spanned::new(
1230 info_span!(target: LOG_GATEWAY, "client", federation_id=%federation_id.clone()),
1231 async { client },
1232 )
1233 .await,
1234 );
1235
1236 let mut dbtx = self.gateway_db.begin_transaction().await;
1237 dbtx.save_federation_config(&federation_config).await;
1238 dbtx.commit_tx().await;
1239 debug!(
1240 target: LOG_GATEWAY,
1241 federation_id = %federation_id,
1242 federation_index = %federation_index,
1243 "Federation connected"
1244 );
1245
1246 Ok(federation_info)
1247 }
1248
1249 pub async fn handle_leave_federation(
1254 &self,
1255 payload: LeaveFedPayload,
1256 ) -> AdminResult<FederationInfo> {
1257 let mut federation_manager = self.federation_manager.write().await;
1260 let mut dbtx = self.gateway_db.begin_transaction().await;
1261
1262 let federation_info = federation_manager
1263 .leave_federation(payload.federation_id, &mut dbtx.to_ref_nc())
1264 .await?;
1265
1266 dbtx.remove_federation_config(payload.federation_id).await;
1267 dbtx.commit_tx().await;
1268 Ok(federation_info)
1269 }
1270
1271 pub async fn handle_backup_msg(
1274 &self,
1275 BackupPayload { federation_id }: BackupPayload,
1276 ) -> AdminResult<()> {
1277 let federation_manager = self.federation_manager.read().await;
1278 let client = federation_manager
1279 .client(&federation_id)
1280 .ok_or(AdminGatewayError::ClientCreationError(anyhow::anyhow!(
1281 format!("Gateway has not connected to {federation_id}")
1282 )))?
1283 .value();
1284 let metadata: BTreeMap<String, String> = BTreeMap::new();
1285 client
1286 .backup_to_federation(fedimint_client::backup::Metadata::from_json_serialized(
1287 metadata,
1288 ))
1289 .await?;
1290 Ok(())
1291 }
1292
1293 pub async fn handle_mnemonic_msg(&self) -> AdminResult<MnemonicResponse> {
1297 let words = self
1298 .mnemonic
1299 .words()
1300 .map(std::string::ToString::to_string)
1301 .collect::<Vec<_>>();
1302 let all_federations = self
1303 .federation_manager
1304 .read()
1305 .await
1306 .get_all_federation_configs()
1307 .await
1308 .keys()
1309 .copied()
1310 .collect::<BTreeSet<_>>();
1311 let legacy_federations = self.client_builder.legacy_federations(all_federations);
1312 let mnemonic_response = MnemonicResponse {
1313 mnemonic: words,
1314 legacy_federations,
1315 };
1316 Ok(mnemonic_response)
1317 }
1318
1319 pub async fn handle_set_fees_msg(
1322 &self,
1323 SetFeesPayload {
1324 federation_id,
1325 lightning_base,
1326 lightning_parts_per_million,
1327 transaction_base,
1328 transaction_parts_per_million,
1329 }: SetFeesPayload,
1330 ) -> AdminResult<()> {
1331 let mut dbtx = self.gateway_db.begin_transaction().await;
1332 let mut fed_configs = if let Some(fed_id) = federation_id {
1333 dbtx.load_federation_configs()
1334 .await
1335 .into_iter()
1336 .filter(|(id, _)| *id == fed_id)
1337 .collect::<BTreeMap<_, _>>()
1338 } else {
1339 dbtx.load_federation_configs().await
1340 };
1341
1342 for config in &mut fed_configs.values_mut() {
1343 let mut lightning_fee = config.lightning_fee;
1344 if let Some(lightning_base) = lightning_base {
1345 lightning_fee.base = lightning_base;
1346 }
1347
1348 if let Some(lightning_ppm) = lightning_parts_per_million {
1349 lightning_fee.parts_per_million = lightning_ppm;
1350 }
1351
1352 let mut transaction_fee = config.transaction_fee;
1353 if let Some(transaction_base) = transaction_base {
1354 transaction_fee.base = transaction_base;
1355 }
1356
1357 if let Some(transaction_ppm) = transaction_parts_per_million {
1358 transaction_fee.parts_per_million = transaction_ppm;
1359 }
1360
1361 let send_fees = lightning_fee + transaction_fee;
1363 if !self.is_running_lnv1() && send_fees.gt(&PaymentFee::SEND_FEE_LIMIT) {
1364 return Err(AdminGatewayError::GatewayConfigurationError(format!(
1365 "Total Send fees exceeded {}",
1366 PaymentFee::SEND_FEE_LIMIT
1367 )));
1368 }
1369
1370 if !self.is_running_lnv1() && transaction_fee.gt(&PaymentFee::RECEIVE_FEE_LIMIT) {
1372 return Err(AdminGatewayError::GatewayConfigurationError(format!(
1373 "Transaction fees exceeded RECEIVE LIMIT {}",
1374 PaymentFee::RECEIVE_FEE_LIMIT
1375 )));
1376 }
1377
1378 config.lightning_fee = lightning_fee;
1379 config.transaction_fee = transaction_fee;
1380 dbtx.save_federation_config(config).await;
1381 }
1382
1383 dbtx.commit_tx().await;
1384
1385 if self.is_running_lnv1() {
1386 let register_task_group = TaskGroup::new();
1387
1388 self.register_federations(&fed_configs, ®ister_task_group)
1389 .await;
1390 }
1391
1392 Ok(())
1393 }
1394
1395 pub async fn handle_get_ln_onchain_address_msg(&self) -> AdminResult<Address> {
1397 let context = self.get_lightning_context().await?;
1398 let response = context.lnrpc.get_ln_onchain_address().await?;
1399
1400 let address = Address::from_str(&response.address).map_err(|e| {
1401 AdminGatewayError::Lightning(LightningRpcError::InvalidMetadata {
1402 failure_reason: e.to_string(),
1403 })
1404 })?;
1405
1406 address.require_network(self.network).map_err(|e| {
1407 AdminGatewayError::Lightning(LightningRpcError::InvalidMetadata {
1408 failure_reason: e.to_string(),
1409 })
1410 })
1411 }
1412
1413 pub async fn handle_open_channel_msg(&self, payload: OpenChannelRequest) -> AdminResult<Txid> {
1416 info!(target: LOG_GATEWAY, pubkey = %payload.pubkey, host = %payload.host, amount = %payload.channel_size_sats, "Opening Lightning channel...");
1417 let context = self.get_lightning_context().await?;
1418 let res = context.lnrpc.open_channel(payload).await?;
1419 info!(target: LOG_GATEWAY, txid = %res.funding_txid, "Initiated channel open");
1420 Txid::from_str(&res.funding_txid).map_err(|e| {
1421 AdminGatewayError::Lightning(LightningRpcError::InvalidMetadata {
1422 failure_reason: format!("Received invalid channel funding txid string {e}"),
1423 })
1424 })
1425 }
1426
1427 pub async fn handle_close_channels_with_peer_msg(
1430 &self,
1431 payload: CloseChannelsWithPeerRequest,
1432 ) -> AdminResult<CloseChannelsWithPeerResponse> {
1433 let context = self.get_lightning_context().await?;
1434 let response = context.lnrpc.close_channels_with_peer(payload).await?;
1435 Ok(response)
1436 }
1437
1438 pub async fn handle_list_active_channels_msg(
1441 &self,
1442 ) -> AdminResult<Vec<fedimint_gateway_common::ChannelInfo>> {
1443 let context = self.get_lightning_context().await?;
1444 let response = context.lnrpc.list_active_channels().await?;
1445 Ok(response.channels)
1446 }
1447
1448 pub async fn handle_send_onchain_msg(&self, payload: SendOnchainRequest) -> AdminResult<Txid> {
1450 let context = self.get_lightning_context().await?;
1451 let response = context.lnrpc.send_onchain(payload).await?;
1452 Txid::from_str(&response.txid).map_err(|e| AdminGatewayError::WithdrawError {
1453 failure_reason: format!("Failed to parse withdrawal TXID: {e}"),
1454 })
1455 }
1456
1457 pub async fn handle_recheck_address_msg(
1459 &self,
1460 payload: DepositAddressRecheckPayload,
1461 ) -> AdminResult<()> {
1462 self.select_client(payload.federation_id)
1463 .await?
1464 .value()
1465 .get_first_module::<WalletClientModule>()
1466 .expect("Must have client module")
1467 .recheck_pegin_address_by_address(payload.address)
1468 .await?;
1469 Ok(())
1470 }
1471
1472 pub async fn handle_get_balances_msg(&self) -> AdminResult<GatewayBalances> {
1475 let dbtx = self.gateway_db.begin_transaction_nc().await;
1476 let federation_infos = self
1477 .federation_manager
1478 .read()
1479 .await
1480 .federation_info_all_federations(dbtx)
1481 .await;
1482
1483 let ecash_balances: Vec<FederationBalanceInfo> = federation_infos
1484 .iter()
1485 .map(|federation_info| FederationBalanceInfo {
1486 federation_id: federation_info.federation_id,
1487 ecash_balance_msats: Amount {
1488 msats: federation_info.balance_msat.msats,
1489 },
1490 })
1491 .collect();
1492
1493 let context = self.get_lightning_context().await?;
1494 let lightning_node_balances = context.lnrpc.get_balances().await?;
1495
1496 Ok(GatewayBalances {
1497 onchain_balance_sats: lightning_node_balances.onchain_balance_sats,
1498 lightning_balance_msats: lightning_node_balances.lightning_balance_msats,
1499 ecash_balances,
1500 inbound_lightning_liquidity_msats: lightning_node_balances
1501 .inbound_lightning_liquidity_msats,
1502 })
1503 }
1504
1505 pub async fn handle_spend_ecash_msg(
1507 &self,
1508 payload: SpendEcashPayload,
1509 ) -> AdminResult<SpendEcashResponse> {
1510 let client = self
1511 .select_client(payload.federation_id)
1512 .await?
1513 .into_value();
1514 let mint_module = client.get_first_module::<MintClientModule>()?;
1515 let timeout = Duration::from_secs(payload.timeout);
1516 let (operation_id, notes) = if payload.allow_overpay {
1517 let (operation_id, notes) = mint_module
1518 .spend_notes_with_selector(
1519 &SelectNotesWithAtleastAmount,
1520 payload.amount,
1521 timeout,
1522 payload.include_invite,
1523 (),
1524 )
1525 .await?;
1526
1527 let overspend_amount = notes.total_amount().saturating_sub(payload.amount);
1528 if overspend_amount != Amount::ZERO {
1529 warn!(
1530 target: LOG_GATEWAY,
1531 overspend_amount = %overspend_amount,
1532 "Selected notes worth more than requested",
1533 );
1534 }
1535
1536 (operation_id, notes)
1537 } else {
1538 mint_module
1539 .spend_notes_with_selector(
1540 &SelectNotesWithExactAmount,
1541 payload.amount,
1542 timeout,
1543 payload.include_invite,
1544 (),
1545 )
1546 .await?
1547 };
1548
1549 debug!(target: LOG_GATEWAY, ?operation_id, ?notes, "Spend ecash notes");
1550
1551 Ok(SpendEcashResponse {
1552 operation_id,
1553 notes,
1554 })
1555 }
1556
1557 pub async fn handle_receive_ecash_msg(
1559 &self,
1560 payload: ReceiveEcashPayload,
1561 ) -> Result<ReceiveEcashResponse> {
1562 let amount = payload.notes.total_amount();
1563 let client = self
1564 .federation_manager
1565 .read()
1566 .await
1567 .get_client_for_federation_id_prefix(payload.notes.federation_id_prefix())
1568 .ok_or(FederationNotConnected {
1569 federation_id_prefix: payload.notes.federation_id_prefix(),
1570 })?;
1571 let mint = client
1572 .value()
1573 .get_first_module::<MintClientModule>()
1574 .map_err(|e| PublicGatewayError::ReceiveEcashError {
1575 failure_reason: format!("Mint module does not exist: {e:?}"),
1576 })?;
1577
1578 let operation_id = mint
1579 .reissue_external_notes(payload.notes, ())
1580 .await
1581 .map_err(|e| PublicGatewayError::ReceiveEcashError {
1582 failure_reason: e.to_string(),
1583 })?;
1584 if payload.wait {
1585 let mut updates = mint
1586 .subscribe_reissue_external_notes(operation_id)
1587 .await
1588 .unwrap()
1589 .into_stream();
1590
1591 while let Some(update) = updates.next().await {
1592 if let fedimint_mint_client::ReissueExternalNotesState::Failed(e) = update {
1593 return Err(PublicGatewayError::ReceiveEcashError {
1594 failure_reason: e.to_string(),
1595 });
1596 }
1597 }
1598 }
1599
1600 Ok(ReceiveEcashResponse { amount })
1601 }
1602
1603 pub async fn handle_shutdown_msg(&self, task_group: TaskGroup) -> AdminResult<()> {
1606 let mut state_guard = self.state.write().await;
1608 if let GatewayState::Running { lightning_context } = state_guard.clone() {
1609 *state_guard = GatewayState::ShuttingDown { lightning_context };
1610
1611 self.federation_manager
1612 .read()
1613 .await
1614 .wait_for_incoming_payments()
1615 .await?;
1616 }
1617
1618 let tg = task_group.clone();
1619 tg.spawn("Kill Gateway", |_task_handle| async {
1620 if let Err(err) = task_group.shutdown_join_all(Duration::from_secs(180)).await {
1621 warn!(target: LOG_GATEWAY, err = %err.fmt_compact_anyhow(), "Error shutting down gateway");
1622 }
1623 });
1624 Ok(())
1625 }
1626
1627 pub async fn handle_payment_log_msg(
1629 &self,
1630 PaymentLogPayload {
1631 end_position,
1632 pagination_size,
1633 federation_id,
1634 event_kinds,
1635 }: PaymentLogPayload,
1636 ) -> AdminResult<PaymentLogResponse> {
1637 const BATCH_SIZE: u64 = 10_000;
1638 let federation_manager = self.federation_manager.read().await;
1639 let client = federation_manager
1640 .client(&federation_id)
1641 .ok_or(FederationNotConnected {
1642 federation_id_prefix: federation_id.to_prefix(),
1643 })?
1644 .value();
1645
1646 let event_kinds = if event_kinds.is_empty() {
1647 ALL_GATEWAY_EVENTS.to_vec()
1648 } else {
1649 event_kinds
1650 };
1651
1652 let end_position = if let Some(position) = end_position {
1653 position
1654 } else {
1655 let mut dbtx = client.db().begin_transaction_nc().await;
1656 dbtx.get_next_event_log_id().await
1657 };
1658
1659 let mut start_position = end_position.saturating_sub(BATCH_SIZE);
1660
1661 let mut payment_log = Vec::new();
1662
1663 while payment_log.len() < pagination_size {
1664 let batch = client.get_event_log(Some(start_position), BATCH_SIZE).await;
1665 let mut filtered_batch = batch
1666 .into_iter()
1667 .filter(|e| e.event_id <= end_position && event_kinds.contains(&e.event_kind))
1668 .collect::<Vec<_>>();
1669 filtered_batch.reverse();
1670 payment_log.extend(filtered_batch);
1671
1672 start_position = start_position.saturating_sub(BATCH_SIZE);
1674
1675 if start_position == EventLogId::LOG_START {
1676 break;
1677 }
1678 }
1679
1680 payment_log.truncate(pagination_size);
1682
1683 Ok(PaymentLogResponse(payment_log))
1684 }
1685
1686 pub async fn handle_payment_summary_msg(
1689 &self,
1690 PaymentSummaryPayload {
1691 start_millis,
1692 end_millis,
1693 }: PaymentSummaryPayload,
1694 ) -> AdminResult<PaymentSummaryResponse> {
1695 let federation_manager = self.federation_manager.read().await;
1696 let fed_configs = federation_manager.get_all_federation_configs().await;
1697 let federation_ids = fed_configs.keys().collect::<Vec<_>>();
1698 let start = UNIX_EPOCH + Duration::from_millis(start_millis);
1699 let end = UNIX_EPOCH + Duration::from_millis(end_millis);
1700
1701 if start > end {
1702 return Err(AdminGatewayError::Unexpected(anyhow!("Invalid time range")));
1703 }
1704
1705 let mut outgoing = StructuredPaymentEvents::default();
1706 let mut incoming = StructuredPaymentEvents::default();
1707 for fed_id in federation_ids {
1708 let client = federation_manager
1709 .client(fed_id)
1710 .expect("No client available")
1711 .value();
1712 let all_events = &get_events_for_duration(client, start, end).await;
1713
1714 if self.is_running_lnv1() && self.is_running_lnv2() {
1715 let (mut lnv1_outgoing, mut lnv1_incoming) = compute_lnv1_stats(all_events);
1716 let (mut lnv2_outgoing, mut lnv2_incoming) = compute_lnv2_stats(all_events);
1717 outgoing.combine(&mut lnv1_outgoing);
1718 incoming.combine(&mut lnv1_incoming);
1719 outgoing.combine(&mut lnv2_outgoing);
1720 incoming.combine(&mut lnv2_incoming);
1721 } else if self.is_running_lnv1() {
1722 let (mut lnv1_outgoing, mut lnv1_incoming) = compute_lnv1_stats(all_events);
1723 outgoing.combine(&mut lnv1_outgoing);
1724 incoming.combine(&mut lnv1_incoming);
1725 } else {
1726 let (mut lnv2_outgoing, mut lnv2_incoming) = compute_lnv2_stats(all_events);
1727 outgoing.combine(&mut lnv2_outgoing);
1728 incoming.combine(&mut lnv2_incoming);
1729 }
1730 }
1731
1732 Ok(PaymentSummaryResponse {
1733 outgoing: PaymentStats::compute(&outgoing),
1734 incoming: PaymentStats::compute(&incoming),
1735 })
1736 }
1737
1738 pub async fn handle_get_invoice_msg(
1741 &self,
1742 payload: GetInvoiceRequest,
1743 ) -> AdminResult<Option<GetInvoiceResponse>> {
1744 let lightning_context = self.get_lightning_context().await?;
1745 let invoice = lightning_context.lnrpc.get_invoice(payload).await?;
1746 Ok(invoice)
1747 }
1748
1749 pub async fn handle_list_transactions_msg(
1750 &self,
1751 payload: ListTransactionsPayload,
1752 ) -> AdminResult<ListTransactionsResponse> {
1753 let lightning_context = self.get_lightning_context().await?;
1754 let response = lightning_context
1755 .lnrpc
1756 .list_transactions(payload.start_secs, payload.end_secs)
1757 .await?;
1758 Ok(response)
1759 }
1760
1761 pub async fn handle_create_offer_for_operator_msg(
1763 &self,
1764 payload: CreateOfferPayload,
1765 ) -> AdminResult<CreateOfferResponse> {
1766 let lightning_context = self.get_lightning_context().await?;
1767 let offer = lightning_context.lnrpc.create_offer(
1768 payload.amount,
1769 payload.description,
1770 payload.expiry_secs,
1771 payload.quantity,
1772 )?;
1773 Ok(CreateOfferResponse { offer })
1774 }
1775
1776 pub async fn handle_pay_offer_for_operator_msg(
1778 &self,
1779 payload: PayOfferPayload,
1780 ) -> AdminResult<PayOfferResponse> {
1781 let lightning_context = self.get_lightning_context().await?;
1782 let preimage = lightning_context
1783 .lnrpc
1784 .pay_offer(
1785 payload.offer,
1786 payload.quantity,
1787 payload.amount,
1788 payload.payer_note,
1789 )
1790 .await?;
1791 Ok(PayOfferResponse {
1792 preimage: preimage.to_string(),
1793 })
1794 }
1795
1796 async fn register_federations(
1798 &self,
1799 federations: &BTreeMap<FederationId, FederationConfig>,
1800 register_task_group: &TaskGroup,
1801 ) {
1802 if let Ok(lightning_context) = self.get_lightning_context().await {
1803 let route_hints = lightning_context
1804 .lnrpc
1805 .parsed_route_hints(self.num_route_hints)
1806 .await;
1807 if route_hints.is_empty() {
1808 warn!(target: LOG_GATEWAY, "Gateway did not retrieve any route hints, may reduce receive success rate.");
1809 }
1810
1811 for (federation_id, federation_config) in federations {
1812 let fed_manager = self.federation_manager.read().await;
1813 if let Some(client) = fed_manager.client(federation_id) {
1814 let client_arc = client.clone().into_value();
1815 let route_hints = route_hints.clone();
1816 let lightning_context = lightning_context.clone();
1817 let federation_config = federation_config.clone();
1818 let api = self.versioned_api.clone();
1819 let gateway_id = self.gateway_id;
1820
1821 if let Err(err) = register_task_group
1822 .spawn_cancellable("register_federation", async move {
1823 let gateway_client = client_arc
1824 .get_first_module::<GatewayClientModule>()
1825 .expect("No GatewayClientModule exists");
1826 gateway_client
1827 .try_register_with_federation(
1828 route_hints,
1829 GW_ANNOUNCEMENT_TTL,
1830 federation_config.lightning_fee.into(),
1831 lightning_context,
1832 api,
1833 gateway_id,
1834 )
1835 .await;
1836 })
1837 .await
1838 {
1839 warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Failed to shutdown register federation task");
1840 }
1841 }
1842 }
1843 }
1844 }
1845
1846 pub async fn select_client(
1849 &self,
1850 federation_id: FederationId,
1851 ) -> std::result::Result<Spanned<fedimint_client::ClientHandleArc>, FederationNotConnected>
1852 {
1853 self.federation_manager
1854 .read()
1855 .await
1856 .client(&federation_id)
1857 .cloned()
1858 .ok_or(FederationNotConnected {
1859 federation_id_prefix: federation_id.to_prefix(),
1860 })
1861 }
1862
1863 async fn load_or_generate_mnemonic(gateway_db: &Database) -> AdminResult<Mnemonic> {
1868 Ok(
1869 if let Ok(entropy) = Client::load_decodable_client_secret::<Vec<u8>>(gateway_db).await {
1870 Mnemonic::from_entropy(&entropy)
1871 .map_err(|e| AdminGatewayError::MnemonicError(anyhow!(e.to_string())))?
1872 } else {
1873 let mnemonic = if let Ok(words) = std::env::var(FM_GATEWAY_MNEMONIC_ENV) {
1874 info!(target: LOG_GATEWAY, "Using provided mnemonic from environment variable");
1875 Mnemonic::parse_in_normalized(Language::English, words.as_str()).map_err(
1876 |e| {
1877 AdminGatewayError::MnemonicError(anyhow!(format!(
1878 "Seed phrase provided in environment was invalid {e:?}"
1879 )))
1880 },
1881 )?
1882 } else {
1883 debug!(target: LOG_GATEWAY, "Generating mnemonic and writing entropy to client storage");
1884 Bip39RootSecretStrategy::<12>::random(&mut thread_rng())
1885 };
1886
1887 Client::store_encodable_client_secret(gateway_db, mnemonic.to_entropy())
1888 .await
1889 .map_err(AdminGatewayError::MnemonicError)?;
1890 mnemonic
1891 },
1892 )
1893 }
1894
1895 async fn load_clients(&self) -> AdminResult<()> {
1899 let mut federation_manager = self.federation_manager.write().await;
1900
1901 let configs = {
1902 let mut dbtx = self.gateway_db.begin_transaction_nc().await;
1903 dbtx.load_federation_configs().await
1904 };
1905
1906 if let Some(max_federation_index) = configs.values().map(|cfg| cfg.federation_index).max() {
1907 federation_manager.set_next_index(max_federation_index + 1);
1908 }
1909
1910 for (federation_id, config) in configs {
1911 let federation_index = config.federation_index;
1912 match Box::pin(Spanned::try_new(
1913 info_span!(target: LOG_GATEWAY, "client", federation_id = %federation_id.clone()),
1914 self.client_builder
1915 .build(config, Arc::new(self.clone()), &self.mnemonic),
1916 ))
1917 .await
1918 {
1919 Ok(client) => {
1920 federation_manager.add_client(federation_index, client);
1921 }
1922 _ => {
1923 warn!(target: LOG_GATEWAY, federation_id = %federation_id, "Failed to load client");
1924 }
1925 }
1926 }
1927
1928 Ok(())
1929 }
1930
1931 fn register_clients_timer(&self) {
1937 if self.is_running_lnv1() {
1939 let lightning_module_mode = self.lightning_module_mode;
1940 info!(target: LOG_GATEWAY, lightning_module_mode = %lightning_module_mode, "Spawning register task...");
1941 let gateway = self.clone();
1942 let register_task_group = self.task_group.make_subgroup();
1943 self.task_group.spawn_cancellable("register clients", async move {
1944 loop {
1945 let gateway_state = gateway.get_state().await;
1946 if let GatewayState::Running { .. } = &gateway_state {
1947 let mut dbtx = gateway.gateway_db.begin_transaction_nc().await;
1948 let all_federations_configs = dbtx.load_federation_configs().await.into_iter().collect();
1949 gateway.register_federations(&all_federations_configs, ®ister_task_group).await;
1950 } else {
1951 const NOT_RUNNING_RETRY: Duration = Duration::from_secs(10);
1953 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");
1954 sleep(NOT_RUNNING_RETRY).await;
1955 continue;
1956 }
1957
1958 sleep(GW_ANNOUNCEMENT_TTL.mul_f32(0.85)).await;
1961 }
1962 });
1963 }
1964 }
1965
1966 async fn check_lnv1_federation_network(
1969 client: &ClientHandleArc,
1970 network: Network,
1971 ) -> AdminResult<()> {
1972 let federation_id = client.federation_id();
1973 let config = client.config().await;
1974 let cfg = config
1975 .modules
1976 .values()
1977 .find(|m| LightningCommonInit::KIND == m.kind)
1978 .ok_or(AdminGatewayError::ClientCreationError(anyhow!(format!(
1979 "Federation {federation_id} does not have an LNv1 module"
1980 ))))?;
1981 let ln_cfg: &LightningClientConfig = cfg.cast()?;
1982
1983 if ln_cfg.network.0 != network {
1984 crit!(
1985 target: LOG_GATEWAY,
1986 federation_id = %federation_id,
1987 network = %network,
1988 "Incorrect network for federation",
1989 );
1990 return Err(AdminGatewayError::ClientCreationError(anyhow!(format!(
1991 "Unsupported network {}",
1992 ln_cfg.network
1993 ))));
1994 }
1995
1996 Ok(())
1997 }
1998
1999 async fn check_lnv2_federation_network(
2002 client: &ClientHandleArc,
2003 network: Network,
2004 ) -> AdminResult<()> {
2005 let federation_id = client.federation_id();
2006 let config = client.config().await;
2007 let cfg = config
2008 .modules
2009 .values()
2010 .find(|m| fedimint_lnv2_common::LightningCommonInit::KIND == m.kind)
2011 .ok_or(AdminGatewayError::ClientCreationError(anyhow!(format!(
2012 "Federation {federation_id} does not have an LNv2 module"
2013 ))))?;
2014 let ln_cfg: &fedimint_lnv2_common::config::LightningClientConfig = cfg.cast()?;
2015
2016 if ln_cfg.network != network {
2017 crit!(
2018 target: LOG_GATEWAY,
2019 federation_id = %federation_id,
2020 network = %network,
2021 "Incorrect network for federation",
2022 );
2023 return Err(AdminGatewayError::ClientCreationError(anyhow!(format!(
2024 "Unsupported network {}",
2025 ln_cfg.network
2026 ))));
2027 }
2028
2029 Ok(())
2030 }
2031
2032 pub async fn get_lightning_context(
2036 &self,
2037 ) -> std::result::Result<LightningContext, LightningRpcError> {
2038 match self.get_state().await {
2039 GatewayState::Running { lightning_context }
2040 | GatewayState::ShuttingDown { lightning_context } => Ok(lightning_context),
2041 _ => Err(LightningRpcError::FailedToConnect),
2042 }
2043 }
2044
2045 pub async fn unannounce_from_all_federations(&self) {
2048 if self.is_running_lnv1() {
2049 let mut dbtx = self.gateway_db.begin_transaction_nc().await;
2050 let gateway_keypair = dbtx.load_gateway_keypair_assert_exists().await;
2051
2052 self.federation_manager
2053 .read()
2054 .await
2055 .unannounce_from_all_federations(gateway_keypair)
2056 .await;
2057 }
2058 }
2059
2060 fn create_lightning_client(
2061 &self,
2062 runtime: Arc<tokio::runtime::Runtime>,
2063 ) -> Box<dyn ILnRpcClient> {
2064 match self.lightning_mode.clone() {
2065 LightningMode::Lnd {
2066 lnd_rpc_addr,
2067 lnd_tls_cert,
2068 lnd_macaroon,
2069 } => Box::new(GatewayLndClient::new(
2070 lnd_rpc_addr,
2071 lnd_tls_cert,
2072 lnd_macaroon,
2073 None,
2074 )),
2075 LightningMode::Ldk {
2076 esplora_server_url,
2077 bitcoind_rpc_url,
2078 network,
2079 lightning_port,
2080 alias,
2081 } => {
2082 let chain_source_config = {
2083 match (esplora_server_url, bitcoind_rpc_url) {
2084 (Some(esplora_server_url), None) => GatewayLdkChainSourceConfig::Esplora {
2085 server_url: SafeUrl::parse(&esplora_server_url.clone())
2086 .expect("Could not parse esplora server url"),
2087 },
2088 (None, Some(bitcoind_rpc_url)) => GatewayLdkChainSourceConfig::Bitcoind {
2089 server_url: SafeUrl::parse(&bitcoind_rpc_url.clone())
2090 .expect("Could not parse bitcoind rpc url"),
2091 },
2092 (None, None) => {
2093 info!("No chain source URL provided, defaulting to esplora...");
2094 GatewayLdkChainSourceConfig::Esplora {
2095 server_url: default_esplora_server(
2096 self.network,
2097 std::env::var(FM_PORT_ESPLORA_ENV).ok(),
2098 )
2099 .url,
2100 }
2101 }
2102 (Some(_), Some(bitcoind_rpc_url)) => {
2103 warn!(
2104 "Esplora and bitcoind connection parameters are both set, using bitcoind..."
2105 );
2106 GatewayLdkChainSourceConfig::Bitcoind {
2107 server_url: SafeUrl::parse(&bitcoind_rpc_url.clone())
2108 .expect("Could not parse bitcoind rpc url"),
2109 }
2110 }
2111 }
2112 };
2113
2114 Box::new(
2115 ldk::GatewayLdkClient::new(
2116 &self.client_builder.data_dir().join(LDK_NODE_DB_FOLDER),
2117 chain_source_config,
2118 network,
2119 lightning_port,
2120 alias,
2121 self.mnemonic.clone(),
2122 runtime,
2123 )
2124 .expect("Failed to create LDK client"),
2125 )
2126 }
2127 }
2128 }
2129}
2130
2131impl Gateway {
2133 async fn public_key_v2(&self, federation_id: &FederationId) -> Option<PublicKey> {
2137 self.federation_manager
2138 .read()
2139 .await
2140 .client(federation_id)
2141 .map(|client| {
2142 client
2143 .value()
2144 .get_first_module::<GatewayClientModuleV2>()
2145 .expect("Must have client module")
2146 .keypair
2147 .public_key()
2148 })
2149 }
2150
2151 pub async fn routing_info_v2(
2154 &self,
2155 federation_id: &FederationId,
2156 ) -> Result<Option<RoutingInfo>> {
2157 let context = self.get_lightning_context().await?;
2158
2159 let mut dbtx = self.gateway_db.begin_transaction_nc().await;
2160 let fed_config = dbtx.load_federation_config(*federation_id).await.ok_or(
2161 PublicGatewayError::FederationNotConnected(FederationNotConnected {
2162 federation_id_prefix: federation_id.to_prefix(),
2163 }),
2164 )?;
2165
2166 let lightning_fee = fed_config.lightning_fee;
2167 let transaction_fee = fed_config.transaction_fee;
2168
2169 Ok(self
2170 .public_key_v2(federation_id)
2171 .await
2172 .map(|module_public_key| RoutingInfo {
2173 lightning_public_key: context.lightning_public_key,
2174 module_public_key,
2175 send_fee_default: lightning_fee + transaction_fee,
2176 send_fee_minimum: transaction_fee,
2180 expiration_delta_default: 1440,
2181 expiration_delta_minimum: EXPIRATION_DELTA_MINIMUM_V2,
2182 receive_fee: transaction_fee,
2185 }))
2186 }
2187
2188 async fn send_payment_v2(
2191 &self,
2192 payload: SendPaymentPayload,
2193 ) -> Result<std::result::Result<[u8; 32], Signature>> {
2194 self.select_client(payload.federation_id)
2195 .await?
2196 .value()
2197 .get_first_module::<GatewayClientModuleV2>()
2198 .expect("Must have client module")
2199 .send_payment(payload)
2200 .await
2201 .map_err(LNv2Error::OutgoingPayment)
2202 .map_err(PublicGatewayError::LNv2)
2203 }
2204
2205 async fn create_bolt11_invoice_v2(
2210 &self,
2211 payload: CreateBolt11InvoicePayload,
2212 ) -> Result<Bolt11Invoice> {
2213 if !payload.contract.verify() {
2214 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
2215 "The contract is invalid".to_string(),
2216 )));
2217 }
2218
2219 let payment_info = self.routing_info_v2(&payload.federation_id).await?.ok_or(
2220 LNv2Error::IncomingPayment(format!(
2221 "Federation {} does not exist",
2222 payload.federation_id
2223 )),
2224 )?;
2225
2226 if payload.contract.commitment.refund_pk != payment_info.module_public_key {
2227 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
2228 "The incoming contract is keyed to another gateway".to_string(),
2229 )));
2230 }
2231
2232 let contract_amount = payment_info.receive_fee.subtract_from(payload.amount.msats);
2233
2234 if contract_amount == Amount::ZERO {
2235 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
2236 "Zero amount incoming contracts are not supported".to_string(),
2237 )));
2238 }
2239
2240 if contract_amount != payload.contract.commitment.amount {
2241 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
2242 "The contract amount does not pay the correct amount of fees".to_string(),
2243 )));
2244 }
2245
2246 if payload.contract.commitment.expiration <= duration_since_epoch().as_secs() {
2247 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
2248 "The contract has already expired".to_string(),
2249 )));
2250 }
2251
2252 let payment_hash = match payload.contract.commitment.payment_image {
2253 PaymentImage::Hash(payment_hash) => payment_hash,
2254 PaymentImage::Point(..) => {
2255 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
2256 "PaymentImage is not a payment hash".to_string(),
2257 )));
2258 }
2259 };
2260
2261 let invoice = self
2262 .create_invoice_via_lnrpc_v2(
2263 payment_hash,
2264 payload.amount,
2265 payload.description.clone(),
2266 payload.expiry_secs,
2267 )
2268 .await?;
2269
2270 let mut dbtx = self.gateway_db.begin_transaction().await;
2271
2272 if dbtx
2273 .save_registered_incoming_contract(
2274 payload.federation_id,
2275 payload.amount,
2276 payload.contract,
2277 )
2278 .await
2279 .is_some()
2280 {
2281 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
2282 "PaymentHash is already registered".to_string(),
2283 )));
2284 }
2285
2286 dbtx.commit_tx_result().await.map_err(|_| {
2287 PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
2288 "Payment hash is already registered".to_string(),
2289 ))
2290 })?;
2291
2292 Ok(invoice)
2293 }
2294
2295 pub async fn create_invoice_via_lnrpc_v2(
2298 &self,
2299 payment_hash: sha256::Hash,
2300 amount: Amount,
2301 description: Bolt11InvoiceDescription,
2302 expiry_time: u32,
2303 ) -> std::result::Result<Bolt11Invoice, LightningRpcError> {
2304 let lnrpc = self.get_lightning_context().await?.lnrpc;
2305
2306 let response = match description {
2307 Bolt11InvoiceDescription::Direct(description) => {
2308 lnrpc
2309 .create_invoice(CreateInvoiceRequest {
2310 payment_hash: Some(payment_hash),
2311 amount_msat: amount.msats,
2312 expiry_secs: expiry_time,
2313 description: Some(InvoiceDescription::Direct(description)),
2314 })
2315 .await?
2316 }
2317 Bolt11InvoiceDescription::Hash(hash) => {
2318 lnrpc
2319 .create_invoice(CreateInvoiceRequest {
2320 payment_hash: Some(payment_hash),
2321 amount_msat: amount.msats,
2322 expiry_secs: expiry_time,
2323 description: Some(InvoiceDescription::Hash(hash)),
2324 })
2325 .await?
2326 }
2327 };
2328
2329 Bolt11Invoice::from_str(&response.invoice).map_err(|e| {
2330 LightningRpcError::FailedToGetInvoice {
2331 failure_reason: e.to_string(),
2332 }
2333 })
2334 }
2335
2336 pub async fn get_registered_incoming_contract_and_client_v2(
2340 &self,
2341 payment_image: PaymentImage,
2342 amount_msats: u64,
2343 ) -> Result<(IncomingContract, ClientHandleArc)> {
2344 let registered_incoming_contract = self
2345 .gateway_db
2346 .begin_transaction_nc()
2347 .await
2348 .load_registered_incoming_contract(payment_image)
2349 .await
2350 .ok_or(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
2351 "No corresponding decryption contract available".to_string(),
2352 )))?;
2353
2354 if registered_incoming_contract.incoming_amount_msats != amount_msats {
2355 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
2356 "The available decryption contract's amount is not equal to the requested amount"
2357 .to_string(),
2358 )));
2359 }
2360
2361 let client = self
2362 .select_client(registered_incoming_contract.federation_id)
2363 .await?
2364 .into_value();
2365
2366 Ok((registered_incoming_contract.contract, client))
2367 }
2368
2369 fn is_running_lnv2(&self) -> bool {
2371 self.lightning_module_mode == LightningModuleMode::LNv2
2372 || self.lightning_module_mode == LightningModuleMode::All
2373 }
2374
2375 fn is_running_lnv1(&self) -> bool {
2377 self.lightning_module_mode == LightningModuleMode::LNv1
2378 || self.lightning_module_mode == LightningModuleMode::All
2379 }
2380}
2381
2382#[async_trait]
2383impl IGatewayClientV2 for Gateway {
2384 async fn complete_htlc(&self, htlc_response: InterceptPaymentResponse) {
2385 loop {
2386 match self.get_lightning_context().await {
2387 Ok(lightning_context) => {
2388 match lightning_context
2389 .lnrpc
2390 .complete_htlc(htlc_response.clone())
2391 .await
2392 {
2393 Ok(..) => return,
2394 Err(err) => {
2395 warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Failure trying to complete payment");
2396 }
2397 }
2398 }
2399 Err(err) => {
2400 warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Failure trying to complete payment");
2401 }
2402 }
2403
2404 sleep(Duration::from_secs(5)).await;
2405 }
2406 }
2407
2408 async fn is_direct_swap(
2409 &self,
2410 invoice: &Bolt11Invoice,
2411 ) -> anyhow::Result<Option<(IncomingContract, ClientHandleArc)>> {
2412 let lightning_context = self.get_lightning_context().await?;
2413 if lightning_context.lightning_public_key == invoice.get_payee_pub_key() {
2414 let (contract, client) = self
2415 .get_registered_incoming_contract_and_client_v2(
2416 PaymentImage::Hash(*invoice.payment_hash()),
2417 invoice
2418 .amount_milli_satoshis()
2419 .expect("The amount invoice has been previously checked"),
2420 )
2421 .await?;
2422 Ok(Some((contract, client)))
2423 } else {
2424 Ok(None)
2425 }
2426 }
2427
2428 async fn pay(
2429 &self,
2430 invoice: Bolt11Invoice,
2431 max_delay: u64,
2432 max_fee: Amount,
2433 ) -> std::result::Result<[u8; 32], LightningRpcError> {
2434 let lightning_context = self.get_lightning_context().await?;
2435 lightning_context
2436 .lnrpc
2437 .pay(invoice, max_delay, max_fee)
2438 .await
2439 .map(|response| response.preimage.0)
2440 }
2441
2442 async fn min_contract_amount(
2443 &self,
2444 federation_id: &FederationId,
2445 amount: u64,
2446 ) -> anyhow::Result<Amount> {
2447 Ok(self
2448 .routing_info_v2(federation_id)
2449 .await?
2450 .ok_or(anyhow!("Routing Info not available"))?
2451 .send_fee_minimum
2452 .add_to(amount))
2453 }
2454}
2455
2456#[async_trait]
2457impl IGatewayClientV1 for Gateway {
2458 async fn verify_preimage_authentication(
2459 &self,
2460 payment_hash: sha256::Hash,
2461 preimage_auth: sha256::Hash,
2462 contract: OutgoingContractAccount,
2463 ) -> std::result::Result<(), OutgoingPaymentError> {
2464 let mut dbtx = self.gateway_db.begin_transaction().await;
2465 if let Some(secret_hash) = dbtx.load_preimage_authentication(payment_hash).await {
2466 if secret_hash != preimage_auth {
2467 return Err(OutgoingPaymentError {
2468 error_type: OutgoingPaymentErrorType::InvalidInvoicePreimage,
2469 contract_id: contract.contract.contract_id(),
2470 contract: Some(contract),
2471 });
2472 }
2473 } else {
2474 dbtx.save_new_preimage_authentication(payment_hash, preimage_auth)
2477 .await;
2478 return dbtx
2479 .commit_tx_result()
2480 .await
2481 .map_err(|_| OutgoingPaymentError {
2482 error_type: OutgoingPaymentErrorType::InvoiceAlreadyPaid,
2483 contract_id: contract.contract.contract_id(),
2484 contract: Some(contract),
2485 });
2486 }
2487
2488 Ok(())
2489 }
2490
2491 async fn verify_pruned_invoice(&self, payment_data: PaymentData) -> anyhow::Result<()> {
2492 let lightning_context = self.get_lightning_context().await?;
2493
2494 if matches!(payment_data, PaymentData::PrunedInvoice { .. }) {
2495 ensure!(
2496 lightning_context.lnrpc.supports_private_payments(),
2497 "Private payments are not supported by the lightning node"
2498 );
2499 }
2500
2501 Ok(())
2502 }
2503
2504 async fn get_routing_fees(&self, federation_id: FederationId) -> Option<RoutingFees> {
2505 let mut gateway_dbtx = self.gateway_db.begin_transaction_nc().await;
2506 gateway_dbtx
2507 .load_federation_config(federation_id)
2508 .await
2509 .map(|c| c.lightning_fee.into())
2510 }
2511
2512 async fn get_client(&self, federation_id: &FederationId) -> Option<Spanned<ClientHandleArc>> {
2513 self.federation_manager
2514 .read()
2515 .await
2516 .client(federation_id)
2517 .cloned()
2518 }
2519
2520 async fn get_client_for_invoice(
2521 &self,
2522 payment_data: PaymentData,
2523 ) -> Option<Spanned<ClientHandleArc>> {
2524 let rhints = payment_data.route_hints();
2525 match rhints.first().and_then(|rh| rh.0.last()) {
2526 None => None,
2527 Some(hop) => match self.get_lightning_context().await {
2528 Ok(lightning_context) => {
2529 if hop.src_node_id != lightning_context.lightning_public_key {
2530 return None;
2531 }
2532
2533 self.federation_manager
2534 .read()
2535 .await
2536 .get_client_for_index(hop.short_channel_id)
2537 }
2538 Err(_) => None,
2539 },
2540 }
2541 }
2542
2543 async fn pay(
2544 &self,
2545 payment_data: PaymentData,
2546 max_delay: u64,
2547 max_fee: Amount,
2548 ) -> std::result::Result<PayInvoiceResponse, LightningRpcError> {
2549 let lightning_context = self.get_lightning_context().await?;
2550
2551 match payment_data {
2552 PaymentData::Invoice(invoice) => {
2553 lightning_context
2554 .lnrpc
2555 .pay(invoice, max_delay, max_fee)
2556 .await
2557 }
2558 PaymentData::PrunedInvoice(invoice) => {
2559 lightning_context
2560 .lnrpc
2561 .pay_private(invoice, max_delay, max_fee)
2562 .await
2563 }
2564 }
2565 }
2566
2567 async fn complete_htlc(
2568 &self,
2569 htlc: InterceptPaymentResponse,
2570 ) -> std::result::Result<(), LightningRpcError> {
2571 let lightning_context = loop {
2573 match self.get_lightning_context().await {
2574 Ok(lightning_context) => break lightning_context,
2575 Err(err) => {
2576 warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Failure trying to complete payment");
2577 sleep(Duration::from_secs(5)).await;
2578 }
2579 }
2580 };
2581
2582 lightning_context.lnrpc.complete_htlc(htlc).await
2583 }
2584}