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