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 federation_status;
24mod iroh_server;
25mod metrics;
26mod rate_limit;
27mod registration_health;
28pub mod rpc_server;
29mod types;
30
31use std::collections::{BTreeMap, BTreeSet};
32use std::env;
33use std::fmt::Display;
34use std::net::SocketAddr;
35use std::str::FromStr;
36use std::sync::Arc;
37use std::time::{Duration, UNIX_EPOCH};
38
39use anyhow::{Context, anyhow, ensure};
40use async_trait::async_trait;
41use bitcoin::hashes::sha256;
42use bitcoin::{Address, Network, Txid, secp256k1};
43use clap::Parser;
44use client::GatewayClientBuilder;
45pub use config::GatewayParameters;
46use config::{DatabaseBackend, GatewayOpts};
47use envs::FM_GATEWAY_SKIP_WAIT_FOR_SYNC_ENV;
48use error::FederationNotConnected;
49use events::ALL_GATEWAY_EVENTS;
50use federation_manager::FederationManager;
51use fedimint_bip39::{Bip39RootSecretStrategy, Language, Mnemonic};
52use fedimint_bitcoind::bitcoincore::BitcoindClient;
53use fedimint_bitcoind::{EsploraClient, IBitcoindRpc};
54use fedimint_client::module_init::ClientModuleInitRegistry;
55use fedimint_client::secret::RootSecretStrategy;
56use fedimint_client::{Client, ClientHandleArc};
57use fedimint_core::base32::{self, FEDIMINT_PREFIX};
58use fedimint_core::config::FederationId;
59use fedimint_core::core::OperationId;
60use fedimint_core::db::{Committable, Database, DatabaseTransaction, apply_migrations};
61use fedimint_core::envs::is_env_var_set;
62use fedimint_core::invite_code::InviteCode;
63use fedimint_core::module::CommonModuleInit;
64use fedimint_core::module::registry::ModuleDecoderRegistry;
65use fedimint_core::rustls::install_crypto_provider;
66use fedimint_core::secp256k1::PublicKey;
67use fedimint_core::secp256k1::schnorr::Signature;
68use fedimint_core::task::{TaskGroup, TaskHandle, TaskShutdownToken, sleep, timeout};
69use fedimint_core::time::duration_since_epoch;
70use fedimint_core::util::backoff_util::fibonacci_max_one_hour;
71use fedimint_core::util::{FmtCompact, FmtCompactAnyhow, SafeUrl, Spanned, retry};
72use fedimint_core::{
73 Amount, BitcoinAmountOrAll, PeerId, TieredCounts, crit, fedimint_build_code_version_env,
74 get_network_for_address,
75};
76use fedimint_eventlog::{DBTransactionEventLogExt, EventLogId, StructuredPaymentEvents};
77use fedimint_gateway_common::{
78 BackupPayload, ChainSource, CloseChannelsWithPeerRequest, CloseChannelsWithPeerResponse,
79 ConnectFedPayload, ConnectPeerRequest, ConnectorType, CreateInvoiceForOperatorPayload,
80 CreateOfferPayload, CreateOfferResponse, DepositAddressPayload, DepositAddressRecheckPayload,
81 FederationBalanceInfo, FederationConfig, FederationInfo, GatewayBalances, GatewayFedConfig,
82 GatewayInfo, GetInvoiceRequest, GetInvoiceResponse, LeaveFedPayload, LightningInfo,
83 LightningMode, ListTransactionsPayload, ListTransactionsResponse, MnemonicResponse,
84 OpenChannelRequest, PayInvoiceForOperatorPayload, PayOfferPayload, PayOfferResponse,
85 PaymentLogPayload, PaymentLogResponse, PaymentStats, PaymentSummaryPayload,
86 PaymentSummaryResponse, PeginFromOnchainPayload, ReceiveEcashPayload, ReceiveEcashResponse,
87 RegisteredProtocol, SendOnchainRequest, SetChannelFeesRequest, SetFeesPayload,
88 SetMnemonicPayload, SpendEcashPayload, SpendEcashResponse, V1_API_ENDPOINT, WithdrawPayload,
89 WithdrawPreviewPayload, WithdrawPreviewResponse, WithdrawResponse, WithdrawToOnchainPayload,
90};
91use fedimint_gateway_server_db::{GatewayDbtxNcExt as _, get_gatewayd_database_migrations};
92pub use fedimint_gateway_ui::IAdminGateway;
93use fedimint_gw_client::events::compute_lnv1_stats;
94use fedimint_gw_client::pay::{OutgoingPaymentError, OutgoingPaymentErrorType};
95use fedimint_gw_client::{
96 GatewayClientModule, GatewayExtPayStates, GatewayExtReceiveStates, IGatewayClientV1,
97 SwapParameters,
98};
99use fedimint_gwv2_client::events::compute_lnv2_stats;
100use fedimint_gwv2_client::{
101 EXPIRATION_DELTA_MINIMUM_V2, FinalReceiveState, GatewayClientModuleV2, IGatewayClientV2,
102};
103use fedimint_lightning::lnd::GatewayLndClient;
104use fedimint_lightning::{
105 CreateInvoiceRequest, ILnRpcClient, InterceptPaymentRequest, InterceptPaymentResponse,
106 InvoiceDescription, LightningContext, LightningRpcError, LnRpcTracked, Lnv2HoldInvoiceFilter,
107 PayInvoiceResponse, PaymentAction, RouteHtlcStream, ldk,
108};
109use fedimint_ln_client::pay::PaymentData;
110use fedimint_ln_common::LightningCommonInit;
111use fedimint_ln_common::config::LightningClientConfig;
112use fedimint_ln_common::contracts::outgoing::OutgoingContractAccount;
113use fedimint_ln_common::contracts::{IdentifiableContract, Preimage};
114use fedimint_lnurl::VerifyResponse;
115use fedimint_lnv2_common::Bolt11InvoiceDescription;
116use fedimint_lnv2_common::contracts::{IncomingContract, PaymentImage};
117use fedimint_lnv2_common::gateway_api::{
118 CreateBolt11InvoicePayload, MAX_INVOICE_EXPIRY_SECS, PaymentFee, RoutingInfo,
119 SendPaymentPayload,
120};
121use fedimint_logging::LOG_GATEWAY;
122use fedimint_mint_client::{MintClientInit, MintClientModule, OOBNotes, ReissueExternalNotesState};
123use fedimint_mintv2_client::{
124 MintClientInit as MintV2ClientInit, MintClientModule as MintV2ClientModule,
125};
126use fedimint_wallet_client::{PegOutFees, WalletClientInit, WalletClientModule, WithdrawState};
127use futures::stream::StreamExt;
128use lightning_invoice::{Bolt11Invoice, RoutingFees};
129use rand::rngs::OsRng;
130use tokio::sync::RwLock;
131use tracing::{debug, info, info_span, warn};
132
133use crate::envs::FM_GATEWAY_MNEMONIC_ENV;
134use crate::error::{AdminGatewayError, LNv1Error, LNv2Error, PublicGatewayError};
135use crate::events::get_events_for_duration;
136use crate::rate_limit::TokenBucketRateLimiter;
137use crate::registration_health::RegistrationHealthTracker;
138use crate::rpc_server::run_webserver;
139use crate::types::PrettyInterceptPaymentRequest;
140
141const GW_ANNOUNCEMENT_TTL: Duration = Duration::from_mins(10);
143
144const DEFAULT_NUM_ROUTE_HINTS: u32 = 1;
147
148const DEFAULT_INVOICE_RATE_LIMIT_BURST: u32 = 50;
150
151const DEFAULT_INVOICE_RATE_LIMIT_PER_SECOND: u32 = 5;
154
155pub const DEFAULT_NETWORK: Network = Network::Regtest;
157
158const LIGHTNING_CONTEXT_RETRY_INTERVAL: Duration = Duration::from_secs(5);
161
162const VERIFY_WAIT_TIMEOUT: Duration = Duration::from_secs(30);
170
171pub type Result<T> = std::result::Result<T, PublicGatewayError>;
172pub type AdminResult<T> = std::result::Result<T, AdminGatewayError>;
173
174const DB_FILE: &str = "gatewayd.db";
177
178const LDK_NODE_DB_FOLDER: &str = "ldk_node";
181
182#[cfg_attr(doc, aquamarine::aquamarine)]
183#[derive(Clone, Debug)]
196pub enum GatewayState {
197 NotConfigured {
198 mnemonic_sender: tokio::sync::broadcast::Sender<()>,
201 },
202 Disconnected,
203 Syncing,
204 Connected,
205 Running {
206 lightning_context: LightningContext,
207 },
208 ShuttingDown {
209 lightning_context: LightningContext,
210 },
211}
212
213impl Display for GatewayState {
214 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
215 match self {
216 GatewayState::NotConfigured { .. } => write!(f, "NotConfigured"),
217 GatewayState::Disconnected => write!(f, "Disconnected"),
218 GatewayState::Syncing => write!(f, "Syncing"),
219 GatewayState::Connected => write!(f, "Connected"),
220 GatewayState::Running { .. } => write!(f, "Running"),
221 GatewayState::ShuttingDown { .. } => write!(f, "ShuttingDown"),
222 }
223 }
224}
225
226#[derive(Debug, Clone)]
229struct Registration {
230 endpoint_url: SafeUrl,
232
233 keypair: secp256k1::Keypair,
235}
236
237impl Registration {
238 pub async fn new(db: &Database, endpoint_url: SafeUrl, protocol: RegisteredProtocol) -> Self {
239 let keypair = Gateway::load_or_create_gateway_keypair(db, protocol).await;
240 Self {
241 endpoint_url,
242 keypair,
243 }
244 }
245}
246
247#[bon::bon]
248impl Gateway {
249 #[builder(start_fn = builder, finish_fn = build)]
264 pub async fn new_with_builder(
265 #[builder(start_fn)] lightning_mode: LightningMode,
266 #[builder(start_fn)] client_builder: GatewayClientBuilder,
267 #[builder(start_fn)] gateway_db: Database,
268 bcrypt_password_hash: bcrypt::HashParts,
269 bcrypt_liquidity_manager_password_hash: Option<bcrypt::HashParts>,
270 gateway_state: GatewayState,
271 chain_source: ChainSource,
272 #[builder(default = ([127, 0, 0, 1], 80).into())] listen: SocketAddr,
273 api_addr: Option<SafeUrl>,
274 #[builder(default = DEFAULT_NETWORK)] network: Network,
275 #[builder(default = DEFAULT_NUM_ROUTE_HINTS)] num_route_hints: u32,
276 #[builder(default = PaymentFee::TRANSACTION_FEE_DEFAULT)] default_routing_fees: PaymentFee,
277 #[builder(default = PaymentFee::TRANSACTION_FEE_DEFAULT)]
278 default_transaction_fees: PaymentFee,
279 iroh_listen: Option<SocketAddr>,
280 iroh_dns: Option<SafeUrl>,
281 #[builder(default)] iroh_relays: Vec<SafeUrl>,
282 metrics_listen: Option<SocketAddr>,
283 ) -> anyhow::Result<Gateway> {
284 let versioned_api = api_addr.map(|addr| {
285 addr.join(V1_API_ENDPOINT)
286 .expect("Failed to version gateway API address")
287 });
288
289 let metrics_listen = metrics_listen.unwrap_or_else(|| {
290 SocketAddr::new(
291 std::net::IpAddr::V4(std::net::Ipv4Addr::LOCALHOST),
292 listen.port() + 1,
293 )
294 });
295
296 Gateway::new(
297 lightning_mode,
298 GatewayParameters {
299 listen,
300 versioned_api,
301 bcrypt_password_hash,
302 bcrypt_liquidity_manager_password_hash,
303 network,
304 num_route_hints,
305 default_routing_fees,
306 default_transaction_fees,
307 iroh_listen,
308 iroh_dns,
309 iroh_relays,
310 skip_setup: true,
311 metrics_listen,
312 invoice_rate_limit_burst: DEFAULT_INVOICE_RATE_LIMIT_BURST,
313 invoice_rate_limit_per_second: DEFAULT_INVOICE_RATE_LIMIT_PER_SECOND,
314 },
315 gateway_db,
316 client_builder,
317 gateway_state,
318 chain_source,
319 )
320 .await
321 }
322}
323
324enum ReceivePaymentStreamAction {
326 RetryAfterDelay,
327 NoRetry,
328}
329
330#[derive(Clone)]
331pub struct Gateway {
332 federation_manager: Arc<RwLock<FederationManager>>,
334
335 lightning_mode: LightningMode,
337
338 state: Arc<RwLock<GatewayState>>,
340
341 client_builder: GatewayClientBuilder,
344
345 gateway_db: Database,
347
348 listen: SocketAddr,
350
351 metrics_listen: SocketAddr,
353
354 task_group: TaskGroup,
356
357 bcrypt_password_hash: String,
359
360 bcrypt_liquidity_manager_password_hash: Option<String>,
363
364 num_route_hints: u32,
366
367 network: Network,
369
370 chain_source: ChainSource,
372
373 default_routing_fees: PaymentFee,
375
376 default_transaction_fees: PaymentFee,
378
379 iroh_sk: iroh::SecretKey,
381
382 iroh_listen: Option<SocketAddr>,
384
385 iroh_dns: Option<SafeUrl>,
387
388 iroh_relays: Vec<SafeUrl>,
391
392 registrations: BTreeMap<RegisteredProtocol, Registration>,
395
396 registration_health: RegistrationHealthTracker,
398
399 invoice_rate_limiter: Arc<TokenBucketRateLimiter>,
401}
402
403impl std::fmt::Debug for Gateway {
404 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
405 f.debug_struct("Gateway")
406 .field("federation_manager", &self.federation_manager)
407 .field("state", &self.state)
408 .field("client_builder", &self.client_builder)
409 .field("gateway_db", &self.gateway_db)
410 .field("listen", &self.listen)
411 .field("registrations", &self.registrations)
412 .finish_non_exhaustive()
413 }
414}
415
416struct WithdrawDetails {
418 amount: Amount,
419 mint_fees: Option<Amount>,
420 peg_out_fees: PegOutFees,
421}
422
423async fn withdraw_v2(
425 client: &ClientHandleArc,
426 wallet_module: &fedimint_walletv2_client::WalletClientModule,
427 address: &Address,
428 amount: BitcoinAmountOrAll,
429) -> AdminResult<WithdrawResponse> {
430 let fee = wallet_module
431 .send_fee()
432 .await
433 .map_err(|e| AdminGatewayError::WithdrawError {
434 failure_reason: e.to_string(),
435 })?;
436
437 let withdraw_amount = match amount {
438 BitcoinAmountOrAll::All => {
439 let balance = client.get_balance_for_btc().await.map_err(|err| {
440 AdminGatewayError::Unexpected(anyhow!(
441 "Balance not available: {}",
442 err.fmt_compact_anyhow()
443 ))
444 })?;
445
446 wallet_module
451 .max_sendable_amount(balance, fee)
452 .await
453 .map_err(|err| AdminGatewayError::WithdrawError {
454 failure_reason: format!(
455 "Insufficient funds. Balance: {balance} Fee: {fee}: {}",
456 err.fmt_compact_anyhow()
457 ),
458 })?
459 }
460 BitcoinAmountOrAll::Amount(a) => a,
461 };
462
463 let operation_id = wallet_module
464 .send(
465 address.as_unchecked().clone(),
466 withdraw_amount,
467 Some(fee),
468 serde_json::Value::Null,
469 )
470 .await
471 .map_err(|e| AdminGatewayError::WithdrawError {
472 failure_reason: e.to_string(),
473 })?;
474
475 let result = wallet_module
476 .await_final_send_operation_state(operation_id)
477 .await
478 .map_err(|e| AdminGatewayError::WithdrawError {
479 failure_reason: e.to_string(),
480 })?;
481
482 let fees = PegOutFees::from_amount(fee);
483
484 match result {
485 fedimint_walletv2_client::FinalSendOperationState::Success(txid) => {
486 info!(target: LOG_GATEWAY, amount = %withdraw_amount, address = %address, "Sent funds via walletv2");
487 Ok(WithdrawResponse { txid, fees })
488 }
489 fedimint_walletv2_client::FinalSendOperationState::Aborted => {
490 Err(AdminGatewayError::WithdrawError {
491 failure_reason: "Withdrawal transaction was aborted".to_string(),
492 })
493 }
494 fedimint_walletv2_client::FinalSendOperationState::Failure => {
495 Err(AdminGatewayError::WithdrawError {
496 failure_reason: "Withdrawal failed".to_string(),
497 })
498 }
499 }
500}
501
502async fn calculate_max_withdrawable(
504 client: &ClientHandleArc,
505 address: &Address,
506) -> AdminResult<WithdrawDetails> {
507 let balance = client.get_balance_for_btc().await.map_err(|err| {
508 AdminGatewayError::Unexpected(anyhow!(
509 "Balance not available: {}",
510 err.fmt_compact_anyhow()
511 ))
512 })?;
513
514 if let Ok(wallet_module) =
515 client.get_first_module::<fedimint_walletv2_client::WalletClientModule>()
516 {
517 let fee = wallet_module
518 .send_fee()
519 .await
520 .map_err(|e| AdminGatewayError::WithdrawError {
521 failure_reason: e.to_string(),
522 })?;
523
524 let max_withdrawable = wallet_module
525 .max_sendable_amount(balance, fee)
526 .await
527 .map_err(|err| AdminGatewayError::WithdrawError {
528 failure_reason: err.fmt_compact_anyhow().to_string(),
529 })?;
530
531 let federation_fees = balance
536 .saturating_sub(Amount::from_sats(max_withdrawable.to_sat()))
537 .saturating_sub(Amount::from_sats(fee.to_sat()));
538
539 return Ok(WithdrawDetails {
540 amount: Amount::from_sats(max_withdrawable.to_sat()),
541 mint_fees: Some(federation_fees),
542 peg_out_fees: PegOutFees::from_amount(fee),
543 });
544 }
545
546 let Ok(wallet_module) = client.get_first_module::<WalletClientModule>() else {
547 return Err(AdminGatewayError::Unexpected(anyhow!(
548 "No wallet module found"
549 )));
550 };
551
552 let (max_withdrawable, peg_out_fees) = wallet_module
553 .max_withdrawable_amount(address, balance)
554 .await
555 .map_err(|err| AdminGatewayError::WithdrawError {
556 failure_reason: err.fmt_compact_anyhow().to_string(),
557 })?;
558
559 let federation_fees = balance
564 .saturating_sub(Amount::from_sats(max_withdrawable.to_sat()))
565 .saturating_sub(Amount::from_sats(peg_out_fees.amount().to_sat()));
566
567 Ok(WithdrawDetails {
568 amount: Amount::from_sats(max_withdrawable.to_sat()),
569 mint_fees: Some(federation_fees),
570 peg_out_fees,
571 })
572}
573
574impl Gateway {
575 fn get_bitcoind_client(
578 opts: &GatewayOpts,
579 network: bitcoin::Network,
580 gateway_id: &PublicKey,
581 ) -> anyhow::Result<(BitcoindClient, ChainSource)> {
582 let bitcoind_username = opts
583 .bitcoind_username
584 .clone()
585 .expect("FM_BITCOIND_URL is set but FM_BITCOIND_USERNAME is not");
586 let url = opts.bitcoind_url.clone().expect("No bitcoind url set");
587 let password = opts
588 .bitcoind_password
589 .clone()
590 .expect("FM_BITCOIND_URL is set but FM_BITCOIND_PASSWORD is not");
591
592 let chain_source = ChainSource::Bitcoind {
593 username: bitcoind_username.clone(),
594 password: password.clone(),
595 server_url: url.clone(),
596 };
597 let wallet_name = format!("gatewayd-{gateway_id}");
598 let client = BitcoindClient::new(&url, bitcoind_username, password, &wallet_name, network)?;
599 Ok((client, chain_source))
600 }
601
602 pub async fn new_with_default_modules(
605 mnemonic_sender: tokio::sync::broadcast::Sender<()>,
606 ) -> anyhow::Result<Gateway> {
607 let opts = GatewayOpts::parse();
608 let gateway_parameters = opts.to_gateway_parameters()?;
609 let decoders = ModuleDecoderRegistry::default();
610
611 let db_path = opts.data_dir.join(DB_FILE);
612 let gateway_db = match opts.db_backend {
613 DatabaseBackend::RocksDb => {
614 debug!(target: LOG_GATEWAY, "Using RocksDB database backend");
615 Database::new(
616 fedimint_rocksdb::RocksDb::build(db_path).open().await?,
617 decoders,
618 )
619 }
620 DatabaseBackend::CursedRedb => {
621 debug!(target: LOG_GATEWAY, "Using CursedRedb database backend");
622 Database::new(
623 fedimint_cursed_redb::MemAndRedb::new(db_path).await?,
624 decoders,
625 )
626 }
627 };
628
629 apply_migrations(
632 &gateway_db,
633 (),
634 "gatewayd".to_string(),
635 get_gatewayd_database_migrations(),
636 None,
637 None,
638 )
639 .await?;
640
641 let http_id = Self::load_or_create_gateway_keypair(&gateway_db, RegisteredProtocol::Http)
644 .await
645 .public_key();
646 let (dyn_bitcoin_rpc, chain_source) =
647 match (opts.bitcoind_url.as_ref(), opts.esplora_url.as_ref()) {
648 (Some(_), None) => {
649 let (client, chain_source) =
650 Self::get_bitcoind_client(&opts, gateway_parameters.network, &http_id)?;
651 (client.into_dyn(), chain_source)
652 }
653 (None, Some(url)) => {
654 let client = EsploraClient::new(url)
655 .expect("Could not create EsploraClient")
656 .into_dyn();
657 let chain_source = ChainSource::Esplora {
658 server_url: url.clone(),
659 };
660 (client, chain_source)
661 }
662 (Some(_), Some(_)) => {
663 let (client, chain_source) =
665 Self::get_bitcoind_client(&opts, gateway_parameters.network, &http_id)?;
666 (client.into_dyn(), chain_source)
667 }
668 _ => unreachable!("ArgGroup already enforced XOR relation"),
669 };
670
671 let mut registry = ClientModuleInitRegistry::new();
674 registry.attach(MintClientInit);
675 registry.attach(MintV2ClientInit);
676 registry.attach(WalletClientInit::new(dyn_bitcoin_rpc));
677 registry.attach(fedimint_walletv2_client::WalletClientInit);
678
679 let client_builder =
680 GatewayClientBuilder::new(opts.data_dir.clone(), registry, opts.db_backend).await?;
681
682 let gateway_state = if Self::load_mnemonic(&gateway_db).await.is_some() {
683 GatewayState::Disconnected
684 } else {
685 if gateway_parameters.skip_setup {
688 let mnemonic = if let Ok(words) = std::env::var(FM_GATEWAY_MNEMONIC_ENV) {
689 info!(target: LOG_GATEWAY, "Using provided mnemonic from environment variable");
690 Mnemonic::parse_in_normalized(Language::English, words.as_str()).map_err(
691 |e| {
692 AdminGatewayError::MnemonicError(anyhow!(format!(
693 "Seed phrase provided in environment was invalid {e:?}"
694 )))
695 },
696 )?
697 } else {
698 debug!(target: LOG_GATEWAY, "Generating mnemonic and writing entropy to client storage");
699 Bip39RootSecretStrategy::<12>::random(&mut OsRng)
700 };
701
702 Client::store_encodable_client_secret(&gateway_db, mnemonic.to_entropy())
703 .await
704 .map_err(AdminGatewayError::MnemonicError)?;
705 GatewayState::Disconnected
706 } else {
707 GatewayState::NotConfigured { mnemonic_sender }
708 }
709 };
710
711 info!(
712 target: LOG_GATEWAY,
713 version = %fedimint_build_code_version_env!(),
714 "Starting gatewayd",
715 );
716
717 Gateway::new(
718 opts.mode,
719 gateway_parameters,
720 gateway_db,
721 client_builder,
722 gateway_state,
723 chain_source,
724 )
725 .await
726 }
727
728 async fn new(
731 lightning_mode: LightningMode,
732 gateway_parameters: GatewayParameters,
733 gateway_db: Database,
734 client_builder: GatewayClientBuilder,
735 gateway_state: GatewayState,
736 chain_source: ChainSource,
737 ) -> anyhow::Result<Gateway> {
738 let num_route_hints = gateway_parameters.num_route_hints;
739 let network = gateway_parameters.network;
740
741 let task_group = TaskGroup::new();
742 task_group.install_kill_handler();
743
744 let mut registrations = BTreeMap::new();
745 if let Some(http_url) = gateway_parameters.versioned_api {
746 registrations.insert(
747 RegisteredProtocol::Http,
748 Registration::new(&gateway_db, http_url, RegisteredProtocol::Http).await,
749 );
750 }
751
752 let iroh_sk = Self::load_or_create_iroh_key(&gateway_db).await;
753 if gateway_parameters.iroh_listen.is_some() {
754 let endpoint_url = SafeUrl::parse(&format!("iroh://{}", iroh_sk.public()))?;
755 registrations.insert(
756 RegisteredProtocol::Iroh,
757 Registration::new(&gateway_db, endpoint_url, RegisteredProtocol::Iroh).await,
758 );
759 }
760
761 Ok(Self {
762 federation_manager: Arc::new(RwLock::new(FederationManager::new())),
763 lightning_mode,
764 state: Arc::new(RwLock::new(gateway_state)),
765 client_builder,
766 gateway_db: gateway_db.clone(),
767 listen: gateway_parameters.listen,
768 metrics_listen: gateway_parameters.metrics_listen,
769 task_group,
770 bcrypt_password_hash: gateway_parameters.bcrypt_password_hash.to_string(),
771 bcrypt_liquidity_manager_password_hash: gateway_parameters
772 .bcrypt_liquidity_manager_password_hash
773 .map(|h| h.to_string()),
774 num_route_hints,
775 network,
776 chain_source,
777 default_routing_fees: gateway_parameters.default_routing_fees,
778 default_transaction_fees: gateway_parameters.default_transaction_fees,
779 iroh_sk,
780 iroh_dns: gateway_parameters.iroh_dns,
781 iroh_relays: gateway_parameters.iroh_relays,
782 iroh_listen: gateway_parameters.iroh_listen,
783 registrations,
784 registration_health: RegistrationHealthTracker::default(),
785 invoice_rate_limiter: Arc::new(TokenBucketRateLimiter::new(
786 gateway_parameters.invoice_rate_limit_burst,
787 gateway_parameters.invoice_rate_limit_per_second,
788 )),
789 })
790 }
791
792 async fn load_or_create_gateway_keypair(
793 gateway_db: &Database,
794 protocol: RegisteredProtocol,
795 ) -> secp256k1::Keypair {
796 let mut dbtx = gateway_db.begin_transaction().await;
797 let keypair = dbtx.load_or_create_gateway_keypair(protocol).await;
798 dbtx.commit_tx().await;
799 keypair
800 }
801
802 async fn load_or_create_iroh_key(gateway_db: &Database) -> iroh::SecretKey {
805 let mut dbtx = gateway_db.begin_transaction().await;
806 let iroh_sk = dbtx.load_or_create_iroh_key().await;
807 dbtx.commit_tx().await;
808 iroh_sk
809 }
810
811 pub async fn http_gateway_id(&self) -> PublicKey {
812 Self::load_or_create_gateway_keypair(&self.gateway_db, RegisteredProtocol::Http)
813 .await
814 .public_key()
815 }
816
817 async fn get_state(&self) -> GatewayState {
818 self.state.read().await.clone()
819 }
820
821 pub async fn dump_database(
824 dbtx: &mut DatabaseTransaction<'_>,
825 prefix_names: Vec<String>,
826 ) -> BTreeMap<String, Box<dyn erased_serde::Serialize + Send>> {
827 dbtx.dump_database(prefix_names).await
828 }
829
830 pub async fn run(
835 self,
836 runtime: Arc<tokio::runtime::Runtime>,
837 mnemonic_receiver: tokio::sync::broadcast::Receiver<()>,
838 ) -> anyhow::Result<TaskShutdownToken> {
839 install_crypto_provider().await;
840 self.register_clients_timer();
841 self.load_clients().await?;
842 self.start_gateway(runtime, mnemonic_receiver.resubscribe());
843 self.spawn_backup_task();
844 self.spawn_prune_registered_contracts_task();
845 fedimint_metrics::spawn_api_server(self.metrics_listen, self.task_group.clone()).await?;
847 let handle = self.task_group.make_handle();
849 run_webserver(Arc::new(self), mnemonic_receiver.resubscribe()).await?;
850 let shutdown_receiver = handle.make_shutdown_rx();
851 Ok(shutdown_receiver)
852 }
853
854 fn spawn_backup_task(&self) {
857 let self_copy = self.clone();
858 self.task_group
859 .spawn_cancellable_silent("backup ecash", async move {
860 const BACKUP_UPDATE_INTERVAL: Duration = Duration::from_hours(1);
861 let mut interval = tokio::time::interval(BACKUP_UPDATE_INTERVAL);
862 interval.tick().await;
863 loop {
864 {
865 let mut dbtx = self_copy.gateway_db.begin_transaction().await;
866 self_copy.backup_all_federations(&mut dbtx).await;
867 dbtx.commit_tx().await;
868 interval.tick().await;
869 }
870 }
871 });
872 }
873
874 fn spawn_prune_registered_contracts_task(&self) {
878 let self_copy = self.clone();
879 self.task_group.spawn_cancellable_silent(
880 "prune registered incoming contracts",
881 async move {
882 const PRUNE_INTERVAL: Duration = Duration::from_hours(1);
883 const RETENTION_AFTER_EXPIRY: Duration = Duration::from_hours(24);
887
888 let mut interval = tokio::time::interval(PRUNE_INTERVAL);
889 loop {
890 interval.tick().await;
891
892 let cutoff_secs = duration_since_epoch()
893 .saturating_sub(RETENTION_AFTER_EXPIRY)
894 .as_secs();
895
896 let mut dbtx = self_copy.gateway_db.begin_transaction().await;
897 let num_pruned = dbtx.prune_registered_incoming_contracts(cutoff_secs).await;
898 match dbtx.commit_tx_result().await {
899 Ok(()) => {
900 if num_pruned > 0 {
901 info!(
902 target: LOG_GATEWAY,
903 num_pruned,
904 "Pruned expired incoming contract records"
905 );
906 }
907 }
908 Err(err) => {
909 warn!(
910 target: LOG_GATEWAY,
911 err = %err.fmt_compact(),
912 "Failed to prune expired incoming contract records"
913 );
914 }
915 }
916 }
917 },
918 );
919 }
920
921 pub async fn backup_all_federations(&self, dbtx: &mut DatabaseTransaction<'_, Committable>) {
925 const BACKUP_THRESHOLD_DURATION: Duration = Duration::from_hours(24);
928
929 let now = fedimint_core::time::now();
930 let threshold = now
931 .checked_sub(BACKUP_THRESHOLD_DURATION)
932 .expect("Cannot be negative");
933 for (id, last_backup) in dbtx.load_backup_records().await {
934 match last_backup {
935 Some(backup_time) if backup_time < threshold => {
936 let fed_manager = self.federation_manager.read().await;
937 fed_manager.backup_federation(&id, dbtx, now).await;
938 }
939 None => {
940 let fed_manager = self.federation_manager.read().await;
941 fed_manager.backup_federation(&id, dbtx, now).await;
942 }
943 _ => {}
944 }
945 }
946 }
947
948 fn start_gateway(
951 &self,
952 runtime: Arc<tokio::runtime::Runtime>,
953 mut mnemonic_receiver: tokio::sync::broadcast::Receiver<()>,
954 ) {
955 const PAYMENT_STREAM_RETRY_SECONDS: u64 = 60;
956
957 let self_copy = self.clone();
958 let tg = self.task_group.clone();
959 self.task_group.spawn(
960 "Subscribe to intercepted lightning payments in stream",
961 |handle| async move {
962 loop {
964 if handle.is_shutting_down() {
965 info!(target: LOG_GATEWAY, "Gateway lightning payment stream handler loop is shutting down");
966 break;
967 }
968
969 if let GatewayState::NotConfigured{ .. } = self_copy.get_state().await {
970 info!(
971 target: LOG_GATEWAY,
972 "Waiting for the mnemonic to be set before starting lightning receive loop."
973 );
974 info!(
975 target: LOG_GATEWAY,
976 "You might need to provide it from the UI or refer to documentation w.r.t how to initialize it."
977 );
978
979 let _ = mnemonic_receiver.recv().await;
980 info!(
981 target: LOG_GATEWAY,
982 "Received mnemonic, attempting to start lightning receive loop"
983 );
984 }
985
986 let payment_stream_task_group = tg.make_subgroup();
987 let lnrpc_route = self_copy.create_lightning_client(runtime.clone()).await;
988
989 debug!(target: LOG_GATEWAY, "Establishing lightning payment stream...");
990 let (stream, ln_client) = match lnrpc_route.route_htlcs(&payment_stream_task_group).await
991 {
992 Ok((stream, ln_client)) => (stream, ln_client),
993 Err(err) => {
994 warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Failed to open lightning payment stream");
995 if let Err(err) = payment_stream_task_group.shutdown_join_all(None).await {
1003 crit!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Lightning payment stream task group shutdown");
1004 }
1005 sleep(Duration::from_secs(PAYMENT_STREAM_RETRY_SECONDS)).await;
1006 continue
1007 }
1008 };
1009
1010 self_copy.set_gateway_state(GatewayState::Connected).await;
1012 info!(target: LOG_GATEWAY, "Established lightning payment stream");
1013
1014 let route_payments_response =
1015 self_copy.route_lightning_payments(&handle, stream, ln_client).await;
1016
1017 self_copy.set_gateway_state(GatewayState::Disconnected).await;
1018 if let Err(err) = payment_stream_task_group.shutdown_join_all(None).await {
1019 crit!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Lightning payment stream task group shutdown");
1020 }
1021
1022 self_copy.unannounce_from_all_federations().await;
1023
1024 match route_payments_response {
1025 ReceivePaymentStreamAction::RetryAfterDelay => {
1026 warn!(target: LOG_GATEWAY, retry_interval = %PAYMENT_STREAM_RETRY_SECONDS, "Disconnected from lightning node");
1027 sleep(Duration::from_secs(PAYMENT_STREAM_RETRY_SECONDS)).await;
1028 }
1029 ReceivePaymentStreamAction::NoRetry => break,
1030 }
1031 }
1032 },
1033 );
1034 }
1035
1036 async fn route_lightning_payments<'a>(
1040 &'a self,
1041 handle: &TaskHandle,
1042 mut stream: RouteHtlcStream<'a>,
1043 ln_client: Arc<dyn ILnRpcClient>,
1044 ) -> ReceivePaymentStreamAction {
1045 let LightningInfo::Connected {
1046 public_key: lightning_public_key,
1047 alias: lightning_alias,
1048 network: lightning_network,
1049 block_height: _,
1050 synced_to_chain,
1051 } = ln_client.parsed_node_info().await
1052 else {
1053 warn!(target: LOG_GATEWAY, "Failed to retrieve Lightning info");
1054 return ReceivePaymentStreamAction::RetryAfterDelay;
1055 };
1056
1057 assert!(
1058 self.network == lightning_network,
1059 "Lightning node network does not match Gateway's network. LN: {lightning_network} Gateway: {}",
1060 self.network
1061 );
1062
1063 if synced_to_chain || is_env_var_set(FM_GATEWAY_SKIP_WAIT_FOR_SYNC_ENV) {
1064 info!(target: LOG_GATEWAY, "Gateway is already synced to chain");
1065 } else {
1066 self.set_gateway_state(GatewayState::Syncing).await;
1067 info!(target: LOG_GATEWAY, "Waiting for chain sync");
1068 if let Err(err) = ln_client.wait_for_chain_sync().await {
1069 warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Failed to wait for chain sync");
1070 return ReceivePaymentStreamAction::RetryAfterDelay;
1071 }
1072 }
1073
1074 let lightning_context = LightningContext {
1075 lnrpc: LnRpcTracked::new(ln_client, "gateway"),
1076 lightning_public_key,
1077 lightning_alias,
1078 lightning_network,
1079 };
1080 if let GatewayState::ShuttingDown { .. } = self
1081 .set_gateway_state(GatewayState::Running { lightning_context })
1082 .await
1083 {
1084 info!(
1085 target: LOG_GATEWAY,
1086 "Reconnected to the lightning node while shutting down, not accepting payments"
1087 );
1088 } else {
1089 info!(target: LOG_GATEWAY, "Gateway is running");
1090 }
1091
1092 if matches!(self.lightning_mode, LightningMode::Lnd { .. }) {
1093 let mut dbtx = self.gateway_db.begin_transaction_nc().await;
1096 let all_federations_configs =
1097 dbtx.load_federation_configs().await.into_iter().collect();
1098 self.register_federations(&all_federations_configs, &self.task_group)
1099 .await;
1100 }
1101
1102 let htlc_task_group = self.task_group.make_subgroup();
1105 if handle
1106 .cancel_on_shutdown(async move {
1107 loop {
1108 let payment_request_or = tokio::select! {
1109 payment_request_or = stream.next() => {
1110 payment_request_or
1111 }
1112 () = self.is_shutting_down_safely() => {
1113 break;
1114 }
1115 };
1116
1117 let Some(payment_request) = payment_request_or else {
1118 warn!(
1119 target: LOG_GATEWAY,
1120 "Unexpected response from incoming lightning payment stream. Shutting down payment processor"
1121 );
1122 break;
1123 };
1124
1125 let state_guard = self.state.read().await;
1126 if let GatewayState::Running { ref lightning_context } = *state_guard {
1127 let gateway = self.clone();
1129 let lightning_context = lightning_context.clone();
1130 htlc_task_group.spawn_cancellable_silent(
1131 "handle_lightning_payment",
1132 async move {
1133 let start = fedimint_core::time::now();
1134 let outcome = gateway
1135 .handle_lightning_payment(payment_request, &lightning_context)
1136 .await;
1137 metrics::HTLC_HANDLING_DURATION_SECONDS
1138 .with_label_values(&[outcome])
1139 .observe(
1140 fedimint_core::time::now()
1141 .duration_since(start)
1142 .unwrap_or_default()
1143 .as_secs_f64(),
1144 );
1145 },
1146 );
1147 } else {
1148 warn!(
1149 target: LOG_GATEWAY,
1150 state = %state_guard,
1151 "Gateway isn't in a running state, cannot handle incoming payments."
1152 );
1153 break;
1154 }
1155 }
1156 })
1157 .await
1158 .is_ok()
1159 {
1160 warn!(target: LOG_GATEWAY, "Lightning payment stream connection broken. Gateway is disconnected");
1161 ReceivePaymentStreamAction::RetryAfterDelay
1162 } else {
1163 info!(target: LOG_GATEWAY, "Received shutdown signal");
1164 ReceivePaymentStreamAction::NoRetry
1165 }
1166 }
1167
1168 async fn is_shutting_down_safely(&self) {
1171 loop {
1172 if let GatewayState::ShuttingDown { .. } = self.get_state().await {
1173 return;
1174 }
1175
1176 fedimint_core::task::sleep(Duration::from_secs(1)).await;
1177 }
1178 }
1179
1180 async fn handle_lightning_payment(
1190 &self,
1191 payment_request: InterceptPaymentRequest,
1192 lightning_context: &LightningContext,
1193 ) -> &'static str {
1194 info!(
1195 target: LOG_GATEWAY,
1196 lightning_payment = %PrettyInterceptPaymentRequest(&payment_request),
1197 "Intercepting lightning payment",
1198 );
1199
1200 let lnv2_start = fedimint_core::time::now();
1201 let lnv2_result = self
1202 .try_handle_lightning_payment_lnv2(&payment_request, lightning_context)
1203 .await;
1204 let lnv2_outcome = if lnv2_result.is_ok() {
1205 "success"
1206 } else {
1207 "error"
1208 };
1209 metrics::HTLC_LNV2_ATTEMPT_DURATION_SECONDS
1210 .with_label_values(&[lnv2_outcome])
1211 .observe(
1212 fedimint_core::time::now()
1213 .duration_since(lnv2_start)
1214 .unwrap_or_default()
1215 .as_secs_f64(),
1216 );
1217 if lnv2_result.is_ok() {
1218 return "lnv2";
1219 }
1220
1221 let lnv1_start = fedimint_core::time::now();
1222 let lnv1_result = self
1223 .try_handle_lightning_payment_ln_legacy(&payment_request, lightning_context)
1224 .await;
1225 let lnv1_outcome = if lnv1_result.is_ok() {
1226 "success"
1227 } else {
1228 "error"
1229 };
1230 metrics::HTLC_LNV1_ATTEMPT_DURATION_SECONDS
1231 .with_label_values(&[lnv1_outcome])
1232 .observe(
1233 fedimint_core::time::now()
1234 .duration_since(lnv1_start)
1235 .unwrap_or_default()
1236 .as_secs_f64(),
1237 );
1238 if lnv1_result.is_ok() {
1239 return "lnv1";
1240 }
1241
1242 let is_federation_scid = match payment_request.short_channel_id {
1247 Some(scid) => self
1248 .federation_manager
1249 .read()
1250 .await
1251 .get_client_for_index(scid)
1252 .is_some(),
1253 None => false,
1254 };
1255
1256 if is_federation_scid {
1257 warn!(
1264 target: LOG_GATEWAY,
1265 payment_hash = %payment_request.payment_hash,
1266 short_channel_id = ?payment_request.short_channel_id,
1267 amount_msat = payment_request.amount_msat,
1268 incoming_chan_id = payment_request.incoming_chan_id,
1269 htlc_id = payment_request.htlc_id,
1270 lnv2_err = ?lnv2_result.as_ref().err(),
1271 lnv1_err = ?lnv1_result.as_ref().err(),
1272 "Unmatched lightning payment for federation scid: cancelling HTLC",
1273 );
1274 Self::cancel_unmatched_lightning_payment(payment_request, lightning_context).await;
1275 "cancel"
1276 } else {
1277 Self::forward_lightning_payment(payment_request, lightning_context).await;
1282 "forward"
1283 }
1284 }
1285
1286 async fn try_handle_lightning_payment_lnv2(
1289 &self,
1290 htlc_request: &InterceptPaymentRequest,
1291 lightning_context: &LightningContext,
1292 ) -> Result<()> {
1293 let (contract, client) = self
1299 .get_registered_incoming_contract_and_client_v2(
1300 PaymentImage::Hash(htlc_request.payment_hash),
1301 htlc_request.amount_msat,
1302 )
1303 .await?;
1304
1305 if let Err(err) = client
1306 .get_first_module::<GatewayClientModuleV2>()
1307 .expect("Must have client module")
1308 .relay_incoming_htlc(
1309 htlc_request.payment_hash,
1310 htlc_request.incoming_chan_id,
1311 htlc_request.htlc_id,
1312 contract,
1313 htlc_request.amount_msat,
1314 )
1315 .await
1316 {
1317 warn!(target: LOG_GATEWAY, err = %err.fmt_compact_anyhow(), "Error relaying incoming lightning payment");
1318
1319 let outcome = InterceptPaymentResponse {
1320 action: PaymentAction::Cancel,
1321 payment_hash: htlc_request.payment_hash,
1322 incoming_chan_id: htlc_request.incoming_chan_id,
1323 htlc_id: htlc_request.htlc_id,
1324 };
1325
1326 if let Err(err) = lightning_context.lnrpc.complete_htlc(outcome).await {
1327 warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Error sending HTLC response to lightning node");
1328 }
1329 }
1330
1331 Ok(())
1332 }
1333
1334 async fn try_handle_lightning_payment_ln_legacy(
1337 &self,
1338 htlc_request: &InterceptPaymentRequest,
1339 lightning_context: &LightningContext,
1340 ) -> Result<()> {
1341 let Some(federation_index) = htlc_request.short_channel_id else {
1343 return Err(PublicGatewayError::LNv1(LNv1Error::IncomingPayment(
1344 "Incoming payment has not last hop short channel id".to_string(),
1345 )));
1346 };
1347
1348 let Some(client) = self
1349 .federation_manager
1350 .read()
1351 .await
1352 .get_client_for_index(federation_index)
1353 else {
1354 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())));
1355 };
1356
1357 client
1362 .borrow()
1363 .with(|client| async {
1364 let htlc = htlc_request.clone().try_into();
1365 match htlc {
1366 Ok(htlc) => {
1367 let lnv1 =
1368 client
1369 .get_first_module::<GatewayClientModule>()
1370 .map_err(|_| {
1371 PublicGatewayError::LNv1(LNv1Error::IncomingPayment(
1372 "Federation does not have LNv1 module".to_string(),
1373 ))
1374 })?;
1375 match lnv1
1376 .gateway_handle_intercepted_htlc(htlc, async {
1377 Ok(lightning_context.lnrpc.info().await?.block_height)
1378 })
1379 .await
1380 {
1381 Ok(_) => Ok(()),
1382 Err(e) => Err(PublicGatewayError::LNv1(LNv1Error::IncomingPayment(
1383 format!("Error intercepting lightning payment {e:?}"),
1384 ))),
1385 }
1386 }
1387 _ => Err(PublicGatewayError::LNv1(LNv1Error::IncomingPayment(
1388 "Could not convert InterceptHtlcResult into an HTLC".to_string(),
1389 ))),
1390 }
1391 })
1392 .await
1393 }
1394
1395 async fn cancel_unmatched_lightning_payment(
1409 htlc_request: InterceptPaymentRequest,
1410 lightning_context: &LightningContext,
1411 ) {
1412 let outcome = InterceptPaymentResponse {
1413 action: PaymentAction::Cancel,
1414 payment_hash: htlc_request.payment_hash,
1415 incoming_chan_id: htlc_request.incoming_chan_id,
1416 htlc_id: htlc_request.htlc_id,
1417 };
1418
1419 if let Err(err) = lightning_context.lnrpc.complete_htlc(outcome).await {
1420 warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Error sending lightning payment response to lightning node");
1421 }
1422 }
1423
1424 async fn forward_lightning_payment(
1429 htlc_request: InterceptPaymentRequest,
1430 lightning_context: &LightningContext,
1431 ) {
1432 let outcome = InterceptPaymentResponse {
1433 action: PaymentAction::Forward,
1434 payment_hash: htlc_request.payment_hash,
1435 incoming_chan_id: htlc_request.incoming_chan_id,
1436 htlc_id: htlc_request.htlc_id,
1437 };
1438
1439 if let Err(err) = lightning_context.lnrpc.complete_htlc(outcome).await {
1440 warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Error sending lightning payment response to lightning node");
1441 }
1442 }
1443
1444 async fn set_gateway_state(&self, state: GatewayState) -> GatewayState {
1455 let mut lock = self.state.write().await;
1456
1457 if let GatewayState::ShuttingDown { .. } = *lock {
1458 match state {
1459 GatewayState::Running { lightning_context } => {
1460 *lock = GatewayState::ShuttingDown { lightning_context };
1461 }
1462 ignored => {
1463 info!(
1464 target: LOG_GATEWAY,
1465 ignored_state = %ignored,
1466 "Gateway is shutting down, ignoring state change"
1467 );
1468 }
1469 }
1470 } else {
1471 *lock = state;
1472 }
1473
1474 lock.clone()
1475 }
1476
1477 #[doc(hidden)]
1485 pub async fn set_gateway_state_out_of_band(&self, state: GatewayState) {
1486 self.set_gateway_state(state).await;
1487 }
1488
1489 pub async fn handle_get_federation_config(
1492 &self,
1493 federation_id_or: Option<FederationId>,
1494 ) -> AdminResult<GatewayFedConfig> {
1495 if !matches!(self.get_state().await, GatewayState::Running { .. }) {
1496 return Ok(GatewayFedConfig {
1497 federations: BTreeMap::new(),
1498 });
1499 }
1500
1501 let federations = if let Some(federation_id) = federation_id_or {
1502 let mut federations = BTreeMap::new();
1503 federations.insert(
1504 federation_id,
1505 self.federation_manager
1506 .read()
1507 .await
1508 .get_federation_config(federation_id)
1509 .await?,
1510 );
1511 federations
1512 } else {
1513 self.federation_manager
1514 .read()
1515 .await
1516 .get_all_federation_configs()
1517 .await
1518 };
1519
1520 Ok(GatewayFedConfig { federations })
1521 }
1522
1523 pub async fn handle_address_msg(&self, payload: DepositAddressPayload) -> AdminResult<Address> {
1526 let client = self.select_client(payload.federation_id).await?;
1527
1528 if let Ok(wallet_module) = client.value().get_first_module::<WalletClientModule>() {
1529 let address = wallet_module
1530 .allocate_deposit_address_expert_only(())
1531 .await?
1532 .address;
1533 Ok(address)
1534 } else if let Ok(wallet_module) = client
1535 .value()
1536 .get_first_module::<fedimint_walletv2_client::WalletClientModule>()
1537 {
1538 Ok(wallet_module.receive().await)
1539 } else {
1540 Err(AdminGatewayError::Unexpected(anyhow!(
1541 "No wallet module found"
1542 )))
1543 }
1544 }
1545
1546 async fn handle_pay_invoice_msg(
1549 &self,
1550 payload: fedimint_ln_client::pay::PayInvoicePayload,
1551 ) -> Result<Preimage> {
1552 let GatewayState::Running { .. } = self.get_state().await else {
1553 return Err(PublicGatewayError::Lightning(
1554 LightningRpcError::FailedToConnect,
1555 ));
1556 };
1557
1558 debug!(target: LOG_GATEWAY, "Handling pay invoice message");
1559 let client = self.select_client(payload.federation_id).await?;
1560 let contract_id = payload.contract_id;
1561 let gateway_module = &client
1562 .value()
1563 .get_first_module::<GatewayClientModule>()
1564 .map_err(LNv1Error::OutgoingPayment)
1565 .map_err(PublicGatewayError::LNv1)?;
1566 let operation_id = gateway_module
1567 .gateway_pay_bolt11_invoice(payload)
1568 .await
1569 .map_err(LNv1Error::OutgoingPayment)
1570 .map_err(PublicGatewayError::LNv1)?;
1571 let mut updates = gateway_module
1572 .gateway_subscribe_ln_pay(operation_id)
1573 .await
1574 .map_err(LNv1Error::OutgoingPayment)
1575 .map_err(PublicGatewayError::LNv1)?
1576 .into_stream();
1577 while let Some(update) = updates.next().await {
1578 match update {
1579 GatewayExtPayStates::Success { preimage, .. } => {
1580 debug!(target: LOG_GATEWAY, contract_id = %contract_id, "Successfully paid invoice");
1581 return Ok(preimage);
1582 }
1583 GatewayExtPayStates::Fail {
1584 error,
1585 error_message,
1586 } => {
1587 return Err(PublicGatewayError::LNv1(LNv1Error::OutgoingContract {
1588 error: Box::new(error),
1589 message: format!(
1590 "{error_message} while paying invoice with contract id {contract_id}"
1591 ),
1592 }));
1593 }
1594 GatewayExtPayStates::Canceled { error } => {
1595 return Err(PublicGatewayError::LNv1(LNv1Error::OutgoingContract {
1596 error: Box::new(error.clone()),
1597 message: format!(
1598 "Cancelled with {error} while paying invoice with contract id {contract_id}"
1599 ),
1600 }));
1601 }
1602 GatewayExtPayStates::Created => {
1603 debug!(target: LOG_GATEWAY, contract_id = %contract_id, "Start pay invoice state machine");
1604 }
1605 other => {
1606 debug!(target: LOG_GATEWAY, state = ?other, contract_id = %contract_id, "Got state while paying invoice");
1607 }
1608 }
1609 }
1610
1611 Err(PublicGatewayError::LNv1(LNv1Error::OutgoingPayment(
1612 anyhow!("Ran out of state updates while paying invoice"),
1613 )))
1614 }
1615
1616 pub async fn handle_backup_msg(
1619 &self,
1620 BackupPayload { federation_id }: BackupPayload,
1621 ) -> AdminResult<()> {
1622 let federation_manager = self.federation_manager.read().await;
1623 let client = federation_manager
1624 .client(&federation_id)
1625 .ok_or(AdminGatewayError::ClientCreationError(anyhow::anyhow!(
1626 format!("Gateway has not connected to {federation_id}")
1627 )))?
1628 .value();
1629 let metadata: BTreeMap<String, String> = BTreeMap::new();
1630 #[allow(deprecated)]
1631 client
1632 .backup_to_federation(fedimint_client::backup::Metadata::from_json_serialized(
1633 metadata,
1634 ))
1635 .await?;
1636 Ok(())
1637 }
1638
1639 pub async fn handle_recheck_address_msg(
1641 &self,
1642 payload: DepositAddressRecheckPayload,
1643 ) -> AdminResult<()> {
1644 let client = self.select_client(payload.federation_id).await?;
1645
1646 if let Ok(wallet_module) = client.value().get_first_module::<WalletClientModule>() {
1647 wallet_module
1648 .recheck_pegin_address_by_address(payload.address)
1649 .await?;
1650 Ok(())
1651 } else if client
1652 .value()
1653 .get_first_module::<fedimint_walletv2_client::WalletClientModule>()
1654 .is_ok()
1655 {
1656 Ok(())
1658 } else {
1659 Err(AdminGatewayError::Unexpected(anyhow!(
1660 "No wallet module found"
1661 )))
1662 }
1663 }
1664
1665 pub async fn handle_receive_ecash_msg(
1667 &self,
1668 payload: ReceiveEcashPayload,
1669 ) -> Result<ReceiveEcashResponse> {
1670 let federation_id_prefix = base32::decode_prefixed::<fedimint_mintv2_client::ECash>(
1672 FEDIMINT_PREFIX,
1673 &payload.notes,
1674 )
1675 .ok()
1676 .and_then(|e| e.mint())
1677 .map(|id| id.to_prefix())
1678 .or_else(|| {
1679 OOBNotes::from_str(&payload.notes)
1680 .ok()
1681 .map(|n| n.federation_id_prefix())
1682 })
1683 .ok_or_else(|| PublicGatewayError::ReceiveEcashError {
1684 failure_reason: "Invalid ecash format: could not parse as ECash or OOBNotes"
1685 .to_string(),
1686 })?;
1687
1688 let client = self
1689 .federation_manager
1690 .read()
1691 .await
1692 .get_client_for_federation_id_prefix(federation_id_prefix)
1693 .ok_or(FederationNotConnected {
1694 federation_id_prefix,
1695 })?;
1696
1697 if let Ok(mint) = client.value().get_first_module::<MintClientModule>() {
1699 let notes = OOBNotes::from_str(&payload.notes).map_err(|e| {
1700 PublicGatewayError::ReceiveEcashError {
1701 failure_reason: format!("Expected OOBNotes for MintV1 federation: {e}"),
1702 }
1703 })?;
1704 let amount = notes.total_amount();
1705
1706 let operation_id = mint.reissue_external_notes(notes, ()).await.map_err(|e| {
1707 PublicGatewayError::ReceiveEcashError {
1708 failure_reason: e.to_string(),
1709 }
1710 })?;
1711
1712 let mut updates = mint
1713 .subscribe_reissue_external_notes(operation_id)
1714 .await
1715 .map_err(|e| PublicGatewayError::ReceiveEcashError {
1716 failure_reason: format!("Could not subscribe to reissue operation: {e}"),
1717 })?
1718 .into_stream();
1719
1720 let mut reissued = false;
1725 while let Some(update) = updates.next().await {
1726 match update {
1727 ReissueExternalNotesState::Failed(failure_reason) => {
1728 return Err(PublicGatewayError::ReceiveEcashError { failure_reason });
1729 }
1730 ReissueExternalNotesState::Done => reissued = true,
1731 ReissueExternalNotesState::Created | ReissueExternalNotesState::Issuing => {}
1732 }
1733 }
1734
1735 if !reissued {
1736 return Err(PublicGatewayError::ReceiveEcashError {
1737 failure_reason: "Reissue operation ended before the notes were reissued"
1738 .to_string(),
1739 });
1740 }
1741
1742 Ok(ReceiveEcashResponse { amount })
1743 } else if let Ok(mint) = client.value().get_first_module::<MintV2ClientModule>() {
1744 let ecash: fedimint_mintv2_client::ECash =
1745 base32::decode_prefixed(FEDIMINT_PREFIX, &payload.notes).map_err(|e| {
1746 PublicGatewayError::ReceiveEcashError {
1747 failure_reason: format!("Expected ECash for MintV2 federation: {e}"),
1748 }
1749 })?;
1750 let amount = ecash.amount();
1751
1752 let operation_id = mint
1753 .receive(ecash, serde_json::Value::Null)
1754 .await
1755 .map_err(|e| PublicGatewayError::ReceiveEcashError {
1756 failure_reason: e.to_string(),
1757 })?;
1758
1759 let final_state = mint
1760 .await_final_receive_operation_state(operation_id)
1761 .await
1762 .map_err(|e| PublicGatewayError::ReceiveEcashError {
1763 failure_reason: e.to_string(),
1764 })?;
1765 match final_state {
1766 fedimint_mintv2_client::FinalReceiveOperationState::Success => {}
1767 fedimint_mintv2_client::FinalReceiveOperationState::Rejected => {
1768 return Err(PublicGatewayError::ReceiveEcashError {
1769 failure_reason: "ECash receive was rejected".to_string(),
1770 });
1771 }
1772 }
1773
1774 Ok(ReceiveEcashResponse { amount })
1775 } else {
1776 Err(PublicGatewayError::ReceiveEcashError {
1777 failure_reason: "No mint module found".to_string(),
1778 })
1779 }
1780 }
1781
1782 pub async fn handle_get_invoice_msg(
1785 &self,
1786 payload: GetInvoiceRequest,
1787 ) -> AdminResult<Option<GetInvoiceResponse>> {
1788 let lightning_context = self.get_lightning_context().await?;
1789 let invoice = lightning_context.lnrpc.get_invoice(payload).await?;
1790 Ok(invoice)
1791 }
1792
1793 pub async fn handle_withdraw_to_onchain_msg(
1796 &self,
1797 payload: WithdrawToOnchainPayload,
1798 ) -> AdminResult<WithdrawResponse> {
1799 let address = self.handle_get_ln_onchain_address_msg().await?;
1800 let withdraw = WithdrawPayload {
1801 address: address.into_unchecked(),
1802 federation_id: payload.federation_id,
1803 amount: payload.amount,
1804 quoted_fees: None,
1805 };
1806 self.handle_withdraw_msg(withdraw).await
1807 }
1808
1809 pub async fn handle_pegin_from_onchain_msg(
1812 &self,
1813 payload: PeginFromOnchainPayload,
1814 ) -> AdminResult<Txid> {
1815 let deposit = DepositAddressPayload {
1816 federation_id: payload.federation_id,
1817 };
1818 let address = self.handle_address_msg(deposit).await?;
1819 let send_onchain = SendOnchainRequest {
1820 address: address.into_unchecked(),
1821 amount: payload.amount,
1822 fee_rate_sats_per_vbyte: payload.fee_rate_sats_per_vbyte,
1823 };
1824 let txid = self.handle_send_onchain_msg(send_onchain).await?;
1825
1826 Ok(txid)
1827 }
1828
1829 async fn register_federations(
1837 &self,
1838 federations: &BTreeMap<FederationId, FederationConfig>,
1839 register_task_group: &TaskGroup,
1840 ) {
1841 if let GatewayState::ShuttingDown { .. } = self.get_state().await {
1842 info!(
1843 target: LOG_GATEWAY,
1844 "Gateway is shutting down, skipping federation registration"
1845 );
1846 return;
1847 }
1848
1849 if let Ok(lightning_context) = self.get_lightning_context().await {
1850 let route_hints = lightning_context
1851 .lnrpc
1852 .parsed_route_hints(self.num_route_hints)
1853 .await;
1854 if route_hints.is_empty() {
1855 warn!(target: LOG_GATEWAY, "Gateway did not retrieve any route hints, may reduce receive success rate.");
1856 }
1857
1858 for (federation_id, federation_config) in federations {
1859 let routing_fees = match RoutingFees::try_from(federation_config.lightning_fee) {
1864 Ok(routing_fees) => routing_fees,
1865 Err(err) => {
1866 warn!(
1867 target: LOG_GATEWAY,
1868 %federation_id,
1869 err = %err.fmt_compact(),
1870 "Skipping registration, the configured lightning fee cannot be announced. Set a smaller fee with `set_fees`."
1871 );
1872 continue;
1873 }
1874 };
1875
1876 let fed_manager = self.federation_manager.read().await;
1877 if let Some(client) = fed_manager.client(federation_id) {
1878 let federation_id = *federation_id;
1879 let client_arc = client.clone().into_value();
1880 let route_hints = route_hints.clone();
1881 let lightning_context = lightning_context.clone();
1882 let registration_health = self.registration_health.clone();
1883 let registrations = self
1884 .registrations
1885 .clone()
1886 .into_iter()
1887 .map(|(protocol, registration)| {
1888 let attempt =
1889 registration_health.begin_lnv1_attempt(federation_id, protocol);
1890 (registration, attempt)
1891 })
1892 .collect::<Vec<_>>();
1893
1894 register_task_group.spawn_cancellable_silent(
1895 "register federation",
1896 async move {
1897 let Ok(gateway_client) =
1898 client_arc.get_first_module::<GatewayClientModule>()
1899 else {
1900 return;
1901 };
1902
1903 for (registration, attempt) in registrations {
1904 let succeeded = gateway_client
1905 .try_register_with_federation(
1906 route_hints.clone(),
1907 GW_ANNOUNCEMENT_TTL,
1908 routing_fees,
1909 lightning_context.clone(),
1910 registration.endpoint_url,
1911 registration.keypair,
1912 )
1913 .await;
1914 registration_health
1915 .complete_attempt(
1916 attempt,
1917 succeeded,
1918 fedimint_core::time::now(),
1919 fedimint_core::runtime::Instant::now(),
1920 )
1921 .await;
1922 }
1923 },
1924 );
1925 }
1926 }
1927 }
1928 }
1929
1930 pub async fn select_client(
1933 &self,
1934 federation_id: FederationId,
1935 ) -> std::result::Result<Spanned<fedimint_client::ClientHandleArc>, FederationNotConnected>
1936 {
1937 self.federation_manager
1938 .read()
1939 .await
1940 .client(&federation_id)
1941 .cloned()
1942 .ok_or(FederationNotConnected {
1943 federation_id_prefix: federation_id.to_prefix(),
1944 })
1945 }
1946
1947 async fn load_mnemonic(gateway_db: &Database) -> Option<Mnemonic> {
1948 let secret = Client::load_decodable_client_secret::<Vec<u8>>(gateway_db)
1949 .await
1950 .ok()?;
1951 Mnemonic::from_entropy(&secret).ok()
1952 }
1953
1954 async fn load_clients(&self) -> AdminResult<()> {
1958 if let GatewayState::NotConfigured { .. } = self.get_state().await {
1959 return Ok(());
1960 }
1961
1962 let mut federation_manager = self.federation_manager.write().await;
1963
1964 let configs = {
1965 let mut dbtx = self.gateway_db.begin_transaction_nc().await;
1966 dbtx.load_federation_configs().await
1967 };
1968
1969 if let Some(max_federation_index) = configs.values().map(|cfg| cfg.federation_index).max() {
1970 federation_manager.set_next_index(max_federation_index + 1);
1971 }
1972
1973 let mnemonic = Self::load_mnemonic(&self.gateway_db)
1974 .await
1975 .expect("mnemonic should be set");
1976
1977 for (federation_id, config) in configs {
1978 let federation_index = config.federation_index;
1979 match Box::pin(Spanned::try_new(
1980 info_span!(target: LOG_GATEWAY, "client", federation_id = %federation_id.clone()),
1981 self.client_builder
1982 .build(config, Arc::new(self.clone()), &mnemonic),
1983 ))
1984 .await
1985 {
1986 Ok(client) => {
1987 federation_manager.add_client(federation_index, client);
1988 }
1989 _ => {
1990 warn!(target: LOG_GATEWAY, federation_id = %federation_id, "Failed to load client");
1991 }
1992 }
1993 }
1994
1995 Ok(())
1996 }
1997
1998 fn register_clients_timer(&self) {
2004 if matches!(self.lightning_mode, LightningMode::Lnd { .. }) {
2006 info!(target: LOG_GATEWAY, "Spawning register task...");
2007 let gateway = self.clone();
2008 let register_task_group = self.task_group.make_subgroup();
2009 self.task_group.spawn_cancellable("register clients", async move {
2010 loop {
2011 let gateway_state = gateway.get_state().await;
2012 if let GatewayState::Running { .. } = &gateway_state {
2013 let mut dbtx = gateway.gateway_db.begin_transaction_nc().await;
2014 let all_federations_configs = dbtx.load_federation_configs().await.into_iter().collect();
2015 gateway.register_federations(&all_federations_configs, ®ister_task_group).await;
2016 } else {
2017 const NOT_RUNNING_RETRY: Duration = Duration::from_secs(10);
2019 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");
2020 sleep(NOT_RUNNING_RETRY).await;
2021 continue;
2022 }
2023
2024 sleep(GW_ANNOUNCEMENT_TTL.mul_f32(0.85)).await;
2027 }
2028 });
2029 }
2030 }
2031
2032 async fn check_federation_network(
2035 client: &ClientHandleArc,
2036 network: Network,
2037 ) -> AdminResult<()> {
2038 let federation_id = client.federation_id();
2039 let config = client.config().await;
2040
2041 let lnv1_cfg = config
2042 .modules
2043 .values()
2044 .find(|m| LightningCommonInit::KIND == m.kind);
2045
2046 let lnv2_cfg = config
2047 .modules
2048 .values()
2049 .find(|m| fedimint_lnv2_common::LightningCommonInit::KIND == m.kind);
2050
2051 if lnv1_cfg.is_none() && lnv2_cfg.is_none() {
2053 return Err(AdminGatewayError::ClientCreationError(anyhow!(
2054 "Federation {federation_id} does not have any lightning module (LNv1 or LNv2)"
2055 )));
2056 }
2057
2058 if let Some(cfg) = lnv1_cfg {
2060 let ln_cfg: &LightningClientConfig = cfg.cast().map_err(anyhow::Error::from)?;
2061
2062 if ln_cfg.network.0 != network {
2063 crit!(
2064 target: LOG_GATEWAY,
2065 federation_id = %federation_id,
2066 network = %network,
2067 "Incorrect LNv1 network for federation",
2068 );
2069 return Err(AdminGatewayError::ClientCreationError(anyhow!(format!(
2070 "Unsupported LNv1 network {}",
2071 ln_cfg.network
2072 ))));
2073 }
2074 }
2075
2076 if let Some(cfg) = lnv2_cfg {
2078 let ln_cfg: &fedimint_lnv2_common::config::LightningClientConfig =
2079 cfg.cast().map_err(anyhow::Error::from)?;
2080
2081 if ln_cfg.network != network {
2082 crit!(
2083 target: LOG_GATEWAY,
2084 federation_id = %federation_id,
2085 network = %network,
2086 "Incorrect LNv2 network for federation",
2087 );
2088 return Err(AdminGatewayError::ClientCreationError(anyhow!(format!(
2089 "Unsupported LNv2 network {}",
2090 ln_cfg.network
2091 ))));
2092 }
2093 }
2094
2095 Ok(())
2096 }
2097
2098 pub async fn get_lightning_context(
2108 &self,
2109 ) -> std::result::Result<LightningContext, LightningRpcError> {
2110 match self.get_state().await {
2111 GatewayState::Running { lightning_context }
2112 | GatewayState::ShuttingDown { lightning_context } => Ok(lightning_context),
2113 _ => Err(LightningRpcError::FailedToConnect),
2114 }
2115 }
2116
2117 async fn await_lightning_context(&self) -> LightningContext {
2138 loop {
2139 match self.get_lightning_context().await {
2140 Ok(lightning_context) => return lightning_context,
2141 Err(err) => {
2142 let state = self.get_state().await;
2143
2144 warn!(
2145 target: LOG_GATEWAY,
2146 err = %err.fmt_compact(),
2147 %state,
2148 retry_interval_secs = LIGHTNING_CONTEXT_RETRY_INTERVAL.as_secs(),
2149 "Not connected to the lightning node, waiting before asking it again",
2150 );
2151
2152 sleep(LIGHTNING_CONTEXT_RETRY_INTERVAL).await;
2153 }
2154 }
2155 }
2156 }
2157
2158 pub async fn unannounce_from_all_federations(&self) {
2161 if matches!(self.lightning_mode, LightningMode::Lnd { .. }) {
2162 for registration in self.registrations.values() {
2163 self.federation_manager
2164 .read()
2165 .await
2166 .unannounce_from_all_federations(registration.keypair)
2167 .await;
2168 }
2169 }
2170 }
2171
2172 async fn create_lightning_client(
2173 &self,
2174 runtime: Arc<tokio::runtime::Runtime>,
2175 ) -> Box<dyn ILnRpcClient> {
2176 match self.lightning_mode.clone() {
2177 LightningMode::Lnd {
2178 lnd_rpc_addr,
2179 lnd_tls_cert,
2180 lnd_macaroon,
2181 lnd_time_pref,
2182 lnd_payment_timeout_secs,
2183 } => {
2184 let gateway_db = self.gateway_db.clone();
2189 let lnv2_filter: Lnv2HoldInvoiceFilter = Arc::new(move |hash| {
2190 let gateway_db = gateway_db.clone();
2191 Box::pin(async move {
2192 gateway_db
2193 .begin_transaction_nc()
2194 .await
2195 .load_registered_incoming_contract(PaymentImage::Hash(hash))
2196 .await
2197 .is_some()
2198 })
2199 });
2200
2201 Box::new(GatewayLndClient::new(
2202 lnd_rpc_addr,
2203 lnd_tls_cert,
2204 lnd_macaroon,
2205 lnd_time_pref,
2206 lnd_payment_timeout_secs,
2207 None,
2208 lnv2_filter,
2209 ))
2210 }
2211 LightningMode::Ldk {
2212 lightning_port,
2213 alias,
2214 } => {
2215 let mnemonic = Self::load_mnemonic(&self.gateway_db)
2216 .await
2217 .expect("mnemonic should be set");
2218 retry("create LDK Node", fibonacci_max_one_hour(), || async {
2222 ldk::GatewayLdkClient::new(
2223 &self.client_builder.data_dir().join(LDK_NODE_DB_FOLDER),
2224 self.chain_source.clone(),
2225 self.network,
2226 lightning_port,
2227 alias.clone(),
2228 mnemonic.clone(),
2229 runtime.clone(),
2230 )
2231 .map(Box::new)
2232 })
2233 .await
2234 .expect("Could not create LDK Node")
2235 }
2236 }
2237 }
2238}
2239
2240#[async_trait]
2241impl IAdminGateway for Gateway {
2242 type Error = AdminGatewayError;
2243
2244 async fn handle_get_info(&self) -> AdminResult<GatewayInfo> {
2247 let GatewayState::Running { lightning_context } = self.get_state().await else {
2248 return Ok(GatewayInfo {
2249 federations: vec![],
2250 federation_fake_scids: None,
2251 version_hash: fedimint_build_code_version_env!().to_string(),
2252 gateway_state: self.state.read().await.to_string(),
2253 lightning_info: LightningInfo::NotConnected,
2254 lightning_mode: self.lightning_mode.clone(),
2255 registrations: self
2256 .registrations
2257 .iter()
2258 .map(|(k, v)| (k.clone(), (v.endpoint_url.clone(), v.keypair.public_key())))
2259 .collect(),
2260 });
2261 };
2262
2263 let dbtx = self.gateway_db.begin_transaction_nc().await;
2264 let federations = self
2265 .federation_manager
2266 .read()
2267 .await
2268 .federation_info_all_federations(dbtx)
2269 .await;
2270
2271 let channels: BTreeMap<u64, FederationId> = federations
2272 .iter()
2273 .map(|federation_info| {
2274 (
2275 federation_info.config.federation_index,
2276 federation_info.federation_id,
2277 )
2278 })
2279 .collect();
2280
2281 let lightning_info = lightning_context.lnrpc.parsed_node_info().await;
2282
2283 Ok(GatewayInfo {
2284 federations,
2285 federation_fake_scids: Some(channels),
2286 version_hash: fedimint_build_code_version_env!().to_string(),
2287 gateway_state: self.state.read().await.to_string(),
2288 lightning_info,
2289 lightning_mode: self.lightning_mode.clone(),
2290 registrations: self
2291 .registrations
2292 .iter()
2293 .map(|(k, v)| (k.clone(), (v.endpoint_url.clone(), v.keypair.public_key())))
2294 .collect(),
2295 })
2296 }
2297
2298 async fn handle_list_channels_msg(
2301 &self,
2302 ) -> AdminResult<Vec<fedimint_gateway_common::ChannelInfo>> {
2303 let context = self.get_lightning_context().await?;
2304 let response = context.lnrpc.list_channels().await?;
2305 Ok(response.channels)
2306 }
2307
2308 async fn handle_payment_summary_msg(
2311 &self,
2312 PaymentSummaryPayload {
2313 start_millis,
2314 end_millis,
2315 }: PaymentSummaryPayload,
2316 ) -> AdminResult<PaymentSummaryResponse> {
2317 let federation_manager = self.federation_manager.read().await;
2318 let fed_configs = federation_manager.get_all_federation_configs().await;
2319 let federation_ids = fed_configs.keys().collect::<Vec<_>>();
2320 let start = UNIX_EPOCH + Duration::from_millis(start_millis);
2321 let end = UNIX_EPOCH + Duration::from_millis(end_millis);
2322
2323 if start > end {
2324 return Err(AdminGatewayError::Unexpected(anyhow!("Invalid time range")));
2325 }
2326
2327 let mut outgoing = StructuredPaymentEvents::default();
2328 let mut incoming = StructuredPaymentEvents::default();
2329 for fed_id in federation_ids {
2330 let client = federation_manager
2331 .client(fed_id)
2332 .expect("No client available")
2333 .value();
2334 let all_events = &get_events_for_duration(client, start, end).await;
2335
2336 let (mut lnv1_outgoing, mut lnv1_incoming) = compute_lnv1_stats(all_events);
2337 let (mut lnv2_outgoing, mut lnv2_incoming) = compute_lnv2_stats(all_events);
2338 outgoing.combine(&mut lnv1_outgoing);
2339 incoming.combine(&mut lnv1_incoming);
2340 outgoing.combine(&mut lnv2_outgoing);
2341 incoming.combine(&mut lnv2_incoming);
2342 }
2343
2344 Ok(PaymentSummaryResponse {
2345 outgoing: PaymentStats::compute(&outgoing),
2346 incoming: PaymentStats::compute(&incoming),
2347 })
2348 }
2349
2350 async fn handle_leave_federation(
2355 &self,
2356 payload: LeaveFedPayload,
2357 ) -> AdminResult<FederationInfo> {
2358 let mut federation_manager = self.federation_manager.write().await;
2361 let mut dbtx = self.gateway_db.begin_transaction().await;
2362
2363 let federation_info = federation_manager
2364 .leave_federation(
2365 payload.federation_id,
2366 &mut dbtx.to_ref_nc(),
2367 self.registrations.values().collect(),
2368 )
2369 .await?;
2370
2371 dbtx.remove_federation_config(payload.federation_id).await;
2372 dbtx.commit_tx().await;
2373 self.registration_health
2374 .clear_federation(payload.federation_id)
2375 .await;
2376 Ok(federation_info)
2377 }
2378
2379 async fn handle_connect_federation(
2384 &self,
2385 payload: ConnectFedPayload,
2386 ) -> AdminResult<FederationInfo> {
2387 let GatewayState::Running { lightning_context } = self.get_state().await else {
2388 return Err(AdminGatewayError::Lightning(
2389 LightningRpcError::FailedToConnect,
2390 ));
2391 };
2392
2393 let invite_code = InviteCode::from_str(&payload.invite_code).map_err(|e| {
2394 AdminGatewayError::ClientCreationError(anyhow!(format!(
2395 "Invalid federation member string {e:?}"
2396 )))
2397 })?;
2398
2399 let federation_id = invite_code.federation_id();
2400
2401 let mut federation_manager = self.federation_manager.write().await;
2402
2403 if federation_manager.has_federation(federation_id) {
2405 return Err(AdminGatewayError::ClientCreationError(anyhow!(
2406 "Federation has already been registered"
2407 )));
2408 }
2409
2410 let federation_index = federation_manager.pop_next_index()?;
2413
2414 let federation_config = FederationConfig {
2415 invite_code,
2416 federation_index,
2417 lightning_fee: self.default_routing_fees,
2418 transaction_fee: self.default_transaction_fees,
2419 _connector: ConnectorType::Tcp,
2421 };
2422
2423 let routing_fees = RoutingFees::try_from(federation_config.lightning_fee)
2427 .map_err(|err| AdminGatewayError::GatewayConfigurationError(err.to_string()))?;
2428
2429 let mnemonic = Self::load_mnemonic(&self.gateway_db)
2430 .await
2431 .expect("mnemonic should be set");
2432 let recover = payload.recover.unwrap_or(false);
2433 if recover {
2434 self.client_builder
2435 .recover(federation_config.clone(), Arc::new(self.clone()), &mnemonic)
2436 .await?;
2437 }
2438
2439 let client = self
2440 .client_builder
2441 .build(federation_config.clone(), Arc::new(self.clone()), &mnemonic)
2442 .await?;
2443
2444 if recover {
2445 client.wait_for_all_active_state_machines().await?;
2446 }
2447
2448 let federation_info = FederationInfo {
2451 federation_id,
2452 federation_name: federation_manager.federation_name(&client).await,
2453 balance_msat: client.get_balance_for_btc().await.unwrap_or_else(|err| {
2454 warn!(
2455 target: LOG_GATEWAY,
2456 err = %err.fmt_compact_anyhow(),
2457 %federation_id,
2458 "Balance not immediately available after joining/recovering."
2459 );
2460 Amount::default()
2461 }),
2462 config: federation_config.clone(),
2463 last_backup_time: None,
2464 };
2465
2466 Self::check_federation_network(&client, self.network).await?;
2467 if matches!(self.lightning_mode, LightningMode::Lnd { .. })
2468 && let Ok(lnv1) = client.get_first_module::<GatewayClientModule>()
2469 {
2470 for (protocol, registration) in &self.registrations {
2471 let attempt = self
2472 .registration_health
2473 .begin_lnv1_attempt(federation_id, protocol.clone());
2474 let succeeded = lnv1
2475 .try_register_with_federation(
2476 Vec::new(),
2478 GW_ANNOUNCEMENT_TTL,
2479 routing_fees,
2480 lightning_context.clone(),
2481 registration.endpoint_url.clone(),
2482 registration.keypair,
2483 )
2484 .await;
2485 self.registration_health
2486 .complete_attempt(
2487 attempt,
2488 succeeded,
2489 fedimint_core::time::now(),
2490 fedimint_core::runtime::Instant::now(),
2491 )
2492 .await;
2493 }
2494 }
2495
2496 federation_manager.add_client(
2498 federation_index,
2499 Spanned::new(
2500 info_span!(target: LOG_GATEWAY, "client", federation_id=%federation_id.clone()),
2501 async { client },
2502 )
2503 .await,
2504 );
2505
2506 let mut dbtx = self.gateway_db.begin_transaction().await;
2507 dbtx.save_federation_config(&federation_config).await;
2508 dbtx.save_federation_backup_record(federation_id, None)
2509 .await;
2510 dbtx.commit_tx().await;
2511 debug!(
2512 target: LOG_GATEWAY,
2513 federation_id = %federation_id,
2514 federation_index = %federation_index,
2515 "Federation connected"
2516 );
2517
2518 Ok(federation_info)
2519 }
2520
2521 async fn handle_set_fees_msg(
2524 &self,
2525 SetFeesPayload {
2526 federation_id,
2527 lightning_base,
2528 lightning_parts_per_million,
2529 transaction_base,
2530 transaction_parts_per_million,
2531 }: SetFeesPayload,
2532 ) -> AdminResult<()> {
2533 let mut dbtx = self.gateway_db.begin_transaction().await;
2534 let mut fed_configs = if let Some(fed_id) = federation_id {
2535 dbtx.load_federation_configs()
2536 .await
2537 .into_iter()
2538 .filter(|(id, _)| *id == fed_id)
2539 .collect::<BTreeMap<_, _>>()
2540 } else {
2541 dbtx.load_federation_configs().await
2542 };
2543
2544 let federation_manager = self.federation_manager.read().await;
2545
2546 for (federation_id, config) in &mut fed_configs {
2547 let mut lightning_fee = config.lightning_fee;
2548 if let Some(lightning_base) = lightning_base {
2549 lightning_fee.base = lightning_base;
2550 }
2551
2552 if let Some(lightning_ppm) = lightning_parts_per_million {
2553 lightning_fee.parts_per_million = lightning_ppm;
2554 }
2555
2556 let mut transaction_fee = config.transaction_fee;
2557 if let Some(transaction_base) = transaction_base {
2558 transaction_fee.base = transaction_base;
2559 }
2560
2561 if let Some(transaction_ppm) = transaction_parts_per_million {
2562 transaction_fee.parts_per_million = transaction_ppm;
2563 }
2564
2565 federation_manager
2568 .client(federation_id)
2569 .ok_or(FederationNotConnected {
2570 federation_id_prefix: federation_id.to_prefix(),
2571 })?;
2572
2573 let send_fees = lightning_fee.checked_add(transaction_fee).ok_or_else(|| {
2579 AdminGatewayError::GatewayConfigurationError(format!(
2580 "Total Send fees overflowed, they may not exceed {}",
2581 PaymentFee::SEND_FEE_LIMIT
2582 ))
2583 })?;
2584
2585 if !send_fees.is_within(&PaymentFee::SEND_FEE_LIMIT) {
2587 return Err(AdminGatewayError::GatewayConfigurationError(format!(
2588 "Total Send fees exceeded {}",
2589 PaymentFee::SEND_FEE_LIMIT
2590 )));
2591 }
2592
2593 if !transaction_fee.is_within(&PaymentFee::RECEIVE_FEE_LIMIT) {
2595 return Err(AdminGatewayError::GatewayConfigurationError(format!(
2596 "Transaction fees exceeded RECEIVE LIMIT {}",
2597 PaymentFee::RECEIVE_FEE_LIMIT
2598 )));
2599 }
2600
2601 config.lightning_fee = lightning_fee;
2602 config.transaction_fee = transaction_fee;
2603 dbtx.save_federation_config(config).await;
2604 }
2605
2606 dbtx.commit_tx().await;
2607
2608 if matches!(self.lightning_mode, LightningMode::Lnd { .. }) {
2609 let register_task_group = TaskGroup::new();
2610
2611 self.register_federations(&fed_configs, ®ister_task_group)
2612 .await;
2613 }
2614
2615 Ok(())
2616 }
2617
2618 async fn handle_mnemonic_msg(&self) -> AdminResult<MnemonicResponse> {
2622 let mnemonic = Self::load_mnemonic(&self.gateway_db)
2623 .await
2624 .expect("mnemonic should be set");
2625 let words = mnemonic
2626 .words()
2627 .map(std::string::ToString::to_string)
2628 .collect::<Vec<_>>();
2629 let all_federations = self
2630 .federation_manager
2631 .read()
2632 .await
2633 .get_all_federation_configs()
2634 .await
2635 .keys()
2636 .copied()
2637 .collect::<BTreeSet<_>>();
2638 let legacy_federations = self.client_builder.legacy_federations(all_federations);
2639 let mnemonic_response = MnemonicResponse {
2640 mnemonic: words,
2641 legacy_federations,
2642 };
2643 Ok(mnemonic_response)
2644 }
2645
2646 async fn handle_open_channel_msg(&self, payload: OpenChannelRequest) -> AdminResult<Txid> {
2649 info!(target: LOG_GATEWAY, pubkey = %payload.pubkey, host = %payload.host, amount = %payload.channel_size_sats, "Opening Lightning channel...");
2650 let context = self.get_lightning_context().await?;
2651 let res = context.lnrpc.open_channel(payload).await?;
2652 info!(target: LOG_GATEWAY, txid = %res.funding_txid, "Initiated channel open");
2653 Txid::from_str(&res.funding_txid).map_err(|e| {
2654 AdminGatewayError::Lightning(LightningRpcError::InvalidMetadata {
2655 failure_reason: format!("Received invalid channel funding txid string {e}"),
2656 })
2657 })
2658 }
2659
2660 async fn handle_connect_peer_msg(&self, payload: ConnectPeerRequest) -> AdminResult<()> {
2663 info!(
2664 target: LOG_GATEWAY,
2665 pubkey = %payload.node_address.pubkey,
2666 host = %payload.node_address.host_with_port(),
2667 "Connecting to Lightning peer..."
2668 );
2669 let context = self.get_lightning_context().await?;
2670 context.lnrpc.connect_peer(payload).await?;
2671 info!(target: LOG_GATEWAY, "Connected to Lightning peer");
2672 Ok(())
2673 }
2674
2675 async fn handle_close_channels_with_peer_msg(
2678 &self,
2679 payload: CloseChannelsWithPeerRequest,
2680 ) -> AdminResult<CloseChannelsWithPeerResponse> {
2681 info!(target: LOG_GATEWAY, close_channel_request = %payload, "Closing lightning channel...");
2682 let context = self.get_lightning_context().await?;
2683 let response = context
2684 .lnrpc
2685 .close_channels_with_peer(payload.clone())
2686 .await?;
2687 info!(target: LOG_GATEWAY, close_channel_request = %payload, "Initiated channel closure");
2688 Ok(response)
2689 }
2690
2691 async fn handle_set_channel_fees_msg(&self, payload: SetChannelFeesRequest) -> AdminResult<()> {
2694 info!(
2695 target: LOG_GATEWAY,
2696 funding_outpoint = %payload.funding_outpoint,
2697 base_fee_msat = payload.base_fee_msat,
2698 parts_per_million = payload.parts_per_million,
2699 "Updating channel fees..."
2700 );
2701 let context = self.get_lightning_context().await?;
2702 context.lnrpc.set_channel_fees(payload).await?;
2703 Ok(())
2704 }
2705
2706 async fn handle_get_balances_msg(&self) -> AdminResult<GatewayBalances> {
2709 let dbtx = self.gateway_db.begin_transaction_nc().await;
2710 let federation_infos = self
2711 .federation_manager
2712 .read()
2713 .await
2714 .federation_info_all_federations(dbtx)
2715 .await;
2716
2717 let ecash_balances: Vec<FederationBalanceInfo> = federation_infos
2718 .iter()
2719 .map(|federation_info| FederationBalanceInfo {
2720 federation_id: federation_info.federation_id,
2721 ecash_balance_msats: Amount {
2722 msats: federation_info.balance_msat.msats,
2723 },
2724 })
2725 .collect();
2726
2727 let context = self.get_lightning_context().await?;
2728 let lightning_node_balances = context.lnrpc.get_balances().await?;
2729
2730 Ok(GatewayBalances {
2731 onchain_balance_sats: lightning_node_balances.onchain_balance_sats,
2732 lightning_balance_msats: lightning_node_balances.lightning_balance_msats,
2733 ecash_balances,
2734 inbound_lightning_liquidity_msats: lightning_node_balances
2735 .inbound_lightning_liquidity_msats,
2736 })
2737 }
2738
2739 async fn handle_send_onchain_msg(&self, payload: SendOnchainRequest) -> AdminResult<Txid> {
2741 let context = self.get_lightning_context().await?;
2742 let response = context.lnrpc.send_onchain(payload.clone()).await?;
2743 let txid =
2744 Txid::from_str(&response.txid).map_err(|e| AdminGatewayError::WithdrawError {
2745 failure_reason: format!("Failed to parse withdrawal TXID: {e}"),
2746 })?;
2747 info!(onchain_request = %payload, txid = %txid, "Sent onchain transaction");
2748 Ok(txid)
2749 }
2750
2751 async fn handle_get_ln_onchain_address_msg(&self) -> AdminResult<Address> {
2753 let context = self.get_lightning_context().await?;
2754 let response = context.lnrpc.get_ln_onchain_address().await?;
2755
2756 let address = Address::from_str(&response.address).map_err(|e| {
2757 AdminGatewayError::Lightning(LightningRpcError::InvalidMetadata {
2758 failure_reason: e.to_string(),
2759 })
2760 })?;
2761
2762 address.require_network(self.network).map_err(|e| {
2763 AdminGatewayError::Lightning(LightningRpcError::InvalidMetadata {
2764 failure_reason: e.to_string(),
2765 })
2766 })
2767 }
2768
2769 async fn handle_deposit_address_msg(
2770 &self,
2771 payload: DepositAddressPayload,
2772 ) -> AdminResult<Address> {
2773 self.handle_address_msg(payload).await
2774 }
2775
2776 async fn handle_receive_ecash_msg(
2777 &self,
2778 payload: ReceiveEcashPayload,
2779 ) -> AdminResult<ReceiveEcashResponse> {
2780 Self::handle_receive_ecash_msg(self, payload)
2781 .await
2782 .map_err(|e| AdminGatewayError::Unexpected(anyhow::anyhow!("{e}")))
2783 }
2784
2785 async fn handle_create_invoice_for_operator_msg(
2788 &self,
2789 payload: CreateInvoiceForOperatorPayload,
2790 ) -> AdminResult<Bolt11Invoice> {
2791 let GatewayState::Running { lightning_context } = self.get_state().await else {
2792 return Err(AdminGatewayError::Lightning(
2793 LightningRpcError::FailedToConnect,
2794 ));
2795 };
2796
2797 Bolt11Invoice::from_str(
2798 &lightning_context
2799 .lnrpc
2800 .create_invoice(CreateInvoiceRequest {
2801 payment_hash: None, amount_msat: payload.amount_msats,
2804 expiry_secs: payload.expiry_secs.unwrap_or(3600),
2805 description: payload.description.map(InvoiceDescription::Direct),
2806 })
2807 .await?
2808 .invoice,
2809 )
2810 .map_err(|e| {
2811 AdminGatewayError::Lightning(LightningRpcError::InvalidMetadata {
2812 failure_reason: e.to_string(),
2813 })
2814 })
2815 }
2816
2817 async fn handle_pay_invoice_for_operator_msg(
2820 &self,
2821 payload: PayInvoiceForOperatorPayload,
2822 ) -> AdminResult<Preimage> {
2823 const BASE_FEE: u64 = 50;
2825 const FEE_DENOMINATOR: u64 = 100;
2826 const MAX_DELAY: u64 = 1008;
2827
2828 let GatewayState::Running { lightning_context } = self.get_state().await else {
2829 return Err(AdminGatewayError::Lightning(
2830 LightningRpcError::FailedToConnect,
2831 ));
2832 };
2833
2834 let max_fee = BASE_FEE
2835 + payload
2836 .invoice
2837 .amount_milli_satoshis()
2838 .context("Invoice is missing amount")?
2839 .saturating_div(FEE_DENOMINATOR);
2840
2841 let res = lightning_context
2842 .lnrpc
2843 .pay(payload.invoice, MAX_DELAY, Amount::from_msats(max_fee))
2844 .await?;
2845 Ok(res.preimage)
2846 }
2847
2848 async fn handle_list_transactions_msg(
2850 &self,
2851 payload: ListTransactionsPayload,
2852 ) -> AdminResult<ListTransactionsResponse> {
2853 let lightning_context = self.get_lightning_context().await?;
2854 let response = lightning_context
2855 .lnrpc
2856 .list_transactions(payload.start_secs, payload.end_secs)
2857 .await?;
2858 Ok(response)
2859 }
2860
2861 async fn handle_spend_ecash_msg(
2863 &self,
2864 payload: SpendEcashPayload,
2865 ) -> AdminResult<SpendEcashResponse> {
2866 let client = self
2867 .select_client(payload.federation_id)
2868 .await?
2869 .into_value();
2870
2871 if let Ok(mint_module) = client.get_first_module::<MintClientModule>() {
2872 let notes = mint_module.send_oob_notes(payload.amount, ()).await?;
2873 debug!(target: LOG_GATEWAY, ?notes, "Spend ecash notes");
2874 Ok(SpendEcashResponse {
2875 notes: notes.to_string(),
2876 })
2877 } else if let Ok(mint_module) = client.get_first_module::<MintV2ClientModule>() {
2878 let (_, ecash) = mint_module
2879 .send(payload.amount, serde_json::Value::Null, true)
2880 .await
2881 .map_err(|e| AdminGatewayError::Unexpected(e.into()))?;
2882
2883 Ok(SpendEcashResponse {
2884 notes: base32::encode_prefixed(FEDIMINT_PREFIX, &ecash),
2885 })
2886 } else {
2887 Err(AdminGatewayError::Unexpected(anyhow::anyhow!(
2888 "No mint module available"
2889 )))
2890 }
2891 }
2892
2893 async fn handle_shutdown_msg(&self, task_group: TaskGroup) -> AdminResult<()> {
2896 let was_running = {
2900 let mut state_guard = self.state.write().await;
2901 if let GatewayState::Running { lightning_context } = state_guard.clone() {
2902 *state_guard = GatewayState::ShuttingDown { lightning_context };
2903 true
2904 } else {
2905 false
2906 }
2907 };
2908
2909 if was_running {
2917 self.federation_manager
2918 .read()
2919 .await
2920 .wait_for_incoming_payments()
2921 .await?;
2922 }
2923
2924 let tg = task_group.clone();
2925 tg.spawn("Kill Gateway", |_task_handle| async {
2926 if let Err(err) = task_group.shutdown_join_all(Duration::from_mins(3)).await {
2927 warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Error shutting down gateway");
2928 }
2929 });
2930 Ok(())
2931 }
2932
2933 fn get_task_group(&self) -> TaskGroup {
2934 self.task_group.clone()
2935 }
2936
2937 async fn handle_withdraw_msg(&self, payload: WithdrawPayload) -> AdminResult<WithdrawResponse> {
2940 let WithdrawPayload {
2941 amount,
2942 address,
2943 federation_id,
2944 quoted_fees,
2945 } = payload;
2946
2947 let address_network = get_network_for_address(&address);
2948 let gateway_network = self.network;
2949 let Ok(address) = address.require_network(gateway_network) else {
2950 return Err(AdminGatewayError::WithdrawError {
2951 failure_reason: format!(
2952 "Gateway is running on network {gateway_network}, but provided withdraw address is for network {address_network}"
2953 ),
2954 });
2955 };
2956
2957 let client = self.select_client(federation_id).await?;
2958
2959 if let Ok(wallet_module) = client
2960 .value()
2961 .get_first_module::<fedimint_walletv2_client::WalletClientModule>()
2962 {
2963 return withdraw_v2(client.value(), &wallet_module, &address, amount).await;
2964 }
2965
2966 let wallet_module = client.value().get_first_module::<WalletClientModule>()?;
2967
2968 let (withdraw_amount, fees) = match quoted_fees {
2971 Some(fees) => {
2973 let amt = match amount {
2974 BitcoinAmountOrAll::Amount(a) => a,
2975 BitcoinAmountOrAll::All => {
2976 return Err(AdminGatewayError::WithdrawError {
2978 failure_reason:
2979 "Cannot use 'all' with quoted fees - amount must be resolved first"
2980 .to_string(),
2981 });
2982 }
2983 };
2984 (amt, fees)
2985 }
2986 None => match amount {
2988 BitcoinAmountOrAll::All => {
2993 let balance = client.value().get_balance_for_btc().await.map_err(|err| {
2994 AdminGatewayError::Unexpected(anyhow!(
2995 "Balance not available: {}",
2996 err.fmt_compact_anyhow()
2997 ))
2998 })?;
2999
3000 wallet_module
3001 .max_withdrawable_amount(&address, balance)
3002 .await
3003 .map_err(|err| AdminGatewayError::WithdrawError {
3004 failure_reason: format!(
3005 "Insufficient funds. Balance: {balance}: {}",
3006 err.fmt_compact_anyhow()
3007 ),
3008 })?
3009 }
3010 BitcoinAmountOrAll::Amount(amount) => (
3011 amount,
3012 wallet_module.get_withdraw_fees(&address, amount).await?,
3013 ),
3014 },
3015 };
3016
3017 let operation_id = wallet_module
3018 .withdraw(&address, withdraw_amount, fees, ())
3019 .await?;
3020 let mut updates = wallet_module
3021 .subscribe_withdraw_updates(operation_id)
3022 .await?
3023 .into_stream();
3024
3025 while let Some(update) = updates.next().await {
3026 match update {
3027 WithdrawState::Succeeded(txid) => {
3028 info!(target: LOG_GATEWAY, amount = %withdraw_amount, address = %address, "Sent funds");
3029 return Ok(WithdrawResponse { txid, fees });
3030 }
3031 WithdrawState::Failed(e) => {
3032 return Err(AdminGatewayError::WithdrawError { failure_reason: e });
3033 }
3034 WithdrawState::Created => {}
3035 }
3036 }
3037
3038 Err(AdminGatewayError::WithdrawError {
3039 failure_reason: "Ran out of state updates while withdrawing".to_string(),
3040 })
3041 }
3042
3043 async fn handle_withdraw_preview_msg(
3046 &self,
3047 payload: WithdrawPreviewPayload,
3048 ) -> AdminResult<WithdrawPreviewResponse> {
3049 let gateway_network = self.network;
3050 let address_checked = payload
3051 .address
3052 .clone()
3053 .require_network(gateway_network)
3054 .map_err(|_| AdminGatewayError::WithdrawError {
3055 failure_reason: "Address network mismatch".to_string(),
3056 })?;
3057
3058 let client = self.select_client(payload.federation_id).await?;
3059
3060 let WithdrawDetails {
3061 amount,
3062 mint_fees,
3063 peg_out_fees,
3064 } = match payload.amount {
3065 BitcoinAmountOrAll::All => {
3066 calculate_max_withdrawable(client.value(), &address_checked).await?
3067 }
3068 BitcoinAmountOrAll::Amount(btc_amount) => {
3069 if let Ok(wallet_module) = client.value().get_first_module::<WalletClientModule>() {
3070 WithdrawDetails {
3071 amount: btc_amount.into(),
3072 mint_fees: None,
3073 peg_out_fees: wallet_module
3074 .get_withdraw_fees(&address_checked, btc_amount)
3075 .await?,
3076 }
3077 } else if let Ok(wallet_module) = client
3078 .value()
3079 .get_first_module::<fedimint_walletv2_client::WalletClientModule>(
3080 ) {
3081 let fee = wallet_module.send_fee().await.map_err(|e| {
3082 AdminGatewayError::WithdrawError {
3083 failure_reason: e.to_string(),
3084 }
3085 })?;
3086 WithdrawDetails {
3087 amount: btc_amount.into(),
3088 mint_fees: None,
3089 peg_out_fees: PegOutFees::from_amount(fee),
3090 }
3091 } else {
3092 return Err(AdminGatewayError::Unexpected(anyhow!(
3093 "No wallet module found"
3094 )));
3095 }
3096 }
3097 };
3098
3099 let total_cost = amount
3100 .checked_add(peg_out_fees.amount().into())
3101 .and_then(|a| a.checked_add(mint_fees.unwrap_or(Amount::ZERO)))
3102 .ok_or_else(|| AdminGatewayError::Unexpected(anyhow!("Total cost overflow")))?;
3103
3104 Ok(WithdrawPreviewResponse {
3105 withdraw_amount: amount,
3106 address: payload.address.assume_checked().to_string(),
3107 peg_out_fees,
3108 total_cost,
3109 mint_fees,
3110 })
3111 }
3112
3113 async fn handle_payment_log_msg(
3125 &self,
3126 PaymentLogPayload {
3127 end_position,
3128 pagination_size,
3129 federation_id,
3130 event_kinds,
3131 }: PaymentLogPayload,
3132 ) -> AdminResult<PaymentLogResponse> {
3133 const BATCH_SIZE: u64 = 10_000;
3134 let federation_manager = self.federation_manager.read().await;
3135 let client = federation_manager
3136 .client(&federation_id)
3137 .ok_or(FederationNotConnected {
3138 federation_id_prefix: federation_id.to_prefix(),
3139 })?
3140 .value();
3141
3142 let event_kinds = if event_kinds.is_empty() {
3146 ALL_GATEWAY_EVENTS.to_vec()
3147 } else {
3148 event_kinds
3149 };
3150
3151 let end_position = if let Some(position) = end_position {
3152 position
3153 } else {
3154 let mut dbtx = client.db().begin_transaction_nc().await;
3155 dbtx.get_next_event_log_id().await
3156 };
3157
3158 let mut start_position = end_position.saturating_sub(BATCH_SIZE);
3159
3160 let mut payment_log = Vec::new();
3161
3162 while payment_log.len() < pagination_size {
3163 let batch = client.get_event_log(Some(start_position), BATCH_SIZE).await;
3164 let mut filtered_batch = batch
3165 .into_iter()
3166 .filter(|e| e.id() <= end_position && event_kinds.contains(&e.as_raw().kind))
3167 .collect::<Vec<_>>();
3168 filtered_batch.reverse();
3169 payment_log.extend(filtered_batch);
3170
3171 start_position = start_position.saturating_sub(BATCH_SIZE);
3173
3174 if start_position == EventLogId::LOG_START {
3175 break;
3176 }
3177 }
3178
3179 payment_log.truncate(pagination_size);
3181
3182 Ok(PaymentLogResponse(payment_log))
3183 }
3184
3185 async fn handle_set_mnemonic_msg(&self, payload: SetMnemonicPayload) -> AdminResult<()> {
3188 let mut state_guard = self.state.write().await;
3193
3194 let GatewayState::NotConfigured { mnemonic_sender } = state_guard.clone() else {
3196 return Err(AdminGatewayError::MnemonicError(anyhow!(
3197 "Gateway is not is NotConfigured state"
3198 )));
3199 };
3200
3201 let mnemonic = if let Some(words) = payload.words {
3202 info!(target: LOG_GATEWAY, "Using user provided mnemonic");
3203 Mnemonic::parse_in_normalized(Language::English, words.as_str()).map_err(|e| {
3204 AdminGatewayError::MnemonicError(anyhow!(format!(
3205 "Seed phrase provided in environment was invalid {e:?}"
3206 )))
3207 })?
3208 } else {
3209 debug!(target: LOG_GATEWAY, "Generating mnemonic and writing entropy to client storage");
3210 Bip39RootSecretStrategy::<12>::random(&mut OsRng)
3211 };
3212
3213 Client::store_encodable_client_secret(&self.gateway_db, mnemonic.to_entropy())
3214 .await
3215 .map_err(AdminGatewayError::MnemonicError)?;
3216
3217 *state_guard = GatewayState::Disconnected;
3218 drop(state_guard);
3219
3220 let _ = mnemonic_sender.send(());
3222
3223 Ok(())
3224 }
3225
3226 async fn handle_create_offer_for_operator_msg(
3228 &self,
3229 payload: CreateOfferPayload,
3230 ) -> AdminResult<CreateOfferResponse> {
3231 let lightning_context = self.get_lightning_context().await?;
3232 let offer = lightning_context.lnrpc.create_offer(
3233 payload.amount,
3234 payload.description,
3235 payload.expiry_secs,
3236 payload.quantity,
3237 )?;
3238 Ok(CreateOfferResponse { offer })
3239 }
3240
3241 async fn handle_pay_offer_for_operator_msg(
3243 &self,
3244 payload: PayOfferPayload,
3245 ) -> AdminResult<PayOfferResponse> {
3246 let lightning_context = self.get_lightning_context().await?;
3247 let preimage = lightning_context
3248 .lnrpc
3249 .pay_offer(
3250 payload.offer,
3251 payload.quantity,
3252 payload.amount,
3253 payload.payer_note,
3254 )
3255 .await?;
3256 Ok(PayOfferResponse {
3257 preimage: preimage.to_string(),
3258 })
3259 }
3260
3261 async fn handle_export_invite_codes(
3264 &self,
3265 ) -> BTreeMap<FederationId, BTreeMap<PeerId, (String, InviteCode)>> {
3266 let fed_manager = self.federation_manager.read().await;
3267 fed_manager.all_invite_codes().await
3268 }
3269
3270 async fn handle_get_note_summary_msg(
3273 &self,
3274 federation_id: &FederationId,
3275 ) -> AdminResult<TieredCounts> {
3276 let fed_manager = self.federation_manager.read().await;
3277 fed_manager.get_note_summary(federation_id).await
3278 }
3279
3280 fn get_password_hash(&self) -> String {
3281 self.bcrypt_password_hash.clone()
3282 }
3283
3284 fn gatewayd_version(&self) -> String {
3285 let gatewayd_version = env!("CARGO_PKG_VERSION");
3286 gatewayd_version.to_string()
3287 }
3288
3289 async fn get_chain_source(&self) -> (ChainSource, Network) {
3290 (self.chain_source.clone(), self.network)
3291 }
3292
3293 fn lightning_mode(&self) -> LightningMode {
3294 self.lightning_mode.clone()
3295 }
3296
3297 async fn is_configured(&self) -> bool {
3298 !matches!(self.get_state().await, GatewayState::NotConfigured { .. })
3299 }
3300}
3301
3302impl Gateway {
3304 async fn public_key_v2(&self, federation_id: &FederationId) -> Option<PublicKey> {
3308 self.federation_manager
3309 .read()
3310 .await
3311 .client(federation_id)
3312 .and_then(|client| {
3313 client
3316 .value()
3317 .get_first_module::<GatewayClientModuleV2>()
3318 .ok()
3319 .map(|module| module.keypair.public_key())
3320 })
3321 }
3322
3323 pub async fn routing_info_v2(
3326 &self,
3327 federation_id: &FederationId,
3328 ) -> Result<Option<RoutingInfo>> {
3329 let context = self.get_lightning_context().await?;
3330
3331 let mut dbtx = self.gateway_db.begin_transaction_nc().await;
3332 let fed_config = dbtx.load_federation_config(*federation_id).await.ok_or(
3333 PublicGatewayError::FederationNotConnected(FederationNotConnected {
3334 federation_id_prefix: federation_id.to_prefix(),
3335 }),
3336 )?;
3337
3338 let lightning_fee = fed_config.lightning_fee;
3339 let transaction_fee = fed_config.transaction_fee;
3340
3341 let send_fee_default = lightning_fee.checked_add(transaction_fee).ok_or_else(|| {
3344 PublicGatewayError::Unexpected(anyhow!(
3345 "The configured fees of federation {federation_id} cannot be added"
3346 ))
3347 })?;
3348
3349 Ok(self
3350 .public_key_v2(federation_id)
3351 .await
3352 .map(|module_public_key| RoutingInfo {
3353 lightning_public_key: context.lightning_public_key,
3354 lightning_alias: Some(context.lightning_alias.clone()),
3355 module_public_key,
3356 send_fee_default,
3357 send_fee_minimum: transaction_fee,
3361 expiration_delta_default: 1440,
3362 expiration_delta_minimum: EXPIRATION_DELTA_MINIMUM_V2,
3363 receive_fee: transaction_fee,
3366 }))
3367 }
3368
3369 pub async fn send_payment_v2(
3372 &self,
3373 payload: SendPaymentPayload,
3374 ) -> Result<std::result::Result<[u8; 32], Signature>> {
3375 let client = self.select_client(payload.federation_id).await?;
3376 let module = client
3379 .value()
3380 .get_first_module::<GatewayClientModuleV2>()
3381 .map_err(|err| PublicGatewayError::LNv2(LNv2Error::OutgoingPayment(err)))?;
3382
3383 module
3384 .send_payment(payload)
3385 .await
3386 .map_err(LNv2Error::OutgoingPayment)
3387 .map_err(PublicGatewayError::LNv2)
3388 }
3389
3390 async fn create_bolt11_invoice_v2(
3395 &self,
3396 payload: CreateBolt11InvoicePayload,
3397 ) -> Result<Bolt11Invoice> {
3398 if !self.invoice_rate_limiter.try_acquire() {
3402 return Err(PublicGatewayError::RateLimited);
3403 }
3404
3405 if !payload.contract.verify() {
3406 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
3407 "The contract is invalid".to_string(),
3408 )));
3409 }
3410
3411 let payment_info = self.routing_info_v2(&payload.federation_id).await?.ok_or(
3412 LNv2Error::IncomingPayment(format!(
3413 "Federation {} does not exist",
3414 payload.federation_id
3415 )),
3416 )?;
3417
3418 if payload.contract.commitment.refund_pk != payment_info.module_public_key {
3419 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
3420 "The incoming contract is keyed to another gateway".to_string(),
3421 )));
3422 }
3423
3424 let contract_amount = payment_info.receive_fee.subtract_from(payload.amount.msats);
3425
3426 if contract_amount == Amount::ZERO {
3427 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
3428 "Zero amount incoming contracts are not supported".to_string(),
3429 )));
3430 }
3431
3432 if contract_amount != payload.contract.commitment.amount {
3433 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
3434 "The contract amount does not pay the correct amount of fees".to_string(),
3435 )));
3436 }
3437
3438 if payload.contract.commitment.expiration_or_fee <= duration_since_epoch().as_secs() {
3439 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
3440 "The contract has already expired".to_string(),
3441 )));
3442 }
3443
3444 if payload.expiry_secs > MAX_INVOICE_EXPIRY_SECS {
3445 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
3446 "The invoice expiry exceeds the maximum of one day".to_string(),
3447 )));
3448 }
3449
3450 let payment_hash = match payload.contract.commitment.payment_image {
3451 PaymentImage::Hash(payment_hash) => payment_hash,
3452 PaymentImage::Point(..) => {
3453 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
3454 "PaymentImage is not a payment hash".to_string(),
3455 )));
3456 }
3457 };
3458
3459 let mut dbtx = self.gateway_db.begin_transaction().await;
3464
3465 let invoice_expires_at_secs = duration_since_epoch()
3466 .as_secs()
3467 .saturating_add(u64::from(payload.expiry_secs));
3468
3469 if dbtx
3470 .save_registered_incoming_contract(
3471 payload.federation_id,
3472 payload.amount,
3473 invoice_expires_at_secs,
3474 payload.contract,
3475 )
3476 .await
3477 .is_some()
3478 {
3479 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
3480 "PaymentHash is already registered".to_string(),
3481 )));
3482 }
3483
3484 dbtx.commit_tx_result().await.map_err(|_| {
3485 PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
3486 "Payment hash is already registered".to_string(),
3487 ))
3488 })?;
3489
3490 match self
3491 .create_invoice_via_lnrpc_v2(
3492 payment_hash,
3493 payload.amount,
3494 payload.description.clone(),
3495 payload.expiry_secs,
3496 )
3497 .await
3498 {
3499 Ok(invoice) => Ok(invoice),
3500 Err(err) => {
3501 let mut dbtx = self.gateway_db.begin_transaction().await;
3504 dbtx.delete_registered_incoming_contract(PaymentImage::Hash(payment_hash))
3505 .await;
3506 if let Err(db_err) = dbtx.commit_tx_result().await {
3507 warn!(
3508 target: LOG_GATEWAY,
3509 err = %db_err.fmt_compact(),
3510 %payment_hash,
3511 "Failed to release incoming contract reservation after Lightning error"
3512 );
3513 }
3514
3515 Err(err.into())
3516 }
3517 }
3518 }
3519
3520 pub async fn create_invoice_via_lnrpc_v2(
3523 &self,
3524 payment_hash: sha256::Hash,
3525 amount: Amount,
3526 description: Bolt11InvoiceDescription,
3527 expiry_time: u32,
3528 ) -> std::result::Result<Bolt11Invoice, LightningRpcError> {
3529 let lnrpc = self.get_lightning_context().await?.lnrpc;
3530
3531 let response = match description {
3532 Bolt11InvoiceDescription::Direct(description) => {
3533 lnrpc
3534 .create_invoice(CreateInvoiceRequest {
3535 payment_hash: Some(payment_hash),
3536 amount_msat: amount.msats,
3537 expiry_secs: expiry_time,
3538 description: Some(InvoiceDescription::Direct(description)),
3539 })
3540 .await?
3541 }
3542 Bolt11InvoiceDescription::Hash(hash) => {
3543 lnrpc
3544 .create_invoice(CreateInvoiceRequest {
3545 payment_hash: Some(payment_hash),
3546 amount_msat: amount.msats,
3547 expiry_secs: expiry_time,
3548 description: Some(InvoiceDescription::Hash(hash)),
3549 })
3550 .await?
3551 }
3552 };
3553
3554 Bolt11Invoice::from_str(&response.invoice).map_err(|e| {
3555 LightningRpcError::FailedToGetInvoice {
3556 failure_reason: e.to_string(),
3557 }
3558 })
3559 }
3560
3561 pub async fn verify_bolt11_preimage_v2(
3562 &self,
3563 payment_hash: sha256::Hash,
3564 wait: bool,
3565 ) -> std::result::Result<VerifyResponse, String> {
3566 let registered_contract = self
3567 .gateway_db
3568 .begin_transaction_nc()
3569 .await
3570 .load_registered_incoming_contract(PaymentImage::Hash(payment_hash))
3571 .await
3572 .ok_or("Unknown payment hash".to_string())?;
3573
3574 let client = self
3575 .select_client(registered_contract.federation_id)
3576 .await
3577 .map_err(|_| "Not connected to federation".to_string())?
3578 .into_value();
3579
3580 let operation_id = OperationId::from_encodable(®istered_contract.contract);
3581
3582 if !(wait || client.operation_exists(operation_id).await) {
3583 return Ok(VerifyResponse {
3584 settled: false,
3585 preimage: None,
3586 });
3587 }
3588
3589 let module = client
3590 .get_first_module::<GatewayClientModuleV2>()
3591 .expect("Must have client module");
3592
3593 let Ok(state) = timeout(VERIFY_WAIT_TIMEOUT, module.await_receive(operation_id)).await
3594 else {
3595 return Ok(VerifyResponse {
3596 settled: false,
3597 preimage: None,
3598 });
3599 };
3600
3601 let preimage = match state {
3602 FinalReceiveState::Success(preimage) => Ok(preimage),
3603 FinalReceiveState::Failure => Err("Payment has failed".to_string()),
3604 FinalReceiveState::Refunded => Err("Payment has been refunded".to_string()),
3605 FinalReceiveState::Rejected => Err("Payment has been rejected".to_string()),
3606 }?;
3607
3608 Ok(VerifyResponse {
3609 settled: true,
3610 preimage: Some(preimage),
3611 })
3612 }
3613
3614 pub async fn get_registered_incoming_contract_and_client_v2(
3618 &self,
3619 payment_image: PaymentImage,
3620 amount_msats: u64,
3621 ) -> Result<(IncomingContract, ClientHandleArc)> {
3622 let registered_incoming_contract = self
3623 .gateway_db
3624 .begin_transaction_nc()
3625 .await
3626 .load_registered_incoming_contract(payment_image)
3627 .await
3628 .ok_or(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
3629 "No corresponding decryption contract available".to_string(),
3630 )))?;
3631
3632 if registered_incoming_contract.incoming_amount_msats != amount_msats {
3633 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
3634 "The available decryption contract's amount is not equal to the requested amount"
3635 .to_string(),
3636 )));
3637 }
3638
3639 let client = self
3640 .select_client(registered_incoming_contract.federation_id)
3641 .await?
3642 .into_value();
3643
3644 Ok((registered_incoming_contract.contract, client))
3645 }
3646}
3647
3648#[async_trait]
3649impl IGatewayClientV2 for Gateway {
3650 async fn complete_htlc(
3651 &self,
3652 htlc_response: InterceptPaymentResponse,
3653 ) -> std::result::Result<(), LightningRpcError> {
3654 loop {
3655 let lightning_context = self.await_lightning_context().await;
3656
3657 match lightning_context
3658 .lnrpc
3659 .complete_htlc(htlc_response.clone())
3660 .await
3661 {
3662 Ok(..) => return Ok(()),
3663 Err(err @ LightningRpcError::HtlcCompletionRejected { .. }) => {
3664 warn!(
3665 target: LOG_GATEWAY,
3666 err = %err.fmt_compact(),
3667 "Lightning cannot reach the requested terminal HTLC outcome",
3668 );
3669 return Err(err);
3670 }
3671 Err(err) => {
3672 warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Failure trying to complete payment");
3673 }
3674 }
3675
3676 sleep(LIGHTNING_CONTEXT_RETRY_INTERVAL).await;
3677 }
3678 }
3679
3680 async fn is_direct_swap(
3681 &self,
3682 invoice: &Bolt11Invoice,
3683 ) -> anyhow::Result<Option<(IncomingContract, ClientHandleArc)>> {
3684 let lightning_context = self.await_lightning_context().await;
3689 if lightning_context.lightning_public_key == invoice.get_payee_pub_key() {
3690 let (contract, client) = self
3691 .get_registered_incoming_contract_and_client_v2(
3692 PaymentImage::Hash(*invoice.payment_hash()),
3693 invoice
3694 .amount_milli_satoshis()
3695 .expect("The amount invoice has been previously checked"),
3696 )
3697 .await?;
3698 Ok(Some((contract, client)))
3699 } else {
3700 Ok(None)
3701 }
3702 }
3703
3704 async fn pay(
3705 &self,
3706 invoice: Bolt11Invoice,
3707 max_delay: u64,
3708 max_fee: Amount,
3709 ) -> std::result::Result<[u8; 32], LightningRpcError> {
3710 let lightning_context = self.await_lightning_context().await;
3713 lightning_context
3714 .lnrpc
3715 .pay(invoice, max_delay, max_fee)
3716 .await
3717 .map(|response| response.preimage.0)
3718 }
3719
3720 async fn min_contract_amount(
3721 &self,
3722 federation_id: &FederationId,
3723 amount: u64,
3724 ) -> anyhow::Result<Amount> {
3725 Ok(self
3726 .routing_info_v2(federation_id)
3727 .await?
3728 .ok_or(anyhow!("Routing Info not available"))?
3729 .send_fee_minimum
3730 .add_to(amount))
3731 }
3732
3733 async fn is_lnv1_invoice(&self, invoice: &Bolt11Invoice) -> Option<Spanned<ClientHandleArc>> {
3734 let rhints = invoice.route_hints();
3735 let hop = rhints.first().and_then(|rh| rh.0.last())?;
3736
3737 let lightning_context = self.await_lightning_context().await;
3740 if hop.src_node_id != lightning_context.lightning_public_key {
3741 return None;
3742 }
3743
3744 self.federation_manager
3745 .read()
3746 .await
3747 .get_client_for_index(hop.short_channel_id)
3748 }
3749
3750 async fn relay_lnv1_swap(
3751 &self,
3752 client: &ClientHandleArc,
3753 invoice: &Bolt11Invoice,
3754 ) -> anyhow::Result<FinalReceiveState> {
3755 let swap_params = SwapParameters {
3756 payment_hash: *invoice.payment_hash(),
3757 amount_msat: Amount::from_msats(
3758 invoice
3759 .amount_milli_satoshis()
3760 .ok_or(anyhow!("Amountless invoice not supported"))?,
3761 ),
3762 };
3763 let lnv1 = client
3764 .get_first_module::<GatewayClientModule>()
3765 .expect("No LNv1 module");
3766 let operation_id = lnv1.gateway_handle_direct_swap(swap_params).await?;
3767 let mut stream = lnv1
3768 .gateway_subscribe_ln_receive(operation_id)
3769 .await?
3770 .into_stream();
3771 let mut final_state = FinalReceiveState::Failure;
3772 while let Some(update) = stream.next().await {
3773 match update {
3774 GatewayExtReceiveStates::Funding => {}
3775 GatewayExtReceiveStates::FundingFailed { error: _ } => {
3776 final_state = FinalReceiveState::Rejected;
3777 }
3778 GatewayExtReceiveStates::Preimage(preimage) => {
3779 final_state = FinalReceiveState::Success(preimage.0);
3780 }
3781 GatewayExtReceiveStates::RefundError {
3782 error_message: _,
3783 error: _,
3784 } => {
3785 final_state = FinalReceiveState::Failure;
3786 }
3787 GatewayExtReceiveStates::RefundSuccess {
3788 out_points: _,
3789 error: _,
3790 } => {
3791 final_state = FinalReceiveState::Refunded;
3792 }
3793 }
3794 }
3795
3796 Ok(final_state)
3797 }
3798
3799 async fn claim_payment_image(
3800 &self,
3801 payment_image: &PaymentImage,
3802 operation_id: OperationId,
3803 ) -> bool {
3804 self.gateway_db
3808 .autocommit(
3809 |dbtx, _| {
3810 let payment_image = payment_image.clone();
3811 Box::pin(async move {
3812 let claimer = dbtx
3813 .claim_outgoing_payment_image(payment_image, operation_id)
3814 .await;
3815 Ok::<_, std::convert::Infallible>(claimer == operation_id)
3816 })
3817 },
3818 None,
3819 )
3820 .await
3821 .expect("Retries until the transaction commits")
3822 }
3823}
3824
3825#[async_trait]
3826impl IGatewayClientV1 for Gateway {
3827 async fn verify_preimage_authentication(
3828 &self,
3829 payment_hash: sha256::Hash,
3830 preimage_auth: sha256::Hash,
3831 contract: OutgoingContractAccount,
3832 ) -> std::result::Result<(), OutgoingPaymentError> {
3833 let mut dbtx = self.gateway_db.begin_transaction().await;
3834 if let Some(secret_hash) = dbtx.load_preimage_authentication(payment_hash).await {
3835 if secret_hash != preimage_auth {
3836 return Err(OutgoingPaymentError {
3837 error_type: OutgoingPaymentErrorType::InvalidInvoicePreimage,
3838 contract_id: contract.contract.contract_id(),
3839 contract: Some(contract),
3840 });
3841 }
3842 } else {
3843 dbtx.save_new_preimage_authentication(payment_hash, preimage_auth)
3846 .await;
3847 return dbtx
3848 .commit_tx_result()
3849 .await
3850 .map_err(|_| OutgoingPaymentError {
3851 error_type: OutgoingPaymentErrorType::InvoiceAlreadyPaid,
3852 contract_id: contract.contract.contract_id(),
3853 contract: Some(contract),
3854 });
3855 }
3856
3857 Ok(())
3858 }
3859
3860 async fn verify_pruned_invoice(&self, payment_data: PaymentData) -> anyhow::Result<()> {
3861 if matches!(payment_data, PaymentData::PrunedInvoice { .. }) {
3862 let lightning_context = self.get_lightning_context().await?;
3863
3864 ensure!(
3865 lightning_context.lnrpc.supports_private_payments(),
3866 "Private payments are not supported by the lightning node"
3867 );
3868 }
3869
3870 Ok(())
3871 }
3872
3873 async fn get_routing_fees(&self, federation_id: FederationId) -> Option<RoutingFees> {
3874 let mut gateway_dbtx = self.gateway_db.begin_transaction_nc().await;
3875 let lightning_fee = gateway_dbtx
3876 .load_federation_config(federation_id)
3877 .await?
3878 .lightning_fee;
3879
3880 RoutingFees::try_from(lightning_fee)
3884 .inspect_err(|err| {
3885 warn!(
3886 target: LOG_GATEWAY,
3887 %federation_id,
3888 err = %err.fmt_compact(),
3889 "Configured lightning fee cannot be used. Set a smaller fee with `set_fees`."
3890 );
3891 })
3892 .ok()
3893 }
3894
3895 async fn get_client(&self, federation_id: &FederationId) -> Option<Spanned<ClientHandleArc>> {
3896 self.federation_manager
3897 .read()
3898 .await
3899 .client(federation_id)
3900 .cloned()
3901 }
3902
3903 async fn get_client_for_invoice(
3904 &self,
3905 payment_data: PaymentData,
3906 ) -> Option<Spanned<ClientHandleArc>> {
3907 let rhints = payment_data.route_hints();
3908 let hop = rhints.first().and_then(|rh| rh.0.last())?;
3909
3910 let lightning_context = self.await_lightning_context().await;
3913 if hop.src_node_id != lightning_context.lightning_public_key {
3914 return None;
3915 }
3916
3917 self.federation_manager
3918 .read()
3919 .await
3920 .get_client_for_index(hop.short_channel_id)
3921 }
3922
3923 async fn pay(
3924 &self,
3925 payment_data: PaymentData,
3926 max_delay: u64,
3927 max_fee: Amount,
3928 ) -> std::result::Result<PayInvoiceResponse, LightningRpcError> {
3929 let lightning_context = self.await_lightning_context().await;
3932
3933 match payment_data {
3934 PaymentData::Invoice(invoice) => {
3935 lightning_context
3936 .lnrpc
3937 .pay(invoice, max_delay, max_fee)
3938 .await
3939 }
3940 PaymentData::PrunedInvoice(invoice) => {
3941 lightning_context
3942 .lnrpc
3943 .pay_private(invoice, max_delay, max_fee)
3944 .await
3945 }
3946 }
3947 }
3948
3949 async fn complete_htlc(
3950 &self,
3951 htlc: InterceptPaymentResponse,
3952 ) -> std::result::Result<(), LightningRpcError> {
3953 let lightning_context = self.await_lightning_context().await;
3955
3956 lightning_context.lnrpc.complete_htlc(htlc).await
3957 }
3958
3959 async fn is_lnv2_direct_swap(
3960 &self,
3961 payment_hash: sha256::Hash,
3962 amount: Amount,
3963 ) -> anyhow::Result<
3964 Option<(
3965 fedimint_lnv2_common::contracts::IncomingContract,
3966 ClientHandleArc,
3967 )>,
3968 > {
3969 let (contract, client) = self
3970 .get_registered_incoming_contract_and_client_v2(
3971 PaymentImage::Hash(payment_hash),
3972 amount.msats,
3973 )
3974 .await?;
3975 Ok(Some((contract, client)))
3976 }
3977}