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