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