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